@shopware/cms-base-layer 3.1.0 → 4.1.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 +79 -23
- package/app/app.config.ts +3 -3
- package/app/assets/css/rich-text.css +80 -0
- package/app/components/SwCategoryNavigationLink.vue +4 -5
- package/app/components/SwContactForm.vue +2 -1
- package/app/components/SwFilterChips.vue +29 -7
- package/app/components/SwMedia3D.vue +8 -3
- package/app/components/SwNewsletterForm.vue +2 -1
- package/app/components/SwProductAddToCart.vue +27 -4
- package/app/components/SwProductCard.vue +26 -4
- package/app/components/SwProductCardDetails.vue +9 -2
- package/app/components/SwProductCardImage.vue +5 -1
- package/app/components/SwProductListingFilter.vue +6 -1
- package/app/components/SwProductListingFilters.vue +13 -189
- package/app/components/SwProductListingFiltersHorizontal.vue +20 -172
- 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/CmsBlockSpatialViewer.vue +7 -7
- package/app/components/public/cms/CmsGenericBlock.vue +6 -10
- package/app/components/public/cms/CmsGenericElement.md +11 -5
- package/app/components/public/cms/CmsGenericElement.vue +17 -11
- package/app/components/public/cms/FrontendAccountCustomerGroupRegistrationPage.vue +1 -1
- package/app/components/public/cms/block/CmsBlockAppRenderer.vue +48 -0
- package/app/components/public/cms/block/CmsBlockCategoryHeading.vue +19 -0
- package/app/components/public/cms/block/CmsBlockCategoryNavigation.vue +3 -2
- package/app/components/public/cms/block/CmsBlockCenterText.vue +5 -4
- package/app/components/public/cms/block/CmsBlockCrossSelling.vue +3 -2
- package/app/components/public/cms/block/CmsBlockCustomForm.vue +3 -2
- package/app/components/public/cms/block/CmsBlockForm.vue +3 -2
- package/app/components/public/cms/block/CmsBlockGalleryBuybox.vue +4 -3
- package/app/components/public/cms/block/CmsBlockImage.vue +3 -2
- package/app/components/public/cms/block/CmsBlockImageBubbleRow.vue +5 -4
- package/app/components/public/cms/block/CmsBlockImageCover.vue +3 -2
- package/app/components/public/cms/block/CmsBlockImageFourColumn.vue +6 -5
- package/app/components/public/cms/block/CmsBlockImageGallery.vue +3 -2
- package/app/components/public/cms/block/CmsBlockImageGalleryBig.vue +3 -2
- package/app/components/public/cms/block/CmsBlockImageHighlightRow.vue +5 -4
- package/app/components/public/cms/block/CmsBlockImageSimpleGrid.vue +5 -4
- package/app/components/public/cms/block/CmsBlockImageSlider.vue +3 -2
- package/app/components/public/cms/block/CmsBlockImageText.vue +4 -3
- package/app/components/public/cms/block/CmsBlockImageTextBubble.vue +8 -7
- package/app/components/public/cms/block/CmsBlockImageTextCover.vue +4 -3
- package/app/components/public/cms/block/CmsBlockImageTextGallery.vue +39 -16
- package/app/components/public/cms/block/CmsBlockImageTextRow.vue +8 -7
- package/app/components/public/cms/block/CmsBlockImageThreeColumn.vue +5 -4
- package/app/components/public/cms/block/CmsBlockImageThreeCover.vue +5 -4
- package/app/components/public/cms/block/CmsBlockImageTwoColumn.vue +4 -3
- package/app/components/public/cms/block/CmsBlockProductDescriptionReviews.vue +3 -2
- package/app/components/public/cms/block/CmsBlockProductHeading.vue +4 -3
- package/app/components/public/cms/block/CmsBlockProductListing.vue +3 -2
- package/app/components/public/cms/block/CmsBlockProductThreeColumn.vue +5 -4
- package/app/components/public/cms/block/CmsBlockSidebarFilter.vue +7 -5
- package/app/components/public/cms/block/CmsBlockText.vue +3 -2
- package/app/components/public/cms/block/CmsBlockTextHero.vue +3 -2
- package/app/components/public/cms/block/CmsBlockTextOnImage.vue +3 -2
- package/app/components/public/cms/block/CmsBlockTextThreeColumn.vue +5 -4
- package/app/components/public/cms/block/CmsBlockTextTwoColumn.vue +4 -3
- package/app/components/public/cms/block/CmsBlockVideo.vue +19 -0
- package/app/components/public/cms/block/CmsBlockVimeoVideo.vue +3 -2
- package/app/components/public/cms/block/CmsBlockYoutubeVideo.vue +3 -2
- package/app/components/public/cms/element/CmsElementBuyBox.vue +4 -1
- package/app/components/public/cms/element/CmsElementCategoryName.md +8 -0
- package/app/components/public/cms/element/CmsElementCategoryName.vue +35 -0
- 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 -1
- package/app/components/public/cms/element/CmsElementImageGallery.vue +2 -1
- package/app/components/public/cms/element/CmsElementProductDescriptionReviews.vue +2 -2
- package/app/components/public/cms/element/CmsElementProductListing.vue +58 -25
- package/app/components/public/cms/element/CmsElementText.md +77 -0
- package/app/components/public/cms/element/CmsElementText.vue +15 -41
- package/app/components/public/cms/element/CmsElementVideo.md +27 -0
- package/app/components/public/cms/element/CmsElementVideo.vue +193 -0
- package/app/components/public/cms/section/CmsSectionSidebar.vue +4 -4
- package/app/composables/useListingFilters.ts +189 -0
- package/app/composables/useSelectedListingFilters.ts +46 -0
- package/app/helpers/cms/getAppRendererLayout.test.ts +93 -0
- package/app/helpers/cms/getAppRendererLayout.ts +36 -0
- package/app/helpers/cms/getCategoryNameContent.test.ts +57 -0
- package/app/helpers/cms/getCategoryNameContent.ts +15 -0
- package/app/helpers/cms/getTextElementContent.test.ts +79 -0
- package/app/helpers/cms/getTextElementContent.ts +37 -0
- package/app/helpers/cms/getVideoElementOptions.test.ts +384 -0
- package/app/helpers/cms/getVideoElementOptions.ts +142 -0
- package/app/helpers/html-to-vue/renderToHtml.test.ts +71 -0
- package/app/helpers/html-to-vue/renderToHtml.ts +5 -1
- package/app/helpers/html-to-vue/renderer.ts +13 -4
- package/app/helpers/product/shouldShowBuyButton.test.ts +21 -0
- package/app/helpers/product/shouldShowBuyButton.ts +6 -0
- package/app/utils/{useSelectedListingFilters.test.ts → listingFilterState.test.ts} +14 -4
- package/app/utils/{useSelectedListingFilters.ts → listingFilterState.ts} +4 -33
- package/app/utils/listingFilters.test.ts +39 -0
- package/app/utils/listingFilters.ts +19 -0
- package/component-dirs.json +20 -0
- package/index.d.ts +2 -2
- package/nuxt.config.ts +11 -28
- package/package.json +23 -26
- 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
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { computed } from "vue";
|
|
3
3
|
import type { Component } from "vue";
|
|
4
4
|
|
|
5
|
+
import SwFilterCategoriesVue from "./listing-filters/SwFilterCategories.vue";
|
|
5
6
|
import SwFilterPriceVue from "./listing-filters/SwFilterPrice.vue";
|
|
6
7
|
import SwFilterPropertiesVue from "./listing-filters/SwFilterProperties.vue";
|
|
7
8
|
import SwFilterRatingVue from "./listing-filters/SwFilterRating.vue";
|
|
@@ -11,6 +12,7 @@ const {
|
|
|
11
12
|
filter,
|
|
12
13
|
selectedManufacturer,
|
|
13
14
|
selectedProperties,
|
|
15
|
+
selectedCategories = new Set<string>(),
|
|
14
16
|
selectedMinPrice,
|
|
15
17
|
selectedMaxPrice,
|
|
16
18
|
selectedRating,
|
|
@@ -20,6 +22,7 @@ const {
|
|
|
20
22
|
filter: ListingFilter;
|
|
21
23
|
selectedManufacturer: Set<string>;
|
|
22
24
|
selectedProperties: Set<string>;
|
|
25
|
+
selectedCategories?: Set<string>;
|
|
23
26
|
selectedMinPrice: number | undefined;
|
|
24
27
|
selectedMaxPrice: number | undefined;
|
|
25
28
|
selectedRating: number | undefined;
|
|
@@ -40,11 +43,13 @@ const transformedFilters = computed(() => ({
|
|
|
40
43
|
"shipping-free": selectedShippingFree,
|
|
41
44
|
manufacturer: [...selectedManufacturer],
|
|
42
45
|
properties: [...selectedProperties],
|
|
46
|
+
categories: [...selectedCategories],
|
|
43
47
|
}));
|
|
44
48
|
|
|
45
49
|
const filterComponent = computed<Component | undefined>(() => {
|
|
46
50
|
const componentMap: Record<string, Component> = {
|
|
47
51
|
manufacturer: SwFilterPropertiesVue,
|
|
52
|
+
categories: SwFilterCategoriesVue,
|
|
48
53
|
price: SwFilterPriceVue,
|
|
49
54
|
rating: SwFilterRatingVue,
|
|
50
55
|
"shipping-free": SwFilterShippingFreeVue,
|
|
@@ -67,7 +72,7 @@ const handleSelectValue = ({
|
|
|
67
72
|
};
|
|
68
73
|
</script>
|
|
69
74
|
<template>
|
|
70
|
-
<div>
|
|
75
|
+
<div :data-testid="`listing-filter-${filter.code}`">
|
|
71
76
|
<component
|
|
72
77
|
:is="filterComponent"
|
|
73
78
|
:filter="filter"
|
|
@@ -5,20 +5,8 @@ import type {
|
|
|
5
5
|
} from "@shopware/composables";
|
|
6
6
|
import { useCmsTranslations } from "@shopware/composables";
|
|
7
7
|
import { defu } from "defu";
|
|
8
|
-
import { computed } from "vue";
|
|
9
|
-
import type { ComputedRef } from "vue";
|
|
10
|
-
import type { LocationQueryRaw } from "vue-router";
|
|
11
8
|
|
|
12
|
-
import {
|
|
13
|
-
firstQueryValue,
|
|
14
|
-
toNumber,
|
|
15
|
-
useCategoryListing,
|
|
16
|
-
useProductSearchListing,
|
|
17
|
-
useRoute,
|
|
18
|
-
useRouter,
|
|
19
|
-
useSelectedListingFilters,
|
|
20
|
-
} from "#imports";
|
|
21
|
-
import type { Schemas, operations } from "#shopware";
|
|
9
|
+
import { useListingFilters } from "#imports";
|
|
22
10
|
|
|
23
11
|
const props = defineProps<{
|
|
24
12
|
content?: CmsElementProductListing | CmsElementSidebarFilter;
|
|
@@ -45,190 +33,25 @@ let translations: Translations = {
|
|
|
45
33
|
|
|
46
34
|
translations = defu(useCmsTranslations(), translations) as Translations;
|
|
47
35
|
|
|
48
|
-
const route = useRoute();
|
|
49
|
-
const router = useRouter();
|
|
50
|
-
|
|
51
36
|
const {
|
|
52
|
-
|
|
53
|
-
getCurrentFilters,
|
|
37
|
+
currentSortingOrder,
|
|
54
38
|
getCurrentSortingOrder,
|
|
55
|
-
getInitialFilters,
|
|
56
39
|
getSortingOrders,
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
sidebarSelectedFilters.properties.size !== 0 ||
|
|
66
|
-
sidebarSelectedFilters["max-price"] ||
|
|
67
|
-
sidebarSelectedFilters["min-price"] ||
|
|
68
|
-
sidebarSelectedFilters.rating ||
|
|
69
|
-
sidebarSelectedFilters["shipping-free"]
|
|
70
|
-
) {
|
|
71
|
-
return true;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
return false;
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
const searchCriteriaForRequest: ComputedRef<Schemas["ProductListingCriteria"]> =
|
|
78
|
-
computed(() => ({
|
|
79
|
-
manufacturer: [
|
|
80
|
-
...(sidebarSelectedFilters.manufacturer as Set<string>),
|
|
81
|
-
]?.join("|"),
|
|
82
|
-
properties: [...(sidebarSelectedFilters.properties as Set<string>)]?.join(
|
|
83
|
-
"|",
|
|
84
|
-
),
|
|
85
|
-
"min-price": sidebarSelectedFilters["min-price"] as number,
|
|
86
|
-
"max-price": sidebarSelectedFilters["max-price"] as number,
|
|
87
|
-
order: getCurrentSortingOrder.value as string,
|
|
88
|
-
"shipping-free": sidebarSelectedFilters["shipping-free"] as boolean,
|
|
89
|
-
rating: sidebarSelectedFilters.rating as number,
|
|
90
|
-
search: isProductSearch ? (firstQueryValue(route.query.search) ?? "") : "",
|
|
91
|
-
limit: toNumber(firstQueryValue(route.query.limit)) ?? 15,
|
|
92
|
-
}));
|
|
93
|
-
|
|
94
|
-
const handleFilterChange = async (event: {
|
|
95
|
-
code: string;
|
|
96
|
-
value: string | number | boolean;
|
|
97
|
-
}) => {
|
|
98
|
-
try {
|
|
99
|
-
const { code, value } = event;
|
|
100
|
-
|
|
101
|
-
if (code === "manufacturer" || code === "properties") {
|
|
102
|
-
const filterSet = sidebarSelectedFilters[code];
|
|
103
|
-
const stringValue = String(value);
|
|
104
|
-
|
|
105
|
-
if (filterSet.has(stringValue)) {
|
|
106
|
-
filterSet.delete(stringValue);
|
|
107
|
-
} else {
|
|
108
|
-
filterSet.add(stringValue);
|
|
109
|
-
}
|
|
110
|
-
} else if (code === "min-price" || code === "max-price") {
|
|
111
|
-
sidebarSelectedFilters[code] =
|
|
112
|
-
typeof value === "number" ? value : Number(value);
|
|
113
|
-
} else if (code === "rating") {
|
|
114
|
-
sidebarSelectedFilters.rating = Number(value);
|
|
115
|
-
} else if (code === "shipping-free") {
|
|
116
|
-
sidebarSelectedFilters["shipping-free"] = Boolean(value);
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
await executeSearch();
|
|
120
|
-
} catch (error) {
|
|
121
|
-
console.error("Filter update failed:", error);
|
|
122
|
-
}
|
|
123
|
-
};
|
|
124
|
-
|
|
125
|
-
const executeSearch = async () => {
|
|
126
|
-
try {
|
|
127
|
-
// Search context refetches from the URL (the page's useAsyncData watches
|
|
128
|
-
// route.query), so navigating below is enough; calling search() here too
|
|
129
|
-
// would double-fetch and flicker. Category listings have no URL watcher.
|
|
130
|
-
if (!isProductSearch) {
|
|
131
|
-
await search(searchCriteriaForRequest.value);
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
// Build query directly from searchCriteriaForRequest which already has pipe-separated strings
|
|
135
|
-
const criteria = searchCriteriaForRequest.value;
|
|
136
|
-
const query: Record<string, unknown> = {};
|
|
137
|
-
|
|
138
|
-
if (criteria.manufacturer) query.manufacturer = criteria.manufacturer;
|
|
139
|
-
if (criteria.properties) query.properties = criteria.properties;
|
|
140
|
-
if (criteria["min-price"]) query["min-price"] = criteria["min-price"];
|
|
141
|
-
if (criteria["max-price"]) query["max-price"] = criteria["max-price"];
|
|
142
|
-
if (criteria.rating) query.rating = criteria.rating;
|
|
143
|
-
if (criteria["shipping-free"])
|
|
144
|
-
query["shipping-free"] = criteria["shipping-free"];
|
|
145
|
-
if (criteria.order) query.order = criteria.order;
|
|
146
|
-
// Keep the search term in the URL so filtering on the search page does not reset the query
|
|
147
|
-
if (isProductSearch && route.query.search)
|
|
148
|
-
query.search = route.query.search;
|
|
149
|
-
// Preserve the page-size selection; rebuilding the query from scratch would
|
|
150
|
-
// otherwise drop `limit` and revert to the default on refresh/navigation.
|
|
151
|
-
if (route.query.limit) query.limit = route.query.limit;
|
|
152
|
-
|
|
153
|
-
await router.push({
|
|
154
|
-
query: query as LocationQueryRaw,
|
|
155
|
-
});
|
|
156
|
-
} catch (error) {
|
|
157
|
-
console.error("Search execution failed:", error);
|
|
158
|
-
}
|
|
159
|
-
};
|
|
160
|
-
|
|
161
|
-
const clearFilters = () => {
|
|
162
|
-
(sidebarSelectedFilters.manufacturer as Set<string>).clear();
|
|
163
|
-
(sidebarSelectedFilters.properties as Set<string>).clear();
|
|
164
|
-
sidebarSelectedFilters["min-price"] = undefined;
|
|
165
|
-
sidebarSelectedFilters["max-price"] = undefined;
|
|
166
|
-
sidebarSelectedFilters.rating = undefined;
|
|
167
|
-
sidebarSelectedFilters["shipping-free"] = undefined;
|
|
168
|
-
};
|
|
169
|
-
|
|
170
|
-
const currentSortingOrder = computed({
|
|
171
|
-
get: (): string => getCurrentSortingOrder.value || "",
|
|
172
|
-
set: async (order: string): Promise<void> => {
|
|
173
|
-
try {
|
|
174
|
-
await router.push({
|
|
175
|
-
query: {
|
|
176
|
-
...route.query,
|
|
177
|
-
order,
|
|
178
|
-
},
|
|
179
|
-
});
|
|
180
|
-
|
|
181
|
-
// Search context refetches from the URL; category fetches directly.
|
|
182
|
-
if (!isProductSearch) {
|
|
183
|
-
await changeCurrentSortingOrder(order, {
|
|
184
|
-
...(route.query as unknown as operations["searchPage post /search"]["body"]),
|
|
185
|
-
limit: toNumber(firstQueryValue(route.query.limit)) ?? 15,
|
|
186
|
-
});
|
|
187
|
-
}
|
|
188
|
-
} catch (error) {
|
|
189
|
-
console.error("Sorting order change failed:", error);
|
|
190
|
-
}
|
|
191
|
-
},
|
|
192
|
-
});
|
|
193
|
-
|
|
194
|
-
async function invokeCleanFilters() {
|
|
195
|
-
try {
|
|
196
|
-
clearFilters();
|
|
197
|
-
await executeSearch();
|
|
198
|
-
} catch (error) {
|
|
199
|
-
console.error("Clear filters failed:", error);
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
const handleSortChange = (sortKey: string) => {
|
|
204
|
-
currentSortingOrder.value = sortKey;
|
|
205
|
-
};
|
|
206
|
-
|
|
207
|
-
const handleRemoveFilterChip = async (chip: {
|
|
208
|
-
code: string;
|
|
209
|
-
value: string | number;
|
|
210
|
-
}) => {
|
|
211
|
-
if (chip.code === "properties" || chip.code === "manufacturer") {
|
|
212
|
-
const filterSet = sidebarSelectedFilters[chip.code] as Set<string>;
|
|
213
|
-
filterSet.delete(String(chip.value));
|
|
214
|
-
} else if (chip.code === "price") {
|
|
215
|
-
sidebarSelectedFilters["min-price"] = undefined;
|
|
216
|
-
sidebarSelectedFilters["max-price"] = undefined;
|
|
217
|
-
} else if (chip.code === "rating") {
|
|
218
|
-
sidebarSelectedFilters.rating = undefined;
|
|
219
|
-
} else if (chip.code === "shipping-free") {
|
|
220
|
-
sidebarSelectedFilters["shipping-free"] = undefined;
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
await executeSearch();
|
|
224
|
-
};
|
|
40
|
+
handleFilterChange,
|
|
41
|
+
handleRemoveFilterChip,
|
|
42
|
+
handleSortChange,
|
|
43
|
+
invokeCleanFilters,
|
|
44
|
+
selectedFilters: sidebarSelectedFilters,
|
|
45
|
+
showResetFiltersButton,
|
|
46
|
+
visibleFilters,
|
|
47
|
+
} = useListingFilters(isProductSearch);
|
|
225
48
|
</script>
|
|
226
49
|
<template>
|
|
227
50
|
<div>
|
|
228
51
|
<!-- Active Filter Chips -->
|
|
229
52
|
<SwFilterChips
|
|
230
53
|
:filters="sidebarSelectedFilters"
|
|
231
|
-
:available-filters="
|
|
54
|
+
:available-filters="visibleFilters"
|
|
232
55
|
@remove="handleRemoveFilterChip"
|
|
233
56
|
/>
|
|
234
57
|
|
|
@@ -254,11 +77,12 @@ const handleRemoveFilterChip = async (chip: {
|
|
|
254
77
|
<!-- Filters List -->
|
|
255
78
|
<div class="self-stretch flex flex-col justify-start items-start gap-4">
|
|
256
79
|
<SwProductListingFilter
|
|
257
|
-
v-for="filter in
|
|
80
|
+
v-for="filter in visibleFilters"
|
|
258
81
|
:key="filter.id"
|
|
259
82
|
:filter="filter"
|
|
260
83
|
:selected-manufacturer="sidebarSelectedFilters.manufacturer"
|
|
261
84
|
:selected-properties="sidebarSelectedFilters.properties"
|
|
85
|
+
:selected-categories="sidebarSelectedFilters.categories"
|
|
262
86
|
:selected-min-price="sidebarSelectedFilters['min-price']"
|
|
263
87
|
:selected-max-price="sidebarSelectedFilters['max-price']"
|
|
264
88
|
:selected-rating="sidebarSelectedFilters.rating"
|
|
@@ -5,20 +5,8 @@ import type {
|
|
|
5
5
|
} from "@shopware/composables";
|
|
6
6
|
import { useCmsTranslations } from "@shopware/composables";
|
|
7
7
|
import { defu } from "defu";
|
|
8
|
-
import { computed } from "vue";
|
|
9
|
-
import type { ComputedRef } from "vue";
|
|
10
|
-
import type { LocationQueryRaw } from "vue-router";
|
|
11
8
|
|
|
12
|
-
import {
|
|
13
|
-
firstQueryValue,
|
|
14
|
-
toNumber,
|
|
15
|
-
useCategoryListing,
|
|
16
|
-
useProductSearchListing,
|
|
17
|
-
useRoute,
|
|
18
|
-
useRouter,
|
|
19
|
-
useSelectedListingFilters,
|
|
20
|
-
} from "#imports";
|
|
21
|
-
import type { Schemas, operations } from "#shopware";
|
|
9
|
+
import { useListingFilters } from "#imports";
|
|
22
10
|
|
|
23
11
|
const props = defineProps<{
|
|
24
12
|
content: CmsElementProductListing | CmsElementSidebarFilter;
|
|
@@ -45,169 +33,27 @@ let translations: Translations = {
|
|
|
45
33
|
|
|
46
34
|
translations = defu(useCmsTranslations(), translations) as Translations;
|
|
47
35
|
|
|
48
|
-
const route = useRoute();
|
|
49
|
-
const router = useRouter();
|
|
50
|
-
|
|
51
36
|
const {
|
|
52
|
-
|
|
37
|
+
currentSortingOrder,
|
|
53
38
|
getCurrentSortingOrder,
|
|
54
|
-
getInitialFilters,
|
|
55
39
|
getSortingOrders,
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
sidebarSelectedFilters["min-price"] ||
|
|
67
|
-
sidebarSelectedFilters.rating ||
|
|
68
|
-
sidebarSelectedFilters["shipping-free"]
|
|
69
|
-
) {
|
|
70
|
-
return true;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
return false;
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
const searchCriteriaForRequest: ComputedRef<Schemas["ProductListingCriteria"]> =
|
|
77
|
-
computed(() => ({
|
|
78
|
-
manufacturer: [
|
|
79
|
-
...(sidebarSelectedFilters.manufacturer as Set<string>),
|
|
80
|
-
]?.join("|"),
|
|
81
|
-
properties: [...(sidebarSelectedFilters.properties as Set<string>)]?.join(
|
|
82
|
-
"|",
|
|
83
|
-
),
|
|
84
|
-
"min-price": sidebarSelectedFilters["min-price"] as number,
|
|
85
|
-
"max-price": sidebarSelectedFilters["max-price"] as number,
|
|
86
|
-
order: getCurrentSortingOrder.value as string,
|
|
87
|
-
"shipping-free": sidebarSelectedFilters["shipping-free"] as boolean,
|
|
88
|
-
rating: sidebarSelectedFilters.rating as number,
|
|
89
|
-
search: isProductSearch ? (firstQueryValue(route.query.search) ?? "") : "",
|
|
90
|
-
limit: toNumber(firstQueryValue(route.query.limit)) ?? 15,
|
|
91
|
-
}));
|
|
92
|
-
|
|
93
|
-
const handleFilterChange = async (event: {
|
|
94
|
-
code: string;
|
|
95
|
-
value: string | number | boolean;
|
|
96
|
-
}) => {
|
|
97
|
-
try {
|
|
98
|
-
const { code, value } = event;
|
|
99
|
-
|
|
100
|
-
if (code === "manufacturer" || code === "properties") {
|
|
101
|
-
const filterSet = sidebarSelectedFilters[code];
|
|
102
|
-
const stringValue = String(value);
|
|
103
|
-
|
|
104
|
-
if (filterSet.has(stringValue)) {
|
|
105
|
-
filterSet.delete(stringValue);
|
|
106
|
-
} else {
|
|
107
|
-
filterSet.add(stringValue);
|
|
108
|
-
}
|
|
109
|
-
} else if (code === "min-price" || code === "max-price") {
|
|
110
|
-
sidebarSelectedFilters[code] =
|
|
111
|
-
typeof value === "number" ? value : Number(value);
|
|
112
|
-
} else if (code === "rating") {
|
|
113
|
-
sidebarSelectedFilters.rating = Number(value);
|
|
114
|
-
} else if (code === "shipping-free") {
|
|
115
|
-
sidebarSelectedFilters["shipping-free"] = Boolean(value);
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
await executeSearch();
|
|
119
|
-
} catch (error) {
|
|
120
|
-
console.error("Filter update failed:", error);
|
|
121
|
-
}
|
|
122
|
-
};
|
|
123
|
-
|
|
124
|
-
const executeSearch = async () => {
|
|
125
|
-
try {
|
|
126
|
-
// Search context refetches from the URL (the page's useAsyncData watches
|
|
127
|
-
// route.query), so navigating below is enough; calling search() here too
|
|
128
|
-
// would double-fetch and flicker. Category listings have no URL watcher.
|
|
129
|
-
if (!isProductSearch) {
|
|
130
|
-
await search(searchCriteriaForRequest.value);
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
// Build query directly from searchCriteriaForRequest which already has pipe-separated strings
|
|
134
|
-
const criteria = searchCriteriaForRequest.value;
|
|
135
|
-
const query: Record<string, unknown> = {};
|
|
136
|
-
|
|
137
|
-
if (criteria.manufacturer) query.manufacturer = criteria.manufacturer;
|
|
138
|
-
if (criteria.properties) query.properties = criteria.properties;
|
|
139
|
-
if (criteria["min-price"]) query["min-price"] = criteria["min-price"];
|
|
140
|
-
if (criteria["max-price"]) query["max-price"] = criteria["max-price"];
|
|
141
|
-
if (criteria.rating) query.rating = criteria.rating;
|
|
142
|
-
if (criteria["shipping-free"])
|
|
143
|
-
query["shipping-free"] = criteria["shipping-free"];
|
|
144
|
-
if (criteria.order) query.order = criteria.order;
|
|
145
|
-
// Keep the search term in the URL so filtering on the search page does not reset the query
|
|
146
|
-
if (isProductSearch && route.query.search)
|
|
147
|
-
query.search = route.query.search;
|
|
148
|
-
// Preserve the page-size selection; rebuilding the query from scratch would
|
|
149
|
-
// otherwise drop `limit` and revert to the default on refresh/navigation.
|
|
150
|
-
if (route.query.limit) query.limit = route.query.limit;
|
|
151
|
-
|
|
152
|
-
await router.push({
|
|
153
|
-
query: query as LocationQueryRaw,
|
|
154
|
-
});
|
|
155
|
-
} catch (error) {
|
|
156
|
-
console.error("Search execution failed:", error);
|
|
157
|
-
}
|
|
158
|
-
};
|
|
159
|
-
|
|
160
|
-
const clearFilters = () => {
|
|
161
|
-
(sidebarSelectedFilters.manufacturer as Set<string>).clear();
|
|
162
|
-
(sidebarSelectedFilters.properties as Set<string>).clear();
|
|
163
|
-
sidebarSelectedFilters["min-price"] = undefined;
|
|
164
|
-
sidebarSelectedFilters["max-price"] = undefined;
|
|
165
|
-
sidebarSelectedFilters.rating = undefined;
|
|
166
|
-
sidebarSelectedFilters["shipping-free"] = undefined;
|
|
167
|
-
};
|
|
168
|
-
|
|
169
|
-
const currentSortingOrder = computed({
|
|
170
|
-
get: (): string => getCurrentSortingOrder.value || "",
|
|
171
|
-
set: async (order: string): Promise<void> => {
|
|
172
|
-
try {
|
|
173
|
-
await router.push({
|
|
174
|
-
query: {
|
|
175
|
-
...route.query,
|
|
176
|
-
order,
|
|
177
|
-
},
|
|
178
|
-
});
|
|
179
|
-
|
|
180
|
-
// Search context refetches from the URL; category fetches directly.
|
|
181
|
-
if (!isProductSearch) {
|
|
182
|
-
await changeCurrentSortingOrder(order, {
|
|
183
|
-
...(route.query as unknown as operations["searchPage post /search"]["body"]),
|
|
184
|
-
limit: toNumber(firstQueryValue(route.query.limit)) ?? 15,
|
|
185
|
-
});
|
|
186
|
-
}
|
|
187
|
-
} catch (error) {
|
|
188
|
-
console.error("Sorting order change failed:", error);
|
|
189
|
-
}
|
|
190
|
-
},
|
|
191
|
-
});
|
|
192
|
-
|
|
193
|
-
async function invokeCleanFilters() {
|
|
194
|
-
try {
|
|
195
|
-
clearFilters();
|
|
196
|
-
await executeSearch();
|
|
197
|
-
} catch (error) {
|
|
198
|
-
console.error("Clear filters failed:", error);
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
const handleSortChange = (sortKey: string) => {
|
|
203
|
-
currentSortingOrder.value = sortKey;
|
|
204
|
-
};
|
|
205
|
-
|
|
206
|
-
// Helper to check if a filter has active selections
|
|
40
|
+
handleFilterChange,
|
|
41
|
+
handleRemoveFilterChip,
|
|
42
|
+
handleSortChange,
|
|
43
|
+
invokeCleanFilters,
|
|
44
|
+
selectedFilters: sidebarSelectedFilters,
|
|
45
|
+
showResetFiltersButton,
|
|
46
|
+
visibleFilters,
|
|
47
|
+
} = useListingFilters(isProductSearch);
|
|
48
|
+
|
|
49
|
+
// Whether a filter has an active selection, for the collapsed chip state.
|
|
207
50
|
const hasActiveFilter = (filter: { code: string }) => {
|
|
208
51
|
if (filter.code === "manufacturer") {
|
|
209
52
|
return sidebarSelectedFilters.manufacturer.size > 0;
|
|
210
53
|
}
|
|
54
|
+
if (filter.code === "categories") {
|
|
55
|
+
return sidebarSelectedFilters.categories.size > 0;
|
|
56
|
+
}
|
|
211
57
|
if (filter.code === "price") {
|
|
212
58
|
return (
|
|
213
59
|
sidebarSelectedFilters["min-price"] !== undefined ||
|
|
@@ -218,9 +64,10 @@ const hasActiveFilter = (filter: { code: string }) => {
|
|
|
218
64
|
return sidebarSelectedFilters.rating !== undefined;
|
|
219
65
|
}
|
|
220
66
|
if (filter.code === "shipping-free") {
|
|
221
|
-
return sidebarSelectedFilters["shipping-free"]
|
|
67
|
+
return sidebarSelectedFilters["shipping-free"] !== undefined;
|
|
222
68
|
}
|
|
223
|
-
//
|
|
69
|
+
// Any other code is a property group. Each carries its own code and only
|
|
70
|
+
// normalises to "properties" when it emits, so this has to stay a fallback.
|
|
224
71
|
return sidebarSelectedFilters.properties.size > 0;
|
|
225
72
|
};
|
|
226
73
|
</script>
|
|
@@ -231,7 +78,7 @@ const hasActiveFilter = (filter: { code: string }) => {
|
|
|
231
78
|
<div class="flex flex-wrap items-center justify-start gap-4 z-10">
|
|
232
79
|
<!-- Filter dropdowns -->
|
|
233
80
|
<SwFilterDropdown
|
|
234
|
-
v-for="filter in
|
|
81
|
+
v-for="filter in visibleFilters"
|
|
235
82
|
:key="filter.id"
|
|
236
83
|
:label="filter.label"
|
|
237
84
|
:is-active="hasActiveFilter(filter)"
|
|
@@ -241,6 +88,7 @@ const hasActiveFilter = (filter: { code: string }) => {
|
|
|
241
88
|
display-mode="dropdown"
|
|
242
89
|
:selected-manufacturer="sidebarSelectedFilters.manufacturer"
|
|
243
90
|
:selected-properties="sidebarSelectedFilters.properties"
|
|
91
|
+
:selected-categories="sidebarSelectedFilters.categories"
|
|
244
92
|
:selected-min-price="sidebarSelectedFilters['min-price']"
|
|
245
93
|
:selected-max-price="sidebarSelectedFilters['max-price']"
|
|
246
94
|
: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
|