@useinsider/guido 3.9.1 → 3.10.0-beta.076d56a
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/dist/composables/useCortexBlueprintBridge.js +36 -32
- package/dist/composables/useCustomInterfaceAppearance.js +16 -18
- package/dist/composables/useEmailTemplateApplier.js +17 -15
- package/dist/composables/useExport.js +18 -18
- package/dist/composables/useHtmlValidator.js +90 -90
- package/dist/composables/useStripo.js +53 -51
- package/dist/composables/useStripoNotifications.js +26 -0
- package/dist/composables/useToaster.js +11 -11
- package/dist/composables/useVersionHistoryApi.js +2 -2
- package/dist/composables/validators/useLiquidValidator.js +1 -1
- package/dist/composables/validators/useUnsubscribeBlockValidator.js +10 -10
- package/dist/config/compiler/htmlCompilerRules.js +30 -7
- package/dist/config/compiler/utils/recommendationCompilerUtils.js +137 -119
- package/dist/config/i18n/en/labels.json.js +4 -1
- package/dist/enums/defaults.js +7 -4
- package/dist/enums/toaster.js +2 -2
- package/dist/enums/unsubscribe.js +41 -36
- package/dist/extensions/Blocks/Checkbox/block.js +5 -3
- package/dist/extensions/Blocks/Checkbox/control.js +42 -28
- package/dist/extensions/Blocks/Items/controls/price/currencySymbol.js +1 -1
- package/dist/extensions/Blocks/RadioButton/block.js +9 -7
- package/dist/extensions/Blocks/RadioButton/control.js +64 -50
- package/dist/extensions/Blocks/Recommendation/useRecommendationBlockWarning.js +7 -7
- package/dist/extensions/Blocks/Recommendation/utils/captureStyleTemplates.js +75 -69
- package/dist/extensions/Blocks/Recommendation/validation/requiredFields.js +18 -17
- package/dist/services/blankTemplate.js +34 -0
- package/dist/services/stripoApi.js +13 -18
- package/dist/services/templateLibraryApi.js +8 -7
- package/dist/src/composables/useCortexBlueprintBridge.d.ts +5 -2
- package/dist/src/composables/useEmailTemplateApplier.d.ts +8 -3
- package/dist/src/composables/useStripoNotifications.d.ts +10 -0
- package/dist/src/enums/defaults.d.ts +3 -0
- package/dist/src/enums/toaster.d.ts +2 -2
- package/dist/src/enums/unsubscribe.d.ts +2 -0
- package/dist/src/extensions/Blocks/Recommendation/utils/captureStyleTemplates.d.ts +6 -0
- package/dist/src/extensions/Blocks/Recommendation/validation/requiredFields.d.ts +1 -0
- package/dist/src/services/blankTemplate.d.ts +11 -0
- package/dist/src/stores/toaster.d.ts +2 -3
- package/dist/static/styles/components/dropdown-menu.css.js +12 -0
- package/dist/stores/toaster.js +10 -10
- package/dist/utils/pairProductVariables.js +64 -65
- package/package.json +2 -2
- package/dist/static/styles/components/notification.css.js +0 -74
- package/dist/static/templates/empty/index.html.js +0 -74
- package/dist/static/templates/empty/style.css.js +0 -779
|
@@ -1,29 +1,30 @@
|
|
|
1
|
-
import { useActionsApi as
|
|
2
|
-
import { useBlocksConfig as
|
|
3
|
-
import { useConfig as
|
|
4
|
-
import { useCustomInterfaceAppearance as
|
|
5
|
-
import { useFullStoryBridge as
|
|
6
|
-
import { useStripoEventHandler as
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
1
|
+
import { useActionsApi as U } from "./useActionsApi.js";
|
|
2
|
+
import { useBlocksConfig as R } from "./useBlocksConfig.js";
|
|
3
|
+
import { useConfig as x } from "./useConfig.js";
|
|
4
|
+
import { useCustomInterfaceAppearance as O } from "./useCustomInterfaceAppearance.js";
|
|
5
|
+
import { useFullStoryBridge as q } from "./useFullStoryBridge.js";
|
|
6
|
+
import { useStripoEventHandler as H } from "./useStripoEventHandler.js";
|
|
7
|
+
import { useStripoNotifications as N } from "./useStripoNotifications.js";
|
|
8
|
+
import { useToaster as z } from "./useToaster.js";
|
|
9
|
+
import { localePatch as L } from "../config/i18n/index.js";
|
|
9
10
|
import { en as h } from "../config/i18n/en/index.js";
|
|
10
|
-
import { ToasterTypeOptions as
|
|
11
|
-
import { useStripoApi as
|
|
12
|
-
import
|
|
13
|
-
import { useEditorStore as
|
|
14
|
-
import { dynamicContentToMergeTags as
|
|
15
|
-
import
|
|
16
|
-
const
|
|
17
|
-
const { features: c, template: w, isFeatureEnabled: u } =
|
|
11
|
+
import { ToasterTypeOptions as $ } from "../enums/toaster.js";
|
|
12
|
+
import { useStripoApi as G } from "../services/stripoApi.js";
|
|
13
|
+
import J from "../static/styles/customEditorStyle.css.js";
|
|
14
|
+
import { useEditorStore as E } from "../stores/editor.js";
|
|
15
|
+
import { dynamicContentToMergeTags as X } from "../utils/genericUtil.js";
|
|
16
|
+
import j from "../package.json.js";
|
|
17
|
+
const fo = (C, l) => {
|
|
18
|
+
const { features: c, template: w, isFeatureEnabled: u } = x(), { handleError: m, showToaster: b } = z(), { getToken: T, getCustomFonts: k, getSyncModulesStatus: B } = G(), { handleEvent: F } = H(), { getStripoBlocksConfig: v } = R(), { getStripoNotifications: V } = N(), _ = async (s, i = [], r = !1) => {
|
|
18
19
|
var g, S, y;
|
|
19
|
-
const
|
|
20
|
+
const o = E(), { html: p, css: a } = s, { baseBlocks: t, extensions: d } = await v(), f = ((g = c.value) == null ? void 0 : g.displayConditions) ?? !0, M = ((S = c.value) == null ? void 0 : S.modulesDisabled) ?? !1, P = ((y = w.value) == null ? void 0 : y.forceRecreate) ?? !1;
|
|
20
21
|
await window.UIEditor.initEditor(
|
|
21
22
|
document.querySelector("#guido-editor"),
|
|
22
23
|
{
|
|
23
|
-
metadata:
|
|
24
|
+
metadata: C,
|
|
24
25
|
html: p,
|
|
25
26
|
css: a,
|
|
26
|
-
forceRecreate:
|
|
27
|
+
forceRecreate: P,
|
|
27
28
|
locale: "en",
|
|
28
29
|
undoButtonSelector: "#guido__undo-button",
|
|
29
30
|
redoButtonSelector: "#guido__redo-button",
|
|
@@ -33,12 +34,12 @@ const ce = (E, l) => {
|
|
|
33
34
|
customAppearanceMergetags: !u("liquidSyntax"),
|
|
34
35
|
customAppearanceMergetagsBorderColor: "#f1f3fe",
|
|
35
36
|
customAppearanceMergetagsBackgroundColor: "#f1f3fe",
|
|
36
|
-
customViewStyles:
|
|
37
|
+
customViewStyles: J,
|
|
37
38
|
conditionsEnabled: f,
|
|
38
39
|
customConditionsEnabled: f,
|
|
39
40
|
enableXSSSecurity: !0,
|
|
40
|
-
modulesDisabled:
|
|
41
|
-
syncModulesEnabled:
|
|
41
|
+
modulesDisabled: M,
|
|
42
|
+
syncModulesEnabled: r,
|
|
42
43
|
messageSettingsEnabled: !0,
|
|
43
44
|
displayGmailAnnotations: !0,
|
|
44
45
|
displayHiddenPreheader: !1,
|
|
@@ -51,82 +52,83 @@ const ce = (E, l) => {
|
|
|
51
52
|
editorFonts: {
|
|
52
53
|
showDefaultStandardFonts: !0,
|
|
53
54
|
showDefaultNotStandardFonts: !0,
|
|
54
|
-
customFonts:
|
|
55
|
+
customFonts: i
|
|
55
56
|
},
|
|
56
57
|
mergeTags: [
|
|
57
58
|
{
|
|
58
|
-
entries:
|
|
59
|
+
entries: X(
|
|
59
60
|
l.preselectedDynamicContentList,
|
|
60
61
|
u("liquidSyntax")
|
|
61
62
|
)
|
|
62
63
|
}
|
|
63
64
|
],
|
|
64
|
-
async onTokenRefreshRequest(
|
|
65
|
+
async onTokenRefreshRequest(e) {
|
|
65
66
|
try {
|
|
66
67
|
const n = await T();
|
|
67
|
-
|
|
68
|
+
e(n);
|
|
68
69
|
} catch (n) {
|
|
69
70
|
m(n, "Failed to refresh token");
|
|
70
71
|
}
|
|
71
72
|
},
|
|
72
73
|
onTemplateLoaded() {
|
|
73
74
|
try {
|
|
74
|
-
const { importCss:
|
|
75
|
-
|
|
76
|
-
|
|
75
|
+
const { importCss: e } = O(), { activateCustomViewStyles: n, updateTimerInClonedTemplate: D } = U(), { injectFullStory: I } = q();
|
|
76
|
+
e(), n(), I(), D(), l.onReady(), o.isStripoInitialized = !0, o.loadingStatus = !1, setTimeout(() => {
|
|
77
|
+
o.hasChanges = !1;
|
|
77
78
|
}, 1e3);
|
|
78
|
-
} catch (
|
|
79
|
-
m(
|
|
79
|
+
} catch (e) {
|
|
80
|
+
m(e, "Failed to load custom interface appearance");
|
|
80
81
|
}
|
|
81
82
|
},
|
|
82
|
-
onCodeEditorVisibilityChanged(
|
|
83
|
-
|
|
83
|
+
onCodeEditorVisibilityChanged(e) {
|
|
84
|
+
o.isCodeEditorOpen = e;
|
|
84
85
|
},
|
|
85
|
-
onEditorVisualModeChanged(
|
|
86
|
-
|
|
86
|
+
onEditorVisualModeChanged(e) {
|
|
87
|
+
o.editorVisualMode = e.toLowerCase();
|
|
87
88
|
},
|
|
88
|
-
onVersionHistoryVisibilityChanged(
|
|
89
|
-
|
|
89
|
+
onVersionHistoryVisibilityChanged(e) {
|
|
90
|
+
o.isVersionHistoryOpen = e;
|
|
90
91
|
},
|
|
91
92
|
onDataChanged() {
|
|
92
|
-
|
|
93
|
+
o.hasChanges = !0;
|
|
93
94
|
},
|
|
94
95
|
onEvent: F,
|
|
96
|
+
notifications: V(),
|
|
95
97
|
ignoreClickOutsideSelectors: [
|
|
96
98
|
"#guido-dynamic-content-modal",
|
|
97
99
|
".in-on-board-wrapper",
|
|
98
100
|
".in-drawer__container"
|
|
99
101
|
],
|
|
100
102
|
extensions: d,
|
|
101
|
-
localePatch:
|
|
103
|
+
localePatch: L
|
|
102
104
|
}
|
|
103
105
|
);
|
|
104
|
-
},
|
|
106
|
+
}, A = (s) => new Promise((i, r) => {
|
|
105
107
|
var d;
|
|
106
108
|
if (document.getElementById("UiEditorScript")) {
|
|
107
|
-
Promise.resolve(s()).then(
|
|
109
|
+
Promise.resolve(s()).then(i, r);
|
|
108
110
|
return;
|
|
109
111
|
}
|
|
110
|
-
const
|
|
112
|
+
const o = j.guido, a = `https://email-static.useinsider.com/guido/${(d = o == null ? void 0 : o.stripo) == null ? void 0 : d.version}/UIEditor.js`, t = document.createElement("script");
|
|
111
113
|
t.id = "UiEditorScript", t.type = "module", t.src = a, t.onload = () => {
|
|
112
|
-
Promise.resolve(s()).then(
|
|
114
|
+
Promise.resolve(s()).then(i, r);
|
|
113
115
|
}, t.onerror = () => {
|
|
114
|
-
|
|
116
|
+
r(new Error(`Failed to load Stripo UIEditor script from S3: ${a}`));
|
|
115
117
|
}, document.body.appendChild(t);
|
|
116
118
|
});
|
|
117
119
|
return { initPlugin: async (s) => {
|
|
118
|
-
const
|
|
120
|
+
const i = E();
|
|
119
121
|
try {
|
|
120
|
-
await
|
|
121
|
-
const [
|
|
122
|
+
await A(async () => {
|
|
123
|
+
const [r, o] = await Promise.all([
|
|
122
124
|
k(),
|
|
123
125
|
B()
|
|
124
126
|
]);
|
|
125
|
-
|
|
127
|
+
i.syncModulesEnabled = o, await _(s, r, o);
|
|
126
128
|
});
|
|
127
129
|
} catch {
|
|
128
|
-
|
|
129
|
-
type:
|
|
130
|
+
i.loadingStatus = !1, b({
|
|
131
|
+
type: $.Error,
|
|
130
132
|
message: h["An error has occurred. Please report the error code to support."],
|
|
131
133
|
actionButton: {
|
|
132
134
|
text: h["Reload page"],
|
|
@@ -137,5 +139,5 @@ const ce = (E, l) => {
|
|
|
137
139
|
} };
|
|
138
140
|
};
|
|
139
141
|
export {
|
|
140
|
-
|
|
142
|
+
fo as useStripo
|
|
141
143
|
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { useToaster as d } from "./useToaster.js";
|
|
2
|
+
import { ToasterTypeOptions as c } from "../enums/toaster.js";
|
|
3
|
+
const g = () => {
|
|
4
|
+
const { showToaster: s, hideToaster: u } = d();
|
|
5
|
+
let r = null;
|
|
6
|
+
const n = (o, t, i, e, f = !0) => {
|
|
7
|
+
r = i;
|
|
8
|
+
const l = e != null && e.action ? { text: e.action.label, onClick: e.action.func } : void 0;
|
|
9
|
+
s({ type: o, message: t, actionButton: l, autoHide: (e == null ? void 0 : e.autoClose) ?? f });
|
|
10
|
+
};
|
|
11
|
+
return { getStripoNotifications: () => ({
|
|
12
|
+
info: (o, t, i) => n(c.Info, o, t, i),
|
|
13
|
+
success: (o, t, i) => n(c.Success, o, t, i),
|
|
14
|
+
warn: (o, t, i) => n(c.Warning, o, t, i),
|
|
15
|
+
error: (o, t, i) => n(c.Error, o, t, i),
|
|
16
|
+
// A loader marks an in-progress operation; Stripo dismisses it via hide(id)
|
|
17
|
+
// when finished, so it must not auto-hide.
|
|
18
|
+
loader: (o, t, i) => n(c.Info, o, t, i, !1),
|
|
19
|
+
hide: (o) => {
|
|
20
|
+
r === o && (u(), r = null);
|
|
21
|
+
}
|
|
22
|
+
}) };
|
|
23
|
+
};
|
|
24
|
+
export {
|
|
25
|
+
g as useStripoNotifications
|
|
26
|
+
};
|
|
@@ -1,29 +1,29 @@
|
|
|
1
1
|
import { ToasterTypeOptions as T } from "../enums/toaster.js";
|
|
2
2
|
import { useToasterStore as c } from "../stores/toaster.js";
|
|
3
|
-
import { watch as u, nextTick as
|
|
3
|
+
import { watch as u, nextTick as f } from "vue";
|
|
4
4
|
const p = () => {
|
|
5
|
-
const
|
|
6
|
-
let
|
|
5
|
+
const o = c();
|
|
6
|
+
let t = null;
|
|
7
7
|
const s = () => {
|
|
8
|
-
|
|
8
|
+
t && (clearTimeout(t), t = null);
|
|
9
9
|
};
|
|
10
10
|
function r(e = { message: "" }) {
|
|
11
|
-
s(),
|
|
12
|
-
|
|
11
|
+
s(), o.hideToaster(), f(() => {
|
|
12
|
+
o.showToaster(e);
|
|
13
13
|
});
|
|
14
14
|
}
|
|
15
15
|
function i() {
|
|
16
|
-
s(),
|
|
16
|
+
s(), o.hideToaster();
|
|
17
17
|
}
|
|
18
18
|
function n(e, a) {
|
|
19
19
|
const m = e instanceof Error ? e.message : String(e);
|
|
20
|
-
r({ type: T.
|
|
20
|
+
r({ type: T.Error, message: `${a}: ${m}` });
|
|
21
21
|
}
|
|
22
22
|
return u(
|
|
23
|
-
() =>
|
|
23
|
+
() => o.shouldAutoHide,
|
|
24
24
|
(e) => {
|
|
25
|
-
s(), e && (
|
|
26
|
-
|
|
25
|
+
s(), e && (t = setTimeout(() => {
|
|
26
|
+
o.hideToaster(), s();
|
|
27
27
|
}, 6e3));
|
|
28
28
|
}
|
|
29
29
|
), { showToaster: r, hideToaster: i, handleError: n };
|
|
@@ -24,7 +24,7 @@ const S = () => {
|
|
|
24
24
|
window.StripoEditorApi.versionHistoryApi.previewVersion(e, (o) => {
|
|
25
25
|
const r = o instanceof Error ? o.message : String(o);
|
|
26
26
|
s({
|
|
27
|
-
type: t.
|
|
27
|
+
type: t.Error,
|
|
28
28
|
message: `Error compiling email: ${r}`
|
|
29
29
|
});
|
|
30
30
|
});
|
|
@@ -41,7 +41,7 @@ const S = () => {
|
|
|
41
41
|
(o) => {
|
|
42
42
|
const r = o instanceof Error ? o.message : String(o);
|
|
43
43
|
s({
|
|
44
|
-
type: t.
|
|
44
|
+
type: t.Error,
|
|
45
45
|
message: `Error compiling email: ${r}`
|
|
46
46
|
});
|
|
47
47
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { ToasterTypeOptions as i } from "../../enums/toaster.js";
|
|
2
2
|
import { PAGE_TYPES as u } from "../../enums/unsubscribe.js";
|
|
3
3
|
import { UNSUBSCRIBE_BLOCK_SELECTOR as l, DATA_ATTRIBUTES as a } from "../../extensions/Blocks/Unsubscribe/utils/constants.js";
|
|
4
|
-
import { useToaster as
|
|
5
|
-
import { useTranslations as
|
|
4
|
+
import { useToaster as E } from "../useToaster.js";
|
|
5
|
+
import { useTranslations as m } from "../useTranslations.js";
|
|
6
6
|
const T = [
|
|
7
7
|
{
|
|
8
8
|
pageType: u.GLOBAL_UNSUBSCRIBE,
|
|
@@ -13,23 +13,23 @@ const T = [
|
|
|
13
13
|
messageKey: "unsubscription-preference.duplicate-pref-center"
|
|
14
14
|
}
|
|
15
15
|
], g = "unsubscription-preference.duplicate-both", b = "unsubscription-preference.select-page-to-continue", P = () => {
|
|
16
|
-
const { showToaster: c } =
|
|
16
|
+
const { showToaster: c } = E(), p = m();
|
|
17
17
|
return { validateUnsubscribeBlockUniqueness: (n) => {
|
|
18
|
-
const o = new DOMParser().parseFromString(n, "text/html"),
|
|
18
|
+
const o = new DOMParser().parseFromString(n, "text/html"), r = /* @__PURE__ */ new Map();
|
|
19
19
|
o.querySelectorAll(l).forEach((e) => {
|
|
20
|
-
const
|
|
21
|
-
|
|
20
|
+
const s = Number(e.getAttribute(a.PAGE_TYPE));
|
|
21
|
+
r.set(s, (r.get(s) ?? 0) + 1);
|
|
22
22
|
});
|
|
23
|
-
const t = T.filter((e) => (
|
|
23
|
+
const t = T.filter((e) => (r.get(e.pageType) ?? 0) > 1).map((e) => e.messageKey);
|
|
24
24
|
return t.length ? (c({
|
|
25
|
-
type: i.
|
|
25
|
+
type: i.Error,
|
|
26
26
|
message: p(t.length > 1 ? g : t[0])
|
|
27
27
|
}), !1) : !0;
|
|
28
28
|
}, validateUnsubscribeBlockHasTemplate: (n) => {
|
|
29
29
|
const o = new DOMParser().parseFromString(n, "text/html");
|
|
30
30
|
return Array.from(o.querySelectorAll(l)).some((t) => {
|
|
31
|
-
const e = t.getAttribute(a.PAGE_TYPE),
|
|
32
|
-
return !e || !
|
|
31
|
+
const e = t.getAttribute(a.PAGE_TYPE), s = t.getAttribute(a.PAGE_LIST);
|
|
32
|
+
return !e || !s;
|
|
33
33
|
}) ? (c({
|
|
34
34
|
type: i.Warning,
|
|
35
35
|
message: p(b)
|
|
@@ -122,10 +122,10 @@ const d = [
|
|
|
122
122
|
], i = /<head>([\S\s]*)<\/head>/, o = new RegExp(i);
|
|
123
123
|
if (!e.match(o))
|
|
124
124
|
return e;
|
|
125
|
-
let
|
|
126
|
-
return t.forEach((
|
|
127
|
-
|
|
128
|
-
}),
|
|
125
|
+
let r = e;
|
|
126
|
+
return t.forEach((s) => {
|
|
127
|
+
r = r.replace("</head>", `${s}</head>`);
|
|
128
|
+
}), r;
|
|
129
129
|
},
|
|
130
130
|
priority: 30
|
|
131
131
|
},
|
|
@@ -156,9 +156,9 @@ const d = [
|
|
|
156
156
|
type: "custom",
|
|
157
157
|
processor: (e) => {
|
|
158
158
|
const t = (i) => i.toString(16).padStart(2, "0");
|
|
159
|
-
return e.replace(/rgb\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*\)/gi, (i, o, n,
|
|
160
|
-
const [
|
|
161
|
-
return
|
|
159
|
+
return e.replace(/rgb\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*\)/gi, (i, o, n, r) => {
|
|
160
|
+
const [s, a, c] = [Number(o), Number(n), Number(r)];
|
|
161
|
+
return s > 255 || a > 255 || c > 255 ? i : `#${t(s)}${t(a)}${t(c)}`;
|
|
162
162
|
}).replace(/#([0-9a-f])\1([0-9a-f])\2([0-9a-f])\3\b/gi, "#$1$2$3");
|
|
163
163
|
},
|
|
164
164
|
priority: 44
|
|
@@ -174,6 +174,29 @@ const d = [
|
|
|
174
174
|
replacement: "",
|
|
175
175
|
flags: "g",
|
|
176
176
|
priority: 45
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
id: "remove-script-tags",
|
|
180
|
+
// SD-145051: parity with HB HtmlCleaner.removeUnsafeHtmlTags — strip <script> from
|
|
181
|
+
// compiled email. Priority 50 runs after the </> unescape rules (13/14) so
|
|
182
|
+
// post-unescape tags are still caught. Do NOT add to AMP_SAFE_RULE_IDS (AMP needs <script>).
|
|
183
|
+
description: "Strip <script>…<\/script> tags from compiled email",
|
|
184
|
+
type: "regex",
|
|
185
|
+
pattern: "<script[^>]*>.*?<\/script>",
|
|
186
|
+
replacement: "",
|
|
187
|
+
flags: "gis",
|
|
188
|
+
priority: 50
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
id: "remove-iframe-tags",
|
|
192
|
+
// SD-145051: parity with HB HtmlCleaner.removeUnsafeHtmlTags — strip <iframe>.
|
|
193
|
+
// Do NOT add this id to AMP_SAFE_RULE_IDS.
|
|
194
|
+
description: "Strip <iframe>…</iframe> tags from compiled email",
|
|
195
|
+
type: "regex",
|
|
196
|
+
pattern: "<iframe[^>]*>.*?</iframe>",
|
|
197
|
+
replacement: "",
|
|
198
|
+
flags: "gis",
|
|
199
|
+
priority: 51
|
|
177
200
|
}
|
|
178
201
|
];
|
|
179
202
|
export {
|