@shopware/cms-base-layer 3.0.1 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/README.md +78 -15
  2. package/app/app.config.ts +3 -3
  3. package/app/assets/css/rich-text.css +80 -0
  4. package/app/components/SwContactForm.vue +2 -1
  5. package/app/components/SwFilterChips.vue +29 -7
  6. package/app/components/SwNewsletterForm.vue +2 -1
  7. package/app/components/SwProductCard.vue +19 -3
  8. package/app/components/SwProductCardImage.vue +5 -1
  9. package/app/components/SwProductListingFilter.vue +5 -0
  10. package/app/components/SwProductListingFilters.vue +76 -74
  11. package/app/components/SwProductListingFiltersHorizontal.vue +73 -72
  12. package/app/components/SwStockInfo.vue +5 -2
  13. package/app/components/SwVariantConfigurator.vue +8 -3
  14. package/app/components/listing-filters/SwFilterCategories.vue +158 -0
  15. package/app/components/public/cms/CmsGenericBlock.vue +1 -1
  16. package/app/components/public/cms/CmsGenericElement.vue +1 -1
  17. package/app/components/public/cms/CmsNoComponent.vue +1 -1
  18. package/app/components/public/cms/FrontendAccountCustomerGroupRegistrationPage.vue +1 -1
  19. package/app/components/public/cms/block/CmsBlockCenterText.vue +2 -1
  20. package/app/components/public/cms/block/CmsBlockImageBubbleRow.vue +5 -1
  21. package/app/components/public/cms/block/CmsBlockImageFourColumn.vue +6 -2
  22. package/app/components/public/cms/block/CmsBlockImageGalleryBig.vue +9 -5
  23. package/app/components/public/cms/block/CmsBlockImageHighlightRow.vue +7 -2
  24. package/app/components/public/cms/block/CmsBlockImageTextRow.vue +9 -3
  25. package/app/components/public/cms/block/CmsBlockImageThreeCover.vue +7 -2
  26. package/app/components/public/cms/block/CmsBlockImageTwoColumn.vue +7 -3
  27. package/app/components/public/cms/block/CmsBlockProductHeading.vue +2 -1
  28. package/app/components/public/cms/element/CmsElementBuyBox.vue +4 -1
  29. package/app/components/public/cms/element/CmsElementCrossSelling.vue +2 -1
  30. package/app/components/public/cms/element/CmsElementHtml.vue +1 -1
  31. package/app/components/public/cms/element/CmsElementImage.vue +41 -2
  32. package/app/components/public/cms/element/CmsElementImageGallery.vue +2 -1
  33. package/app/components/public/cms/element/CmsElementProductDescriptionReviews.vue +18 -9
  34. package/app/components/public/cms/element/CmsElementText.md +77 -0
  35. package/app/components/public/cms/element/CmsElementText.vue +5 -32
  36. package/app/helpers/html-to-vue/renderToHtml.test.ts +55 -0
  37. package/app/helpers/html-to-vue/renderToHtml.ts +5 -1
  38. package/app/helpers/html-to-vue/renderer.ts +10 -1
  39. package/app/utils/listingFilters.test.ts +39 -0
  40. package/app/utils/listingFilters.ts +19 -0
  41. package/app/utils/routeQuery.test.ts +65 -0
  42. package/app/utils/routeQuery.ts +26 -0
  43. package/app/utils/useSelectedListingFilters.test.ts +92 -0
  44. package/app/utils/useSelectedListingFilters.ts +111 -0
  45. package/component-dirs.json +20 -0
  46. package/index.d.ts +2 -2
  47. package/nuxt.config.ts +35 -29
  48. package/package.json +27 -31
  49. package/dist/index.d.mts +0 -5
  50. package/dist/index.d.ts +0 -5
  51. package/dist/index.mjs +0 -31
  52. package/index.cjs +0 -7
@@ -4,12 +4,22 @@ import type {
4
4
  CmsElementSidebarFilter,
5
5
  } from "@shopware/composables";
6
6
  import { useCmsTranslations } from "@shopware/composables";
7
+ import { getCategoryFilterPostFilter } from "@shopware/helpers";
7
8
  import { defu } from "defu";
8
- import { computed, reactive } from "vue";
9
- import type { ComputedRef, UnwrapNestedRefs } from "vue";
9
+ import { computed } from "vue";
10
+ import type { ComputedRef } from "vue";
10
11
  import type { LocationQueryRaw } from "vue-router";
