@fonderie/react-courier-admin-screens 0.9.0 → 0.11.0
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/index.cjs +362 -136
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +30 -21
- package/dist/index.d.ts +30 -21
- package/dist/index.js +372 -143
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -46,8 +46,16 @@ function TemplateEditorScreen({
|
|
|
46
46
|
}) {
|
|
47
47
|
const t = (0, import_react.useMemo)(() => (0, import_client.createAdminT)(uiLocale), [uiLocale]);
|
|
48
48
|
const { template, isLoading, error, refresh } = (0, import_react_courier_admin.useTemplate)(client, type, locale);
|
|
49
|
-
const
|
|
50
|
-
const
|
|
49
|
+
const notSaved = error?.status === 404;
|
|
50
|
+
const { builtIn, isLoading: builtInLoading } = (0, import_react_courier_admin.useBuiltInTemplate)(client, type, locale);
|
|
51
|
+
const { catalog, refresh: refreshCatalog } = (0, import_react_courier_admin.useTemplateCatalog)(client);
|
|
52
|
+
const defaultLocale = catalog?.defaultLocale ?? "en-US";
|
|
53
|
+
const email = catalog?.emails.find((e) => e.type === type);
|
|
54
|
+
const languages = email ? (0, import_client.templateLanguages)(email, defaultLocale) : [];
|
|
55
|
+
const source = template ?? (notSaved ? builtIn : null);
|
|
56
|
+
const { resolution, resolve } = (0, import_react_courier_admin.useTemplateResolution)(client);
|
|
57
|
+
const [probe, setProbe] = (0, import_react.useState)("");
|
|
58
|
+
const { saveTemplate, removeTemplate } = (0, import_react_courier_admin.useTemplates)(client);
|
|
51
59
|
const [isDeleting, setIsDeleting] = (0, import_react.useState)(false);
|
|
52
60
|
const { revisions, rollback } = (0, import_react_courier_admin.useTemplateRevisions)(client, type, locale);
|
|
53
61
|
const [isSaving, setIsSaving] = (0, import_react.useState)(false);
|
|
@@ -60,12 +68,15 @@ function TemplateEditorScreen({
|
|
|
60
68
|
const [sampleJson, setSampleJson] = (0, import_react.useState)("{}");
|
|
61
69
|
const [sampleError, setSampleError] = (0, import_react.useState)(null);
|
|
62
70
|
(0, import_react.useEffect)(() => {
|
|
63
|
-
if (!
|
|
64
|
-
setSubject(
|
|
65
|
-
setHtml(
|
|
66
|
-
setText(
|
|
67
|
-
setActive(template.active);
|
|
68
|
-
}, [template]);
|
|
71
|
+
if (!source) return;
|
|
72
|
+
setSubject(source.subject ?? "");
|
|
73
|
+
setHtml(source.html ?? "");
|
|
74
|
+
setText(source.text);
|
|
75
|
+
setActive(template ? template.active : true);
|
|
76
|
+
}, [source, template]);
|
|
77
|
+
(0, import_react.useEffect)(() => {
|
|
78
|
+
void resolve(type, locale ?? defaultLocale);
|
|
79
|
+
}, [resolve, type, locale, defaultLocale]);
|
|
69
80
|
const run = (0, import_react.useCallback)(async () => {
|
|
70
81
|
let data = {};
|
|
71
82
|
try {
|
|
@@ -93,13 +104,15 @@ function TemplateEditorScreen({
|
|
|
93
104
|
}, [renderPreview, type, text, subject, html, locale, sampleJson, t]);
|
|
94
105
|
const runRef = (0, import_react.useRef)(run);
|
|
95
106
|
runRef.current = run;
|
|
96
|
-
const loadedType =
|
|
107
|
+
const loadedType = source ? `${type}:${locale ?? ""}` : null;
|
|
97
108
|
(0, import_react.useEffect)(() => {
|
|
98
109
|
if (!loadedType) return;
|
|
99
110
|
const timer = setTimeout(() => void runRef.current(), PREVIEW_DEBOUNCE_MS);
|
|
100
111
|
return () => clearTimeout(timer);
|
|
101
112
|
}, [loadedType, subject, html, text, sampleJson]);
|
|
102
|
-
const dirty = !!template && (subject !== (template.subject ?? "") || html !== (template.html ?? "") || text !== template.text || active !== template.active);
|
|
113
|
+
const dirty = notSaved && !!builtIn || !!template && (subject !== (template.subject ?? "") || html !== (template.html ?? "") || text !== template.text || active !== template.active);
|
|
114
|
+
const protectedRow = !locale && (email?.system ?? system);
|
|
115
|
+
const shipsHere = locale ? languages.some((l) => l.locale === locale && l.builtIn) : !!email?.builtIn.default;
|
|
103
116
|
const handleSubmit = async (event) => {
|
|
104
117
|
event.preventDefault();
|
|
105
118
|
setIsSaving(true);
|
|
@@ -110,7 +123,7 @@ function TemplateEditorScreen({
|
|
|
110
123
|
if (html) input.html = html;
|
|
111
124
|
if (template?.version !== void 0) input.ifVersion = template.version;
|
|
112
125
|
await saveTemplate(type, input, locale);
|
|
113
|
-
await refresh();
|
|
126
|
+
await Promise.all([refresh(), refreshCatalog()]);
|
|
114
127
|
onSaved?.();
|
|
115
128
|
} catch (err) {
|
|
116
129
|
setSaveError(err);
|
|
@@ -118,9 +131,32 @@ function TemplateEditorScreen({
|
|
|
118
131
|
setIsSaving(false);
|
|
119
132
|
}
|
|
120
133
|
};
|
|
121
|
-
const switchTo = (
|
|
122
|
-
if (dirty && !window.confirm(t("templates.editor.discardChanges")))
|
|
123
|
-
|
|
134
|
+
const switchTo = (next) => {
|
|
135
|
+
if (dirty && !(notSaved && !!builtIn) && !window.confirm(t("templates.editor.discardChanges")))
|
|
136
|
+
return;
|
|
137
|
+
onSelectLocale?.(next);
|
|
138
|
+
};
|
|
139
|
+
const handleReset = async () => {
|
|
140
|
+
if (!builtIn || !window.confirm(t("templates.editor.confirmReset", { locale: locale ?? defaultLocale })))
|
|
141
|
+
return;
|
|
142
|
+
setIsDeleting(true);
|
|
143
|
+
setSaveError(null);
|
|
144
|
+
try {
|
|
145
|
+
if (locale) {
|
|
146
|
+
await removeTemplate(type, locale);
|
|
147
|
+
} else {
|
|
148
|
+
const input = { text: builtIn.text, active };
|
|
149
|
+
if (builtIn.subject) input.subject = builtIn.subject;
|
|
150
|
+
if (builtIn.html) input.html = builtIn.html;
|
|
151
|
+
if (template?.version !== void 0) input.ifVersion = template.version;
|
|
152
|
+
await saveTemplate(type, input, null);
|
|
153
|
+
}
|
|
154
|
+
await Promise.all([refresh(), refreshCatalog()]);
|
|
155
|
+
} catch (err) {
|
|
156
|
+
setSaveError(err);
|
|
157
|
+
} finally {
|
|
158
|
+
setIsDeleting(false);
|
|
159
|
+
}
|
|
124
160
|
};
|
|
125
161
|
const handleDelete = async () => {
|
|
126
162
|
const message = locale ? t("templates.editor.confirmDeleteLocale", { type, locale }) : t("templates.editor.confirmDelete", { type });
|
|
@@ -136,8 +172,9 @@ function TemplateEditorScreen({
|
|
|
136
172
|
setIsDeleting(false);
|
|
137
173
|
}
|
|
138
174
|
};
|
|
139
|
-
if (isLoading
|
|
140
|
-
|
|
175
|
+
if (isLoading || notSaved && builtInLoading)
|
|
176
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { style: styles.status, children: t("templates.editor.loading") });
|
|
177
|
+
if (error && !(notSaved && builtIn))
|
|
141
178
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { style: styles.error, role: "alert", children: error.explanation });
|
|
142
179
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: styles.container, children: [
|
|
143
180
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("h1", { style: styles.title, children: type }),
|
|
@@ -145,19 +182,23 @@ function TemplateEditorScreen({
|
|
|
145
182
|
onSelectLocale ? (
|
|
146
183
|
// Each tab opens another stored row, so this is navigation, not an
|
|
147
184
|
// ARIA tablist: plain buttons, the open one marked aria-current.
|
|
148
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("nav", { "aria-label": t("templates.editor.locales"), style: styles.tabs, children:
|
|
149
|
-
const selected =
|
|
185
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("nav", { "aria-label": t("templates.editor.locales"), style: styles.tabs, children: languages.map((l) => {
|
|
186
|
+
const selected = l.locale === (locale ?? null);
|
|
150
187
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
151
188
|
"button",
|
|
152
189
|
{
|
|
153
190
|
type: "button",
|
|
154
191
|
"aria-current": selected ? "true" : void 0,
|
|
155
|
-
onClick: () => selected ? void 0 : switchTo(
|
|
156
|
-
style:
|
|
157
|
-
|
|
158
|
-
|
|
192
|
+
onClick: () => selected ? void 0 : switchTo({ type, locale: l.locale, system: !!email?.system }),
|
|
193
|
+
style: {
|
|
194
|
+
...selected ? styles.tabSelected : styles.tab,
|
|
195
|
+
...l.saved ? {} : styles.tabBuiltIn,
|
|
196
|
+
...l.active ? {} : styles.tabInactive
|
|
197
|
+
},
|
|
198
|
+
title: l.active ? void 0 : t("common.status.inactive"),
|
|
199
|
+
children: l.label
|
|
159
200
|
},
|
|
160
|
-
|
|
201
|
+
l.label
|
|
161
202
|
);
|
|
162
203
|
}) })
|
|
163
204
|
) : null,
|
|
@@ -168,17 +209,21 @@ function TemplateEditorScreen({
|
|
|
168
209
|
style: { ...styles.rollbackButton, marginLeft: "auto" },
|
|
169
210
|
onClick: () => {
|
|
170
211
|
if (dirty && !window.confirm(t("templates.editor.discardChanges"))) return;
|
|
171
|
-
onAddLocale(type, {
|
|
212
|
+
onAddLocale(type, {
|
|
213
|
+
locales: catalog ? (0, import_client.suggestTemplateLocales)(catalog, type) : [],
|
|
214
|
+
defaultLocale
|
|
215
|
+
});
|
|
172
216
|
},
|
|
173
217
|
children: t("templates.editor.addLocale")
|
|
174
218
|
}
|
|
175
219
|
) : null
|
|
176
220
|
] }) : null,
|
|
177
221
|
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("p", { style: styles.meta, children: [
|
|
178
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("strong", { children: locale ??
|
|
179
|
-
|
|
180
|
-
template
|
|
181
|
-
|
|
222
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("strong", { children: locale ?? defaultLocale }),
|
|
223
|
+
template ? ` \xB7 v${template.version}` : "",
|
|
224
|
+
template && protectedRow ? ` \xB7 ${t("templates.editor.builtInNote")}` : "",
|
|
225
|
+
!template && builtIn ? ` \xB7 ${t("templates.editor.builtInCopy")}` : "",
|
|
226
|
+
resolution && resolution.chain.length > 1 && resolution.requested === (locale ?? defaultLocale) ? ` \xB7 ${t("templates.editor.chain", { chain: [...resolution.chain, defaultLocale].join(" \u2192 ") })}` : ""
|
|
182
227
|
] }),
|
|
183
228
|
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: styles.split, children: [
|
|
184
229
|
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: styles.column, children: [
|
|
@@ -254,7 +299,16 @@ function TemplateEditorScreen({
|
|
|
254
299
|
}
|
|
255
300
|
),
|
|
256
301
|
!dirty && !isSaving && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: styles.meta, children: t("templates.editor.noChanges") }),
|
|
257
|
-
|
|
302
|
+
template && shipsHere && builtIn ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
303
|
+
"button",
|
|
304
|
+
{
|
|
305
|
+
type: "button",
|
|
306
|
+
disabled: isDeleting,
|
|
307
|
+
onClick: () => void handleReset(),
|
|
308
|
+
style: { ...styles.rollbackButton, height: 36, marginLeft: "auto" },
|
|
309
|
+
children: t("templates.editor.resetToBuiltIn")
|
|
310
|
+
}
|
|
311
|
+
) : template && !protectedRow ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
258
312
|
"button",
|
|
259
313
|
{
|
|
260
314
|
type: "button",
|
|
@@ -309,6 +363,43 @@ function TemplateEditorScreen({
|
|
|
309
363
|
) : preview ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("pre", { style: styles.previewText, children: preview.text }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { style: styles.meta, children: t("templates.editor.nothingRendered") })
|
|
310
364
|
] })
|
|
311
365
|
] }),
|
|
366
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: styles.revisions, children: [
|
|
367
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("h2", { style: styles.subtitle, children: t("templates.editor.whoReceives") }),
|
|
368
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
369
|
+
"form",
|
|
370
|
+
{
|
|
371
|
+
style: { display: "flex", gap: 8, alignItems: "center", flexWrap: "wrap" },
|
|
372
|
+
onSubmit: (event) => {
|
|
373
|
+
event.preventDefault();
|
|
374
|
+
if (probe.trim()) void resolve(type, probe.trim());
|
|
375
|
+
},
|
|
376
|
+
children: [
|
|
377
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
378
|
+
"input",
|
|
379
|
+
{
|
|
380
|
+
"aria-label": t("templates.editor.whoReceives"),
|
|
381
|
+
style: { ...styles.input, width: 200 },
|
|
382
|
+
value: probe,
|
|
383
|
+
placeholder: t("templates.editor.whoPlaceholder"),
|
|
384
|
+
spellCheck: false,
|
|
385
|
+
onChange: (event) => setProbe(event.target.value)
|
|
386
|
+
}
|
|
387
|
+
),
|
|
388
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "submit", style: { ...styles.rollbackButton, height: 36 }, children: t("templates.editor.check") }),
|
|
389
|
+
resolution && probe.trim() ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { style: { ...styles.meta, marginBottom: 0 }, role: "status", children: [
|
|
390
|
+
t("templates.editor.receives", {
|
|
391
|
+
requested: resolution.requested,
|
|
392
|
+
sent: resolution.sent
|
|
393
|
+
}),
|
|
394
|
+
" ",
|
|
395
|
+
resolution.source === "saved" ? t("templates.editor.sourceSaved") : t("templates.editor.sourceBuiltIn"),
|
|
396
|
+
" ",
|
|
397
|
+
resolution.chain.length > 0 ? `(${[...resolution.chain, resolution.defaultLocale].join(" \u2192 ")})` : ""
|
|
398
|
+
] }) : null
|
|
399
|
+
]
|
|
400
|
+
}
|
|
401
|
+
)
|
|
402
|
+
] }),
|
|
312
403
|
revisions.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: styles.revisions, children: [
|
|
313
404
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("h2", { style: styles.subtitle, children: t("templates.editor.history") }),
|
|
314
405
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("ul", { style: styles.list, children: revisions.map((rev) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("li", { style: styles.row, children: [
|
|
@@ -365,7 +456,9 @@ var styles = {
|
|
|
365
456
|
borderBottomColor: "var(--fonderie-text,#171717)",
|
|
366
457
|
cursor: "default"
|
|
367
458
|
},
|
|
368
|
-
tabInactive: {
|
|
459
|
+
tabInactive: { opacity: 0.55, textDecoration: "line-through" },
|
|
460
|
+
// Fonderie's built-in copy, not saved by the app — the list's dashed chip.
|
|
461
|
+
tabBuiltIn: { fontStyle: "italic" },
|
|
369
462
|
// Two columns, always: the editor on the left, the rendered result on the
|
|
370
463
|
// right. Stacking the preview under a long form hid it below the fold.
|
|
371
464
|
split: {
|
|
@@ -543,8 +636,14 @@ function TemplateListScreen({
|
|
|
543
636
|
locale
|
|
544
637
|
}) {
|
|
545
638
|
const t = (0, import_client2.createAdminT)(locale);
|
|
546
|
-
const {
|
|
547
|
-
const
|
|
639
|
+
const { catalog, isLoading, error } = (0, import_react_courier_admin2.useTemplateCatalog)(client);
|
|
640
|
+
const emails = catalog?.emails ?? [];
|
|
641
|
+
const defaultLocale = catalog?.defaultLocale ?? "en-US";
|
|
642
|
+
const chipStyle = (l) => ({
|
|
643
|
+
...chip,
|
|
644
|
+
...l.saved ? {} : chipBuiltIn,
|
|
645
|
+
...l.active ? {} : chipOff
|
|
646
|
+
});
|
|
548
647
|
return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { style: styles2.container, children: [
|
|
549
648
|
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
|
|
550
649
|
"div",
|
|
@@ -558,8 +657,12 @@ function TemplateListScreen({
|
|
|
558
657
|
type: "button",
|
|
559
658
|
onClick: () => onCreateTemplate({
|
|
560
659
|
locales: [
|
|
561
|
-
...new Set(
|
|
562
|
-
|
|
660
|
+
...new Set(
|
|
661
|
+
emails.flatMap(
|
|
662
|
+
(e) => (0, import_client2.templateLanguages)(e, defaultLocale).filter((l) => l.locale !== null).map((l) => l.label)
|
|
663
|
+
)
|
|
664
|
+
)
|
|
665
|
+
].sort()
|
|
563
666
|
}),
|
|
564
667
|
style: styles2.newButton,
|
|
565
668
|
children: t("templates.list.newTemplate")
|
|
@@ -569,39 +672,47 @@ function TemplateListScreen({
|
|
|
569
672
|
}
|
|
570
673
|
),
|
|
571
674
|
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { style: styles2.hint, children: t("templates.list.hint") }),
|
|
572
|
-
isLoading ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { style: styles2.status, children: t("templates.list.loading") }) : error ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { style: styles2.error, role: "alert", children: error.explanation }) : /* @__PURE__ */ (0, import_jsx_runtime2.
|
|
573
|
-
/* @__PURE__ */ (0, import_jsx_runtime2.
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
675
|
+
isLoading ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { style: styles2.status, children: t("templates.list.loading") }) : error ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { style: styles2.error, role: "alert", children: error.explanation }) : /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_jsx_runtime2.Fragment, { children: [
|
|
676
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("ul", { style: styles2.list, children: emails.map((email) => {
|
|
677
|
+
const languages = (0, import_client2.templateLanguages)(email, defaultLocale);
|
|
678
|
+
const primary = languages.find((l) => l.locale === null) ?? languages[0];
|
|
679
|
+
const open = (l) => onSelectTemplate?.({
|
|
680
|
+
type: email.type,
|
|
681
|
+
locale: l?.locale ?? null,
|
|
682
|
+
system: email.system
|
|
683
|
+
});
|
|
684
|
+
return (
|
|
685
|
+
// Columns, not a flex row: the language chips line up down the
|
|
686
|
+
// list whatever the email name or badge.
|
|
687
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("li", { style: styles2.row, children: [
|
|
688
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("button", { type: "button", onClick: () => open(primary), style: styles2.rowButton, children: [
|
|
689
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { style: styles2.type, children: email.type }),
|
|
690
|
+
email.system ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { style: styles2.builtIn, children: t("templates.builtInBadge") }) : /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", {})
|
|
691
|
+
] }),
|
|
692
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { style: styles2.chips, children: languages.map((l) => /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
693
|
+
"button",
|
|
694
|
+
{
|
|
695
|
+
type: "button",
|
|
696
|
+
onClick: () => open(l),
|
|
697
|
+
style: chipStyle(l),
|
|
698
|
+
"aria-label": t("templates.list.openLocale", {
|
|
699
|
+
type: email.type,
|
|
700
|
+
locale: l.label
|
|
701
|
+
}),
|
|
702
|
+
title: l.active ? void 0 : t("common.status.inactive"),
|
|
703
|
+
children: l.label
|
|
704
|
+
},
|
|
705
|
+
l.label
|
|
706
|
+
)) }),
|
|
707
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("span", { style: primary?.active === false ? styles2.inactive : styles2.active, children: [
|
|
583
708
|
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { style: styles2.dot }),
|
|
584
|
-
|
|
709
|
+
primary?.active === false ? t("common.status.inactive") : t("common.status.active")
|
|
585
710
|
] })
|
|
586
|
-
]
|
|
587
|
-
}
|
|
588
|
-
),
|
|
589
|
-
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { style: styles2.chips, children: group.entries.map((entry) => {
|
|
590
|
-
const name = entry.locale ?? t("templates.defaultChip");
|
|
591
|
-
return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
592
|
-
"button",
|
|
593
|
-
{
|
|
594
|
-
type: "button",
|
|
595
|
-
onClick: () => onSelectTemplate?.(entry),
|
|
596
|
-
style: entry.active ? styles2.chip : styles2.chipInactive,
|
|
597
|
-
"aria-label": t("templates.list.openLocale", { type: group.type, locale: name }),
|
|
598
|
-
title: entry.active ? void 0 : t("common.status.inactive"),
|
|
599
|
-
children: name
|
|
600
|
-
},
|
|
601
|
-
entry.locale ?? ""
|
|
711
|
+
] }, email.type)
|
|
602
712
|
);
|
|
603
|
-
}) })
|
|
604
|
-
|
|
713
|
+
}) }),
|
|
714
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { style: styles2.legend, children: t("templates.list.legend") })
|
|
715
|
+
] })
|
|
605
716
|
] });
|
|
606
717
|
}
|
|
607
718
|
var pill = {
|
|
@@ -615,6 +726,7 @@ var pill = {
|
|
|
615
726
|
lineHeight: 1.5,
|
|
616
727
|
minWidth: 64
|
|
617
728
|
};
|
|
729
|
+
var MONO = 'var(--fonderie-mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace)';
|
|
618
730
|
var chip = {
|
|
619
731
|
height: 24,
|
|
620
732
|
boxSizing: "border-box",
|
|
@@ -624,9 +736,15 @@ var chip = {
|
|
|
624
736
|
background: "var(--fonderie-surface,#fff)",
|
|
625
737
|
color: "var(--fonderie-text,#171717)",
|
|
626
738
|
fontSize: 12,
|
|
627
|
-
fontFamily:
|
|
739
|
+
fontFamily: MONO,
|
|
628
740
|
cursor: "pointer"
|
|
629
741
|
};
|
|
742
|
+
var chipBuiltIn = {
|
|
743
|
+
borderStyle: "dashed",
|
|
744
|
+
background: "transparent",
|
|
745
|
+
color: "var(--fonderie-text-muted,#5c5c5c)"
|
|
746
|
+
};
|
|
747
|
+
var chipOff = { opacity: 0.5, textDecoration: "line-through" };
|
|
630
748
|
var styles2 = {
|
|
631
749
|
container: { padding: "32px 40px 64px", maxWidth: 1160, boxSizing: "border-box" },
|
|
632
750
|
title: {
|
|
@@ -637,6 +755,7 @@ var styles2 = {
|
|
|
637
755
|
lineHeight: 1.25
|
|
638
756
|
},
|
|
639
757
|
hint: { fontSize: 13.5, color: "var(--fonderie-text-muted,#5c5c5c)", margin: "4px 0 24px" },
|
|
758
|
+
legend: { fontSize: 12.5, color: "var(--fonderie-text-muted,#5c5c5c)", margin: "10px 2px 0" },
|
|
640
759
|
status: { padding: "24px 0", color: "var(--fonderie-text-muted,#5c5c5c)", fontSize: 13.5 },
|
|
641
760
|
error: { color: "var(--fonderie-danger,#e00)", marginBottom: 12, fontSize: 14 },
|
|
642
761
|
list: {
|
|
@@ -649,20 +768,20 @@ var styles2 = {
|
|
|
649
768
|
overflow: "hidden",
|
|
650
769
|
boxShadow: "var(--fonderie-shadow-card,0 2px 3px 0 rgba(0,0,0,.05))"
|
|
651
770
|
},
|
|
771
|
+
// name + badge | languages | status — fixed tracks so every column aligns.
|
|
652
772
|
row: {
|
|
653
|
-
display: "
|
|
773
|
+
display: "grid",
|
|
774
|
+
gridTemplateColumns: "minmax(0, 1fr) minmax(180px, 280px) 88px",
|
|
654
775
|
alignItems: "center",
|
|
655
|
-
gap:
|
|
776
|
+
gap: 16,
|
|
656
777
|
paddingRight: 16,
|
|
657
778
|
borderBottom: "1px solid var(--fonderie-border-light,#f5f5f5)"
|
|
658
779
|
},
|
|
659
780
|
rowButton: {
|
|
660
|
-
flex: 1,
|
|
661
781
|
minWidth: 0,
|
|
662
782
|
display: "flex",
|
|
663
|
-
justifyContent: "space-between",
|
|
664
783
|
alignItems: "center",
|
|
665
|
-
gap:
|
|
784
|
+
gap: 12,
|
|
666
785
|
padding: "13px 16px",
|
|
667
786
|
background: "none",
|
|
668
787
|
border: "none",
|
|
@@ -674,12 +793,12 @@ var styles2 = {
|
|
|
674
793
|
type: {
|
|
675
794
|
fontSize: 13,
|
|
676
795
|
fontWeight: 600,
|
|
677
|
-
|
|
678
|
-
|
|
796
|
+
overflow: "hidden",
|
|
797
|
+
textOverflow: "ellipsis",
|
|
798
|
+
whiteSpace: "nowrap",
|
|
799
|
+
fontFamily: MONO
|
|
679
800
|
},
|
|
680
|
-
chips: { display: "flex", flexWrap: "wrap",
|
|
681
|
-
chip,
|
|
682
|
-
chipInactive: { ...chip, opacity: 0.5, textDecoration: "line-through" },
|
|
801
|
+
chips: { display: "flex", flexWrap: "wrap", gap: 6 },
|
|
683
802
|
dot: { width: 6, height: 6, borderRadius: 999, background: "currentColor" },
|
|
684
803
|
builtIn: {
|
|
685
804
|
fontSize: 11,
|
|
@@ -687,7 +806,8 @@ var styles2 = {
|
|
|
687
806
|
padding: "1px 7px",
|
|
688
807
|
borderRadius: 999,
|
|
689
808
|
border: "1px solid var(--fonderie-border,#e0e0e0)",
|
|
690
|
-
color: "var(--fonderie-text-muted,#5c5c5c)"
|
|
809
|
+
color: "var(--fonderie-text-muted,#5c5c5c)",
|
|
810
|
+
flexShrink: 0
|
|
691
811
|
},
|
|
692
812
|
newButton: {
|
|
693
813
|
display: "inline-flex",
|
|
@@ -718,14 +838,18 @@ var styles2 = {
|
|
|
718
838
|
|
|
719
839
|
// src/screens/TemplateCreateScreen.tsx
|
|
720
840
|
var import_client3 = require("@fonderie/client");
|
|
841
|
+
var import_react_courier_admin3 = require("@fonderie/react-courier-admin");
|
|
721
842
|
var import_react2 = require("react");
|
|
722
843
|
var import_jsx_runtime3 = require("react/jsx-runtime");
|
|
723
844
|
var TYPE_RE = /^[a-z0-9][a-z0-9_-]{0,80}$/;
|
|
724
845
|
var LOCALE_RE = /^[a-z]{2,3}(-[A-Za-z0-9]{2,8})*$/;
|
|
846
|
+
var IMPLICIT2 = /* @__PURE__ */ new Set(["subject", "preheader", "brandName"]);
|
|
847
|
+
var PREVIEW_DEBOUNCE_MS2 = 500;
|
|
725
848
|
function TemplateCreateScreen({
|
|
726
849
|
client,
|
|
727
850
|
type,
|
|
728
851
|
locales = [],
|
|
852
|
+
defaultLocale,
|
|
729
853
|
onCreated,
|
|
730
854
|
locale
|
|
731
855
|
}) {
|
|
@@ -738,14 +862,48 @@ function TemplateCreateScreen({
|
|
|
738
862
|
const [text, setText] = (0, import_react2.useState)("");
|
|
739
863
|
const [error, setError] = (0, import_react2.useState)(null);
|
|
740
864
|
const [busy, setBusy] = (0, import_react2.useState)(false);
|
|
865
|
+
const [source, setSource] = (0, import_react2.useState)(
|
|
866
|
+
null
|
|
867
|
+
);
|
|
868
|
+
const { preview, isPreviewing, error: previewError, renderPreview } = (0, import_react_courier_admin3.useTemplatePreview)(client);
|
|
869
|
+
const variables = (0, import_react2.useRef)([]);
|
|
741
870
|
(0, import_react2.useEffect)(() => {
|
|
742
871
|
if (!type) return;
|
|
743
|
-
client.getTemplate(type, null).then(({ result }) => {
|
|
872
|
+
client.getTemplate(type, null).catch(() => client.getBuiltInTemplate(type, null)).then(({ result }) => {
|
|
744
873
|
setSubject(result.subject ?? "");
|
|
745
874
|
setHtml(result.html ?? "");
|
|
746
875
|
setText(result.text);
|
|
876
|
+
setSource({ subject: result.subject ?? "", html: result.html ?? "", text: result.text });
|
|
747
877
|
}).catch(() => void 0);
|
|
748
878
|
}, [client, type]);
|
|
879
|
+
const previewType = addingLocale ? type : newType.trim();
|
|
880
|
+
const previewLocale = LOCALE_RE.test(targetLocale.trim()) ? targetLocale.trim() : null;
|
|
881
|
+
(0, import_react2.useEffect)(() => {
|
|
882
|
+
if (!TYPE_RE.test(previewType) || !text.trim()) return;
|
|
883
|
+
const timer = setTimeout(async () => {
|
|
884
|
+
const input = { text, ...subject ? { subject } : {}, ...html ? { html } : {} };
|
|
885
|
+
const render = () => renderPreview(
|
|
886
|
+
previewType,
|
|
887
|
+
{ ...input, data: Object.fromEntries(variables.current.map((v) => [v, v])) },
|
|
888
|
+
previewLocale
|
|
889
|
+
);
|
|
890
|
+
try {
|
|
891
|
+
const result = await render();
|
|
892
|
+
const used = result.variables.filter((v) => !IMPLICIT2.has(v));
|
|
893
|
+
if (used.some((v) => !variables.current.includes(v))) {
|
|
894
|
+
variables.current = used;
|
|
895
|
+
await render();
|
|
896
|
+
}
|
|
897
|
+
} catch {
|
|
898
|
+
}
|
|
899
|
+
}, PREVIEW_DEBOUNCE_MS2);
|
|
900
|
+
return () => clearTimeout(timer);
|
|
901
|
+
}, [renderPreview, previewType, previewLocale, subject, html, text]);
|
|
902
|
+
const untranslated = source ? [
|
|
903
|
+
subject && subject === source.subject ? tr("templates.subject") : null,
|
|
904
|
+
html && html === source.html ? tr("templates.htmlBody") : null,
|
|
905
|
+
text === source.text ? tr("templates.textBody") : null
|
|
906
|
+
].filter((f) => f !== null) : [];
|
|
749
907
|
const submit = async (e) => {
|
|
750
908
|
e.preventDefault();
|
|
751
909
|
setError(null);
|
|
@@ -754,6 +912,9 @@ function TemplateCreateScreen({
|
|
|
754
912
|
if (!TYPE_RE.test(t)) return setError(tr("templates.create.errorType"));
|
|
755
913
|
if (addingLocale && !l) return setError(tr("templates.create.errorLocaleRequired"));
|
|
756
914
|
if (l && !LOCALE_RE.test(l)) return setError(tr("templates.create.errorLocale"));
|
|
915
|
+
if (l && defaultLocale && l.toLowerCase() === defaultLocale.toLowerCase()) {
|
|
916
|
+
return setError(tr("templates.create.errorDefaultLocale", { locale: defaultLocale }));
|
|
917
|
+
}
|
|
757
918
|
if (!text.trim()) return setError(tr("templates.create.errorText"));
|
|
758
919
|
setBusy(true);
|
|
759
920
|
try {
|
|
@@ -781,75 +942,140 @@ function TemplateCreateScreen({
|
|
|
781
942
|
return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: styles3.container, children: [
|
|
782
943
|
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("h1", { style: styles3.title, children: addingLocale ? tr("templates.create.titleLocale", { type }) : tr("templates.create.titleNew") }),
|
|
783
944
|
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { style: styles3.meta, children: addingLocale ? tr("templates.create.hintLocale") : tr("templates.create.hintNew") }),
|
|
784
|
-
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("
|
|
785
|
-
|
|
786
|
-
/* @__PURE__ */ (0, import_jsx_runtime3.
|
|
945
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: styles3.split, children: [
|
|
946
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("form", { style: styles3.form, onSubmit: (e) => void submit(e), children: [
|
|
947
|
+
!addingLocale ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_jsx_runtime3.Fragment, { children: [
|
|
948
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("label", { style: styles3.label, htmlFor: "new-template-type", children: tr("templates.create.type") }),
|
|
949
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
950
|
+
"input",
|
|
951
|
+
{
|
|
952
|
+
id: "new-template-type",
|
|
953
|
+
style: styles3.input,
|
|
954
|
+
value: newType,
|
|
955
|
+
placeholder: "weekly-digest",
|
|
956
|
+
autoComplete: "off",
|
|
957
|
+
spellCheck: false,
|
|
958
|
+
onChange: (e) => setNewType(e.target.value)
|
|
959
|
+
}
|
|
960
|
+
)
|
|
961
|
+
] }) : null,
|
|
962
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("label", { style: styles3.label, htmlFor: "new-template-locale", children: addingLocale ? tr("templates.create.locale") : tr("templates.create.localeOptional") }),
|
|
787
963
|
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
788
964
|
"input",
|
|
789
965
|
{
|
|
790
|
-
id: "new-template-
|
|
966
|
+
id: "new-template-locale",
|
|
791
967
|
style: styles3.input,
|
|
792
|
-
value:
|
|
793
|
-
placeholder: "
|
|
968
|
+
value: targetLocale,
|
|
969
|
+
placeholder: "fr",
|
|
970
|
+
list: "new-template-locales",
|
|
794
971
|
autoComplete: "off",
|
|
795
972
|
spellCheck: false,
|
|
796
|
-
onChange: (e) =>
|
|
973
|
+
onChange: (e) => setTargetLocale(e.target.value)
|
|
797
974
|
}
|
|
798
|
-
)
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
),
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
975
|
+
),
|
|
976
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("datalist", { id: "new-template-locales", children: [...new Set(locales)].map((l) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("option", { value: l }, l)) }),
|
|
977
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("label", { style: styles3.label, htmlFor: "new-template-subject", children: tr("templates.subject") }),
|
|
978
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
979
|
+
"input",
|
|
980
|
+
{
|
|
981
|
+
id: "new-template-subject",
|
|
982
|
+
style: styles3.input,
|
|
983
|
+
value: subject,
|
|
984
|
+
onChange: (e) => setSubject(e.target.value)
|
|
985
|
+
}
|
|
986
|
+
),
|
|
987
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("label", { style: styles3.label, htmlFor: "new-template-html", children: tr("templates.htmlBody") }),
|
|
988
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
989
|
+
"textarea",
|
|
990
|
+
{
|
|
991
|
+
id: "new-template-html",
|
|
992
|
+
style: { ...styles3.textarea, minHeight: 160 },
|
|
993
|
+
value: html,
|
|
994
|
+
onChange: (e) => setHtml(e.target.value)
|
|
995
|
+
}
|
|
996
|
+
),
|
|
997
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("label", { style: styles3.label, htmlFor: "new-template-text", children: tr("templates.textBody") }),
|
|
998
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
999
|
+
"textarea",
|
|
1000
|
+
{
|
|
1001
|
+
id: "new-template-text",
|
|
1002
|
+
style: { ...styles3.textarea, minHeight: 100 },
|
|
1003
|
+
value: text,
|
|
1004
|
+
onChange: (e) => setText(e.target.value)
|
|
1005
|
+
}
|
|
1006
|
+
),
|
|
1007
|
+
untranslated.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { style: styles3.notice, children: tr("templates.create.untranslated", { fields: untranslated.join(", ") }) }) : null,
|
|
1008
|
+
error ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { style: styles3.error, role: "alert", children: error }) : null,
|
|
1009
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { type: "submit", disabled: busy, style: styles3.button, children: busy ? tr("templates.create.creating") : addingLocale ? tr("templates.create.submitLocale") : tr("templates.create.submitNew") })
|
|
1010
|
+
] }),
|
|
1011
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: styles3.previewColumn, children: [
|
|
1012
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: styles3.previewHeader, children: [
|
|
1013
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { style: styles3.label, children: tr("templates.editor.preview") }),
|
|
1014
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { style: styles3.meta, children: isPreviewing ? tr("templates.editor.rendering") : tr("templates.editor.live") })
|
|
1015
|
+
] }),
|
|
1016
|
+
previewError ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { style: styles3.error, role: "alert", children: previewError.explanation }) : null,
|
|
1017
|
+
preview?.subject ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { style: styles3.previewSubject, children: preview.subject }) : null,
|
|
1018
|
+
preview?.html ? (
|
|
1019
|
+
// sandbox="" — no scripts, no same-origin: operator-authored HTML must
|
|
1020
|
+
// never execute in the console's origin.
|
|
1021
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
1022
|
+
"iframe",
|
|
1023
|
+
{
|
|
1024
|
+
title: tr("templates.editor.previewTitle"),
|
|
1025
|
+
style: styles3.previewFrame,
|
|
1026
|
+
sandbox: "",
|
|
1027
|
+
srcDoc: preview.html
|
|
1028
|
+
}
|
|
1029
|
+
)
|
|
1030
|
+
) : preview ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("pre", { style: styles3.previewText, children: preview.text }) : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { style: styles3.meta, children: tr("templates.editor.nothingRendered") })
|
|
1031
|
+
] })
|
|
847
1032
|
] })
|
|
848
1033
|
] });
|
|
849
1034
|
}
|
|
850
|
-
var
|
|
1035
|
+
var MONO2 = 'var(--fonderie-mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace)';
|
|
851
1036
|
var styles3 = {
|
|
852
|
-
container: { padding: "8px 40px 64px", maxWidth:
|
|
1037
|
+
container: { padding: "8px 40px 64px", maxWidth: 1400, boxSizing: "border-box" },
|
|
1038
|
+
// The editor's layout: the form on the left, the render pinned on the right.
|
|
1039
|
+
split: {
|
|
1040
|
+
display: "grid",
|
|
1041
|
+
gridTemplateColumns: "minmax(0, 1fr) minmax(0, 1fr)",
|
|
1042
|
+
gap: 24,
|
|
1043
|
+
alignItems: "start"
|
|
1044
|
+
},
|
|
1045
|
+
previewColumn: {
|
|
1046
|
+
minWidth: 0,
|
|
1047
|
+
display: "flex",
|
|
1048
|
+
flexDirection: "column",
|
|
1049
|
+
position: "sticky",
|
|
1050
|
+
top: 16
|
|
1051
|
+
},
|
|
1052
|
+
previewHeader: { display: "flex", justifyContent: "space-between", alignItems: "center" },
|
|
1053
|
+
previewSubject: { fontSize: 14, fontWeight: 600, margin: "8px 0" },
|
|
1054
|
+
previewFrame: {
|
|
1055
|
+
boxShadow: "var(--fonderie-shadow-card,0 2px 3px 0 rgba(0,0,0,.05))",
|
|
1056
|
+
width: "100%",
|
|
1057
|
+
height: 520,
|
|
1058
|
+
border: "1px solid var(--fonderie-border,#e0e0e0)",
|
|
1059
|
+
borderRadius: 8,
|
|
1060
|
+
background: "var(--fonderie-surface,#fff)"
|
|
1061
|
+
},
|
|
1062
|
+
previewText: {
|
|
1063
|
+
whiteSpace: "pre-wrap",
|
|
1064
|
+
border: "1px solid var(--fonderie-border,#e0e0e0)",
|
|
1065
|
+
borderRadius: 8,
|
|
1066
|
+
padding: 12,
|
|
1067
|
+
fontSize: 13,
|
|
1068
|
+
fontFamily: MONO2
|
|
1069
|
+
},
|
|
1070
|
+
// The console's warning token (the shell's amber), readable in both themes.
|
|
1071
|
+
notice: {
|
|
1072
|
+
fontSize: 13,
|
|
1073
|
+
color: "var(--fonderie-text,#171717)",
|
|
1074
|
+
background: "color-mix(in srgb, var(--fonderie-warning,#f5a623) 16%, transparent)",
|
|
1075
|
+
borderRadius: 6,
|
|
1076
|
+
padding: "8px 12px",
|
|
1077
|
+
margin: "12px 0 0"
|
|
1078
|
+
},
|
|
853
1079
|
title: {
|
|
854
1080
|
fontSize: 22,
|
|
855
1081
|
fontWeight: 600,
|
|
@@ -892,7 +1118,7 @@ var styles3 = {
|
|
|
892
1118
|
lineHeight: 1.55,
|
|
893
1119
|
background: "var(--fonderie-surface,#fff)",
|
|
894
1120
|
color: "var(--fonderie-text,#171717)",
|
|
895
|
-
fontFamily:
|
|
1121
|
+
fontFamily: MONO2
|
|
896
1122
|
},
|
|
897
1123
|
error: {
|
|
898
1124
|
color: "var(--fonderie-danger,#e00)",
|