@useinsider/guido 3.19.0-beta.f02eaa1 → 3.19.0-beta.f0d5980

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 (72) hide show
  1. package/dist/@types/config/schemas.js +104 -91
  2. package/dist/components/Guido.vue.js +8 -8
  3. package/dist/components/Guido.vue2.js +86 -81
  4. package/dist/components/organisms/base/AuthErrorModal.vue.js +18 -0
  5. package/dist/components/organisms/base/AuthErrorModal.vue2.js +42 -0
  6. package/dist/components/organisms/base/Toaster.vue2.js +13 -12
  7. package/dist/components/organisms/extensions/recommendation/AssignStrategyDrawer.vue.js +9 -7
  8. package/dist/components/organisms/extensions/recommendation/AssignStrategyDrawer.vue2.js +69 -52
  9. package/dist/components/organisms/extensions/recommendation/RemoveStrategyModal.vue2.js +28 -44
  10. package/dist/components/organisms/header/EditorActions.vue.js +4 -4
  11. package/dist/components/organisms/header/EditorActions.vue2.js +23 -25
  12. package/dist/components/organisms/onboarding/RecommendationStrategyOnboarding.vue.js +3 -3
  13. package/dist/components/organisms/onboarding/RecommendationStrategyOnboarding.vue2.js +21 -20
  14. package/dist/components/wrappers/WpModal.vue.js +1 -1
  15. package/dist/components/wrappers/WpModal.vue2.js +4 -3
  16. package/dist/composables/useActionsApi.js +52 -47
  17. package/dist/composables/useAuthError.js +52 -0
  18. package/dist/composables/useCodeEditorApi.js +23 -9
  19. package/dist/composables/useCustomInterfaceAppearance.js +61 -51
  20. package/dist/composables/useHtmlValidator.js +87 -82
  21. package/dist/composables/useHttp.js +64 -54
  22. package/dist/composables/useLegacyStrategyConversion.js +23 -19
  23. package/dist/composables/useRecommendation.js +40 -35
  24. package/dist/composables/useStripo.js +43 -39
  25. package/dist/composables/useToaster.js +18 -22
  26. package/dist/composables/useTranslations.js +3 -2
  27. package/dist/composables/useVersionHistoryApi.js +33 -24
  28. package/dist/composables/validators/useLiquidValidator.js +15 -15
  29. package/dist/config/migrator/index.js +14 -13
  30. package/dist/config/migrator/recommendationPartnerMigrator.js +50 -0
  31. package/dist/enums/extensions/recommendationBlock.js +43 -35
  32. package/dist/extensions/Blocks/Recommendation/block.js +65 -45
  33. package/dist/extensions/Blocks/Recommendation/constants/defaultConfig.js +21 -20
  34. package/dist/extensions/Blocks/Recommendation/controls/main/index.js +70 -58
  35. package/dist/extensions/Blocks/Recommendation/controls/main/strategy.js +133 -79
  36. package/dist/extensions/Blocks/Recommendation/controls/main/utils.js +96 -97
  37. package/dist/extensions/Blocks/Recommendation/store/recommendation.js +212 -94
  38. package/dist/extensions/Blocks/Recommendation/utils/legacyStrategyMigration.js +1 -1
  39. package/dist/extensions/Blocks/Recommendation/utils/nodeConfigWriter.js +26 -16
  40. package/dist/extensions/Blocks/Recommendation/utils/partnerScope.js +25 -0
  41. package/dist/extensions/Blocks/Recommendation/utils/strategyCommit.js +24 -0
  42. package/dist/extensions/Blocks/Recommendation/validation/requiredFields.js +21 -12
  43. package/dist/guido.css +1 -1
  44. package/dist/src/@types/config/schemas.d.ts +16 -0
  45. package/dist/src/components/organisms/base/AuthErrorModal.vue.d.ts +2 -0
  46. package/dist/src/components/wrappers/WpModal.vue.d.ts +5 -1
  47. package/dist/src/composables/useAuthError.d.ts +12 -0
  48. package/dist/src/composables/useConfig.d.ts +2 -0
  49. package/dist/src/composables/useHttp.d.ts +2 -0
  50. package/dist/src/config/migrator/index.d.ts +2 -1
  51. package/dist/src/config/migrator/recommendationPartnerMigrator.d.ts +19 -0
  52. package/dist/src/enums/extensions/recommendationBlock.d.ts +2 -0
  53. package/dist/src/extensions/Blocks/Recommendation/block.d.ts +10 -0
  54. package/dist/src/extensions/Blocks/Recommendation/constants/defaultConfig.d.ts +1 -0
  55. package/dist/src/extensions/Blocks/Recommendation/constants/index.d.ts +1 -1
  56. package/dist/src/extensions/Blocks/Recommendation/controls/main/index.d.ts +2 -0
  57. package/dist/src/extensions/Blocks/Recommendation/controls/main/strategy.d.ts +8 -0
  58. package/dist/src/extensions/Blocks/Recommendation/controls/main/utils.d.ts +7 -6
  59. package/dist/src/extensions/Blocks/Recommendation/store/recommendation.d.ts +92 -0
  60. package/dist/src/extensions/Blocks/Recommendation/types/nodeConfig.d.ts +6 -0
  61. package/dist/src/extensions/Blocks/Recommendation/utils/legacyStrategyMigration.d.ts +1 -1
  62. package/dist/src/extensions/Blocks/Recommendation/utils/partnerScope.d.ts +33 -0
  63. package/dist/src/extensions/Blocks/Recommendation/utils/strategyCommit.d.ts +14 -0
  64. package/dist/src/extensions/Blocks/Recommendation/validation/requiredFields.d.ts +4 -0
  65. package/dist/src/stores/config.d.ts +20 -0
  66. package/dist/src/utils/stripoEditorApi.d.ts +4 -0
  67. package/dist/static/styles/components/recommendation-strategy.css.js +43 -4
  68. package/dist/static/styles/components/skeleton.css.js +23 -0
  69. package/dist/stores/unsubscribe.js +3 -3
  70. package/dist/utils/stripoEditorApi.js +11 -0
  71. package/dist/utils/templatePreparation.js +55 -55
  72. package/package.json +2 -1
