@useinsider/guido 3.19.0-beta.3906ea8 → 3.19.0-beta.3abc7c5

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 (54) hide show
  1. package/README.md +1 -0
  2. package/dist/@types/config/schemas.js +33 -31
  3. package/dist/components/Guido.vue.js +8 -8
  4. package/dist/components/Guido.vue2.js +50 -48
  5. package/dist/components/organisms/AutoSaveController.vue2.js +9 -8
  6. package/dist/components/organisms/header/EditorActions.vue.js +2 -2
  7. package/dist/components/organisms/header/EditorActions.vue2.js +85 -57
  8. package/dist/components/organisms/header/LeftSlot.vue.js +5 -5
  9. package/dist/components/organisms/header/LeftSlot.vue2.js +16 -12
  10. package/dist/components/organisms/validation/ValidationErrorDrawer.vue.js +20 -0
  11. package/dist/components/organisms/validation/ValidationErrorDrawer.vue2.js +48 -0
  12. package/dist/components/wrappers/WpDrawer.vue.js +9 -7
  13. package/dist/components/wrappers/WpDrawer.vue2.js +1 -1
  14. package/dist/composables/useActionsApi.js +52 -47
  15. package/dist/composables/useCodeEditorApi.js +23 -9
  16. package/dist/composables/useHtmlCompiler.js +37 -26
  17. package/dist/composables/useHtmlValidator.js +2 -0
  18. package/dist/composables/useSave.js +90 -45
  19. package/dist/composables/useSaveValidation.js +279 -0
  20. package/dist/composables/useSaveValidationRollout.js +9 -0
  21. package/dist/composables/useTranslations.js +3 -2
  22. package/dist/composables/useVersionHistoryApi.js +33 -24
  23. package/dist/composables/validators/useCouponBlockValidator.js +24 -21
  24. package/dist/composables/validators/useUnsubscribeBlockValidator.js +34 -27
  25. package/dist/enums/save-validation.js +4 -0
  26. package/dist/extensions/Blocks/Recommendation/store/recommendation.js +54 -47
  27. package/dist/guido.css +1 -1
  28. package/dist/src/@types/config/schemas.d.ts +4 -0
  29. package/dist/src/@types/save-validation.d.ts +19 -0
  30. package/dist/src/components/Guido.vue.d.ts +17 -1
  31. package/dist/src/components/organisms/header/EditorActions.vue.d.ts +17 -1
  32. package/dist/src/components/organisms/header/HeaderWrapper.vue.d.ts +17 -1
  33. package/dist/src/components/organisms/header/RightSlot.vue.d.ts +17 -1
  34. package/dist/src/components/organisms/validation/ValidationErrorDrawer.vue.d.ts +2 -0
  35. package/dist/src/components/wrappers/WpDrawer.vue.d.ts +4 -3
  36. package/dist/src/composables/useConfig.d.ts +2 -0
  37. package/dist/src/composables/useHtmlValidator.d.ts +21 -0
  38. package/dist/src/composables/useSave.d.ts +6 -1
  39. package/dist/src/composables/useSaveValidation.d.ts +6 -0
  40. package/dist/src/composables/useSaveValidationRollout.d.ts +3 -0
  41. package/dist/src/composables/validators/useCouponBlockValidator.d.ts +3 -0
  42. package/dist/src/composables/validators/useUnsubscribeBlockValidator.d.ts +10 -0
  43. package/dist/src/enums/save-validation.d.ts +27 -0
  44. package/dist/src/extensions/Blocks/Recommendation/store/recommendation.d.ts +5 -0
  45. package/dist/src/stores/config.d.ts +32 -0
  46. package/dist/src/stores/save-validation.d.ts +22 -0
  47. package/dist/src/utils/stripoEditorApi.d.ts +4 -0
  48. package/dist/src/utils/templatePreparation.d.ts +30 -0
  49. package/dist/stores/config.js +84 -67
  50. package/dist/stores/save-validation.js +30 -0
  51. package/dist/stores/unsubscribe.js +3 -3
  52. package/dist/utils/stripoEditorApi.js +11 -0
  53. package/dist/utils/templatePreparation.js +39 -36
  54. package/package.json +3 -2
@@ -0,0 +1,4 @@
1
+ var n = /* @__PURE__ */ ((s) => (s.CouponTagModified = "coupon-tag-modified", s.DuplicateUnsubBlock = "duplicate-unsub-block", s.UnsubBlockMissingTemplate = "unsub-block-missing-template", s.LiquidSyntax = "liquid-syntax", s.InvalidLinks = "invalid-links", s.LinkCheckInProgress = "link-check-in-progress", s.CustomFieldScript = "custom-field-script", s.EmptyHtml = "empty-html", s.MismatchedBraces = "mismatched-braces", s.InvalidDynamicTag = "invalid-dynamic-tag", s.InvalidCustomField = "invalid-custom-field", s.ConditionsSyntax = "conditions-syntax", s.WrongEqualityOperator = "wrong-equality-operator", s.MissingIfEndif = "missing-if-endif", s.ConditionTagSpacing = "condition-tag-spacing", s.BracesInsideIfTag = "braces-inside-if-tag", s.RecommendationRequiredFields = "recommendation-required-fields", s.RecommendationFilters = "recommendation-filters", s.InvalidImageType = "invalid-image-type", s.CheckboxUnsubGroup = "checkbox-unsub-group", s.RadioButtonUnsubGroup = "radio-button-unsub-group", s))(n || {});
2
+ export {
3
+ n as SaveValidationRule
4
+ };
@@ -1,27 +1,27 @@
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";
1
+ import { useToaster as L } from "../../../../composables/useToaster.js";
2
+ import { useTranslations as U } from "../../../../composables/useTranslations.js";
3
+ import { getRecommendationFeedSourceMaps as F, strategyEditPath as E, URLS as N, getOperatorOptions as M, PriceAttributes as A } from "../../../../enums/extensions/recommendationBlock.js";
4
+ import { ToasterTypeOptions as B } from "../../../../enums/toaster.js";
5
+ import { useRecommendationApi as O } from "../../../../services/recommendationApi.js";
6
+ import { useConfigStore as R } from "../../../../stores/config.js";
7
+ import { defineStore as T } from "pinia";
8
8
  import "../constants/selectors.js";
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";
9
+ import { DEFAULT_MOBILE_CARDS_IN_ROW as z, DEFAULT_CARDS_IN_ROW as x } from "../constants/layout.js";
10
+ import { CURRENT_CONFIG_VERSION as C, DEFAULT_NODE_CONFIG as V, EXCLUDED_ALGORITHM_IDS as j } from "../constants/defaultConfig.js";
11
11
  import { getDefaultProducts as w } from "../templates/utils.js";
