@cosmicdrift/kumiko-document-designer 0.363.0 → 0.364.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.
Files changed (81) hide show
  1. package/dist/constants.d.ts +3 -0
  2. package/dist/constants.js +4 -0
  3. package/dist/i18n.js +144 -50
  4. package/dist/web/cells.js +1 -1
  5. package/dist/web/editor-header.d.ts +37 -0
  6. package/dist/web/editor-header.js +43 -0
  7. package/dist/web/editor-shell.d.ts +40 -0
  8. package/dist/web/editor-shell.js +36 -0
  9. package/dist/web/i18n.js +35 -59
  10. package/dist/web/layout-editor-body.d.ts +2 -2
  11. package/dist/web/layout-editor-body.js +94 -49
  12. package/dist/web/layout-editor.js +8 -6
  13. package/dist/web/layout-html.d.ts +2 -0
  14. package/dist/web/layout-html.js +13 -1
  15. package/dist/web/layout-meta.d.ts +29 -0
  16. package/dist/web/layout-meta.js +32 -0
  17. package/dist/web/layout-rails.d.ts +15 -5
  18. package/dist/web/layout-rails.js +61 -68
  19. package/dist/web/layout-types.d.ts +4 -0
  20. package/dist/web/layout-users.d.ts +1 -0
  21. package/dist/web/layout-users.js +4 -1
  22. package/dist/web/mail-dialogs.d.ts +1 -6
  23. package/dist/web/mail-dialogs.js +2 -2
  24. package/dist/web/mail-draft.d.ts +8 -6
  25. package/dist/web/mail-draft.js +33 -11
  26. package/dist/web/mail-edit-center.d.ts +3 -1
  27. package/dist/web/mail-edit-center.js +5 -4
  28. package/dist/web/mail-editor-body.d.ts +0 -1
  29. package/dist/web/mail-editor-body.js +57 -41
  30. package/dist/web/mail-editor.js +2 -5
  31. package/dist/web/mail-footer.d.ts +3 -14
  32. package/dist/web/mail-footer.js +12 -13
  33. package/dist/web/mail-left-rail.d.ts +9 -4
  34. package/dist/web/mail-left-rail.js +18 -15
  35. package/dist/web/mail-missing-locale.d.ts +10 -0
  36. package/dist/web/mail-missing-locale.js +45 -0
  37. package/dist/web/mail-origin-bar.d.ts +3 -5
  38. package/dist/web/mail-origin-bar.js +26 -50
  39. package/dist/web/mail-preview-side.d.ts +12 -5
  40. package/dist/web/mail-preview-side.js +14 -5
  41. package/dist/web/mail-preview.d.ts +9 -5
  42. package/dist/web/mail-preview.js +24 -13
  43. package/dist/web/mail-right-rail.js +23 -13
  44. package/dist/web/mail-types.d.ts +6 -1
  45. package/dist/web/mail-versions.js +3 -3
  46. package/dist/web/origin-bar.d.ts +11 -2
  47. package/dist/web/origin-bar.js +46 -24
  48. package/dist/web/overrides-drawer.d.ts +3 -2
  49. package/dist/web/overrides-drawer.js +87 -11
  50. package/dist/web/overview-row.d.ts +2 -0
  51. package/dist/web/overview-row.js +2 -1
  52. package/dist/web/page-canvas.d.ts +1 -0
  53. package/dist/web/page-canvas.js +23 -13
  54. package/dist/web/pdf-footer.d.ts +13 -0
  55. package/dist/web/pdf-footer.js +34 -0
  56. package/dist/web/pdf-left-rail.d.ts +9 -0
  57. package/dist/web/pdf-left-rail.js +16 -0
  58. package/dist/web/pdf-meta.d.ts +3 -5
  59. package/dist/web/pdf-meta.js +10 -16
  60. package/dist/web/pdf-preview.js +21 -8
  61. package/dist/web/pdf-template-editor.js +90 -76
  62. package/dist/web/rail.d.ts +13 -1
  63. package/dist/web/rail.js +15 -7
  64. package/dist/web/relative-time.d.ts +1 -0
  65. package/dist/web/relative-time.js +18 -0
  66. package/dist/web/settings-panel.d.ts +11 -5
  67. package/dist/web/settings-panel.js +24 -13
  68. package/dist/web/status-label.d.ts +7 -0
  69. package/dist/web/status-label.js +8 -0
  70. package/dist/web/types.d.ts +23 -0
  71. package/dist/web/use-insert-targets.d.ts +1 -1
  72. package/dist/web/use-mail-persistence.d.ts +0 -1
  73. package/dist/web/use-mail-persistence.js +1 -1
  74. package/dist/web/use-own-address.d.ts +5 -0
  75. package/dist/web/use-own-address.js +14 -0
  76. package/package.json +8 -8
  77. package/src/changes.json +24 -0
  78. package/dist/web/mail-header.d.ts +0 -23
  79. package/dist/web/mail-header.js +0 -16
  80. package/dist/web/placeholder-panel.d.ts +0 -21
  81. package/dist/web/placeholder-panel.js +0 -35
@@ -3,13 +3,15 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
3
3
  import { TemplateResolverHandlers, TemplateResolverQueries, } from "@cosmicdrift/kumiko-bundled-features/template-resolver/web";
4
4
  import { dispatcherErrorText, useMutation, usePrimitives, useQuery, useTranslation, } from "@cosmicdrift/kumiko-renderer";
5
5
  import { useState } from "react";
6
+ import { EditorHeader, EditorLocaleSwitch, EditorModeSwitch, originOf, } from "./editor-header.js";
7
+ import { EditorBody, EditorFooter, EditorFrame } from "./editor-shell.js";
6
8
  import { fileUrl } from "./file-name.js";
7
9
  import { MailConfirmDialogs } from "./mail-dialogs.js";
8
- import { buildMeta, isDraftDirty, loadedMailOf, parseJsonRecord, parseLinkedResources, variableLabelMap, } from "./mail-draft.js";
10
+ import { buildMeta, isDraftDirty, loadedMailOf, parseJsonRecord, parseLinkedResources, systemLabelMap, variableLabelMap, } from "./mail-draft.js";
9
11
  import { MailEditCenter } from "./mail-edit-center.js";
10
- import { MailFooter } from "./mail-footer.js";
11
- import { MailHeader } from "./mail-header.js";
12
+ import { footerMessage, metaFieldMessages } from "./mail-footer.js";
12
13
  import { PlaceholderSection, ResourceSection, SystemPlaceholderSection } from "./mail-left-rail.js";
14
+ import { MissingLocaleCanvas } from "./mail-missing-locale.js";
13
15
  import { MailOriginBar } from "./mail-origin-bar.js";
14
16
  import { PdfLinkModal } from "./mail-pdf-link-modal.js";
