@fonderie/vue-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 +380 -146
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +76 -56
- package/dist/index.d.ts +76 -56
- package/dist/index.js +388 -151
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -28,6 +28,7 @@ module.exports = __toCommonJS(index_exports);
|
|
|
28
28
|
|
|
29
29
|
// src/screens/TemplateCreateScreen.ts
|
|
30
30
|
var import_client = require("@fonderie/client");
|
|
31
|
+
var import_vue_courier_admin = require("@fonderie/vue-courier-admin");
|
|
31
32
|
var import_vue = require("vue");
|
|
32
33
|
|
|
33
34
|
// src/styles.ts
|
|
@@ -269,6 +270,16 @@ var styles = {
|
|
|
269
270
|
// src/screens/TemplateCreateScreen.ts
|
|
270
271
|
var TYPE_RE = /^[a-z0-9][a-z0-9_-]{0,80}$/;
|
|
271
272
|
var LOCALE_RE = /^[a-z]{2,3}(-[A-Za-z0-9]{2,8})*$/;
|
|
273
|
+
var IMPLICIT = /* @__PURE__ */ new Set(["subject", "preheader", "brandName"]);
|
|
274
|
+
var PREVIEW_DEBOUNCE_MS = 500;
|
|
275
|
+
var notice = {
|
|
276
|
+
fontSize: "13px",
|
|
277
|
+
color: "var(--fonderie-text,#171717)",
|
|
278
|
+
background: "color-mix(in srgb, var(--fonderie-warning,#f5a623) 16%, transparent)",
|
|
279
|
+
borderRadius: "6px",
|
|
280
|
+
padding: "8px 12px",
|
|
281
|
+
margin: "12px 0 0"
|
|
282
|
+
};
|
|
272
283
|
var TemplateCreateScreen = (0, import_vue.defineComponent)({
|
|
273
284
|
name: "FonderieTemplateCreateScreen",
|
|
274
285
|
props: {
|
|
@@ -281,6 +292,8 @@ var TemplateCreateScreen = (0, import_vue.defineComponent)({
|
|
|
281
292
|
type: { type: String, default: void 0 },
|
|
282
293
|
/** Locales already in use, offered as suggestions. */
|
|
283
294
|
locales: { type: Array, default: () => [] },
|
|
295
|
+
/** The system locale: the default version already is it, so adding it is refused. */
|
|
296
|
+
defaultLocale: { type: String, default: void 0 },
|
|
284
297
|
/** The console language; defaults to English. */
|
|
285
298
|
locale: { type: String, default: void 0 }
|
|
286
299
|
},
|
|
@@ -297,15 +310,65 @@ var TemplateCreateScreen = (0, import_vue.defineComponent)({
|
|
|
297
310
|
const text = (0, import_vue.ref)("");
|
|
298
311
|
const error = (0, import_vue.ref)(null);
|
|
299
312
|
const busy = (0, import_vue.ref)(false);
|
|
313
|
+
const source = (0, import_vue.ref)(null);
|
|
314
|
+
const {
|
|
315
|
+
preview,
|
|
316
|
+
isPreviewing,
|
|
317
|
+
error: previewError,
|
|
318
|
+
renderPreview
|
|
319
|
+
} = (0, import_vue_courier_admin.useTemplatePreview)(props.client);
|
|
320
|
+
let variables = [];
|
|
300
321
|
const tr = (key, params) => (0, import_client.createAdminT)(props.locale)(key, params);
|
|
301
322
|
(0, import_vue.onMounted)(() => {
|
|
302
323
|
if (!props.type) return;
|
|
303
|
-
props.client.getTemplate(props.type, null).then(({ result }) => {
|
|
324
|
+
props.client.getTemplate(props.type, null).catch(() => props.client.getBuiltInTemplate(props.type, null)).then(({ result }) => {
|
|
304
325
|
subject.value = result.subject ?? "";
|
|
305
326
|
html.value = result.html ?? "";
|
|
306
327
|
text.value = result.text;
|
|
328
|
+
source.value = {
|
|
329
|
+
subject: result.subject ?? "",
|
|
330
|
+
html: result.html ?? "",
|
|
331
|
+
text: result.text
|
|
332
|
+
};
|
|
307
333
|
}).catch(() => void 0);
|
|
308
334
|
});
|
|
335
|
+
let timer;
|
|
336
|
+
(0, import_vue.watch)([newType, targetLocale, subject, html, text], () => {
|
|
337
|
+
clearTimeout(timer);
|
|
338
|
+
const type = addingLocale ? props.type : newType.value.trim();
|
|
339
|
+
const l = targetLocale.value.trim();
|
|
340
|
+
if (!TYPE_RE.test(type) || !text.value.trim()) return;
|
|
341
|
+
timer = setTimeout(async () => {
|
|
342
|
+
const input = {
|
|
343
|
+
text: text.value,
|
|
344
|
+
...subject.value ? { subject: subject.value } : {},
|
|
345
|
+
...html.value ? { html: html.value } : {}
|
|
346
|
+
};
|
|
347
|
+
const render = () => renderPreview(
|
|
348
|
+
type,
|
|
349
|
+
{ ...input, data: Object.fromEntries(variables.map((v) => [v, v])) },
|
|
350
|
+
LOCALE_RE.test(l) ? l : null
|
|
351
|
+
);
|
|
352
|
+
try {
|
|
353
|
+
const result = await render();
|
|
354
|
+
const used = result.variables.filter((v) => !IMPLICIT.has(v));
|
|
355
|
+
if (used.some((v) => !variables.includes(v))) {
|
|
356
|
+
variables = used;
|
|
357
|
+
await render();
|
|
358
|
+
}
|
|
359
|
+
} catch {
|
|
360
|
+
}
|
|
361
|
+
}, PREVIEW_DEBOUNCE_MS);
|
|
362
|
+
});
|
|
363
|
+
const untranslated = (0, import_vue.computed)(() => {
|
|
364
|
+
const src = source.value;
|
|
365
|
+
if (!src) return [];
|
|
366
|
+
return [
|
|
367
|
+
subject.value && subject.value === src.subject ? tr("templates.subject") : null,
|
|
368
|
+
html.value && html.value === src.html ? tr("templates.htmlBody") : null,
|
|
369
|
+
text.value === src.text ? tr("templates.textBody") : null
|
|
370
|
+
].filter((f) => f !== null);
|
|
371
|
+
});
|
|
309
372
|
async function submit(e) {
|
|
310
373
|
e.preventDefault();
|
|
311
374
|
error.value = null;
|
|
@@ -323,6 +386,10 @@ var TemplateCreateScreen = (0, import_vue.defineComponent)({
|
|
|
323
386
|
error.value = tr("templates.create.errorLocale");
|
|
324
387
|
return;
|
|
325
388
|
}
|
|
389
|
+
if (l && props.defaultLocale && l.toLowerCase() === props.defaultLocale.toLowerCase()) {
|
|
390
|
+
error.value = tr("templates.create.errorDefaultLocale", { locale: props.defaultLocale });
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
326
393
|
if (!text.value.trim()) {
|
|
327
394
|
error.value = tr("templates.create.errorText");
|
|
328
395
|
return;
|
|
@@ -363,7 +430,7 @@ var TemplateCreateScreen = (0, import_vue.defineComponent)({
|
|
|
363
430
|
}
|
|
364
431
|
})
|
|
365
432
|
];
|
|
366
|
-
return () => (0, import_vue.h)("div", { style:
|
|
433
|
+
return () => (0, import_vue.h)("div", { style: styles.container }, [
|
|
367
434
|
(0, import_vue.h)(
|
|
368
435
|
"h1",
|
|
369
436
|
{ style: styles.title },
|
|
@@ -374,35 +441,69 @@ var TemplateCreateScreen = (0, import_vue.defineComponent)({
|
|
|
374
441
|
{ style: { ...styles.meta, lineHeight: 1.5 } },
|
|
375
442
|
addingLocale ? tr("templates.create.hintLocale") : tr("templates.create.hintNew")
|
|
376
443
|
),
|
|
377
|
-
(0, import_vue.h)("
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
"
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
444
|
+
(0, import_vue.h)("div", { style: styles.split }, [
|
|
445
|
+
(0, import_vue.h)("form", { style: styles.form, onSubmit: (ev) => void submit(ev) }, [
|
|
446
|
+
...addingLocale ? [] : field("new-template-type", tr("templates.create.type"), newType, {
|
|
447
|
+
placeholder: "weekly-digest"
|
|
448
|
+
}),
|
|
449
|
+
...field(
|
|
450
|
+
"new-template-locale",
|
|
451
|
+
addingLocale ? tr("templates.create.locale") : tr("templates.create.localeOptional"),
|
|
452
|
+
targetLocale,
|
|
453
|
+
{ placeholder: "fr", list: "new-template-locales" }
|
|
454
|
+
),
|
|
455
|
+
(0, import_vue.h)(
|
|
456
|
+
"datalist",
|
|
457
|
+
{ id: "new-template-locales" },
|
|
458
|
+
[...new Set(props.locales)].map((l) => (0, import_vue.h)("option", { key: l, value: l }))
|
|
459
|
+
),
|
|
460
|
+
...field("new-template-subject", tr("templates.subject"), subject),
|
|
461
|
+
...field("new-template-html", tr("templates.htmlBody"), html, {
|
|
462
|
+
textarea: true,
|
|
463
|
+
minHeight: "160px"
|
|
464
|
+
}),
|
|
465
|
+
...field("new-template-text", tr("templates.textBody"), text, {
|
|
466
|
+
textarea: true,
|
|
467
|
+
minHeight: "100px"
|
|
468
|
+
}),
|
|
469
|
+
untranslated.value.length > 0 ? (0, import_vue.h)(
|
|
470
|
+
"p",
|
|
471
|
+
{ style: notice },
|
|
472
|
+
tr("templates.create.untranslated", { fields: untranslated.value.join(", ") })
|
|
473
|
+
) : null,
|
|
474
|
+
error.value ? (0, import_vue.h)("p", { style: styles.error, role: "alert" }, error.value) : null,
|
|
475
|
+
(0, import_vue.h)(
|
|
476
|
+
"button",
|
|
477
|
+
{
|
|
478
|
+
type: "submit",
|
|
479
|
+
disabled: busy.value,
|
|
480
|
+
style: { ...styles.button, marginTop: "16px" }
|
|
481
|
+
},
|
|
482
|
+
busy.value ? tr("templates.create.creating") : addingLocale ? tr("templates.create.submitLocale") : tr("templates.create.submitNew")
|
|
483
|
+
)
|
|
484
|
+
]),
|
|
485
|
+
(0, import_vue.h)("div", { style: styles.previewColumn }, [
|
|
486
|
+
(0, import_vue.h)("div", { style: styles.previewHeader }, [
|
|
487
|
+
(0, import_vue.h)("span", { style: styles.label }, tr("templates.editor.preview")),
|
|
488
|
+
(0, import_vue.h)(
|
|
489
|
+
"span",
|
|
490
|
+
{ style: styles.meta },
|
|
491
|
+
isPreviewing.value ? tr("templates.editor.rendering") : tr("templates.editor.live")
|
|
492
|
+
)
|
|
493
|
+
]),
|
|
494
|
+
previewError.value ? (0, import_vue.h)("p", { style: styles.error, role: "alert" }, previewError.value.explanation) : null,
|
|
495
|
+
preview.value?.subject ? (0, import_vue.h)("p", { style: styles.previewSubject }, preview.value.subject) : null,
|
|
496
|
+
preview.value?.html ? (
|
|
497
|
+
// sandbox= — no scripts, no same-origin: operator-authored HTML must
|
|
498
|
+
// never execute in the console's origin.
|
|
499
|
+
(0, import_vue.h)("iframe", {
|
|
500
|
+
title: tr("templates.editor.previewTitle"),
|
|
501
|
+
style: styles.previewFrame,
|
|
502
|
+
sandbox: "",
|
|
503
|
+
srcdoc: preview.value.html
|
|
504
|
+
})
|
|
505
|
+
) : preview.value ? (0, import_vue.h)("pre", { style: styles.previewText }, preview.value.text) : (0, import_vue.h)("p", { style: styles.meta }, tr("templates.editor.nothingRendered"))
|
|
506
|
+
])
|
|
406
507
|
])
|
|
407
508
|
]);
|
|
408
509
|
}
|
|
@@ -410,10 +511,10 @@ var TemplateCreateScreen = (0, import_vue.defineComponent)({
|
|
|
410
511
|
|
|
411
512
|
// src/screens/TemplateEditorScreen.ts
|
|
412
513
|
var import_client2 = require("@fonderie/client");
|
|
413
|
-
var
|
|
514
|
+
var import_vue_courier_admin2 = require("@fonderie/vue-courier-admin");
|
|
414
515
|
var import_vue2 = require("vue");
|
|
415
|
-
var
|
|
416
|
-
var
|
|
516
|
+
var PREVIEW_DEBOUNCE_MS2 = 500;
|
|
517
|
+
var IMPLICIT2 = /* @__PURE__ */ new Set(["subject", "preheader", "brandName"]);
|
|
417
518
|
var tab = {
|
|
418
519
|
height: "32px",
|
|
419
520
|
padding: "0 10px",
|
|
@@ -432,7 +533,8 @@ var tabSelected = {
|
|
|
432
533
|
borderBottomColor: "var(--fonderie-text,#171717)",
|
|
433
534
|
cursor: "default"
|
|
434
535
|
};
|
|
435
|
-
var tabInactive = {
|
|
536
|
+
var tabInactive = { opacity: 0.55, textDecoration: "line-through" };
|
|
537
|
+
var tabBuiltIn = { fontStyle: "italic" };
|
|
436
538
|
var tabBar = {
|
|
437
539
|
display: "flex",
|
|
438
540
|
alignItems: "flex-end",
|
|
@@ -477,36 +579,53 @@ var TemplateEditorScreen = (0, import_vue2.defineComponent)({
|
|
|
477
579
|
},
|
|
478
580
|
setup(props, { emit }) {
|
|
479
581
|
const t = (key, params) => (0, import_client2.createAdminT)(props.uiLocale)(key, params);
|
|
480
|
-
const { template, isLoading, error, refresh } = (0,
|
|
582
|
+
const { template, isLoading, error, refresh } = (0, import_vue_courier_admin2.useTemplate)(
|
|
481
583
|
props.client,
|
|
482
584
|
props.type,
|
|
483
585
|
props.locale
|
|
484
586
|
);
|
|
485
|
-
const {
|
|
486
|
-
const
|
|
487
|
-
|
|
587
|
+
const { saveTemplate, removeTemplate } = (0, import_vue_courier_admin2.useTemplates)(props.client);
|
|
588
|
+
const notSaved = (0, import_vue2.computed)(() => error.value?.status === 404);
|
|
589
|
+
const { builtIn: builtIn2, isLoading: builtInLoading } = (0, import_vue_courier_admin2.useBuiltInTemplate)(
|
|
590
|
+
props.client,
|
|
591
|
+
props.type,
|
|
592
|
+
props.locale
|
|
488
593
|
);
|
|
594
|
+
const { catalog, refresh: refreshCatalog } = (0, import_vue_courier_admin2.useTemplateCatalog)(props.client);
|
|
595
|
+
const defaultLocale = (0, import_vue2.computed)(() => catalog.value?.defaultLocale ?? "en-US");
|
|
596
|
+
const email = (0, import_vue2.computed)(() => catalog.value?.emails.find((e) => e.type === props.type));
|
|
597
|
+
const languages = (0, import_vue2.computed)(
|
|
598
|
+
() => email.value ? (0, import_client2.templateLanguages)(email.value, defaultLocale.value) : []
|
|
599
|
+
);
|
|
600
|
+
const source = (0, import_vue2.computed)(() => template.value ?? (notSaved.value ? builtIn2.value : null));
|
|
601
|
+
const protectedRow = (0, import_vue2.computed)(() => !props.locale && (email.value?.system ?? props.system));
|
|
602
|
+
const shipsHere = (0, import_vue2.computed)(
|
|
603
|
+
() => props.locale ? languages.value.some((l) => l.locale === props.locale && l.builtIn) : !!email.value?.builtIn.default
|
|
604
|
+
);
|
|
605
|
+
const { resolution, resolve } = (0, import_vue_courier_admin2.useTemplateResolution)(props.client);
|
|
606
|
+
const probe = (0, import_vue2.ref)("");
|
|
607
|
+
(0, import_vue2.watch)(defaultLocale, (dl) => void resolve(props.type, props.locale ?? dl), { immediate: true });
|
|
489
608
|
const isDeleting = (0, import_vue2.ref)(false);
|
|
490
|
-
const { revisions, rollback } = (0,
|
|
609
|
+
const { revisions, rollback } = (0, import_vue_courier_admin2.useTemplateRevisions)(props.client, props.type, props.locale);
|
|
491
610
|
const isSaving = (0, import_vue2.ref)(false);
|
|
492
611
|
const saveError = (0, import_vue2.ref)(null);
|
|
493
612
|
const subject = (0, import_vue2.ref)("");
|
|
494
613
|
const html = (0, import_vue2.ref)("");
|
|
495
614
|
const text = (0, import_vue2.ref)("");
|
|
496
615
|
const active = (0, import_vue2.ref)(true);
|
|
497
|
-
(0, import_vue2.watch)(
|
|
498
|
-
if (!
|
|
499
|
-
subject.value =
|
|
500
|
-
html.value =
|
|
501
|
-
text.value =
|
|
502
|
-
active.value =
|
|
616
|
+
(0, import_vue2.watch)(source, (src) => {
|
|
617
|
+
if (!src) return;
|
|
618
|
+
subject.value = src.subject ?? "";
|
|
619
|
+
html.value = src.html ?? "";
|
|
620
|
+
text.value = src.text;
|
|
621
|
+
active.value = template.value ? template.value.active : true;
|
|
503
622
|
});
|
|
504
623
|
const {
|
|
505
624
|
preview,
|
|
506
625
|
isPreviewing,
|
|
507
626
|
error: previewError,
|
|
508
627
|
renderPreview
|
|
509
|
-
} = (0,
|
|
628
|
+
} = (0, import_vue_courier_admin2.useTemplatePreview)(props.client);
|
|
510
629
|
const sampleJson = (0, import_vue2.ref)("{}");
|
|
511
630
|
const sampleError = (0, import_vue2.ref)(null);
|
|
512
631
|
async function run() {
|
|
@@ -533,7 +652,7 @@ var TemplateEditorScreen = (0, import_vue2.defineComponent)({
|
|
|
533
652
|
},
|
|
534
653
|
props.locale
|
|
535
654
|
);
|
|
536
|
-
const missing = result.variables.filter((v) => !
|
|
655
|
+
const missing = result.variables.filter((v) => !IMPLICIT2.has(v) && !(v in data));
|
|
537
656
|
if (missing.length > 0) {
|
|
538
657
|
sampleJson.value = JSON.stringify(
|
|
539
658
|
{ ...data, ...Object.fromEntries(missing.map((v) => [v, v])) },
|
|
@@ -544,13 +663,13 @@ var TemplateEditorScreen = (0, import_vue2.defineComponent)({
|
|
|
544
663
|
}
|
|
545
664
|
let previewTimer;
|
|
546
665
|
(0, import_vue2.watch)([template, subject, html, text, sampleJson], () => {
|
|
547
|
-
if (!
|
|
666
|
+
if (!source.value) return;
|
|
548
667
|
clearTimeout(previewTimer);
|
|
549
|
-
previewTimer = setTimeout(() => void run(),
|
|
668
|
+
previewTimer = setTimeout(() => void run(), PREVIEW_DEBOUNCE_MS2);
|
|
550
669
|
});
|
|
551
670
|
const dirty = (0, import_vue2.computed)(() => {
|
|
552
671
|
const tpl = template.value;
|
|
553
|
-
return !!tpl && (subject.value !== (tpl.subject ?? "") || html.value !== (tpl.html ?? "") || text.value !== tpl.text || active.value !== tpl.active);
|
|
672
|
+
return notSaved.value && !!builtIn2.value || !!tpl && (subject.value !== (tpl.subject ?? "") || html.value !== (tpl.html ?? "") || text.value !== tpl.text || active.value !== tpl.active);
|
|
554
673
|
});
|
|
555
674
|
async function handleSubmit(event) {
|
|
556
675
|
event.preventDefault();
|
|
@@ -562,7 +681,7 @@ var TemplateEditorScreen = (0, import_vue2.defineComponent)({
|
|
|
562
681
|
if (html.value) input.html = html.value;
|
|
563
682
|
if (template.value?.version !== void 0) input.ifVersion = template.value.version;
|
|
564
683
|
await saveTemplate(props.type, input, props.locale);
|
|
565
|
-
await refresh();
|
|
684
|
+
await Promise.all([refresh(), refreshCatalog()]);
|
|
566
685
|
emit("saved");
|
|
567
686
|
} catch (err) {
|
|
568
687
|
saveError.value = err;
|
|
@@ -570,7 +689,32 @@ var TemplateEditorScreen = (0, import_vue2.defineComponent)({
|
|
|
570
689
|
isSaving.value = false;
|
|
571
690
|
}
|
|
572
691
|
}
|
|
573
|
-
const leave = () => !dirty.value || window.confirm(t("templates.editor.discardChanges"));
|
|
692
|
+
const leave = () => !dirty.value || notSaved.value && !!builtIn2.value || window.confirm(t("templates.editor.discardChanges"));
|
|
693
|
+
async function handleReset() {
|
|
694
|
+
const copy = builtIn2.value;
|
|
695
|
+
if (!copy || !window.confirm(
|
|
696
|
+
t("templates.editor.confirmReset", { locale: props.locale ?? defaultLocale.value })
|
|
697
|
+
))
|
|
698
|
+
return;
|
|
699
|
+
isDeleting.value = true;
|
|
700
|
+
saveError.value = null;
|
|
701
|
+
try {
|
|
702
|
+
if (props.locale) {
|
|
703
|
+
await removeTemplate(props.type, props.locale);
|
|
704
|
+
} else {
|
|
705
|
+
const input = { text: copy.text, active: active.value };
|
|
706
|
+
if (copy.subject) input.subject = copy.subject;
|
|
707
|
+
if (copy.html) input.html = copy.html;
|
|
708
|
+
if (template.value?.version !== void 0) input.ifVersion = template.value.version;
|
|
709
|
+
await saveTemplate(props.type, input, null);
|
|
710
|
+
}
|
|
711
|
+
await Promise.all([refresh(), refreshCatalog()]);
|
|
712
|
+
} catch (err) {
|
|
713
|
+
saveError.value = err;
|
|
714
|
+
} finally {
|
|
715
|
+
isDeleting.value = false;
|
|
716
|
+
}
|
|
717
|
+
}
|
|
574
718
|
function renderTabBar() {
|
|
575
719
|
if (!props.localeTabs && !props.allowAddLocale) return null;
|
|
576
720
|
return (0, import_vue2.h)("div", { style: tabBar }, [
|
|
@@ -580,21 +724,30 @@ var TemplateEditorScreen = (0, import_vue2.defineComponent)({
|
|
|
580
724
|
(0, import_vue2.h)(
|
|
581
725
|
"nav",
|
|
582
726
|
{ "aria-label": t("templates.editor.locales"), style: tabs },
|
|
583
|
-
|
|
584
|
-
const selected =
|
|
727
|
+
languages.value.map((l) => {
|
|
728
|
+
const selected = l.locale === (props.locale ?? null);
|
|
585
729
|
return (0, import_vue2.h)(
|
|
586
730
|
"button",
|
|
587
731
|
{
|
|
588
|
-
key:
|
|
732
|
+
key: l.label,
|
|
589
733
|
type: "button",
|
|
590
734
|
"aria-current": selected ? "true" : void 0,
|
|
591
|
-
style:
|
|
592
|
-
|
|
735
|
+
style: {
|
|
736
|
+
...selected ? tabSelected : tab,
|
|
737
|
+
...l.saved ? {} : tabBuiltIn,
|
|
738
|
+
...l.active ? {} : tabInactive
|
|
739
|
+
},
|
|
740
|
+
title: l.active ? void 0 : t("common.status.inactive"),
|
|
593
741
|
onClick: () => {
|
|
594
|
-
if (!selected && leave())
|
|
742
|
+
if (!selected && leave())
|
|
743
|
+
emit("select-locale", {
|
|
744
|
+
type: props.type,
|
|
745
|
+
locale: l.locale,
|
|
746
|
+
system: !!email.value?.system
|
|
747
|
+
});
|
|
595
748
|
}
|
|
596
749
|
},
|
|
597
|
-
|
|
750
|
+
l.label
|
|
598
751
|
);
|
|
599
752
|
})
|
|
600
753
|
)
|
|
@@ -607,7 +760,8 @@ var TemplateEditorScreen = (0, import_vue2.defineComponent)({
|
|
|
607
760
|
onClick: () => {
|
|
608
761
|
if (leave())
|
|
609
762
|
emit("add-locale", props.type, {
|
|
610
|
-
locales: (0, import_client2.
|
|
763
|
+
locales: catalog.value ? (0, import_client2.suggestTemplateLocales)(catalog.value, props.type) : [],
|
|
764
|
+
defaultLocale: defaultLocale.value
|
|
611
765
|
});
|
|
612
766
|
}
|
|
613
767
|
},
|
|
@@ -643,16 +797,19 @@ var TemplateEditorScreen = (0, import_vue2.defineComponent)({
|
|
|
643
797
|
]);
|
|
644
798
|
}
|
|
645
799
|
return () => {
|
|
646
|
-
if (isLoading.value
|
|
647
|
-
|
|
800
|
+
if (isLoading.value || notSaved.value && builtInLoading.value)
|
|
801
|
+
return (0, import_vue2.h)("p", { style: styles.status }, t("templates.editor.loading"));
|
|
802
|
+
if (error.value && !(notSaved.value && builtIn2.value))
|
|
648
803
|
return (0, import_vue2.h)("p", { style: styles.error, role: "alert" }, error.value.explanation);
|
|
649
804
|
return (0, import_vue2.h)("div", { style: styles.container }, [
|
|
650
805
|
(0, import_vue2.h)("h1", { style: styles.title }, props.type),
|
|
651
806
|
renderTabBar(),
|
|
652
807
|
(0, import_vue2.h)("p", { style: styles.meta }, [
|
|
653
|
-
(0, import_vue2.h)("strong", props.locale ??
|
|
654
|
-
` \xB7 v${template.value
|
|
655
|
-
|
|
808
|
+
(0, import_vue2.h)("strong", props.locale ?? defaultLocale.value),
|
|
809
|
+
template.value ? ` \xB7 v${template.value.version}` : "",
|
|
810
|
+
template.value && protectedRow.value ? ` \xB7 ${t("templates.editor.builtInNote")}` : "",
|
|
811
|
+
!template.value && builtIn2.value ? ` \xB7 ${t("templates.editor.builtInCopy")}` : "",
|
|
812
|
+
resolution.value && resolution.value.chain.length > 1 && resolution.value.requested === (props.locale ?? defaultLocale.value) ? ` \xB7 ${t("templates.editor.chain", { chain: [...resolution.value.chain, defaultLocale.value].join(" \u2192 ") })}` : ""
|
|
656
813
|
]),
|
|
657
814
|
(0, import_vue2.h)("div", { style: styles.split }, [
|
|
658
815
|
(0, import_vue2.h)("div", { style: styles.column }, [
|
|
@@ -723,7 +880,16 @@ var TemplateEditorScreen = (0, import_vue2.defineComponent)({
|
|
|
723
880
|
isSaving.value ? t("common.saving") : t("common.save")
|
|
724
881
|
),
|
|
725
882
|
!dirty.value && !isSaving.value ? (0, import_vue2.h)("span", { style: styles.meta }, t("templates.editor.noChanges")) : null,
|
|
726
|
-
|
|
883
|
+
template.value && shipsHere.value && builtIn2.value ? (0, import_vue2.h)(
|
|
884
|
+
"button",
|
|
885
|
+
{
|
|
886
|
+
type: "button",
|
|
887
|
+
disabled: isDeleting.value,
|
|
888
|
+
onClick: () => void handleReset(),
|
|
889
|
+
style: { ...styles.rollbackButton, height: "36px", marginLeft: "auto" }
|
|
890
|
+
},
|
|
891
|
+
t("templates.editor.resetToBuiltIn")
|
|
892
|
+
) : template.value && !protectedRow.value ? (0, import_vue2.h)(
|
|
727
893
|
"button",
|
|
728
894
|
{
|
|
729
895
|
type: "button",
|
|
@@ -782,6 +948,46 @@ var TemplateEditorScreen = (0, import_vue2.defineComponent)({
|
|
|
782
948
|
) : preview.value ? (0, import_vue2.h)("pre", { style: styles.previewText }, preview.value.text) : (0, import_vue2.h)("p", { style: styles.meta }, t("templates.editor.nothingRendered"))
|
|
783
949
|
])
|
|
784
950
|
]),
|
|
951
|
+
// Who receives what: any locale, answered by the same decision a send makes.
|
|
952
|
+
(0, import_vue2.h)("div", { style: styles.revisions }, [
|
|
953
|
+
(0, import_vue2.h)("h2", { style: styles.subtitle }, t("templates.editor.whoReceives")),
|
|
954
|
+
(0, import_vue2.h)(
|
|
955
|
+
"form",
|
|
956
|
+
{
|
|
957
|
+
style: { display: "flex", gap: "8px", alignItems: "center", flexWrap: "wrap" },
|
|
958
|
+
onSubmit: (event) => {
|
|
959
|
+
event.preventDefault();
|
|
960
|
+
if (probe.value.trim()) void resolve(props.type, probe.value.trim());
|
|
961
|
+
}
|
|
962
|
+
},
|
|
963
|
+
[
|
|
964
|
+
(0, import_vue2.h)("input", {
|
|
965
|
+
"aria-label": t("templates.editor.whoReceives"),
|
|
966
|
+
style: { ...styles.input, width: "200px" },
|
|
967
|
+
value: probe.value,
|
|
968
|
+
placeholder: t("templates.editor.whoPlaceholder"),
|
|
969
|
+
spellcheck: false,
|
|
970
|
+
onInput: (e) => {
|
|
971
|
+
probe.value = e.target.value;
|
|
972
|
+
}
|
|
973
|
+
}),
|
|
974
|
+
(0, import_vue2.h)(
|
|
975
|
+
"button",
|
|
976
|
+
{ type: "submit", style: { ...styles.rollbackButton, height: "36px" } },
|
|
977
|
+
t("templates.editor.check")
|
|
978
|
+
),
|
|
979
|
+
resolution.value && probe.value.trim() ? (0, import_vue2.h)("span", { style: { ...styles.meta, marginBottom: "0" }, role: "status" }, [
|
|
980
|
+
t("templates.editor.receives", {
|
|
981
|
+
requested: resolution.value.requested,
|
|
982
|
+
sent: resolution.value.sent
|
|
983
|
+
}),
|
|
984
|
+
" ",
|
|
985
|
+
resolution.value.source === "saved" ? t("templates.editor.sourceSaved") : t("templates.editor.sourceBuiltIn"),
|
|
986
|
+
resolution.value.chain.length > 0 ? ` (${[...resolution.value.chain, resolution.value.defaultLocale].join(" \u2192 ")})` : ""
|
|
987
|
+
]) : null
|
|
988
|
+
]
|
|
989
|
+
)
|
|
990
|
+
]),
|
|
785
991
|
revisions.value.length > 0 ? (0, import_vue2.h)("div", { style: styles.revisions }, [
|
|
786
992
|
(0, import_vue2.h)("h2", { style: styles.subtitle }, t("templates.editor.history")),
|
|
787
993
|
(0, import_vue2.h)("ul", { style: styles.list }, revisions.value.map(renderRevision))
|
|
@@ -793,15 +999,17 @@ var TemplateEditorScreen = (0, import_vue2.defineComponent)({
|
|
|
793
999
|
|
|
794
1000
|
// src/screens/TemplateListScreen.ts
|
|
795
1001
|
var import_client3 = require("@fonderie/client");
|
|
796
|
-
var
|
|
1002
|
+
var import_vue_courier_admin3 = require("@fonderie/vue-courier-admin");
|
|
797
1003
|
var import_vue3 = require("vue");
|
|
1004
|
+
var MONO = 'var(--fonderie-mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace)';
|
|
798
1005
|
var builtIn = {
|
|
799
1006
|
fontSize: "11px",
|
|
800
1007
|
fontWeight: 500,
|
|
801
1008
|
padding: "1px 7px",
|
|
802
1009
|
borderRadius: "999px",
|
|
803
1010
|
border: "1px solid var(--fonderie-border,#e0e0e0)",
|
|
804
|
-
color: "var(--fonderie-text-muted,#5c5c5c)"
|
|
1011
|
+
color: "var(--fonderie-text-muted,#5c5c5c)",
|
|
1012
|
+
flexShrink: 0
|
|
805
1013
|
};
|
|
806
1014
|
var chip = {
|
|
807
1015
|
height: "24px",
|
|
@@ -812,23 +1020,42 @@ var chip = {
|
|
|
812
1020
|
background: "var(--fonderie-surface,#fff)",
|
|
813
1021
|
color: "var(--fonderie-text,#171717)",
|
|
814
1022
|
fontSize: "12px",
|
|
815
|
-
fontFamily:
|
|
1023
|
+
fontFamily: MONO,
|
|
816
1024
|
cursor: "pointer"
|
|
817
1025
|
};
|
|
818
|
-
var
|
|
1026
|
+
var chipBuiltIn = {
|
|
1027
|
+
borderStyle: "dashed",
|
|
1028
|
+
background: "transparent",
|
|
1029
|
+
color: "var(--fonderie-text-muted,#5c5c5c)"
|
|
1030
|
+
};
|
|
1031
|
+
var chipOff = { opacity: 0.5, textDecoration: "line-through" };
|
|
819
1032
|
var row = {
|
|
820
1033
|
...styles.row,
|
|
821
|
-
display: "
|
|
1034
|
+
display: "grid",
|
|
1035
|
+
gridTemplateColumns: "minmax(0, 1fr) minmax(180px, 280px) 88px",
|
|
822
1036
|
alignItems: "center",
|
|
823
|
-
gap: "
|
|
1037
|
+
gap: "16px",
|
|
824
1038
|
paddingRight: "16px"
|
|
825
1039
|
};
|
|
826
|
-
var rowButton = {
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
justifyContent: "flex-
|
|
831
|
-
gap: "
|
|
1040
|
+
var rowButton = {
|
|
1041
|
+
...styles.rowButton,
|
|
1042
|
+
width: "auto",
|
|
1043
|
+
minWidth: "0",
|
|
1044
|
+
justifyContent: "flex-start",
|
|
1045
|
+
gap: "12px"
|
|
1046
|
+
};
|
|
1047
|
+
var typeName = {
|
|
1048
|
+
...styles.type,
|
|
1049
|
+
flex: "none",
|
|
1050
|
+
overflow: "hidden",
|
|
1051
|
+
textOverflow: "ellipsis",
|
|
1052
|
+
whiteSpace: "nowrap"
|
|
1053
|
+
};
|
|
1054
|
+
var chips = { display: "flex", flexWrap: "wrap", gap: "6px" };
|
|
1055
|
+
var legend = {
|
|
1056
|
+
fontSize: "12.5px",
|
|
1057
|
+
color: "var(--fonderie-text-muted,#5c5c5c)",
|
|
1058
|
+
margin: "10px 2px 0"
|
|
832
1059
|
};
|
|
833
1060
|
var newButton = {
|
|
834
1061
|
display: "inline-flex",
|
|
@@ -856,90 +1083,97 @@ var TemplateListScreen = (0, import_vue3.defineComponent)({
|
|
|
856
1083
|
},
|
|
857
1084
|
emits: {
|
|
858
1085
|
/**
|
|
859
|
-
*
|
|
860
|
-
*
|
|
1086
|
+
* One row per email — built-in ones included, even when nobody saved a
|
|
1087
|
+
* copy. The row opens the default version; a language chip that language.
|
|
861
1088
|
*/
|
|
862
1089
|
"select-template": (_template) => true,
|
|
863
1090
|
/** Receives the locales in use, to suggest. */
|
|
864
1091
|
"create-template": (_context) => true
|
|
865
1092
|
},
|
|
866
1093
|
setup(props, { emit }) {
|
|
867
|
-
const {
|
|
1094
|
+
const { catalog, isLoading, error } = (0, import_vue_courier_admin3.useTemplateCatalog)(props.client);
|
|
868
1095
|
const t = (key, params) => (0, import_client3.createAdminT)(props.locale)(key, params);
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
])
|
|
886
|
-
]
|
|
887
|
-
),
|
|
888
|
-
// Siblings of the row button, not inside it: a button cannot hold buttons.
|
|
1096
|
+
const defaultLocale = () => catalog.value?.defaultLocale ?? "en-US";
|
|
1097
|
+
function renderRow(email) {
|
|
1098
|
+
const languages = (0, import_client3.templateLanguages)(email, defaultLocale());
|
|
1099
|
+
const primary = languages.find((l) => l.locale === null) ?? languages[0];
|
|
1100
|
+
const open = (l) => emit("select-template", {
|
|
1101
|
+
type: email.type,
|
|
1102
|
+
locale: l?.locale ?? null,
|
|
1103
|
+
system: email.system
|
|
1104
|
+
});
|
|
1105
|
+
return (0, import_vue3.h)("li", { key: email.type, style: row }, [
|
|
1106
|
+
(0, import_vue3.h)("button", { type: "button", style: rowButton, onClick: () => open(primary) }, [
|
|
1107
|
+
(0, import_vue3.h)("span", { style: typeName }, email.type),
|
|
1108
|
+
email.system ? (0, import_vue3.h)("span", { style: builtIn }, t("templates.builtInBadge")) : null
|
|
1109
|
+
]),
|
|
1110
|
+
// Siblings of the row button: a button cannot hold buttons. Each chip
|
|
1111
|
+
// names its email and language for screen readers.
|
|
889
1112
|
(0, import_vue3.h)(
|
|
890
1113
|
"span",
|
|
891
1114
|
{ style: chips },
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
return (0, import_vue3.h)(
|
|
1115
|
+
languages.map(
|
|
1116
|
+
(l) => (0, import_vue3.h)(
|
|
895
1117
|
"button",
|
|
896
1118
|
{
|
|
897
|
-
key:
|
|
1119
|
+
key: l.label,
|
|
898
1120
|
type: "button",
|
|
899
|
-
style:
|
|
900
|
-
"aria-label": t("templates.list.openLocale", { type:
|
|
901
|
-
title:
|
|
902
|
-
onClick: () =>
|
|
1121
|
+
style: { ...chip, ...l.saved ? {} : chipBuiltIn, ...l.active ? {} : chipOff },
|
|
1122
|
+
"aria-label": t("templates.list.openLocale", { type: email.type, locale: l.label }),
|
|
1123
|
+
title: l.active ? void 0 : t("common.status.inactive"),
|
|
1124
|
+
onClick: () => open(l)
|
|
903
1125
|
},
|
|
904
|
-
|
|
905
|
-
)
|
|
906
|
-
|
|
907
|
-
)
|
|
1126
|
+
l.label
|
|
1127
|
+
)
|
|
1128
|
+
)
|
|
1129
|
+
),
|
|
1130
|
+
(0, import_vue3.h)("span", { style: primary?.active === false ? styles.inactive : styles.active }, [
|
|
1131
|
+
(0, import_vue3.h)("span", { style: styles.dot }),
|
|
1132
|
+
primary?.active === false ? t("common.status.inactive") : t("common.status.active")
|
|
1133
|
+
])
|
|
908
1134
|
]);
|
|
909
1135
|
}
|
|
910
|
-
return () =>
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
"
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
1136
|
+
return () => {
|
|
1137
|
+
const emails = catalog.value?.emails ?? [];
|
|
1138
|
+
return (0, import_vue3.h)("div", { style: styles.listContainer }, [
|
|
1139
|
+
(0, import_vue3.h)(
|
|
1140
|
+
"div",
|
|
1141
|
+
{
|
|
1142
|
+
style: {
|
|
1143
|
+
display: "flex",
|
|
1144
|
+
alignItems: "center",
|
|
1145
|
+
justifyContent: "space-between",
|
|
1146
|
+
gap: "16px"
|
|
1147
|
+
}
|
|
1148
|
+
},
|
|
1149
|
+
[
|
|
1150
|
+
(0, import_vue3.h)("h1", { style: styles.listTitle }, t("templates.list.title")),
|
|
1151
|
+
props.allowCreate ? (0, import_vue3.h)(
|
|
1152
|
+
"button",
|
|
1153
|
+
{
|
|
1154
|
+
type: "button",
|
|
1155
|
+
style: newButton,
|
|
1156
|
+
onClick: () => emit("create-template", {
|
|
1157
|
+
locales: [
|
|
1158
|
+
...new Set(
|
|
1159
|
+
emails.flatMap(
|
|
1160
|
+
(e) => (0, import_client3.templateLanguages)(e, defaultLocale()).filter((l) => l.locale !== null).map((l) => l.label)
|
|
1161
|
+
)
|
|
1162
|
+
)
|
|
1163
|
+
].sort()
|
|
1164
|
+
})
|
|
1165
|
+
},
|
|
1166
|
+
t("templates.list.newTemplate")
|
|
1167
|
+
) : null
|
|
1168
|
+
]
|
|
1169
|
+
),
|
|
1170
|
+
(0, import_vue3.h)("p", { style: styles.hint }, t("templates.list.hint")),
|
|
1171
|
+
isLoading.value ? (0, import_vue3.h)("p", { style: styles.status }, t("templates.list.loading")) : error.value ? (0, import_vue3.h)("p", { style: styles.error, role: "alert" }, error.value.explanation) : [
|
|
1172
|
+
(0, import_vue3.h)("ul", { style: styles.list }, emails.map(renderRow)),
|
|
1173
|
+
(0, import_vue3.h)("p", { style: legend }, t("templates.list.legend"))
|
|
938
1174
|
]
|
|
939
|
-
)
|
|
940
|
-
|
|
941
|
-
isLoading.value ? (0, import_vue3.h)("p", { style: styles.status }, t("templates.list.loading")) : error.value ? (0, import_vue3.h)("p", { style: styles.error, role: "alert" }, error.value.explanation) : (0, import_vue3.h)("ul", { style: styles.list }, (0, import_client3.groupTemplatesByType)(templates.value).map(renderRow))
|
|
942
|
-
]);
|
|
1175
|
+
]);
|
|
1176
|
+
};
|
|
943
1177
|
}
|
|
944
1178
|
});
|
|
945
1179
|
// Annotate the CommonJS export names for ESM import in node:
|