11
12
 
12
- import { useCategoryListing, useRoute, useRouter } from "#imports";
13
+ import {
14
+ firstQueryValue,
15
+ getVisibleListingFilters,
16
+ toNumber,
17
+ useCategoryListing,
18
+ useProductSearchListing,
19
+ useRoute,
20
+ useRouter,
21
+ useSelectedListingFilters,
22
+ } from "#imports";
13
23
  import type { Schemas, operations } from "#shopware";
14
24
 
15
25
  const props = defineProps<{
@@ -17,6 +27,8 @@ const props = defineProps<{
17
27
  listingType?: string;
18
28
  }>();
19
29
 
30
+ const isProductSearch = props.listingType === "productSearchListing";
31
+
20
32
  type Translations = {
21
33
  listing: {
22
34
  filters: string;
@@ -25,15 +37,6 @@ type Translations = {
25
37
  };
26
38
  };
27
39
 
28
- type FilterState = {
29
- manufacturer: Set<string>;
30
- properties: Set<string>;
31
- "min-price": number | undefined;
32
- "max-price": number | undefined;
33
- rating: number | undefined;
34
- "shipping-free": boolean | undefined;
35
- };
36
-
37
40
  let translations: Translations = {
38
41
  listing: {
39
42
  filters: "Filters",
@@ -54,22 +57,21 @@ const {
54
57
  getInitialFilters,
55
58
  getSortingOrders,
56
59
  search,
57
- } = useCategoryListing();
60
+ } = isProductSearch ? useProductSearchListing() : useCategoryListing();
58
61
 
59
- const sidebarSelectedFilters: UnwrapNestedRefs<FilterState> =
60
- reactive<FilterState>({
61
- manufacturer: new Set(),
62
- properties: new Set(),
63
- "min-price": undefined,
64
- "max-price": undefined,
65
- rating: undefined,
66
- "shipping-free": undefined,
67
- });
62
+ const sidebarSelectedFilters = useSelectedListingFilters();
63
+
64
+ // Only the filters this listing can actually apply - the category filter is
65
+ // search-only, matching the post-filter and URL gates below.
66
+ const visibleFilters = computed(() =>
67
+ getVisibleListingFilters(getInitialFilters.value, { isProductSearch }),
68
+ );
68
69
 
69
70
  const showResetFiltersButton = computed<boolean>(() => {
70
71
  if (
71
72
  sidebarSelectedFilters.manufacturer.size !== 0 ||
72
73
  sidebarSelectedFilters.properties.size !== 0 ||
74
+ (isProductSearch && sidebarSelectedFilters.categories.size !== 0) ||
73
75
  sidebarSelectedFilters["max-price"] ||
74
76
  sidebarSelectedFilters["min-price"] ||
75
77
  sidebarSelectedFilters.rating ||
@@ -89,56 +91,28 @@ const searchCriteriaForRequest: ComputedRef<Schemas["ProductListingCriteria"]> =
89
91
  properties: [...(sidebarSelectedFilters.properties as Set<string>)]?.join(
90
92
  "|",
91
93
  ),
94
+ // Category selection travels as a post-filter so the category
95
+ // aggregation itself is not reduced (faceted behavior). Search pages
96
+ // only: category pages never offer this filter, so a stale
97
+ // ?categories= param must not silently narrow them.
98
+ ...(isProductSearch && sidebarSelectedFilters.categories.size > 0
99
+ ? {
100
+ "post-filter": [
101
+ getCategoryFilterPostFilter([
102
+ ...(sidebarSelectedFilters.categories as Set<string>),
103
+ ]),
104
+ ],
105
+ }
106
+ : {}),
92
107
  "min-price": sidebarSelectedFilters["min-price"] as number,
93
108
  "max-price": sidebarSelectedFilters["max-price"] as number,
94
109
  order: getCurrentSortingOrder.value as string,
95
110
  "shipping-free": sidebarSelectedFilters["shipping-free"] as boolean,
96
111
  rating: sidebarSelectedFilters.rating as number,
97
- search: "",
98
- limit: route.query.limit ? Number(route.query.limit) : 15,
112
+ search: isProductSearch ? (firstQueryValue(route.query.search) ?? "") : "",
113
+ limit: toNumber(firstQueryValue(route.query.limit)) ?? 15,
99
114
  }));
100
115
 
101
- for (const param in route.query) {
102
- if (param in sidebarSelectedFilters) {
103
- const queryValue = route.query[param];
104
-
105
- // Skip arrays
106
- if (Array.isArray(queryValue)) continue;
107
-
108
- if (["manufacturer", "properties"].includes(param)) {
109
- if (typeof queryValue === "string") {
110
- const elements = queryValue.split("|");
111
- const targetSet = sidebarSelectedFilters[
112
- param as keyof FilterState
113
- ] as Set<string>;
114
- for (const element of elements) {
115
- targetSet.add(element);
116
- }
117
- }
118
- } else if (queryValue && typeof queryValue === "string") {
119
- // Fix: Use specific property assignments instead of generic keyof
120
- if (param === "min-price") {
121
- const numValue = Number(queryValue);
122
- if (!Number.isNaN(numValue)) {
123
- sidebarSelectedFilters["min-price"] = numValue;
124
- }
125
- } else if (param === "max-price") {
126
- const numValue = Number(queryValue);
127
- if (!Number.isNaN(numValue)) {
128
- sidebarSelectedFilters["max-price"] = numValue;
129
- }
130
- } else if (param === "rating") {
131
- const numValue = Number(queryValue);
132
- if (!Number.isNaN(numValue)) {
133
- sidebarSelectedFilters.rating = numValue;
134
- }
135
- } else if (param === "shipping-free") {
136
- sidebarSelectedFilters["shipping-free"] = queryValue === "true";
137
- }
138
- }
139
- }
140
- }
141
-
142
116
  const handleFilterChange = async (event: {
143
117
  code: string;
144
118
  value: string | number | boolean;
@@ -146,7 +120,11 @@ const handleFilterChange = async (event: {
146
120
  try {
147
121
  const { code, value } = event;
148
122
 
149
- if (code === "manufacturer" || code === "properties") {
123
+ if (
124
+ code === "manufacturer" ||
125
+ code === "properties" ||
126
+ code === "categories"
127
+ ) {
150
128
  const filterSet = sidebarSelectedFilters[code];
151
129
  const stringValue = String(value);
152
130
 
@@ -172,7 +150,12 @@ const handleFilterChange = async (event: {
172
150
 
173
151
  const executeSearch = async () => {
174
152
  try {
175
- await search(searchCriteriaForRequest.value);
153
+ // Search context refetches from the URL (the page's useAsyncData watches
154
+ // route.query), so navigating below is enough; calling search() here too
155
+ // would double-fetch and flicker. Category listings have no URL watcher.
156
+ if (!isProductSearch) {
157
+ await search(searchCriteriaForRequest.value);
158
+ }
176
159
 
177
160
  // Build query directly from searchCriteriaForRequest which already has pipe-separated strings
178
161
  const criteria = searchCriteriaForRequest.value;
@@ -180,12 +163,22 @@ const executeSearch = async () => {
180
163
 
181
164
  if (criteria.manufacturer) query.manufacturer = criteria.manufacturer;
182
165
  if (criteria.properties) query.properties = criteria.properties;
166
+ if (isProductSearch && sidebarSelectedFilters.categories.size > 0)
167
+ query.categories = [
168
+ ...(sidebarSelectedFilters.categories as Set<string>),
169
+ ].join("|");
183
170
  if (criteria["min-price"]) query["min-price"] = criteria["min-price"];
184
171
  if (criteria["max-price"]) query["max-price"] = criteria["max-price"];
185
172
  if (criteria.rating) query.rating = criteria.rating;
186
173
  if (criteria["shipping-free"])
187
174
  query["shipping-free"] = criteria["shipping-free"];
188
175
  if (criteria.order) query.order = criteria.order;
176
+ // Keep the search term in the URL so filtering on the search page does not reset the query
177
+ if (isProductSearch && route.query.search)
178
+ query.search = route.query.search;
179
+ // Preserve the page-size selection; rebuilding the query from scratch would
180
+ // otherwise drop `limit` and revert to the default on refresh/navigation.
181
+ if (route.query.limit) query.limit = route.query.limit;
189
182
 
190
183
  await router.push({
191
184
  query: query as LocationQueryRaw,
@@ -198,6 +191,7 @@ const executeSearch = async () => {
198
191
  const clearFilters = () => {
199
192
  (sidebarSelectedFilters.manufacturer as Set<string>).clear();
200
193
  (sidebarSelectedFilters.properties as Set<string>).clear();
194
+ (sidebarSelectedFilters.categories as Set<string>).clear();
201
195
  sidebarSelectedFilters["min-price"] = undefined;
202
196
  sidebarSelectedFilters["max-price"] = undefined;
203
197
  sidebarSelectedFilters.rating = undefined;
@@ -215,10 +209,13 @@ const currentSortingOrder = computed({
215
209
  },
216
210
  });
217
211
 
218
- await changeCurrentSortingOrder(order, {
219
- ...(route.query as unknown as operations["searchPage post /search"]["body"]),
220
- limit: route.query.limit ? Number(route.query.limit) : 15,
221
- });
212
+ // Search context refetches from the URL; category fetches directly.
213
+ if (!isProductSearch) {
214
+ await changeCurrentSortingOrder(order, {
215
+ ...(route.query as unknown as operations["searchPage post /search"]["body"]),
216
+ limit: toNumber(firstQueryValue(route.query.limit)) ?? 15,
217
+ });
218
+ }
222
219
  } catch (error) {
223
220
  console.error("Sorting order change failed:", error);
224
221
  }
@@ -242,7 +239,11 @@ const handleRemoveFilterChip = async (chip: {
242
239
  code: string;
243
240
  value: string | number;
244
241
  }) => {
245
- if (chip.code === "properties" || chip.code === "manufacturer") {
242
+ if (
243
+ chip.code === "properties" ||
244
+ chip.code === "manufacturer" ||
245
+ chip.code === "categories"
246
+ ) {
246
247
  const filterSet = sidebarSelectedFilters[chip.code] as Set<string>;
247
248
  filterSet.delete(String(chip.value));
248
249
  } else if (chip.code === "price") {
@@ -262,7 +263,7 @@ const handleRemoveFilterChip = async (chip: {
262
263
  <!-- Active Filter Chips -->
263
264
  <SwFilterChips
264
265
  :filters="sidebarSelectedFilters"
265
- :available-filters="getInitialFilters"
266
+ :available-filters="visibleFilters"
266
267
  @remove="handleRemoveFilterChip"
267
268
  />
268
269
 
@@ -288,11 +289,12 @@ const handleRemoveFilterChip = async (chip: {
288
289
  <!-- Filters List -->
289
290
  <div class="self-stretch flex flex-col justify-start items-start gap-4">
290
291
  <SwProductListingFilter
291
- v-for="filter in getInitialFilters"
292
+ v-for="filter in visibleFilters"
292
293
  :key="filter.id"
293
294
  :filter="filter"
294
295
  :selected-manufacturer="sidebarSelectedFilters.manufacturer"
295
296
  :selected-properties="sidebarSelectedFilters.properties"
297
+ :selected-categories="sidebarSelectedFilters.categories"
296
298
  :selected-min-price="sidebarSelectedFilters['min-price']"
297
299
  :selected-max-price="sidebarSelectedFilters['max-price']"
298
300
  :selected-rating="sidebarSelectedFilters.rating"
@@ -4,12 +4,22 @@ import type {
4
4
  CmsElementSidebarFilter,
5
5
  } from "@shopware/composables";
6
6
  import { useCmsTranslations } from "@shopware/composables";
7
+ import { getCategoryFilterPostFilter } from "@shopware/helpers";
7
8
  import { defu } from "defu";
8
- import { computed, reactive } from "vue";
9
- import type { ComputedRef, UnwrapNestedRefs } from "vue";
9
+ import { computed } from "vue";
10
+ import type { ComputedRef } from "vue";
10
11
  import type { LocationQueryRaw } from "vue-router";
11
12
 
12
- import { useCategoryListing, useRoute, useRouter } from "#imports";
13
+ import {
14
+ firstQueryValue,
15
+ getVisibleListingFilters,
16
+ toNumber,
17
+ useCategoryListing,
18
+ useProductSearchListing,
19
+ useRoute,
20
+ useRouter,
21
+ useSelectedListingFilters,
22
+ } from "#imports";
13
23
  import type { Schemas, operations } from "#shopware";
14
24
 
15
25
  const props = defineProps<{
@@ -17,6 +27,8 @@ const props = defineProps<{
17
27
  listingType?: string;
18
28
  }>();
19
29
 
30
+ const isProductSearch = props.listingType === "productSearchListing";
31
+
20
32
  type Translations = {
21
33
  listing: {
22
34
  filters: string;
@@ -25,15 +37,6 @@ type Translations = {
25
37
  };
26
38
  };
27
39
 
28
- type FilterState = {
29
- manufacturer: Set<string>;
30
- properties: Set<string>;
31
- "min-price": number | undefined;
32
- "max-price": number | undefined;
33
- rating: number | undefined;
34
- "shipping-free": boolean | undefined;
35
- };
36
-
37
40
  let translations: Translations = {
38
41
  listing: {
39
42
  filters: "Filters",
@@ -53,22 +56,21 @@ const {
53
56
  getInitialFilters,
54
57
  getSortingOrders,
55
58
  search,
56
- } = useCategoryListing();
59
+ } = isProductSearch ? useProductSearchListing() : useCategoryListing();
57
60
 
58
- const sidebarSelectedFilters: UnwrapNestedRefs<FilterState> =
59
- reactive<FilterState>({
60
- manufacturer: new Set(),
61
- properties: new Set(),
62
- "min-price": undefined,
63
- "max-price": undefined,
64
- rating: undefined,
65
- "shipping-free": undefined,
66
- });
61
+ const sidebarSelectedFilters = useSelectedListingFilters();
62
+
63
+ // Only the filters this listing can actually apply - the category filter is
64
+ // search-only, matching the post-filter and URL gates below.
65
+ const visibleFilters = computed(() =>
66
+ getVisibleListingFilters(getInitialFilters.value, { isProductSearch }),
67
+ );
67
68
 
68
69
  const showResetFiltersButton = computed<boolean>(() => {
69
70
  if (
70
71
  sidebarSelectedFilters.manufacturer.size !== 0 ||
71
72
  sidebarSelectedFilters.properties.size !== 0 ||
73
+ (isProductSearch && sidebarSelectedFilters.categories.size !== 0) ||
72
74
  sidebarSelectedFilters["max-price"] ||
73
75
  sidebarSelectedFilters["min-price"] ||
74
76
  sidebarSelectedFilters.rating ||
@@ -88,56 +90,28 @@ const searchCriteriaForRequest: ComputedRef<Schemas["ProductListingCriteria"]> =
88
90
  properties: [...(sidebarSelectedFilters.properties as Set<string>)]?.join(
89
91
  "|",
90
92
  ),
93
+ // Category selection travels as a post-filter so the category
94
+ // aggregation itself is not reduced (faceted behavior). Search pages
95
+ // only: category pages never offer this filter, so a stale
96
+ // ?categories= param must not silently narrow them.
97
+ ...(isProductSearch && sidebarSelectedFilters.categories.size > 0
98
+ ? {
99
+ "post-filter": [
100
+ getCategoryFilterPostFilter([
101
+ ...(sidebarSelectedFilters.categories as Set<string>),
102
+ ]),
103
+ ],
104
+ }
105
+ : {}),
91
106
  "min-price": sidebarSelectedFilters["min-price"] as number,
92
107
  "max-price": sidebarSelectedFilters["max-price"] as number,
93
108
  order: getCurrentSortingOrder.value as string,
94
109
  "shipping-free": sidebarSelectedFilters["shipping-free"] as boolean,
95
110
  rating: sidebarSelectedFilters.rating as number,
96
- search: "",
97
- limit: route.query.limit ? Number(route.query.limit) : 15,
111
+ search: isProductSearch ? (firstQueryValue(route.query.search) ?? "") : "",
112
+ limit: toNumber(firstQueryValue(route.query.limit)) ?? 15,
98
113
  }));
99
114
 
100
- for (const param in route.query) {
101
- if (param in sidebarSelectedFilters) {
102
- const queryValue = route.query[param];
103
-
104
- // Skip arrays
105
- if (Array.isArray(queryValue)) continue;
106
-
107
- if (["manufacturer", "properties"].includes(param)) {
108
- if (typeof queryValue === "string") {
109
- const elements = queryValue.split("|");
110
- const targetSet = sidebarSelectedFilters[
111
- param as keyof FilterState
112
- ] as Set<string>;
113
- for (const element of elements) {
114
- targetSet.add(element);
115
- }
116
- }
117
- } else if (queryValue && typeof queryValue === "string") {
118
- // Fix: Use specific property assignments instead of generic keyof
119
- if (param === "min-price") {
120
- const numValue = Number(queryValue);
121
- if (!Number.isNaN(numValue)) {
122
- sidebarSelectedFilters["min-price"] = numValue;
123
- }
124
- } else if (param === "max-price") {
125
- const numValue = Number(queryValue);
126
- if (!Number.isNaN(numValue)) {
127
- sidebarSelectedFilters["max-price"] = numValue;
128
- }
129
- } else if (param === "rating") {
130
- const numValue = Number(queryValue);
131
- if (!Number.isNaN(numValue)) {
132
- sidebarSelectedFilters.rating = numValue;
133
- }
134
- } else if (param === "shipping-free") {
135
- sidebarSelectedFilters["shipping-free"] = queryValue === "true";
136
- }
137
- }
138
- }
139
- }
140
-
141
115
  const handleFilterChange = async (event: {
142
116
  code: string;
143
117
  value: string | number | boolean;
@@ -145,7 +119,11 @@ const handleFilterChange = async (event: {
145
119
  try {
146
120
  const { code, value } = event;
147
121
 
148
- if (code === "manufacturer" || code === "properties") {
122
+ if (
123
+ code === "manufacturer" ||
124
+ code === "properties" ||
125
+ code === "categories"
126
+ ) {
149
127
  const filterSet = sidebarSelectedFilters[code];
150
128
  const stringValue = String(value);
151
129
 
@@ -171,7 +149,12 @@ const handleFilterChange = async (event: {
171
149
 
172
150
  const executeSearch = async () => {
173
151
  try {
174
- await search(searchCriteriaForRequest.value);
152
+ // Search context refetches from the URL (the page's useAsyncData watches
153
+ // route.query), so navigating below is enough; calling search() here too
154
+ // would double-fetch and flicker. Category listings have no URL watcher.
155
+ if (!isProductSearch) {
156
+ await search(searchCriteriaForRequest.value);
157
+ }
175
158
 
176
159
  // Build query directly from searchCriteriaForRequest which already has pipe-separated strings
177
160
  const criteria = searchCriteriaForRequest.value;
@@ -179,12 +162,22 @@ const executeSearch = async () => {
179
162
 
180
163
  if (criteria.manufacturer) query.manufacturer = criteria.manufacturer;
181
164
  if (criteria.properties) query.properties = criteria.properties;
165
+ if (isProductSearch && sidebarSelectedFilters.categories.size > 0)
166
+ query.categories = [
167
+ ...(sidebarSelectedFilters.categories as Set<string>),
168
+ ].join("|");
182
169
  if (criteria["min-price"]) query["min-price"] = criteria["min-price"];
183
170
  if (criteria["max-price"]) query["max-price"] = criteria["max-price"];
184
171
  if (criteria.rating) query.rating = criteria.rating;
185
172
  if (criteria["shipping-free"])
186
173
  query["shipping-free"] = criteria["shipping-free"];
187
174
  if (criteria.order) query.order = criteria.order;
175
+ // Keep the search term in the URL so filtering on the search page does not reset the query
176
+ if (isProductSearch && route.query.search)
177
+ query.search = route.query.search;
178
+ // Preserve the page-size selection; rebuilding the query from scratch would
179
+ // otherwise drop `limit` and revert to the default on refresh/navigation.
180
+ if (route.query.limit) query.limit = route.query.limit;
188
181
 
189
182
  await router.push({
190
183
  query: query as LocationQueryRaw,
@@ -197,6 +190,7 @@ const executeSearch = async () => {
197
190
  const clearFilters = () => {
198
191
  (sidebarSelectedFilters.manufacturer as Set<string>).clear();
199
192
  (sidebarSelectedFilters.properties as Set<string>).clear();
193
+ (sidebarSelectedFilters.categories as Set<string>).clear();
200
194
  sidebarSelectedFilters["min-price"] = undefined;
201
195
  sidebarSelectedFilters["max-price"] = undefined;
202
196
  sidebarSelectedFilters.rating = undefined;
@@ -214,10 +208,13 @@ const currentSortingOrder = computed({
214
208
  },
215
209
  });
216
210
 
217
- await changeCurrentSortingOrder(order, {
218
- ...(route.query as unknown as operations["searchPage post /search"]["body"]),
219
- limit: route.query.limit ? Number(route.query.limit) : 15,
220
- });
211
+ // Search context refetches from the URL; category fetches directly.
212
+ if (!isProductSearch) {
213
+ await changeCurrentSortingOrder(order, {
214
+ ...(route.query as unknown as operations["searchPage post /search"]["body"]),
215
+ limit: toNumber(firstQueryValue(route.query.limit)) ?? 15,
216
+ });
217
+ }
221
218
  } catch (error) {
222
219
  console.error("Sorting order change failed:", error);
223
220
  }
@@ -242,6 +239,9 @@ const hasActiveFilter = (filter: { code: string }) => {
242
239
  if (filter.code === "manufacturer") {
243
240
  return sidebarSelectedFilters.manufacturer.size > 0;
244
241
  }
242
+ if (filter.code === "categories") {
243
+ return sidebarSelectedFilters.categories.size > 0;
244
+ }
245
245
  if (filter.code === "price") {
246
246
  return (
247
247
  sidebarSelectedFilters["min-price"] !== undefined ||
@@ -265,7 +265,7 @@ const hasActiveFilter = (filter: { code: string }) => {
265
265
  <div class="flex flex-wrap items-center justify-start gap-4 z-10">
266
266
  <!-- Filter dropdowns -->
267
267
  <SwFilterDropdown
268
- v-for="filter in getInitialFilters"
268
+ v-for="filter in visibleFilters"
269
269
  :key="filter.id"
270
270
  :label="filter.label"
271
271
  :is-active="hasActiveFilter(filter)"
@@ -275,6 +275,7 @@ const hasActiveFilter = (filter: { code: string }) => {
275
275
  display-mode="dropdown"
276
276
  :selected-manufacturer="sidebarSelectedFilters.manufacturer"
277
277
  :selected-properties="sidebarSelectedFilters.properties"
278
+ :selected-categories="sidebarSelectedFilters.categories"
278
279
  :selected-min-price="sidebarSelectedFilters['min-price']"
279
280
  :selected-max-price="sidebarSelectedFilters['max-price']"
280
281
  :selected-rating="sidebarSelectedFilters.rating"
@@ -1,4 +1,5 @@
1
1
  <script setup lang="ts">
2
+ import { getTranslatedProperty } from "@shopware/helpers";
2
3
  import { defu } from "defu";
3
4
 
4
5
  import { useCmsTranslations } from "#imports";
@@ -37,13 +38,15 @@ translations = defu(useCmsTranslations(), translations) as Translations;
37
38
  ></div>
38
39
  <div class="w-2 h-2 bg-states-error rounded-full" v-else></div>
39
40
  <span v-if="availableStock >= minPurchase && deliveryTime"
40
- >{{ translations.product.deliveryTime }} {{ deliveryTime?.name }}
41
+ >{{ translations.product.deliveryTime }}
42
+ {{ getTranslatedProperty(deliveryTime, "name") }}
41
43
  </span>
42
44
  <span
43
45
  v-else-if="availableStock < minPurchase && deliveryTime && restockTime"
44
46
  >
45
47
  {{ translations.product.deliveryTime }} {{ restockTime }}
46
- {{ translations.product.days }} {{ deliveryTime?.name }}</span
48
+ {{ translations.product.days }}
49
+ {{ getTranslatedProperty(deliveryTime, "name") }}</span
47
50
  >
48
51
  <span v-else>{{ translations.product.noAvailable }}</span>
49
52
  </div>
@@ -1,6 +1,10 @@
1
1
  <script setup lang="ts">
2
2
  import { useCmsTranslations } from "@shopware/composables";
3
- import { buildUrlPrefix, getProductRoute } from "@shopware/helpers";
3
+ import {
4
+ buildUrlPrefix,
5
+ getProductRoute,
6
+ getTranslatedProperty,
7
+ } from "@shopware/helpers";
4
8
  import { defu } from "defu";
5
9
  import { computed, ref, unref } from "vue";
6
10
  import type { ComputedRef } from "vue";
@@ -86,11 +90,12 @@ const onHandleChange = async () => {
86
90
  class="mt-6"
87
91
  >
88
92
  <div class="text-sm text-gray-900 font-medium">
89
- {{ optionGroup.name }}
93
+ {{ getTranslatedProperty(optionGroup, "name") }}
90
94
  </div>
91
95
  <fieldset class="mt-4 flex-1">
92
96
  <legend class="sr-only">
93
- {{ translations.product.chooseA }} {{ optionGroup.name }}
97
+ {{ translations.product.chooseA }}
98
+ {{ getTranslatedProperty(optionGroup, "name") }}
94
99
  </legend>
95
100
  <div class="flex gap-3">
96
101
  <label