@useinsider/guido 3.18.2-beta.8ef3138 → 3.18.2-beta.eedafd2

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 (50) hide show
  1. package/README.md +1 -0
  2. package/dist/@types/config/schemas.js +28 -26
  3. package/dist/components/Guido.vue.js +8 -8
  4. package/dist/components/Guido.vue2.js +53 -51
  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 +86 -56
  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/useHtmlCompiler.js +37 -26
  15. package/dist/composables/useHtmlValidator.js +2 -0
  16. package/dist/composables/useSave.js +90 -45
  17. package/dist/composables/useSaveValidation.js +279 -0
  18. package/dist/composables/useSaveValidationRollout.js +9 -0
  19. package/dist/composables/validators/useCouponBlockValidator.js +24 -21
  20. package/dist/composables/validators/useUnsubscribeBlockValidator.js +34 -27
  21. package/dist/enums/save-validation.js +4 -0
  22. package/dist/extensions/Blocks/Recommendation/store/recommendation.js +43 -36
  23. package/dist/guido.css +1 -1
  24. package/dist/src/@types/config/schemas.d.ts +4 -0
  25. package/dist/src/@types/save-validation.d.ts +19 -0
  26. package/dist/src/components/Guido.vue.d.ts +17 -1
  27. package/dist/src/components/organisms/header/EditorActions.vue.d.ts +17 -1
  28. package/dist/src/components/organisms/header/HeaderWrapper.vue.d.ts +17 -1
  29. package/dist/src/components/organisms/header/RightSlot.vue.d.ts +17 -1
  30. package/dist/src/components/organisms/validation/ValidationErrorDrawer.vue.d.ts +2 -0
  31. package/dist/src/components/wrappers/WpDrawer.vue.d.ts +4 -3
  32. package/dist/src/composables/useConfig.d.ts +2 -0
  33. package/dist/src/composables/useHtmlValidator.d.ts +21 -0
  34. package/dist/src/composables/useSave.d.ts +6 -1
  35. package/dist/src/composables/useSaveValidation.d.ts +6 -0
  36. package/dist/src/composables/useSaveValidationRollout.d.ts +3 -0
  37. package/dist/src/composables/validators/useCouponBlockValidator.d.ts +3 -0
  38. package/dist/src/composables/validators/useUnsubscribeBlockValidator.d.ts +10 -0
  39. package/dist/src/enums/save-validation.d.ts +27 -0
  40. package/dist/src/extensions/Blocks/Recommendation/store/recommendation.d.ts +5 -0
  41. package/dist/src/stores/config.d.ts +30 -0
  42. package/dist/src/stores/save-validation.d.ts +22 -0
  43. package/dist/src/utils/liquidDynamicContent.d.ts +20 -0
  44. package/dist/src/utils/templatePreparation.d.ts +30 -0
  45. package/dist/stores/config.js +39 -22
  46. package/dist/stores/save-validation.js +30 -0
  47. package/dist/utils/genericUtil.js +64 -61
  48. package/dist/utils/liquidDynamicContent.js +15 -0
  49. package/dist/utils/templatePreparation.js +28 -25
  50. package/package.json +1 -1
@@ -395,6 +395,8 @@ export declare const FeaturesSchema: v.ObjectSchema<{
395
395
  readonly allowlistEnabled: v.OptionalSchema<v.BooleanSchema<undefined>, false>;
396
396
  /** Enable persistent link tracking-id assignment for the click heatmap (opt-in) */
397
397
  readonly linkTrackingIds: v.OptionalSchema<v.BooleanSchema<undefined>, false>;
398
+ /** Allow persisting a template that still has validation errors (drawer "Save and Continue" soft-save). Host sets false for transactional + unsubscribe. */
399
+ readonly allowSaveWithErrors: v.OptionalSchema<v.BooleanSchema<undefined>, true>;
398
400
  readonly reusableRecommendationStrategy: v.OptionalSchema<v.BooleanSchema<undefined>, false>;
399
401
  }, undefined>;
