@useinsider/guido 3.12.0-beta.841bb6a → 3.12.0-beta.86e34da

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 (90) hide show
  1. package/dist/@types/config/schemas.js +9 -4
  2. package/dist/components/Guido.vue.js +6 -6
  3. package/dist/components/Guido.vue2.js +50 -49
  4. package/dist/components/organisms/email-preview/desktop-preview/DesktopPreview.vue.js +1 -1
  5. package/dist/components/organisms/email-preview/desktop-preview/DesktopPreview.vue2.js +8 -8
  6. package/dist/components/organisms/email-preview/mobile-preview/ContentView.vue.js +5 -5
  7. package/dist/components/organisms/email-preview/mobile-preview/ContentView.vue2.js +10 -10
  8. package/dist/components/organisms/extensions/recommendation/StrategyDetailDrawer.vue.js +38 -0
  9. package/dist/components/organisms/extensions/recommendation/StrategyDetailDrawer.vue2.js +271 -0
  10. package/dist/components/organisms/extensions/recommendation/strategy-detail/CampaignTable.vue.js +18 -0
  11. package/dist/components/organisms/extensions/recommendation/strategy-detail/CampaignTable.vue2.js +42 -0
  12. package/dist/components/organisms/extensions/recommendation/strategy-detail/FilterHumanizer.vue.js +20 -0
  13. package/dist/components/organisms/extensions/recommendation/strategy-detail/FilterHumanizer.vue2.js +18 -0
  14. package/dist/components/organisms/extensions/recommendation/strategy-detail/HumanizerCondition.vue.js +20 -0
  15. package/dist/components/organisms/extensions/recommendation/strategy-detail/HumanizerCondition.vue2.js +60 -0
  16. package/dist/components/organisms/extensions/recommendation/strategy-detail/HumanizerGroup.vue.js +19 -0
  17. package/dist/components/organisms/extensions/recommendation/strategy-detail/HumanizerGroup.vue2.js +19 -0
  18. package/dist/components/organisms/extensions/recommendation/strategy-detail/InfoDisplayValue.vue.js +22 -0
  19. package/dist/components/organisms/extensions/recommendation/strategy-detail/InfoDisplayValue.vue2.js +29 -0
  20. package/dist/composables/usePreviewInteractionGuard.js +36 -11
  21. package/dist/composables/useRecommendation.js +56 -40
  22. package/dist/composables/useRecommendationPreview.js +61 -60
  23. package/dist/composables/useStrategyFilters.js +16 -0
  24. package/dist/config/migrator/recommendationMigrator.js +7 -4
  25. package/dist/enums/date.js +4 -3
  26. package/dist/enums/extensions/filteringV2.js +1024 -0
  27. package/dist/enums/extensions/recommendationBlock.js +45 -24
  28. package/dist/enums/extensions/strategyDetail.js +148 -0
  29. package/dist/extensions/Blocks/Recommendation/constants/controlIds.js +1 -1
  30. package/dist/extensions/Blocks/Recommendation/constants/defaultConfig.js +12 -11
  31. package/dist/extensions/Blocks/Recommendation/controls/blockBackground/index.js +10 -0
  32. package/dist/extensions/Blocks/Recommendation/controls/main/index.js +141 -123
  33. package/dist/extensions/Blocks/Recommendation/controls/main/strategy.js +285 -0
  34. package/dist/extensions/Blocks/Recommendation/extension.js +23 -21
  35. package/dist/extensions/Blocks/Recommendation/iconsRegistry.js +16 -2
  36. package/dist/extensions/Blocks/Recommendation/recommendation.css.js +257 -0
  37. package/dist/extensions/Blocks/Recommendation/settingsPanel.js +41 -40
  38. package/dist/extensions/Blocks/Recommendation/store/recommendation.js +460 -113
  39. package/dist/extensions/Blocks/Recommendation/utils/strategyHumanizer.js +97 -0
  40. package/dist/extensions/Blocks/Recommendation/utils/strategyNavigation.js +8 -0
  41. package/dist/extensions/Blocks/Recommendation/utils/strategySummary.js +95 -0
  42. package/dist/extensions/Blocks/Recommendation/utils/strategyUrl.js +43 -0
  43. package/dist/extensions/Blocks/Recommendation/validation/requiredFields.js +24 -10
  44. package/dist/extensions/Blocks/Unsubscribe/utils/constants.js +3 -2
  45. package/dist/extensions/Blocks/common-control.js +2 -1
  46. package/dist/extensions/Blocks/controlFactories.js +125 -75
  47. package/dist/guido.css +1 -1
  48. package/dist/services/recommendationApi.js +120 -32
  49. package/dist/services/stripoApi.js +18 -18
  50. package/dist/src/@types/config/schemas.d.ts +10 -0
  51. package/dist/src/components/organisms/extensions/recommendation/StrategyDetailDrawer.vue.d.ts +2 -0
  52. package/dist/src/components/organisms/extensions/recommendation/strategy-detail/CampaignTable.vue.d.ts +43 -0
  53. package/dist/src/components/organisms/extensions/recommendation/strategy-detail/FilterHumanizer.vue.d.ts +29 -0
  54. package/dist/src/components/organisms/extensions/recommendation/strategy-detail/HumanizerCondition.vue.d.ts +40 -0
  55. package/dist/src/components/organisms/extensions/recommendation/strategy-detail/HumanizerGroup.vue.d.ts +38 -0
  56. package/dist/src/components/organisms/extensions/recommendation/strategy-detail/InfoDisplayValue.vue.d.ts +51 -0
  57. package/dist/src/composables/useConfig.d.ts +2 -0
  58. package/dist/src/composables/usePreviewInteractionGuard.d.ts +1 -1
  59. package/dist/src/composables/useRecommendation.d.ts +2 -0
  60. package/dist/src/composables/useStrategyFilters.d.ts +24 -0
  61. package/dist/src/enums/extensions/filteringV2.d.ts +72 -0
  62. package/dist/src/enums/extensions/recommendationBlock.d.ts +33 -0
  63. package/dist/src/enums/extensions/strategyDetail.d.ts +90 -0
  64. package/dist/src/extensions/Blocks/Recommendation/constants/controlIds.d.ts +1 -0
  65. package/dist/src/extensions/Blocks/Recommendation/controls/blockBackground/index.d.ts +14 -0
  66. package/dist/src/extensions/Blocks/Recommendation/controls/index.d.ts +1 -0
  67. package/dist/src/extensions/Blocks/Recommendation/controls/main/index.d.ts +10 -4
  68. package/dist/src/extensions/Blocks/Recommendation/controls/main/strategy.d.ts +75 -0
  69. package/dist/src/extensions/Blocks/Recommendation/store/recommendation.d.ts +903 -1
  70. package/dist/src/extensions/Blocks/Recommendation/types/nodeConfig.d.ts +7 -0
  71. package/dist/src/extensions/Blocks/Recommendation/utils/strategyHumanizer.d.ts +50 -0
  72. package/dist/src/extensions/Blocks/Recommendation/utils/strategyNavigation.d.ts +10 -0
  73. package/dist/src/extensions/Blocks/Recommendation/utils/strategySummary.d.ts +34 -0
  74. package/dist/src/extensions/Blocks/Recommendation/utils/strategyUrl.d.ts +51 -0
  75. package/dist/src/extensions/Blocks/Recommendation/validation/requiredFields.d.ts +8 -1
  76. package/dist/src/extensions/Blocks/Unsubscribe/utils/constants.d.ts +1 -0
  77. package/dist/src/extensions/Blocks/common-control.d.ts +2 -1
  78. package/dist/src/extensions/Blocks/controlFactories.d.ts +36 -0
  79. package/dist/src/mock/api/recommendation-strategies.d.ts +2 -0
  80. package/dist/src/services/recommendationApi.d.ts +9 -1
  81. package/dist/src/stores/config.d.ts +18 -0
  82. package/dist/src/utils/genericUtil.d.ts +9 -0
  83. package/dist/src/utils/urlSchemes.d.ts +6 -0
  84. package/dist/static/styles/components/base-input.css.js +6 -7
  85. package/dist/utils/dateUtil.js +23 -10
  86. package/dist/utils/genericUtil.js +10 -1
  87. package/dist/utils/pairProductVariables.js +43 -44
  88. package/dist/utils/templatePreparation.js +70 -57
  89. package/dist/utils/urlSchemes.js +4 -0
  90. package/package.json +1 -1
