@shopware/cms-base-layer 4.0.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 +58 -43
- package/app/components/SwCategoryNavigationLink.vue +4 -5
- package/app/components/SwMedia3D.vue +8 -3
- package/app/components/SwProductAddToCart.vue +27 -4
- package/app/components/SwProductCard.vue +26 -4
- package/app/components/SwProductCardDetails.vue +9 -2
- package/app/components/SwProductListingFilter.vue +1 -1
- package/app/components/SwProductListingFilters.vue +10 -222
- package/app/components/SwProductListingFiltersHorizontal.vue +15 -202
- 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/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/CmsElementCategoryName.md +8 -0
- package/app/components/public/cms/element/CmsElementCategoryName.vue +35 -0
- package/app/components/public/cms/element/CmsElementProductListing.vue +58 -25
- package/app/components/public/cms/element/CmsElementText.vue +11 -10
- 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 +16 -0
- package/app/helpers/html-to-vue/renderer.ts +3 -3
- 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} +1 -1
- package/app/utils/{useSelectedListingFilters.ts → listingFilterState.ts} +0 -35
- package/package.json +10 -10
|
@@ -4,23 +4,9 @@ import type {
|
|
|
4
4
|
CmsElementSidebarFilter,
|
|
5
5
|
} from "@shopware/composables";
|
|
6
6
|
import { useCmsTranslations } from "@shopware/composables";
|
|
7
|
-
import { getCategoryFilterPostFilter } from "@shopware/helpers";
|
|
8
7
|
import { defu } from "defu";
|
|
9
|
-
import { computed } from "vue";
|
|
10
|
-
import type { ComputedRef } from "vue";
|
|
11
|
-
import type { LocationQueryRaw } from "vue-router";
|
|
12
8
|
|
|
13
|
-
import {
|
|
14
|
-
firstQueryValue,
|
|
15
|
-
getVisibleListingFilters,
|
|
16
|
-
toNumber,
|
|
17
|
-
useCategoryListing,
|
|
18
|
-
useProductSearchListing,
|
|
19
|
-
useRoute,
|
|
20
|
-
useRouter,
|
|
21
|
-
useSelectedListingFilters,
|
|
22
|
-
} from "#imports";
|
|
23
|
-
import type { Schemas, operations } from "#shopware";
|
|
9
|
+
import { useListingFilters } from "#imports";
|
|
24
10
|
|
|
25
11
|
const props = defineProps<{
|
|
26
12
|
content: CmsElementProductListing | CmsElementSidebarFilter;
|
|
@@ -47,194 +33,20 @@ let translations: Translations = {
|
|
|
47
33
|
|
|
48
34
|
translations = defu(useCmsTranslations(), translations) as Translations;
|
|
49
35
|
|
|
50
|
-
const route = useRoute();
|
|
51
|
-
const router = useRouter();
|
|
52
|
-
|
|
53
36
|
const {
|
|
54
|
-
|
|
37
|
+
currentSortingOrder,
|
|
55
38
|
getCurrentSortingOrder,
|
|
56
|
-
getInitialFilters,
|
|
57
39
|
getSortingOrders,
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
const showResetFiltersButton = computed<boolean>(() => {
|
|
70
|
-
if (
|
|
71
|
-
sidebarSelectedFilters.manufacturer.size !== 0 ||
|
|
72
|
-
sidebarSelectedFilters.properties.size !== 0 ||
|
|
73
|
-
(isProductSearch && sidebarSelectedFilters.categories.size !== 0) ||
|
|
74
|
-
sidebarSelectedFilters["max-price"] ||
|
|
75
|
-
sidebarSelectedFilters["min-price"] ||
|
|
76
|
-
sidebarSelectedFilters.rating ||
|
|
77
|
-
sidebarSelectedFilters["shipping-free"]
|
|
78
|
-
) {
|
|
79
|
-
return true;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
return false;
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
const searchCriteriaForRequest: ComputedRef<Schemas["ProductListingCriteria"]> =
|
|
86
|
-
computed(() => ({
|
|
87
|
-
manufacturer: [
|
|
88
|
-
...(sidebarSelectedFilters.manufacturer as Set<string>),
|
|
89
|
-
]?.join("|"),
|
|
90
|
-
properties: [...(sidebarSelectedFilters.properties as Set<string>)]?.join(
|
|
91
|
-
"|",
|
|
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
|
-
: {}),
|
|
106
|
-
"min-price": sidebarSelectedFilters["min-price"] as number,
|
|
107
|
-
"max-price": sidebarSelectedFilters["max-price"] as number,
|
|
108
|
-
order: getCurrentSortingOrder.value as string,
|
|
109
|
-
"shipping-free": sidebarSelectedFilters["shipping-free"] as boolean,
|
|
110
|
-
rating: sidebarSelectedFilters.rating as number,
|
|
111
|
-
search: isProductSearch ? (firstQueryValue(route.query.search) ?? "") : "",
|
|
112
|
-
limit: toNumber(firstQueryValue(route.query.limit)) ?? 15,
|
|
113
|
-
}));
|
|
114
|
-
|
|
115
|
-
const handleFilterChange = async (event: {
|
|
116
|
-
code: string;
|
|
117
|
-
value: string | number | boolean;
|
|
118
|
-
}) => {
|
|
119
|
-
try {
|
|
120
|
-
const { code, value } = event;
|
|
121
|
-
|
|
122
|
-
if (
|
|
123
|
-
code === "manufacturer" ||
|
|
124
|
-
code === "properties" ||
|
|
125
|
-
code === "categories"
|
|
126
|
-
) {
|
|
127
|
-
const filterSet = sidebarSelectedFilters[code];
|
|
128
|
-
const stringValue = String(value);
|
|
129
|
-
|
|
130
|
-
if (filterSet.has(stringValue)) {
|
|
131
|
-
filterSet.delete(stringValue);
|
|
132
|
-
} else {
|
|
133
|
-
filterSet.add(stringValue);
|
|
134
|
-
}
|
|
135
|
-
} else if (code === "min-price" || code === "max-price") {
|
|
136
|
-
sidebarSelectedFilters[code] =
|
|
137
|
-
typeof value === "number" ? value : Number(value);
|
|
138
|
-
} else if (code === "rating") {
|
|
139
|
-
sidebarSelectedFilters.rating = Number(value);
|
|
140
|
-
} else if (code === "shipping-free") {
|
|
141
|
-
sidebarSelectedFilters["shipping-free"] = Boolean(value);
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
await executeSearch();
|
|
145
|
-
} catch (error) {
|
|
146
|
-
console.error("Filter update failed:", error);
|
|
147
|
-
}
|
|
148
|
-
};
|
|
149
|
-
|
|
150
|
-
const executeSearch = async () => {
|
|
151
|
-
try {
|
|
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
|
-
}
|
|
158
|
-
|
|
159
|
-
// Build query directly from searchCriteriaForRequest which already has pipe-separated strings
|
|
160
|
-
const criteria = searchCriteriaForRequest.value;
|
|
161
|
-
const query: Record<string, unknown> = {};
|
|
162
|
-
|
|
163
|
-
if (criteria.manufacturer) query.manufacturer = criteria.manufacturer;
|
|
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("|");
|
|
169
|
-
if (criteria["min-price"]) query["min-price"] = criteria["min-price"];
|
|
170
|
-
if (criteria["max-price"]) query["max-price"] = criteria["max-price"];
|
|
171
|
-
if (criteria.rating) query.rating = criteria.rating;
|
|
172
|
-
if (criteria["shipping-free"])
|
|
173
|
-
query["shipping-free"] = criteria["shipping-free"];
|
|
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;
|
|
181
|
-
|
|
182
|
-
await router.push({
|
|
183
|
-
query: query as LocationQueryRaw,
|
|
184
|
-
});
|
|
185
|
-
} catch (error) {
|
|
186
|
-
console.error("Search execution failed:", error);
|
|
187
|
-
}
|
|
188
|
-
};
|
|
189
|
-
|
|
190
|
-
const clearFilters = () => {
|
|
191
|
-
(sidebarSelectedFilters.manufacturer as Set<string>).clear();
|
|
192
|
-
(sidebarSelectedFilters.properties as Set<string>).clear();
|
|
193
|
-
(sidebarSelectedFilters.categories as Set<string>).clear();
|
|
194
|
-
sidebarSelectedFilters["min-price"] = undefined;
|
|
195
|
-
sidebarSelectedFilters["max-price"] = undefined;
|
|
196
|
-
sidebarSelectedFilters.rating = undefined;
|
|
197
|
-
sidebarSelectedFilters["shipping-free"] = undefined;
|
|
198
|
-
};
|
|
199
|
-
|
|
200
|
-
const currentSortingOrder = computed({
|
|
201
|
-
get: (): string => getCurrentSortingOrder.value || "",
|
|
202
|
-
set: async (order: string): Promise<void> => {
|
|
203
|
-
try {
|
|
204
|
-
await router.push({
|
|
205
|
-
query: {
|
|
206
|
-
...route.query,
|
|
207
|
-
order,
|
|
208
|
-
},
|
|
209
|
-
});
|
|
210
|
-
|
|
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
|
-
}
|
|
218
|
-
} catch (error) {
|
|
219
|
-
console.error("Sorting order change failed:", error);
|
|
220
|
-
}
|
|
221
|
-
},
|
|
222
|
-
});
|
|
223
|
-
|
|
224
|
-
async function invokeCleanFilters() {
|
|
225
|
-
try {
|
|
226
|
-
clearFilters();
|
|
227
|
-
await executeSearch();
|
|
228
|
-
} catch (error) {
|
|
229
|
-
console.error("Clear filters failed:", error);
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
const handleSortChange = (sortKey: string) => {
|
|
234
|
-
currentSortingOrder.value = sortKey;
|
|
235
|
-
};
|
|
236
|
-
|
|
237
|
-
// 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.
|
|
238
50
|
const hasActiveFilter = (filter: { code: string }) => {
|
|
239
51
|
if (filter.code === "manufacturer") {
|
|
240
52
|
return sidebarSelectedFilters.manufacturer.size > 0;
|
|
@@ -252,9 +64,10 @@ const hasActiveFilter = (filter: { code: string }) => {
|
|
|
252
64
|
return sidebarSelectedFilters.rating !== undefined;
|
|
253
65
|
}
|
|
254
66
|
if (filter.code === "shipping-free") {
|
|
255
|
-
return sidebarSelectedFilters["shipping-free"]
|
|
67
|
+
return sidebarSelectedFilters["shipping-free"] !== undefined;
|
|
256
68
|
}
|
|
257
|
-
//
|
|
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.
|
|
258
71
|
return sidebarSelectedFilters.properties.size > 0;
|
|
259
72
|
};
|
|
260
73
|
</script>
|
|
@@ -12,13 +12,13 @@ const props = defineProps<{
|
|
|
12
12
|
content: Schemas["CmsBlock"];
|
|
13
13
|
}>();
|
|
14
14
|
|
|
15
|
-
const { getSlotContent } = useCmsBlock(props.content);
|
|
16
|
-
const slotContent = getSlotContent("default");
|
|
15
|
+
const { getSlotContent } = useCmsBlock(() => props.content);
|
|
16
|
+
const slotContent = computed(() => getSlotContent("default"));
|
|
17
17
|
|
|
18
18
|
function getConfigValue(key: string): unknown {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
19
|
+
const slot = slotContent.value;
|
|
20
|
+
if (!slot?.config) return null;
|
|
21
|
+
const configEntry = slot.config[key as keyof typeof slot.config];
|
|
22
22
|
if (
|
|
23
23
|
configEntry &&
|
|
24
24
|
typeof configEntry === "object" &&
|
|
@@ -31,8 +31,8 @@ function getConfigValue(key: string): unknown {
|
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
const modelUrl = computed(() => {
|
|
34
|
-
if (slotContent?.data) {
|
|
35
|
-
const data = slotContent.data as unknown as Schemas["Media"];
|
|
34
|
+
if (slotContent.value?.data) {
|
|
35
|
+
const data = slotContent.value.data as unknown as Schemas["Media"];
|
|
36
36
|
if (data?.url && typeof data.url === "string") {
|
|
37
37
|
return data.url;
|
|
38
38
|
}
|
|
@@ -17,22 +17,17 @@ const props = defineProps<{
|
|
|
17
17
|
|
|
18
18
|
const appConfig = useTypedAppConfig();
|
|
19
19
|
|
|
20
|
+
// Plain values, not refs: custom elements inject these as a `number` and a
|
|
21
|
+
// `string` (see the README), so providing a ref here would break them.
|
|
20
22
|
const slotCount = props.content.slots?.length || 1;
|
|
21
23
|
provide("cms-block-slot-count", slotCount);
|
|
22
24
|
provide("cms-image-sizes", getImageSizes(slotCount, appConfig.imageSizes));
|
|
23
25
|
|
|
24
26
|
const DynamicRender = () => {
|
|
25
|
-
const {
|
|
26
|
-
|
|
27
|
-
componentName,
|
|
28
|
-
isResolved,
|
|
29
|
-
componentNameToResolve,
|
|
30
|
-
} = resolveCmsComponent(props.content);
|
|
27
|
+
const { resolvedComponent, componentName, componentNameToResolve } =
|
|
28
|
+
resolveCmsComponent(props.content);
|
|
31
29
|
|
|
32
30
|
if (resolvedComponent) {
|
|
33
|
-
if (!isResolved)
|
|
34
|
-
return h("div", {}, `Problem resolving component: ${componentName}`);
|
|
35
|
-
|
|
36
31
|
const { cssClasses, layoutStyles } = getCmsLayoutConfiguration(
|
|
37
32
|
props.content,
|
|
38
33
|
);
|
|
@@ -75,7 +70,8 @@ const DynamicRender = () => {
|
|
|
75
70
|
);
|
|
76
71
|
return h(resolveComponent("CmsNoComponent"), { content: props.content });
|
|
77
72
|
}
|
|
78
|
-
|
|
73
|
+
// Production: a block type with no component renders nothing.
|
|
74
|
+
return null;
|
|
79
75
|
};
|
|
80
76
|
</script>
|
|
81
77
|
|
|
@@ -1,19 +1,25 @@
|
|
|
1
|
-
Renders an Element type structure
|
|
1
|
+
Renders an Element type structure.
|
|
2
|
+
|
|
3
|
+
`content` is optional: `getSlotContent()` returns `undefined` for a slot the block
|
|
4
|
+
does not carry, and this component renders nothing in that case.
|
|
2
5
|
|
|
3
6
|
Example usage:
|
|
4
7
|
|
|
5
|
-
```vue{
|
|
8
|
+
```vue{22,25}
|
|
6
9
|
<script setup lang="ts">
|
|
7
10
|
import type { CmsBlockGalleryBuybox } from "@shopware/composables";
|
|
11
|
+
import { computed } from "vue";
|
|
8
12
|
import { useCmsBlock } from "#imports";
|
|
9
13
|
|
|
10
14
|
const props = defineProps<{
|
|
11
15
|
content: CmsBlockGalleryBuybox;
|
|
12
16
|
}>();
|
|
13
17
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
const
|
|
18
|
+
// Pass a getter so the lookups follow a replaced block, and read them through
|
|
19
|
+
// computeds so each one re-runs when it does.
|
|
20
|
+
const { getSlotContent } = useCmsBlock(() => props.content);
|
|
21
|
+
const rightContent = computed(() => getSlotContent("right"));
|
|
22
|
+
const leftContent = computed(() => getSlotContent("left"));
|
|
17
23
|
</script>
|
|
18
24
|
|
|
19
25
|
<template>
|
|
@@ -6,20 +6,24 @@ import { h, resolveComponent } from "vue";
|
|
|
6
6
|
import type { Schemas } from "#shopware";
|
|
7
7
|
|
|
8
8
|
const props = defineProps<{
|
|
9
|
-
|
|
9
|
+
/**
|
|
10
|
+
* A block asks for its slots by name, and a block does not have to carry
|
|
11
|
+
* every slot its layout allows, so this can be undefined.
|
|
12
|
+
*/
|
|
13
|
+
content?: Schemas["CmsSlot"];
|
|
10
14
|
}>();
|
|
11
15
|
|
|
12
16
|
const DynamicRender = () => {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
if (resolvedComponent) {
|
|
20
|
-
if (!isResolved)
|
|
21
|
-
return h("div", {}, `Problem resolving component: ${componentName}`);
|
|
17
|
+
// Nothing to render for a slot the block does not carry. An element would
|
|
18
|
+
// take the class the call site passes and occupy a cell of its own wherever a
|
|
19
|
+
// block renders this straight into a grid, as CmsBlockCenterText does. A
|
|
20
|
+
// wrapper the block puts around the call site renders either way — sizing
|
|
21
|
+
// that wrapper for an absent slot is the block's business, not this one's.
|
|
22
|
+
if (!props.content) return null;
|
|
22
23
|
|
|
24
|
+
const { resolvedComponent, componentName, componentNameToResolve } =
|
|
25
|
+
resolveCmsComponent(props.content);
|
|
26
|
+
if (resolvedComponent) {
|
|
23
27
|
const { cssClasses, layoutStyles } = getCmsLayoutConfiguration(
|
|
24
28
|
props.content,
|
|
25
29
|
);
|
|
@@ -35,7 +39,9 @@ const DynamicRender = () => {
|
|
|
35
39
|
);
|
|
36
40
|
return h(resolveComponent("CmsNoComponent"), { content: props.content });
|
|
37
41
|
}
|
|
38
|
-
|
|
42
|
+
// Production: an element type with no component renders nothing, for the same
|
|
43
|
+
// reason the missing slot above does.
|
|
44
|
+
return null;
|
|
39
45
|
};
|
|
40
46
|
</script>
|
|
41
47
|
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { CmsBlockAppRenderer } from "@shopware/composables";
|
|
3
|
+
import { computed, getCurrentInstance } from "vue";
|
|
4
|
+
import type { Component, CSSProperties } from "vue";
|
|
5
|
+
|
|
6
|
+
import { getAppRendererLayout } from "../../../../helpers/cms/getAppRendererLayout";
|
|
7
|
+
|
|
8
|
+
const props = defineProps<{
|
|
9
|
+
content: CmsBlockAppRenderer;
|
|
10
|
+
}>();
|
|
11
|
+
|
|
12
|
+
const appContext = getCurrentInstance()?.appContext;
|
|
13
|
+
|
|
14
|
+
const layout = computed(() => getAppRendererLayout(props.content));
|
|
15
|
+
|
|
16
|
+
const appBlockComponent = computed<Component | undefined>(() => {
|
|
17
|
+
const name = layout.value.componentName;
|
|
18
|
+
|
|
19
|
+
return name ? appContext?.components[name] : undefined;
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
const gridStyle = computed<CSSProperties>(() => ({
|
|
23
|
+
display: "grid",
|
|
24
|
+
grid: layout.value.grid,
|
|
25
|
+
}));
|
|
26
|
+
</script>
|
|
27
|
+
<template>
|
|
28
|
+
<component
|
|
29
|
+
:is="appBlockComponent"
|
|
30
|
+
v-if="appBlockComponent"
|
|
31
|
+
:content="content"
|
|
32
|
+
/>
|
|
33
|
+
<div
|
|
34
|
+
v-else
|
|
35
|
+
class="cms-block-app-renderer"
|
|
36
|
+
:class="`cms-block-app-renderer-fallback-block-${layout.appBlockName}`"
|
|
37
|
+
:style="gridStyle"
|
|
38
|
+
>
|
|
39
|
+
<div
|
|
40
|
+
v-for="slot in layout.slots"
|
|
41
|
+
:key="slot.id"
|
|
42
|
+
class="min-w-0"
|
|
43
|
+
:class="`cms-block-app-renderer-fallback-slot-${slot.type}`"
|
|
44
|
+
>
|
|
45
|
+
<CmsGenericElement :content="slot" />
|
|
46
|
+
</div>
|
|
47
|
+
</div>
|
|
48
|
+
</template>
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { CmsBlockCategoryHeading } from "@shopware/composables";
|
|
3
|
+
import { computed } from "vue";
|
|
4
|
+
|
|
5
|
+
import { useCmsBlock } from "#imports";
|
|
6
|
+
|
|
7
|
+
const props = defineProps<{
|
|
8
|
+
content: CmsBlockCategoryHeading;
|
|
9
|
+
}>();
|
|
10
|
+
|
|
11
|
+
const { getSlotContent } = useCmsBlock(() => props.content);
|
|
12
|
+
|
|
13
|
+
const slotContent = computed(() => getSlotContent("content"));
|
|
14
|
+
</script>
|
|
15
|
+
<template>
|
|
16
|
+
<div class="cms-block-category-heading">
|
|
17
|
+
<CmsGenericElement :content="slotContent" />
|
|
18
|
+
</div>
|
|
19
|
+
</template>
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import type { CmsBlockCategoryNavigation } from "@shopware/composables";
|
|
3
|
+
import { computed } from "vue";
|
|
3
4
|
|
|
4
5
|
import { useCmsBlock } from "#imports";
|
|
5
6
|
|
|
6
7
|
const props = defineProps<{
|
|
7
8
|
content: CmsBlockCategoryNavigation;
|
|
8
9
|
}>();
|
|
9
|
-
const { getSlotContent } = useCmsBlock(props.content);
|
|
10
|
-
const slotContent = getSlotContent("content");
|
|
10
|
+
const { getSlotContent } = useCmsBlock(() => props.content);
|
|
11
|
+
const slotContent = computed(() => getSlotContent("content"));
|
|
11
12
|
</script>
|
|
12
13
|
|
|
13
14
|
<template>
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import type { CmsBlockCenterText } from "@shopware/composables";
|
|
3
|
+
import { computed } from "vue";
|
|
3
4
|
|
|
4
5
|
import { useCmsBlock } from "#imports";
|
|
5
6
|
|
|
@@ -7,10 +8,10 @@ const props = defineProps<{
|
|
|
7
8
|
content: CmsBlockCenterText;
|
|
8
9
|
}>();
|
|
9
10
|
|
|
10
|
-
const { getSlotContent } = useCmsBlock(props.content);
|
|
11
|
-
const slotLeftContent = getSlotContent("left");
|
|
12
|
-
const slotRightContent = getSlotContent("right");
|
|
13
|
-
const slotCenterContent = getSlotContent("center");
|
|
11
|
+
const { getSlotContent } = useCmsBlock(() => props.content);
|
|
12
|
+
const slotLeftContent = computed(() => getSlotContent("left"));
|
|
13
|
+
const slotRightContent = computed(() => getSlotContent("right"));
|
|
14
|
+
const slotCenterContent = computed(() => getSlotContent("center"));
|
|
14
15
|
</script>
|
|
15
16
|
<template>
|
|
16
17
|
<div class="cms-block-center-text grid md:grid-cols-3 gap-10 content-center">
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import type { CmsBlockCrossSelling } from "@shopware/composables";
|
|
3
|
+
import { computed } from "vue";
|
|
3
4
|
|
|
4
5
|
import { useCmsBlock } from "#imports";
|
|
5
6
|
|
|
6
7
|
const props = defineProps<{
|
|
7
8
|
content: CmsBlockCrossSelling;
|
|
8
9
|
}>();
|
|
9
|
-
const { getSlotContent } = useCmsBlock(props.content);
|
|
10
|
-
const slotContent = getSlotContent("content");
|
|
10
|
+
const { getSlotContent } = useCmsBlock(() => props.content);
|
|
11
|
+
const slotContent = computed(() => getSlotContent("content"));
|
|
11
12
|
</script>
|
|
12
13
|
<template>
|
|
13
14
|
<div class="cms-block-cross-selling">
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import type { CmsBlockForm } from "@shopware/composables";
|
|
3
|
+
import { computed } from "vue";
|
|
3
4
|
|
|
4
5
|
import { useCmsBlock } from "#imports";
|
|
5
6
|
|
|
@@ -7,9 +8,9 @@ const props = defineProps<{
|
|
|
7
8
|
content: CmsBlockForm;
|
|
8
9
|
}>();
|
|
9
10
|
|
|
10
|
-
const { getSlotContent } = useCmsBlock(props.content);
|
|
11
|
+
const { getSlotContent } = useCmsBlock(() => props.content);
|
|
11
12
|
|
|
12
|
-
const cmsContent = getSlotContent("content");
|
|
13
|
+
const cmsContent = computed(() => getSlotContent("content"));
|
|
13
14
|
</script>
|
|
14
15
|
<template>
|
|
15
16
|
<div class="cms-block-form">
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import type { CmsBlockForm } from "@shopware/composables";
|
|
3
|
+
import { computed } from "vue";
|
|
3
4
|
|
|
4
5
|
import { useCmsBlock } from "#imports";
|
|
5
6
|
|
|
@@ -7,9 +8,9 @@ const props = defineProps<{
|
|
|
7
8
|
content: CmsBlockForm;
|
|
8
9
|
}>();
|
|
9
10
|
|
|
10
|
-
const { getSlotContent } = useCmsBlock(props.content);
|
|
11
|
+
const { getSlotContent } = useCmsBlock(() => props.content);
|
|
11
12
|
|
|
12
|
-
const cmsContent = getSlotContent("content");
|
|
13
|
+
const cmsContent = computed(() => getSlotContent("content"));
|
|
13
14
|
</script>
|
|
14
15
|
<template>
|
|
15
16
|
<div class="cms-block-form">
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import type { CmsBlockGalleryBuybox } from "@shopware/composables";
|
|
3
|
+
import { computed } from "vue";
|
|
3
4
|
|
|
4
5
|
import { useCmsBlock } from "#imports";
|
|
5
6
|
|
|
@@ -7,9 +8,9 @@ const props = defineProps<{
|
|
|
7
8
|
content: CmsBlockGalleryBuybox;
|
|
8
9
|
}>();
|
|
9
10
|
|
|
10
|
-
const { getSlotContent } = useCmsBlock(props.content);
|
|
11
|
-
const rightContent = getSlotContent("right");
|
|
12
|
-
const leftContent = getSlotContent("left");
|
|
11
|
+
const { getSlotContent } = useCmsBlock(() => props.content);
|
|
12
|
+
const rightContent = computed(() => getSlotContent("right"));
|
|
13
|
+
const leftContent = computed(() => getSlotContent("left"));
|
|
13
14
|
</script>
|
|
14
15
|
|
|
15
16
|
<template>
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import type { CmsBlockImage } from "@shopware/composables";
|
|
3
|
+
import { computed } from "vue";
|
|
3
4
|
|
|
4
5
|
import { useCmsBlock } from "#imports";
|
|
5
6
|
|
|
@@ -7,8 +8,8 @@ const props = defineProps<{
|
|
|
7
8
|
content: CmsBlockImage;
|
|
8
9
|
}>();
|
|
9
10
|
|
|
10
|
-
const { getSlotContent } = useCmsBlock(props.content);
|
|
11
|
-
const imageContent = getSlotContent("image");
|
|
11
|
+
const { getSlotContent } = useCmsBlock(() => props.content);
|
|
12
|
+
const imageContent = computed(() => getSlotContent("image"));
|
|
12
13
|
</script>
|
|
13
14
|
<template>
|
|
14
15
|
<div>
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import type { CmsBlockImageBubbleRow } from "@shopware/composables";
|
|
3
|
+
import { computed } from "vue";
|
|
3
4
|
|
|
4
5
|
import { useCmsBlock } from "#imports";
|
|
5
6
|
|
|
@@ -7,11 +8,11 @@ const props = defineProps<{
|
|
|
7
8
|
content: CmsBlockImageBubbleRow;
|
|
8
9
|
}>();
|
|
9
10
|
|
|
10
|
-
const { getSlotContent } = useCmsBlock(props.content);
|
|
11
|
+
const { getSlotContent } = useCmsBlock(() => props.content);
|
|
11
12
|
|
|
12
|
-
const leftContent = getSlotContent("left");
|
|
13
|
-
const rightContent = getSlotContent("right");
|
|
14
|
-
const centerContent = getSlotContent("center");
|
|
13
|
+
const leftContent = computed(() => getSlotContent("left"));
|
|
14
|
+
const rightContent = computed(() => getSlotContent("right"));
|
|
15
|
+
const centerContent = computed(() => getSlotContent("center"));
|
|
15
16
|
</script>
|
|
16
17
|
<template>
|
|
17
18
|
<div
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import type { CmsBlockImageCover } from "@shopware/composables";
|
|
3
|
+
import { computed } from "vue";
|
|
3
4
|
|
|
4
5
|
import { useCmsBlock } from "#imports";
|
|
5
6
|
|
|
@@ -7,9 +8,9 @@ const props = defineProps<{
|
|
|
7
8
|
content: CmsBlockImageCover;
|
|
8
9
|
}>();
|
|
9
10
|
|
|
10
|
-
const { getSlotContent } = useCmsBlock(props.content);
|
|
11
|
+
const { getSlotContent } = useCmsBlock(() => props.content);
|
|
11
12
|
|
|
12
|
-
const cmsContent = getSlotContent("image");
|
|
13
|
+
const cmsContent = computed(() => getSlotContent("image"));
|
|
13
14
|
</script>
|
|
14
15
|
<template>
|
|
15
16
|
<div class="cms-block-image-cover">
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import type { CmsBlockImageFourColumn } from "@shopware/composables";
|
|
3
|
+
import { computed } from "vue";
|
|
3
4
|
|
|
4
5
|
import { useCmsBlock } from "#imports";
|
|
5
6
|
|
|
@@ -7,12 +8,12 @@ const props = defineProps<{
|
|
|
7
8
|
content: CmsBlockImageFourColumn;
|
|
8
9
|
}>();
|
|
9
10
|
|
|
10
|
-
const { getSlotContent } = useCmsBlock(props.content);
|
|
11
|
+
const { getSlotContent } = useCmsBlock(() => props.content);
|
|
11
12
|
|
|
12
|
-
const leftContent = getSlotContent("left");
|
|
13
|
-
const rightContent = getSlotContent("right");
|
|
14
|
-
const centerLeftContent = getSlotContent("center-left");
|
|
15
|
-
const centerRightContent = getSlotContent("center-right");
|
|
13
|
+
const leftContent = computed(() => getSlotContent("left"));
|
|
14
|
+
const rightContent = computed(() => getSlotContent("right"));
|
|
15
|
+
const centerLeftContent = computed(() => getSlotContent("center-left"));
|
|
16
|
+
const centerRightContent = computed(() => getSlotContent("center-right"));
|
|
16
17
|
</script>
|
|
17
18
|
<template>
|
|
18
19
|
<div
|