400
402
  /**
@@ -795,6 +797,8 @@ export declare const GuidoConfigSchema: v.ObjectSchema<{
795
797
  readonly allowlistEnabled: v.OptionalSchema<v.BooleanSchema<undefined>, false>;
796
798
  /** Enable persistent link tracking-id assignment for the click heatmap (opt-in) */
797
799
  readonly linkTrackingIds: v.OptionalSchema<v.BooleanSchema<undefined>, false>;
800
+ /** Allow persisting a template that still has validation errors (drawer "Save and Continue" soft-save). Host sets false for transactional + unsubscribe. */
801
+ readonly allowSaveWithErrors: v.OptionalSchema<v.BooleanSchema<undefined>, true>;
798
802
  readonly reusableRecommendationStrategy: v.OptionalSchema<v.BooleanSchema<undefined>, false>;
799
803
  }, undefined>, {}>;
800
804
  /** 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;
@@ -99,6 +99,7 @@ export declare const useConfig: () => {
99
99
  autosave: boolean;
100
100
  allowlistEnabled: boolean;
101
101
  linkTrackingIds: boolean;
102
+ allowSaveWithErrors: boolean;
102
103
  reusableRecommendationStrategy: boolean;
103
104
  };
104
105
  blocks: {
@@ -234,6 +235,7 @@ export declare const useConfig: () => {
234
235
  autosave: boolean;
235
236
  allowlistEnabled: boolean;
236
237
  linkTrackingIds: boolean;
238
+ allowSaveWithErrors: boolean;
237
239
  reusableRecommendationStrategy: boolean;
238
240
  } | null>;
239
241
  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
+ }
@@ -462,6 +462,11 @@ export declare const useRecommendationExtensionStore: import("pinia").StoreDefin
462
462
  * every block's recommendationConfigs across user edits.
463
463
  */
464
464
  hasInvalidBlock(): boolean;
465
+ /**
466
+ * Per-instance counterpart to `hasInvalidBlock`: the ids of every block
467
+ * that fails required-field validation, one drawer row each (SD-147430).
468
+ */
469
+ invalidBlockIds(): string[];
465
470
  fetchRecommendationProducts(): Promise<void>;
466
471
  _doFetchProducts(): Promise<void>;
467
472
  _doFetchStrategyProducts(blockId: number): Promise<void>;
@@ -104,6 +104,7 @@ export declare const useConfigStore: import("pinia").StoreDefinition<"guido-conf
104
104
  autosave: boolean;
105
105
  allowlistEnabled: boolean;
106
106
  linkTrackingIds: boolean;
107
+ allowSaveWithErrors: boolean;
107
108
  reusableRecommendationStrategy: boolean;
108
109
  };
109
110
  blocks: {
@@ -245,6 +246,7 @@ export declare const useConfigStore: import("pinia").StoreDefinition<"guido-conf
245
246
  autosave: boolean;
246
247
  allowlistEnabled: boolean;
247
248
  linkTrackingIds: boolean;
249
+ allowSaveWithErrors: boolean;
248
250
  reusableRecommendationStrategy: boolean;
249
251
  };
250
252
  blocks: {
@@ -386,6 +388,7 @@ export declare const useConfigStore: import("pinia").StoreDefinition<"guido-conf
386
388
  autosave: boolean;
387
389
  allowlistEnabled: boolean;
388
390
  linkTrackingIds: boolean;
391
+ allowSaveWithErrors: boolean;
389
392
  reusableRecommendationStrategy: boolean;
390
393
  };
391
394
  blocks: {
@@ -527,6 +530,7 @@ export declare const useConfigStore: import("pinia").StoreDefinition<"guido-conf
527
530
  autosave: boolean;
528
531
  allowlistEnabled: boolean;
529
532
  linkTrackingIds: boolean;
533
+ allowSaveWithErrors: boolean;
530
534
  reusableRecommendationStrategy: boolean;
531
535
  };