12
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;
13
+ import { isLegacyStrategyConfig as $ } from "../utils/legacyStrategyMigration.js";
14
+ import { getPartnerRecommendationParams as q } from "../utils/partnerCustomizations.js";
15
+ import { sanitizeProductIds as K } from "../utils/productIds.js";
16
+ import { isFilterValid as H } from "../validation/filterSchema.js";
17
+ import { getInvalidFields as J, isConfigValid as G } from "../validation/requiredFields.js";
18
+ const m = O();
19
+ let S = null, p = null, y = null, I = "", k = null, b = 0;
20
20
  const g = /* @__PURE__ */ new Map();
21
21
  let l = null;
22
- function _() {
22
+ function D() {
23
23
  return {
24
- cardsInRow: z,
24
+ cardsInRow: x,
25
25
  currencySettings: {
26
26
  name: "USD",
27
27
  value: "USD",
@@ -35,7 +35,7 @@ function _() {
35
35
  productIds: [],
36
36
  id: 1,
37
37
  language: "en_US",
38
- mobileCardsInRow: O,
38
+ mobileCardsInRow: z,
39
39
  mobileLayoutEnabled: !1,
40
40
  orientation: "grid",
41
41
  recommendedProducts: [],
@@ -45,7 +45,7 @@ function _() {
45
45
  // that default is what reached buildCampaignUrl and baked a most-popular feed URL
46
46
  // for a block the user never configured (SD-149916). Legacy blocks still get their real
47
47
  // algorithm from the node-config sync or the save-time seed.
48
- strategy: k().isFeatureEnabled("reusableRecommendationStrategy") ? "" : "mostPopular",
48
+ strategy: R().isFeatureEnabled("reusableRecommendationStrategy") ? "" : "mostPopular",
49
49
  strategyId: "",
50
50
  nodeConfigVersion: C,
51
51
  textTrimming: !0,
@@ -56,9 +56,9 @@ function _() {
56
56
  priceHideIfSameAsDiscounted: !1
57
57
  };
58
58
  }
59
- function G() {
59
+ function _() {
60
60
  return {
61
- recommendationConfigs: _(),
61
+ recommendationConfigs: D(),
62
62
  recommendationProducts: [],
63
63
  filterStatus: !1,
64
64
  filterSelectionDrawerStatus: !1,
@@ -87,7 +87,7 @@ const Q = () => ({
87
87
  currentRecommendationId: null,
88
88
  strategyPanelOpen: !1,
89
89
  configVersion: 0
90
- }), ft = B("guidoRecommendationExtension", {
90
+ }), ft = T("guidoRecommendationExtension", {
91
91
  state: () => Q(),
92
92
  getters: {
93
93
  // ====================================================================
@@ -98,7 +98,7 @@ const Q = () => ({
98
98
  * This allows all existing code that reads `store.recommendationConfigs` to work unchanged.
99
99
  */
100
100
  recommendationConfigs(t) {
101
- return t.currentRecommendationId !== null && t.blockStates[t.currentRecommendationId] ? t.blockStates[t.currentRecommendationId].recommendationConfigs : _();
101
+ return t.currentRecommendationId !== null && t.blockStates[t.currentRecommendationId] ? t.blockStates[t.currentRecommendationId].recommendationConfigs : D();
102
102
  },
103
103
  /**
104
104
  * Proxy getter: delegates to blockStates[currentRecommendationId].recommendationProducts
@@ -119,7 +119,7 @@ const Q = () => ({
119
119
  return t.currentRecommendationId !== null && t.blockStates[t.currentRecommendationId] ? t.blockStates[t.currentRecommendationId].removeStrategyModalStatus : !1;
120
120
  },
121
121
  isLegacyStrategyBlock() {
122
- return k().isFeatureEnabled("reusableRecommendationStrategy") && j(this.recommendationConfigs);
122
+ return R().isFeatureEnabled("reusableRecommendationStrategy") && $(this.recommendationConfigs);
123
123
  },
124
124
  legacyConversionLoading(t) {
125
125
  return t.currentRecommendationId !== null && t.blockStates[t.currentRecommendationId] ? t.blockStates[t.currentRecommendationId].legacyConversionLoading ?? !1 : !1;
@@ -163,7 +163,7 @@ const Q = () => ({
163
163
  },
164
164
  getActivePredictiveAlgorithms: (t) => {
165
165
  const e = F(), r = [];
166
- return t.activePredictiveAlgorithms.filter((n) => !V.includes(n)).forEach((n) => {
166
+ return t.activePredictiveAlgorithms.filter((n) => !j.includes(n)).forEach((n) => {
167
167
  r.push(...e.filter((s) => s.id === n));
168
168
  }), r.map((n) => ({
169
169
  text: n.name,
@@ -181,7 +181,7 @@ const Q = () => ({
181
181
  getFilterList() {
182
182
  return Object.values(this.filterList).filter((t) => t.isFilterable !== 0 && t.isFilterable !== !1).map((t) => {
183
183
  let e;
184
- return t.type === "productAttribute" ? e = `product_attributes.${t.attributeName}` : M.includes(t.attributeName) ? e = `${t.attributeName}.${this.recommendationConfigs.currencySettings.value}` : e = t.attributeName, {
184
+ return t.type === "productAttribute" ? e = `product_attributes.${t.attributeName}` : A.includes(t.attributeName) ? e = `${t.attributeName}.${this.recommendationConfigs.currencySettings.value}` : e = t.attributeName, {
185
185
  text: t.displayName,
186
186
  value: e,
187
187
  type: t.attributeType
@@ -203,7 +203,7 @@ const Q = () => ({
203
203
  setCurrentBlock(t) {
204
204
  this.blockStates[t] || (this.blockStates = {
205
205
  ...this.blockStates,
206
- [t]: G()
206
+ [t]: _()
207
207
  }), this.currentRecommendationId = t;
208
208
  },
209
209
  /**
@@ -234,7 +234,7 @@ const Q = () => ({
234
234
  const r = this.blockStates[t];
235
235
  if (r) {
236
236
  const o = structuredClone(r);
237
- o.recommendationConfigs.id = e, o.recommendationConfigs.strategyId && k().isFeatureEnabled("reusableRecommendationStrategy") && (o.isInitialized = !1, o.syncedConfigFingerprint = null), this.blockStates = {
237
+ o.recommendationConfigs.id = e, o.recommendationConfigs.strategyId && R().isFeatureEnabled("reusableRecommendationStrategy") && (o.isInitialized = !1, o.syncedConfigFingerprint = null), this.blockStates = {
238
238
  ...this.blockStates,
239
239
  [e]: o
240
240
  };
@@ -275,7 +275,7 @@ const Q = () => ({
275
275
  decimalCount: String(e.currencyDecimalCount),
276
276
  decimalSeparator: r(e.currencyDecimalSeparator, ","),
277
277
  thousandSeparator: r(e.currencyThousandSeparator, ".")
278
- }, s = !this.blockStates[t], i = s ? G() : this.blockStates[t];
278
+ }, s = !this.blockStates[t], i = s ? _() : this.blockStates[t];
279
279
  i.recommendationConfigs = {
280
280
  ...i.recommendationConfigs,
281
281
  strategy: e.strategy,
@@ -314,7 +314,7 @@ const Q = () => ({
314
314
  * Creates a filter with the first available attribute and operator pre-selected.
315
315
  */
316
316
  createDefaultFilter(t, e) {
317
- const [r] = this.getFilterList, [n] = N(r == null ? void 0 : r.type);
317
+ const [r] = this.getFilterList, [n] = M(r == null ? void 0 : r.type);
318
318
  return {
319
319
  type: "standardFilter",
320
320
  attribute: (r == null ? void 0 : r.value) ?? "",
@@ -380,9 +380,9 @@ const Q = () => ({
380
380
  this.currentRecommendationId === null || !this.blockStates[this.currentRecommendationId] || (this.blockStates[this.currentRecommendationId].removeStrategyModalStatus = !1);
381
381
  },
382
382
  async fetchStrategies() {
383
- for (; R; )
383
+ for (; k; )
384
384
  try {
385
- await R;
385
+ await k;
386
386
  } catch {
387
387
  }
388
388
  if (this.strategiesFetched)
@@ -398,11 +398,11 @@ const Q = () => ({
398
398
  this.strategiesLoading = !1, t === b && (this.strategiesSettled = !0);
399
399
  }
400
400
  })();
401
- R = e;
401
+ k = e;
402
402
  try {
403
403
  await e;
404
404
  } finally {
405
- R = null;
405
+ k = null;
406
406
  }
407
407
  },
408
408
  /**
@@ -434,9 +434,9 @@ const Q = () => ({
434
434
  _armStrategyPageReturn(t) {
435
435
  const e = window.open(t, "_blank");
436
436
  if (!e)
437
- return D().showToaster({
438
- type: A.Error,
439
- message: L()("discovery-strategy.error-description")
437
+ return L().showToaster({
438
+ type: B.Error,
439
+ message: U()("discovery-strategy.error-description")
440
440
  }), !1;
441
441
  e.opener = null, l == null || l();
442
442
  let r = !1;
@@ -471,14 +471,14 @@ const Q = () => ({
471
471
  * new strategy is offered; the user picks it themselves.
472
472
  */
473
473
  openStrategyCreation() {
474
- return this._armStrategyPageReturn(E.RECOMMENDATION_STRATEGY_CREATE_PATH);
474
+ return this._armStrategyPageReturn(N.RECOMMENDATION_STRATEGY_CREATE_PATH);
475
475
  },
476
476
  /**
477
477
  * Opens the strategy's edit page. Every return refreshes the list so the block reflects the
478
478
  * edited configuration.
479
479
  */
480
480
  openStrategyEdit(t) {
481
- return this._armStrategyPageReturn(U(t));
481
+ return this._armStrategyPageReturn(E(t));
482
482
  },
483
483
  async resolveStrategyUrlTemplate(t, e) {
484
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;
@@ -532,7 +532,7 @@ const Q = () => ({
532
532
  this.patchCurrentBlockConfig({
533
533
  strategyId: "",
534
534
  strategy: "",
535
- size: x.size,
535
+ size: V.size,
536
536
  nodeConfigVersion: C
537
537
  });
538
538
  },
@@ -606,7 +606,7 @@ const Q = () => ({
606
606
  const n = [...e.recommendationConfigs.filters];
607
607
  n[r] = {
608
608
  ...t,
609
- isValid: K(t)
609
+ isValid: H(t)
610
610
  }, e.recommendationConfigs.filters = n;
611
611
  }
612
612
  },
@@ -652,14 +652,21 @@ const Q = () => ({
652
652
  * every block's recommendationConfigs across user edits.
653
653
  */
654
654
  hasInvalidBlock() {
655
- return Object.values(this.blockStates).some((t) => !J(t.recommendationConfigs, this));
655
+ return Object.values(this.blockStates).some((t) => !G(t.recommendationConfigs, this));
656
+ },
657
+ /**
658
+ * Per-instance counterpart to `hasInvalidBlock`: the ids of every block
659
+ * that fails required-field validation, one drawer row each (SD-147430).
660
+ */
661
+ invalidBlockIds() {
662
+ return Object.entries(this.blockStates).filter(([, t]) => !G(t.recommendationConfigs, this)).map(([t]) => t);
656
663
  },
657
664
  /**
658
665
  * Which required fields are missing across the design's blocks, so the save
659
666
  * toaster can name the cause instead of always showing the generic message.
660
667
  */
661
668
  invalidBlockFields() {
662
- return Object.values(this.blockStates).flatMap((t) => H(t.recommendationConfigs, this));
669
+ return Object.values(this.blockStates).flatMap((t) => J(t.recommendationConfigs, this));
663
670
  },
664
671
  // ====================================================================
665
672
  // Per-Block Product Fetching
@@ -685,7 +692,7 @@ const Q = () => ({
685
692
  },
686
693
  async _doFetchProducts() {
687
694
  var h;
688
- const t = this.currentRecommendationId, e = this.blockStates[t], { recommendationConfigs: r } = e, n = k();
695
+ const t = this.currentRecommendationId, e = this.blockStates[t], { recommendationConfigs: r } = e, n = R();
689
696
  if (r.strategyId && n.isFeatureEnabled("reusableRecommendationStrategy")) {
690
697
  await this._doFetchStrategyProducts(t);
691
698
  return;
@@ -705,9 +712,9 @@ const Q = () => ({
705
712
  details: !0,
706
713
  campaignId: n.variationId
707
714
  };
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(
715
+ r.strategy === "manualMerchandising" ? c.productId = K(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
716
  c,
710
- $(n.partnerName, r.strategy)
717
+ q(n.partnerName, r.strategy)
711
718
  );
712
719
  let u;
713
720
  try {
package/dist/guido.css CHANGED
@@ -1 +1 @@
1
- @charset "UTF-8";.gap-16[data-v-5553d071],.gap-16[data-v-0e1b0c54]{gap:16px}[data-v-73199fa4] .in-button-v2__wrapper{line-height:0}[data-v-22226124] .in-segments-wrapper__button_selected,[data-v-22226124] .in-segments-wrapper__button_selected:hover{background-color:#dae1fb;color:#0010ac;border-color:#0010ac}[data-v-2cb418af] .in-progress-wrapper__progress p span:last-child{display:none!important}[data-v-2cb418af] .in-progress-description-status{display:none!important}.view-options-wrapper[data-v-87033101]{position:relative;display:inline-block}.new-tag[data-v-87033101]{position:absolute;top:-8px;right:-16px;z-index:10}[data-v-87033101] .guido__view-option-selection-desktop svg,[data-v-87033101] .guido__view-option-selection-mobile svg{margin:0 0 0 2px}[data-v-87033101] .in-segments-wrapper__button_selected,[data-v-87033101] .in-segments-wrapper__button_selected:hover{background-color:#dae1fb}[data-v-87033101] .in-tooltip-wrapper-v2__icon{cursor:pointer}.editor-toolbar[data-v-173c3a40]{gap:4px}.version-history-item[data-v-ee4b9c3f]{flex-basis:200px}.version-history[data-v-fad98586]{gap:8px}.version-history__toolbar[data-v-fad98586]{gap:4px}.view-options-wrapper[data-v-6ebabfe4]{position:relative;display:inline-block}.new-tag[data-v-6ebabfe4]{position:absolute;top:-8px;right:-16px;z-index:10}[data-v-6ebabfe4] .guido__verion-history-view-option-selection-desktop svg,[data-v-6ebabfe4] .guido__verion-history-view-option-selection-mobile svg{margin:0 0 0 2px}[data-v-6ebabfe4] .in-segments-wrapper__button_selected,[data-v-6ebabfe4] .in-segments-wrapper__button_selected:hover{background-color:#dae1fb}[data-v-6ebabfe4] .in-tooltip-wrapper-v2__icon{cursor:pointer}.auto-save-toggle[data-v-2c964af4]{position:relative}.auto-save-toggle__info-box[data-v-2c964af4]{position:absolute;top:100%;left:0;z-index:10;width:280px}.editor-actions[data-v-1ca6baf0]{gap:4px}.header-wrapper[data-v-d11dd577]{min-width:1000px}.guido-loading__wrapper[data-v-07c4b2d8]{height:100%;top:75px!important;bottom:0!important}.guido-editor__wrapper[data-v-36867e2e]{--ribbon-offset: 0px;position:relative;width:100%;height:calc(100vh - 128px - var(--ribbon-offset))}.guido-editor__container[data-v-36867e2e]{width:100%;height:calc(100vh - 128px - var(--ribbon-offset))}.guido-editor__no-header[data-v-36867e2e]{height:calc(100vh - 75px - var(--ribbon-offset))}[data-v-293f1c47] .in-breadcrumb-wrapper__links{cursor:pointer}.templates-wrapper[data-v-91a26acd]{gap:16px;grid-template-columns:repeat(3,1fr)}.templates-wrapper .template-wrapper[data-v-91a26acd]{cursor:pointer}.templates-wrapper .template-wrapper .template-container[data-v-91a26acd]{height:274px;padding:2px;transition:none}.templates-wrapper .template-wrapper .template-container.selected[data-v-91a26acd]{padding:1px}.templates-wrapper .template-wrapper .template-container .thumbnail[data-v-91a26acd]{object-fit:cover;transform:scale(1)}[data-v-b9a93c6e] .guido__verion-history-view-option-selection-desktop svg,[data-v-b9a93c6e] .guido__verion-history-view-option-selection-mobile svg{margin:0 0 0 2px}[data-v-b9a93c6e] .in-segments-wrapper__button_selected,[data-v-b9a93c6e] .in-segments-wrapper__button_selected:hover{background-color:#dae1fb}.error-list[data-v-c3fd5d4b]{gap:16px}.desktop-browser-header[data-v-d86c5af5]{height:79px;min-height:79px}.desktop-browser-header__left[data-v-d86c5af5]{-webkit-user-drag:none;height:79px;width:378px}.desktop-browser-header__center[data-v-d86c5af5]{height:79px;background-repeat:repeat-x;background-size:auto 100%;background-position:left top}.desktop-browser-header__right[data-v-d86c5af5]{-webkit-user-drag:none;height:79px;width:112px}.desktop-preview[data-v-cf946232]{min-width:602px;height:70vh;min-height:583px;border-radius:10px}.desktop-preview iframe[data-v-cf946232]{min-height:504px}.iframe-wrapper[data-v-f255b2bb]{width:258px}.iframe-scaled[data-v-f255b2bb]{width:320px;height:124.0310077519%;transform:scale(.80625);transform-origin:top left}.cropped-text[data-v-eb3d05d7]{width:220px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.mobile-preview-wrapper__phone[data-v-3f472f96]{width:282px}.mobile-preview-wrapper__phone img[data-v-3f472f96]{object-fit:cover;border-radius:44px}.mobile-preview-wrapper__content[data-v-3f472f96]{width:258px;height:450px;left:12px}[data-v-6bcaca8a] .vueperslides__parallax-wrapper{padding-bottom:110px!important}[data-v-f16f20f8] .vueperslides__bullets{pointer-events:none!important}[data-v-f16f20f8] .vueperslides__parallax-wrapper{padding-bottom:110px!important}[data-v-bb3cca55] .vueperslides__bullets{pointer-events:none!important}[data-v-bb3cca55] .vueperslides__parallax-wrapper{height:110px!important;padding-bottom:110px!important}[data-v-19d8cb42] .vueperslides__bullets,[data-v-ec2027ff] .vueperslides__bullets,[data-v-b09e80c4] .vueperslides__bullets{pointer-events:none!important}[data-v-b09e80c4] .vueperslides__parallax-wrapper{height:110px!important;padding-bottom:110px!important}[data-v-9bdef600] .vueperslides__parallax-wrapper{padding-bottom:110px!important}.assign-strategy-drawer-content .assign-strategy-select[data-v-6ce48dfd]{width:100%;max-width:none}.assign-strategy-drawer-content .assign-strategy-select[data-v-6ce48dfd] .in-skeleton{width:100%!important}.assign-strategy-drawer-content .assign-strategy-select[data-v-6ce48dfd] .in-dropdown-box__container{width:413px}.assign-strategy-drawer .spaced-dot-sep{background-image:linear-gradient(90deg,#91949f 20%,#fff0 0);background-position:center bottom;background-repeat:repeat-x;background-size:5px 1px}.assign-strategy-drawer .spaced-dot-sep--top{background-position:center top}.assign-strategy-drawer .spaced-dot-sep--fc-n:first-child{background:none}.legacy-strategy-updated-modal[data-v-da2225a1]{position:relative;z-index:145}
1
+ @charset "UTF-8";.gap-16[data-v-5553d071],.gap-16[data-v-0e1b0c54]{gap:16px}[data-v-b8f24f34] .in-button-v2__wrapper{line-height:0}[data-v-22226124] .in-segments-wrapper__button_selected,[data-v-22226124] .in-segments-wrapper__button_selected:hover{background-color:#dae1fb;color:#0010ac;border-color:#0010ac}[data-v-2cb418af] .in-progress-wrapper__progress p span:last-child{display:none!important}[data-v-2cb418af] .in-progress-description-status{display:none!important}.view-options-wrapper[data-v-87033101]{position:relative;display:inline-block}.new-tag[data-v-87033101]{position:absolute;top:-8px;right:-16px;z-index:10}[data-v-87033101] .guido__view-option-selection-desktop svg,[data-v-87033101] .guido__view-option-selection-mobile svg{margin:0 0 0 2px}[data-v-87033101] .in-segments-wrapper__button_selected,[data-v-87033101] .in-segments-wrapper__button_selected:hover{background-color:#dae1fb}[data-v-87033101] .in-tooltip-wrapper-v2__icon{cursor:pointer}.editor-toolbar[data-v-173c3a40]{gap:4px}.version-history-item[data-v-ee4b9c3f]{flex-basis:200px}.version-history[data-v-fad98586]{gap:8px}.version-history__toolbar[data-v-fad98586]{gap:4px}.view-options-wrapper[data-v-6ebabfe4]{position:relative;display:inline-block}.new-tag[data-v-6ebabfe4]{position:absolute;top:-8px;right:-16px;z-index:10}[data-v-6ebabfe4] .guido__verion-history-view-option-selection-desktop svg,[data-v-6ebabfe4] .guido__verion-history-view-option-selection-mobile svg{margin:0 0 0 2px}[data-v-6ebabfe4] .in-segments-wrapper__button_selected,[data-v-6ebabfe4] .in-segments-wrapper__button_selected:hover{background-color:#dae1fb}[data-v-6ebabfe4] .in-tooltip-wrapper-v2__icon{cursor:pointer}.auto-save-toggle[data-v-2c964af4]{position:relative}.auto-save-toggle__info-box[data-v-2c964af4]{position:absolute;top:100%;left:0;z-index:10;width:280px}.editor-actions[data-v-5e7f10cc]{gap:4px}.header-wrapper[data-v-d11dd577]{min-width:1000px}.guido-loading__wrapper[data-v-07c4b2d8]{height:100%;top:75px!important;bottom:0!important}.validation-error-list[data-v-30d2e1f9]{gap:16px}.validation-error-row[data-v-30d2e1f9]{gap:8px;padding:24px;border:1px solid #E1E1E1;border-radius:8px}.validation-error-row__icon[data-v-30d2e1f9]{flex-shrink:0;font-size:24px;line-height:0}.validation-error-row__text[data-v-30d2e1f9]{font-size:13px;line-height:16px;color:#292929;word-break:break-word}.guido-editor__wrapper[data-v-74f39e54]{--ribbon-offset: 0px;position:relative;width:100%;height:calc(100vh - 128px - var(--ribbon-offset))}.guido-editor__container[data-v-74f39e54]{width:100%;height:calc(100vh - 128px - var(--ribbon-offset))}.guido-editor__no-header[data-v-74f39e54]{height:calc(100vh - 75px - var(--ribbon-offset))}[data-v-293f1c47] .in-breadcrumb-wrapper__links{cursor:pointer}.templates-wrapper[data-v-91a26acd]{gap:16px;grid-template-columns:repeat(3,1fr)}.templates-wrapper .template-wrapper[data-v-91a26acd]{cursor:pointer}.templates-wrapper .template-wrapper .template-container[data-v-91a26acd]{height:274px;padding:2px;transition:none}.templates-wrapper .template-wrapper .template-container.selected[data-v-91a26acd]{padding:1px}.templates-wrapper .template-wrapper .template-container .thumbnail[data-v-91a26acd]{object-fit:cover;transform:scale(1)}[data-v-b9a93c6e] .guido__verion-history-view-option-selection-desktop svg,[data-v-b9a93c6e] .guido__verion-history-view-option-selection-mobile svg{margin:0 0 0 2px}[data-v-b9a93c6e] .in-segments-wrapper__button_selected,[data-v-b9a93c6e] .in-segments-wrapper__button_selected:hover{background-color:#dae1fb}.error-list[data-v-c3fd5d4b]{gap:16px}.desktop-browser-header[data-v-d86c5af5]{height:79px;min-height:79px}.desktop-browser-header__left[data-v-d86c5af5]{-webkit-user-drag:none;height:79px;width:378px}.desktop-browser-header__center[data-v-d86c5af5]{height:79px;background-repeat:repeat-x;background-size:auto 100%;background-position:left top}.desktop-browser-header__right[data-v-d86c5af5]{-webkit-user-drag:none;height:79px;width:112px}.desktop-preview[data-v-cf946232]{min-width:602px;height:70vh;min-height:583px;border-radius:10px}.desktop-preview iframe[data-v-cf946232]{min-height:504px}.iframe-wrapper[data-v-f255b2bb]{width:258px}.iframe-scaled[data-v-f255b2bb]{width:320px;height:124.0310077519%;transform:scale(.80625);transform-origin:top left}.cropped-text[data-v-eb3d05d7]{width:220px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.mobile-preview-wrapper__phone[data-v-3f472f96]{width:282px}.mobile-preview-wrapper__phone img[data-v-3f472f96]{object-fit:cover;border-radius:44px}.mobile-preview-wrapper__content[data-v-3f472f96]{width:258px;height:450px;left:12px}[data-v-6bcaca8a] .vueperslides__parallax-wrapper{padding-bottom:110px!important}[data-v-f16f20f8] .vueperslides__bullets{pointer-events:none!important}[data-v-f16f20f8] .vueperslides__parallax-wrapper{padding-bottom:110px!important}[data-v-bb3cca55] .vueperslides__bullets{pointer-events:none!important}[data-v-bb3cca55] .vueperslides__parallax-wrapper{height:110px!important;padding-bottom:110px!important}[data-v-19d8cb42] .vueperslides__bullets,[data-v-ec2027ff] .vueperslides__bullets,[data-v-b09e80c4] .vueperslides__bullets{pointer-events:none!important}[data-v-b09e80c4] .vueperslides__parallax-wrapper{height:110px!important;padding-bottom:110px!important}[data-v-9bdef600] .vueperslides__parallax-wrapper{padding-bottom:110px!important}.assign-strategy-drawer-content .assign-strategy-select[data-v-6ce48dfd]{width:100%;max-width:none}.assign-strategy-drawer-content .assign-strategy-select[data-v-6ce48dfd] .in-skeleton{width:100%!important}.assign-strategy-drawer-content .assign-strategy-select[data-v-6ce48dfd] .in-dropdown-box__container{width:413px}.assign-strategy-drawer .spaced-dot-sep{background-image:linear-gradient(90deg,#91949f 20%,#fff0 0);background-position:center bottom;background-repeat:repeat-x;background-size:5px 1px}.assign-strategy-drawer .spaced-dot-sep--top{background-position:center top}.assign-strategy-drawer .spaced-dot-sep--fc-n:first-child{background:none}.legacy-strategy-updated-modal[data-v-da2225a1]{position:relative;z-index:145}
@@ -403,6 +403,8 @@ export declare const FeaturesSchema: v.ObjectSchema<{
403
403
  readonly allowlistEnabled: v.OptionalSchema<v.BooleanSchema<undefined>, false>;
404
404
  /** Enable persistent link tracking-id assignment for the click heatmap (opt-in) */
405
405
  readonly linkTrackingIds: v.OptionalSchema<v.BooleanSchema<undefined>, false>;
406
+ /** Allow persisting a template that still has validation errors (drawer "Save and Continue" soft-save). Host sets false for transactional + unsubscribe. */
407
+ readonly allowSaveWithErrors: v.OptionalSchema<v.BooleanSchema<undefined>, true>;
406
408
  readonly reusableRecommendationStrategy: v.OptionalSchema<v.BooleanSchema<undefined>, false>;
407
409
  }, undefined>;
408
410
  /**
@@ -811,6 +813,8 @@ export declare const GuidoConfigSchema: v.ObjectSchema<{
811
813
  readonly allowlistEnabled: v.OptionalSchema<v.BooleanSchema<undefined>, false>;
812
814
  /** Enable persistent link tracking-id assignment for the click heatmap (opt-in) */
813
815
  readonly linkTrackingIds: v.OptionalSchema<v.BooleanSchema<undefined>, false>;
816
+ /** Allow persisting a template that still has validation errors (drawer "Save and Continue" soft-save). Host sets false for transactional + unsubscribe. */
817
+ readonly allowSaveWithErrors: v.OptionalSchema<v.BooleanSchema<undefined>, true>;
814
818
  readonly reusableRecommendationStrategy: v.OptionalSchema<v.BooleanSchema<undefined>, false>;
815
819
  }, undefined>, {}>;
816
820
  /** Block configuration */
@@ -0,0 +1,19 @@
1
+ import type { SaveValidationRule } from '@@/Enums/save-validation';
2
+ import type { ToasterTypeOptions } from '@@/Enums/toaster';
3
+ /** Which action triggered the validation run. Drives telemetry only in v1. */
4
+ export type SaveValidationIntent = 'save' | 'back' | 'test';
5
+ /**
6
+ * A single validation failure instance, one per drawer row. Names avoid the
7
+ * public `ValidationError` (config validation) collision — NOT exported from
8
+ * library.ts.
9
+ */
10
+ export interface SaveValidationError {
11
+ /** Stable discriminator for tests and de-duplication. */
12
+ rule: SaveValidationRule;
13
+ /** Final translated string — trans() is applied at collect time. */
14
+ message: string;
15
+ /** Raw user content (field name / tag / src) — not i18n. */
16
+ detail?: string;
17
+ /** Mirrors the legacy toaster severity. Kept for the discriminator/tests; the drawer renders a uniform icon and never reads it. */
18
+ type: ToasterTypeOptions;
19
+ }
@@ -15,7 +15,23 @@ declare const _default: import("vue").DefineComponent<__VLS_WithDefaults<__VLS_T
15
15
  close: () => void;
16
16
  };
17
17
  hasChanges: import("vue").ComputedRef<boolean>;
18
- saveSilent: () => Promise<Omit<SavedTemplateDetails, "metadata" | "silent"> | undefined> | undefined;
18
+ saveSilent: () => Promise<{
19
+ dynamicContentList: import("@@/Types/generic").DynamicContent[];
20
+ compiledHtml: string;
21
+ rawHtml: string;
22
+ css: string;
23
+ ampHtml: string;
24
+ ampErrors: string[];
25
+ modules: number[];
26
+ recommendation: {
27
+ campaignUrls: Record<string, import("../@types/recommendation").RecommendationCampaignUrlValue>;
28
+ configs: Record<string, string>;
29
+ };
30
+ unsubscribe: {
31
+ status: boolean;
32
+ config: number[];
33
+ };
34
+ } | undefined> | undefined;
19
35
  setLoading: (value: boolean) => void;
20
36
  }, {}, {}, {}, import("vue/types/v3-component-options.js").ComponentOptionsMixin, import("vue/types/v3-component-options.js").ComponentOptionsMixin, {
21
37
  "dynamic-content:open": (detail: {
@@ -1,4 +1,20 @@
1
1
  declare const _default: import("vue").DefineComponent<{}, {
2
- handleSave: (isSilent: boolean) => Promise<Omit<import("../../../@types/stripo.js").SavedTemplateDetails, "metadata" | "silent"> | undefined> | undefined;
2
+ handleSave: (isSilent: boolean) => Promise<{
3
+ dynamicContentList: import("../../../@types/generic.js").DynamicContent[];
4
+ compiledHtml: string;
5
+ rawHtml: string;
6
+ css: string;
7
+ ampHtml: string;
8
+ ampErrors: string[];
9
+ modules: number[];
10
+ recommendation: {
11
+ campaignUrls: Record<string, import("../../../@types/recommendation.js").RecommendationCampaignUrlValue>;
12
+ configs: Record<string, string>;
13
+ };
14
+ unsubscribe: {
15
+ status: boolean;
16
+ config: number[];
17
+ };
18
+ } | undefined> | undefined;
3
19
  }, {}, {}, {}, import("vue/types/v3-component-options.js").ComponentOptionsMixin, import("vue/types/v3-component-options.js").ComponentOptionsMixin, {}, string, Readonly<import("vue").ExtractPropTypes<{}>>, {}>;
4
20
  export default _default;
@@ -1,4 +1,20 @@
1
1
  declare const _default: import("vue").DefineComponent<{}, {
2
- handleSave: (isSilent: boolean) => Promise<Omit<import("../../../@types/stripo.js").SavedTemplateDetails, "metadata" | "silent"> | undefined> | undefined;
2
+ handleSave: (isSilent: boolean) => Promise<{
3
+ dynamicContentList: import("../../../@types/generic.js").DynamicContent[];
4
+ compiledHtml: string;
5
+ rawHtml: string;
6
+ css: string;
7
+ ampHtml: string;
8
+ ampErrors: string[];
9
+ modules: number[];
10
+ recommendation: {
11
+ campaignUrls: Record<string, import("../../../@types/recommendation.js").RecommendationCampaignUrlValue>;
12
+ configs: Record<string, string>;
13
+ };
14
+ unsubscribe: {
15
+ status: boolean;
16
+ config: number[];
17
+ };
18
+ } | undefined> | undefined;
3
19
  }, {}, {}, {}, import("vue/types/v3-component-options.js").ComponentOptionsMixin, import("vue/types/v3-component-options.js").ComponentOptionsMixin, {}, string, Readonly<import("vue").ExtractPropTypes<{}>>, {}>;
4
20
  export default _default;
@@ -1,4 +1,20 @@
1
1
  declare const _default: import("vue").DefineComponent<{}, {
2
- handleSave: (isSilent: boolean) => Promise<Omit<import("../../../@types/stripo.js").SavedTemplateDetails, "metadata" | "silent"> | undefined> | undefined;
2
+ handleSave: (isSilent: boolean) => Promise<{
3
+ dynamicContentList: import("../../../@types/generic.js").DynamicContent[];
4
+ compiledHtml: string;
5
+ rawHtml: string;
6
+ css: string;
7
+ ampHtml: string;
8
+ ampErrors: string[];
9
+ modules: number[];
10
+ recommendation: {
11
+ campaignUrls: Record<string, import("../../../@types/recommendation.js").RecommendationCampaignUrlValue>;
12
+ configs: Record<string, string>;
13
+ };
14
+ unsubscribe: {
15
+ status: boolean;
16
+ config: number[];
17
+ };
18
+ } | undefined> | undefined;
3
19
  }, {}, {}, {}, import("vue/types/v3-component-options.js").ComponentOptionsMixin, import("vue/types/v3-component-options.js").ComponentOptionsMixin, {}, string, Readonly<import("vue").ExtractPropTypes<{}>>, {}>;
4
20
  export default _default;
@@ -0,0 +1,2 @@
1
+ declare const _default: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue/types/v3-component-options.js").ComponentOptionsMixin, import("vue/types/v3-component-options.js").ComponentOptionsMixin, {}, string, Readonly<import("vue").ExtractPropTypes<{}>>, {}>;
2
+ export default _default;
@@ -10,11 +10,11 @@ interface Props {
10
10
  status: boolean;
11
11
  titleText: string;
12
12
  }
13
- declare var __VLS_12: {}, __VLS_14: {};
13
+ declare var __VLS_13: {}, __VLS_15: {};
14
14
  type __VLS_Slots = {} & {
15
- headerBottomSlot?: (props: typeof __VLS_12) => any;
15
+ headerBottomSlot?: (props: typeof __VLS_13) => any;
16
16
  } & {
17
- default?: (props: typeof __VLS_14) => any;
17
+ default?: (props: typeof __VLS_15) => any;
18
18
  };
19
19
  declare const __VLS_component: import("vue").DefineComponent<__VLS_WithDefaults<__VLS_TypePropsToOption<Props>, {
20
20
  className: string;
@@ -26,6 +26,7 @@ declare const __VLS_component: import("vue").DefineComponent<__VLS_WithDefaults<
26
26
  cancelOrBackButtonEvent: () => void;
27
27
  onCloseEvent: () => void;
28
28
  primaryButtonEvent: () => void;
29
+ secondaryButtonEvent: () => void;
29
30
  }, string, Readonly<import("vue").ExtractPropTypes<__VLS_WithDefaults<__VLS_TypePropsToOption<Props>, {
30
31
  className: string;
31
32
  size: string;
@@ -101,6 +101,7 @@ export declare const useConfig: () => {
101
101
  autosave: boolean;
102
102
  allowlistEnabled: boolean;
103
103
  linkTrackingIds: boolean;
104
+ allowSaveWithErrors: boolean;
104
105
  reusableRecommendationStrategy: boolean;
105
106
  };
106
107
  blocks: {
@@ -238,6 +239,7 @@ export declare const useConfig: () => {
238
239
  autosave: boolean;
239
240
  allowlistEnabled: boolean;
240
241
  linkTrackingIds: boolean;
242
+ allowSaveWithErrors: boolean;
241
243
  reusableRecommendationStrategy: boolean;
242
244
  } | null>;
243
245
  blocks: import("vue").ComputedRef<{
@@ -1,5 +1,12 @@
1
1
  import type { DynamicContent } from '@@/Types/generic';
2
2
  import type { FiltersResponse } from '@@/Types/recommendation';
3
+ /**
4
+ * Extracts recommendation block IDs directly from the HTML by parsing
5
+ * the recommendation-id attribute on block elements.
6
+ * This avoids depending on store state which may not be populated
7
+ * when the settings panel hasn't been opened yet (e.g., saved templates).
8
+ */
9
+ export declare function getRecommendationBlockIds(html: string): string[];
3
10
  /**
4
11
  * Parses a `{{recoId_index_attributeName}}` variable (already stripped of the
5
12
  * braces/whitespace) into its recommendation id and attribute name. Returns
@@ -27,6 +34,20 @@ export declare function buildPartnerAttributeNameSet(filterList: FiltersResponse
27
34
  * before, so flagging it would false-positive on merge tags like `{{user.name}}`.
28
35
  */
29
36
  export declare function findInvalidCustomFields(html: string, validCustomFields: string[], recommendationBlockIds: string[], ignoredIds: Set<string>, ignoredAttributeNames: Set<string> | null): string[];
37
+ /**
38
+ * Returns the invalid `{{ ... }}` dynamic-content tags found in the HTML.
39
+ *
40
+ * A tag's variable name is the part before the first `|` (everything after it is the fallback).
41
+ * Surrounding whitespace is always allowed (trimmed). A tag is valid only when the variable name
42
+ * is an allowed system token, or it is a clean identifier (`[A-Za-z0-9_.]`) and — when a `|` is
43
+ * present — the fallback after it is not empty. Otherwise the tag is invalid (e.g.
44
+ * `{{Api Response: OA1 Main Item Url}}` — spaces/`:` — or `{{bounce|}}` — empty fallback).
45
+ *
46
+ * `data-*` block-config attribute values are excluded from the scan: blocks such as Items/cart
47
+ * store product merge tags (`data-product_button_link="{{Abandoned Cart Item (1) Url}}"`) that the
48
+ * backend resolves through its own pipeline, so they must not be treated as user-authored tags.
49
+ */
50
+ export declare function getInvalidDynamicContentTags(html: string): string[];
30
51
  export declare const useHtmlValidator: () => {
31
52
  validateHtml: (html: string, customFields: DynamicContent[], isOnSaveValidation?: boolean) => Promise<boolean>;
32
53
  };
@@ -1,4 +1,9 @@
1
1
  import type { SavedTemplateDetails } from '@@/Types/stripo';
2
+ type SaveDetails = Omit<SavedTemplateDetails, 'metadata' | 'silent'>;
3
+ type SaveResult = SaveDetails | undefined;
2
4
  export declare const useSave: () => {
3
- save: (suppressEmit?: boolean, silent?: boolean) => Promise<Omit<SavedTemplateDetails, "metadata" | "silent"> | undefined>;
5
+ save: (suppressEmit?: boolean, silent?: boolean) => Promise<SaveResult>;
6
+ saveWithErrors: () => Promise<SaveResult>;
7
+ saveRawOnly: () => Promise<SaveResult>;
4
8
  };
9
+ export {};
@@ -0,0 +1,6 @@
1
+ import type { DynamicContent } from '@@/Types/generic';
2
+ import type { SaveValidationError } from '@@/Types/save-validation';
3
+ export declare const useSaveValidation: () => {
4
+ collectSaveValidationErrors: (compiledHtml: string, dynamicContentList: DynamicContent[]) => Promise<SaveValidationError[]>;
5
+ validateCurrentTemplate: () => Promise<SaveValidationError[]>;
6
+ };
@@ -0,0 +1,3 @@
1
+ export declare const useSaveValidationRollout: () => {
2
+ isEnabled: import("vue").ComputedRef<boolean>;
3
+ };
@@ -1,3 +1,6 @@
1
+ export declare const COUPON_TAG_MODIFIED_KEY = "newsletter.coupon-tag-modified";
2
+ /** Pure core: the tag text of every coupon block whose code was modified. */
3
+ export declare function findInvalidCouponBlocks(html: string): string[];
1
4
  export declare const useCouponBlockValidator: () => {
2
5
  validateCouponBlockTags: (html: string) => boolean;
3
6
  };
@@ -1,4 +1,14 @@
1
+ declare const SINGLETON_BLOCK_RULES: {
2
+ pageType: number;
3
+ messageKey: string;
4
+ }[];
5
+ export declare const EMPTY_BLOCK_KEY = "unsubscription-preference.select-page-to-continue";
6
+ /** Pure core: the singleton rules whose page type appears more than once. */
7
+ export declare function findDuplicateUnsubPageTypes(html: string): typeof SINGLETON_BLOCK_RULES;
8
+ /** Pure core: count of unsubscribe blocks with no page type or page list chosen. */
9
+ export declare function countUnsubBlocksWithoutPage(html: string): number;
1
10
  export declare const useUnsubscribeBlockValidator: () => {
2
11
  validateUnsubscribeBlockUniqueness: (html: string) => boolean;
3
12
  validateUnsubscribeBlockHasTemplate: (html: string) => boolean;
4
13
  };
14
+ export {};
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Stable discriminators for each save-validation rule site — one per rule that
3
+ * can produce a drawer row, kept in sync with the collector + legacy validator chains.
4
+ */
5
+ export declare enum SaveValidationRule {
6
+ CouponTagModified = "coupon-tag-modified",
7
+ DuplicateUnsubBlock = "duplicate-unsub-block",
8
+ UnsubBlockMissingTemplate = "unsub-block-missing-template",
9
+ LiquidSyntax = "liquid-syntax",
10
+ InvalidLinks = "invalid-links",
11
+ LinkCheckInProgress = "link-check-in-progress",
12
+ CustomFieldScript = "custom-field-script",
13
+ EmptyHtml = "empty-html",
14
+ MismatchedBraces = "mismatched-braces",
15
+ InvalidDynamicTag = "invalid-dynamic-tag",
16
+ InvalidCustomField = "invalid-custom-field",
17
+ ConditionsSyntax = "conditions-syntax",
18
+ WrongEqualityOperator = "wrong-equality-operator",
19
+ MissingIfEndif = "missing-if-endif",
20
+ ConditionTagSpacing = "condition-tag-spacing",
21
+ BracesInsideIfTag = "braces-inside-if-tag",
22
+ RecommendationRequiredFields = "recommendation-required-fields",
23
+ RecommendationFilters = "recommendation-filters",
24
+ InvalidImageType = "invalid-image-type",
25
+ CheckboxUnsubGroup = "checkbox-unsub-group",
26
+ RadioButtonUnsubGroup = "radio-button-unsub-group"
27
+ }
@@ -612,6 +612,11 @@ export declare const useRecommendationExtensionStore: import("pinia").StoreDefin
612
612
  * every block's recommendationConfigs across user edits.
613
613
  */
614
614
  hasInvalidBlock(): boolean;
615
+ /**
616
+ * Per-instance counterpart to `hasInvalidBlock`: the ids of every block
617
+ * that fails required-field validation, one drawer row each (SD-147430).
618
+ */
619
+ invalidBlockIds(): string[];
615
620
  /**
616
621
  * Which required fields are missing across the design's blocks, so the save
617
622
  * toaster can name the cause instead of always showing the generic message.