[IMP] website_sale_aplicoop: skip the product-card tags row when unused

The tags row was reserved (empty, aria-hidden) on every product card so
prices stay aligned across a grid row regardless of which cards have tags.
But when no product in the current batch has a published tag, that row was
still a fixed, permanent gap on every card for a feature nobody was using.

any_product_has_tags is now computed once per batch (controller-side, per
CLAUDE.md's no-logic-in-QWeb rule) from the already-filtered published_tags,
and the template skips the row entirely when it's False across all three
render paths (initial page, load-more, infinite-scroll AJAX).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
GitHub Copilot 2026-08-16 12:50:32 +02:00
parent f3e39550c2
commit a67181ab42
2 changed files with 39 additions and 16 deletions

View file

@ -847,6 +847,12 @@ class AplicoopWebsiteSale(WebsiteSale):
product_display_info, product_display_info,
filtered_products_dict, filtered_products_dict,
) = self._prepare_products_maps(products, pricelist) ) = self._prepare_products_maps(products, pricelist)
# Whether the tags row needs to be reserved on every card: if nothing in
# this batch has a published tag, the row is skipped entirely instead of
# leaving an empty aria-hidden gap on every product (see product-card.css).
any_product_has_tags = any(
v["published_tags"] for v in filtered_products_dict.values()
)
# Inject draft sale demand context so ribbons/is_out_of_stock use forecasted net qty # Inject draft sale demand context so ribbons/is_out_of_stock use forecasted net qty
products, product_max_qty = self._prepare_draft_stock_data(products) products, product_max_qty = self._prepare_draft_stock_data(products)
@ -876,6 +882,7 @@ class AplicoopWebsiteSale(WebsiteSale):
"group_order": group_order, "group_order": group_order,
"products": products, "products": products,
"filtered_product_tags": filtered_products_dict, "filtered_product_tags": filtered_products_dict,
"any_product_has_tags": any_product_has_tags,
"cart": cart, "cart": cart,
"available_categories": available_categories, "available_categories": available_categories,
"category_hierarchy": category_hierarchy, "category_hierarchy": category_hierarchy,
@ -986,6 +993,10 @@ class AplicoopWebsiteSale(WebsiteSale):
"product": product, "product": product,
"published_tags": published_tags, "published_tags": published_tags,
} }
# See eskaera_shop for why this decides whether to reserve the tags row.
any_product_has_tags = any(
v["published_tags"] for v in filtered_products_dict.values()
)
product_display_info = {} product_display_info = {}
for product in products_page: for product in products_page:
@ -1004,6 +1015,7 @@ class AplicoopWebsiteSale(WebsiteSale):
"group_order": group_order, "group_order": group_order,
"products": products_page, "products": products_page,
"filtered_product_tags": filtered_products_dict, "filtered_product_tags": filtered_products_dict,
"any_product_has_tags": any_product_has_tags,
"product_supplier_info": product_supplier_info, "product_supplier_info": product_supplier_info,
"product_price_info": product_price_info, "product_price_info": product_price_info,
"product_display_info": product_display_info, "product_display_info": product_display_info,
@ -1112,6 +1124,10 @@ class AplicoopWebsiteSale(WebsiteSale):
} }
for product in products_page for product in products_page
} }
# See eskaera_shop for why this decides whether to reserve the tags row.
any_product_has_tags = any(
v["published_tags"] for v in filtered_products_dict.values()
)
# Inject draft demand context for ribbons/stock flags # Inject draft demand context for ribbons/stock flags
products_page, product_max_qty = self._prepare_draft_stock_data(products_page) products_page, product_max_qty = self._prepare_draft_stock_data(products_page)
@ -1126,6 +1142,7 @@ class AplicoopWebsiteSale(WebsiteSale):
"group_order": group_order, "group_order": group_order,
"products": products_page, "products": products_page,
"filtered_product_tags": filtered_products_dict, "filtered_product_tags": filtered_products_dict,
"any_product_has_tags": any_product_has_tags,
"product_supplier_info": product_supplier_info, "product_supplier_info": product_supplier_info,
"product_price_info": product_price_info, "product_price_info": product_price_info,
"product_display_info": product_display_info, "product_display_info": product_display_info,

View file

@ -661,23 +661,29 @@
<!-- Tags/supplier/origin always occupy their slot (empty and <!-- Tags/supplier/origin always occupy their slot (empty and
aria-hidden when the product has no value) so that the price aria-hidden when the product has no value) so that the price
row below lines up across every card in the grid row, row below lines up across every card in the grid row,
regardless of which optional fields a given product has. --> regardless of which optional fields a given product has. The
<t t-if="product.product_tag_ids"> tags row itself is only reserved when at least one product in
<div class="product-tags"> this batch actually has a tag (any_product_has_tags, computed
<t t-foreach="filtered_product_tags.get(product.id, {}).get('published_tags', product.product_tag_ids)" t-as="tag"> controller-side) — otherwise it would add a permanent empty
<t t-if="tag.color"> gap to every card for a feature nobody in the list uses. -->
<!-- Per-record colour: passed as a custom property so the <t t-if="any_product_has_tags">
stylesheet keeps owning the badge, no !important needed. --> <t t-if="product.product_tag_ids">
<span class="badge badge-km" t-attf-style="--ac-tag-color: {{ tag.color }};" t-esc="tag.name" /> <div class="product-tags">
<t t-foreach="filtered_product_tags.get(product.id, {}).get('published_tags', product.product_tag_ids)" t-as="tag">
<t t-if="tag.color">
<!-- Per-record colour: passed as a custom property so the
stylesheet keeps owning the badge, no !important needed. -->
<span class="badge badge-km" t-attf-style="--ac-tag-color: {{ tag.color }};" t-esc="tag.name" />
</t>
<t t-else="">
<span class="badge badge-km tag-use-theme-color" t-esc="tag.name" />
</t>
</t> </t>
<t t-else=""> </div>
<span class="badge badge-km tag-use-theme-color" t-esc="tag.name" /> </t>
</t> <t t-else="">
</t> <div class="product-tags" aria-hidden="true" />
</div> </t>
</t>
<t t-else="">
<div class="product-tags" aria-hidden="true" />
</t> </t>
<t t-if="product_supplier_info.get(product.id)"> <t t-if="product_supplier_info.get(product.id)">
<p class="product-supplier"> <p class="product-supplier">