@@ -1,20 +1,27 @@
1
- import { getRecommendationFeedSourceMaps as g, getOperatorOptions as R, PriceAttributes as C } from "../../../../enums/extensions/recommendationBlock.js";
2
- import { useRecommendationApi as y } from "../../../../services/recommendationApi.js";
3
- import { useConfigStore as G } from "../../../../stores/config.js";
4
- import { defineStore as F } from "pinia";
1
+ import { useConfig as U } from "../../../../composables/useConfig.js";
2
+ import { useTranslations as u } from "../../../../composables/useTranslations.js";
3
+ import { getRecommendationFeedSourceMaps as w, getOperatorOptions as L, PriceAttributes as T } from "../../../../enums/extensions/recommendationBlock.js";
4
+ import { ToasterTypeOptions as g } from "../../../../enums/toaster.js";
5
+ import { useRecommendationApi as _ } from "../../../../services/recommendationApi.js";
6
+ import { useConfigStore as F } from "../../../../stores/config.js";
7
+ import { useToasterStore as f } from "../../../../stores/toaster.js";
8
+ import { defineStore as N } from "pinia";
5
9
  import "../constants/selectors.js";
6
- import { DEFAULT_MOBILE_CARDS_IN_ROW as P, DEFAULT_CARDS_IN_ROW as D } from "../constants/layout.js";
7
- import { EXCLUDED_ALGORITHM_IDS as w } from "../constants/defaultConfig.js";
8
- import { getDefaultProducts as S } from "../templates/utils.js";
9
- import { generateCompleteFilterQuery as b } from "../utils/filterUtil.js";
10
- import { getPartnerRecommendationParams as v } from "../utils/partnerCustomizations.js";
11
- import { isFilterValid as N } from "../validation/filterSchema.js";
12
- import { isConfigValid as x } from "../validation/requiredFields.js";
13
- const h = y();
14
- let m = null, u = null, d = null;
15
- function k() {
10
+ import { DEFAULT_MOBILE_CARDS_IN_ROW as x, DEFAULT_CARDS_IN_ROW as B } from "../constants/layout.js";
11
+ import { EXCLUDED_ALGORITHM_IDS as A } from "../constants/defaultConfig.js";
12
+ import { getDefaultProducts as D } from "../templates/utils.js";
13
+ import { generateCompleteFilterQuery as v } from "../utils/filterUtil.js";
14
+ import { getPartnerRecommendationParams as E } from "../utils/partnerCustomizations.js";
15
+ import { resolveStrategyUrl as O, buildStrategyUrlValues as V } from "../utils/strategyUrl.js";
16
+ import { isFilterValid as z } from "../validation/filterSchema.js";
17
+ import { isConfigValid as M } from "../validation/requiredFields.js";
18
+ const c = _();
19
+ let h = null, p = null, S = null, d = null, C = 0;
20
+ const y = {};
21
+ let b = 0;
22
+ function P() {
16
23
  return {
17
- cardsInRow: D,
24
+ cardsInRow: B,
18
25
  currencySettings: {
19
26
  name: "USD",
20
27
  value: "USD",
@@ -28,13 +35,14 @@ function k() {
28
35
  productIds: [],
29
36
  id: 1,
30
37
  language: "en_US",
31
- mobileCardsInRow: P,
38
+ mobileCardsInRow: x,
32
39
  mobileLayoutEnabled: !1,
33
40
  orientation: "grid",
34
41
  recommendedProducts: [],
35
42
  sendProductRequestFlag: !1,
36
43
  shuffleProducts: !1,
37
44
  strategy: "mostPopular",
45
+ strategyId: "",
38
46
  textTrimming: !0,
39
47
  unresponsive: !1,
40
48
  size: "6",
@@ -43,28 +51,37 @@ function k() {
43
51
  priceHideIfSameAsDiscounted: !1
44
52
  };
45
53
  }
46
- function I() {
54
+ function G() {
47
55
  return {
48
- recommendationConfigs: k(),
56
+ recommendationConfigs: P(),
49
57
  recommendationProducts: [],
50
58
  filterStatus: !1,
51
59
  filterSelectionDrawerStatus: !1,
60
+ strategyDetailDrawerStatus: !1,
52
61
  filterGroup: 1,
53
62
  isInitialized: !1,
54
63
  filterSnapshot: null
55
64
  };
56
65
  }
57
- const L = () => ({
66
+ const j = () => ({
58
67
  recommendationCampaignUrls: {},
59
68
  activePredictiveAlgorithms: [],
60
69
  languages: {},
61
70
  currencyList: [],
62
71
  filterList: {},
72
+ strategies: [],
73
+ strategiesLoading: !1,
74
+ strategyCampaigns: [],
75
+ strategyCampaignsLoading: !1,
76
+ strategyCampaignsFetchedFor: null,
77
+ strategyUrlTemplates: {},
78
+ strategiesLoadFailed: !1,
79
+ strategyDuplicating: !1,
63
80
  blockStates: {},
64
81
  currentRecommendationId: null,
65
82
  configVersion: 0
66
- }), $ = F("guidoRecommendationExtension", {
67
- state: () => L(),
83
+ }), ot = N("guidoRecommendationExtension", {
84
+ state: () => j(),
68
85
  getters: {
69
86
  // ====================================================================
70
87
  // Proxy Getters — Backward Compatible Access to Current Block State
@@ -74,7 +91,7 @@ const L = () => ({
74
91
  * This allows all existing code that reads `store.recommendationConfigs` to work unchanged.
75
92
  */
76
93
  recommendationConfigs(t) {
77
- return t.currentRecommendationId !== null && t.blockStates[t.currentRecommendationId] ? t.blockStates[t.currentRecommendationId].recommendationConfigs : k();
94
+ return t.currentRecommendationId !== null && t.blockStates[t.currentRecommendationId] ? t.blockStates[t.currentRecommendationId].recommendationConfigs : P();
78
95
  },
79
96
  /**
80
97
  * Proxy getter: delegates to blockStates[currentRecommendationId].recommendationProducts
@@ -88,6 +105,12 @@ const L = () => ({
88
105
  filterSelectionDrawerStatus(t) {
89
106
  return t.currentRecommendationId !== null && t.blockStates[t.currentRecommendationId] ? t.blockStates[t.currentRecommendationId].filterSelectionDrawerStatus : !1;
90
107
  },
108
+ /**
109
+ * Proxy getter: delegates to blockStates[currentRecommendationId].strategyDetailDrawerStatus
110
+ */
111
+ strategyDetailDrawerStatus(t) {
112
+ return t.currentRecommendationId !== null && t.blockStates[t.currentRecommendationId] ? t.blockStates[t.currentRecommendationId].strategyDetailDrawerStatus : !1;
113
+ },
91
114
  /**
92
115
  * Proxy getter: delegates to blockStates[currentRecommendationId].filterStatus
93
116
  */
@@ -119,14 +142,62 @@ const L = () => ({
119
142
  return [...new Set(t.map((e) => e.filterGroup))].sort((e, r) => e - r);
120
143
  },
121
144
  getActivePredictiveAlgorithms: (t) => {
122
- const e = g(), r = [];
123
- return t.activePredictiveAlgorithms.filter((n) => !w.includes(n)).forEach((n) => {
124
- r.push(...e.filter((c) => c.id === n));
125
- }), r.map((n) => ({
126
- text: n.name,
127
- value: n.key
145
+ const e = w(), r = [];
146
+ return t.activePredictiveAlgorithms.filter((i) => !A.includes(i)).forEach((i) => {
147
+ r.push(...e.filter((s) => s.id === i));
148
+ }), r.map((i) => ({
149
+ text: i.name,
150
+ value: i.key
128
151
  }));
129
152
  },
153
+ /**
154
+ * Reusable Recommendation Strategies as dropdown options.
155
+ *
156
+ * Unlike algorithms, strategy ids come straight from Smart Recommender —
157
+ * there is no local id→key table to join against.
158
+ */
159
+ getStrategyOptions: (t) => t.strategies.map((e) => ({
160
+ text: e.name,
161
+ value: String(e.strategy_id)
162
+ })),
163
+ /**
164
+ * The current block's selected strategy, or null when none is selected
165
+ * or the selected id is no longer in the listing.
166
+ *
167
+ * A null return with a non-empty `strategyId` means the strategy was
168
+ * deleted in Smart Recommender after this block was configured — the
169
+ * panel flags that rather than silently clearing the selection.
170
+ */
171
+ getSelectedStrategy() {
172
+ const { strategyId: t } = this.recommendationConfigs;
173
+ return t ? this.strategies.find((e) => String(e.strategy_id) === t) ?? null : null;
174
+ },
175
+ /**
176
+ * Whether the RRS feature is enabled for this editor.
177
+ *
178
+ * Read through a getter so `hasInvalidBlock` can pass the store straight to
179
+ * the required-field descriptors, which need the flag to decide whether a
180
+ * missing strategy blocks the save.
181
+ */
182
+ isRrsEnabled() {
183
+ return U().isFeatureEnabled("reusableRecommendationStrategy");
184
+ },
185
+ /** Strategy ids currently offered for email, for the save-time deleted-strategy check. */
186
+ strategyIds: (t) => t.strategies.map((e) => String(e.strategy_id)),
187
+ /**
188
+ * Whether the listing is trustworthy enough to judge an assigned id against.
189
+ * Mid-fetch it is empty, and every assigned id would look deleted.
190
+ */
191
+ canValidateStrategy: (t) => !t.strategiesLoading && t.strategies.length > 0,
192
+ /**
193
+ * True when a strategy id is set but absent from the fetched listing.
194
+ *
195
+ * Guarded on the listing having loaded — mid-fetch every id looks missing.
196
+ */
197
+ hasMissingStrategy() {
198
+ const { strategyId: t } = this.recommendationConfigs;
199
+ return !t || this.strategiesLoading || !this.strategies.length ? !1 : this.getSelectedStrategy === null;
200
+ },
130
201
  getLanguages: (t) => Object.entries(t.languages).map(([e, r]) => ({
131
202
  text: r,
132
203
  value: e
@@ -138,7 +209,7 @@ const L = () => ({
138
209
  getFilterList() {
139
210
  return Object.values(this.filterList).filter((t) => t.isFilterable !== 0 && t.isFilterable !== !1).map((t) => {
140
211
  let e;
141
- return t.type === "productAttribute" ? e = `product_attributes.${t.attributeName}` : C.includes(t.attributeName) ? e = `${t.attributeName}.${this.recommendationConfigs.currencySettings.value}` : e = t.attributeName, {
212
+ return t.type === "productAttribute" ? e = `product_attributes.${t.attributeName}` : T.includes(t.attributeName) ? e = `${t.attributeName}.${this.recommendationConfigs.currencySettings.value}` : e = t.attributeName, {
142
213
  text: t.displayName,
143
214
  value: e,
144
215
  type: t.attributeType
@@ -160,7 +231,7 @@ const L = () => ({
160
231
  setCurrentBlock(t) {
161
232
  this.blockStates[t] || (this.blockStates = {
162
233
  ...this.blockStates,
163
- [t]: I()
234
+ [t]: G()
164
235
  }), this.currentRecommendationId = t;
165
236
  },
166
237
  /**
@@ -170,13 +241,13 @@ const L = () => ({
170
241
  removeBlockState(t) {
171
242
  const e = t.toString();
172
243
  if (this.recommendationCampaignUrls[e]) {
173
- const n = { ...this.recommendationCampaignUrls };
174
- delete n[e], this.recommendationCampaignUrls = n;
244
+ const i = { ...this.recommendationCampaignUrls };
245
+ delete i[e], this.recommendationCampaignUrls = i;
175
246
  }
176
247
  const r = { ...this.blockStates };
177
248
  if (delete r[t], this.blockStates = r, this.currentRecommendationId === t) {
178
- const n = Object.keys(this.blockStates).map(Number);
179
- this.currentRecommendationId = n.length > 0 ? n[0] : null;
249
+ const i = Object.keys(this.blockStates).map(Number);
250
+ this.currentRecommendationId = i.length > 0 ? i[0] : null;
180
251
  }
181
252
  },
182
253
  /**
@@ -186,16 +257,16 @@ const L = () => ({
186
257
  cloneBlockState(t, e) {
187
258
  const r = this.blockStates[t];
188
259
  if (r) {
189
- const o = structuredClone(r);
190
- o.recommendationConfigs.id = e, this.blockStates = {
260
+ const a = structuredClone(r);
261
+ a.recommendationConfigs.id = e, this.blockStates = {
191
262
  ...this.blockStates,
192
- [e]: o
263
+ [e]: a
193
264
  };
194
265
  }
195
- const n = t.toString(), c = e.toString(), i = this.recommendationCampaignUrls[n];
196
- i && (this.recommendationCampaignUrls = {
266
+ const i = t.toString(), s = e.toString(), n = this.recommendationCampaignUrls[i];
267
+ n && (this.recommendationCampaignUrls = {
197
268
  ...this.recommendationCampaignUrls,
198
- [c]: i
269
+ [s]: n
199
270
  });
200
271
  },
201
272
  /**
@@ -220,7 +291,7 @@ const L = () => ({
220
291
  * `isInitialized`) alone.
221
292
  */
222
293
  seedBlockUrlConfig(t, e) {
223
- const r = (o, s) => o === "." || o === "," || o === " " ? o : s, n = {
294
+ const r = (a, o) => a === "." || a === "," || a === " " ? a : o, i = {
224
295
  name: e.currencyCode,
225
296
  value: e.currencyCode,
226
297
  symbol: e.currencyCode,
@@ -228,19 +299,20 @@ const L = () => ({
228
299
  decimalCount: String(e.currencyDecimalCount),
229
300
  decimalSeparator: r(e.currencyDecimalSeparator, ","),
230
301
  thousandSeparator: r(e.currencyThousandSeparator, ".")
231
- }, c = !this.blockStates[t], i = c ? I() : this.blockStates[t];
232
- i.recommendationConfigs = {
233
- ...i.recommendationConfigs,
302
+ }, s = !this.blockStates[t], n = s ? G() : this.blockStates[t];
303
+ n.recommendationConfigs = {
304
+ ...n.recommendationConfigs,
234
305
  strategy: e.strategy,
306
+ strategyId: e.strategyId,
235
307
  language: e.language,
236
308
  size: e.size,
237
309
  productIds: e.productIds,
238
310
  filters: e.filters,
239
311
  shuffleProducts: e.shuffleProducts,
240
- currencySettings: n
241
- }, c && (this.blockStates = {
312
+ currencySettings: i
313
+ }, s && (this.blockStates = {
242
314
  ...this.blockStates,
243
- [t]: i
315
+ [t]: n
244
316
  });
245
317
  },
246
318
  /**
@@ -259,18 +331,18 @@ const L = () => ({
259
331
  ...t.currencySettings || {}
260
332
  }
261
333
  };
262
- const { triggerRefetch: n = !0 } = e;
263
- n && this.configVersion++;
334
+ const { triggerRefetch: i = !0 } = e;
335
+ i && this.configVersion++;
264
336
  },
265
337
  /**
266
338
  * Creates a filter with the first available attribute and operator pre-selected.
267
339
  */
268
340
  createDefaultFilter(t, e) {
269
- const [r] = this.getFilterList, [n] = R(r == null ? void 0 : r.type);
341
+ const [r] = this.getFilterList, [i] = L(r == null ? void 0 : r.type);
270
342
  return {
271
343
  type: "standardFilter",
272
344
  attribute: (r == null ? void 0 : r.value) ?? "",
273
- operator: (n == null ? void 0 : n.value) ?? "",
345
+ operator: (i == null ? void 0 : i.value) ?? "",
274
346
  innerGroupOperator: "*",
275
347
  outerGroupOperator: "*",
276
348
  filterNumber: e,
@@ -303,6 +375,209 @@ const L = () => ({
303
375
  const t = this.blockStates[this.currentRecommendationId];
304
376
  t.filterSnapshot = null, t.filterSelectionDrawerStatus = !1;
305
377
  },
378
+ /**
379
+ * Find a strategy record by id, fetching the listing once if it is not loaded.
380
+ *
381
+ * The endpoint-generate call needs the record, and the save/hydrate path can
382
+ * reach it on a template the marketer never clicked into — where no panel has
383
+ * ever triggered the listing fetch.
384
+ *
385
+ * THROWS when the listing could not be loaded, rather than reporting "not
386
+ * found". The caller caches a not-found as a settled "this strategy has no
387
+ * URL"; doing that for a network blip would strand the block on its old
388
+ * algorithm feed for the rest of the session while the panel still showed
389
+ * the strategy as assigned. A throw leaves the cache key unset, so the next
390
+ * selection or save tries again.
391
+ */
392
+ async _resolveStrategyRecord(t) {
393
+ const e = () => this.strategies.find((i) => String(i.strategy_id) === t) ?? null, r = e();
394
+ if (r || this.strategies.length)
395
+ return r;
396
+ if (await this.fetchRecommendationStrategies(), this.strategiesLoadFailed)
397
+ throw new Error("Recommendation strategy listing is unavailable.");
398
+ return e();
399
+ },
400
+ /**
401
+ * Assign a strategy to the current block.
402
+ *
403
+ * Only patches Pinia: the `configVersion` bump makes the main control persist
404
+ * `strategyId` into the block's node config (see `_persistStoreConfigToNode`),
405
+ * which is the same route filters take. Resolving the URL template here rather
406
+ * than at compile time matters — `buildCampaignUrl` is synchronous and would
407
+ * otherwise silently fall back to the algorithm URL.
408
+ */
409
+ async selectStrategy(t) {
410
+ if (this.recommendationConfigs.strategyId === t)
411
+ return;
412
+ const e = this.currentRecommendationId, r = ++b;
413
+ this.patchCurrentBlockConfig({ strategyId: t }, { triggerRefetch: !0 });
414
+ const i = await this.fetchStrategyUrlTemplate(t), s = this.strategies.find((n) => String(n.strategy_id) === t);
415
+ if (!(this.currentRecommendationId !== e || r !== b)) {
416
+ if (!i) {
417
+ f().showToaster({
418
+ type: g.Error,
419
+ message: u()("discovery-strategy.error-description")
420
+ });
421
+ return;
422
+ }
423
+ f().showToaster({
424
+ type: g.Success,
425
+ message: u()("campaign-builder.variation-strategy-assign", {
426
+ strategyName: (s == null ? void 0 : s.name) ?? t
427
+ })
428
+ });
429
+ }
430
+ },
431
+ /**
432
+ * Duplicate the selected strategy and assign the copy to this block.
433
+ *
434
+ * Runs inline — the marketer never leaves the editor. Two things the web flow
435
+ * does that we deliberately skip: its follow-up `POST /{id}/assign` (that binds
436
+ * a strategy to a campaign *variation*, a concept email blocks do not have —
437
+ * here the assignment is the block's own config), and displaying the copy's
438
+ * name from the response, which only carries the new id. Hence the refetch.
439
+ */
440
+ async duplicateAndAssignStrategy() {
441
+ const t = this.getSelectedStrategy;
442
+ if (!t || this.strategyDuplicating)
443
+ return;
444
+ this.strategyDuplicating = !0;
445
+ const e = this.currentRecommendationId, r = ++b;
446
+ try {
447
+ const i = await c.duplicateStrategy(String(t.strategy_id));
448
+ if (!i) {
449
+ f().showToaster({
450
+ type: g.Error,
451
+ message: u()("discovery-strategy.error-description")
452
+ });
453
+ return;
454
+ }
455
+ if (await this.refreshRecommendationStrategies(), this.currentRecommendationId !== e || r !== b)
456
+ return;
457
+ this.patchCurrentBlockConfig({ strategyId: i }, { triggerRefetch: !0 });
458
+ const s = await this.fetchStrategyUrlTemplate(i);
459
+ if (this.currentRecommendationId !== e || r !== b)
460
+ return;
461
+ if (this.closeStrategyDrawer(), !s) {
462
+ f().showToaster({
463
+ type: g.Error,
464
+ message: u()("discovery-strategy.error-description")
465
+ });
466
+ return;
467
+ }
468
+ f().showToaster({
469
+ type: g.Success,
470
+ message: u()("recommendation-strategies.duplicate-and-assign-text", {
471
+ strategyName: t.name
472
+ })
473
+ });
474
+ } finally {
475
+ this.strategyDuplicating = !1;
476
+ }
477
+ },
478
+ /**
479
+ * Opens the RRS detail drawer for the current block.
480
+ *
481
+ * Unlike the filter drawer this takes no snapshot — the drawer is
482
+ * read-only, so there is nothing to revert on close.
483
+ */
484
+ openStrategyDrawer() {
485
+ this.currentRecommendationId === null || !this.blockStates[this.currentRecommendationId] || (this.blockStates[this.currentRecommendationId].strategyDetailDrawerStatus = !0);
486
+ },
487
+ /**
488
+ * Closes the RRS detail drawer for the current block and drops the campaign
489
+ * list, so reopening on a different strategy never flashes stale rows.
490
+ */
491
+ closeStrategyDrawer() {
492
+ this.clearStrategyCampaigns(), !(this.currentRecommendationId === null || !this.blockStates[this.currentRecommendationId]) && (this.blockStates[this.currentRecommendationId].strategyDetailDrawerStatus = !1);
493
+ },
494
+ /** Drops the campaign list and lets the next open refetch. */
495
+ clearStrategyCampaigns() {
496
+ this.strategyCampaigns = [], this.strategyCampaignsLoading = !1, this.strategyCampaignsFetchedFor = null, C += 1;
497
+ },
498
+ /**
499
+ * Resolve and cache a strategy's recommendation-URL template.
500
+ *
501
+ * Keyed by id for caching, but the request itself carries the whole strategy
502
+ * record — the algorithm inside it decides the feed's path and params, so an
503
+ * id alone cannot describe the URL. The listing is therefore a prerequisite;
504
+ * it is fetched on demand for callers (template hydrate) that may run before
505
+ * the panel ever opened.
506
+ *
507
+ * Must be awaited before `buildCampaignUrl` runs for a strategy-driven block,
508
+ * because that builder is synchronous and can only read what is already
509
+ * cached. Safe to call repeatedly: a resolved id short-circuits, and
510
+ * concurrent callers for the same id share one request.
511
+ *
512
+ * Only a *settled* answer is cached. An id the server rejects caches as '',
513
+ * so the block falls back instead of retrying on every compile. A request
514
+ * that merely failed to complete leaves the key unset and resolves to false,
515
+ * so the next selection or save tries again — caching a network blip as "no
516
+ * URL" would strand the block on its old algorithm feed for the session while
517
+ * the panel still showed the strategy as assigned.
518
+ * @returns whether the strategy now has a USABLE url — not merely a cache
519
+ * entry. A rejected id caches as '' to stop the retry storm, but reporting
520
+ * that as success would let the caller confirm an assignment the compiled
521
+ * email cannot honour.
522
+ */
523
+ async fetchStrategyUrlTemplate(t) {
524
+ if (!t)
525
+ return !1;
526
+ if (t in this.strategyUrlTemplates)
527
+ return !!this.strategyUrlTemplates[t];
528
+ const e = y[t];
529
+ if (e)
530
+ return await e, !!this.strategyUrlTemplates[t];
531
+ y[t] = (async () => {
532
+ try {
533
+ const r = await this._resolveStrategyRecord(t);
534
+ if (!r) {
535
+ this.strategyUrlTemplates = { ...this.strategyUrlTemplates, [t]: "" };
536
+ return;
537
+ }
538
+ const i = await c.generateStrategyEndpoint(r, F().variationId);
539
+ this.strategyUrlTemplates = {
540
+ ...this.strategyUrlTemplates,
541
+ [t]: (i == null ? void 0 : i.full_path) ?? ""
542
+ };
543
+ } catch (r) {
544
+ console.error("fetchStrategyUrlTemplate error:", r);
545
+ }
546
+ })();
547
+ try {
548
+ await y[t];
549
+ } finally {
550
+ delete y[t];
551
+ }
552
+ return !!this.strategyUrlTemplates[t];
553
+ },
554
+ /**
555
+ * Wait for every strategy-URL request still in flight.
556
+ *
557
+ * The compiler is synchronous: whatever is not cached by the time it runs
558
+ * compiles as the block's old algorithm URL. Selection and template hydrate
559
+ * both await their own request, but a save fired inside that round trip would
560
+ * still race — so the save path drains the queue first.
561
+ */
562
+ async awaitPendingStrategyUrls() {
563
+ await Promise.all(Object.values(y).filter(Boolean));
564
+ },
565
+ /**
566
+ * Fetch the campaigns a strategy is used in, for the drawer's second tab.
567
+ *
568
+ * Lazy and once-per-strategy: the caller invokes this only while the drawer is
569
+ * open on that tab, and a repeat call for the same id is a no-op. A version
570
+ * counter discards a response that arrives after the user switched strategies,
571
+ * which would otherwise show one strategy's campaigns under another's name.
572
+ */
573
+ async fetchStrategyCampaigns(t) {
574
+ if (!t || this.strategyCampaignsFetchedFor === t)
575
+ return;
576
+ const e = ++C;
577
+ this.strategyCampaignsLoading = !0;
578
+ const r = await c.fetchStrategyCampaigns(t, u());
579
+ e === C && (this.strategyCampaigns = r, this.strategyCampaignsLoading = !1, this.strategyCampaignsFetchedFor = t);
580
+ },
306
581
  /**
307
582
  * Cancels the filter selection drawer and reverts filters
308
583
  * to the snapshot taken when the drawer was opened.
@@ -318,43 +593,82 @@ const L = () => ({
318
593
  // ====================================================================
319
594
  async fetchRecommendationCreateData() {
320
595
  if (!this.activePredictiveAlgorithms.length) {
321
- if (m) {
322
- await m;
596
+ if (h) {
597
+ await h;
323
598
  return;
324
599
  }
325
- m = (async () => {
600
+ h = (async () => {
326
601
  const {
327
602
  activePredictiveAlgorithms: t,
328
603
  languages: e,
329
604
  currencies: r
330
- } = await h.fetchRecommendationCreateData();
605
+ } = await c.fetchRecommendationCreateData();
331
606
  if (this.activePredictiveAlgorithms = t, this.languages = e, this.currentRecommendationId !== null && this.blockStates[this.currentRecommendationId]) {
332
- const n = this.blockStates[this.currentRecommendationId];
333
- n.filterStatus = !!n.recommendationConfigs.filters.length;
607
+ const i = this.blockStates[this.currentRecommendationId];
608
+ i.filterStatus = !!i.recommendationConfigs.filters.length;
334
609
  }
335
610
  this.currencyList = r;
336
611
  })();
337
612
  try {
338
- await m;
613
+ await h;
339
614
  } finally {
340
- m = null;
615
+ h = null;
341
616
  }
342
617
  }
343
618
  },
619
+ /**
620
+ * Fetch the partner's Reusable Recommendation Strategies for the EMAIL channel.
621
+ *
622
+ * Same cache-guard + in-flight dedupe shape as fetchRecommendationCreateData.
623
+ * A failure leaves `strategies` empty so the dropdown renders empty rather
624
+ * than blocking the panel — the block keeps working on its algorithm.
625
+ */
626
+ async fetchRecommendationStrategies() {
627
+ if (!this.strategies.length) {
628
+ if (d) {
629
+ await d;
630
+ return;
631
+ }
632
+ this.strategiesLoading = !0, d = (async () => {
633
+ try {
634
+ this.strategies = await c.fetchRecommendationStrategies() ?? [], this.strategiesLoadFailed = !1;
635
+ } catch {
636
+ this.strategies = [], this.strategiesLoadFailed = !0;
637
+ } finally {
638
+ this.strategiesLoading = !1;
639
+ }
640
+ })();
641
+ try {
642
+ await d;
643
+ } finally {
644
+ d = null;
645
+ }
646
+ }
647
+ },
648
+ /**
649
+ * Refetch the listing, ignoring the once-per-session cache.
650
+ *
651
+ * Needed after Duplicate and Assign: the copy exists server-side but not in
652
+ * `strategies`, so without this the panel would read the freshly assigned id
653
+ * back as a deleted strategy.
654
+ */
655
+ async refreshRecommendationStrategies() {
656
+ this.strategies = [], d = null, await this.fetchRecommendationStrategies();
657
+ },
344
658
  async fetchRecommendationFilters() {
345
659
  if (!Object.keys(this.filterList).length) {
346
- if (u) {
347
- await u;
660
+ if (p) {
661
+ await p;
348
662
  return;
349
663
  }
350
- u = (async () => {
351
- const t = await h.fetchRecommendationFilters();
664
+ p = (async () => {
665
+ const t = await c.fetchRecommendationFilters();
352
666
  this.filterList = t;
353
667
  })();
354
668
  try {
355
- await u;
669
+ await p;
356
670
  } finally {
357
- u = null;
671
+ p = null;
358
672
  }
359
673
  }
360
674
  },
@@ -369,59 +683,59 @@ const L = () => ({
369
683
  deleteFilterGroup(t) {
370
684
  if (this.currentRecommendationId === null || !this.blockStates[this.currentRecommendationId])
371
685
  return;
372
- const e = this.blockStates[this.currentRecommendationId], r = e.recommendationConfigs.filters.filter((i) => i.filterGroup !== t), n = [...new Set(r.map((i) => i.filterGroup))].sort((i, o) => i - o), c = new Map(n.map((i, o) => [i, o + 1]));
373
- e.recommendationConfigs.filters = r.map((i) => ({
374
- ...i,
375
- filterGroup: c.get(i.filterGroup) ?? i.filterGroup
686
+ const e = this.blockStates[this.currentRecommendationId], r = e.recommendationConfigs.filters.filter((n) => n.filterGroup !== t), i = [...new Set(r.map((n) => n.filterGroup))].sort((n, a) => n - a), s = new Map(i.map((n, a) => [n, a + 1]));
687
+ e.recommendationConfigs.filters = r.map((n) => ({
688
+ ...n,
689
+ filterGroup: s.get(n.filterGroup) ?? n.filterGroup
376
690
  }));
377
691
  },
378
692
  updateFilter(t) {
379
693
  if (this.currentRecommendationId === null || !this.blockStates[this.currentRecommendationId])
380
694
  return;
381
- const e = this.blockStates[this.currentRecommendationId], r = e.recommendationConfigs.filters.findIndex((n) => n.filterNumber === t.filterNumber && n.filterGroup === t.filterGroup);
695
+ const e = this.blockStates[this.currentRecommendationId], r = e.recommendationConfigs.filters.findIndex((i) => i.filterNumber === t.filterNumber && i.filterGroup === t.filterGroup);
382
696
  if (r !== -1) {
383
- const n = [...e.recommendationConfigs.filters];
384
- n[r] = {
697
+ const i = [...e.recommendationConfigs.filters];
698
+ i[r] = {
385
699
  ...t,
386
- isValid: N(t)
387
- }, e.recommendationConfigs.filters = n;
700
+ isValid: z(t)
701
+ }, e.recommendationConfigs.filters = i;
388
702
  }
389
703
  },
390
704
  deleteFilter(t) {
391
705
  if (this.currentRecommendationId === null || !this.blockStates[this.currentRecommendationId])
392
706
  return;
393
- const e = this.blockStates[this.currentRecommendationId], r = [...e.recommendationConfigs.filters].findIndex((n) => n.filterNumber === t.filterNumber && n.filterGroup === t.filterGroup);
707
+ const e = this.blockStates[this.currentRecommendationId], r = [...e.recommendationConfigs.filters].findIndex((i) => i.filterNumber === t.filterNumber && i.filterGroup === t.filterGroup);
394
708
  if (r !== -1) {
395
- let n = [...e.recommendationConfigs.filters];
396
- if (n.splice(r, 1), n.some((i) => i.filterGroup === t.filterGroup)) {
397
- let i = 1;
398
- n = n.map((o) => o.filterGroup === t.filterGroup ? { ...o, filterNumber: i++ } : o);
709
+ let i = [...e.recommendationConfigs.filters];
710
+ if (i.splice(r, 1), i.some((n) => n.filterGroup === t.filterGroup)) {
711
+ let n = 1;
712
+ i = i.map((a) => a.filterGroup === t.filterGroup ? { ...a, filterNumber: n++ } : a);
399
713
  } else {
400
- const i = [...new Set(n.map((s) => s.filterGroup))].sort((s, a) => s - a), o = new Map(i.map((s, a) => [s, a + 1]));
401
- n = n.map((s) => ({
402
- ...s,
403
- filterGroup: o.get(s.filterGroup) ?? s.filterGroup
714
+ const n = [...new Set(i.map((o) => o.filterGroup))].sort((o, l) => o - l), a = new Map(n.map((o, l) => [o, l + 1]));
715
+ i = i.map((o) => ({
716
+ ...o,
717
+ filterGroup: a.get(o.filterGroup) ?? o.filterGroup
404
718
  }));
405
719
  }
406
- e.recommendationConfigs.filters = n;
720
+ e.recommendationConfigs.filters = i;
407
721
  }
408
722
  },
409
723
  addFilter(t) {
410
724
  if (this.currentRecommendationId === null || !this.blockStates[this.currentRecommendationId])
411
725
  return;
412
- const e = this.blockStates[this.currentRecommendationId], r = [...e.recommendationConfigs.filters], c = r.filter(
413
- (o) => o.filterGroup === t.filterGroup
414
- ).length + 1, i = r.findLastIndex((o) => o.filterGroup === t.filterGroup);
415
- i !== -1 ? r.splice(i + 1, 0, {
726
+ const e = this.blockStates[this.currentRecommendationId], r = [...e.recommendationConfigs.filters], s = r.filter(
727
+ (a) => a.filterGroup === t.filterGroup
728
+ ).length + 1, n = r.findLastIndex((a) => a.filterGroup === t.filterGroup);
729
+ n !== -1 ? r.splice(n + 1, 0, {
416
730
  ...t,
417
- filterNumber: c
731
+ filterNumber: s
418
732
  }) : r.push({
419
733
  ...t,
420
- filterNumber: c
734
+ filterNumber: s
421
735
  }), e.recommendationConfigs.filters = r;
422
736
  },
423
737
  generateFilterQuery() {
424
- return b(this.recommendationConfigs.filters);
738
+ return v(this.recommendationConfigs.filters);
425
739
  },
426
740
  /**
427
741
  * Validation-only check invoked at save-CTA time. Defined as an action
@@ -429,55 +743,88 @@ const L = () => ({
429
743
  * every block's recommendationConfigs across user edits.
430
744
  */
431
745
  hasInvalidBlock() {
432
- return Object.values(this.blockStates).some((t) => !x(t.recommendationConfigs, this));
746
+ return Object.values(this.blockStates).some((t) => !M(t.recommendationConfigs, this));
433
747
  },
434
748
  // ====================================================================
435
749
  // Per-Block Product Fetching
436
750
  // ====================================================================
437
751
  async fetchRecommendationProducts() {
438
752
  if (!(this.currentRecommendationId === null || !this.blockStates[this.currentRecommendationId])) {
439
- if (d) {
440
- await d;
753
+ if (S) {
754
+ await S;
441
755
  return;
442
756
  }
443
- d = this._doFetchProducts();
757
+ S = this._doFetchProducts();
444
758
  try {
445
- await d;
759
+ await S;
446
760
  } finally {
447
- d = null;
761
+ S = null;
448
762
  }
449
763
  }
450
764
  },
451
765
  async _doFetchProducts() {
452
- var p;
453
- const t = this.currentRecommendationId, e = this.blockStates[t], { recommendationConfigs: r } = e, n = r.filters.filter((l) => l.isValid), c = b(n), i = ((p = g().find((l) => l.key === r.strategy)) == null ? void 0 : p.path) || "", o = G(), s = parseInt(r.size) || 6, a = {
766
+ var I, k;
767
+ const t = this.currentRecommendationId, e = this.blockStates[t];
768
+ e.recommendationConfigs.strategyId && await this.fetchStrategyUrlTemplate(e.recommendationConfigs.strategyId);
769
+ const r = ((I = this.blockStates[t]) == null ? void 0 : I.recommendationConfigs) ?? e.recommendationConfigs, i = r.strategyId ? this.strategyUrlTemplates[r.strategyId] : "", s = O(
770
+ i ?? "",
771
+ V({
772
+ language: r.language,
773
+ currencyCode: r.currencySettings.value,
774
+ size: r.size,
775
+ strategyId: r.strategyId
776
+ }),
777
+ "preview"
778
+ );
779
+ if (s) {
780
+ await this._applyFetchedProducts(
781
+ t,
782
+ () => c.fetchRecommendationProductsByUrl(s)
783
+ );
784
+ return;
785
+ }
786
+ const n = r.filters.filter((R) => R.isValid), a = v(n), o = ((k = w().find((R) => R.key === r.strategy)) == null ? void 0 : k.path) || "", l = F(), m = {
454
787
  locale: r.language,
455
788
  currency: r.currencySettings.value,
456
- partnerName: o.partnerName,
789
+ partnerName: l.partnerName,
457
790
  size: r.size,
458
791
  details: !0,
459
- campaignId: o.variationId
792
+ campaignId: l.variationId
460
793
  };
461
- r.strategy === "manualMerchandising" ? a.productId = r.productIds.join(",") : r.strategy === "similarViewed" && (a.productId = "{itemId}"), r.strategy === "userBased" && (a.userId = "{user_id}"), c && (a.filter = c), r.shuffleProducts && (a.shuffle = !0), Object.assign(
462
- a,
463
- v(o.partnerName, r.strategy)
794
+ r.strategy === "manualMerchandising" ? m.productId = r.productIds.join(",") : r.strategy === "similarViewed" && (m.productId = "{itemId}"), r.strategy === "userBased" && (m.userId = "{user_id}"), a && (m.filter = a), r.shuffleProducts && (m.shuffle = !0), Object.assign(
795
+ m,
796
+ E(l.partnerName, r.strategy)
797
+ ), await this._applyFetchedProducts(
798
+ t,
799
+ () => c.fetchRecommendationProducts(o, m)
464
800
  );
465
- let f;
801
+ },
802
+ /**
803
+ * Runs a product fetch and writes the result to `blockId`.
804
+ *
805
+ * Shared by the strategy and algorithm paths so the size padding/clipping —
806
+ * which the in-place card renderer depends on for its element-count match —
807
+ * cannot drift between them.
808
+ */
809
+ async _applyFetchedProducts(t, e) {
810
+ var s;
811
+ const r = parseInt(((s = this.blockStates[t]) == null ? void 0 : s.recommendationConfigs.size) ?? "") || 6;
812
+ let i;
466
813
  try {
467
- f = await h.fetchRecommendationProducts(i, a);
814
+ i = await e();
468
815
  } catch {
469
- f = [];
816
+ i = [];
470
817
  }
471
818
  if (this.blockStates[t]) {
472
- const l = f.length > 0 ? f : S(s);
473
- l.length < s ? this.blockStates[t].recommendationProducts = [
474
- ...l,
475
- ...S(s - l.length)
476
- ] : l.length > s ? this.blockStates[t].recommendationProducts = l.slice(0, s) : this.blockStates[t].recommendationProducts = l;
819
+ const n = i.length > 0 ? i : D(r);
820
+ n.length < r ? this.blockStates[t].recommendationProducts = [
821
+ ...n,
822
+ ...D(r - n.length)
823
+ ] : n.length > r ? this.blockStates[t].recommendationProducts = n.slice(0, r) : this.blockStates[t].recommendationProducts = n;
477
824
  }
478
825
  }
479
826
  }
480
827
  });
481
828
  export {
482
- $ as useRecommendationExtensionStore
829
+ ot as useRecommendationExtensionStore
483
830
  };