@useinsider/guido 3.19.0 → 3.20.0-beta.7b31fec

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 (62) hide show
  1. package/README.md +21 -3
  2. package/dist/@types/config/schemas.js +59 -51
  3. package/dist/components/Guido.vue.js +4 -4
  4. package/dist/components/Guido.vue2.js +70 -68
  5. package/dist/components/organisms/base/Toaster.vue2.js +13 -12
  6. package/dist/components/organisms/extensions/recommendation/AssignStrategyDrawer.vue.js +13 -11
  7. package/dist/components/organisms/extensions/recommendation/AssignStrategyDrawer.vue2.js +78 -57
  8. package/dist/components/organisms/extensions/recommendation/LegacyStrategyUpdatedModal.vue.js +18 -0
  9. package/dist/components/organisms/extensions/recommendation/LegacyStrategyUpdatedModal.vue2.js +25 -0
  10. package/dist/components/organisms/extensions/recommendation/RemoveStrategyModal.vue2.js +28 -42
  11. package/dist/components/organisms/onboarding/OnboardingWrapper.vue2.js +28 -24
  12. package/dist/components/organisms/onboarding/RecommendationLegacyStrategyOnboarding.vue.js +21 -0
  13. package/dist/components/organisms/onboarding/RecommendationLegacyStrategyOnboarding.vue2.js +46 -0
  14. package/dist/components/organisms/onboarding/RecommendationStrategyOnboarding.vue.js +21 -0
  15. package/dist/components/organisms/onboarding/RecommendationStrategyOnboarding.vue2.js +47 -0
  16. package/dist/composables/useCustomInterfaceAppearance.js +65 -51
  17. package/dist/composables/useHtmlValidator.js +87 -82
  18. package/dist/composables/useLegacyStrategyConversion.js +61 -0
  19. package/dist/composables/useRecommendation.js +40 -35
  20. package/dist/composables/useStripo.js +30 -30
  21. package/dist/composables/useToaster.js +14 -20
  22. package/dist/enums/extensions/recommendationBlock.js +49 -33
  23. package/dist/extensions/Blocks/Recommendation/block.js +61 -42
  24. package/dist/extensions/Blocks/Recommendation/constants/defaultConfig.js +21 -20
  25. package/dist/extensions/Blocks/Recommendation/controls/main/index.js +100 -94
  26. package/dist/extensions/Blocks/Recommendation/controls/main/strategy.js +130 -55
  27. package/dist/extensions/Blocks/Recommendation/controls/main/utils.js +74 -74
  28. package/dist/extensions/Blocks/Recommendation/store/recommendation.js +273 -92
  29. package/dist/extensions/Blocks/Recommendation/utils/legacyStrategyMigration.js +42 -0
  30. package/dist/extensions/Blocks/Recommendation/utils/nodeConfigWriter.js +26 -16
  31. package/dist/extensions/Blocks/Recommendation/utils/strategyCommit.js +24 -0
  32. package/dist/extensions/Blocks/Recommendation/validation/requiredFields.js +21 -12
  33. package/dist/guido.css +1 -1
  34. package/dist/services/recommendationApi.js +46 -37
  35. package/dist/src/@types/config/schemas.d.ts +16 -0
  36. package/dist/src/components/organisms/extensions/recommendation/LegacyStrategyUpdatedModal.vue.d.ts +2 -0
  37. package/dist/src/components/organisms/onboarding/RecommendationLegacyStrategyOnboarding.vue.d.ts +2 -0
  38. package/dist/src/components/organisms/onboarding/RecommendationStrategyOnboarding.vue.d.ts +2 -0
  39. package/dist/src/composables/useConfig.d.ts +4 -0
  40. package/dist/src/composables/useLegacyStrategyConversion.d.ts +3 -0
  41. package/dist/src/enums/extensions/recommendationBlock.d.ts +7 -1
  42. package/dist/src/extensions/Blocks/Recommendation/block.d.ts +10 -0
  43. package/dist/src/extensions/Blocks/Recommendation/constants/defaultConfig.d.ts +1 -0
  44. package/dist/src/extensions/Blocks/Recommendation/constants/index.d.ts +1 -1
  45. package/dist/src/extensions/Blocks/Recommendation/controls/main/index.d.ts +2 -0
  46. package/dist/src/extensions/Blocks/Recommendation/controls/main/strategy.d.ts +10 -0
  47. package/dist/src/extensions/Blocks/Recommendation/controls/main/utils.d.ts +7 -2
  48. package/dist/src/extensions/Blocks/Recommendation/store/recommendation.d.ts +162 -1
  49. package/dist/src/extensions/Blocks/Recommendation/utils/legacyStrategyMigration.d.ts +32 -0
  50. package/dist/src/extensions/Blocks/Recommendation/utils/strategyCommit.d.ts +14 -0
  51. package/dist/src/extensions/Blocks/Recommendation/validation/requiredFields.d.ts +4 -0
  52. package/dist/src/services/recommendationApi.d.ts +2 -1
  53. package/dist/src/stores/config.d.ts +322 -0
  54. package/dist/src/stores/onboarding.d.ts +808 -10
  55. package/dist/static/assets/icons/filled-arrow-rising.svg.js +5 -0
  56. package/dist/static/assets/icons/line-info.svg.js +5 -0
  57. package/dist/static/styles/components/recommendation-strategy.css.js +104 -9
  58. package/dist/static/styles/components/skeleton.css.js +23 -0
  59. package/dist/stores/config.js +74 -60
  60. package/dist/stores/onboarding.js +46 -36
  61. package/dist/utils/templatePreparation.js +55 -55
  62. package/package.json +1 -1