@@ -1,23 +1,27 @@
1
- import { getRecommendationFeedSourceMaps as R, getOperatorOptions as F, PriceAttributes as P } from "../../../../enums/extensions/recommendationBlock.js";
2
- import { useRecommendationApi as D } from "../../../../services/recommendationApi.js";
3
- import { useConfigStore as k } from "../../../../stores/config.js";
4
- import { defineStore as L } from "pinia";
1
+ import { useToaster as _ } from "../../../../composables/useToaster.js";
2
+ import { useTranslations as L } from "../../../../composables/useTranslations.js";
3
+ import { getRecommendationFeedSourceMaps as F, strategyEditPath as U, strategyCreatePath as N, getOperatorOptions as M, PriceAttributes as E } from "../../../../enums/extensions/recommendationBlock.js";
4
+ import { ToasterTypeOptions as B } from "../../../../enums/toaster.js";
5
+ import { useRecommendationApi as z } from "../../../../services/recommendationApi.js";
6
+ import { useConfigStore as R } from "../../../../stores/config.js";
7
+ import { defineStore as A } from "pinia";
5
8
  import "../constants/selectors.js";
6
- import { DEFAULT_MOBILE_CARDS_IN_ROW as U, DEFAULT_CARDS_IN_ROW as _ } from "../constants/layout.js";
7
- import { CURRENT_CONFIG_VERSION as y, DEFAULT_NODE_CONFIG as N, EXCLUDED_ALGORITHM_IDS as M } from "../constants/defaultConfig.js";
8
- import { getDefaultProducts as C } from "../templates/utils.js";
9
- import { generateCompleteFilterQuery as v } from "../utils/filterUtil.js";
10
- import { isLegacyStrategyConfig as x } from "../utils/legacyStrategyMigration.js";
11
- import { getPartnerRecommendationParams as B } from "../utils/partnerCustomizations.js";
12
- import { sanitizeProductIds as z } from "../utils/productIds.js";
13
- import { isFilterValid as A } from "../validation/filterSchema.js";
14
- import { isConfigValid as O } from "../validation/requiredFields.js";
15
- const m = D();
16
- let f = null, g = null, h = null, S = null;
17
- const p = /* @__PURE__ */ new Map();
18
- function G() {
9
+ import { DEFAULT_MOBILE_CARDS_IN_ROW as O, DEFAULT_CARDS_IN_ROW as x } from "../constants/layout.js";
10
+ import { CURRENT_CONFIG_VERSION as C, DEFAULT_NODE_CONFIG as T, 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 = z();
19
+ let S = null, p = null, y = null, I = "", k = null, b = 0;
20
+ const g = /* @__PURE__ */ new Map();
21
+ let l = null;
22
+ function D() {
19
23
  return {
20
- cardsInRow: _,
24
+ cardsInRow: x,
21
25
  currencySettings: {
22
26
  name: "USD",
23
27
  value: "USD",
@@ -31,15 +35,19 @@ function G() {
31
35
  productIds: [],
32
36
  id: 1,
33
37
  language: "en_US",
34
- mobileCardsInRow: U,
38
+ mobileCardsInRow: O,
35
39
  mobileLayoutEnabled: !1,
36
40
  orientation: "grid",
37
41
  recommendedProducts: [],
38
42
  sendProductRequestFlag: !1,
39
43
  shuffleProducts: !1,
40
- 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: R().isFeatureEnabled("reusableRecommendationStrategy") ? "" : "mostPopular",
41
49
  strategyId: "",
42
- nodeConfigVersion: y,
50
+ nodeConfigVersion: C,
43
51
  textTrimming: !0,
44
52
  unresponsive: !1,
45
53
  size: "6",
@@ -48,9 +56,9 @@ function G() {
48
56
  priceHideIfSameAsDiscounted: !1
49
57
  };
50
58
  }
51
- function w() {
59
+ function G() {
52
60
  return {
53
- recommendationConfigs: G(),
61
+ recommendationConfigs: D(),
54
62
  recommendationProducts: [],
55
63
  filterStatus: !1,
56
64
  filterSelectionDrawerStatus: !1,
@@ -64,7 +72,7 @@ function w() {
64
72
  legacyConversionLoading: !1
65
73
  };
66
74
  }
67
- const E = () => ({
75
+ const Q = () => ({
68
76
  recommendationCampaignUrls: {},
69
77
  activePredictiveAlgorithms: [],
70
78
  languages: {},
@@ -73,11 +81,14 @@ const E = () => ({
73
81
  strategies: [],
74
82
  strategiesLoading: !1,
75
83
  strategyUrlTemplates: {},
84
+ strategiesFetched: !1,
85
+ strategiesSettled: !1,
76
86
  blockStates: {},
77
87
  currentRecommendationId: null,
88
+ strategyPanelOpen: !1,
78
89
  configVersion: 0
79
- }), et = L("guidoRecommendationExtension", {
80
- state: () => E(),
90
+ }), ft = A("guidoRecommendationExtension", {
91
+ state: () => Q(),
81
92
  getters: {
82
93
  // ====================================================================
83
94
  // Proxy Getters — Backward Compatible Access to Current Block State
@@ -87,7 +98,7 @@ const E = () => ({
87
98
  * This allows all existing code that reads `store.recommendationConfigs` to work unchanged.
88
99
  */
89
100
  recommendationConfigs(t) {
90
- return t.currentRecommendationId !== null && t.blockStates[t.currentRecommendationId] ? t.blockStates[t.currentRecommendationId].recommendationConfigs : G();
101
+ return t.currentRecommendationId !== null && t.blockStates[t.currentRecommendationId] ? t.blockStates[t.currentRecommendationId].recommendationConfigs : D();
91
102
  },
92
103
  /**
93
104
  * Proxy getter: delegates to blockStates[currentRecommendationId].recommendationProducts
@@ -108,10 +119,10 @@ const E = () => ({
108
119
  return t.currentRecommendationId !== null && t.blockStates[t.currentRecommendationId] ? t.blockStates[t.currentRecommendationId].removeStrategyModalStatus : !1;
109
120
  },
110
121
  isLegacyStrategyBlock() {
111
- return k().isFeatureEnabled("reusableRecommendationStrategy") && x(this.recommendationConfigs);
122
+ return R().isFeatureEnabled("reusableRecommendationStrategy") && j(this.recommendationConfigs);
112
123
  },
113
124
  legacyConversionLoading(t) {
114
- return t.currentRecommendationId !== null && t.blockStates[t.currentRecommendationId] ? t.blockStates[t.currentRecommendationId].legacyConversionLoading : !1;
125
+ return t.currentRecommendationId !== null && t.blockStates[t.currentRecommendationId] ? t.blockStates[t.currentRecommendationId].legacyConversionLoading ?? !1 : !1;
115
126
  },
116
127
  legacyStrategyUpdatedModalStatus(t) {
117
128
  return t.currentRecommendationId !== null && t.blockStates[t.currentRecommendationId] ? t.blockStates[t.currentRecommendationId].legacyStrategyUpdatedModalStatus : !1;
@@ -151,8 +162,8 @@ const E = () => ({
151
162
  return [...new Set(t.map((e) => e.filterGroup))].sort((e, r) => e - r);
152
163
  },
153
164
  getActivePredictiveAlgorithms: (t) => {
154
- const e = R(), r = [];
155
- return t.activePredictiveAlgorithms.filter((n) => !M.includes(n)).forEach((n) => {
165
+ const e = F(), r = [];
166
+ return t.activePredictiveAlgorithms.filter((n) => !V.includes(n)).forEach((n) => {
156
167
  r.push(...e.filter((s) => s.id === n));
157
168
  }), r.map((n) => ({
158
169
  text: n.name,
@@ -170,7 +181,7 @@ const E = () => ({
170
181
  getFilterList() {
171
182
  return Object.values(this.filterList).filter((t) => t.isFilterable !== 0 && t.isFilterable !== !1).map((t) => {
172
183
  let e;
173
- return t.type === "productAttribute" ? e = `product_attributes.${t.attributeName}` : P.includes(t.attributeName) ? e = `${t.attributeName}.${this.recommendationConfigs.currencySettings.value}` : e = t.attributeName, {
184
+ return t.type === "productAttribute" ? e = `product_attributes.${t.attributeName}` : E.includes(t.attributeName) ? e = `${t.attributeName}.${this.recommendationConfigs.currencySettings.value}` : e = t.attributeName, {
174
185
  text: t.displayName,
175
186
  value: e,
176
187
  type: t.attributeType
@@ -192,7 +203,7 @@ const E = () => ({
192
203
  setCurrentBlock(t) {
193
204
  this.blockStates[t] || (this.blockStates = {
194
205
  ...this.blockStates,
195
- [t]: w()
206
+ [t]: G()
196
207
  }), this.currentRecommendationId = t;
197
208
  },
198
209
  /**
@@ -214,12 +225,16 @@ const E = () => ({
214
225
  /**
215
226
  * Deep-clones a block's state and mirrors its campaign URL under a new id.
216
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).
217
232
  */
218
233
  cloneBlockState(t, e) {
219
234
  const r = this.blockStates[t];
220
235
  if (r) {
221
236
  const o = structuredClone(r);
222
- o.recommendationConfigs.id = e, this.blockStates = {
237
+ o.recommendationConfigs.id = e, o.recommendationConfigs.strategyId && R().isFeatureEnabled("reusableRecommendationStrategy") && (o.isInitialized = !1, o.syncedConfigFingerprint = null), this.blockStates = {
223
238
  ...this.blockStates,
224
239
  [e]: o
225
240
  };
@@ -252,7 +267,7 @@ const E = () => ({
252
267
  * `isInitialized`) alone.
253
268
  */
254
269
  seedBlockUrlConfig(t, e) {
255
- const r = (o, c) => o === "." || o === "," || o === " " ? o : c, n = {
270
+ const r = (o, a) => o === "." || o === "," || o === " " ? o : a, n = {
256
271
  name: e.currencyCode,
257
272
  value: e.currencyCode,
258
273
  symbol: e.currencySymbol,
@@ -260,7 +275,7 @@ const E = () => ({
260
275
  decimalCount: String(e.currencyDecimalCount),
261
276
  decimalSeparator: r(e.currencyDecimalSeparator, ","),
262
277
  thousandSeparator: r(e.currencyThousandSeparator, ".")
263
- }, s = !this.blockStates[t], i = s ? w() : this.blockStates[t];
278
+ }, s = !this.blockStates[t], i = s ? G() : this.blockStates[t];
264
279
  i.recommendationConfigs = {
265
280
  ...i.recommendationConfigs,
266
281
  strategy: e.strategy,
@@ -299,7 +314,7 @@ const E = () => ({
299
314
  * Creates a filter with the first available attribute and operator pre-selected.
300
315
  */
301
316
  createDefaultFilter(t, e) {
302
- const [r] = this.getFilterList, [n] = F(r == null ? void 0 : r.type);
317
+ const [r] = this.getFilterList, [n] = M(r == null ? void 0 : r.type);
303
318
  return {
304
319
  type: "standardFilter",
305
320
  attribute: (r == null ? void 0 : r.value) ?? "",
@@ -365,24 +380,106 @@ const E = () => ({
365
380
  this.currentRecommendationId === null || !this.blockStates[this.currentRecommendationId] || (this.blockStates[this.currentRecommendationId].removeStrategyModalStatus = !1);
366
381
  },
367
382
  async fetchStrategies() {
368
- if (S) {
369
- await S;
383
+ for (; k; )
384
+ try {
385
+ await k;
386
+ } catch {
387
+ }
388
+ if (this.strategiesFetched)
370
389
  return;
371
- }
372
- S = (async () => {
390
+ const t = b, e = (async () => {
373
391
  this.strategiesLoading = !0;
374
392
  try {
375
- this.strategies = await m.fetchRecommendationStrategies();
393
+ const r = await m.fetchRecommendationStrategies();
394
+ if (t !== b)
395
+ return;
396
+ this.strategies = r, this.strategiesFetched = !0;
376
397
  } finally {
377
- this.strategiesLoading = !1;
398
+ this.strategiesLoading = !1, t === b && (this.strategiesSettled = !0);
378
399
  }
379
400
  })();
401
+ k = e;
380
402
  try {
381
- await S;
403
+ await e;
382
404
  } finally {
383
- S = null;
405
+ k = null;
384
406
  }
385
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 _().showToaster({
438
+ type: B.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(N());
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
+ },
386
483
  async resolveStrategyUrlTemplate(t, e) {
387
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;
388
485
  if (s)
@@ -406,6 +503,7 @@ const E = () => ({
406
503
  this.blockStates[r].legacyConversionLoading = !0;
407
504
  try {
408
505
  const n = await m.migrateLegacyStrategy(t, e);
506
+ this.invalidateStrategies();
409
507
  try {
410
508
  await this.fetchStrategies();
411
509
  } catch (o) {
@@ -415,7 +513,7 @@ const E = () => ({
415
513
  return i && (i.recommendationConfigs = {
416
514
  ...i.recommendationConfigs,
417
515
  strategyId: n,
418
- nodeConfigVersion: y,
516
+ nodeConfigVersion: C,
419
517
  ...(s == null ? void 0 : s.size) !== void 0 ? { size: String(s.size) } : {}
420
518
  }), this.currentRecommendationId === r && this.configVersion++, n;
421
519
  } finally {
@@ -434,8 +532,9 @@ const E = () => ({
434
532
  removeStrategy() {
435
533
  this.patchCurrentBlockConfig({
436
534
  strategyId: "",
437
- size: N.size,
438
- nodeConfigVersion: y
535
+ strategy: "",
536
+ size: T.size,
537
+ nodeConfigVersion: C
439
538
  });
440
539
  },
441
540
  // ====================================================================
@@ -443,11 +542,11 @@ const E = () => ({
443
542
  // ====================================================================
444
543
  async fetchRecommendationCreateData() {
445
544
  if (!this.activePredictiveAlgorithms.length) {
446
- if (f) {
447
- await f;
545
+ if (S) {
546
+ await S;
448
547
  return;
449
548
  }
450
- f = (async () => {
549
+ S = (async () => {
451
550
  const {
452
551
  activePredictiveAlgorithms: t,
453
552
  languages: e,
@@ -460,26 +559,26 @@ const E = () => ({
460
559
  this.currencyList = r;
461
560
  })();
462
561
  try {
463
- await f;
562
+ await S;
464
563
  } finally {
465
- f = null;
564
+ S = null;
466
565
  }
467
566
  }
468
567
  },
469
568
  async fetchRecommendationFilters() {
470
569
  if (!Object.keys(this.filterList).length) {
471
- if (g) {
472
- await g;
570
+ if (p) {
571
+ await p;
473
572
  return;
474
573
  }
475
- g = (async () => {
574
+ p = (async () => {
476
575
  const t = await m.fetchRecommendationFilters();
477
576
  this.filterList = t;
478
577
  })();
479
578
  try {
480
- await g;
579
+ await p;
481
580
  } finally {
482
- g = null;
581
+ p = null;
483
582
  }
484
583
  }
485
584
  },
@@ -508,7 +607,7 @@ const E = () => ({
508
607
  const n = [...e.recommendationConfigs.filters];
509
608
  n[r] = {
510
609
  ...t,
511
- isValid: A(t)
610
+ isValid: K(t)
512
611
  }, e.recommendationConfigs.filters = n;
513
612
  }
514
613
  },
@@ -522,10 +621,10 @@ const E = () => ({
522
621
  let i = 1;
523
622
  n = n.map((o) => o.filterGroup === t.filterGroup ? { ...o, filterNumber: i++ } : o);
524
623
  } else {
525
- const i = [...new Set(n.map((c) => c.filterGroup))].sort((c, a) => c - a), o = new Map(i.map((c, a) => [c, a + 1]));
526
- n = n.map((c) => ({
527
- ...c,
528
- filterGroup: o.get(c.filterGroup) ?? c.filterGroup
624
+ const i = [...new Set(n.map((a) => a.filterGroup))].sort((a, d) => a - d), o = new Map(i.map((a, d) => [a, d + 1]));
625
+ n = n.map((a) => ({
626
+ ...a,
627
+ filterGroup: o.get(a.filterGroup) ?? a.filterGroup
529
628
  }));
530
629
  }
531
630
  e.recommendationConfigs.filters = n;
@@ -546,7 +645,7 @@ const E = () => ({
546
645
  }), e.recommendationConfigs.filters = r;
547
646
  },
548
647
  generateFilterQuery() {
549
- return v(this.recommendationConfigs.filters);
648
+ return P(this.recommendationConfigs.filters);
550
649
  },
551
650
  /**
552
651
  * Validation-only check invoked at save-CTA time. Defined as an action
@@ -554,33 +653,52 @@ const E = () => ({
554
653
  * every block's recommendationConfigs across user edits.
555
654
  */
556
655
  hasInvalidBlock() {
557
- return Object.values(this.blockStates).some((t) => !O(t.recommendationConfigs, this));
656
+ return Object.values(this.blockStates).some((t) => !J(t.recommendationConfigs, this));
657
+ },
658
+ /**
659
+ * Which required fields are missing across the design's blocks, so the save
660
+ * toaster can name the cause instead of always showing the generic message.
661
+ */
662
+ invalidBlockFields() {
663
+ return Object.values(this.blockStates).flatMap((t) => H(t.recommendationConfigs, this));
558
664
  },
559
665
  // ====================================================================
560
666
  // Per-Block Product Fetching
561
667
  // ====================================================================
562
668
  async fetchRecommendationProducts() {
563
- if (!(this.currentRecommendationId === null || !this.blockStates[this.currentRecommendationId])) {
564
- if (h) {
565
- await h;
566
- return;
567
- }
568
- h = this._doFetchProducts();
569
- try {
570
- await h;
571
- } finally {
572
- h = null;
573
- }
669
+ if (this.currentRecommendationId === null || !this.blockStates[this.currentRecommendationId])
670
+ return;
671
+ const t = [
672
+ this.currentRecommendationId,
673
+ this.configVersion,
674
+ b
675
+ ].join(":");
676
+ if (y && I === t) {
677
+ await y;
678
+ return;
679
+ }
680
+ I = t, y = this._doFetchProducts();
681
+ try {
682
+ await y;
683
+ } finally {
684
+ I === t && (y = null, I = "");
574
685
  }
575
686
  },
576
687
  async _doFetchProducts() {
577
- var d;
578
- const t = this.currentRecommendationId, e = this.blockStates[t], { recommendationConfigs: r } = e, n = k();
688
+ var h;
689
+ const t = this.currentRecommendationId, e = this.blockStates[t], { recommendationConfigs: r } = e, n = R();
579
690
  if (r.strategyId && n.isFeatureEnabled("reusableRecommendationStrategy")) {
580
691
  await this._doFetchStrategyProducts(t);
581
692
  return;
582
693
  }
583
- const s = r.filters.filter((u) => u.isValid), i = v(s), o = ((d = R().find((u) => u.key === r.strategy)) == null ? void 0 : d.path) || "", c = parseInt(r.size) || 6, a = {
694
+ const s = (g.get(t) ?? 0) + 1;
695
+ g.set(t, s);
696
+ const i = parseInt(r.size) || 6;
697
+ if (!r.strategy) {
698
+ this._setBlockProducts(t, [], i);
699
+ return;
700
+ }
701
+ 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 = {
584
702
  locale: r.language,
585
703
  currency: r.currencySettings.value,
586
704
  partnerName: n.partnerName,
@@ -588,17 +706,17 @@ const E = () => ({
588
706
  details: !0,
589
707
  campaignId: n.variationId
590
708
  };
591
- r.strategy === "manualMerchandising" ? a.productId = z(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(
592
- a,
593
- B(n.partnerName, r.strategy)
709
+ 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(
710
+ c,
711
+ $(n.partnerName, r.strategy)
594
712
  );
595
- let l;
713
+ let u;
596
714
  try {
597
- l = await m.fetchRecommendationProducts(o, a);
715
+ u = await m.fetchRecommendationProducts(d, c);
598
716
  } catch {
599
- l = [];
717
+ u = [];
600
718
  }
601
- this._setBlockProducts(t, l, c);
719
+ g.get(t) === s && this._setBlockProducts(t, u, i);
602
720
  },
603
721
  /**
604
722
  * Block-scoped counterpart of `fetchRecommendationProducts`, which only ever serves the
@@ -610,8 +728,8 @@ const E = () => ({
610
728
  (e = this.blockStates[t]) != null && e.recommendationConfigs.strategyId && await this._doFetchStrategyProducts(t);
611
729
  },
612
730
  async _doFetchStrategyProducts(t) {
613
- const e = (p.get(t) ?? 0) + 1;
614
- p.set(t, e);
731
+ const e = (g.get(t) ?? 0) + 1;
732
+ g.set(t, e);
615
733
  const { recommendationConfigs: r } = this.blockStates[t];
616
734
  let n = [], s = parseInt(r.size) || 6;
617
735
  try {
@@ -621,25 +739,25 @@ const E = () => ({
621
739
  );
622
740
  if (i) {
623
741
  s = parseInt(String(i.size ?? "")) || s;
624
- 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), u = (l === -1 ? "" : a.slice(l + 1)).split("&").filter((I) => I && !I.startsWith("user_id=")), b = u.length ? `${d}?${u.join("&")}` : d;
625
- b && (n = await m.fetchRecommendationProductsByUrl(b));
742
+ 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;
743
+ f && (n = await m.fetchRecommendationProductsByUrl(f));
626
744
  }
627
745
  } catch {
628
746
  n = [];
629
747
  }
630
- p.get(t) === e && this._setBlockProducts(t, n, s);
748
+ g.get(t) === e && this._setBlockProducts(t, n, s);
631
749
  },
632
750
  _setBlockProducts(t, e, r) {
633
751
  if (!this.blockStates[t])
634
752
  return;
635
- const n = e.length > 0 ? e : C(r);
753
+ const n = e.length > 0 ? e : w(r);
636
754
  this.blockStates[t].recommendationProducts = [
637
755
  ...n.slice(0, r),
638
- ...C(Math.max(0, r - n.length))
756
+ ...w(Math.max(0, r - n.length))
639
757
  ];
640
758
  }
641
759
  }
642
760
  });
643
761
  export {
644
- et as useRecommendationExtensionStore
762
+ ft as useRecommendationExtensionStore
645
763
  };
@@ -1,5 +1,5 @@
1
1
  import "../constants/selectors.js";
2
- import { CURRENT_CONFIG_VERSION as m } from "../constants/defaultConfig.js";
2
+ import { RRS_CONFIG_VERSION as m } from "../constants/defaultConfig.js";
3
3
  import { sanitizeProductIds as p } from "./productIds.js";
4
4
  const d = "manualMerchandising";
5
5
  function N({ strategyId: e, nodeConfigVersion: t }) {
@@ -1,22 +1,32 @@
1
- import { ModificationDescription as f } from "../../../../node_modules/@stripoinc/ui-editor-extensions/dist/esm/index.js";
2
- import { RECOMMENDATION_ID_ATTR as m } from "../constants/selectors.js";
3
- import { isPartnerManagedBlock as l, updateProductContentInPlace as a, regenerateProductRowsWithStyles as d } from "../controls/main/utils.js";
4
- import { RecommendationConfigService as p } from "../services/configService.js";
5
- let e = null;
6
- function M(r) {
7
- e = r;
1
+ import { ModificationDescription as g } from "../../../../node_modules/@stripoinc/ui-editor-extensions/dist/esm/index.js";
2
+ import { RECOMMENDATION_ID_ATTR as s } from "../constants/selectors.js";
3
+ import { isPartnerManagedBlock as p, updateProductContentInPlace as C, regenerateProductRowsWithStyles as N } from "../controls/main/utils.js";
4
+ import { RecommendationConfigService as m } from "../services/configService.js";
5
+ let o = null;
6
+ function D(n) {
7
+ o = n;
8
8
  }
9
- function N(r, c, i, u) {
10
- const n = e == null ? void 0 : e.getDocumentRootHtmlNode();
11
- if (!e || !n || !("querySelector" in n))
9
+ function P(n, r, c, l) {
10
+ const i = o == null ? void 0 : o.getDocumentRootHtmlNode();
11
+ if (!o || !i || !("querySelector" in i))
12
12
  return !1;
13
- const t = n.querySelector(`[${m}="${r}"]`);
14
- if (!t)
13
+ const e = i.querySelector(`[${s}="${n}"]`);
14
+ if (!e)
15
15
  return !1;
16
- const o = e.getDocumentModifier();
17
- return p.stageConfig(o, t, c), l(t) ? (o.apply(new f(u)), !0) : (a({ currentNode: t, documentModifier: o, products: i, forceApply: !0 }) || d({ currentNode: t, documentModifier: o, products: i, forceApply: !0 }), !0);
16
+ const t = o.getDocumentModifier(), a = m.getConfig(e), d = Object.keys(r).some((u) => JSON.stringify(a[u]) !== JSON.stringify(r[u]));
17
+ if (m.stageConfig(t, e, r), p(e))
18
+ return t.apply(new g(l)), !0;
19
+ let f = !1;
20
+ return C({
21
+ currentNode: e,
22
+ documentModifier: t,
23
+ products: c,
24
+ onApplied: () => {
25
+ f = !0;
26
+ }
27
+ }) || (f = N({ currentNode: e, documentModifier: t, products: c })), !f && d && t.apply(new g(l)), !0;
18
28
  }
19
29
  export {
20
- N as commitBlockChangeOutsidePanel,
21
- M as registerNodeConfigWriter
30
+ P as commitBlockChangeOutsidePanel,
31
+ D as registerNodeConfigWriter
22
32
  };
@@ -0,0 +1,25 @@
1
+ import { ATTR_CUSTOM_PREFIX as f } from "../constants/selectors.js";
2
+ function o(t) {
3
+ if (typeof t == "string")
4
+ return t.trim() !== "";
5
+ if (t && typeof t == "object" && "strategyId" in t) {
6
+ const { strategyId: r } = t;
7
+ return (typeof r == "string" || typeof r == "number") && String(r).trim() !== "";
8
+ }
9
+ return !1;
10
+ }
11
+ function u({ config: t, blockId: r, partnerName: e, campaignUrls: n, rrsEnabled: i }) {
12
+ return e ? t.partnerName ? t.partnerName !== e : !i || !t.strategyId || !n ? !1 : !o(n[String(r)]) : !1;
13
+ }
14
+ function a({ config: t, campaignUrls: r, rrsEnabled: e }) {
15
+ return !t.strategyId || e && !!r;
16
+ }
17
+ function g(t) {
18
+ return t.filter((r) => !r.startsWith(f));
19
+ }
20
+ export {
21
+ a as canJudgeUnstampedBlock,
22
+ u as carriesForeignPartnerConfig,
23
+ o as isDefinedCampaignUrl,
24
+ g as withoutCustomAttributes
25
+ };
@@ -0,0 +1,24 @@
1
+ import { useRecommendationExtensionStore as d } from "../store/recommendation.js";
2
+ import { commitBlockChangeOutsidePanel as f } from "./nodeConfigWriter.js";
3
+ async function h(e, r) {
4
+ const t = d(), o = t.blockStates[e];
5
+ if (!o)
6
+ return !1;
7
+ const { strategyId: a, strategy: c, size: i, nodeConfigVersion: s } = o.recommendationConfigs;
8
+ if (t.currentRecommendationId === e)
9
+ try {
10
+ await t.fetchRecommendationProducts();
11
+ } catch (m) {
12
+ console.error("[recommendation] product refetch after a strategy change failed", m);
13
+ }
14
+ const n = f(
15
+ e,
16
+ { strategyId: a, strategy: c, size: i, configVersion: s },
17
+ o.recommendationProducts,
18
+ r
19
+ );
20
+ return n || console.error("[recommendation] strategy change did not reach a node", { blockId: e }), n;
21
+ }
22
+ export {
23
+ h as commitStrategyChange
24
+ };