feat(cms-base-layer): add filter by category - #2574
feat(cms-base-layer): add filter by category#2574Maciek Kucmus (mkucmus) wants to merge 6 commits into
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
There was a problem hiding this comment.
Pull request overview
Adds first-class support in the frontends repo for category-based filtering on search result listings by requesting category aggregations and applying selected categories via a criteria post-filter. This extends @shopware/helpers (criteria builders + filter merging), updates @shopware/cms-base-layer to render a categories filter when aggregations are present, and wires the feature into the Nuxt template search pages and docs.
Changes:
- Add
@shopware/helpersutilities to request category aggregations and build a categorypost-filter, plusgetListingFilterssupport to merge category entities + counts. - Extend
@shopware/cms-base-layerlisting filters UI/state to support acategoriesURL param and render a newSwFilterCategoriescheckbox filter. - Update demo/starter template search pages and docs to demonstrate requesting category aggregations and filtering via
post-filter.
Reviewed changes
Copilot reviewed 21 out of 22 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| templates/vue-starter-template/app/pages/search.vue | Requests category aggregations and applies selected categories as post-filter from ?categories=. |
| templates/vue-demo-store/app/pages/search.vue | Adds category aggregations + category post-filter handling for the demo search page. |
| templates/vue-demo-store/app/components/listing-filters/ListingFilters.vue | Tracks selected categories in filter UI state and syncs them to/from route.query. |
| templates/vue-demo-store/app/components/listing-filters/ListingFilter.vue | Hides the navigation root category from the category filter options. |
| pnpm-lock.yaml | Locks additional tool dependencies (oxfmt/oxlint) for the workspace. |
| packages/helpers/src/listing/index.ts | Exposes new category listing helpers from the listing entrypoint. |
| packages/helpers/src/listing/filters.ts | Merges categories + categories-counts into a single categories listing filter with per-entity counts. |
| packages/helpers/src/listing/filters.test.ts | Adds unit coverage for categories filter merging and edge cases. |
| packages/helpers/src/listing/categoryFilter.ts | Introduces getCategoryFilterAggregations, getCategoryFilterPostFilter, and count resolution helper. |
| packages/helpers/src/listing/categoryFilter.test.ts | Adds unit coverage for the new category filter helper utilities. |
| packages/helpers/src/index.test.ts | Ensures new helpers are exported on the helpers public API. |
| packages/cms-base-layer/app/utils/useSelectedListingFilters.ts | Adds categories into shared listing filter state syncing from URL query. |
| packages/cms-base-layer/app/utils/useSelectedListingFilters.test.ts | Extends tests to cover category query parsing and reset behavior. |
| packages/cms-base-layer/app/components/SwProductListingFiltersHorizontal.vue | Enables category selection + URL syncing for horizontal filter UI (search listing only). |
| packages/cms-base-layer/app/components/SwProductListingFilters.vue | Enables category selection + URL syncing for sidebar filter UI (search listing only). |
| packages/cms-base-layer/app/components/SwProductListingFilter.vue | Wires categories to a dedicated SwFilterCategories component. |
| packages/cms-base-layer/app/components/SwFilterChips.vue | Adds chips for selected categories when the categories filter is available. |
| packages/cms-base-layer/app/components/listing-filters/SwFilterCategories.vue | New categories checkbox filter UI component (hides navigation root category). |
| examples/sanity-cms/package.json | Adds oxlint/oxfmt dev dependencies to the example. |
| apps/docs/src/getting-started/e-commerce/product-listing.md | Documents how to request category aggregations and filter via post-filter. |
| .changeset/curly-donuts-search.md | Changeset for publishing @shopware/helpers additions. |
| .changeset/curly-donuts-search-ui.md | Changeset for publishing @shopware/cms-base-layer UI/state changes. |
Files not reviewed (1)
- pnpm-lock.yaml: Generated file
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Patryk Tomczyk (patzick)
left a comment
There was a problem hiding this comment.
🔍 Code Review
Verdict: request changes — 2 majors. CI is green (all 11 checks pass), no conflicts, @beta helpers are well tested.
🚫 Majors
1. SwFilterChips crashes when filters.categories is absent — packages/cms-base-layer/app/components/SwFilterChips.vue:9,92
FilterState.categories is a new required field and Array.from(props.filters.categories) runs unguarded. Any consumer of the published component that builds its own filter-state object (not via useSelectedListingFilters) now gets TypeError: undefined is not iterable and a dead chips row — on a minor bump. Fix: categories?: Set<string> + Array.from(props.filters.categories ?? []).
2. Category filter renders on non-search listings but does nothing — SwProductListingFilters.vue:66,88,159 and SwProductListingFiltersHorizontal.vue:65,87,158
All three write paths are gated on isProductSearch (post-filter, ?categories= param, reset button), but the render loop at SwProductListingFilters.vue:284 is not. On a category listing whose criteria include the category aggregations, checking a box mutates the set, executeSearch pushes a query without categories, applyQueryToFilters clears the set again → the checkbox flips back and the result set never changes. Either gate the render (skip code === "categories" when !isProductSearch) or drop the gates and let the post-filter apply everywhere.
⚠️ Minors
getListingFilterssilently changes shape for existingcategoriesconsumers — entities are reordered by count,countis injected,apiAliasis dropped, and acategories-countsaggregation is now omitted from the filter list (packages/helpers/src/listing/filters.ts:61,114). It is@beta, but the changeset should say so.- Root-category hiding is duplicated in
SwFilterCategories.vue:60andtemplates/vue-demo-store/.../ListingFilter.vue:57. Better as an opt-in helper next togetCategoryFilterAggregations. examples/sanity-cms/package.json+ lockfile addoxfmt/oxlint. It fixes a genuinely undeclared dep for that example'slintscript, but it is unrelated to this feature and unmentioned in the description — split it out or note it.templates/vue-demo-store/app/pages/search.vue:157:p: route.query.p ? Number(route.query.p) : 1skips thefirstQueryValue/toNumberpair used two lines above;?p=1&p=2sendsp: NaN.CATEGORY_PARENTS_AGGREGATION_NAMEis not re-exported frompackages/helpers/src/listing/index.ts, so consumers following the new doc section must hardcode"categories-parents".- No component-level test for
SwFilterCategoriesor for theisProductSearchgating — behavioral logic landed in three components with helper-level tests only.
💡 Nits
-
SwFilterCategories.vue:139::key="${option.id}-${isChecked(option.id)}"tears down and recreates each label on every toggle;:key="option.id"suffices sinceSwCheckboxis reactively bound. -
SwFilterChips.vue:93:availableFilters.find(...)is re-run per selected category inside the loop; hoist it (same pre-existing pattern for manufacturer). -
vue-starter-templatehas nolisting.categoriesi18n entry, so the heading reads "Categories" in de/pl. Consistent with the existing gap forlisting.filtersthere, so only worth knowing.
✅ Good
- Post-filter choice for faceted behavior is correct and well documented, including the ES nested-aggregation caveat.
resolveCategoryBucketCountvariant collapsing and thegetListingFiltersmerge are covered by focused unit tests, including the missing-counts and counts-without-entities paths.- Removing the
search()/changeCurrentPage()double-fetch in favor of theuseAsyncDatawatch: [() => route.query]refetch is a real fix; the demo-store'slimitstill round-trips viav-model+ query.
🔁 Inherited feedback
Both Copilot inline comments (string[] query params in search.vue and ListingFilters.vue) are already addressed — the current head uses firstQueryValue(...) in search.vue:148 and ListingFilters.vue:65.
🧪 Validation
Not run locally by request — verdict is based on CI: Build, Test, lint, Check audit, CodeQL and all Vercel deployments pass on fb3205c.
Description
closes: #2560
one note: there is a hidden count number next to a category's name due to issue that is already addressed in the backend. I will make sure the fix is provided afterwards.