@useinsider/guido 3.4.1-beta.1fc7f53 → 3.4.1-beta.3b1ce15

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.
package/README.md CHANGED
@@ -8,6 +8,8 @@
8
8
 
9
9
  # @useinsider/guido
10
10
 
11
+ [![Coverus](https://github.com/useinsider/coverus-artifacts/raw/main/badges/github/useinsider/guido/coverage.svg)](http://coverus.internal.dataforce/app/project/github/useinsider/guido)
12
+
11
13
  Guido is a Vue 2 + TypeScript wrapper for the Stripo Email Editor plugin. Easily embed the professional email editor in your Vue applications with a clean, type-safe configuration.
12
14
 
13
15
  ## Installation
@@ -21,9 +21,9 @@ const v = () => {
21
21
  resetDataSavedFlag: !1,
22
22
  disableLineHeightsReplace: !0
23
23
  }, ...t }, s = {
24
- callback: (l, p, d, c, u) => {
24
+ callback: (l, r, d, c, u) => {
25
25
  l ? a(l) : e({
26
- html: p,
26
+ html: r,
27
27
  ampHtml: d,
28
28
  ampErrors: c,
29
29
  displayConditions: u
@@ -36,11 +36,11 @@ const v = () => {
36
36
  return {
37
37
  getCompiledEmail: m,
38
38
  getTemplateData: () => new Promise((t) => {
39
- const e = ({ html: a, css: i, width: o, height: r, utmParams: s, syncModulesIds: l }) => t({
39
+ const e = ({ html: a, css: i, width: o, height: p, utmParams: s, syncModulesIds: l }) => t({
40
40
  html: a,
41
41
  css: i,
42
42
  width: o,
43
- height: r,
43
+ height: p,
44
44
  utmParams: s,
45
45
  syncModulesIds: l
46
46
  });
@@ -91,11 +91,7 @@ const v = () => {
91
91
  },
92
92
  editorSave: () => new Promise((t) => {
93
93
  window.StripoEditorApi.actionsApi.save((e) => {
94
- if (e) {
95
- n(e, "Failed to save template"), t(!1);
96
- return;
97
- }
98
- t(!0);
94
+ e && n(e, "Failed to save template"), t();
99
95
  });
100
96
  })
101
97
  };
@@ -1,31 +1,32 @@
1
1
  import { useActionsApi as x } from "./useActionsApi.js";
2
2
  import { useConfig as y } from "./useConfig.js";
3
3
  import { useSaveStart as w, useSaveComplete as C } from "./useGuidoActions.js";
4
- import { useSyncModuleExtractor as E } from "./useSyncModuleExtractor.js";
5
- import { useStripoApi as H } from "../services/stripoApi.js";
6
- import { useTemplatePreparation as b } from "../utils/templatePreparation.js";
7
- import { useHtmlValidator as q } from "./useHtmlValidator.js";
8
- import { useCouponBlockValidator as L } from "./validators/useCouponBlockValidator.js";
9
- import { useLiquidValidator as P } from "./validators/useLiquidValidator.js";
10
- const z = () => {
11
- const o = w(), s = C(), { validateHtml: r } = q(), { validateLiquidSyntax: n } = P(), { validateCouponBlockTags: l } = L(), { callbacks: a, isFeatureEnabled: d } = y(), { extractSyncModuleData: u } = E(), { setSyncModuleUnsubscriptionPages: c } = H(), { editorSave: m } = x();
4
+ import { useSyncModuleExtractor as H } from "./useSyncModuleExtractor.js";
5
+ import { useStripoApi as b } from "../services/stripoApi.js";
6
+ import { useTemplatePreparation as q } from "../utils/templatePreparation.js";
7
+ import { useHtmlValidator as L } from "./useHtmlValidator.js";
8
+ import { useCouponBlockValidator as P } from "./validators/useCouponBlockValidator.js";
9
+ import { useLiquidValidator as E } from "./validators/useLiquidValidator.js";
10
+ const j = () => {
11
+ const o = w(), s = C(), { validateHtml: r } = L(), { validateLiquidSyntax: l } = E(), { validateCouponBlockTags: n } = P(), { callbacks: a, isFeatureEnabled: d } = y(), { extractSyncModuleData: u } = H(), { setSyncModuleUnsubscriptionPages: c } = b(), { editorSave: m } = x();
12
12
  return { save: async (p = !1, f = !1) => {
13
13
  var i;
14
14
  o();
15
- const { prepareTemplateDetails: v } = b(), t = await v();
16
- if (!l(t.compiledHtml))
15
+ const { prepareTemplateDetails: v } = q(), t = await v();
16
+ if (!n(t.compiledHtml))
17
17
  return;
18
18
  if (d("liquidSyntax")) {
19
- if (!await n(t.compiledHtml))
19
+ if (!await l(t.compiledHtml))
20
20
  return;
21
21
  } else if (!await r(t.compiledHtml, t.dynamicContentList, !0))
22
22
  return;
23
- if ((i = a.value) != null && i.externalValidation && !await a.value.externalValidation(t) || !await m())
23
+ if ((i = a.value) != null && i.externalValidation && !await a.value.externalValidation(t))
24
24
  return;
25
+ await m();
25
26
  const { unsubscribePayload: S, stripoModules: V } = u(t.rawHtml);
26
27
  return await c(S), t.modules = V, p || s({ ...t, silent: f }), t;
27
28
  } };
28
29
  };
29
30
  export {
30
- z as useSave
31
+ j as useSave
31
32
  };
@@ -155,6 +155,8 @@ class z {
155
155
  }
156
156
  migrate(t) {
157
157
  try {
158
+ if (!this.containsItemsBlock(t))
159
+ return t;
158
160
  let o = this.removeJinjaConditionals(t);
159
161
  o = this.replaceTemplateVariables(o);
160
162
  const l = this.parser.parseFromString(o, "text/html"), a = l.querySelectorAll(
@@ -178,15 +180,15 @@ class z {
178
180
  originalPriceStyles: e.originalPriceStyles,
179
181
  quantityStyles: e.quantityStyles,
180
182
  nodeConfig: R(e.configBlockAttributes)
181
- }), d = this.parser.parseFromString(
183
+ }), u = this.parser.parseFromString(
182
184
  `<table><tbody><tr>${c}</tr></tbody></table>`,
183
185
  "text/html"
184
186
  ).querySelector("td");
185
- if (d && i.parentNode) {
187
+ if (u && i.parentNode) {
186
188
  const p = R(e.configBlockAttributes);
187
- d.setAttribute("esd-ext-config", JSON.stringify(p));
188
- const u = d.querySelector("esd-config-block");
189
- u && u.remove(), i.parentNode.replaceChild(d, i);
189
+ u.setAttribute("esd-ext-config", JSON.stringify(p));
190
+ const d = u.querySelector("esd-config-block");
191
+ d && d.remove(), i.parentNode.replaceChild(u, i);
190
192
  }
191
193
  }), l.documentElement.outerHTML);
192
194
  } catch (o) {
@@ -201,22 +203,22 @@ class z {
201
203
  */
202
204
  extractConfiguration(t) {
203
205
  var C, D, P;
204
- const o = ((C = t.querySelector("[data-type]")) == null ? void 0 : C.getAttribute("data-type")) || "CART_ITEMS", l = ((D = t.querySelector("[data-number]")) == null ? void 0 : D.getAttribute("data-number")) || "1", a = parseInt(l) - 1, n = T[o], i = ((P = n == null ? void 0 : n[a]) == null ? void 0 : P.value) || n[0].value, e = t.querySelector('[product-attr="price"]'), c = (e == null ? void 0 : e.getAttribute("data-currency_symbol")) || "USD", d = ((e == null ? void 0 : e.getAttribute("data-curency")) || "before") === "after" ? "1" : "0", p = (e == null ? void 0 : e.getAttribute("data-formated")) !== "false", u = this.extractConfigBlockAttributes(t, o, l), S = u["data-card_orientation_control_value"];
206
+ const o = ((C = t.querySelector("[data-type]")) == null ? void 0 : C.getAttribute("data-type")) || "CART_ITEMS", l = ((D = t.querySelector("[data-number]")) == null ? void 0 : D.getAttribute("data-number")) || "1", a = parseInt(l) - 1, n = T[o], i = ((P = n == null ? void 0 : n[a]) == null ? void 0 : P.value) || n[0].value, e = t.querySelector('[product-attr="price"]'), c = (e == null ? void 0 : e.getAttribute("data-currency_symbol")) || "USD", u = ((e == null ? void 0 : e.getAttribute("data-curency")) || "before") === "after" ? "1" : "0", p = (e == null ? void 0 : e.getAttribute("data-formated")) !== "false", d = this.extractConfigBlockAttributes(t, o, l), S = d["data-card_orientation_control_value"];
205
207
  let b;
206
208
  S ? b = S === "horizontal" ? "horizontal" : "vertical" : b = t.querySelector('[colspan="2"]') !== null ? "vertical" : "horizontal";
207
- const y = t.querySelector('a[product-attr="name"]'), I = (y == null ? void 0 : y.getAttribute("style")) || void 0, f = t.querySelector('a[product-attr="button"]'), m = (f == null ? void 0 : f.getAttribute("style")) || void 0, h = (e == null ? void 0 : e.getAttribute("style")) || void 0, A = t.querySelector("p.original-price"), k = (A == null ? void 0 : A.getAttribute("style")) || void 0, E = t.querySelector('[product-attr="quantity"]'), L = (E == null ? void 0 : E.getAttribute("style")) || void 0;
209
+ const y = t.querySelector('a[product-attr="name"]'), I = (y == null ? void 0 : y.getAttribute("style")) || void 0, f = t.querySelector('a[product-attr="button"]'), m = (f == null ? void 0 : f.getAttribute("style")) || void 0, k = (e == null ? void 0 : e.getAttribute("style")) || void 0, A = t.querySelector("p.original-price"), h = (A == null ? void 0 : A.getAttribute("style")) || void 0, E = t.querySelector('[product-attr="quantity"]'), L = (E == null ? void 0 : E.getAttribute("style")) || void 0;
208
210
  return {
209
211
  orientation: b,
210
212
  itemsType: o,
211
213
  itemId: i,
212
214
  currencySymbol: c,
213
- currencyLocation: d,
215
+ currencyLocation: u,
214
216
  formattedPrice: p,
215
- configBlockAttributes: u,
217
+ configBlockAttributes: d,
216
218
  nameStyles: I,
217
219
  buttonStyles: m,
218
- priceStyles: h,
219
- originalPriceStyles: k,
220
+ priceStyles: k,
221
+ originalPriceStyles: h,
220
222
  quantityStyles: L
221
223
  };
222
224
  }
@@ -296,6 +298,14 @@ class z {
296
298
  "data-product_button_link": "{{Abandoned Cart Item (1) Url}}"
297
299
  };
298
300
  }
301
+ /**
302
+ * Fast check for the presence of any items block (cart / browsed / purchased)
303
+ * in the raw HTML. Used to gate the migration pipeline so non-items templates
304
+ * are returned untouched.
305
+ */
306
+ containsItemsBlock(t) {
307
+ return t.includes("esd-cart-items-block") || t.includes("esd-browsed-items-block") || t.includes("esd-purchased-items-block");
308
+ }
299
309
  /**
300
310
  * Removes Jinja2 conditional statements from HTML
301
311
  * Handles all items block types:
@@ -337,18 +347,18 @@ class z {
337
347
  const [, i, e, c] = n, _ = x[i];
338
348
  if (!_)
339
349
  return console.warn(`Unknown variable prefix: ${i}`), l;
340
- const d = q[e];
341
- if (!d)
350
+ const u = q[e];
351
+ if (!u)
342
352
  return console.warn(`Unknown variable suffix mapping for: ${e}`), l;
343
- const { pairsKey: p, defaultKey: u, isArray: S } = d, y = o[p][_];
353
+ const { pairsKey: p, defaultKey: d, isArray: S } = u, y = o[p][_];
344
354
  if (!y)
345
355
  return console.warn(`No data found for: ${p}.${_}`), l;
346
356
  if (S) {
347
- const f = parseInt(c) - 1, m = y[u];
348
- return Array.isArray(m) && m[f] ? m[f] : (console.warn(`Array value not found: ${p}.${_}.${u}[${f}]`), l);
357
+ const f = parseInt(c) - 1, m = y[d];
358
+ return Array.isArray(m) && m[f] ? m[f] : (console.warn(`Array value not found: ${p}.${_}.${d}[${f}]`), l);
349
359
  }
350
- const I = y[u];
351
- return I !== void 0 ? String(I) : (console.warn(`Default value not found: ${p}.${_}.${u}`), l);
360
+ const I = y[d];
361
+ return I !== void 0 ? String(I) : (console.warn(`Default value not found: ${p}.${_}.${d}`), l);
352
362
  });
353
363
  }
354
364
  }
@@ -1,31 +1,34 @@
1
1
  var d = Object.defineProperty;
2
- var h = (n, i, e) => i in n ? d(n, i, { enumerable: !0, configurable: !0, writable: !0, value: e }) : n[i] = e;
3
- var u = (n, i, e) => h(n, typeof i != "symbol" ? i + "" : i, e);
4
- import { PAGE_TYPES as E } from "../../../enums/unsubscribe.js";
5
- import { useUnsubscribeStore as c } from "../../../stores/unsubscribe.js";
6
- import { Block as _, BlockCompositionType as S, BlockType as L, ModificationDescription as b } from "../../../node_modules/@stripoinc/ui-editor-extensions/dist/esm/index.js";
7
- import { getDefaultTemplate as v } from "./template.js";
8
- import { UNSUBSCRIBE_EVENTS as a, DATA_ATTRIBUTES as o } from "./utils/constants.js";
9
- import { parsePageList as p } from "./utils/utils.js";
10
- const g = "unsubscribe-block", T = 'a[data-unsubscribe-link="true"]', f = ".unsubscribe-block-v2", B = "{{ins-unsubscribe-link}}", C = {
11
- [E.GLOBAL_UNSUBSCRIBE]: "{{ins-global-unsubscribe-link}}",
12
- [E.SUBSCRIPTION_PREFERENCE_CENTER]: "{{ins-preferences-unsubscribe-link}}"
2
+ var _ = (n, i, e) => i in n ? d(n, i, { enumerable: !0, configurable: !0, writable: !0, value: e }) : n[i] = e;
3
+ var c = (n, i, e) => _(n, typeof i != "symbol" ? i + "" : i, e);
4
+ import { useToaster as L } from "../../../composables/useToaster.js";
5
+ import { ToasterTypeOptions as S } from "../../../enums/toaster.js";
6
+ import { PAGE_TYPES as b } from "../../../enums/unsubscribe.js";
7
+ import { useUnsubscribeStore as a } from "../../../stores/unsubscribe.js";
8
+ import { Block as g, BlockCompositionType as v, BlockType as k, ModificationDescription as h } from "../../../node_modules/@stripoinc/ui-editor-extensions/dist/esm/index.js";
9
+ import { getDefaultTemplate as f } from "./template.js";
10
+ import { UNSUBSCRIBE_EVENTS as u, DATA_ATTRIBUTES as o } from "./utils/constants.js";
11
+ import { parsePageList as E } from "./utils/utils.js";
12
+ const T = "unsubscribe-block", p = 'a[data-unsubscribe-link="true"]', B = ".unsubscribe-block-v2", U = "{{ins-unsubscribe-link}}", C = "https://academy.insiderone.com/docs/adding-unsubscribe-pages-into-emails", A = {
13
+ [b.GLOBAL_UNSUBSCRIBE]: "{{ins-global-unsubscribe-link}}",
14
+ [b.SUBSCRIPTION_PREFERENCE_CENTER]: "{{ins-preferences-unsubscribe-link}}"
13
15
  };
14
- class I extends _ {
16
+ class O extends g {
15
17
  constructor() {
16
18
  super();
17
- u(this, "selectEventListener", null);
18
- u(this, "cancelEventListener", null);
19
- u(this, "currentNode");
19
+ c(this, "selectEventListener", null);
20
+ c(this, "cancelEventListener", null);
21
+ c(this, "currentNode");
22
+ c(this, "hadUnsubscribeLink", !1);
20
23
  }
21
24
  getId() {
22
- return g;
25
+ return T;
23
26
  }
24
27
  getIcon() {
25
28
  return "unsubscribe-icon";
26
29
  }
27
30
  getBlockCompositionType() {
28
- return S.BLOCK;
31
+ return v.BLOCK;
29
32
  }
30
33
  getName() {
31
34
  return this.api.translate("Unsubscribe Block");
@@ -34,39 +37,66 @@ class I extends _ {
34
37
  return this.api.translate("Unsubscribe Block Description");
35
38
  }
36
39
  getTemplate() {
37
- return v();
40
+ return f();
38
41
  }
39
42
  onSelect(e) {
40
- this.currentNode = e, !("getAttribute" in e && e.getAttribute("data-migration")) && (this._resetStoreState(), this._loadBlockState(e), this._setupSelectEventListener(), this._setupCancelEventListener(), this._checkExistingBlocks(), this._openDrawer());
43
+ this.currentNode = e, this.hadUnsubscribeLink = this._hasUnsubscribeLink(e), !("getAttribute" in e && e.getAttribute("data-migration")) && (this._resetStoreState(), this._loadBlockState(e), this._setupSelectEventListener(), this._setupCancelEventListener(), this._checkExistingBlocks(), this._openDrawer());
44
+ }
45
+ onDocumentChanged(e) {
46
+ const t = this._hasUnsubscribeLink(e);
47
+ this.hadUnsubscribeLink && !t && this._warnLinkRemoved(), this.hadUnsubscribeLink = t;
41
48
  }
42
49
  onDelete(e) {
43
- this._removeEventListeners(), this._removeBlockTemplatesFromStore(e), this._resetStoreState();
50
+ this._removeEventListeners(), this._removeBlockTemplatesFromStore(e), this._resetStoreState(), this.hadUnsubscribeLink = !1;
44
51
  }
45
52
  onDestroy() {
46
- this._removeEventListeners(), this.currentNode = void 0;
53
+ this._removeEventListeners(), this.currentNode = void 0, this.hadUnsubscribeLink = !1;
54
+ }
55
+ _hasUnsubscribeLink(e) {
56
+ return "querySelector" in e ? !!e.querySelector(p) : !1;
57
+ }
58
+ _warnLinkRemoved() {
59
+ try {
60
+ const { showToaster: e } = L();
61
+ e({
62
+ type: S.Warning,
63
+ message: this.api.translate(
64
+ `Removing the unsubscribe text leaves an empty block, but an active unsubscribe link is required.
65
+ Undo your last action to restore the text, or delete the unsubscribe block entirely.`
66
+ ),
67
+ actionButton: {
68
+ text: this.api.translate("Visit Academy"),
69
+ onClick: () => {
70
+ window.open(C, "_blank", "noopener,noreferrer");
71
+ }
72
+ }
73
+ });
74
+ } catch (e) {
75
+ console.warn("[UnsubscribeBlock] Failed to show warning toaster:", e);
76
+ }
47
77
  }
48
78
  _setupSelectEventListener() {
49
79
  this._removeSelectEventListener(), this.selectEventListener = (e) => {
50
- const r = e, { collectionType: s, selectedPages: t } = r.detail;
51
- this._updateBlock(s, t.join(","));
52
- }, document.addEventListener(a.SELECT, this.selectEventListener);
80
+ const t = e, { collectionType: r, selectedPages: s } = t.detail;
81
+ this._updateBlock(r, s.join(","));
82
+ }, document.addEventListener(u.SELECT, this.selectEventListener);
53
83
  }
54
84
  _removeSelectEventListener() {
55
- this.selectEventListener && (document.removeEventListener(a.SELECT, this.selectEventListener), this.selectEventListener = null);
85
+ this.selectEventListener && (document.removeEventListener(u.SELECT, this.selectEventListener), this.selectEventListener = null);
56
86
  }
57
87
  _setupCancelEventListener() {
58
88
  this._removeCancelEventListener(), this.cancelEventListener = () => {
59
89
  this._handleCancel();
60
- }, document.addEventListener(a.CANCEL, this.cancelEventListener);
90
+ }, document.addEventListener(u.CANCEL, this.cancelEventListener);
61
91
  }
62
92
  _removeCancelEventListener() {
63
- this.cancelEventListener && (document.removeEventListener(a.CANCEL, this.cancelEventListener), this.cancelEventListener = null);
93
+ this.cancelEventListener && (document.removeEventListener(u.CANCEL, this.cancelEventListener), this.cancelEventListener = null);
64
94
  }
65
95
  _handleCancel() {
66
96
  try {
67
97
  if (!this.currentNode)
68
98
  return;
69
- this.api.getDocumentModifier().modifyHtml(this.currentNode).replaceWith(`<${L.EMPTY_CONTAINER}/>`).apply(new b("Removed unsubscribe block due to cancel"));
99
+ this.api.getDocumentModifier().modifyHtml(this.currentNode).replaceWith(`<${k.EMPTY_CONTAINER}/>`).apply(new h("Removed unsubscribe block due to cancel"));
70
100
  } catch (e) {
71
101
  console.warn("[UnsubscribeBlock] Failed to remove unsubscribe block:", e);
72
102
  }
@@ -74,35 +104,35 @@ class I extends _ {
74
104
  _removeEventListeners() {
75
105
  this._removeSelectEventListener(), this._removeCancelEventListener();
76
106
  }
77
- _updateBlock(e, r) {
107
+ _updateBlock(e, t) {
78
108
  if (!this.currentNode || !("querySelector" in this.currentNode))
79
109
  return;
80
- const s = this.currentNode.querySelector(T);
81
- if (!s)
110
+ const r = this.currentNode.querySelector(p);
111
+ if (!r)
82
112
  return;
83
- const t = this._getMergeTag(e);
84
- this.api.getDocumentModifier().modifyHtml(s).setAttribute("href", t).apply(new b(`Updated unsubscribe link to ${t}`)), this.api.getDocumentModifier().modifyHtml(this.currentNode).setAttribute(o.PAGE_TYPE, e.toString()).setAttribute(o.PAGE_LIST, r).apply(new b("Updated unsubscribe block metadata"));
113
+ const s = this._getMergeTag(e);
114
+ this.api.getDocumentModifier().modifyHtml(r).setAttribute("href", s).apply(new h(`Updated unsubscribe link to ${s}`)), this.api.getDocumentModifier().modifyHtml(this.currentNode).setAttribute(o.PAGE_TYPE, e.toString()).setAttribute(o.PAGE_LIST, t).apply(new h("Updated unsubscribe block metadata"));
85
115
  }
86
116
  _getMergeTag(e) {
87
- return C[e] ?? B;
117
+ return A[e] ?? U;
88
118
  }
89
119
  _openDrawer() {
90
120
  if (!(this.currentNode && this.currentNode.getAttribute("data-unsubscribe-page-type")))
91
121
  try {
92
- const e = c();
122
+ const e = a();
93
123
  e.typeSelectionDrawerStatus = !0;
94
124
  } catch (e) {
95
125
  console.error("[UnsubscribeBlock] Failed to open drawer:", e);
96
126
  }
97
127
  }
98
128
  _checkExistingBlocks() {
99
- const e = c();
100
- e.isGlobalUnsubscribeDisabled = !1, e.isSubscriptionPreferencesCenterDisabled = !1, this.api.getDocumentRoot().querySelectorAll(f).forEach((s) => {
101
- if ("getAttribute" in s) {
102
- const t = s.getAttribute(o.PAGE_TYPE);
103
- if (t) {
104
- const l = Number(t);
105
- l === E.GLOBAL_UNSUBSCRIBE ? e.isGlobalUnsubscribeDisabled = !0 : l === E.SUBSCRIPTION_PREFERENCE_CENTER && (e.isSubscriptionPreferencesCenterDisabled = !0);
129
+ const e = a();
130
+ e.isGlobalUnsubscribeDisabled = !1, e.isSubscriptionPreferencesCenterDisabled = !1, this.api.getDocumentRoot().querySelectorAll(B).forEach((r) => {
131
+ if ("getAttribute" in r) {
132
+ const s = r.getAttribute(o.PAGE_TYPE);
133
+ if (s) {
134
+ const l = Number(s);
135
+ l === b.GLOBAL_UNSUBSCRIBE ? e.isGlobalUnsubscribeDisabled = !0 : l === b.SUBSCRIPTION_PREFERENCE_CENTER && (e.isSubscriptionPreferencesCenterDisabled = !0);
106
136
  }
107
137
  }
108
138
  });
@@ -110,26 +140,26 @@ class I extends _ {
110
140
  async _loadBlockState(e) {
111
141
  if (!("getAttribute" in e))
112
142
  return;
113
- const r = e.getAttribute(o.PAGE_TYPE), s = e.getAttribute(o.PAGE_LIST);
114
- if (!r || !s)
143
+ const t = e.getAttribute(o.PAGE_TYPE), r = e.getAttribute(o.PAGE_LIST);
144
+ if (!t || !r)
115
145
  return;
116
- const t = c(), l = Number(r), m = p(s);
117
- await t.fetchTemplates(), t.setCollectionWithoutAutoSelection(l), t.loadSelectedTemplates(m);
146
+ const s = a(), l = Number(t), m = E(r);
147
+ await s.fetchTemplates(), s.setCollectionWithoutAutoSelection(l), s.loadSelectedTemplates(m);
118
148
  }
119
149
  _resetStoreState() {
120
- c().$reset();
150
+ a().$reset();
121
151
  }
122
152
  _removeBlockTemplatesFromStore(e) {
123
153
  if (!("getAttribute" in e))
124
154
  return;
125
- const r = e.getAttribute(o.PAGE_LIST);
126
- if (!r)
155
+ const t = e.getAttribute(o.PAGE_LIST);
156
+ if (!t)
127
157
  return;
128
- const s = c(), t = p(r);
129
- s.removeUnsubscribePages(t);
158
+ const r = a(), s = E(t);
159
+ r.removeUnsubscribePages(s);
130
160
  }
131
161
  }
132
162
  export {
133
- g as UNSUBSCRIBE_BLOCK_ID,
134
- I as UnsubscribeBlock
163
+ T as UNSUBSCRIBE_BLOCK_ID,
164
+ O as UnsubscribeBlock
135
165
  };
@@ -1,22 +1,22 @@
1
- import { BlockType as e } from "../../../node_modules/@stripoinc/ui-editor-extensions/dist/esm/index.js";
2
- const s = "unsubscribe", n = "{{ins-unsubscribe-link}}", t = `
3
- <${e.BLOCK_TEXT}
1
+ import { BlockType as s } from "../../../node_modules/@stripoinc/ui-editor-extensions/dist/esm/index.js";
2
+ const e = "unsubscribe", n = "{{ins-unsubscribe-link}}", t = `
3
+ <${s.BLOCK_TEXT}
4
4
  class="unsubscribe-block-v2 esd-block-unsubscribe es-p0"
5
5
  align="center"
6
6
  data-unsubscribe-page-type=""
7
7
  data-unsubscribe-page-list=""
8
8
  >
9
- <p>You can <strong><a
9
+ <p>&nbsp;<strong><a
10
10
  href="${n}"
11
11
  class="unsubscribe-link"
12
12
  target="_blank"
13
13
  data-unsubscribe-link="true"
14
- >${s}</a></strong> from our emails, if you need to.</p>
15
- </${e.BLOCK_TEXT}>
14
+ >${e}</a></strong></p>
15
+ </${s.BLOCK_TEXT}>
16
16
  `;
17
- function r() {
17
+ function u() {
18
18
  return t;
19
19
  }
20
20
  export {
21
- r as getDefaultTemplate
21
+ u as getDefaultTemplate
22
22
  };
@@ -7,5 +7,5 @@ export declare const useActionsApi: () => {
7
7
  getPreviewData: (options?: CompileEmailOptions) => Promise<CompiledEmailResult>;
8
8
  updateTimerInClonedTemplate: () => Promise<string | null>;
9
9
  updateHtmlAndCss: (html: string, css: string) => void;
10
- editorSave: () => Promise<boolean>;
10
+ editorSave: () => Promise<void>;
11
11
  };
@@ -1,10 +1,11 @@
1
- import type { ImmutableHtmlElementNode } from '@stripoinc/ui-editor-extensions';
1
+ import type { ImmutableHtmlElementNode, ImmutableHtmlNode } from '@stripoinc/ui-editor-extensions';
2
2
  import { Block, BlockCompositionType } from '@stripoinc/ui-editor-extensions';
3
3
  export declare const UNSUBSCRIBE_BLOCK_ID = "unsubscribe-block";
4
4
  export declare class UnsubscribeBlock extends Block {
5
5
  private selectEventListener;
6
6
  private cancelEventListener;
7
7
  private currentNode?;
8
+ private hadUnsubscribeLink;
8
9
  constructor();
9
10
  getId(): string;
10
11
  getIcon(): string;
@@ -13,8 +14,11 @@ export declare class UnsubscribeBlock extends Block {
13
14
  getDescription(): string;
14
15
  getTemplate(): string;
15
16
  onSelect(node: ImmutableHtmlElementNode): void;
17
+ onDocumentChanged(node: ImmutableHtmlNode): void;
16
18
  onDelete(node: ImmutableHtmlElementNode): void;
17
19
  onDestroy(): void;
20
+ private _hasUnsubscribeLink;
21
+ private _warnLinkRemoved;
18
22
  private _setupSelectEventListener;
19
23
  private _removeSelectEventListener;
20
24
  private _setupCancelEventListener;
@@ -0,0 +1 @@
1
+ export {};
File without changes
@@ -1,8 +1,8 @@
1
1
  import "../@types/config/schemas.js";
2
2
  import "../@types/config/defaults.js";
3
- import { parseConfig as n } from "../@types/config/validator.js";
4
- import { defineStore as o } from "pinia";
5
- const s = o("guido-config", {
3
+ import { parseConfig as o } from "../@types/config/validator.js";
4
+ import { defineStore as r } from "pinia";
5
+ const f = r("guido-config", {
6
6
  state: () => ({
7
7
  initialized: !1,
8
8
  config: null
@@ -75,64 +75,65 @@ const s = o("guido-config", {
75
75
  * Get the template ID
76
76
  */
77
77
  templateId: (i) => {
78
- var e;
79
- return ((e = i.config) == null ? void 0 : e.identity.templateId) ?? "";
78
+ var e, t;
79
+ return ((t = (e = i.config) == null ? void 0 : e.identity) == null ? void 0 : t.templateId) ?? "";
80
80
  },
81
81
  /**
82
82
  * Get the user ID
83
83
  */
84
84
  userId: (i) => {
85
- var e;
86
- return ((e = i.config) == null ? void 0 : e.identity.userId) ?? "";
85
+ var e, t;
86
+ return ((t = (e = i.config) == null ? void 0 : e.identity) == null ? void 0 : t.userId) ?? "";
87
87
  },
88
88
  /**
89
89
  * Get the variation ID
90
90
  */
91
91
  variationId: (i) => {
92
- var e;
93
- return ((e = i.config) == null ? void 0 : e.identity.variationId) ?? "";
92
+ var e, t;
93
+ return ((t = (e = i.config) == null ? void 0 : e.identity) == null ? void 0 : t.variationId) ?? "";
94
94
  },
95
95
  /**
96
96
  * Get the partner name
97
97
  */
98
98
  partnerName: (i) => {
99
- var e;
100
- return ((e = i.config) == null ? void 0 : e.partner.name) ?? "";
99
+ var e, t;
100
+ return ((t = (e = i.config) == null ? void 0 : e.partner) == null ? void 0 : t.name) ?? "";
101
101
  },
102
102
  /**
103
103
  * Get the product type
104
104
  */
105
105
  productType: (i) => {
106
- var e;
107
- return ((e = i.config) == null ? void 0 : e.partner.productType) ?? 60;
106
+ var e, t;
107
+ return ((t = (e = i.config) == null ? void 0 : e.partner) == null ? void 0 : t.productType) ?? 60;
108
108
  },
109
109
  /**
110
110
  * Get the message type
111
111
  */
112
112
  messageType: (i) => {
113
- var e;
114
- return ((e = i.config) == null ? void 0 : e.partner.messageType) ?? 1;
113
+ var e, t;
114
+ return ((t = (e = i.config) == null ? void 0 : e.partner) == null ? void 0 : t.messageType) ?? 1;
115
115
  },
116
116
  /**
117
117
  * Get the username
118
118
  */
119
119
  username: (i) => {
120
- var e;
121
- return ((e = i.config) == null ? void 0 : e.partner.username) ?? "Guido User";
120
+ var e, t;
121
+ return ((t = (e = i.config) == null ? void 0 : e.partner) == null ? void 0 : t.username) ?? "Guido User";
122
122
  },
123
123
  /**
124
124
  * Check if header should be shown
125
125
  */
126
126
  showHeader: (i) => {
127
- var e;
128
- return ((e = i.config) == null ? void 0 : e.ui.showHeader) ?? !0;
127
+ var e, t;
128
+ return ((t = (e = i.config) == null ? void 0 : e.ui) == null ? void 0 : t.showHeader) ?? !0;
129
129
  },
130
130
  /**
131
131
  * Check if a specific feature is enabled
132
132
  */
133
+ // eslint-disable-next-line @stylistic/max-len
133
134
  isFeatureEnabled: (i) => (e) => {
134
- var t;
135
- return ((t = i.config) == null ? void 0 : t.features[e]) ?? !0;
135
+ var t, n;
136
+ return ((n = (t = i.config) == null ? void 0 : t.features) == null ? void 0 : n[e]) ?? !0;
136
137
  }
137
138
  },
138
139
  actions: {
@@ -140,7 +141,7 @@ const s = o("guido-config", {
140
141
  * Initialize the config store with validated configuration
141
142
  */
142
143
  init(i) {
143
- const e = n(i);
144
+ const e = o(i);
144
145
  this.config = e, this.initialized = !0;
145
146
  },
146
147
  /**
@@ -152,5 +153,5 @@ const s = o("guido-config", {
152
153
  }
153
154
  });
154
155
  export {
155
- s as useConfigStore
156
+ f as useConfigStore
156
157
  };
@@ -1,79 +1,87 @@
1
- import { useActionsApi as T } from "../composables/useActionsApi.js";
2
- import { useHtmlCompiler as C } from "../composables/useHtmlCompiler.js";
3
- import { DEFAULT_CURRENCY as l, DEFAULT_NODE_CONFIG as a } from "../extensions/Blocks/Recommendation/constants/defaultConfig.js";
1
+ import { useActionsApi as C } from "../composables/useActionsApi.js";
2
+ import { useHtmlCompiler as T } from "../composables/useHtmlCompiler.js";
3
+ import { DEFAULT_CURRENCY as d, DEFAULT_NODE_CONFIG as s } from "../extensions/Blocks/Recommendation/constants/defaultConfig.js";
4
4
  import { useRecommendationExtensionStore as b } from "../extensions/Blocks/Recommendation/store/recommendation.js";
5
- import { DATA_ATTRIBUTES as y } from "../extensions/Blocks/Unsubscribe/utils/constants.js";
6
- import { parsePageList as P } from "../extensions/Blocks/Unsubscribe/utils/utils.js";
7
- import { useDynamicContentStore as D } from "../stores/dynamic-content.js";
8
- import { useUnsubscribeStore as E } from "../stores/unsubscribe.js";
9
- function U(i, r) {
10
- const s = new DOMParser().parseFromString(i, "text/html").querySelectorAll(`[${y.PAGE_LIST}]`), c = [];
11
- return s.forEach((t) => {
12
- const o = t.getAttribute(y.PAGE_LIST);
13
- o && c.push(...P(o));
14
- }), r.filter((t) => c.includes(t));
5
+ import { DATA_ATTRIBUTES as S } from "../extensions/Blocks/Unsubscribe/utils/constants.js";
6
+ import { parsePageList as D } from "../extensions/Blocks/Unsubscribe/utils/utils.js";
7
+ import { useDynamicContentStore as P } from "../stores/dynamic-content.js";
8
+ import { useUnsubscribeStore as w } from "../stores/unsubscribe.js";
9
+ function E(i, o) {
10
+ const t = new DOMParser().parseFromString(i, "text/html").querySelectorAll(`[${S.PAGE_LIST}]`), c = [];
11
+ return t.forEach((r) => {
12
+ const a = r.getAttribute(S.PAGE_LIST);
13
+ a && c.push(...D(a));
14
+ }), o.filter((r) => c.includes(r));
15
15
  }
16
- function F(i) {
16
+ async function U(i) {
17
17
  const m = new DOMParser().parseFromString(i, "text/html").querySelectorAll(".recommendation-block-v2");
18
18
  if (m.length === 0)
19
19
  return;
20
- const d = b();
21
- m.forEach((s) => {
22
- var g, f, u, n, S;
23
- const c = s.getAttribute("recommendation-id"), t = c ? Number(c) : NaN;
24
- if (!Number.isFinite(t))
20
+ const l = b();
21
+ m.forEach((t) => {
22
+ var g, f, u, n, y;
23
+ const c = t.getAttribute("recommendation-id"), r = c ? Number(c) : NaN;
24
+ if (!Number.isFinite(r))
25
25
  return;
26
- const o = s.getAttribute("esd-ext-config");
27
- if (!o)
26
+ const a = t.getAttribute("esd-ext-config");
27
+ if (!a)
28
28
  return;
29
29
  let e;
30
30
  try {
31
- e = JSON.parse(o);
31
+ e = JSON.parse(a);
32
32
  } catch {
33
33
  return;
34
34
  }
35
35
  if (!e || typeof e != "object" || Array.isArray(e))
36
36
  return;
37
37
  const p = {
38
- strategy: e.strategy ?? a.strategy,
39
- language: e.language ?? a.language,
40
- size: e.size ?? a.size,
38
+ strategy: e.strategy ?? s.strategy,
39
+ language: e.language ?? s.language,
40
+ size: e.size ?? s.size,
41
41
  // Spread the default arrays so each block gets a fresh reference
42
42
  // instead of sharing the singleton in DEFAULT_NODE_CONFIG.
43
- productIds: e.productIds ?? [...a.productIds],
44
- filters: e.filters ?? [...a.filters],
45
- shuffleProducts: e.shuffleProducts ?? a.shuffleProducts,
46
- currencyCode: ((g = e.currency) == null ? void 0 : g.code) ?? l.code,
47
- currencyAlignment: ((f = e.currency) == null ? void 0 : f.alignment) ?? l.alignment,
48
- currencyDecimalCount: ((u = e.currency) == null ? void 0 : u.decimalCount) ?? l.decimalCount,
49
- currencyDecimalSeparator: ((n = e.currency) == null ? void 0 : n.decimalSeparator) ?? l.decimalSeparator,
50
- currencyThousandSeparator: ((S = e.currency) == null ? void 0 : S.thousandSeparator) ?? l.thousandSeparator
43
+ productIds: e.productIds ?? [...s.productIds],
44
+ filters: e.filters ?? [...s.filters],
45
+ shuffleProducts: e.shuffleProducts ?? s.shuffleProducts,
46
+ currencyCode: ((g = e.currency) == null ? void 0 : g.code) ?? d.code,
47
+ currencyAlignment: ((f = e.currency) == null ? void 0 : f.alignment) ?? d.alignment,
48
+ currencyDecimalCount: ((u = e.currency) == null ? void 0 : u.decimalCount) ?? d.decimalCount,
49
+ currencyDecimalSeparator: ((n = e.currency) == null ? void 0 : n.decimalSeparator) ?? d.decimalSeparator,
50
+ currencyThousandSeparator: ((y = e.currency) == null ? void 0 : y.thousandSeparator) ?? d.thousandSeparator
51
51
  };
52
- d.seedBlockUrlConfig(t, p);
52
+ l.seedBlockUrlConfig(r, p);
53
53
  });
54
+ try {
55
+ await l.fetchRecommendationCreateData();
56
+ } catch (t) {
57
+ console.warn(
58
+ "Recommendation reference data pre-load failed; validator will skip the availability check.",
59
+ t
60
+ );
61
+ }
54
62
  }
55
63
  const _ = () => {
56
- const i = D(), r = E(), { getCompiledEmail: m, getTemplateData: d } = T(), { compileHtml: s } = C();
64
+ const i = P(), o = w(), { getCompiledEmail: m, getTemplateData: l } = C(), { compileHtml: t } = T();
57
65
  return {
58
66
  prepareTemplateDetails: async () => {
59
- const { html: t, ampHtml: o = "", ampErrors: e = [] } = await m({
67
+ const { html: r, ampHtml: a = "", ampErrors: e = [] } = await m({
60
68
  minimize: !0,
61
69
  resetDataSavedFlag: !1
62
- }), { html: p, css: g, syncModulesIds: f = [] } = await d();
63
- r.selectedUnsubscribePages.length && await r.fetchTemplates(), F(p);
64
- const { compiledHtml: u, stats: n, appliedRules: S } = s(t), h = i.getSelectedDynamicContentList, A = b();
70
+ }), { html: p, css: g, syncModulesIds: f = [] } = await l();
71
+ o.selectedUnsubscribePages.length && await o.fetchTemplates(), await U(p);
72
+ const { compiledHtml: u, stats: n, appliedRules: y } = t(r), h = i.getSelectedDynamicContentList, A = b();
65
73
  return console.debug("HTML Compilation Stats:", {
66
74
  originalSize: n.originalSize,
67
75
  compiledSize: n.compiledSize,
68
76
  reduction: `${n.reductionPercentage.toFixed(2)}%`,
69
- appliedRules: S,
77
+ appliedRules: y,
70
78
  executionTime: `${n.executionTime.toFixed(2)}ms`
71
79
  }), {
72
80
  dynamicContentList: h,
73
81
  compiledHtml: u,
74
82
  rawHtml: p,
75
83
  css: g,
76
- ampHtml: o,
84
+ ampHtml: a,
77
85
  ampErrors: e,
78
86
  modules: f.map(Number),
79
87
  recommendation: {
@@ -81,8 +89,8 @@ const _ = () => {
81
89
  configs: {}
82
90
  },
83
91
  unsubscribe: {
84
- status: r.unsubscribePagesStatus,
85
- config: U(u, r.selectedUnsubscribePages)
92
+ status: o.unsubscribePagesStatus,
93
+ config: E(u, o.selectedUnsubscribePages)
86
94
  }
87
95
  };
88
96
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@useinsider/guido",
3
- "version": "3.4.1-beta.1fc7f53",
3
+ "version": "3.4.1-beta.3b1ce15",
4
4
  "description": "Guido is a Vue + TypeScript wrapper for Email Plugin. Easily embed the email editor in your Vue applications.",
5
5
  "main": "./dist/guido.umd.cjs",
6
6
  "module": "./dist/library.js",
@@ -24,7 +24,7 @@
24
24
  "lint": "NODE_OPTIONS=--max-old-space-size=2048 eslint ./ && bun run type-check",
25
25
  "lint:fix": "NODE_OPTIONS=--max-old-space-size=2048 eslint --fix ./",
26
26
  "type-check": "vue-tsc --noEmit --project tsconfig.config.json && vue-tsc --noEmit --project tsconfig.app.json",
27
- "test": "vitest run",
27
+ "test": "vitest run --coverage",
28
28
  "test:watch": "vitest",
29
29
  "test:visual-update": "npx playwright test --update-snapshots --reporter html",
30
30
  "prepare": "husky",
@@ -61,6 +61,7 @@
61
61
  "@typescript-eslint/parser": "7.18.0",
62
62
  "@useinsider/eslint-config": "1.5.3",
63
63
  "@vitejs/plugin-vue2": "2.3.1",
64
+ "@vitest/coverage-v8": "2.1.9",
64
65
  "eslint": "8.57.1",
65
66
  "eslint-config-airbnb-base": "15.0.0",
66
67
  "eslint-config-airbnb-typescript": "18.0.0",