@shopware/cms-base-layer 3.1.0 → 4.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +79 -23
- package/app/app.config.ts +3 -3
- package/app/assets/css/rich-text.css +80 -0
- package/app/components/SwCategoryNavigationLink.vue +4 -5
- package/app/components/SwContactForm.vue +2 -1
- package/app/components/SwFilterChips.vue +29 -7
- package/app/components/SwMedia3D.vue +8 -3
- package/app/components/SwNewsletterForm.vue +2 -1
- package/app/components/SwProductAddToCart.vue +27 -4
- package/app/components/SwProductCard.vue +26 -4
- package/app/components/SwProductCardDetails.vue +9 -2
- package/app/components/SwProductCardImage.vue +5 -1
- package/app/components/SwProductListingFilter.vue +6 -1
- package/app/components/SwProductListingFilters.vue +13 -189
- package/app/components/SwProductListingFiltersHorizontal.vue +20 -172
- package/app/components/SwStockInfo.vue +5 -2
- package/app/components/SwVariantConfigurator.vue +8 -3
- package/app/components/listing-filters/SwFilterCategories.vue +158 -0
- package/app/components/public/cms/CmsBlockSpatialViewer.vue +7 -7
- package/app/components/public/cms/CmsGenericBlock.vue +6 -10
- package/app/components/public/cms/CmsGenericElement.md +11 -5
- package/app/components/public/cms/CmsGenericElement.vue +17 -11
- package/app/components/public/cms/FrontendAccountCustomerGroupRegistrationPage.vue +1 -1
- package/app/components/public/cms/block/CmsBlockAppRenderer.vue +48 -0
- package/app/components/public/cms/block/CmsBlockCategoryHeading.vue +19 -0
- package/app/components/public/cms/block/CmsBlockCategoryNavigation.vue +3 -2
- package/app/components/public/cms/block/CmsBlockCenterText.vue +5 -4
- package/app/components/public/cms/block/CmsBlockCrossSelling.vue +3 -2
- package/app/components/public/cms/block/CmsBlockCustomForm.vue +3 -2
- package/app/components/public/cms/block/CmsBlockForm.vue +3 -2
- package/app/components/public/cms/block/CmsBlockGalleryBuybox.vue +4 -3
- package/app/components/public/cms/block/CmsBlockImage.vue +3 -2
- package/app/components/public/cms/block/CmsBlockImageBubbleRow.vue +5 -4
- package/app/components/public/cms/block/CmsBlockImageCover.vue +3 -2
- package/app/components/public/cms/block/CmsBlockImageFourColumn.vue +6 -5
- package/app/components/public/cms/block/CmsBlockImageGallery.vue +3 -2
- package/app/components/public/cms/block/CmsBlockImageGalleryBig.vue +3 -2
- package/app/components/public/cms/block/CmsBlockImageHighlightRow.vue +5 -4
- package/app/components/public/cms/block/CmsBlockImageSimpleGrid.vue +5 -4
- package/app/components/public/cms/block/CmsBlockImageSlider.vue +3 -2
- package/app/components/public/cms/block/CmsBlockImageText.vue +4 -3
- package/app/components/public/cms/block/CmsBlockImageTextBubble.vue +8 -7
- package/app/components/public/cms/block/CmsBlockImageTextCover.vue +4 -3
- package/app/components/public/cms/block/CmsBlockImageTextGallery.vue +39 -16
- package/app/components/public/cms/block/CmsBlockImageTextRow.vue +8 -7
- package/app/components/public/cms/block/CmsBlockImageThreeColumn.vue +5 -4
- package/app/components/public/cms/block/CmsBlockImageThreeCover.vue +5 -4
- package/app/components/public/cms/block/CmsBlockImageTwoColumn.vue +4 -3
- package/app/components/public/cms/block/CmsBlockProductDescriptionReviews.vue +3 -2
- package/app/components/public/cms/block/CmsBlockProductHeading.vue +4 -3
- package/app/components/public/cms/block/CmsBlockProductListing.vue +3 -2
- package/app/components/public/cms/block/CmsBlockProductThreeColumn.vue +5 -4
- package/app/components/public/cms/block/CmsBlockSidebarFilter.vue +7 -5
- package/app/components/public/cms/block/CmsBlockText.vue +3 -2
- package/app/components/public/cms/block/CmsBlockTextHero.vue +3 -2
- package/app/components/public/cms/block/CmsBlockTextOnImage.vue +3 -2
- package/app/components/public/cms/block/CmsBlockTextThreeColumn.vue +5 -4
- package/app/components/public/cms/block/CmsBlockTextTwoColumn.vue +4 -3
- package/app/components/public/cms/block/CmsBlockVideo.vue +19 -0
- package/app/components/public/cms/block/CmsBlockVimeoVideo.vue +3 -2
- package/app/components/public/cms/block/CmsBlockYoutubeVideo.vue +3 -2
- package/app/components/public/cms/element/CmsElementBuyBox.vue +4 -1
- package/app/components/public/cms/element/CmsElementCategoryName.md +8 -0
- package/app/components/public/cms/element/CmsElementCategoryName.vue +35 -0
- package/app/components/public/cms/element/CmsElementCrossSelling.vue +2 -1
- package/app/components/public/cms/element/CmsElementHtml.vue +1 -1
- package/app/components/public/cms/element/CmsElementImage.vue +41 -1
- package/app/components/public/cms/element/CmsElementImageGallery.vue +2 -1
- package/app/components/public/cms/element/CmsElementProductDescriptionReviews.vue +2 -2
- package/app/components/public/cms/element/CmsElementProductListing.vue +58 -25
- package/app/components/public/cms/element/CmsElementText.md +77 -0
- package/app/components/public/cms/element/CmsElementText.vue +15 -41
- package/app/components/public/cms/element/CmsElementVideo.md +27 -0
- package/app/components/public/cms/element/CmsElementVideo.vue +193 -0
- package/app/components/public/cms/section/CmsSectionSidebar.vue +4 -4
- package/app/composables/useListingFilters.ts +189 -0
- package/app/composables/useSelectedListingFilters.ts +46 -0
- package/app/helpers/cms/getAppRendererLayout.test.ts +93 -0
- package/app/helpers/cms/getAppRendererLayout.ts +36 -0
- package/app/helpers/cms/getCategoryNameContent.test.ts +57 -0
- package/app/helpers/cms/getCategoryNameContent.ts +15 -0
- package/app/helpers/cms/getTextElementContent.test.ts +79 -0
- package/app/helpers/cms/getTextElementContent.ts +37 -0
- package/app/helpers/cms/getVideoElementOptions.test.ts +384 -0
- package/app/helpers/cms/getVideoElementOptions.ts +142 -0
- package/app/helpers/html-to-vue/renderToHtml.test.ts +71 -0
- package/app/helpers/html-to-vue/renderToHtml.ts +5 -1
- package/app/helpers/html-to-vue/renderer.ts +13 -4
- package/app/helpers/product/shouldShowBuyButton.test.ts +21 -0
- package/app/helpers/product/shouldShowBuyButton.ts +6 -0
- package/app/utils/{useSelectedListingFilters.test.ts → listingFilterState.test.ts} +14 -4
- package/app/utils/{useSelectedListingFilters.ts → listingFilterState.ts} +4 -33
- package/app/utils/listingFilters.test.ts +39 -0
- package/app/utils/listingFilters.ts +19 -0
- package/component-dirs.json +20 -0
- package/index.d.ts +2 -2
- package/nuxt.config.ts +11 -28
- package/package.json +23 -26
- package/dist/index.d.mts +0 -5
- package/dist/index.d.ts +0 -5
- package/dist/index.mjs +0 -31
- package/index.cjs +0 -7
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,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
|
|
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:
|
|
524
|
+
### Latest changes: 4.1.0
|
|
489
525
|
|
|
490
526
|
### Minor Changes
|
|
491
527
|
|
|
492
|
-
- [#
|
|
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
|
-
- [#
|
|
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
|
-
- [#
|
|
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
|
-
- `
|
|
501
|
-
-
|
|
502
|
-
-
|
|
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
|
-
- [#
|
|
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
|
-
- [#
|
|
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
|
-
- [#
|
|
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
|
-
|
|
567
|
+
New translation key: `product.notAddedToCart`. Variant parents now use `product.details`. Add both to your locale files.
|
|
511
568
|
|
|
512
|
-
- Updated dependencies [[`
|
|
513
|
-
- @shopware/
|
|
514
|
-
- @shopware/
|
|
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 {
|
|
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
|
+
}
|
|
@@ -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
|
-
<
|
|
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
|
-
</
|
|
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
|
-
<
|
|
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
|
-
</
|
|
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
|
|
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;
|
|
@@ -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
|
|
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="
|
|
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
|
|
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 (
|
|
68
|
-
|
|
69
|
-
|
|
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 (
|
|
137
|
-
|
|
138
|
-
|
|
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="
|
|
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
|
|
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));
|