@laioutr/app-shopware 0.16.0 → 0.17.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/dist/module.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@laioutr/app-shopware",
3
- "version": "0.16.0",
3
+ "version": "0.17.0",
4
4
  "configKey": "@laioutr/app-shopware",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "1.0.1",
package/dist/module.mjs CHANGED
@@ -4,7 +4,7 @@ import { CHECKOUT_ENDPOINT_PATH, ADOPT_SESSION_ENDPOINT_PATH, ORDER_HANDOFF_ENDP
4
4
  import { registerLaioutrApp } from '@laioutr-core/kit';
5
5
 
6
6
  const name = "@laioutr/app-shopware";
7
- const version = "0.16.0";
7
+ const version = "0.17.0";
8
8
 
9
9
  const module$1 = defineNuxtModule({
10
10
  meta: {
@@ -6,6 +6,7 @@ import {
6
6
  ProductFlags,
7
7
  ProductInfo,
8
8
  ProductMedia,
9
+ ProductOptionGroups,
9
10
  ProductPrices,
10
11
  ProductRating,
11
12
  ProductSeo
@@ -16,6 +17,7 @@ import { defineShopwareComponentResolver } from "../../middleware/defineShopware
16
17
  import { fetchAllProducts } from "../../shopware-helper/fetchAllProductVariants.js";
17
18
  import { entitySlug } from "../../shopware-helper/mappers/slugMapper.js";
18
19
  import { mapMedia } from "../../shopware-helper/mediaMapper.js";
20
+ import { mapProductOptionGroups } from "../../shopware-helper/optionGroupsMapper.js";
19
21
  import { swTranslated } from "../../shopware-helper/swTranslated.js";
20
22
  export default defineShopwareComponentResolver({
21
23
  label: "Shopware Product Connector",
@@ -29,6 +31,7 @@ export default defineShopwareComponentResolver({
29
31
  ProductSeo,
30
32
  ProductDescription,
31
33
  ProductDefaultVariant,
34
+ ProductOptionGroups,
32
35
  ProductRating
33
36
  ],
34
37
  resolve: async ({ entityIds, context, $entity, passthrough }) => {
@@ -65,8 +68,15 @@ export default defineShopwareComponentResolver({
65
68
  slug: entitySlug(rawProduct)
66
69
  },
67
70
  defaultVariant: {
68
- id: rawVariant.id
71
+ id: rawVariant.id,
72
+ sku: rawVariant.productNumber ?? void 0,
73
+ // Typed locally because the resolver hands `rawVariant` back as `any`.
74
+ options: rawVariant.options?.map((option) => swTranslated(option, "name") ?? "").filter(Boolean),
75
+ status: rawVariant.available ? "inStock" : "outOfStock"
69
76
  },
77
+ // The configurator hangs off the parent, never the variant the rest of this
78
+ // projection reads from.
79
+ optionGroups: mapProductOptionGroups(rawProduct),
70
80
  info: {
71
81
  cover: mappedCover,
72
82
  shortDescription: swTranslated(rawProduct, "description") || swTranslated(rawVariant, "description"),
@@ -120,6 +130,11 @@ export default defineShopwareComponentResolver({
120
130
  components: {
121
131
  prices: {
122
132
  ttl: "15 minutes"
133
+ },
134
+ // Half of it is stock-derived, so it decays like prices rather than living for
135
+ // the resolver's full day. `optionGroups` needs no override: nothing in it is.
136
+ defaultVariant: {
137
+ ttl: "15 minutes"
123
138
  }
124
139
  }
125
140
  }
@@ -15,6 +15,7 @@ import { resolveProductVariantFields } from "../../orchestr-helper/requestedFiel
15
15
  import { toRequestCriteria } from "../../shopware-helper/criteria.js";
16
16
  import { mapMedia } from "../../shopware-helper/mediaMapper.js";
17
17
  import { swTranslated } from "../../shopware-helper/swTranslated.js";
18
+ import { guessWellKnownName } from "../../shopware-helper/wellKnownOptionName.js";
18
19
  export default defineShopwareComponentResolver({
19
20
  entityType: "ProductVariant",
20
21
  label: "Shopware Product Variant Connector",
@@ -119,10 +120,14 @@ export default defineShopwareComponentResolver({
119
120
  required: true
120
121
  }),
121
122
  options: () => ({
122
- selected: entity.options?.map((option) => ({
123
- name: swTranslated(option.group, "name") ?? swTranslated(option, "name") ?? "Unknown Property",
124
- value: swTranslated(option, "name") ?? swTranslated(option, "option") ?? "Unknown Value"
125
- })) ?? []
123
+ selected: entity.options?.map((option) => {
124
+ const name = swTranslated(option.group, "name") ?? swTranslated(option, "name") ?? "Unknown Property";
125
+ return {
126
+ name,
127
+ wellKnownName: guessWellKnownName(name),
128
+ value: swTranslated(option, "name") ?? swTranslated(option, "option") ?? "Unknown Value"
129
+ };
130
+ }) ?? []
126
131
  })
127
132
  })
128
133
  )
@@ -49,7 +49,16 @@ export const resolveProductFields = async (resolveCriteria) => {
49
49
  return resolveCriteria("product", {
50
50
  associations: {
51
51
  cover: { associations: { media: {} } },
52
- media: { associations: { media: {} } }
52
+ media: { associations: { media: {} } },
53
+ // Only the parent carries the configurator, which is what defines the option
54
+ // axes; `properties` on the same row are filterable facets and never do.
55
+ // `media` needs its own association: without it the option's swatch image is
56
+ // never returned, whatever the group's displayType claims.
57
+ configuratorSettings: { associations: { option: { associations: { group: {}, media: {} } } } },
58
+ // Only the variant projection's `includes` are merged below, never its
59
+ // associations, so the selected options need associating here too or the
60
+ // default variant reports none.
61
+ options: { associations: { group: {} } }
53
62
  },
54
63
  includes: mergeIncludes(
55
64
  {
@@ -74,10 +83,16 @@ export const resolveProductFields = async (resolveCriteria) => {
74
83
  "calculatedPrice",
75
84
  "calculatedPrices",
76
85
  "ratingAverage",
77
- "productReviews"
86
+ "productReviews",
87
+ "configuratorSettings"
78
88
  ],
79
89
  product_media: ["id", "mediaId", "media"],
80
- media: MediaIncludes
90
+ media: MediaIncludes,
91
+ product_configurator_setting: ["id", "optionId", "option", "position"],
92
+ // Merged with the variant projection's narrower list, which carries neither
93
+ // the swatch fields nor the ordering the configurator is authored in.
94
+ property_group_option: ["colorHexCode", "media", "position"],
95
+ property_group: ["displayType", "position"]
81
96
  },
82
97
  variantFields.includes
83
98
  )
@@ -0,0 +1,23 @@
1
+ import type { ShopwareProduct } from '../types/shopware.js';
2
+ import type { Swatch } from '@laioutr-core/core-types/common';
3
+ /**
4
+ * The option axes a product offers, read from its configurator.
5
+ *
6
+ * The configurator is what defines a variant; `properties` on the same product are
7
+ * filterable facets and never do. Only the parent carries it, so this must be given
8
+ * the parent row rather than the variant the rest of the projection reads from.
9
+ *
10
+ * Per-value stock and a per-value variant id are deliberately absent: both would
11
+ * mean loading every child variant, which is the cost this component exists to
12
+ * avoid. A consumer that needs them falls back to the variants link.
13
+ */
14
+ export declare const mapProductOptionGroups: (product: Pick<ShopwareProduct, "configuratorSettings">) => {
15
+ groups: {
16
+ name: string;
17
+ wellKnownName: import("@laioutr-core/canonical-types/entity/product-variant").WellKnownOptionName | undefined;
18
+ values: {
19
+ value: string;
20
+ swatch: Swatch | undefined;
21
+ }[];
22
+ }[];
23
+ };
@@ -0,0 +1,37 @@
1
+ import { mapMedia } from "./mediaMapper.js";
2
+ import { swTranslated } from "./swTranslated.js";
3
+ import { guessWellKnownName } from "./wellKnownOptionName.js";
4
+ const byPosition = (a, b) => (a.position ?? 0) - (b.position ?? 0);
5
+ const bySettingThenOption = (a, b) => (a.position ?? 0) - (b.position ?? 0) || (a.option.position ?? 0) - (b.option.position ?? 0);
6
+ const mapSwatch = (option) => {
7
+ const hex = swTranslated(option, "colorHexCode") ?? option.colorHexCode;
8
+ if (hex) return ["color", hex];
9
+ const media = option.media ? mapMedia(option.media) : void 0;
10
+ return media?.type === "image" ? ["image", media] : void 0;
11
+ };
12
+ export const mapProductOptionGroups = (product) => {
13
+ const settings = (product.configuratorSettings ?? []).filter((setting) => !!setting.option);
14
+ const groups = /* @__PURE__ */ new Map();
15
+ for (const setting of settings) {
16
+ const group = setting.option.group;
17
+ const key = group?.id ?? swTranslated(group, "name") ?? "";
18
+ const existing = groups.get(key);
19
+ if (existing) {
20
+ existing.settings.push(setting);
21
+ continue;
22
+ }
23
+ groups.set(key, { name: swTranslated(group, "name") ?? "", position: group?.position ?? 0, settings: [setting] });
24
+ }
25
+ return {
26
+ groups: [...groups.values()].sort(byPosition).map((group) => ({
27
+ name: group.name,
28
+ wellKnownName: guessWellKnownName(group.name),
29
+ // The merchant's configurator order, which is what keeps a size run out of
30
+ // alphabetical order.
31
+ values: [...group.settings].sort(bySettingThenOption).map((setting) => ({
32
+ value: swTranslated(setting.option, "name") ?? "",
33
+ swatch: mapSwatch(setting.option)
34
+ }))
35
+ }))
36
+ };
37
+ };
@@ -0,0 +1,2 @@
1
+ import type { WellKnownOptionName } from '@laioutr-core/canonical-types/entity/product-variant';
2
+ export declare const guessWellKnownName: (name: string) => WellKnownOptionName | undefined;
@@ -0,0 +1,278 @@
1
+ const OPTION_NAME_ALIASES = {
2
+ color: [
3
+ "color",
4
+ // en (US), es
5
+ "colour",
6
+ // en (GB)
7
+ "farbe",
8
+ // de
9
+ "farbton",
10
+ // de — shade rather than colour, and a real axis name in the wild
11
+ "kleur",
12
+ // nl
13
+ "couleur",
14
+ // fr
15
+ "colore",
16
+ // it
17
+ "cor",
18
+ // pt
19
+ "kolor",
20
+ // pl
21
+ "barva",
22
+ // cs, sl
23
+ "farba",
24
+ // sk
25
+ "boja",
26
+ // hr
27
+ "sz\xEDn",
28
+ // hu
29
+ "culoare",
30
+ // ro
31
+ "\u0446\u0432\u044F\u0442",
32
+ // bg
33
+ "\u03C7\u03C1\u03CE\u03BC\u03B1",
34
+ // el
35
+ "f\xE4rg",
36
+ // sv
37
+ "farve",
38
+ // da
39
+ "farge",
40
+ // no
41
+ "v\xE4ri",
42
+ // fi
43
+ "litur",
44
+ // is
45
+ "v\xE4rv",
46
+ // et
47
+ "kr\u0101sa",
48
+ // lv
49
+ "spalva",
50
+ // lt
51
+ "renk",
52
+ // tr
53
+ "\u8272",
54
+ // ja
55
+ "\u30AB\u30E9\u30FC",
56
+ // ja
57
+ "\uC0C9\uC0C1",
58
+ // ko
59
+ "\uCEEC\uB7EC",
60
+ // ko
61
+ "\u989C\u8272",
62
+ // zh-Hans
63
+ "\u984F\u8272"
64
+ // zh-Hant
65
+ ],
66
+ size: [
67
+ "size",
68
+ // en
69
+ "gr\xF6\xDFe",
70
+ // de
71
+ "groesse",
72
+ // de, ASCII transliteration — "oe" survives diacritic folding
73
+ "maat",
74
+ // nl
75
+ "grootte",
76
+ // nl
77
+ "taille",
78
+ // fr
79
+ "pointure",
80
+ // fr, footwear
81
+ "taglia",
82
+ // it
83
+ "misura",
84
+ // it
85
+ "talla",
86
+ // es
87
+ "tama\xF1o",
88
+ // es
89
+ "tamanho",
90
+ // pt
91
+ "rozmiar",
92
+ // pl
93
+ "velikost",
94
+ // cs, sl
95
+ "ve\u013Ekos\u0165",
96
+ // sk
97
+ "veli\u010Dina",
98
+ // hr
99
+ "m\xE9ret",
100
+ // hu
101
+ "m\u0103rime",
102
+ // ro
103
+ "\u0440\u0430\u0437\u043C\u0435\u0440",
104
+ // bg
105
+ "\u03BC\u03AD\u03B3\u03B5\u03B8\u03BF\u03C2",
106
+ // el
107
+ "storlek",
108
+ // sv
109
+ "st\xF8rrelse",
110
+ // da, no
111
+ "koko",
112
+ // fi
113
+ "st\xE6r\xF0",
114
+ // is
115
+ "suurus",
116
+ // et
117
+ "izm\u0113rs",
118
+ // lv
119
+ "dydis",
120
+ // lt
121
+ "beden",
122
+ // tr
123
+ "\u30B5\u30A4\u30BA",
124
+ // ja
125
+ "\uC0AC\uC774\uC988",
126
+ // ko
127
+ "\uD06C\uAE30",
128
+ // ko
129
+ "\u5C3A\u5BF8",
130
+ // zh
131
+ "\u5C3A\u7801",
132
+ // zh-Hans
133
+ "\u5C3A\u78BC"
134
+ // zh-Hant
135
+ ],
136
+ material: [
137
+ "material",
138
+ // en, de, es, pt, ro, sv, sl, and cs/sk `materiál` once folded
139
+ "materiaal",
140
+ // nl
141
+ "mati\xE8re",
142
+ // fr
143
+ "mat\xE9riau",
144
+ // fr
145
+ "materiale",
146
+ // it, da, no
147
+ "materia\u0142",
148
+ // pl
149
+ "materijal",
150
+ // hr
151
+ "anyag",
152
+ // hu
153
+ "\u043C\u0430\u0442\u0435\u0440\u0438\u0430\u043B",
154
+ // bg
155
+ "\u03C5\u03BB\u03B9\u03BA\u03CC",
156
+ // el
157
+ "materiaali",
158
+ // fi
159
+ "efni",
160
+ // is
161
+ "materjal",
162
+ // et
163
+ "materi\u0101ls",
164
+ // lv
165
+ "med\u017Eiaga",
166
+ // lt
167
+ "malzeme",
168
+ // tr
169
+ "\u7D20\u6750",
170
+ // ja
171
+ "\u6750\u8CEA",
172
+ // ja, zh-Hant
173
+ "\uC18C\uC7AC",
174
+ // ko
175
+ "\uC7AC\uC9C8",
176
+ // ko
177
+ "\u6750\u6599",
178
+ // zh
179
+ "\u6750\u8D28"
180
+ // zh-Hans
181
+ ],
182
+ style: [
183
+ "style",
184
+ // en, fr
185
+ "stil",
186
+ // de, sv, da, no, hr, tr
187
+ "stijl",
188
+ // nl
189
+ "stile",
190
+ // it
191
+ "estilo",
192
+ // es, pt
193
+ "styl",
194
+ // pl, cs, and sk `štýl` once folded
195
+ "slog",
196
+ // sl
197
+ "st\xEDlus",
198
+ // hu
199
+ "\u0441\u0442\u0438\u043B",
200
+ // bg
201
+ "\u03C3\u03C4\u03C5\u03BB",
202
+ // el
203
+ "tyyli",
204
+ // fi
205
+ "st\xEDll",
206
+ // is
207
+ "stiil",
208
+ // et
209
+ "stils",
210
+ // lv
211
+ "stilius",
212
+ // lt
213
+ "\u30B9\u30BF\u30A4\u30EB",
214
+ // ja
215
+ "\uC2A4\uD0C0\uC77C",
216
+ // ko
217
+ "\u6B3E\u5F0F"
218
+ // zh
219
+ ],
220
+ type: [
221
+ "type",
222
+ // en, nl, fr, da, no
223
+ "typ",
224
+ // de, pl, cs, sk, sv
225
+ "tipo",
226
+ // it, es, pt
227
+ "tip",
228
+ // sl, hr, tr
229
+ "vrsta",
230
+ // sl, hr
231
+ "rodzaj",
232
+ // pl
233
+ "t\xEDpus",
234
+ // hu
235
+ "\u0442\u0438\u043F",
236
+ // bg
237
+ "\u03C4\u03CD\u03C0\u03BF\u03C2",
238
+ // el
239
+ "tyyppi",
240
+ // fi
241
+ "tegund",
242
+ // is
243
+ "t\xFC\xFCp",
244
+ // et
245
+ "veids",
246
+ // lv
247
+ "tipas",
248
+ // lt
249
+ "\u30BF\u30A4\u30D7",
250
+ // ja
251
+ "\u7A2E\u985E",
252
+ // ja
253
+ "\uC720\uD615",
254
+ // ko
255
+ "\uD0C0\uC785",
256
+ // ko
257
+ "\u7C7B\u578B",
258
+ // zh-Hans
259
+ "\u985E\u578B"
260
+ // zh-Hant
261
+ ]
262
+ };
263
+ const normalizeOptionName = (name) => name.trim().toLowerCase().replace(/ß/g, "ss").normalize("NFD").replace(/\p{M}/gu, "");
264
+ const OPTION_NAME_LOOKUP = new Map(
265
+ Object.entries(OPTION_NAME_ALIASES).flatMap(
266
+ ([wellKnownName, aliases]) => aliases.map((alias) => [normalizeOptionName(alias), wellKnownName])
267
+ )
268
+ );
269
+ export const guessWellKnownName = (name) => {
270
+ const normalized = normalizeOptionName(name);
271
+ const exact = OPTION_NAME_LOOKUP.get(normalized);
272
+ if (exact) return exact;
273
+ for (const token of normalized.split(/[^\p{L}\p{N}]+/u)) {
274
+ const match = OPTION_NAME_LOOKUP.get(token);
275
+ if (match) return match;
276
+ }
277
+ return void 0;
278
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@laioutr/app-shopware",
3
- "version": "0.16.0",
3
+ "version": "0.17.0",
4
4
  "description": "Laioutr integration with Shopware 6",
5
5
  "repository": "github:laioutr/app-shopware",
6
6
  "license": "MIT",
@@ -40,7 +40,7 @@
40
40
  "devDependencies": {
41
41
  "@changesets/cli": "^2.31.0",
42
42
  "@laioutr-app/ui": "^2.10.6",
43
- "@laioutr-core/canonical-types": "^0.27.4",
43
+ "@laioutr-core/canonical-types": "^0.31.0",
44
44
  "@laioutr-core/core-types": "^0.40.3",
45
45
  "@laioutr-core/devtools": "^0.40.3",
46
46
  "@laioutr-core/frontend-core": "^0.40.3",
@@ -69,7 +69,7 @@
69
69
  "vue-tsc": "2.2.10"
70
70
  },
71
71
  "peerDependencies": {
72
- "@laioutr-core/canonical-types": ">=0.27.4",
72
+ "@laioutr-core/canonical-types": ">=0.31.0",
73
73
  "@laioutr-core/core-types": ">=0.40.3",
74
74
  "@laioutr-core/frontend-core": ">=0.40.3",
75
75
  "@laioutr-core/kit": ">=0.40.3",