532
536
  blocks: {
@@ -668,6 +672,7 @@ export declare const useConfigStore: import("pinia").StoreDefinition<"guido-conf
668
672
  autosave: boolean;
669
673
  allowlistEnabled: boolean;
670
674
  linkTrackingIds: boolean;
675
+ allowSaveWithErrors: boolean;
671
676
  reusableRecommendationStrategy: boolean;
672
677
  };
673
678
  blocks: {
@@ -809,6 +814,7 @@ export declare const useConfigStore: import("pinia").StoreDefinition<"guido-conf
809
814
  autosave: boolean;
810
815
  allowlistEnabled: boolean;
811
816
  linkTrackingIds: boolean;
817
+ allowSaveWithErrors: boolean;
812
818
  reusableRecommendationStrategy: boolean;
813
819
  };
814
820
  blocks: {
@@ -950,6 +956,7 @@ export declare const useConfigStore: import("pinia").StoreDefinition<"guido-conf
950
956
  autosave: boolean;
951
957
  allowlistEnabled: boolean;
952
958
  linkTrackingIds: boolean;
959
+ allowSaveWithErrors: boolean;
953
960
  reusableRecommendationStrategy: boolean;
954
961
  };
955
962
  blocks: {
@@ -1091,6 +1098,7 @@ export declare const useConfigStore: import("pinia").StoreDefinition<"guido-conf
1091
1098
  autosave: boolean;
1092
1099
  allowlistEnabled: boolean;
1093
1100
  linkTrackingIds: boolean;
1101
+ allowSaveWithErrors: boolean;
1094
1102
  reusableRecommendationStrategy: boolean;
1095
1103
  };
1096
1104
  blocks: {
@@ -1232,6 +1240,7 @@ export declare const useConfigStore: import("pinia").StoreDefinition<"guido-conf
1232
1240
  autosave: boolean;
1233
1241
  allowlistEnabled: boolean;
1234
1242
  linkTrackingIds: boolean;
1243
+ allowSaveWithErrors: boolean;
1235
1244
  reusableRecommendationStrategy: boolean;
1236
1245
  };
1237
1246
  blocks: {
@@ -1373,6 +1382,7 @@ export declare const useConfigStore: import("pinia").StoreDefinition<"guido-conf
1373
1382
  autosave: boolean;
1374
1383
  allowlistEnabled: boolean;
1375
1384
  linkTrackingIds: boolean;
1385
+ allowSaveWithErrors: boolean;
1376
1386
  reusableRecommendationStrategy: boolean;
1377
1387
  };
1378
1388
  blocks: {
@@ -1514,6 +1524,7 @@ export declare const useConfigStore: import("pinia").StoreDefinition<"guido-conf
1514
1524
  autosave: boolean;
1515
1525
  allowlistEnabled: boolean;
1516
1526
  linkTrackingIds: boolean;
1527
+ allowSaveWithErrors: boolean;
1517
1528
  reusableRecommendationStrategy: boolean;
1518
1529
  };
1519
1530
  blocks: {
@@ -1655,6 +1666,7 @@ export declare const useConfigStore: import("pinia").StoreDefinition<"guido-conf
1655
1666
  autosave: boolean;
1656
1667
  allowlistEnabled: boolean;
1657
1668
  linkTrackingIds: boolean;
1669
+ allowSaveWithErrors: boolean;
1658
1670
  reusableRecommendationStrategy: boolean;
1659
1671
  };
1660
1672
  blocks: {
@@ -1796,6 +1808,7 @@ export declare const useConfigStore: import("pinia").StoreDefinition<"guido-conf
1796
1808
  autosave: boolean;
1797
1809
  allowlistEnabled: boolean;
1798
1810
  linkTrackingIds: boolean;
1811
+ allowSaveWithErrors: boolean;
1799
1812
  reusableRecommendationStrategy: boolean;
1800
1813
  };
