@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.
Files changed (102) hide show
  1. package/README.md +79 -23
  2. package/app/app.config.ts +3 -3
  3. package/app/assets/css/rich-text.css +80 -0
  4. package/app/components/SwCategoryNavigationLink.vue +4 -5
  5. package/app/components/SwContactForm.vue +2 -1
  6. package/app/components/SwFilterChips.vue +29 -7
  7. package/app/components/SwMedia3D.vue +8 -3
  8. package/app/components/SwNewsletterForm.vue +2 -1
  9. package/app/components/SwProductAddToCart.vue +27 -4
  10. package/app/components/SwProductCard.vue +26 -4
  11. package/app/components/SwProductCardDetails.vue +9 -2
  12. package/app/components/SwProductCardImage.vue +5 -1
  13. package/app/components/SwProductListingFilter.vue +6 -1
  14. package/app/components/SwProductListingFilters.vue +13 -189
  15. package/app/components/SwProductListingFiltersHorizontal.vue +20 -172
  16. package/app/components/SwStockInfo.vue +5 -2
  17. package/app/components/SwVariantConfigurator.vue +8 -3
  18. package/app/components/listing-filters/SwFilterCategories.vue +158 -0
  19. package/app/components/public/cms/CmsBlockSpatialViewer.vue +7 -7
  20. package/app/components/public/cms/CmsGenericBlock.vue +6 -10
  21. package/app/components/public/cms/CmsGenericElement.md +11 -5
  22. package/app/components/public/cms/CmsGenericElement.vue +17 -11
  23. package/app/components/public/cms/FrontendAccountCustomerGroupRegistrationPage.vue +1 -1
  24. package/app/components/public/cms/block/CmsBlockAppRenderer.vue +48 -0
  25. package/app/components/public/cms/block/CmsBlockCategoryHeading.vue +19 -0
  26. package/app/components/public/cms/block/CmsBlockCategoryNavigation.vue +3 -2
  27. package/app/components/public/cms/block/CmsBlockCenterText.vue +5 -4
  28. package/app/components/public/cms/block/CmsBlockCrossSelling.vue +3 -2
  29. package/app/components/public/cms/block/CmsBlockCustomForm.vue +3 -2
  30. package/app/components/public/cms/block/CmsBlockForm.vue +3 -2
  31. package/app/components/public/cms/block/CmsBlockGalleryBuybox.vue +4 -3
  32. package/app/components/public/cms/block/CmsBlockImage.vue +3 -2
  33. package/app/components/public/cms/block/CmsBlockImageBubbleRow.vue +5 -4
  34. package/app/components/public/cms/block/CmsBlockImageCover.vue +3 -2
  35. package/app/components/public/cms/block/CmsBlockImageFourColumn.vue +6 -5
  36. package/app/components/public/cms/block/CmsBlockImageGallery.vue +3 -2
  37. package/app/components/public/cms/block/CmsBlockImageGalleryBig.vue +3 -2
  38. package/app/components/public/cms/block/CmsBlockImageHighlightRow.vue +5 -4
  39. package/app/components/public/cms/block/CmsBlockImageSimpleGrid.vue +5 -4
  40. package/app/components/public/cms/block/CmsBlockImageSlider.vue +3 -2
  41. package/app/components/public/cms/block/CmsBlockImageText.vue +4 -3
  42. package/app/components/public/cms/block/CmsBlockImageTextBubble.vue +8 -7
  43. package/app/components/public/cms/block/CmsBlockImageTextCover.vue +4 -3
  44. package/app/components/public/cms/block/CmsBlockImageTextGallery.vue +39 -16
  45. package/app/components/public/cms/block/CmsBlockImageTextRow.vue +8 -7
  46. package/app/components/public/cms/block/CmsBlockImageThreeColumn.vue +5 -4
  47. package/app/components/public/cms/block/CmsBlockImageThreeCover.vue +5 -4
  48. package/app/components/public/cms/block/CmsBlockImageTwoColumn.vue +4 -3
  49. package/app/components/public/cms/block/CmsBlockProductDescriptionReviews.vue +3 -2
  50. package/app/components/public/cms/block/CmsBlockProductHeading.vue +4 -3
  51. package/app/components/public/cms/block/CmsBlockProductListing.vue +3 -2
  52. package/app/components/public/cms/block/CmsBlockProductThreeColumn.vue +5 -4
  53. package/app/components/public/cms/block/CmsBlockSidebarFilter.vue +7 -5
  54. package/app/components/public/cms/block/CmsBlockText.vue +3 -2
  55. package/app/components/public/cms/block/CmsBlockTextHero.vue +3 -2
  56. package/app/components/public/cms/block/CmsBlockTextOnImage.vue +3 -2
  57. package/app/components/public/cms/block/CmsBlockTextThreeColumn.vue +5 -4
  58. package/app/components/public/cms/block/CmsBlockTextTwoColumn.vue +4 -3
  59. package/app/components/public/cms/block/CmsBlockVideo.vue +19 -0
  60. package/app/components/public/cms/block/CmsBlockVimeoVideo.vue +3 -2
  61. package/app/components/public/cms/block/CmsBlockYoutubeVideo.vue +3 -2
  62. package/app/components/public/cms/element/CmsElementBuyBox.vue +4 -1
  63. package/app/components/public/cms/element/CmsElementCategoryName.md +8 -0
  64. package/app/components/public/cms/element/CmsElementCategoryName.vue +35 -0
  65. package/app/components/public/cms/element/CmsElementCrossSelling.vue +2 -1
  66. package/app/components/public/cms/element/CmsElementHtml.vue +1 -1
  67. package/app/components/public/cms/element/CmsElementImage.vue +41 -1
  68. package/app/components/public/cms/element/CmsElementImageGallery.vue +2 -1
  69. package/app/components/public/cms/element/CmsElementProductDescriptionReviews.vue +2 -2
  70. package/app/components/public/cms/element/CmsElementProductListing.vue +58 -25
  71. package/app/components/public/cms/element/CmsElementText.md +77 -0
  72. package/app/components/public/cms/element/CmsElementText.vue +15 -41
  73. package/app/components/public/cms/element/CmsElementVideo.md +27 -0
  74. package/app/components/public/cms/element/CmsElementVideo.vue +193 -0
  75. package/app/components/public/cms/section/CmsSectionSidebar.vue +4 -4
  76. package/app/composables/useListingFilters.ts +189 -0
  77. package/app/composables/useSelectedListingFilters.ts +46 -0
  78. package/app/helpers/cms/getAppRendererLayout.test.ts +93 -0
  79. package/app/helpers/cms/getAppRendererLayout.ts +36 -0
  80. package/app/helpers/cms/getCategoryNameContent.test.ts +57 -0
  81. package/app/helpers/cms/getCategoryNameContent.ts +15 -0
  82. package/app/helpers/cms/getTextElementContent.test.ts +79 -0
  83. package/app/helpers/cms/getTextElementContent.ts +37 -0
  84. package/app/helpers/cms/getVideoElementOptions.test.ts +384 -0
  85. package/app/helpers/cms/getVideoElementOptions.ts +142 -0
  86. package/app/helpers/html-to-vue/renderToHtml.test.ts +71 -0
  87. package/app/helpers/html-to-vue/renderToHtml.ts +5 -1
  88. package/app/helpers/html-to-vue/renderer.ts +13 -4
  89. package/app/helpers/product/shouldShowBuyButton.test.ts +21 -0
  90. package/app/helpers/product/shouldShowBuyButton.ts +6 -0
  91. package/app/utils/{useSelectedListingFilters.test.ts → listingFilterState.test.ts} +14 -4
  92. package/app/utils/{useSelectedListingFilters.ts → listingFilterState.ts} +4 -33
  93. package/app/utils/listingFilters.test.ts +39 -0
  94. package/app/utils/listingFilters.ts +19 -0
  95. package/component-dirs.json +20 -0
  96. package/index.d.ts +2 -2
  97. package/nuxt.config.ts +11 -28
  98. package/package.json +23 -26
  99. package/dist/index.d.mts +0 -5
  100. package/dist/index.d.ts +0 -5
  101. package/dist/index.mjs +0 -31
  102. 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.alt"
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 automatically preloads the first image found in CMS page content. 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.
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`/`densities`/`loading` stay on the component — NuxtImg presets don't propagate these reliably:
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,12 +454,42 @@ 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 into `~/components` dir in your nuxt project (or wherever according your nuxt config)
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
 
463
+ ### App blocks (`app-renderer`)
464
+
465
+ Every block an app registers through the Meteor Admin SDK (`cms.registerCmsBlock`) reaches the Store API with the type `app-renderer`, so they all render through `CmsBlockAppRenderer`. By default it places the block's slots in the CSS grid the app declared, like the Storefront's fallback, in the order the app declared them.
466
+
467
+ To give one app block its own markup, add a global component named after the block, `CmsBlockAppRenderer` followed by the PascalCase `appBlockName` — for a block registered as `swag-two-columns`, that is `CmsBlockAppRendererSwagTwoColumns.vue`. It receives the block as its `content` prop, and every other app block keeps the fallback:
468
+
469
+ ```vue
470
+ <script setup lang="ts">
471
+ import type { CmsBlockAppRenderer } from "@shopware/composables";
472
+
473
+ const props = defineProps<{ content: CmsBlockAppRenderer }>();
474
+
475
+ const { getSlotContent } = useCmsBlock(() => props.content);
476
+
477
+ const text = computed(() => getSlotContent("text-0"));
478
+ const image = computed(() => getSlotContent("image-1"));
479
+ </script>
480
+
481
+ <template>
482
+ <div class="grid gap-6 md:grid-cols-2">
483
+ <CmsGenericElement :content="text" />
484
+ <CmsGenericElement :content="image" />
485
+ </div>
486
+ </template>
487
+ ```
488
+
489
+ The slots are named `{element}-{index}` in the order the app declared them, so look them up by name rather than by position: the Store API sorts them by name, so `image-1` comes before `text-0` and `text-10` before `text-2`. The fallback restores the declared order from the index, as the Administration preview shows it. The Storefront's fallback keeps the Store API order, so a block that mixes element types can place its slots differently there.
490
+
491
+ The lookup of these components lives in `CmsBlockAppRenderer` itself. If you override `CmsBlockAppRenderer`, for example to change the fallback markup, your override replaces that lookup too, and your `CmsBlockAppRenderer{AppBlockName}` components are no longer used unless it renders them. A component whose name does not match the `appBlockName` renders the fallback without a warning, as a misnamed override of any other CMS component does.
492
+
457
493
  ### Internal components
458
494
 
459
495
  ❗**Internal components are not a part of public API. Once overwritten you need to track the changes on your own.**
@@ -485,31 +521,51 @@ No additional packages needed to be installed.
485
521
 
486
522
  Full changelog for stable version is available [here](https://github.com/shopware/frontends/blob/main/packages/cms-base-layer/CHANGELOG.md)
487
523
 
488
- ### Latest changes: 3.1.0
524
+ ### Latest changes: 4.1.0
489
525
 
490
526
  ### Minor Changes
491
527
 
492
- - [#2503](https://github.com/shopware/frontends/pull/2503) [`1b0ccc9`](https://github.com/shopware/frontends/commit/1b0ccc95fcef89de42931155fb40119b9075d21b) Thanks [@mkucmus](https://github.com/mkucmus)! - `SwProductListingFilters` and `SwProductListingFiltersHorizontal`: honor the `listingType` prop. When set to `productSearchListing` the components drive `useProductSearchListing` instead of `useCategoryListing` and keep the active search term in the request criteria and resulting URL, so the shared filter sidebar can be reused on the product search page without resetting the query. Category listing behavior is unchanged.
528
+ - [#2785](https://github.com/shopware/frontends/pull/2785) [`74a477b`](https://github.com/shopware/frontends/commit/74a477bfa88e3c518d3a0336e4d301c823122ba8) Thanks [@mdanilowicz](https://github.com/mdanilowicz)! - Render the CMS blocks and elements Shopware added since 6.6, which rendered nothing until now:
529
+
530
+ - `category-heading` block and `category-name` element (6.7.12), used by the default listing layouts for the category headline.
531
+ - `video` block and element (6.7.8) for media library videos, with every option of the Administration. New translation keys: `cms.video.playLabel`, `cms.video.pauseLabel`, `cms.video.loadError` and `cms.video.notSupported`.
532
+ - `app-renderer` block (6.6.1) for blocks registered through the Meteor Admin SDK. A global `CmsBlockAppRenderer{AppBlockName}` component overrides a single app block, see "App blocks" in the README.
533
+
534
+ - [#2774](https://github.com/shopware/frontends/pull/2774) [`5961f55`](https://github.com/shopware/frontends/commit/5961f55b9f7cebad626cc073ae0bf857aa26f91b) Thanks [@mdanilowicz](https://github.com/mdanilowicz)! - Render blocks reactively and survive a missing slot
535
+
536
+ `CmsGenericElement` now takes `content` as an optional prop and renders nothing when it is missing, instead of handing `undefined` to `resolveCmsComponent` and throwing. A block does not have to carry every slot its layout allows, so that is no longer an error path.
537
+
538
+ Every block component now passes its `content` to `useCmsBlock` as a getter and reads slot lookups through a `computed`, and `CmsSectionSidebar` does the same with `useCmsSection`. A block or section that receives new content re-resolves which slot goes where, instead of rendering the tree it was mounted with.
539
+
540
+ That stops at the element boundary. Element components still call `useCmsElementConfig(props.content)` and `useCmsElementImage(props.content)`, which capture the slot object at setup, so an element reused for a different slot of the same type keeps its old config- and media-derived values — an image its old source, a text its old configured content. Only values read straight from the prop (`props.content.data`) follow. Making those composables accept a getter is a separate change.
541
+
542
+ Both generic components also stop emitting an empty `<div>` where they used to render a placeholder: a missing slot and — in production — a block or element type with no component now render nothing. Dev mode is unchanged: it still warns and renders `CmsNoComponent`.
543
+
544
+ `CmsGenericBlock` and `CmsGenericElement` dropped their `Problem resolving component: …` branch. It sat behind `if (resolvedComponent)` and tested `isResolved`, which was always `true` there, so it never rendered; an unresolved component still logs a dev warning and renders `CmsNoComponent`.
493
545
 
494
546
  ### Patch Changes
495
547
 
496
- - [#2593](https://github.com/shopware/frontends/pull/2593) [`760635c`](https://github.com/shopware/frontends/commit/760635cdbaf3c80ae99fbe0f42a31a8ebaa14675) Thanks [@patzick](https://github.com/patzick)! - Stop registering the `@unocss/nuxt` module from the CMS layer. UnoCSS is not a dependency of this package anymore (it lives in `@shopware/unocss-design-tokens-layer`), so registering the module made every app that doesn't install UnoCSS itself fail to build with `Could not load @unocss/nuxt. Is it installed?` - for example the blank template. Apps that want the shared UnoCSS setup keep getting it from `@shopware/unocss-design-tokens-layer` or their own `@unocss/nuxt` registration.
548
+ - [#2778](https://github.com/shopware/frontends/pull/2778) [`0d4151c`](https://github.com/shopware/frontends/commit/0d4151ce1fb231606c33e1fd88f525baff60416d) Thanks [@grenzenlos-digital](https://github.com/grenzenlos-digital)! - Use Three.js vectors for the 3D camera and light positions.
549
+
550
+ - [#2731](https://github.com/shopware/frontends/pull/2731) [`46d6daa`](https://github.com/shopware/frontends/commit/46d6daade81e3d9e443f3badfeba2ca0e2751477) Thanks [@patzick](https://github.com/patzick)! - Add an optional notification action (label + link) so add-to-cart toasts can offer a "View cart" shortcut, and keep those toasts visible a little longer.
551
+
552
+ - [#2813](https://github.com/shopware/frontends/pull/2813) [`c6abd30`](https://github.com/shopware/frontends/commit/c6abd3000a9c3ad8ec4512704aa99a66ab0e671c) Thanks [@patzick](https://github.com/patzick)! - Fix a 500 on every page with a listing filter (`Cannot read properties of undefined (reading 'query')`) in projects that install the layer outside this monorepo. `useSelectedListingFilters` imported `useRoute` straight from `vue-router`, which the layer does not depend on, so it could resolve to a different `vue-router` copy than the one Nuxt's router uses and get no route back. It now uses Nuxt's `useRoute`, and `SwCategoryNavigationLink` renders `NuxtLink` instead of importing `RouterLink` from `vue-router` for the same reason. The pure filter-state helpers moved from `app/utils/useSelectedListingFilters.ts` to `app/utils/listingFilterState.ts`; their auto-imported names are unchanged.
497
553
 
498
- - [#2568](https://github.com/shopware/frontends/pull/2568) [`6315350`](https://github.com/shopware/frontends/commit/6315350add0464abef153343897d42f5808f2003) Thanks [@patzick](https://github.com/patzick)! - Add cacheable GET support for category details and product reviews while preserving the existing POST behavior when `cacheableReads` is disabled.
554
+ - [#2785](https://github.com/shopware/frontends/pull/2785) [`74a477b`](https://github.com/shopware/frontends/commit/74a477bfa88e3c518d3a0336e4d301c823122ba8) Thanks [@mdanilowicz](https://github.com/mdanilowicz)! - Fix how `CmsElementText` renders CMS text. `CmsElementProductName` and `CmsElementCategoryName` render through it and get the same fixes.
499
555
 
500
- - `useCategorySearch.search` now calls `GET /category/{navigationId}` and sends the complete encoded Criteria in the `_criteria` query parameter when cacheable reads are enabled.
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.
556
+ - It renders only the content the Store API resolved. When that content was empty, it fell back to the raw `config.content` value, which the backend sanitizer never saw. Static content that the sanitizer removed completely, such as a lone `<script>`, came back unsanitized, and a `{{ … }}` placeholder that resolved to nothing was shown as it was typed. Both now render nothing, as in the Storefront. An element built by hand without `data` still renders its static config.
557
+ - It no longer shows a mapping path. The backend answers a mapping to a value that is not a string with the path itself, for example `category.customFields`. That is now treated as unresolved.
558
+ - It follows a new `content`. It read its content and config once, when it was set up, so a component that received a new `content` kept the old text.
559
+ - HTML attributes other than `class`, `style` and `align` now reach the page, for example `id`, `title` and `colspan`. They were passed in the Vue 2 shape, so server renders dropped them and client renders added `attrs="[object Object]"`. That also made the server and client markup differ on hydration.
503
560
 
504
- - [#2571](https://github.com/shopware/frontends/pull/2571) [`c64ae1c`](https://github.com/shopware/frontends/commit/c64ae1c6e2d11e33bd52df4b55177cc26b646433) Thanks [@patzick](https://github.com/patzick)! - Show the category a shopper came from in product breadcrumbs, while direct product links continue to use the default category.
561
+ - [#2677](https://github.com/shopware/frontends/pull/2677) [`62c8d4c`](https://github.com/shopware/frontends/commit/62c8d4c86130c076b8391271593372eac83380e3) Thanks [@mkucmus](https://github.com/mkucmus)! - Drive the product listing from the URL. Browser back and forward now update the products, and sorting no longer fires a duplicate request.
505
562
 
506
- - [#2596](https://github.com/shopware/frontends/pull/2596) [`6572aa8`](https://github.com/shopware/frontends/commit/6572aa84431e1f4a34d6cf04e549037692d638a6) Thanks [@patzick](https://github.com/patzick)! - Reduce Rolldown/Vite build noise: scope Nuxt global components, keep the three.js async chunk warning intentional, avoid shipping full carbon icon JSON via UnoCSS runtime, and bump @vueuse to 14.4.0.
563
+ - [#2677](https://github.com/shopware/frontends/pull/2677) [`62c8d4c`](https://github.com/shopware/frontends/commit/62c8d4c86130c076b8391271593372eac83380e3) Thanks [@mkucmus](https://github.com/mkucmus)! - Write listing filters to the URL before fetching, so a slow or failed request no longer drops the selection. Expose the product id on the add-to-cart button.
507
564
 
508
- - [#2592](https://github.com/shopware/frontends/pull/2592) [`fcc3d71`](https://github.com/shopware/frontends/commit/fcc3d71af05c2f2d409aa63ecb562714e6bc0e69) Thanks [@patzick](https://github.com/patzick)! - Replace CMS layer `@apply` styles with plain CSS declarations to avoid Nuxt build warnings.
565
+ - [#2812](https://github.com/shopware/frontends/pull/2812) [`09d8b0f`](https://github.com/shopware/frontends/commit/09d8b0fea5489e973ef4f1f49d09e72372ab9ecf) Thanks [@mkucmus](https://github.com/mkucmus)! - Show "Details" instead of "Add to cart" for variant parents, and show success only when the product is in the cart. A product with a single price tier now shows "Add to cart", like in the Twig storefront.
509
566
 
510
- - [#2568](https://github.com/shopware/frontends/pull/2568) [`6315350`](https://github.com/shopware/frontends/commit/6315350add0464abef153343897d42f5808f2003) Thanks [@patzick](https://github.com/patzick)! - Preserve CMS image aspect ratios when responsive dimensions are calculated after hydration, preventing visible image scaling as optimized sources load.
567
+ New translation key: `product.notAddedToCart`. Variant parents now use `product.details`. Add both to your locale files.
511
568
 
512
- - Updated dependencies [[`6315350`](https://github.com/shopware/frontends/commit/6315350add0464abef153343897d42f5808f2003), [`b767721`](https://github.com/shopware/frontends/commit/b767721847bf3391f9067eca7a045089fb22fce0), [`f16c5a0`](https://github.com/shopware/frontends/commit/f16c5a0785d6187b73c3edcf37feab7c90bd7988), [`978b02c`](https://github.com/shopware/frontends/commit/978b02c969ca4b16f5fc1d7a953ec4cce3d98173), [`6572aa8`](https://github.com/shopware/frontends/commit/6572aa84431e1f4a34d6cf04e549037692d638a6), [`744833b`](https://github.com/shopware/frontends/commit/744833b9d7d2f8ea1f5dfe65be3fa554dbe4a09f), [`e03c91b`](https://github.com/shopware/frontends/commit/e03c91be172374894d90b7a0111855b76719fee1), [`33facb1`](https://github.com/shopware/frontends/commit/33facb178792c8cb26b47ab984ac48c08ab4b72b), [`9137475`](https://github.com/shopware/frontends/commit/91374753cedb2034385f642e6af11314f2971caa), [`474d3fe`](https://github.com/shopware/frontends/commit/474d3fed346816135b0c7c797990b215a8b691c0), [`38379a5`](https://github.com/shopware/frontends/commit/38379a5d52ab09008774533ef417ebe2cde3f7fd)]:
513
- - @shopware/composables@1.12.1
514
- - @shopware/api-client@1.5.1
515
- - @shopware/helpers@1.7.2
569
+ - Updated dependencies [[`44ece9d`](https://github.com/shopware/frontends/commit/44ece9dac2e4d0248c2270f7eff496c258632f5b), [`46d6daa`](https://github.com/shopware/frontends/commit/46d6daade81e3d9e443f3badfeba2ca0e2751477), [`7dbca8b`](https://github.com/shopware/frontends/commit/7dbca8bf8bca1225b365bc03e4dbc07d3764c2e1), [`74a477b`](https://github.com/shopware/frontends/commit/74a477bfa88e3c518d3a0336e4d301c823122ba8), [`74a477b`](https://github.com/shopware/frontends/commit/74a477bfa88e3c518d3a0336e4d301c823122ba8), [`5961f55`](https://github.com/shopware/frontends/commit/5961f55b9f7cebad626cc073ae0bf857aa26f91b), [`c78188a`](https://github.com/shopware/frontends/commit/c78188ac6c95ae19b070f5ea3361a60f21dff281), [`0df4c17`](https://github.com/shopware/frontends/commit/0df4c17b18ec38fc4d0c33f1cb6396f8f532a65d), [`4b43e64`](https://github.com/shopware/frontends/commit/4b43e64a8d78be6eca1f8d9c24140e046193af41)]:
570
+ - @shopware/api-client@1.7.0
571
+ - @shopware/composables@1.14.0
package/app/app.config.ts CHANGED
@@ -1,6 +1,6 @@
1
- import { defineAppConfig } from "#imports";
1
+ import type { AppConfigInput } from "nuxt/schema";
2
2
 
3
- export default defineAppConfig({
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
+ }
@@ -6,7 +6,6 @@ import {
6
6
  urlIsAbsolute,
7
7
  } from "@shopware/helpers";
8
8
  import { computed } from "vue";
9
- import { RouterLink } from "vue-router";
10
9
 
11
10
  import { useUrlResolver } from "#imports";
12
11
  import type { Schemas } from "#shopware";
@@ -48,7 +47,7 @@ const hasChildren = computed(() => {
48
47
  class="self-stretch py-3 border-b border-outline-outline-variant inline-flex justify-start items-center gap-1"
49
48
  >
50
49
  <div class="flex-1 flex justify-start items-center gap-2.5">
51
- <RouterLink
50
+ <NuxtLink
52
51
  v-if="!urlIsAbsolute(url.path)"
53
52
  :to="url"
54
53
  :class="[
@@ -56,7 +55,7 @@ const hasChildren = computed(() => {
56
55
  ]"
57
56
  >
58
57
  {{ getTranslatedProperty(navigationElement, "name") }}
59
- </RouterLink>
58
+ </NuxtLink>
60
59
  <a
61
60
  v-else
62
61
  :href="url.path"
@@ -93,7 +92,7 @@ const hasChildren = computed(() => {
93
92
  class="self-stretch pl-4 py-1.5 inline-flex justify-start items-center gap-2"
94
93
  >
95
94
  <div class="py-0.5 flex-1 flex justify-start items-center gap-2.5">
96
- <RouterLink
95
+ <NuxtLink
97
96
  v-if="!urlIsAbsolute(url.path)"
98
97
  :to="url"
99
98
  :class="[
@@ -102,7 +101,7 @@ const hasChildren = computed(() => {
102
101
  ]"
103
102
  >
104
103
  {{ getTranslatedProperty(navigationElement, "name") }}
105
- </RouterLink>
104
+ </NuxtLink>
106
105
  <a
107
106
  v-else
108
107
  :href="url.path"
@@ -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.displayName }}
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 manufacturers = Array.from(props.filters.manufacturer);
74
- for (const manufacturerId of manufacturers) {
75
- const filter = props.availableFilters.find(
76
- (f) => f.code === "manufacturer",
77
- );
78
- if (filter && "entities" in filter && filter.entities) {
79
- const entity = filter.entities.find((e) => e.id === manufacturerId);
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;
@@ -1,7 +1,7 @@
1
1
  <script lang="ts" setup>
2
2
  import { Bounds, OrbitControls, useGLTF } from "@tresjs/cientos";
3
3
  import { TresCanvas } from "@tresjs/core";
4
- import { BasicShadowMap, NoToneMapping, SRGBColorSpace } from "three";
4
+ import { BasicShadowMap, NoToneMapping, SRGBColorSpace, Vector3 } from "three";
5
5
  import { computed, shallowRef } from "vue";
6
6
 
7
7
  const props = defineProps<{
@@ -20,6 +20,8 @@ const gl = {
20
20
 
21
21
  const { state } = await useGLTF(props.src);
22
22
  const model = computed(() => state.value?.scene);
23
+ const cameraPosition = new Vector3(0, 0, 500);
24
+ const lightPosition = new Vector3(3, 3, 3);
23
25
 
24
26
  const boundsRef = shallowRef();
25
27
 
@@ -29,12 +31,15 @@ function focusObject() {
29
31
  </script>
30
32
  <template>
31
33
  <TresCanvas v-bind="gl">
32
- <TresPerspectiveCamera :args="[75, 1, 0.1, 2000]" :position="[0, 0, 500]" />
34
+ <TresPerspectiveCamera
35
+ :args="[75, 1, 0.1, 2000]"
36
+ :position="cameraPosition"
37
+ />
33
38
  <OrbitControls make-default />
34
39
  <Bounds ref="boundsRef" clip use-mounted>
35
40
  <primitive v-if="model" :object="model" @click="focusObject" />
36
41
  </Bounds>
37
- <TresDirectionalLight :position="[3, 3, 3]" :intensity="1" />
42
+ <TresDirectionalLight :position="lightPosition" :intensity="1" />
38
43
  <TresAmbientLight :intensity="2" />
39
44
  </TresCanvas>
40
45
  </template>
@@ -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.displayName }}
251
+ {{ getTranslatedProperty(salutation, "displayName") }}
251
252
  </option>
252
253
  </select>
253
254
  </div>
@@ -22,6 +22,8 @@ const props = defineProps<{
22
22
  type Translations = {
23
23
  product: {
24
24
  addedToCart: string;
25
+ notAddedToCart: string;
26
+ viewCart: string;
25
27
  qty: string;
26
28
  addToCart: string;
27
29
  productNumber: string;
@@ -34,6 +36,8 @@ type Translations = {
34
36
  let translations: Translations = {
35
37
  product: {
36
38
  addedToCart: "has been added to cart.",
39
+ notAddedToCart: "could not be added to cart.",
40
+ viewCart: "View cart",
37
41
  qty: "Qty",
38
42
  addToCart: "Add to cart",
39
43
  productNumber: "Product number",
@@ -56,7 +60,7 @@ const restockTime = computed(() => product.value?.restockTime);
56
60
  const productNumber = computed(() => product.value?.productNumber ?? "");
57
61
 
58
62
  const addToCartProxy = async () => {
59
- await addToCart();
63
+ const cart = await addToCart();
60
64
  const errors = getErrorsCodes();
61
65
  for (const element of errors) {
62
66
  const { messageKey, params } = resolveCartError(element);
@@ -64,10 +68,28 @@ const addToCartProxy = async () => {
64
68
  pushError(getCmsTranslate(translations.errors[messageKey], params));
65
69
  }
66
70
 
67
- if (!errors.length)
68
- pushSuccess(
69
- `${props.product?.translated.name} ${translations.product.addedToCart}`,
71
+ if (errors.length) return;
72
+
73
+ // Shopware drops some items without a cart error, e.g. a variant parent.
74
+ const added =
75
+ cart?.lineItems?.some((item) => item.referencedId === product.value.id) ??
76
+ true;
77
+ if (!added) {
78
+ pushError(
79
+ `${props.product?.translated.name} ${translations.product.notAddedToCart}`,
70
80
  );
81
+ return;
82
+ }
83
+
84
+ pushSuccess(
85
+ `${props.product?.translated.name} ${translations.product.addedToCart}`,
86
+ {
87
+ action: {
88
+ label: translations.product.viewCart,
89
+ to: "/checkout/cart",
90
+ },
91
+ },
92
+ );
71
93
  };
72
94
  </script>
73
95
 
@@ -92,6 +114,7 @@ const addToCartProxy = async () => {
92
114
  :disabled="!product?.available"
93
115
  block
94
116
  data-testid="add-to-cart-button"
117
+ :data-product-id="product?.id"
95
118
  @click="addToCartProxy"
96
119
  >
97
120
  {{ translations.product.addToCart }}
@@ -47,6 +47,8 @@ type Translations = {
47
47
  reason: string;
48
48
  cannotAddToWishlist: string;
49
49
  addedToCart: string;
50
+ notAddedToCart: string;
51
+ viewCart: string;
50
52
  addToCart: string;
51
53
  details: string;
52
54
  badges: {
@@ -67,6 +69,8 @@ let translations: Translations = {
67
69
  reason: "Reason",
68
70
  cannotAddToWishlist: "cannot be added to wishlist.",
69
71
  addedToCart: "has been added to cart.",
72
+ notAddedToCart: "could not be added to cart.",
73
+ viewCart: "View cart",
70
74
  addToCart: "Add to cart",
71
75
  details: "Details",
72
76
  badges: {
@@ -125,7 +129,7 @@ const toggleWishlistProduct = async () => {
125
129
  };
126
130
 
127
131
  const addToCartProxy = async () => {
128
- await addToCart();
132
+ const cart = await addToCart();
129
133
  const errors = getErrorsCodes();
130
134
  for (const element of errors) {
131
135
  const { messageKey, params } = resolveCartError(element);
@@ -133,10 +137,28 @@ const addToCartProxy = async () => {
133
137
  pushError(getCmsTranslate(translations.errors[messageKey], params));
134
138
  }
135
139
 
136
- if (!errors.length)
137
- pushSuccess(
138
- `${product?.value.translated.name} ${translations.product.addedToCart}`,
140
+ if (errors.length) return;
141
+
142
+ // Shopware drops some items without a cart error, e.g. a variant parent.
143
+ const added =
144
+ cart?.lineItems?.some((item) => item.referencedId === product.value.id) ??
145
+ true;
146
+ if (!added) {
147
+ pushError(
148
+ `${product?.value.translated.name} ${translations.product.notAddedToCart}`,
139
149
  );
150
+ return;
151
+ }
152
+
153
+ pushSuccess(
154
+ `${product?.value.translated.name} ${translations.product.addedToCart}`,
155
+ {
156
+ action: {
157
+ label: translations.product.viewCart,
158
+ to: "/checkout/cart",
159
+ },
160
+ },
161
+ );
140
162
  };
141
163
 
142
164
  const fromPrice = getProductFromPrice(product.value);
@@ -1,10 +1,13 @@
1
1
  <script setup lang="ts">
2
2
  import type { BoxLayout } from "@shopware/composables";
3
+ import { useProductPrice } from "@shopware/composables";
3
4
  import type { UrlRouteOutput } from "@shopware/helpers";
4
- import { computed } from "vue";
5
+ import { computed, toRef } from "vue";
5
6
 
6
7
  import type { Schemas } from "#shopware";
7
8
 
9
+ import { shouldShowBuyButton } from "../helpers/product/shouldShowBuyButton";
10
+
8
11
  type Translations = {
9
12
  product: {
10
13
  addToCart: string;
@@ -27,6 +30,10 @@ const props = defineProps<{
27
30
  }>();
28
31
 
29
32
  const isMinimalLayout = computed(() => props.layoutType === "minimal");
33
+ const { displayFrom } = useProductPrice(toRef(props, "product"));
34
+ const showBuyButton = computed(() =>
35
+ shouldShowBuyButton(props.product, displayFrom.value),
36
+ );
30
37
  </script>
31
38
  <template>
32
39
  <div
@@ -72,7 +79,7 @@ const isMinimalLayout = computed(() => props.layoutType === "minimal");
72
79
  <template v-if="!isMinimalLayout">
73
80
  <SwBaseButton
74
81
  variant="primary"
75
- v-if="!fromPrice"
82
+ v-if="showBuyButton"
76
83
  size="medium"
77
84
  :disabled="!product?.available"
78
85
  block
@@ -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 props.product?.cover?.media?.alt || props.product?.translated?.name;
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));