@shopware/cms-base-layer 3.1.0 → 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 +64 -23
- 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/SwProductCardImage.vue +5 -1
- package/app/components/SwProductListingFilter.vue +5 -0
- package/app/components/SwProductListingFilters.vue +40 -4
- package/app/components/SwProductListingFiltersHorizontal.vue +37 -2
- 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/FrontendAccountCustomerGroupRegistrationPage.vue +1 -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 -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/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/useSelectedListingFilters.test.ts +13 -3
- package/app/utils/useSelectedListingFilters.ts +7 -1
- package/component-dirs.json +20 -0
- package/index.d.ts +2 -2
- package/nuxt.config.ts +11 -28
- package/package.json +20 -23
- 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
package/README.md
CHANGED
|
@@ -192,10 +192,18 @@ Replace standard `<img>` tags with `<NuxtImg>` to enable automatic optimization:
|
|
|
192
192
|
:src="product.cover.media.url"
|
|
193
193
|
preset="productDetail"
|
|
194
194
|
:width="800"
|
|
195
|
-
:alt="product.cover.media
|
|
195
|
+
:alt="getTranslatedProperty(product.cover.media, 'alt')"
|
|
196
196
|
/>
|
|
197
197
|
```
|
|
198
198
|
|
|
199
|
+
The media `alt` text is a translatable field, so read it through `getTranslatedProperty` rather than
|
|
200
|
+
`media.alt` — the latter always holds the system language value, no matter which language the current
|
|
201
|
+
request asks for. The helper is not auto-imported, so add it to the component's script block:
|
|
202
|
+
|
|
203
|
+
```ts
|
|
204
|
+
import { getTranslatedProperty } from "@shopware/helpers";
|
|
205
|
+
```
|
|
206
|
+
|
|
199
207
|
### Supported Modifiers
|
|
200
208
|
|
|
201
209
|
Shopware supports the following URL parameters for image transformation:
|
|
@@ -350,7 +358,7 @@ url("https://cdn.shopware.store/.../image.jpg?width=1000&fit=crop,smart&format=w
|
|
|
350
358
|
|
|
351
359
|
## LCP Image Preload
|
|
352
360
|
|
|
353
|
-
This layer includes a `useLcpImagePreload` composable that
|
|
361
|
+
This layer includes a `useLcpImagePreload` composable that preloads the first image found in CMS page content when enabled via `lcpImagePreload: true` in your `app.config.ts` (disabled by default). This targets the [Largest Contentful Paint (LCP)](https://web.dev/lcp/) element, which is often a hero background image or the first visible image element.
|
|
354
362
|
|
|
355
363
|
### How it works
|
|
356
364
|
|
|
@@ -364,12 +372,10 @@ The first image found is injected as a `<link rel="preload" as="image" fetchprio
|
|
|
364
372
|
|
|
365
373
|
### Usage
|
|
366
374
|
|
|
367
|
-
The composable is already called in `CmsPage.vue`. If you override `CmsPage`, you can use it in your custom component:
|
|
375
|
+
The composable is already called in `CmsPage.vue`. If you override `CmsPage`, you can use it in your custom component. It is auto-imported once your app extends this layer, so it needs no import statement:
|
|
368
376
|
|
|
369
377
|
```vue
|
|
370
378
|
<script setup>
|
|
371
|
-
import { useLcpImagePreload } from "@shopware/cms-base-layer/composables/useLcpImagePreload";
|
|
372
|
-
|
|
373
379
|
const props = defineProps<{ content: Schemas["CmsPage"] }>();
|
|
374
380
|
|
|
375
381
|
useLcpImagePreload(props.content?.sections || []);
|
|
@@ -384,7 +390,7 @@ Images are optimized to prevent the browser from downloading images larger than
|
|
|
384
390
|
|
|
385
391
|
### Product Card Images (`SwProductCardImage`)
|
|
386
392
|
|
|
387
|
-
The `productCard` preset only defines URL modifiers (format/quality/fit). `width`/`height
|
|
393
|
+
The `productCard` preset only defines URL modifiers (format/quality/fit). `width`/`height` and `loading` stay on the component: in `@nuxt/image` 2.1.0 a preset carries `width`/`height` only as `modifiers`, which shape the URL rather than the rendered attributes, and `loading` is not a preset field at all. `densities` is kept alongside them for consistency, though a preset would propagate it:
|
|
388
394
|
|
|
389
395
|
```ts
|
|
390
396
|
// nuxt.config.ts
|
|
@@ -448,9 +454,9 @@ The list of available blocks and elements is [here](https://developer.shopware.c
|
|
|
448
454
|
|
|
449
455
|
The procedure is:
|
|
450
456
|
|
|
451
|
-
- find a component in component's [list](https://developer.shopware.com/frontends/packages/cms-base.html#available-components), using a [Vue devtools](https://devtools.vuejs.org/) or browsing the github [repository](https://github.com/shopware/frontends/tree/main/packages/cms-base-layer/app/components)
|
|
457
|
+
- find a component in component's [list](https://developer.shopware.com/frontends/packages/cms-base-layer.html#available-components), using a [Vue devtools](https://devtools.vuejs.org/) or browsing the github [repository](https://github.com/shopware/frontends/tree/main/packages/cms-base-layer/app/components)
|
|
452
458
|
- take its name
|
|
453
|
-
- create a file with the same name and place it
|
|
459
|
+
- create a file with the same name and place it under a `components` directory that your `nuxt.config.ts` registers with `global: true` — CMS components are looked up with `resolveComponent`, so an override outside a global path is never found and this layer's version keeps rendering with no error. In the starter template that directory is `app/components/cms/`; because it is registered with `pathPrefix: false`, the name comes from the filename alone and subdirectory depth under it does not matter.
|
|
454
460
|
|
|
455
461
|
✅ Thanks to this, nuxt will take the component registered in your app instead of the one registered by this nuxt layer.
|
|
456
462
|
|
|
@@ -485,31 +491,66 @@ No additional packages needed to be installed.
|
|
|
485
491
|
|
|
486
492
|
Full changelog for stable version is available [here](https://github.com/shopware/frontends/blob/main/packages/cms-base-layer/CHANGELOG.md)
|
|
487
493
|
|
|
488
|
-
### Latest changes:
|
|
494
|
+
### Latest changes: 4.0.0
|
|
495
|
+
|
|
496
|
+
### Major Changes
|
|
497
|
+
|
|
498
|
+
- [#2609](https://github.com/shopware/frontends/pull/2609) [`2dc86da`](https://github.com/shopware/frontends/commit/2dc86da2532e986ebdadd06a922e68403414a0ff) Thanks [@mkucmus](https://github.com/mkucmus)! - Make the package a plain Nuxt layer.
|
|
499
|
+
|
|
500
|
+
**Breaking:** the package now declares an `exports` map, so only the layer entry point
|
|
501
|
+
is importable. Deep imports such as
|
|
502
|
+
`@shopware/cms-base-layer/app/components/SwProductCard.vue` no longer resolve. Use the
|
|
503
|
+
auto-registered global components instead, which is what extending the layer gives you.
|
|
504
|
+
|
|
505
|
+
**Breaking:** the leftover Nuxt module is gone, so `index.cjs` and the `dist` build are
|
|
506
|
+
no longer published. The module registered a component folder that moved to `app/` a
|
|
507
|
+
while ago, so it could not work any more. Consume this package with `extends`, which
|
|
508
|
+
resolves `nuxt.config.ts`:
|
|
509
|
+
|
|
510
|
+
```ts
|
|
511
|
+
export default defineNuxtConfig({
|
|
512
|
+
extends: ["@shopware/cms-base-layer"],
|
|
513
|
+
});
|
|
514
|
+
```
|
|
515
|
+
|
|
516
|
+
Other changes:
|
|
517
|
+
|
|
518
|
+
- Dropped the `build` and `dev` scripts and the `unbuild` dev dependency. There is
|
|
519
|
+
nothing left to bundle.
|
|
520
|
+
- Fixed `files`. It listed `helpers` and `app.config.ts`, which do not exist at the
|
|
521
|
+
package root, and shipped the dead `dist` and `index.cjs`.
|
|
522
|
+
- Deleted `components.d.ts`. It typed two components at paths that moved to
|
|
523
|
+
`app/components/public/cms/`, and no tsconfig referenced the file.
|
|
524
|
+
- Removed two vitest aliases that pointed at files which do not exist.
|
|
525
|
+
- Removed the `check-colors` script. It read `uno.config.ts` from this package, which
|
|
526
|
+
moved to `@shopware/unocss-design-tokens-layer`, so it always found zero colors. It
|
|
527
|
+
also relied on `tsx` without declaring it.
|
|
489
528
|
|
|
490
529
|
### Minor Changes
|
|
491
530
|
|
|
492
|
-
- [#
|
|
531
|
+
- [#2574](https://github.com/shopware/frontends/pull/2574) [`2ddf156`](https://github.com/shopware/frontends/commit/2ddf156805b2941fe2069e78453fb3c4eb6d44ac) Thanks [@mkucmus](https://github.com/mkucmus)! - `SwProductListingFilters` and `SwProductListingFiltersHorizontal` render a new `SwFilterCategories` checkbox filter when the listing response contains the category aggregations from `getCategoryFilterAggregations()`. The selection is kept in the `categories` URL param and applied as a criteria `post-filter`. Both are search listings only (`listing-type="productSearchListing"`); on any other listing the category filter is not rendered, because the selection would neither reach the URL nor the criteria. Listings without these aggregations are unaffected.
|
|
493
532
|
|
|
494
533
|
### Patch Changes
|
|
495
534
|
|
|
496
|
-
- [#
|
|
535
|
+
- [#2645](https://github.com/shopware/frontends/pull/2645) [`7a7ce3e`](https://github.com/shopware/frontends/commit/7a7ce3e5649e7ab84f7e547ce1ebcd9413117dde) Thanks [@mkucmus](https://github.com/mkucmus)! - Ship `component-dirs.json` in the published package. `nuxt.config.ts` imports it at load time, but it was missing from `files`, so the layer failed to load when installed from the registry.
|
|
536
|
+
|
|
537
|
+
- [#2660](https://github.com/shopware/frontends/pull/2660) [`8913956`](https://github.com/shopware/frontends/commit/89139563924163e57cafdd9770fe603f2dbd8cba) Thanks [@mdanilowicz](https://github.com/mdanilowicz)! - Read translatable entity fields through their `translated` object instead of the untranslated entity root, so they follow the language of the current request instead of rendering the system language. Affected components: `SwContactForm` and `SwNewsletterForm` (salutation `displayName`), `SwStockInfo` (delivery time `name`), `SwVariantConfigurator` (property group `name` — the option values below it already used `translated`), `CmsElementBuyBox` (unit `name`), `CmsElementCrossSelling` (cross selling `name`), `CmsElementProductDescriptionReviews` (category `name`), `SwProductCardImage` and `CmsElementImageGallery` (media `alt`).
|
|
497
538
|
|
|
498
|
-
|
|
539
|
+
All of them now go through `getTranslatedProperty()` from `@shopware/helpers`, which falls back to the root property when `translated` is missing — so the rendered value only ever changes on storefronts whose language differs from the system language.
|
|
499
540
|
|
|
500
|
-
|
|
501
|
-
- `useProductReviews.loadProductReviews` now calls `GET /product/{productId}/reviews` and sends its encoded Criteria in `_criteria` when cacheable reads are enabled.
|
|
502
|
-
- The CMS product description reviews element follows the same flag and endpoint behavior when it needs to fetch reviews directly.
|
|
541
|
+
- [#2606](https://github.com/shopware/frontends/pull/2606) [`1c9a604`](https://github.com/shopware/frontends/commit/1c9a604e05c0fa1b27708a5338cd5690a0ff5c67) Thanks [@mkucmus](https://github.com/mkucmus)! - Stop leaking internal types into the layer's public type surface.
|
|
503
542
|
|
|
504
|
-
-
|
|
543
|
+
- `index.d.ts` now only augments the app config. It no longer re-exports `@shopware/composables` and `./.nuxt/imports`.
|
|
544
|
+
- The `#imports` shim moved to a private `types/imports.d.ts`, which is not published.
|
|
545
|
+
- `app.config` exports a plain object with `satisfies AppConfigInput` instead of calling `defineAppConfig`. No runtime change.
|
|
505
546
|
|
|
506
|
-
|
|
547
|
+
This fixes `Cannot find name 'ref'` and similar errors in the shared and node contexts when a project uses the Nuxt 4 project-references `tsconfig.json`.
|
|
507
548
|
|
|
508
|
-
- [#
|
|
549
|
+
- [#2601](https://github.com/shopware/frontends/pull/2601) [`e64d2f9`](https://github.com/shopware/frontends/commit/e64d2f9c13a99054d7e56e4b7ec2f1f23f9768b7) Thanks [@mdanilowicz](https://github.com/mdanilowicz)! - Typography for CMS-authored HTML is now scoped to a `cms-element-text` class and ships from the layer as `app/assets/css/rich-text.css`, with overflow guards for long strings, media, `pre` and tables. Headings and lists in your own markup that had no explicit size or `list-*` class were relying on the removed global rules and will change.
|
|
509
550
|
|
|
510
|
-
- [#
|
|
551
|
+
- [#2663](https://github.com/shopware/frontends/pull/2663) [`7020545`](https://github.com/shopware/frontends/commit/70205458cb9357a068029d0aaef41898ab94b354) Thanks [@mkucmus](https://github.com/mkucmus)! - A linked CMS image always has an accessible name now. `CmsElementImage` names the link with the element's `ariaLabel`, then the media title, then a generic fallback translatable through `cms.image.linkWithoutLabel`. A decorative image skips the media title.
|
|
511
552
|
|
|
512
|
-
- Updated dependencies [[`
|
|
513
|
-
- @shopware/
|
|
514
|
-
- @shopware/
|
|
515
|
-
- @shopware/
|
|
553
|
+
- Updated dependencies [[`2ddf156`](https://github.com/shopware/frontends/commit/2ddf156805b2941fe2069e78453fb3c4eb6d44ac), [`204c8f4`](https://github.com/shopware/frontends/commit/204c8f45f737e724db6d00b80c5faef8ddb77cb4), [`7020545`](https://github.com/shopware/frontends/commit/70205458cb9357a068029d0aaef41898ab94b354), [`183c183`](https://github.com/shopware/frontends/commit/183c183f905486c27fa770fd0f4cd9993e86c20e), [`458494e`](https://github.com/shopware/frontends/commit/458494e8bd2be88d4fbf161636a109c8f4efc443), [`183c183`](https://github.com/shopware/frontends/commit/183c183f905486c27fa770fd0f4cd9993e86c20e), [`183c183`](https://github.com/shopware/frontends/commit/183c183f905486c27fa770fd0f4cd9993e86c20e), [`8913956`](https://github.com/shopware/frontends/commit/89139563924163e57cafdd9770fe603f2dbd8cba), [`458494e`](https://github.com/shopware/frontends/commit/458494e8bd2be88d4fbf161636a109c8f4efc443)]:
|
|
554
|
+
- @shopware/helpers@1.8.0
|
|
555
|
+
- @shopware/composables@1.13.0
|
|
556
|
+
- @shopware/api-client@1.6.0
|
package/app/app.config.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { AppConfigInput } from "nuxt/schema";
|
|
2
2
|
|
|
3
|
-
export default
|
|
3
|
+
export default {
|
|
4
4
|
imagePlaceholder: {
|
|
5
5
|
color: "#543B95",
|
|
6
6
|
},
|
|
@@ -16,4 +16,4 @@ export default defineAppConfig({
|
|
|
16
16
|
default: "(max-width: 768px) 50vw, 25vw",
|
|
17
17
|
},
|
|
18
18
|
unocssRuntime: true,
|
|
19
|
-
}
|
|
19
|
+
} satisfies AppConfigInput;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
.cms-element-text {
|
|
2
|
+
word-break: break-word;
|
|
3
|
+
|
|
4
|
+
:where(h1, h2, h3, h4, h5, h6) {
|
|
5
|
+
margin-bottom: var(--rte-heading-mb, 10px);
|
|
6
|
+
font-weight: var(--rte-heading-weight, 600);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
:where(h1) {
|
|
10
|
+
font-size: var(--rte-h1-size, 2.25rem);
|
|
11
|
+
line-height: var(--rte-h1-line, 2.5rem);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
:where(h2) {
|
|
15
|
+
font-size: var(--rte-h2-size, 1.75rem);
|
|
16
|
+
line-height: var(--rte-h2-line, 2rem);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
:where(h3) {
|
|
20
|
+
font-size: var(--rte-h3-size, 1.25rem);
|
|
21
|
+
line-height: var(--rte-h3-line, 1.5rem);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
:where(h4, h5, h6) {
|
|
25
|
+
font-size: var(--rte-h4-size, 1.125rem);
|
|
26
|
+
line-height: var(--rte-h4-line, 1.5rem);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
:where(p, blockquote, pre) {
|
|
30
|
+
margin-bottom: var(--rte-flow, 1rem);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
:where(ol, ul, dl) {
|
|
34
|
+
list-style-type: disc;
|
|
35
|
+
padding-left: var(--rte-list-indent, 40px);
|
|
36
|
+
margin-top: 0;
|
|
37
|
+
margin-bottom: var(--rte-flow, 1rem);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
:where(ol) {
|
|
41
|
+
list-style-type: decimal;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
:where(img, iframe, video) {
|
|
45
|
+
max-width: 100%;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
:where(img) {
|
|
49
|
+
height: auto;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
:where(table) {
|
|
53
|
+
width: 100%;
|
|
54
|
+
margin-bottom: var(--rte-flow, 1rem);
|
|
55
|
+
border-collapse: collapse;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
:where(th, td) {
|
|
59
|
+
padding: var(--rte-cell-padding, 0.5rem 0.75rem);
|
|
60
|
+
border: 1px solid var(--rte-border-color, #e5e7eb);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
:where(th, thead td) {
|
|
64
|
+
text-align: left;
|
|
65
|
+
font-weight: var(--rte-heading-weight, 600);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
:where(hr) {
|
|
69
|
+
margin: var(--rte-flow, 1rem) 0;
|
|
70
|
+
border-top: 1px solid var(--rte-border-color, #e5e7eb);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
> :last-child {
|
|
74
|
+
margin-bottom: 0;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
:where(.sw-text-editor-table__col-selector) {
|
|
78
|
+
display: none;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
@@ -3,6 +3,7 @@ import { ApiClientError } from "@shopware/api-client";
|
|
|
3
3
|
import type { ApiError } from "@shopware/api-client";
|
|
4
4
|
import type { CmsElementForm } from "@shopware/composables";
|
|
5
5
|
import { useCmsTranslations } from "@shopware/composables";
|
|
6
|
+
import { getTranslatedProperty } from "@shopware/helpers";
|
|
6
7
|
import { useVuelidate } from "@vuelidate/core";
|
|
7
8
|
import { email, minLength, required } from "@vuelidate/validators";
|
|
8
9
|
import { defu } from "defu";
|
|
@@ -189,7 +190,7 @@ const invokeSubmit = async () => {
|
|
|
189
190
|
:key="salutation.id"
|
|
190
191
|
:value="salutation.id"
|
|
191
192
|
>
|
|
192
|
-
{{ salutation
|
|
193
|
+
{{ getTranslatedProperty(salutation, "displayName") }}
|
|
193
194
|
</option>
|
|
194
195
|
</select>
|
|
195
196
|
</div>
|
|
@@ -6,6 +6,9 @@ import type { Schemas } from "#shopware";
|
|
|
6
6
|
type FilterState = {
|
|
7
7
|
manufacturer: Set<string>;
|
|
8
8
|
properties: Set<string>;
|
|
9
|
+
// Optional: consumers building their own filter-state object predate this
|
|
10
|
+
// field, so a missing one must render no category chips, not throw.
|
|
11
|
+
categories?: Set<string>;
|
|
9
12
|
"min-price": number | undefined;
|
|
10
13
|
"max-price": number | undefined;
|
|
11
14
|
rating: number | undefined;
|
|
@@ -70,13 +73,14 @@ const activeChips = computed(() => {
|
|
|
70
73
|
}
|
|
71
74
|
|
|
72
75
|
// Add manufacturer filters
|
|
73
|
-
const
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
)
|
|
78
|
-
|
|
79
|
-
|
|
76
|
+
const manufacturerFilter = props.availableFilters.find(
|
|
77
|
+
(f) => f.code === "manufacturer",
|
|
78
|
+
);
|
|
79
|
+
if (manufacturerFilter?.entities) {
|
|
80
|
+
for (const manufacturerId of props.filters.manufacturer) {
|
|
81
|
+
const entity = manufacturerFilter.entities.find(
|
|
82
|
+
(e) => e.id === manufacturerId,
|
|
83
|
+
);
|
|
80
84
|
const name = getTranslatedName(entity);
|
|
81
85
|
if (name) {
|
|
82
86
|
chips.push({
|
|
@@ -88,6 +92,24 @@ const activeChips = computed(() => {
|
|
|
88
92
|
}
|
|
89
93
|
}
|
|
90
94
|
|
|
95
|
+
// Add category filters
|
|
96
|
+
const categoryFilter = props.availableFilters.find(
|
|
97
|
+
(f) => f.code === "categories",
|
|
98
|
+
);
|
|
99
|
+
if (categoryFilter?.entities) {
|
|
100
|
+
for (const categoryId of props.filters.categories ?? []) {
|
|
101
|
+
const entity = categoryFilter.entities.find((e) => e.id === categoryId);
|
|
102
|
+
const name = getTranslatedName(entity);
|
|
103
|
+
if (name) {
|
|
104
|
+
chips.push({
|
|
105
|
+
label: name,
|
|
106
|
+
code: "categories",
|
|
107
|
+
value: categoryId,
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
91
113
|
// Add price filters
|
|
92
114
|
if (props.filters["min-price"] || props.filters["max-price"]) {
|
|
93
115
|
const min = props.filters["min-price"] || 0;
|
|
@@ -3,6 +3,7 @@ import { ApiClientError } from "@shopware/api-client";
|
|
|
3
3
|
import type { ApiError } from "@shopware/api-client";
|
|
4
4
|
import type { CmsElementForm } from "@shopware/composables";
|
|
5
5
|
import { useCmsTranslations } from "@shopware/composables";
|
|
6
|
+
import { getTranslatedProperty } from "@shopware/helpers";
|
|
6
7
|
import { useVuelidate } from "@vuelidate/core";
|
|
7
8
|
import type { ValidationRuleWithoutParams } from "@vuelidate/core";
|
|
8
9
|
import { email, required } from "@vuelidate/validators";
|
|
@@ -247,7 +248,7 @@ const invokeSubmit = async () => {
|
|
|
247
248
|
:key="salutation.id"
|
|
248
249
|
:value="salutation.id"
|
|
249
250
|
>
|
|
250
|
-
{{ salutation
|
|
251
|
+
{{ getTranslatedProperty(salutation, "displayName") }}
|
|
251
252
|
</option>
|
|
252
253
|
</select>
|
|
253
254
|
</div>
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import {
|
|
3
3
|
type UrlRouteOutput,
|
|
4
4
|
getSmallestThumbnailUrl,
|
|
5
|
+
getTranslatedProperty,
|
|
5
6
|
isProductOnSale,
|
|
6
7
|
isProductTopSeller,
|
|
7
8
|
} from "@shopware/helpers";
|
|
@@ -37,7 +38,10 @@ const coverSrcPath = computed(() => {
|
|
|
37
38
|
});
|
|
38
39
|
|
|
39
40
|
const coverAlt = computed(() => {
|
|
40
|
-
return
|
|
41
|
+
return (
|
|
42
|
+
getTranslatedProperty(props.product?.cover?.media, "alt") ||
|
|
43
|
+
props.product?.translated?.name
|
|
44
|
+
);
|
|
41
45
|
});
|
|
42
46
|
|
|
43
47
|
const isOnSale = computed(() => isProductOnSale(props.product));
|
|
@@ -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,
|
|
@@ -4,6 +4,7 @@ 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
9
|
import { computed } from "vue";
|
|
9
10
|
import type { ComputedRef } from "vue";
|
|
@@ -11,6 +12,7 @@ import type { LocationQueryRaw } from "vue-router";
|
|
|
11
12
|
|
|
12
13
|
import {
|
|
13
14
|
firstQueryValue,
|
|
15
|
+
getVisibleListingFilters,
|
|
14
16
|
toNumber,
|
|
15
17
|
useCategoryListing,
|
|
16
18
|
useProductSearchListing,
|
|
@@ -59,10 +61,17 @@ const {
|
|
|
59
61
|
|
|
60
62
|
const sidebarSelectedFilters = useSelectedListingFilters();
|
|
61
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
|
+
);
|
|
69
|
+
|
|
62
70
|
const showResetFiltersButton = computed<boolean>(() => {
|
|
63
71
|
if (
|
|
64
72
|
sidebarSelectedFilters.manufacturer.size !== 0 ||
|
|
65
73
|
sidebarSelectedFilters.properties.size !== 0 ||
|
|
74
|
+
(isProductSearch && sidebarSelectedFilters.categories.size !== 0) ||
|
|
66
75
|
sidebarSelectedFilters["max-price"] ||
|
|
67
76
|
sidebarSelectedFilters["min-price"] ||
|
|
68
77
|
sidebarSelectedFilters.rating ||
|
|
@@ -82,6 +91,19 @@ const searchCriteriaForRequest: ComputedRef<Schemas["ProductListingCriteria"]> =
|
|
|
82
91
|
properties: [...(sidebarSelectedFilters.properties as Set<string>)]?.join(
|
|
83
92
|
"|",
|
|
84
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
|
+
: {}),
|
|
85
107
|
"min-price": sidebarSelectedFilters["min-price"] as number,
|
|
86
108
|
"max-price": sidebarSelectedFilters["max-price"] as number,
|
|
87
109
|
order: getCurrentSortingOrder.value as string,
|
|
@@ -98,7 +120,11 @@ const handleFilterChange = async (event: {
|
|
|
98
120
|
try {
|
|
99
121
|
const { code, value } = event;
|
|
100
122
|
|
|
101
|
-
if (
|
|
123
|
+
if (
|
|
124
|
+
code === "manufacturer" ||
|
|
125
|
+
code === "properties" ||
|
|
126
|
+
code === "categories"
|
|
127
|
+
) {
|
|
102
128
|
const filterSet = sidebarSelectedFilters[code];
|
|
103
129
|
const stringValue = String(value);
|
|
104
130
|
|
|
@@ -137,6 +163,10 @@ const executeSearch = async () => {
|
|
|
137
163
|
|
|
138
164
|
if (criteria.manufacturer) query.manufacturer = criteria.manufacturer;
|
|
139
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("|");
|
|
140
170
|
if (criteria["min-price"]) query["min-price"] = criteria["min-price"];
|
|
141
171
|
if (criteria["max-price"]) query["max-price"] = criteria["max-price"];
|
|
142
172
|
if (criteria.rating) query.rating = criteria.rating;
|
|
@@ -161,6 +191,7 @@ const executeSearch = async () => {
|
|
|
161
191
|
const clearFilters = () => {
|
|
162
192
|
(sidebarSelectedFilters.manufacturer as Set<string>).clear();
|
|
163
193
|
(sidebarSelectedFilters.properties as Set<string>).clear();
|
|
194
|
+
(sidebarSelectedFilters.categories as Set<string>).clear();
|
|
164
195
|
sidebarSelectedFilters["min-price"] = undefined;
|
|
165
196
|
sidebarSelectedFilters["max-price"] = undefined;
|
|
166
197
|
sidebarSelectedFilters.rating = undefined;
|
|
@@ -208,7 +239,11 @@ const handleRemoveFilterChip = async (chip: {
|
|
|
208
239
|
code: string;
|
|
209
240
|
value: string | number;
|
|
210
241
|
}) => {
|
|
211
|
-
if (
|
|
242
|
+
if (
|
|
243
|
+
chip.code === "properties" ||
|
|
244
|
+
chip.code === "manufacturer" ||
|
|
245
|
+
chip.code === "categories"
|
|
246
|
+
) {
|
|
212
247
|
const filterSet = sidebarSelectedFilters[chip.code] as Set<string>;
|
|
213
248
|
filterSet.delete(String(chip.value));
|
|
214
249
|
} else if (chip.code === "price") {
|
|
@@ -228,7 +263,7 @@ const handleRemoveFilterChip = async (chip: {
|
|
|
228
263
|
<!-- Active Filter Chips -->
|
|
229
264
|
<SwFilterChips
|
|
230
265
|
:filters="sidebarSelectedFilters"
|
|
231
|
-
:available-filters="
|
|
266
|
+
:available-filters="visibleFilters"
|
|
232
267
|
@remove="handleRemoveFilterChip"
|
|
233
268
|
/>
|
|
234
269
|
|
|
@@ -254,11 +289,12 @@ const handleRemoveFilterChip = async (chip: {
|
|
|
254
289
|
<!-- Filters List -->
|
|
255
290
|
<div class="self-stretch flex flex-col justify-start items-start gap-4">
|
|
256
291
|
<SwProductListingFilter
|
|
257
|
-
v-for="filter in
|
|
292
|
+
v-for="filter in visibleFilters"
|
|
258
293
|
:key="filter.id"
|
|
259
294
|
:filter="filter"
|
|
260
295
|
:selected-manufacturer="sidebarSelectedFilters.manufacturer"
|
|
261
296
|
:selected-properties="sidebarSelectedFilters.properties"
|
|
297
|
+
:selected-categories="sidebarSelectedFilters.categories"
|
|
262
298
|
:selected-min-price="sidebarSelectedFilters['min-price']"
|
|
263
299
|
:selected-max-price="sidebarSelectedFilters['max-price']"
|
|
264
300
|
:selected-rating="sidebarSelectedFilters.rating"
|
|
@@ -4,6 +4,7 @@ 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
9
|
import { computed } from "vue";
|
|
9
10
|
import type { ComputedRef } from "vue";
|
|
@@ -11,6 +12,7 @@ import type { LocationQueryRaw } from "vue-router";
|
|
|
11
12
|
|
|
12
13
|
import {
|
|
13
14
|
firstQueryValue,
|
|
15
|
+
getVisibleListingFilters,
|
|
14
16
|
toNumber,
|
|
15
17
|
useCategoryListing,
|
|
16
18
|
useProductSearchListing,
|
|
@@ -58,10 +60,17 @@ const {
|
|
|
58
60
|
|
|
59
61
|
const sidebarSelectedFilters = useSelectedListingFilters();
|
|
60
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
|
+
);
|
|
68
|
+
|
|
61
69
|
const showResetFiltersButton = computed<boolean>(() => {
|
|
62
70
|
if (
|
|
63
71
|
sidebarSelectedFilters.manufacturer.size !== 0 ||
|
|
64
72
|
sidebarSelectedFilters.properties.size !== 0 ||
|
|
73
|
+
(isProductSearch && sidebarSelectedFilters.categories.size !== 0) ||
|
|
65
74
|
sidebarSelectedFilters["max-price"] ||
|
|
66
75
|
sidebarSelectedFilters["min-price"] ||
|
|
67
76
|
sidebarSelectedFilters.rating ||
|
|
@@ -81,6 +90,19 @@ const searchCriteriaForRequest: ComputedRef<Schemas["ProductListingCriteria"]> =
|
|
|
81
90
|
properties: [...(sidebarSelectedFilters.properties as Set<string>)]?.join(
|
|
82
91
|
"|",
|
|
83
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
|
+
: {}),
|
|
84
106
|
"min-price": sidebarSelectedFilters["min-price"] as number,
|
|
85
107
|
"max-price": sidebarSelectedFilters["max-price"] as number,
|
|
86
108
|
order: getCurrentSortingOrder.value as string,
|
|
@@ -97,7 +119,11 @@ const handleFilterChange = async (event: {
|
|
|
97
119
|
try {
|
|
98
120
|
const { code, value } = event;
|
|
99
121
|
|
|
100
|
-
if (
|
|
122
|
+
if (
|
|
123
|
+
code === "manufacturer" ||
|
|
124
|
+
code === "properties" ||
|
|
125
|
+
code === "categories"
|
|
126
|
+
) {
|
|
101
127
|
const filterSet = sidebarSelectedFilters[code];
|
|
102
128
|
const stringValue = String(value);
|
|
103
129
|
|
|
@@ -136,6 +162,10 @@ const executeSearch = async () => {
|
|
|
136
162
|
|
|
137
163
|
if (criteria.manufacturer) query.manufacturer = criteria.manufacturer;
|
|
138
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("|");
|
|
139
169
|
if (criteria["min-price"]) query["min-price"] = criteria["min-price"];
|
|
140
170
|
if (criteria["max-price"]) query["max-price"] = criteria["max-price"];
|
|
141
171
|
if (criteria.rating) query.rating = criteria.rating;
|
|
@@ -160,6 +190,7 @@ const executeSearch = async () => {
|
|
|
160
190
|
const clearFilters = () => {
|
|
161
191
|
(sidebarSelectedFilters.manufacturer as Set<string>).clear();
|
|
162
192
|
(sidebarSelectedFilters.properties as Set<string>).clear();
|
|
193
|
+
(sidebarSelectedFilters.categories as Set<string>).clear();
|
|
163
194
|
sidebarSelectedFilters["min-price"] = undefined;
|
|
164
195
|
sidebarSelectedFilters["max-price"] = undefined;
|
|
165
196
|
sidebarSelectedFilters.rating = undefined;
|
|
@@ -208,6 +239,9 @@ const hasActiveFilter = (filter: { code: string }) => {
|
|
|
208
239
|
if (filter.code === "manufacturer") {
|
|
209
240
|
return sidebarSelectedFilters.manufacturer.size > 0;
|
|
210
241
|
}
|
|
242
|
+
if (filter.code === "categories") {
|
|
243
|
+
return sidebarSelectedFilters.categories.size > 0;
|
|
244
|
+
}
|
|
211
245
|
if (filter.code === "price") {
|
|
212
246
|
return (
|
|
213
247
|
sidebarSelectedFilters["min-price"] !== undefined ||
|
|
@@ -231,7 +265,7 @@ const hasActiveFilter = (filter: { code: string }) => {
|
|
|
231
265
|
<div class="flex flex-wrap items-center justify-start gap-4 z-10">
|
|
232
266
|
<!-- Filter dropdowns -->
|
|
233
267
|
<SwFilterDropdown
|
|
234
|
-
v-for="filter in
|
|
268
|
+
v-for="filter in visibleFilters"
|
|
235
269
|
:key="filter.id"
|
|
236
270
|
:label="filter.label"
|
|
237
271
|
:is-active="hasActiveFilter(filter)"
|
|
@@ -241,6 +275,7 @@ const hasActiveFilter = (filter: { code: string }) => {
|
|
|
241
275
|
display-mode="dropdown"
|
|
242
276
|
:selected-manufacturer="sidebarSelectedFilters.manufacturer"
|
|
243
277
|
:selected-properties="sidebarSelectedFilters.properties"
|
|
278
|
+
:selected-categories="sidebarSelectedFilters.categories"
|
|
244
279
|
:selected-min-price="sidebarSelectedFilters['min-price']"
|
|
245
280
|
:selected-max-price="sidebarSelectedFilters['max-price']"
|
|
246
281
|
:selected-rating="sidebarSelectedFilters.rating"
|