1801
1814
  blocks: {
@@ -1937,6 +1950,7 @@ export declare const useConfigStore: import("pinia").StoreDefinition<"guido-conf
1937
1950
  autosave: boolean;
1938
1951
  allowlistEnabled: boolean;
1939
1952
  linkTrackingIds: boolean;
1953
+ allowSaveWithErrors: boolean;
1940
1954
  reusableRecommendationStrategy: boolean;
1941
1955
  };
1942
1956
  blocks: {
@@ -2078,6 +2092,7 @@ export declare const useConfigStore: import("pinia").StoreDefinition<"guido-conf
2078
2092
  autosave: boolean;
2079
2093
  allowlistEnabled: boolean;
2080
2094
  linkTrackingIds: boolean;
2095
+ allowSaveWithErrors: boolean;
2081
2096
  reusableRecommendationStrategy: boolean;
2082
2097
  };
2083
2098
  blocks: {
@@ -2121,6 +2136,18 @@ export declare const useConfigStore: import("pinia").StoreDefinition<"guido-conf
2121
2136
  };
2122
2137
  } | null;
2123
2138
  } & import("pinia").PiniaCustomStateProperties<ConfigStoreState>) => number;
2139
+ /**
2140
+ * Transactional campaigns keep the hard save block. Reactive getter, so
2141
+ * consumers stay safe in setup where a raw config read snapshots null (SD-147945).
2142
+ */
2143
+ isTransactional(): boolean;
2144
+ /**
2145
+ * Soft-save policy: may the editor persist a template with validation errors?
2146
+ * Default true (host sets features.allowSaveWithErrors=false for transactional +
2147
+ * unsub). Reactive getter defaulting true — reading via isFeatureEnabled would
2148
+ * floor to false pre-init and momentarily hide the soft-save option (SD-147430).
2149
+ */
2150
+ allowSaveWithErrors(): boolean;
2124
2151
  /**
2125
2152
  * Get the username
2126
2153
  */
@@ -2219,6 +2246,7 @@ export declare const useConfigStore: import("pinia").StoreDefinition<"guido-conf
2219
2246
  autosave: boolean;
2220
2247
  allowlistEnabled: boolean;
2221
2248
  linkTrackingIds: boolean;
2249
+ allowSaveWithErrors: boolean;
2222
2250
  reusableRecommendationStrategy: boolean;
2223
2251
  };
2224
2252
  blocks: {
@@ -2360,6 +2388,7 @@ export declare const useConfigStore: import("pinia").StoreDefinition<"guido-conf
2360
2388
  autosave: boolean;
2361
2389
  allowlistEnabled: boolean;
2362
2390
  linkTrackingIds: boolean;
2391
+ allowSaveWithErrors: boolean;
2363
2392
  reusableRecommendationStrategy: boolean;
2364
2393
  };
2365
2394
  blocks: {
@@ -2503,6 +2532,7 @@ export declare const useConfigStore: import("pinia").StoreDefinition<"guido-conf
2503
2532
  autosave: boolean;
2504
2533
  allowlistEnabled: boolean;
2505
2534
  linkTrackingIds: boolean;
2535
+ allowSaveWithErrors: boolean;
2506
2536
  reusableRecommendationStrategy: boolean;
2507
2537
  };
