@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.
- package/README.md +78 -15
- package/app/app.config.ts +3 -3
- package/app/assets/css/rich-text.css +80 -0
- package/app/components/SwContactForm.vue +2 -1
- package/app/components/SwFilterChips.vue +29 -7
- package/app/components/SwNewsletterForm.vue +2 -1
- package/app/components/SwProductCard.vue +19 -3
- package/app/components/SwProductCardImage.vue +5 -1
- package/app/components/SwProductListingFilter.vue +5 -0
- package/app/components/SwProductListingFilters.vue +76 -74
- package/app/components/SwProductListingFiltersHorizontal.vue +73 -72
- package/app/components/SwStockInfo.vue +5 -2
- package/app/components/SwVariantConfigurator.vue +8 -3
- package/app/components/listing-filters/SwFilterCategories.vue +158 -0
- package/app/components/public/cms/CmsGenericBlock.vue +1 -1
- package/app/components/public/cms/CmsGenericElement.vue +1 -1
- package/app/components/public/cms/CmsNoComponent.vue +1 -1
- package/app/components/public/cms/FrontendAccountCustomerGroupRegistrationPage.vue +1 -1
- package/app/components/public/cms/block/CmsBlockCenterText.vue +2 -1
- package/app/components/public/cms/block/CmsBlockImageBubbleRow.vue +5 -1
- package/app/components/public/cms/block/CmsBlockImageFourColumn.vue +6 -2
- package/app/components/public/cms/block/CmsBlockImageGalleryBig.vue +9 -5
- package/app/components/public/cms/block/CmsBlockImageHighlightRow.vue +7 -2
- package/app/components/public/cms/block/CmsBlockImageTextRow.vue +9 -3
- package/app/components/public/cms/block/CmsBlockImageThreeCover.vue +7 -2
- package/app/components/public/cms/block/CmsBlockImageTwoColumn.vue +7 -3
- package/app/components/public/cms/block/CmsBlockProductHeading.vue +2 -1
- package/app/components/public/cms/element/CmsElementBuyBox.vue +4 -1
- package/app/components/public/cms/element/CmsElementCrossSelling.vue +2 -1
- package/app/components/public/cms/element/CmsElementHtml.vue +1 -1
- package/app/components/public/cms/element/CmsElementImage.vue +41 -2
- package/app/components/public/cms/element/CmsElementImageGallery.vue +2 -1
- package/app/components/public/cms/element/CmsElementProductDescriptionReviews.vue +18 -9
- package/app/components/public/cms/element/CmsElementText.md +77 -0
- package/app/components/public/cms/element/CmsElementText.vue +5 -32
- package/app/helpers/html-to-vue/renderToHtml.test.ts +55 -0
- package/app/helpers/html-to-vue/renderToHtml.ts +5 -1
- package/app/helpers/html-to-vue/renderer.ts +10 -1
- package/app/utils/listingFilters.test.ts +39 -0
- package/app/utils/listingFilters.ts +19 -0
- package/app/utils/routeQuery.test.ts +65 -0
- package/app/utils/routeQuery.ts +26 -0
- package/app/utils/useSelectedListingFilters.test.ts +92 -0
- package/app/utils/useSelectedListingFilters.ts +111 -0
- package/component-dirs.json +20 -0
- package/index.d.ts +2 -2
- package/nuxt.config.ts +35 -29
- package/package.json +27 -31
- package/dist/index.d.mts +0 -5
- package/dist/index.d.ts +0 -5
- package/dist/index.mjs +0 -31
- 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
|
|
9
|
-
import type { ComputedRef
|
|
9
|
+
import { computed } from "vue";
|
|
10
|
+
import type { ComputedRef } from "vue";
|
|
10
11
|
import type { LocationQueryRaw } from "vue-router";
|
|
11
12
|
|
|
12
|
-
import {
|
|
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
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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:
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
219
|
-
|
|
220
|
-
|
|
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 (
|
|
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="
|
|
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
|
|
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
|
|
9
|
-
import type { ComputedRef
|
|
9
|
+
import { computed } from "vue";
|
|
10
|
+
import type { ComputedRef } from "vue";
|
|
10
11
|
import type { LocationQueryRaw } from "vue-router";
|
|
11
12
|
|
|
12
|
-
import {
|
|
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
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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:
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
218
|
-
|
|
219
|
-
|
|
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
|
|
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 }}
|
|
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 }}
|
|
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 {
|
|
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
|
|
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 }}
|
|
97
|
+
{{ translations.product.chooseA }}
|
|
98
|
+
{{ getTranslatedProperty(optionGroup, "name") }}
|
|
94
99
|
</legend>
|
|
95
100
|
<div class="flex gap-3">
|
|
96
101
|
<label
|