@@ -1,22 +1,27 @@
1
- import { getRecommendationFeedSourceMaps as b, getOperatorOptions as G, PriceAttributes as v } from "../../../../enums/extensions/recommendationBlock.js";
2
- import { useRecommendationApi as D } from "../../../../services/recommendationApi.js";
3
- import { useConfigStore as F } from "../../../../stores/config.js";
4
- import { defineStore as P } from "pinia";
1
+ import { useToaster as D } from "../../../../composables/useToaster.js";
2
+ import { useTranslations as L } from "../../../../composables/useTranslations.js";
3
+ import { getRecommendationFeedSourceMaps as F, strategyEditPath as U, URLS as E, getOperatorOptions as N, PriceAttributes as M } from "../../../../enums/extensions/recommendationBlock.js";
4
+ import { ToasterTypeOptions as A } from "../../../../enums/toaster.js";
5
+ import { useRecommendationApi as T } from "../../../../services/recommendationApi.js";
6
+ import { useConfigStore as k } from "../../../../stores/config.js";
7
+ import { defineStore as B } from "pinia";
5
8
  import "../constants/selectors.js";
6
- import { DEFAULT_MOBILE_CARDS_IN_ROW as _, DEFAULT_CARDS_IN_ROW as U } from "../constants/layout.js";
7
- import { DEFAULT_NODE_CONFIG as N, EXCLUDED_ALGORITHM_IDS as L } from "../constants/defaultConfig.js";
8
- import { getDefaultProducts as R } from "../templates/utils.js";
9
- import { generateCompleteFilterQuery as k } from "../utils/filterUtil.js";
10
- import { getPartnerRecommendationParams as x } from "../utils/partnerCustomizations.js";
11
- import { sanitizeProductIds as A } from "../utils/productIds.js";
12
- import { isFilterValid as B } from "../validation/filterSchema.js";
13
- import { isConfigValid as M } from "../validation/requiredFields.js";
14
- const u = D();
15
- let f = null, g = null, h = null, S = null;
16
- const p = /* @__PURE__ */ new Map();
17
- function w() {
9
+ import { DEFAULT_MOBILE_CARDS_IN_ROW as O, DEFAULT_CARDS_IN_ROW as z } from "../constants/layout.js";
10
+ import { CURRENT_CONFIG_VERSION as C, DEFAULT_NODE_CONFIG as x, EXCLUDED_ALGORITHM_IDS as V } from "../constants/defaultConfig.js";
11
+ import { getDefaultProducts as w } from "../templates/utils.js";
12
+ import { generateCompleteFilterQuery as P } from "../utils/filterUtil.js";
13
+ import { isLegacyStrategyConfig as j } from "../utils/legacyStrategyMigration.js";
14
+ import { getPartnerRecommendationParams as $ } from "../utils/partnerCustomizations.js";
15
+ import { sanitizeProductIds as q } from "../utils/productIds.js";
16
+ import { isFilterValid as K } from "../validation/filterSchema.js";
17
+ import { getInvalidFields as H, isConfigValid as J } from "../validation/requiredFields.js";
18
+ const m = T();
19
+ let S = null, p = null, y = null, I = "", R = null, b = 0;
20
+ const g = /* @__PURE__ */ new Map();
21
+ let l = null;
22
+ function _() {
18
23
  return {
19
- cardsInRow: U,
24
+ cardsInRow: z,
20
25
  currencySettings: {
21
26
  name: "USD",
22
27
  value: "USD",
@@ -30,14 +35,19 @@ function w() {
30
35
  productIds: [],
31
36
  id: 1,
32
37
  language: "en_US",
33
- mobileCardsInRow: _,
38
+ mobileCardsInRow: O,
34
39
  mobileLayoutEnabled: !1,
35
40
  orientation: "grid",
36
41
  recommendedProducts: [],
37
42
  sendProductRequestFlag: !1,
38
43
  shuffleProducts: !1,
39
- strategy: "mostPopular",
44
+ // With RRS on, a block that has not been synced yet must not claim an algorithm:
45
+ // that default is what reached buildCampaignUrl and baked a most-popular feed URL
46
+ // for a block the user never configured (SD-149916). Legacy blocks still get their real
47
+ // algorithm from the node-config sync or the save-time seed.
48
+ strategy: k().isFeatureEnabled("reusableRecommendationStrategy") ? "" : "mostPopular",
40
49
  strategyId: "",
50
+ nodeConfigVersion: C,
41
51
  textTrimming: !0,
42
52
  unresponsive: !1,
43
53
  size: "6",
@@ -46,21 +56,23 @@ function w() {
46
56
  priceHideIfSameAsDiscounted: !1
47
57
  };
48
58
  }
49
- function C() {
59
+ function G() {
50
60
  return {
51
- recommendationConfigs: w(),
61
+ recommendationConfigs: _(),
52
62
  recommendationProducts: [],
53
63
  filterStatus: !1,
54
64
  filterSelectionDrawerStatus: !1,
55
65
  assignStrategyDrawerStatus: !1,
56
66
  removeStrategyModalStatus: !1,
67
+ legacyStrategyUpdatedModalStatus: !1,
57
68
  filterGroup: 1,
58
69
  isInitialized: !1,
59
70
  filterSnapshot: null,
60
- syncedConfigFingerprint: null
71
+ syncedConfigFingerprint: null,
72
+ legacyConversionLoading: !1
61
73
  };
62
74
  }
63
- const z = () => ({
75
+ const Q = () => ({
64
76
  recommendationCampaignUrls: {},
65
77
  activePredictiveAlgorithms: [],
66
78
  languages: {},
@@ -69,11 +81,14 @@ const z = () => ({
69
81
  strategies: [],
70
82
  strategiesLoading: !1,
71
83
  strategyUrlTemplates: {},
84
+ strategiesFetched: !1,
85
+ strategiesSettled: !1,
72
86
  blockStates: {},
73
87
  currentRecommendationId: null,
88
+ strategyPanelOpen: !1,
74
89
  configVersion: 0
75
- }), Y = P("guidoRecommendationExtension", {
76
- state: () => z(),
90
+ }), ft = B("guidoRecommendationExtension", {
91
+ state: () => Q(),
77
92
  getters: {
78
93
  // ====================================================================
79
94
  // Proxy Getters — Backward Compatible Access to Current Block State
@@ -83,7 +98,7 @@ const z = () => ({
83
98
  * This allows all existing code that reads `store.recommendationConfigs` to work unchanged.
84
99
  */
85
100
  recommendationConfigs(t) {
86
- return t.currentRecommendationId !== null && t.blockStates[t.currentRecommendationId] ? t.blockStates[t.currentRecommendationId].recommendationConfigs : w();
101
+ return t.currentRecommendationId !== null && t.blockStates[t.currentRecommendationId] ? t.blockStates[t.currentRecommendationId].recommendationConfigs : _();
87
102
  },
88
103
  /**
89
104
  * Proxy getter: delegates to blockStates[currentRecommendationId].recommendationProducts
@@ -103,6 +118,15 @@ const z = () => ({
103
118
  removeStrategyModalStatus(t) {
104
119
  return t.currentRecommendationId !== null && t.blockStates[t.currentRecommendationId] ? t.blockStates[t.currentRecommendationId].removeStrategyModalStatus : !1;
105
120
  },
121
+ isLegacyStrategyBlock() {
122
+ return k().isFeatureEnabled("reusableRecommendationStrategy") && j(this.recommendationConfigs);
123
+ },
124
+ legacyConversionLoading(t) {
125
+ return t.currentRecommendationId !== null && t.blockStates[t.currentRecommendationId] ? t.blockStates[t.currentRecommendationId].legacyConversionLoading ?? !1 : !1;
126
+ },
127
+ legacyStrategyUpdatedModalStatus(t) {
128
+ return t.currentRecommendationId !== null && t.blockStates[t.currentRecommendationId] ? t.blockStates[t.currentRecommendationId].legacyStrategyUpdatedModalStatus : !1;
129
+ },
106
130
  assignedStrategy(t) {
107
131
  const { strategyId: e } = this.recommendationConfigs;
108
132
  return e ? t.strategies.find((r) => String(r.strategy_id) === e) ?? null : null;
@@ -138,8 +162,8 @@ const z = () => ({
138
162
  return [...new Set(t.map((e) => e.filterGroup))].sort((e, r) => e - r);
139
163
  },
140
164
  getActivePredictiveAlgorithms: (t) => {
141
- const e = b(), r = [];
142
- return t.activePredictiveAlgorithms.filter((n) => !L.includes(n)).forEach((n) => {
165
+ const e = F(), r = [];
166
+ return t.activePredictiveAlgorithms.filter((n) => !V.includes(n)).forEach((n) => {
143
167
  r.push(...e.filter((s) => s.id === n));
144
168
  }), r.map((n) => ({
145
169
  text: n.name,
@@ -157,7 +181,7 @@ const z = () => ({
157
181
  getFilterList() {
158
182
  return Object.values(this.filterList).filter((t) => t.isFilterable !== 0 && t.isFilterable !== !1).map((t) => {
159
183
  let e;
160
- return t.type === "productAttribute" ? e = `product_attributes.${t.attributeName}` : v.includes(t.attributeName) ? e = `${t.attributeName}.${this.recommendationConfigs.currencySettings.value}` : e = t.attributeName, {
184
+ return t.type === "productAttribute" ? e = `product_attributes.${t.attributeName}` : M.includes(t.attributeName) ? e = `${t.attributeName}.${this.recommendationConfigs.currencySettings.value}` : e = t.attributeName, {
161
185
  text: t.displayName,
162
186
  value: e,
163
187
  type: t.attributeType
@@ -179,7 +203,7 @@ const z = () => ({
179
203
  setCurrentBlock(t) {
180
204
  this.blockStates[t] || (this.blockStates = {
181
205
  ...this.blockStates,
182
- [t]: C()
206
+ [t]: G()
183
207
  }), this.currentRecommendationId = t;
184
208
  },
185
209
  /**
@@ -201,12 +225,16 @@ const z = () => ({
201
225
  /**
202
226
  * Deep-clones a block's state and mirrors its campaign URL under a new id.
203
227
  * Invoked from `onCreated` when a duplicate is detected (SD-142352).
228
+ *
229
+ * The clone keeps the source's data but drops its "already fetched / already
230
+ * synced" claims for strategy blocks, so it fetches again like a fresh drop
231
+ * (SD-149916).
204
232
  */
205
233
  cloneBlockState(t, e) {
206
234
  const r = this.blockStates[t];
207
235
  if (r) {
208
236
  const o = structuredClone(r);
209
- o.recommendationConfigs.id = e, this.blockStates = {
237
+ o.recommendationConfigs.id = e, o.recommendationConfigs.strategyId && k().isFeatureEnabled("reusableRecommendationStrategy") && (o.isInitialized = !1, o.syncedConfigFingerprint = null), this.blockStates = {
210
238
  ...this.blockStates,
211
239
  [e]: o
212
240
  };
@@ -239,7 +267,7 @@ const z = () => ({
239
267
  * `isInitialized`) alone.
240
268
  */
241
269
  seedBlockUrlConfig(t, e) {
242
- const r = (o, c) => o === "." || o === "," || o === " " ? o : c, n = {
270
+ const r = (o, a) => o === "." || o === "," || o === " " ? o : a, n = {
243
271
  name: e.currencyCode,
244
272
  value: e.currencyCode,
245
273
  symbol: e.currencySymbol,
@@ -247,7 +275,7 @@ const z = () => ({
247
275
  decimalCount: String(e.currencyDecimalCount),
248
276
  decimalSeparator: r(e.currencyDecimalSeparator, ","),
249
277
  thousandSeparator: r(e.currencyThousandSeparator, ".")
250
- }, s = !this.blockStates[t], i = s ? C() : this.blockStates[t];
278
+ }, s = !this.blockStates[t], i = s ? G() : this.blockStates[t];
251
279
  i.recommendationConfigs = {
252
280
  ...i.recommendationConfigs,
253
281
  strategy: e.strategy,
@@ -286,7 +314,7 @@ const z = () => ({
286
314
  * Creates a filter with the first available attribute and operator pre-selected.
287
315
  */
288
316
  createDefaultFilter(t, e) {
289
- const [r] = this.getFilterList, [n] = G(r == null ? void 0 : r.type);
317
+ const [r] = this.getFilterList, [n] = N(r == null ? void 0 : r.type);
290
318
  return {
291
319
  type: "standardFilter",
292
320
  attribute: (r == null ? void 0 : r.value) ?? "",
@@ -339,6 +367,12 @@ const z = () => ({
339
367
  closeAssignStrategyDrawer() {
340
368
  this.currentRecommendationId === null || !this.blockStates[this.currentRecommendationId] || (this.blockStates[this.currentRecommendationId].assignStrategyDrawerStatus = !1);
341
369
  },
370
+ openLegacyStrategyUpdatedModal() {
371
+ this.currentRecommendationId === null || !this.blockStates[this.currentRecommendationId] || (this.blockStates[this.currentRecommendationId].legacyStrategyUpdatedModalStatus = !0);
372
+ },
373
+ closeLegacyStrategyUpdatedModal() {
374
+ this.currentRecommendationId === null || !this.blockStates[this.currentRecommendationId] || (this.blockStates[this.currentRecommendationId].legacyStrategyUpdatedModalStatus = !1);
375
+ },
342
376
  openRemoveStrategyModal() {
343
377
  this.currentRecommendationId === null || !this.blockStates[this.currentRecommendationId] || (this.blockStates[this.currentRecommendationId].removeStrategyModalStatus = !0);
344
378
  },
@@ -346,29 +380,111 @@ const z = () => ({
346
380
  this.currentRecommendationId === null || !this.blockStates[this.currentRecommendationId] || (this.blockStates[this.currentRecommendationId].removeStrategyModalStatus = !1);
347
381
  },
348
382
  async fetchStrategies() {
349
- if (S) {
350
- await S;
383
+ for (; R; )
384
+ try {
385
+ await R;
386
+ } catch {
387
+ }
388
+ if (this.strategiesFetched)
351
389
  return;
352
- }
353
- S = (async () => {
390
+ const t = b, e = (async () => {
354
391
  this.strategiesLoading = !0;
355
392
  try {
356
- this.strategies = await u.fetchRecommendationStrategies();
393
+ const r = await m.fetchRecommendationStrategies();
394
+ if (t !== b)
395
+ return;
396
+ this.strategies = r, this.strategiesFetched = !0;
357
397
  } finally {
358
- this.strategiesLoading = !1;
398
+ this.strategiesLoading = !1, t === b && (this.strategiesSettled = !0);
359
399
  }
360
400
  })();
401
+ R = e;
361
402
  try {
362
- await S;
403
+ await e;
363
404
  } finally {
364
- S = null;
405
+ R = null;
365
406
  }
366
407
  },
408
+ /**
409
+ * Clears what a block inherited from the id it arrived holding, so the panel fetches for it.
410
+ * A re-dropped saved module can keep the source's id — and its "already fetched" entry —
411
+ * while the strategy's product count has moved on (SD-149916).
412
+ */
413
+ resetArrivedBlock(t) {
414
+ const e = this.blockStates[t];
415
+ e != null && e.recommendationConfigs.strategyId && (e.isInitialized = !1, e.syncedConfigFingerprint = null, this.invalidateStrategies(), this.fetchStrategies().catch(() => {
416
+ }));
417
+ },
418
+ invalidateStrategies() {
419
+ this.strategiesFetched = !1, this.strategiesSettled = !1, b += 1;
420
+ },
421
+ /**
422
+ * Opens a Smart Recommender strategy page in a new tab and refreshes the list every time
423
+ * the user comes back. `visibilitychange` only: a `focus` listener would also fire when
424
+ * focus merely returns from the Stripo canvas iframe to the panel, refetching and
425
+ * regenerating the canvas while the user never left the editor (SD-149916).
426
+ *
427
+ * Deliberately NOT one-shot: the strategy tab stays open, so a user can create a second
428
+ * strategy in it and come back again — a listener that disarmed on the first return would
429
+ * leave that one missing from the list. It disarms on the first return AFTER that tab is
430
+ * closed instead, so an ordinary alt-tab later in the session costs nothing. Armed only
431
+ * once the user actually opens a strategy page, so anyone who never does pays nothing.
432
+ * @returns whether a tab actually opened, so callers can skip work that assumes one did
433
+ */
434
+ _armStrategyPageReturn(t) {
435
+ const e = window.open(t, "_blank");
436
+ if (!e)
437
+ return D().showToaster({
438
+ type: A.Error,
439
+ message: L()("discovery-strategy.error-description")
440
+ }), !1;
441
+ e.opener = null, l == null || l();
442
+ let r = !1;
443
+ const n = () => {
444
+ document.hidden || r || (e != null && e.closed && (l == null || l()), r = !0, this.invalidateStrategies(), this.fetchStrategies().then(() => this._refreshAssignedStrategyProducts()).catch(() => {
445
+ }).finally(() => {
446
+ r = !1;
447
+ }));
448
+ };
449
+ return document.addEventListener("visibilitychange", n), l = () => {
450
+ document.removeEventListener("visibilitychange", n), l = null;
451
+ }, !0;
452
+ },
453
+ /**
454
+ * Reloads the products of the block the user is on, so an edit made in the strategy tab
455
+ * lands on the canvas instead of waiting for the assign drawer to be reopened.
456
+ *
457
+ * Only the selected block: the canvas regeneration is driven off the active block's
458
+ * product array, so refreshing an unselected block would fetch without redrawing it.
459
+ */
460
+ async _refreshAssignedStrategyProducts() {
461
+ var e;
462
+ const t = this.currentRecommendationId;
463
+ t === null || !((e = this.blockStates[t]) != null && e.recommendationConfigs.strategyId) || await this.fetchRecommendationProducts();
464
+ },
465
+ /** Lets the editor tear the return listeners down when it unmounts. */
466
+ disposeStrategyPageReturn() {
467
+ l == null || l();
468
+ },
469
+ /**
470
+ * Opens Smart Recommender's strategy creation page. Every return refreshes the list so the
471
+ * new strategy is offered; the user picks it themselves.
472
+ */
473
+ openStrategyCreation() {
474
+ return this._armStrategyPageReturn(E.RECOMMENDATION_STRATEGY_CREATE_PATH);
475
+ },
476
+ /**
477
+ * Opens the strategy's edit page. Every return refreshes the list so the block reflects the
478
+ * edited configuration.
479
+ */
480
+ openStrategyEdit(t) {
481
+ return this._armStrategyPageReturn(U(t));
482
+ },
367
483
  async resolveStrategyUrlTemplate(t, e) {
368
484
  const r = typeof t.updated_at == "string" ? t.updated_at : "", n = r ? `${t.strategy_id}:${r}:${e}` : "", s = n ? this.strategyUrlTemplates[n] : void 0;
369
485
  if (s)
370
486
  return s;
371
- const { full_path: i = "" } = await u.generateStrategyEndpoint(t, e) ?? {};
487
+ const { full_path: i = "" } = await m.generateStrategyEndpoint(t, e) ?? {};
372
488
  return n && i && (this.strategyUrlTemplates = {
373
489
  ...this.strategyUrlTemplates,
374
490
  [n]: i
@@ -380,24 +496,61 @@ const z = () => ({
380
496
  ...t.size !== void 0 ? { size: String(t.size) } : {}
381
497
  });
382
498
  },
499
+ async convertLegacyStrategy(t, e = { scope: "campaign" }) {
500
+ const r = this.currentRecommendationId;
501
+ if (r === null || !this.blockStates[r])
502
+ throw new Error("[Guido] convertLegacyStrategy called without an active block");
503
+ this.blockStates[r].legacyConversionLoading = !0;
504
+ try {
505
+ const n = await m.migrateLegacyStrategy(t, e);
506
+ try {
507
+ await this.fetchStrategies();
508
+ } catch (o) {
509
+ console.error("[Guido] strategy listing refresh after conversion failed", o);
510
+ }
511
+ const s = this.strategies.find((o) => String(o.strategy_id) === n), i = this.blockStates[r];
512
+ return i && (i.recommendationConfigs = {
513
+ ...i.recommendationConfigs,
514
+ strategyId: n,
515
+ nodeConfigVersion: C,
516
+ ...(s == null ? void 0 : s.size) !== void 0 ? { size: String(s.size) } : {}
517
+ }), this.currentRecommendationId === r && this.configVersion++, n;
518
+ } finally {
519
+ this.blockStates[r] && (this.blockStates[r].legacyConversionLoading = !1);
520
+ }
521
+ },
522
+ /**
523
+ * Undoes the config half of a conversion whose document write never landed, so the
524
+ * panel and the drawer fall back to "Legacy Strategy" instead of showing an assignment
525
+ * the document never received.
526
+ */
527
+ revertLegacyConversion(t, e) {
528
+ const r = this.blockStates[t];
529
+ r && (r.recommendationConfigs = { ...r.recommendationConfigs, ...e }, this.currentRecommendationId === t && this.configVersion++);
530
+ },
383
531
  removeStrategy() {
384
- this.patchCurrentBlockConfig({ strategyId: "", size: N.size });
532
+ this.patchCurrentBlockConfig({
533
+ strategyId: "",
534
+ strategy: "",
535
+ size: x.size,
536
+ nodeConfigVersion: C
537
+ });
385
538
  },
386
539
  // ====================================================================
387
540
  // Shared Data Fetching (fetched once, used by all blocks)
388
541
  // ====================================================================
389
542
  async fetchRecommendationCreateData() {
390
543
  if (!this.activePredictiveAlgorithms.length) {
391
- if (f) {
392
- await f;
544
+ if (S) {
545
+ await S;
393
546
  return;
394
547
  }
395
- f = (async () => {
548
+ S = (async () => {
396
549
  const {
397
550
  activePredictiveAlgorithms: t,
398
551
  languages: e,
399
552
  currencies: r
400
- } = await u.fetchRecommendationCreateData();
553
+ } = await m.fetchRecommendationCreateData();
401
554
  if (this.activePredictiveAlgorithms = t, this.languages = e, this.currentRecommendationId !== null && this.blockStates[this.currentRecommendationId]) {
402
555
  const n = this.blockStates[this.currentRecommendationId];
403
556
  n.filterStatus = !!n.recommendationConfigs.filters.length;
@@ -405,26 +558,26 @@ const z = () => ({
405
558
  this.currencyList = r;
406
559
  })();
407
560
  try {
408
- await f;
561
+ await S;
409
562
  } finally {
410
- f = null;
563
+ S = null;
411
564
  }
412
565
  }
413
566
  },
414
567
  async fetchRecommendationFilters() {
415
568
  if (!Object.keys(this.filterList).length) {
416
- if (g) {
417
- await g;
569
+ if (p) {
570
+ await p;
418
571
  return;
419
572
  }
420
- g = (async () => {
421
- const t = await u.fetchRecommendationFilters();
573
+ p = (async () => {
574
+ const t = await m.fetchRecommendationFilters();
422
575
  this.filterList = t;
423
576
  })();
424
577
  try {
425
- await g;
578
+ await p;
426
579
  } finally {
427
- g = null;
580
+ p = null;
428
581
  }
429
582
  }
430
583
  },
@@ -453,7 +606,7 @@ const z = () => ({
453
606
  const n = [...e.recommendationConfigs.filters];
454
607
  n[r] = {
455
608
  ...t,
456
- isValid: B(t)
609
+ isValid: K(t)
457
610
  }, e.recommendationConfigs.filters = n;
458
611
  }
459
612
  },
@@ -467,10 +620,10 @@ const z = () => ({
467
620
  let i = 1;
468
621
  n = n.map((o) => o.filterGroup === t.filterGroup ? { ...o, filterNumber: i++ } : o);
469
622
  } else {
470
- const i = [...new Set(n.map((c) => c.filterGroup))].sort((c, a) => c - a), o = new Map(i.map((c, a) => [c, a + 1]));
471
- n = n.map((c) => ({
472
- ...c,
473
- filterGroup: o.get(c.filterGroup) ?? c.filterGroup
623
+ const i = [...new Set(n.map((a) => a.filterGroup))].sort((a, d) => a - d), o = new Map(i.map((a, d) => [a, d + 1]));
624
+ n = n.map((a) => ({
625
+ ...a,
626
+ filterGroup: o.get(a.filterGroup) ?? a.filterGroup
474
627
  }));
475
628
  }
476
629
  e.recommendationConfigs.filters = n;
@@ -491,7 +644,7 @@ const z = () => ({
491
644
  }), e.recommendationConfigs.filters = r;
492
645
  },
493
646
  generateFilterQuery() {
494
- return k(this.recommendationConfigs.filters);
647
+ return P(this.recommendationConfigs.filters);
495
648
  },
496
649
  /**
497
650
  * Validation-only check invoked at save-CTA time. Defined as an action
@@ -499,33 +652,52 @@ const z = () => ({
499
652
  * every block's recommendationConfigs across user edits.
500
653
  */
501
654
  hasInvalidBlock() {
502
- return Object.values(this.blockStates).some((t) => !M(t.recommendationConfigs, this));
655
+ return Object.values(this.blockStates).some((t) => !J(t.recommendationConfigs, this));
656
+ },
657
+ /**
658
+ * Which required fields are missing across the design's blocks, so the save
659
+ * toaster can name the cause instead of always showing the generic message.
660
+ */
661
+ invalidBlockFields() {
662
+ return Object.values(this.blockStates).flatMap((t) => H(t.recommendationConfigs, this));
503
663
  },
504
664
  // ====================================================================
505
665
  // Per-Block Product Fetching
506
666
  // ====================================================================
507
667
  async fetchRecommendationProducts() {
508
- if (!(this.currentRecommendationId === null || !this.blockStates[this.currentRecommendationId])) {
509
- if (h) {
510
- await h;
511
- return;
512
- }
513
- h = this._doFetchProducts();
514
- try {
515
- await h;
516
- } finally {
517
- h = null;
518
- }
668
+ if (this.currentRecommendationId === null || !this.blockStates[this.currentRecommendationId])
669
+ return;
670
+ const t = [
671
+ this.currentRecommendationId,
672
+ this.configVersion,
673
+ b
674
+ ].join(":");
675
+ if (y && I === t) {
676
+ await y;
677
+ return;
678
+ }
679
+ I = t, y = this._doFetchProducts();
680
+ try {
681
+ await y;
682
+ } finally {
683
+ I === t && (y = null, I = "");
519
684
  }
520
685
  },
521
686
  async _doFetchProducts() {
522
- var d;
523
- const t = this.currentRecommendationId, e = this.blockStates[t], { recommendationConfigs: r } = e, n = F();
687
+ var h;
688
+ const t = this.currentRecommendationId, e = this.blockStates[t], { recommendationConfigs: r } = e, n = k();
524
689
  if (r.strategyId && n.isFeatureEnabled("reusableRecommendationStrategy")) {
525
690
  await this._doFetchStrategyProducts(t);
526
691
  return;
527
692
  }
528
- const s = r.filters.filter((m) => m.isValid), i = k(s), o = ((d = b().find((m) => m.key === r.strategy)) == null ? void 0 : d.path) || "", c = parseInt(r.size) || 6, a = {
693
+ const s = (g.get(t) ?? 0) + 1;
694
+ g.set(t, s);
695
+ const i = parseInt(r.size) || 6;
696
+ if (!r.strategy) {
697
+ this._setBlockProducts(t, [], i);
698
+ return;
699
+ }
700
+ const o = r.filters.filter((f) => f.isValid), a = P(o), d = ((h = F().find((f) => f.key === r.strategy)) == null ? void 0 : h.path) || "", c = {
529
701
  locale: r.language,
530
702
  currency: r.currencySettings.value,
531
703
  partnerName: n.partnerName,
@@ -533,21 +705,30 @@ const z = () => ({
533
705
  details: !0,
534
706
  campaignId: n.variationId
535
707
  };
536
- r.strategy === "manualMerchandising" ? a.productId = A(r.productIds) : r.strategy === "similarViewed" && (a.productId = "{itemId}"), r.strategy === "userBased" && (a.userId = "{user_id}"), i && (a.filter = i), r.shuffleProducts && (a.shuffle = !0), Object.assign(
537
- a,
538
- x(n.partnerName, r.strategy)
708
+ r.strategy === "manualMerchandising" ? c.productId = q(r.productIds) : r.strategy === "similarViewed" && (c.productId = "{itemId}"), r.strategy === "userBased" && (c.userId = "{user_id}"), a && (c.filter = a), r.shuffleProducts && (c.shuffle = !0), Object.assign(
709
+ c,
710
+ $(n.partnerName, r.strategy)
539
711
  );
540
- let l;
712
+ let u;
541
713
  try {
542
- l = await u.fetchRecommendationProducts(o, a);
714
+ u = await m.fetchRecommendationProducts(d, c);
543
715
  } catch {
544
- l = [];
716
+ u = [];
545
717
  }
546
- this._setBlockProducts(t, l, c);
718
+ g.get(t) === s && this._setBlockProducts(t, u, i);
719
+ },
720
+ /**
721
+ * Block-scoped counterpart of `fetchRecommendationProducts`, which only ever serves the
722
+ * selected block. The legacy conversion must refresh the products of the block that
723
+ * started it even after the user selected another one.
724
+ */
725
+ async fetchStrategyProductsForBlock(t) {
726
+ var e;
727
+ (e = this.blockStates[t]) != null && e.recommendationConfigs.strategyId && await this._doFetchStrategyProducts(t);
547
728
  },
548
729
  async _doFetchStrategyProducts(t) {
549
- const e = (p.get(t) ?? 0) + 1;
550
- p.set(t, e);
730
+ const e = (g.get(t) ?? 0) + 1;
731
+ g.set(t, e);
551
732
  const { recommendationConfigs: r } = this.blockStates[t];
552
733
  let n = [], s = parseInt(r.size) || 6;
553
734
  try {
@@ -557,25 +738,25 @@ const z = () => ({
557
738
  );
558
739
  if (i) {
559
740
  s = parseInt(String(i.size ?? "")) || s;
560
- const o = r.currencySettings.value, a = (await this.resolveStrategyUrlTemplate(i, o)).replaceAll("{locale}", r.language).replaceAll("{currency}", o), l = a.indexOf("?"), d = l === -1 ? a : a.slice(0, l), m = (l === -1 ? "" : a.slice(l + 1)).split("&").filter((I) => I && !I.startsWith("user_id=")), y = m.length ? `${d}?${m.join("&")}` : d;
561
- y && (n = await u.fetchRecommendationProductsByUrl(y));
741
+ const o = r.currencySettings.value, d = (await this.resolveStrategyUrlTemplate(i, o)).replaceAll("{locale}", r.language).replaceAll("{currency}", o), c = d.indexOf("?"), u = c === -1 ? d : d.slice(0, c), h = (c === -1 ? "" : d.slice(c + 1)).split("&").filter((v) => v && !v.startsWith("user_id=")), f = h.length ? `${u}?${h.join("&")}` : u;
742
+ f && (n = await m.fetchRecommendationProductsByUrl(f));
562
743
  }
563
744
  } catch {
564
745
  n = [];
565
746
  }
566
- p.get(t) === e && this._setBlockProducts(t, n, s);
747
+ g.get(t) === e && this._setBlockProducts(t, n, s);
567
748
  },
568
749
  _setBlockProducts(t, e, r) {
569
750
  if (!this.blockStates[t])
570
751
  return;
571
- const n = e.length > 0 ? e : R(r);
752
+ const n = e.length > 0 ? e : w(r);
572
753
  this.blockStates[t].recommendationProducts = [
573
754
  ...n.slice(0, r),
574
- ...R(Math.max(0, r - n.length))
755
+ ...w(Math.max(0, r - n.length))
575
756
  ];
576
757
  }
577
758
  }
578
759
  });
579
760
  export {
580
- Y as useRecommendationExtensionStore
761
+ ft as useRecommendationExtensionStore
581
762
  };
@@ -0,0 +1,42 @@
1
+ import "../constants/selectors.js";
2
+ import { RRS_CONFIG_VERSION as m } from "../constants/defaultConfig.js";
3
+ import { sanitizeProductIds as p } from "./productIds.js";
4
+ const d = "manualMerchandising";
5
+ function N({ strategyId: e, nodeConfigVersion: t }) {
6
+ return !e && t < m;
7
+ }
8
+ const o = (e) => {
9
+ const t = String(e ?? "").trim();
10
+ return /^[1-9]\d*$/.test(t) ? t : null;
11
+ };
12
+ function f({ campaignId: e, templateLibraryId: t }) {
13
+ if (o(e))
14
+ return { scope: "campaign" };
15
+ const n = o(t);
16
+ return n ? { scope: "template", templateId: n } : { scope: "campaign" };
17
+ }
18
+ function y({
19
+ blockId: e,
20
+ url: t,
21
+ campaignId: n,
22
+ variationId: a,
23
+ strategy: c,
24
+ productIds: s,
25
+ target: r
26
+ }) {
27
+ const i = c === d ? p(s).split(",").filter(Boolean) : [];
28
+ return {
29
+ ...r.scope === "template" ? { templateId: Number.parseInt(r.templateId) } : {
30
+ campaignId: Number.parseInt(n) || 0,
31
+ variationId: Number.parseInt(a) || 0
32
+ },
33
+ blockId: String(e),
34
+ url: t,
35
+ ...i.length ? { productIds: i } : {}
36
+ };
37
+ }
38
+ export {
39
+ y as buildLegacyMigrationPayload,
40
+ N as isLegacyStrategyConfig,
41
+ f as resolveMigrationTarget
42
+ };