15
17
  import { MailPreviewMode } from "./mail-preview.js";
@@ -19,9 +21,12 @@ import { VersionsSection } from "./mail-versions.js";
19
21
  import { useInsertTargets } from "./use-insert-targets.js";
20
22
  import { useMailPersistence } from "./use-mail-persistence.js";
21
23
  import { useOverviewRows } from "./use-overview-rows.js";
24
+ import { useOwnAddress } from "./use-own-address.js";
22
25
  import { useSettledValue } from "./use-settled-value.js";
23
26
  import { useTestMail } from "./use-test-mail.js";
24
- function sortedUnique(values) {
27
+ // The editor replaces this token with the decoded image size once the preview image loaded.
28
+ const SIZE_TOKEN = "{size}";
29
+ function uniqueValues(values) {
25
30
  return [...new Set(values)];
26
31
  }
27
32
  export function placeholderNames(validation) {
@@ -31,22 +36,25 @@ export function placeholderNames(validation) {
31
36
  name,
32
37
  ...Object.keys(variable.item ?? {}).map((item) => `${name}.${item}`),
33
38
  ]);
34
- return sortedUnique([...own, ...validation.systemPlaceholders]);
39
+ return uniqueValues([...own, ...validation.systemPlaceholders]);
35
40
  }
36
41
  export function MailEditorBody(props) {
37
42
  const { slug, locale, title, isSystemAdmin, row, onReload, onRefreshRow } = props;
38
43
  const t = useTranslation();
39
44
  const { Banner } = usePrimitives();
40
45
  const [baseline] = useState(() => loadedMailOf(row));
41
- const [draft, setDraft] = useState(baseline.draft);
42
- const [settled, settle] = useSettledValue(baseline.draft);
46
+ const [draft, setDraft] = useState(baseline);
47
+ const [settled, settle] = useSettledValue(baseline);
43
48
  const [mode, setMode] = useState("edit");
44
49
  const [creating, setCreating] = useState(false);
45
50
  const [dialog, setDialog] = useState({ kind: "none" });
46
51
  const [uploadOpen, setUploadOpen] = useState(false);
47
52
  const [pdfOpen, setPdfOpen] = useState(false);
48
53
  const [resourceError, setResourceError] = useState(null);
54
+ const [resetError, setResetError] = useState(null);
49
55
  const [editorEpoch, setEditorEpoch] = useState(0);
56
+ const [includeAttachments, setIncludeAttachments] = useState(true);
57
+ const [prefixSubject, setPrefixSubject] = useState(true);
50
58
  const insert = useInsertTargets();
51
59
  const persistence = useMailPersistence({
52
60
  slug,
@@ -54,9 +62,9 @@ export function MailEditorBody(props) {
54
62
  title,
55
63
  isSystemAdmin,
56
64
  row,
57
- sender: baseline.sender,
58
65
  });
59
66
  const testMail = useTestMail();
67
+ const own = useOwnAddress();
60
68
  const resourceRemove = useMutation(TemplateResolverHandlers.resourceRemove);
61
69
  const layouts = useOverviewRows("mail-layout");
62
70
  const pdfTemplates = useOverviewRows("document-pdf");
@@ -67,7 +75,7 @@ export function MailEditorBody(props) {
67
75
  kind: "mail",
68
76
  contentFormat: "html",
69
77
  content: settled.body,
70
- meta: buildMeta(settled, baseline.sender),
78
+ meta: buildMeta(settled),
71
79
  variableSchema,
72
80
  locale,
73
81
  scope: isSystemAdmin ? "system" : "tenant",
@@ -78,13 +86,12 @@ export function MailEditorBody(props) {
78
86
  settle(next);
79
87
  };
80
88
  const reset = () => {
81
- setDraft(baseline.draft);
82
- settle(baseline.draft);
89
+ setDraft(baseline);
90
+ settle(baseline);
83
91
  setEditorEpoch((epoch) => epoch + 1);
84
92
  persistence.clearError();
85
93
  };
86
- const dirty = isDraftDirty(draft, baseline.draft);
87
- const isActiveTenantRow = row?.scope === "tenant" && row.status === "active";
94
+ const dirty = isDraftDirty(draft, baseline);
88
95
  const busy = persistence.busy;
89
96
  const canSave = !locked && !busy && validation?.canSave === true && (dirty || creating);
90
97
  const canPublish = !locked &&
@@ -96,20 +103,10 @@ export function MailEditorBody(props) {
96
103
  if (await persist(publish))
97
104
  await onReload();
98
105
  };
99
- const requestSave = () => {
100
- if (isActiveTenantRow)
101
- setDialog({ kind: "activeSave" });
102
- else
103
- void saveAndReload(false);
104
- };
105
- const perform = (target) => {
106
- if (target.kind === "locale")
107
- props.onLocaleChange(target.locale);
108
- else
109
- props.onBack();
110
- };
106
+ const saveDraft = () => void saveAndReload(false);
107
+ const perform = (target) => props.onLocaleChange(target.locale);
111
108
  const leave = (target) => {
112
- if (target.kind === "locale" && target.locale === locale)
109
+ if (target.locale === locale)
113
110
  return;
114
111
  if (dirty)
115
112
  setDialog({ kind: "leave", target });
@@ -124,7 +121,10 @@ export function MailEditorBody(props) {
124
121
  resource.key,
125
122
  {
126
123
  src: fileUrl(resource.fileRefId),
127
- caption: t("documentDesigner.mail.resource.caption", { key: resource.key }),
124
+ caption: t("documentDesigner.mail.resource.caption", {
125
+ key: resource.key,
126
+ size: SIZE_TOKEN,
127
+ }),
128
128
  },
129
129
  ],
130
130
  ]
@@ -146,31 +146,49 @@ export function MailEditorBody(props) {
146
146
  slug,
147
147
  locale,
148
148
  content: draft.body,
149
- meta: buildMeta(draft, baseline.sender),
149
+ meta: buildMeta(draft),
150
150
  variableSchema,
151
+ includeAttachments,
152
+ prefixSubject,
151
153
  });
152
154
  };
155
+ // Nothing is written yet in a missing language, so no placeholder may read as missing.
156
+ const railValidation = locked && validation != null ? { ...validation, missingRequired: [] } : (validation ?? null);
153
157
  const names = placeholderNames(validation ?? null);
154
158
  const vocabulary = {
155
159
  names,
156
- labels: variableLabelMap(validation?.variables ?? {}),
160
+ labels: {
161
+ ...systemLabelMap(t, validation?.systemPlaceholders ?? []),
162
+ ...variableLabelMap(validation?.variables ?? {}),
163
+ },
157
164
  system: validation?.systemPlaceholders ?? [],
158
165
  };
159
- const rails = { validation: validation ?? null, row, slug, draft, onChange: update };
160
- return (_jsxs("div", { className: "flex flex-col gap-4 p-6", "data-testid": "document-designer-mail-editor", children: [_jsx(MailHeader, { title: title, row: row, locales: props.locales, locale: locale, mode: mode, testPending: testMail.pending, actions: {
161
- busy,
162
- canSave,
163
- canPublish,
164
- onSave: requestSave,
165
- onPublish: () => void saveAndReload(true),
166
- }, onBack: () => leave({ kind: "back" }), onLocale: (next) => leave({ kind: "locale", locale: next }), onMode: setMode, onTestMail: sendTestMail }), testMail.state.kind === "sent" && (_jsx(Banner, { variant: "primary", testId: "mail-test-sent", children: t("documentDesigner.mail.testMail.sent", { subject: testMail.state.subject }) })), testMail.state.kind === "error" && (_jsx(Banner, { variant: "error", testId: "mail-test-error", children: testMail.state.text })), resourceError !== null && (_jsx(Banner, { variant: "error", testId: "mail-resource-error", children: resourceError })), _jsx(MailOriginBar, { row: row, title: title, systemVersion: props.systemVersion, slug: slug, locale: locale, fallbackLocale: props.fallbackLocale, isSystemAdmin: isSystemAdmin, creating: creating, onCreateBlank: () => setCreating(true), onReload: () => void onReload() }), mode === "edit" ? (_jsxs("div", { className: "grid items-start gap-4 lg:grid-cols-[260px_minmax(0,1fr)_300px]", children: [_jsxs("div", { className: "flex max-h-[70vh] flex-col overflow-y-auto", children: [_jsx(PlaceholderSection, { validation: rails.validation, onInsert: insert.insertPlaceholder }), _jsx(SystemPlaceholderSection, { validation: rails.validation, onInsert: insert.insertPlaceholder }), _jsx(ResourceSection, { resources: resources, pdfTitles: pdfTitles, canUpload: uploadTemplateId !== null, onUpload: () => setUploadOpen(true), onInsert: insert.insertResource, onRemove: (key) => setDialog({ kind: "remove", key }) })] }), _jsx(MailEditCenter, { draft: draft, vocabulary: vocabulary, resourcePreviews: resourcePreviews, insert: insert, readOnly: locked, onChange: update }, editorEpoch), _jsxs("div", { className: "flex max-h-[70vh] flex-col overflow-y-auto", children: [_jsx(PropertiesSection, { draft: draft, layouts: layouts.rows, onChange: update }), _jsx(AttachmentSection, { attachments: resources.filter((resource) => resource.kind !== "image"), pdfTitles: pdfTitles, canEdit: uploadTemplateId !== null, onAddPdf: () => setPdfOpen(true), onRemove: (key) => setDialog({ kind: "remove", key }) }), _jsx(UsageSection, { slug: slug }), _jsx(VersionsSection, { rowId: row?.id ?? null, currentVersion: row?.version ?? 0, currentStatus: row?.status ?? "draft" })] })] })) : (_jsx(MailPreviewMode, { payload: {
166
+ const attachments = resources.filter((resource) => resource.kind !== "image");
167
+ const footer = locked || validation === undefined || validation === null
168
+ ? null
169
+ : footerMessage(t, validation, mode === "preview");
170
+ const fieldErrors = locked ? {} : metaFieldMessages(t, validation ?? null);
171
+ const publish = () => void saveAndReload(true);
172
+ const controls = (_jsx(EditorLocaleSwitch, { testIdPrefix: "mail", locales: props.locales, locale: locale, onLocale: (next) => leave({ kind: "locale", locale: next }) }));
173
+ const origin = row === null ? null : originOf(row.scope !== "tenant", props.systemVersion !== null);
174
+ return (_jsxs(EditorFrame, { testId: "document-designer-mail-editor", children: [_jsx(EditorHeader, { title: title, stored: row === null ? null : { status: row.status, version: row.version }, actions: { busy, canSave, canPublish, onSave: saveDraft, onPublish: publish }, children: _jsx(EditorModeSwitch, { testIdPrefix: "mail", mode: mode, onMode: setMode }) }), mode === "edit" && (_jsx(MailOriginBar, { row: row, systemVersion: props.systemVersion, locale: locale, isSystemAdmin: isSystemAdmin, creating: creating, controls: controls, onReload: () => {
175
+ setResetError(null);
176
+ void onReload();
177
+ }, onResetError: setResetError })), testMail.state.kind === "sent" && (_jsx(Banner, { variant: "primary", testId: "mail-test-sent", children: t("documentDesigner.mail.testMail.sent", { subject: testMail.state.subject }) })), testMail.state.kind === "error" && (_jsx(Banner, { variant: "error", testId: "mail-test-error", children: testMail.state.text })), resetError !== null && (_jsx(Banner, { variant: "error", testId: "mail-reset-error", children: resetError })), resourceError !== null && (_jsx(Banner, { variant: "error", testId: "mail-resource-error", children: resourceError })), mode === "edit" ? (_jsx(EditorBody, { left: _jsxs(_Fragment, { children: [_jsx(PlaceholderSection, { validation: railValidation, onInsert: insert.insertPlaceholder }), _jsx(SystemPlaceholderSection, { validation: railValidation, onInsert: insert.insertPlaceholder }), _jsx(ResourceSection, { resources: resources, pdfTitles: pdfTitles, canUpload: uploadTemplateId !== null, onUpload: () => setUploadOpen(true), onInsert: insert.insertResource, onRemove: (key) => setDialog({ kind: "remove", key }) })] }), center: locked ? (_jsx(MissingLocaleCanvas, { slug: slug, locale: locale, fallbackLocale: props.fallbackLocale, copyScope: isSystemAdmin ? "system" : "tenant", onCreateBlank: () => setCreating(true), onCopied: () => void onReload() })) : (_jsx(MailEditCenter, { draft: draft, vocabulary: vocabulary, resourcePreviews: resourcePreviews, insert: insert, readOnly: false, fieldErrors: fieldErrors, onChange: update }, editorEpoch)), right: _jsxs(_Fragment, { children: [_jsx(PropertiesSection, { draft: draft, layouts: layouts.rows, onChange: update }), _jsx(AttachmentSection, { attachments: attachments, pdfTitles: pdfTitles, canEdit: uploadTemplateId !== null, onAddPdf: () => setPdfOpen(true), onRemove: (key) => setDialog({ kind: "remove", key }) }), _jsx(UsageSection, { slug: slug }), _jsx(VersionsSection, { rowId: row?.id ?? null, currentVersion: row?.version ?? 0, currentStatus: row?.status ?? "draft" })] }) })) : (_jsx(MailPreviewMode, { payload: {
167
178
  slug,
168
179
  kind: "mail",
169
180
  locale,
170
181
  content: settled.body,
171
- meta: buildMeta(settled, baseline.sender),
182
+ meta: buildMeta(settled),
172
183
  variableSchema,
173
- }, validation: validation ?? null, slug: slug, locale: locale, testMail: { pending: testMail.pending, onSend: sendTestMail } })), _jsx(MailFooter, { validation: validation ?? null, previewMode: mode === "preview", saveError: persistence.error, modifiedAt: row?.modifiedAt ?? null, dirty: dirty, busy: busy, canSave: canSave, canPublish: canPublish, onDiscard: () => setDialog({ kind: "discard" }), onSave: requestSave, onPublish: () => void saveAndReload(true) }), uploadTemplateId !== null && (_jsxs(_Fragment, { children: [_jsx(UploadModal, { open: uploadOpen, onOpenChange: setUploadOpen, target: {
184
+ }, validation: validation ?? null, slug: slug, locale: locale, fromName: settled.senderName, own: own, origin: origin, controls: controls, testMail: {
185
+ pending: testMail.pending,
186
+ onSend: sendTestMail,
187
+ includeAttachments,
188
+ onIncludeAttachments: setIncludeAttachments,
189
+ prefixSubject,
190
+ onPrefixSubject: setPrefixSubject,
191
+ } })), !locked && (_jsx(EditorFooter, { testIdPrefix: "mail-editor", message: footer, saveError: persistence.error, modifiedAt: row?.modifiedAt ?? null, dirty: dirty, busy: busy, canSave: canSave, canPublish: canPublish, onDiscard: () => setDialog({ kind: "discard" }), onSave: saveDraft, onPublish: publish })), uploadTemplateId !== null && (_jsxs(_Fragment, { children: [_jsx(UploadModal, { open: uploadOpen, onOpenChange: setUploadOpen, target: {
174
192
  templateId: uploadTemplateId,
175
193
  takenKeys,
176
194
  onUploaded: (uploaded) => {
@@ -181,8 +199,6 @@ export function MailEditorBody(props) {
181
199
  } }), _jsx(PdfLinkModal, { open: pdfOpen, onOpenChange: setPdfOpen, templates: pdfTemplates.rows, variableLabels: vocabulary.labels, templateId: uploadTemplateId, takenKeys: takenKeys, onLinked: () => void onRefreshRow() })] })), _jsx(MailConfirmDialogs, { dialog: dialog, onClose: () => setDialog({ kind: "none" }), onDiscard: () => {
182
200
  reset();
183
201
  setDialog({ kind: "none" });
184
- }, onSaveDraft: async () => {
185
- await saveAndReload(false);
186
202
  }, onRemove: removeResource, onLeaveSave: async (target) => {
187
203
  if (await persist(false))
188
204
  perform(target);
@@ -4,7 +4,7 @@ import { TemplateResolverQueries } from "@cosmicdrift/kumiko-bundled-features/te
4
4
  import { useNav, useQuery, useUserRoles } from "@cosmicdrift/kumiko-renderer";
5
5
  import { LoadingState } from "@cosmicdrift/kumiko-renderer-web";
6
6
  import { useState } from "react";
7
- import { DEFAULT_TEMPLATE_LOCALES, PLATFORM_TEMPLATES_SCREEN_ID, TEMPLATES_SCREEN_ID, } from "../constants.js";
7
+ import { DEFAULT_TEMPLATE_LOCALES } from "../constants.js";
8
8
  import { MailEditorBody } from "./mail-editor-body.js";
9
9
  import { useOverviewRows } from "./use-overview-rows.js";
10
10
  function defaultLocaleOf(appLocales, rowLocales) {
@@ -25,7 +25,6 @@ function shownSummaryOf(summaries, slug, isSystemAdmin) {
25
25
  return (isSystemAdmin ? system : (tenant ?? system)) ?? null;
26
26
  }
27
27
  function MailEditorData({ slug, locale, locales, overviewRow, isSystemAdmin, onLocaleChange, refreshOverview, }) {
28
- const nav = useNav();
29
28
  const [generation, setGeneration] = useState(0);
30
29
  const list = useQuery(TemplateResolverQueries.list, {
31
30
  kind: "mail",
@@ -46,9 +45,7 @@ function MailEditorData({ slug, locale, locales, overviewRow, isSystemAdmin, onL
46
45
  const rowLocales = overviewRow?.locales ?? [];
47
46
  return (_jsx(MailEditorBody, { slug: slug, locale: locale, locales: locales, title: overviewRow?.title ?? slug, isSystemAdmin: isSystemAdmin, row: row, systemVersion: overviewRow?.systemVersion ?? null, fallbackLocale: fallbackLocaleOf(locale, rowLocales), onLocaleChange: onLocaleChange, onReload: reload, onRefreshRow: async () => {
48
47
  await stored.refetch();
49
- }, onBack: () => nav.navigate({
50
- screenId: isSystemAdmin ? PLATFORM_TEMPLATES_SCREEN_ID : TEMPLATES_SCREEN_ID,
51
- }) }, `${locale}|${generation}|${row?.id ?? "none"}`));
48
+ } }, `${locale}|${generation}|${row?.id ?? "none"}`));
52
49
  }
53
50
  function MailEditorEntry({ appLocales }) {
54
51
  const nav = useNav();
@@ -1,21 +1,10 @@
1
- import type { ReactNode } from "react";
2
1
  import type { MailValidation } from "./mail-types.js";
3
2
  type Translate = (key: string, params?: Readonly<Record<string, unknown>>) => string;
4
3
  export declare function footerMessage(t: Translate, validation: MailValidation, previewMode: boolean): {
5
4
  readonly tone: "bad" | "warn" | "ok";
6
5
  readonly text: string;
7
6
  };
8
- export declare function MailFooter({ validation, previewMode, saveError, modifiedAt, dirty, busy, canSave, canPublish, onDiscard, onSave, onPublish, }: {
9
- readonly validation: MailValidation | null;
10
- readonly previewMode: boolean;
11
- readonly saveError: string | null;
12
- readonly modifiedAt: string | null;
13
- readonly dirty: boolean;
14
- readonly busy: boolean;
15
- readonly canSave: boolean;
16
- readonly canPublish: boolean;
17
- readonly onDiscard: () => void;
18
- readonly onSave: () => void;
19
- readonly onPublish: () => void;
20
- }): ReactNode;
7
+ declare const MAIL_META_FIELDS: readonly ["subject", "previewText"];
8
+ export type MailMetaField = (typeof MAIL_META_FIELDS)[number];
9
+ export declare function metaFieldMessages(t: Translate, validation: MailValidation | null): Partial<Record<MailMetaField, string>>;
21
10
  export {};
@@ -1,6 +1,3 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- // @runtime client
3
- import { formatWhen, usePrimitives, useTranslation } from "@cosmicdrift/kumiko-renderer";
4
1
  function issueText(t, issue) {
5
2
  const translated = t(issue.i18nKey, issue.params);
6
3
  return translated === issue.i18nKey ? issue.code : translated;
@@ -31,14 +28,16 @@ export function footerMessage(t, validation, previewMode) {
31
28
  }
32
29
  return { tone: "ok", text: t("documentDesigner.mail.footer.ok") };
33
30
  }
34
- const TONE_CLASS = {
35
- bad: "text-status-bad",
36
- warn: "text-status-warn",
37
- ok: "text-status-ok",
38
- };
39
- export function MailFooter({ validation, previewMode, saveError, modifiedAt, dirty, busy, canSave, canPublish, onDiscard, onSave, onPublish, }) {
40
- const t = useTranslation();
41
- const { Banner, Button, Text } = usePrimitives();
42
- const message = validation === null ? null : footerMessage(t, validation, previewMode);
43
- return (_jsxs("div", { className: "flex flex-wrap items-center gap-3 border-t pt-3", "data-testid": "mail-editor-footer", children: [saveError !== null ? (_jsx(Banner, { variant: "error", testId: "mail-editor-save-error", children: saveError })) : (_jsx("span", { className: `text-sm ${message === null ? "text-muted-foreground" : TONE_CLASS[message.tone]}`, "data-testid": "mail-editor-footer-message", "data-tone": message?.tone, children: message === null ? t("documentDesigner.editor.footer.checking") : message.text })), _jsx("div", { className: "grow" }), modifiedAt !== null && (_jsx(Text, { variant: "muted", testId: "mail-editor-saved-at", children: t("documentDesigner.mail.footer.savedAt", { when: formatWhen(modifiedAt) }) })), dirty && (_jsx(Button, { variant: "ghost", onClick: onDiscard, testId: "document-designer-discard", children: t("documentDesigner.editor.discard") })), _jsx(Button, { variant: "secondary", disabled: !canSave, loading: busy, onClick: onSave, testId: "document-designer-save", children: t("documentDesigner.editor.save") }), _jsx(Button, { disabled: !canPublish, onClick: onPublish, testId: "document-designer-publish", children: t("documentDesigner.editor.publish") })] }));
31
+ const MAIL_META_FIELDS = ["subject", "previewText"];
32
+ // The server names the setting and the reason per issue; the field itself shows that sentence.
33
+ export function metaFieldMessages(t, validation) {
34
+ if (validation === null)
35
+ return {};
36
+ const messages = {};
37
+ for (const field of MAIL_META_FIELDS) {
38
+ const issue = validation.issues.find((candidate) => candidate.path === `meta.${field}`);
39
+ if (issue !== undefined)
40
+ messages[field] = issueText(t, issue);
41
+ }
42
+ return messages;
44
43
  }
@@ -1,12 +1,17 @@
1
1
  import type { ReactNode } from "react";
2
2
  import type { LinkedResourceView, MailValidation } from "./mail-types.js";
3
- export declare function PlaceholderSection({ validation, onInsert, }: {
4
- readonly validation: MailValidation | null;
3
+ export type PlaceholderSource = Pick<MailValidation, "variables" | "used" | "missingRequired" | "systemPlaceholders">;
4
+ export declare function PlaceholderSection({ validation, onInsert, idPrefix, }: {
5
+ readonly validation: PlaceholderSource | null;
5
6
  readonly onInsert: (name: string) => void;
7
+ readonly idPrefix?: string;
6
8
  }): ReactNode;
7
- export declare function SystemPlaceholderSection({ validation, onInsert, }: {
8
- readonly validation: MailValidation | null;
9
+ export declare function SystemPlaceholderSection({ validation, onInsert, idPrefix, isLayoutOwned, }: {
10
+ readonly validation: PlaceholderSource | null;
9
11
  readonly onInsert: (name: string) => void;
12
+ readonly idPrefix?: string;
13
+ /** Placeholders the layout fills in, so a template that never uses them is fine. */
14
+ readonly isLayoutOwned?: (name: string) => boolean;
10
15
  }): ReactNode;
11
16
  export declare function ResourceSection({ resources, pdfTitles, canUpload, onUpload, onInsert, onRemove, }: {
12
17
  readonly resources: readonly LinkedResourceView[];
@@ -1,27 +1,30 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  // @runtime client
3
3
  import { usePrimitives, useTranslation } from "@cosmicdrift/kumiko-renderer";
4
- import { PLACEHOLDER_SYMBOL } from "./mail-draft.js";
4
+ import { LAYOUT_OWNED_PLACEHOLDERS, PLACEHOLDER_SYMBOL, SYSTEM_PLACEHOLDER_TYPE, systemPlaceholderLabel, } from "./mail-draft.js";
5
5
  import { RailHint, RailRow, RailSection } from "./rail.js";
6
6
  function placeholderStatus(t, validation, name) {
7
- if (validation.missingRequired.includes(name))
8
- return t("documentDesigner.mail.placeholder.missing");
9
- return validation.used.includes(name) ? "✓" : "–";
7
+ if (validation.missingRequired.includes(name)) {
8
+ return { text: t("documentDesigner.mail.placeholder.missing"), tone: "bad" };
9
+ }
10
+ return validation.used.includes(name) ? { text: "✓", tone: "ok" } : { text: "–", tone: "muted" };
10
11
  }
11
- export function PlaceholderSection({ validation, onInsert, }) {
12
+ export function PlaceholderSection({ validation, onInsert, idPrefix = "mail", }) {
12
13
  const t = useTranslation();
13
14
  const entries = Object.entries(validation?.variables ?? {});
14
- return (_jsxs(RailSection, { title: t("documentDesigner.mail.placeholder.title"), testId: "mail-editor-placeholders", children: [validation !== null &&
15
- entries.map(([name, variable]) => (_jsx(RailRow, { testId: `mail-placeholder-${name}`, symbol: PLACEHOLDER_SYMBOL[variable.type], label: `${variable.label ?? name}${variable.required === true ? " *" : ""}`, bad: validation.missingRequired.includes(name), trailing: placeholderStatus(t, validation, name), onPress: () => onInsert(name) }, name))), _jsx(RailHint, { children: t("documentDesigner.mail.placeholder.hint") })] }));
16
- }
17
- function systemLabel(t, name) {
18
- const key = `documentDesigner.placeholder.${name}`;
19
- const translated = t(key);
20
- return translated === key ? name : translated;
15
+ return (_jsxs(RailSection, { title: t("documentDesigner.mail.placeholder.title"), testId: `${idPrefix}-editor-placeholders`, children: [validation !== null &&
16
+ entries.map(([name, variable]) => {
17
+ const status = placeholderStatus(t, validation, name);
18
+ return (_jsx(RailRow, { testId: `${idPrefix}-placeholder-${name}`, symbol: PLACEHOLDER_SYMBOL[variable.type], label: variable.label ?? name, required: variable.required === true, bad: validation.missingRequired.includes(name), trailing: status.text, trailingTone: status.tone, onPress: () => onInsert(name) }, name));
19
+ }), _jsx(RailHint, { children: t("documentDesigner.mail.placeholder.hint") })] }));
21
20
  }
22
- export function SystemPlaceholderSection({ validation, onInsert, }) {
21
+ export function SystemPlaceholderSection({ validation, onInsert, idPrefix = "mail", isLayoutOwned = (name) => LAYOUT_OWNED_PLACEHOLDERS.has(name), }) {
23
22
  const t = useTranslation();
24
- return (_jsx(RailSection, { title: t("documentDesigner.mail.system.title"), testId: "mail-editor-system-placeholders", children: (validation?.systemPlaceholders ?? []).map((name) => (_jsx(RailRow, { testId: `mail-system-${name}`, label: systemLabel(t, name), trailing: validation?.used.includes(name) === true ? "✓" : "–", onPress: () => onInsert(name) }, name))) }));
23
+ return (_jsxs(RailSection, { title: t("documentDesigner.mail.system.title"), testId: `${idPrefix}-editor-system-placeholders`, children: [(validation?.systemPlaceholders ?? []).map((name) => {
24
+ const used = validation?.used.includes(name) === true;
25
+ const layoutOwned = !used && isLayoutOwned(name);
26
+ return (_jsx(RailRow, { testId: `${idPrefix}-system-${name}`, symbol: PLACEHOLDER_SYMBOL[SYSTEM_PLACEHOLDER_TYPE[name] ?? "text"], label: systemPlaceholderLabel(t, name), trailing: used ? "✓" : layoutOwned ? t("documentDesigner.mail.system.layout") : "–", trailingTone: used ? "ok" : "muted", onPress: () => onInsert(name) }, name));
27
+ }), _jsx(RailHint, { children: t("documentDesigner.mail.system.hint") })] }));
25
28
  }
26
29
  function resourceRow(t, resource, pdfTitles) {
27
30
  switch (resource.kind) {
@@ -48,6 +51,6 @@ export function ResourceSection({ resources, pdfTitles, canUpload, onUpload, onI
48
51
  const { Button } = usePrimitives();
49
52
  return (_jsxs(RailSection, { title: t("documentDesigner.mail.resource.title"), testId: "mail-editor-resources", action: _jsx(Button, { variant: "link", disabled: !canUpload, onClick: onUpload, testId: "mail-resource-upload-open", children: t("documentDesigner.mail.resource.upload") }), children: [!canUpload && _jsx(RailHint, { children: t("documentDesigner.mail.resource.saveFirst") }), resources.map((resource) => {
50
53
  const row = resourceRow(t, resource, pdfTitles);
51
- return (_jsxs("div", { className: "flex items-center gap-1", "data-testid": "mail-resource-row", children: [_jsx("div", { className: "min-w-0 grow", children: _jsx(RailRow, { symbol: row.symbol, label: row.label, trailing: row.meta, ...(resource.kind === "image" && { onPress: () => onInsert(resource.key) }) }) }), _jsx(Button, { variant: "danger-ghost", size: "sm", onClick: () => onRemove(resource.key), testId: `mail-resource-remove-${resource.key}`, children: t("documentDesigner.mail.resource.remove") })] }, resource.key));
54
+ return (_jsx(RailRow, { testId: "mail-resource-row", symbol: row.symbol, label: row.label, trailing: row.meta, action: _jsx(Button, { variant: "danger-ghost", size: "sm", onClick: () => onRemove(resource.key), testId: `mail-resource-remove-${resource.key}`, children: t("documentDesigner.mail.resource.remove") }), ...(resource.kind === "image" && { onPress: () => onInsert(resource.key) }) }, resource.key));
52
55
  }), _jsx(RailHint, { children: t("documentDesigner.mail.resource.hint") })] }));
53
56
  }
@@ -0,0 +1,10 @@
1
+ import { type ReactNode } from "react";
2
+ export declare function languageAdjective(t: (key: string, params?: Readonly<Record<string, unknown>>) => string, locale: string): string;
3
+ export declare function MissingLocaleCanvas({ slug, locale, fallbackLocale, copyScope, onCreateBlank, onCopied, }: {
4
+ readonly slug: string;
5
+ readonly locale: string;
6
+ readonly fallbackLocale: string | null;
7
+ readonly copyScope: "tenant" | "system";
8
+ readonly onCreateBlank: () => void;
9
+ readonly onCopied: () => void;
10
+ }): ReactNode;
@@ -0,0 +1,45 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ // @runtime client
3
+ import { TemplateResolverHandlers } from "@cosmicdrift/kumiko-bundled-features/template-resolver/web";
4
+ import { dispatcherErrorText, useMutation, usePrimitives, useTranslation, } from "@cosmicdrift/kumiko-renderer";
5
+ import { useState } from "react";
6
+ import { languageName } from "./mail-origin-bar.js";
7
+ export function languageAdjective(t, locale) {
8
+ const key = `documentDesigner.mail.languageAdjective.${locale}`;
9
+ const translated = t(key);
10
+ return translated === key ? locale : translated;
11
+ }
12
+ // Shown in place of the editor while a language has no version yet: nothing to edit, so no
13
+ // subject line, toolbar or properties; the two ways to get a version sit right here.
14
+ export function MissingLocaleCanvas({ slug, locale, fallbackLocale, copyScope, onCreateBlank, onCopied, }) {
15
+ const t = useTranslation();
16
+ const { Banner, Button } = usePrimitives();
17
+ const copyLocale = useMutation(TemplateResolverHandlers.copyLocale);
18
+ const [error, setError] = useState(null);
19
+ const copy = async (from) => {
20
+ setError(null);
21
+ const result = await copyLocale.mutate({
22
+ slug,
23
+ kind: "mail",
24
+ fromLocale: from,
25
+ toLocale: locale,
26
+ overwrite: false,
27
+ scope: copyScope,
28
+ });
29
+ if (result.isSuccess)
30
+ onCopied();
31
+ else
32
+ setError(dispatcherErrorText(result.error, t));
33
+ };
34
+ return (_jsx("div", { className: "flex min-h-0 flex-1 items-center justify-center p-8", "data-testid": "mail-missing-locale", children: _jsxs("div", { className: "flex max-w-md flex-col items-center gap-4 text-center", children: [_jsx("p", { className: "m-0 text-base", children: fallbackLocale === null
35
+ ? t("documentDesigner.mail.missing.textNoFallback", {
36
+ language: languageName(t, locale),
37
+ })
38
+ : t("documentDesigner.mail.missing.text", {
39
+ language: languageName(t, locale),
40
+ locale,
41
+ fallbackAdjective: languageAdjective(t, fallbackLocale),
42
+ }) }), _jsxs("div", { className: "flex flex-wrap items-center justify-center gap-2", children: [fallbackLocale !== null && (_jsx(Button, { variant: "primary", disabled: copyLocale.pending, onClick: () => copy(fallbackLocale), testId: "mail-copy-locale", children: t("documentDesigner.mail.missing.copy", {
43
+ language: languageName(t, fallbackLocale),
44
+ }) })), _jsx(Button, { variant: "secondary", onClick: onCreateBlank, testId: "mail-create-blank", children: t("documentDesigner.mail.origin.createBlank") })] }), fallbackLocale !== null && (_jsx("p", { className: "m-0 text-xs text-muted-foreground", children: t("documentDesigner.mail.origin.copyNote") })), error !== null && (_jsx(Banner, { variant: "error", testId: "mail-copy-error", children: error }))] }) }));
45
+ }
@@ -2,16 +2,14 @@ import { type ReactNode } from "react";
2
2
  import type { TemplateRowView } from "./mail-types.js";
3
3
  type Translate = (key: string, params?: Readonly<Record<string, unknown>>) => string;
4
4
  export declare function languageName(t: Translate, locale: string): string;
5
- export declare function MailOriginBar({ row, title, systemVersion, slug, locale, fallbackLocale, isSystemAdmin, creating, onCreateBlank, onReload, }: {
5
+ export declare function MailOriginBar({ row, systemVersion, locale, isSystemAdmin, creating, controls, onReload, onResetError, }: {
6
6
  readonly row: TemplateRowView | null;
7
- readonly title: string;
8
7
  readonly systemVersion: number | null;
9
- readonly slug: string;
10
8
  readonly locale: string;
11
- readonly fallbackLocale: string | null;
12
9
  readonly isSystemAdmin: boolean;
13
10
  readonly creating: boolean;
14
- readonly onCreateBlank: () => void;
11
+ readonly controls: ReactNode;
15
12
  readonly onReload: () => void;
13
+ readonly onResetError: (message: string) => void;
16
14
  }): ReactNode;
17
15
  export {};
@@ -3,13 +3,19 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
3
3
  import { TemplateResolverHandlers } from "@cosmicdrift/kumiko-bundled-features/template-resolver/web";
4
4
  import { dispatcherErrorText, formatWhen, useMutation, usePrimitives, useTranslation, } from "@cosmicdrift/kumiko-renderer";
5
5
  import { useState } from "react";
6
- import { DiffPanel } from "./origin-bar.js";
6
+ import { originOf } from "./editor-header.js";
7
+ import { EditorInfoBar } from "./editor-shell.js";
8
+ import { DiffDrawer } from "./origin-bar.js";
9
+ import { draftBesideActiveSentence } from "./status-label.js";
7
10
  export function languageName(t, locale) {
8
11
  const key = `documentDesigner.mail.language.${locale}`;
9
12
  const translated = t(key);
10
13
  return translated === key ? locale : translated;
11
14
  }
12
15
  function statusSentence(t, row) {
16
+ const besideActive = draftBesideActiveSentence(t, row);
17
+ if (besideActive !== null)
18
+ return besideActive;
13
19
  if (row.status === "active") {
14
20
  return t("documentDesigner.mail.origin.activeSince", {
15
21
  when: row.modifiedAt === null ? "–" : formatWhen(row.modifiedAt),
@@ -19,66 +25,36 @@ function statusSentence(t, row) {
19
25
  ? "documentDesigner.mail.origin.archived"
20
26
  : "documentDesigner.mail.origin.notPublished");
21
27
  }
22
- const BAR_CLASS = "flex flex-wrap items-center gap-x-4 gap-y-2 rounded-md border px-4 py-2.5";
23
- function MissingLocaleBar({ slug, locale, fallbackLocale, copyScope, creating, onCreateBlank, onCopied, }) {
28
+ function MissingLocaleBar({ locale, creating, controls, }) {
24
29
  const t = useTranslation();
25
- const { Banner, Button, Text } = usePrimitives();
26
- const copyLocale = useMutation(TemplateResolverHandlers.copyLocale);
27
- const [error, setError] = useState(null);
28
- if (creating) {
29
- return (_jsx("div", { className: `${BAR_CLASS} bg-muted`, "data-testid": "mail-origin-bar", children: _jsx(Text, { children: t("documentDesigner.mail.origin.creating", { language: languageName(t, locale) }) }) }));
30
- }
31
- const copy = async (from) => {
32
- setError(null);
33
- const result = await copyLocale.mutate({
34
- slug,
35
- kind: "mail",
36
- fromLocale: from,
37
- toLocale: locale,
38
- overwrite: false,
39
- scope: copyScope,
40
- });
41
- if (result.isSuccess)
42
- onCopied();
43
- else
44
- setError(dispatcherErrorText(result.error, t));
45
- };
46
- return (_jsxs("div", { className: `${BAR_CLASS} border-status-warn bg-status-warn-surface`, "data-testid": "mail-origin-bar", "data-variant": "warn", children: [_jsxs(Text, { children: [t(fallbackLocale === null
47
- ? "documentDesigner.mail.origin.missingNoFallback"
48
- : "documentDesigner.mail.origin.missing", {
49
- language: languageName(t, locale),
50
- locale,
51
- fallback: fallbackLocale ?? "",
52
- }), fallbackLocale !== null && ` ${t("documentDesigner.mail.origin.copyNote")}`] }), _jsx("div", { className: "grow" }), fallbackLocale !== null && (_jsx(Button, { size: "sm", variant: "secondary", loading: copyLocale.pending, onClick: () => copy(fallbackLocale), testId: "mail-copy-locale", children: t("documentDesigner.mail.origin.copyFrom", { fallback: fallbackLocale }) })), _jsx(Button, { size: "sm", variant: "secondary", onClick: onCreateBlank, testId: "mail-create-blank", children: t("documentDesigner.mail.origin.createBlank") }), error !== null && (_jsx(Banner, { variant: "error", testId: "mail-copy-error", children: error }))] }));
30
+ return creating ? (_jsx(EditorInfoBar, { tone: "muted", testId: "mail-origin-bar", controls: controls, children: t("documentDesigner.mail.origin.creating", { language: languageName(t, locale) }) })) : (_jsx(EditorInfoBar, { tone: "warn", testId: "mail-origin-bar", controls: controls, children: t("documentDesigner.mail.origin.missing", { language: languageName(t, locale) }) }));
53
31
  }
54
- function TenantOriginBar({ row, title, systemVersion, onReset, }) {
32
+ function TenantOriginBar({ row, systemVersion, controls, onReset, onResetError, }) {
55
33
  const t = useTranslation();
56
- const { Button, Dialog, Drawer, Modal, Text } = usePrimitives();
34
+ const { Button, Dialog } = usePrimitives();
57
35
  const [diffOpen, setDiffOpen] = useState(false);
58
36
  const [resetOpen, setResetOpen] = useState(false);
59
37
  const resetToSystem = useMutation(TemplateResolverHandlers.resetToSystem);
60
38
  const hasSystem = systemVersion !== null;
61
- const diffTitle = t("documentDesigner.editor.diff.title");
62
- const diffBody = _jsx(DiffPanel, { tenantId: row.id });
63
- return (_jsxs("div", { className: `${BAR_CLASS} bg-muted`, "data-testid": "mail-origin-bar", children: [_jsxs(Text, { children: [hasSystem
64
- ? t("documentDesigner.mail.origin.overrides", { title, version: systemVersion })
65
- : t("documentDesigner.mail.origin.tenantOnly"), " ", statusSentence(t, row)] }), _jsx("div", { className: "grow" }), hasSystem && (_jsxs(_Fragment, { children: [_jsx(Button, { variant: "link", onClick: () => setDiffOpen(true), testId: "document-designer-diff-open", children: t("documentDesigner.editor.diff.open") }), _jsx(Button, { variant: "link", onClick: () => setResetOpen(true), testId: "document-designer-reset-open", children: t("documentDesigner.mail.origin.reset") })] })), diffOpen &&
66
- (Drawer === undefined ? (_jsx(Modal, { open: true, onOpenChange: setDiffOpen, title: diffTitle, children: diffBody })) : (_jsx(Drawer, { open: true, onOpenChange: setDiffOpen, title: diffTitle, children: diffBody }))), _jsx(Dialog, { open: resetOpen, onOpenChange: setResetOpen, title: t("documentDesigner.mail.reset.title"), description: t("documentDesigner.mail.reset.description"), confirmLabel: t("documentDesigner.mail.reset.confirm"), variant: "danger", onConfirm: async () => {
67
- const result = await resetToSystem.mutate({ id: row.id });
68
- if (result.isSuccess)
69
- onReset();
70
- } })] }));
39
+ return (_jsxs(EditorInfoBar, { tone: "active", testId: "mail-origin-bar", origin: originOf(false, hasSystem), controls: controls, overlays: _jsxs(_Fragment, { children: [diffOpen && _jsx(DiffDrawer, { tenantId: row.id, onClose: () => setDiffOpen(false) }), _jsx(Dialog, { open: resetOpen, onOpenChange: setResetOpen, title: t("documentDesigner.mail.reset.title"), description: t("documentDesigner.mail.reset.description"), confirmLabel: t("documentDesigner.mail.reset.confirm"), variant: "danger", onConfirm: async () => {
40
+ const result = await resetToSystem.mutate({ id: row.id });
41
+ if (result.isSuccess)
42
+ onReset();
43
+ else
44
+ onResetError(dispatcherErrorText(result.error, t));
45
+ } })] }), links: hasSystem && (_jsxs(_Fragment, { children: [_jsx(Button, { variant: "link", onClick: () => setDiffOpen(true), testId: "document-designer-diff-open", children: t("documentDesigner.editor.diff.open") }), _jsx(Button, { variant: "link", onClick: () => setResetOpen(true), testId: "document-designer-reset-open", children: t("documentDesigner.mail.origin.reset") })] })), children: [hasSystem
46
+ ? t("documentDesigner.mail.origin.overrides", { version: systemVersion })
47
+ : t("documentDesigner.mail.origin.tenantOnly"), " ", statusSentence(t, row)] }));
71
48
  }
72
- export function MailOriginBar({ row, title, systemVersion, slug, locale, fallbackLocale, isSystemAdmin, creating, onCreateBlank, onReload, }) {
49
+ export function MailOriginBar({ row, systemVersion, locale, isSystemAdmin, creating, controls, onReload, onResetError, }) {
73
50
  const t = useTranslation();
74
- const { Text } = usePrimitives();
75
51
  if (row === null) {
76
- return (_jsx(MissingLocaleBar, { slug: slug, locale: locale, fallbackLocale: fallbackLocale, copyScope: isSystemAdmin ? "system" : "tenant", creating: creating, onCreateBlank: onCreateBlank, onCopied: onReload }));
52
+ return _jsx(MissingLocaleBar, { locale: locale, creating: creating, controls: controls });
77
53
  }
78
54
  if (row.scope === "tenant") {
79
- return (_jsx(TenantOriginBar, { row: row, title: title, systemVersion: systemVersion, onReset: onReload }));
55
+ return (_jsx(TenantOriginBar, { row: row, systemVersion: systemVersion, controls: controls, onReset: onReload, onResetError: onResetError }));
80
56
  }
81
- return (_jsx("div", { className: `${BAR_CLASS} bg-muted`, "data-testid": "mail-origin-bar", children: _jsx(Text, { children: t(isSystemAdmin
82
- ? "documentDesigner.mail.origin.platform"
83
- : "documentDesigner.mail.origin.system", { version: row.version }) }) }));
57
+ return (_jsx(EditorInfoBar, { tone: "muted", testId: "mail-origin-bar", origin: "system", controls: controls, children: t(isSystemAdmin
58
+ ? "documentDesigner.mail.origin.platform"
59
+ : "documentDesigner.mail.origin.system", { version: row.version }) }));
84
60
  }
@@ -1,12 +1,19 @@
1
1
  import { type ReactNode } from "react";
2
2
  import type { MailPreview, MailValidation } from "./mail-types.js";
3
- export declare function PreviewSide({ preview, validation, slug, locale, testMail, }: {
3
+ import type { OwnAddress } from "./use-own-address.js";
4
+ export type TestMailControls = {
5
+ readonly pending: boolean;
6
+ readonly onSend: () => void;
7
+ readonly includeAttachments: boolean;
8
+ readonly onIncludeAttachments: (next: boolean) => void;
9
+ readonly prefixSubject: boolean;
10
+ readonly onPrefixSubject: (next: boolean) => void;
11
+ };
12
+ export declare function PreviewSide({ preview, validation, slug, locale, own, testMail, }: {
4
13
  readonly preview: MailPreview | null;
5
14
  readonly validation: MailValidation | null;
6
15
  readonly slug: string;
7
16
  readonly locale: string;
8
- readonly testMail: {
9
- readonly pending: boolean;
10
- readonly onSend: () => void;
11
- };
17
+ readonly own: OwnAddress;
18
+ readonly testMail: TestMailControls;
12
19
  }): ReactNode;