2508
2538
  blocks: {
@@ -0,0 +1,22 @@
1
+ import type { SaveValidationError, SaveValidationIntent } from '@@/Types/save-validation';
2
+ interface SaveValidationState {
3
+ errors: SaveValidationError[];
4
+ isDrawerOpen: boolean;
5
+ intent: SaveValidationIntent | null;
6
+ }
7
+ export declare const useSaveValidationStore: import("pinia").StoreDefinition<"guidoSaveValidation", SaveValidationState, {
8
+ errorCount: (state: {
9
+ errors: {
10
+ rule: import("../enums/save-validation").SaveValidationRule;
11
+ message: string;
12
+ detail?: string | undefined;
13
+ type: import("../enums/toaster").ToasterTypeOptions;
14
+ }[];
15
+ isDrawerOpen: boolean;
16
+ intent: SaveValidationIntent | null;
17
+ } & import("pinia").PiniaCustomStateProperties<SaveValidationState>) => number;
18
+ }, {
19
+ open(errors: SaveValidationError[], intent: SaveValidationIntent): void;
20
+ close(): void;
21
+ }>;
22
+ export {};
@@ -0,0 +1,20 @@
1
+ import type { BaseDynamicContent } from '@@/Types/generic';
2
+ export type DynamicContentFormat = NonNullable<BaseDynamicContent['format']>;
3
+ /** Format key the dynamic-content picker sends for a custom date/time format. */
4
+ export declare const TIME_FORMAT_KEY = "time-format";
5
+ /** Wraps a Liquid filter argument in whichever quote style the value itself does not use. */
6
+ export declare const quoteArgument: (value: string) => string | null;
7
+ /**
8
+ * `{ key: 'time-format', value: '%Y-%m-%d' }` -> `date: "%Y-%m-%d"`.
9
+ *
10
+ * Only a custom date/time strftime pattern has a Liquid equivalent. Anything else returns null so
11
+ * the caller emits a plain tag rather than one that silently lost — or literally printed — its
12
+ * format.
13
+ * @example formatToLiquidFilter({ key: 'time-format', value: '%b %-d, %Y' }) -> date: "%b %-d, %Y"
14
+ */
15
+ export declare const formatToLiquidFilter: (format: DynamicContentFormat) => string | null;
16
+ /**
17
+ * Inverse of `formatToLiquidFilter` — reads the picker format back out of one filter segment, so a
18
+ * reopened tag restores its selection in the host's format dropdown.
19
+ */
20
+ export declare const liquidFilterToFormat: (segment: string) => DynamicContentFormat | null;
@@ -15,6 +15,36 @@ import type { SavedTemplateDetails } from '@@/Types/stripo';
15
15
  * same source), so it is reliably present regardless of how the block was added.
16
16
  */
17
17
  export declare function getHtmlReferencedUnsubscribePages(html: string): number[];
18
+ /**
19
+ * Hydrates the recommendation extension store from the persisted `esd-ext-config`
20
+ * JSON blob on each `.recommendation-block-v2` element in the raw editor HTML.
21
+ *
22
+ * Why this runs before `compileHtml`:
23
+ * The HTML compiler's `prepareRecommendationBlocks` rule asks
24
+ * `useRecommendation.buildCampaignUrl(id)` to assemble each block's API URL
25
+ * from `blockStates[id].recommendationConfigs`. That Pinia entry is normally
26
+ * populated by `RecommendationBlockControl._syncNodeConfigToStore`, which
27
+ * only fires when the user *selects* the block. Saving without ever
28
+ * selecting a migrated block would otherwise build the URL from empty/
29
+ * default state and the campaign URL would not be saved.
30
+ *
31
+ * Why we read raw HTML, not compiled HTML:
32
+ * Stripo's `compileEmail()` strips editor-only attributes (`esd-*`) from the
33
+ * compiled output, so `esd-ext-config` is gone by the time the compiler
34
+ * pipeline sees the HTML. The raw HTML from `getTemplateData()` preserves
35
+ * the attribute as the migrator wrote it.
36
+ *
37
+ * Reference data pre-load:
38
+ * When the template has recommendation blocks, this also pre-loads the
39
+ * shared reference data (languages / currency list) so the save-time
40
+ * required-fields validator can check each block's locale and currency
41
+ * against the partner's real options. Best-effort — on a fetch failure the
42
+ * validator degrades to its empty-list guard (`requiredFields.ts`) instead
43
+ * of blocking the save or stranding the Save button.
44
+ */
45
+ declare function hydrateRecommendationStoreFromRawHtml(rawHtml: string): Promise<void>;
18
46
  export declare const useTemplatePreparation: () => {
19
47
  prepareTemplateDetails: () => Promise<Omit<SavedTemplateDetails, "metadata">>;
48
+ hydrateRecommendationStore: typeof hydrateRecommendationStoreFromRawHtml;
20
49
  };
50
+ export {};