@cosmicdrift/kumiko-document-designer 0.0.0-stage → 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.
- package/LICENSE +57 -0
- package/README.md +37 -2
- package/dist/constants.d.ts +27 -0
- package/dist/constants.js +37 -0
- package/dist/feature.d.ts +2 -0
- package/dist/feature.js +85 -0
- package/dist/i18n.d.ts +6 -0
- package/dist/i18n.js +764 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/list-definition.d.ts +6 -0
- package/dist/list-definition.js +109 -0
- package/dist/web/cells.d.ts +10 -0
- package/dist/web/cells.js +87 -0
- package/dist/web/client-plugin.d.ts +12 -0
- package/dist/web/client-plugin.js +43 -0
- package/dist/web/compose-layout.d.ts +6 -0
- package/dist/web/compose-layout.js +13 -0
- package/dist/web/editor-header.d.ts +37 -0
- package/dist/web/editor-header.js +43 -0
- package/dist/web/editor-shell.d.ts +40 -0
- package/dist/web/editor-shell.js +36 -0
- package/dist/web/errors.d.ts +6 -0
- package/dist/web/errors.js +24 -0
- package/dist/web/field-issue-text.d.ts +4 -0
- package/dist/web/field-issue-text.js +12 -0
- package/dist/web/file-name.d.ts +2 -0
- package/dist/web/file-name.js +17 -0
- package/dist/web/i18n.d.ts +2 -0
- package/dist/web/i18n.js +146 -0
- package/dist/web/index.d.ts +6 -0
- package/dist/web/index.js +7 -0
- package/dist/web/layout-editor-body.d.ts +14 -0
- package/dist/web/layout-editor-body.js +187 -0
- package/dist/web/layout-editor.d.ts +2 -0
- package/dist/web/layout-editor.js +43 -0
- package/dist/web/layout-html.d.ts +9 -0
- package/dist/web/layout-html.js +93 -0
- package/dist/web/layout-meta.d.ts +29 -0
- package/dist/web/layout-meta.js +32 -0
- package/dist/web/layout-rails.d.ts +28 -0
- package/dist/web/layout-rails.js +100 -0
- package/dist/web/layout-types.d.ts +66 -0
- package/dist/web/layout-types.js +1 -0
- package/dist/web/layout-users.d.ts +4 -0
- package/dist/web/layout-users.js +11 -0
- package/dist/web/mail-dialogs.d.ts +24 -0
- package/dist/web/mail-dialogs.js +15 -0
- package/dist/web/mail-draft.d.ts +19 -0
- package/dist/web/mail-draft.js +154 -0
- package/dist/web/mail-edit-center.d.ts +19 -0
- package/dist/web/mail-edit-center.js +12 -0
- package/dist/web/mail-editor-body.d.ts +17 -0
- package/dist/web/mail-editor-body.js +209 -0
- package/dist/web/mail-editor.d.ts +2 -0
- package/dist/web/mail-editor.js +68 -0
- package/dist/web/mail-footer.d.ts +10 -0
- package/dist/web/mail-footer.js +43 -0
- package/dist/web/mail-left-rail.d.ts +23 -0
- package/dist/web/mail-left-rail.js +56 -0
- package/dist/web/mail-missing-locale.d.ts +10 -0
- package/dist/web/mail-missing-locale.js +45 -0
- package/dist/web/mail-origin-bar.d.ts +15 -0
- package/dist/web/mail-origin-bar.js +60 -0
- package/dist/web/mail-pdf-link-modal.d.ts +11 -0
- package/dist/web/mail-pdf-link-modal.js +54 -0
- package/dist/web/mail-preview-side.d.ts +19 -0
- package/dist/web/mail-preview-side.js +57 -0
- package/dist/web/mail-preview.d.ts +24 -0
- package/dist/web/mail-preview.js +54 -0
- package/dist/web/mail-right-rail.d.ts +19 -0
- package/dist/web/mail-right-rail.js +57 -0
- package/dist/web/mail-types.d.ts +117 -0
- package/dist/web/mail-types.js +1 -0
- package/dist/web/mail-upload-modal.d.ts +16 -0
- package/dist/web/mail-upload-modal.js +56 -0
- package/dist/web/mail-versions.d.ts +6 -0
- package/dist/web/mail-versions.js +43 -0
- package/dist/web/origin-bar.d.ts +22 -0
- package/dist/web/origin-bar.js +104 -0
- package/dist/web/overrides-drawer.d.ts +10 -0
- package/dist/web/overrides-drawer.js +95 -0
- package/dist/web/overview-row.d.ts +23 -0
- package/dist/web/overview-row.js +48 -0
- package/dist/web/page-canvas.d.ts +19 -0
- package/dist/web/page-canvas.js +48 -0
- package/dist/web/pdf-footer.d.ts +13 -0
- package/dist/web/pdf-footer.js +34 -0
- package/dist/web/pdf-left-rail.d.ts +9 -0
- package/dist/web/pdf-left-rail.js +16 -0
- package/dist/web/pdf-meta.d.ts +21 -0
- package/dist/web/pdf-meta.js +95 -0
- package/dist/web/pdf-preview.d.ts +4 -0
- package/dist/web/pdf-preview.js +112 -0
- package/dist/web/pdf-template-editor.d.ts +8 -0
- package/dist/web/pdf-template-editor.js +340 -0
- package/dist/web/preview-record-picker.d.ts +12 -0
- package/dist/web/preview-record-picker.js +7 -0
- package/dist/web/rail.d.ts +30 -0
- package/dist/web/rail.js +22 -0
- package/dist/web/relative-time.d.ts +1 -0
- package/dist/web/relative-time.js +18 -0
- package/dist/web/settings-panel.d.ts +23 -0
- package/dist/web/settings-panel.js +36 -0
- package/dist/web/status-label.d.ts +14 -0
- package/dist/web/status-label.js +26 -0
- package/dist/web/template-rows.d.ts +9 -0
- package/dist/web/template-rows.js +34 -0
- package/dist/web/types.d.ts +126 -0
- package/dist/web/types.js +3 -0
- package/dist/web/use-insert-targets.d.ts +9 -0
- package/dist/web/use-insert-targets.js +17 -0
- package/dist/web/use-mail-persistence.d.ts +14 -0
- package/dist/web/use-mail-persistence.js +49 -0
- package/dist/web/use-overview-rows.d.ts +7 -0
- package/dist/web/use-overview-rows.js +19 -0
- package/dist/web/use-own-address.d.ts +5 -0
- package/dist/web/use-own-address.js +14 -0
- package/dist/web/use-settled-value.d.ts +2 -0
- package/dist/web/use-settled-value.js +18 -0
- package/dist/web/use-test-mail.d.ts +15 -0
- package/dist/web/use-test-mail.js +27 -0
- package/dist/web/used-by-label.d.ts +9 -0
- package/dist/web/used-by-label.js +32 -0
- package/package.json +63 -4
- package/src/changes.json +44 -0
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
// @runtime client
|
|
3
|
+
import { TemplateResolverHandlers, TemplateResolverQueries, } from "@cosmicdrift/kumiko-bundled-features/template-resolver/web";
|
|
4
|
+
import { dispatcherErrorText, useMutation, usePrimitives, useQuery, useTranslation, } from "@cosmicdrift/kumiko-renderer";
|
|
5
|
+
import { useState } from "react";
|
|
6
|
+
import { EditorHeader, EditorLocaleSwitch, EditorModeSwitch, originOf, } from "./editor-header.js";
|
|
7
|
+
import { EditorBody, EditorFooter, EditorFrame } from "./editor-shell.js";
|
|
8
|
+
import { fileUrl } from "./file-name.js";
|
|
9
|
+
import { MailConfirmDialogs } from "./mail-dialogs.js";
|
|
10
|
+
import { buildMeta, isDraftDirty, loadedMailOf, parseJsonRecord, parseLinkedResources, systemLabelMap, variableLabelMap, } from "./mail-draft.js";
|
|
11
|
+
import { MailEditCenter } from "./mail-edit-center.js";
|
|
12
|
+
import { footerMessage, metaFieldMessages } from "./mail-footer.js";
|
|
13
|
+
import { PlaceholderSection, ResourceSection, SystemPlaceholderSection } from "./mail-left-rail.js";
|
|
14
|
+
import { MissingLocaleCanvas } from "./mail-missing-locale.js";
|
|
15
|
+
import { MailOriginBar } from "./mail-origin-bar.js";
|
|
16
|
+
import { PdfLinkModal } from "./mail-pdf-link-modal.js";
|
|
17
|
+
import { MailPreviewMode } from "./mail-preview.js";
|
|
18
|
+
import { AttachmentSection, PropertiesSection, UsageSection } from "./mail-right-rail.js";
|
|
19
|
+
import { UploadModal } from "./mail-upload-modal.js";
|
|
20
|
+
import { VersionsSection } from "./mail-versions.js";
|
|
21
|
+
import { useInsertTargets } from "./use-insert-targets.js";
|
|
22
|
+
import { useMailPersistence } from "./use-mail-persistence.js";
|
|
23
|
+
import { useOverviewRows } from "./use-overview-rows.js";
|
|
24
|
+
import { useOwnAddress } from "./use-own-address.js";
|
|
25
|
+
import { useSettledValue } from "./use-settled-value.js";
|
|
26
|
+
import { useTestMail } from "./use-test-mail.js";
|
|
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) {
|
|
30
|
+
return [...new Set(values)];
|
|
31
|
+
}
|
|
32
|
+
export function placeholderNames(validation) {
|
|
33
|
+
if (validation === null)
|
|
34
|
+
return [];
|
|
35
|
+
const own = Object.entries(validation.variables).flatMap(([name, variable]) => [
|
|
36
|
+
name,
|
|
37
|
+
...Object.keys(variable.item ?? {}).map((item) => `${name}.${item}`),
|
|
38
|
+
]);
|
|
39
|
+
return uniqueValues([...own, ...validation.systemPlaceholders]);
|
|
40
|
+
}
|
|
41
|
+
export function MailEditorBody(props) {
|
|
42
|
+
const { slug, locale, title, isSystemAdmin, row, onReload, onRefreshRow } = props;
|
|
43
|
+
const t = useTranslation();
|
|
44
|
+
const { Banner } = usePrimitives();
|
|
45
|
+
const [baseline] = useState(() => loadedMailOf(row));
|
|
46
|
+
const [draft, setDraft] = useState(baseline);
|
|
47
|
+
const [settled, settle] = useSettledValue(baseline);
|
|
48
|
+
const [mode, setMode] = useState("edit");
|
|
49
|
+
const [creating, setCreating] = useState(false);
|
|
50
|
+
const [dialog, setDialog] = useState({ kind: "none" });
|
|
51
|
+
const [uploadOpen, setUploadOpen] = useState(false);
|
|
52
|
+
const [pdfOpen, setPdfOpen] = useState(false);
|
|
53
|
+
const [resourceError, setResourceError] = useState(null);
|
|
54
|
+
const [resetError, setResetError] = useState(null);
|
|
55
|
+
const [editorEpoch, setEditorEpoch] = useState(0);
|
|
56
|
+
const [includeAttachments, setIncludeAttachments] = useState(true);
|
|
57
|
+
const [prefixSubject, setPrefixSubject] = useState(true);
|
|
58
|
+
const insert = useInsertTargets();
|
|
59
|
+
const persistence = useMailPersistence({
|
|
60
|
+
slug,
|
|
61
|
+
locale,
|
|
62
|
+
title,
|
|
63
|
+
isSystemAdmin,
|
|
64
|
+
row,
|
|
65
|
+
});
|
|
66
|
+
const testMail = useTestMail();
|
|
67
|
+
const own = useOwnAddress();
|
|
68
|
+
const resourceRemove = useMutation(TemplateResolverHandlers.resourceRemove);
|
|
69
|
+
const layouts = useOverviewRows("mail-layout");
|
|
70
|
+
const pdfTemplates = useOverviewRows("document-pdf");
|
|
71
|
+
const locked = row === null && !creating;
|
|
72
|
+
const variableSchema = parseJsonRecord(row?.variableSchema);
|
|
73
|
+
const validation = useQuery(TemplateResolverQueries.validate, {
|
|
74
|
+
slug,
|
|
75
|
+
kind: "mail",
|
|
76
|
+
contentFormat: "html",
|
|
77
|
+
content: settled.body,
|
|
78
|
+
meta: buildMeta(settled),
|
|
79
|
+
variableSchema,
|
|
80
|
+
locale,
|
|
81
|
+
scope: isSystemAdmin ? "system" : "tenant",
|
|
82
|
+
}).data;
|
|
83
|
+
const update = (patch) => {
|
|
84
|
+
const next = { ...draft, ...patch };
|
|
85
|
+
setDraft(next);
|
|
86
|
+
settle(next);
|
|
87
|
+
};
|
|
88
|
+
const reset = () => {
|
|
89
|
+
setDraft(baseline);
|
|
90
|
+
settle(baseline);
|
|
91
|
+
setEditorEpoch((epoch) => epoch + 1);
|
|
92
|
+
persistence.clearError();
|
|
93
|
+
};
|
|
94
|
+
const dirty = isDraftDirty(draft, baseline);
|
|
95
|
+
const busy = persistence.busy;
|
|
96
|
+
const canSave = !locked && !busy && validation?.canSave === true && (dirty || creating);
|
|
97
|
+
const canPublish = !locked &&
|
|
98
|
+
!busy &&
|
|
99
|
+
validation?.canPublish === true &&
|
|
100
|
+
(dirty || creating || row?.status !== "active");
|
|
101
|
+
const persist = async (publish) => persistence.save(draft, publish);
|
|
102
|
+
const saveAndReload = async (publish) => {
|
|
103
|
+
if (await persist(publish))
|
|
104
|
+
await onReload();
|
|
105
|
+
};
|
|
106
|
+
const saveDraft = () => void saveAndReload(false);
|
|
107
|
+
const perform = (target) => props.onLocaleChange(target.locale);
|
|
108
|
+
const leave = (target) => {
|
|
109
|
+
if (target.locale === locale)
|
|
110
|
+
return;
|
|
111
|
+
if (dirty)
|
|
112
|
+
setDialog({ kind: "leave", target });
|
|
113
|
+
else
|
|
114
|
+
perform(target);
|
|
115
|
+
};
|
|
116
|
+
const resources = parseLinkedResources(row?.linkedResources);
|
|
117
|
+
const pdfTitles = new Map(pdfTemplates.rows.map((template) => [template.slug, template.title]));
|
|
118
|
+
const resourcePreviews = Object.fromEntries(resources.flatMap((resource) => resource.kind === "image"
|
|
119
|
+
? [
|
|
120
|
+
[
|
|
121
|
+
resource.key,
|
|
122
|
+
{
|
|
123
|
+
src: fileUrl(resource.fileRefId),
|
|
124
|
+
caption: t("documentDesigner.mail.resource.caption", {
|
|
125
|
+
key: resource.key,
|
|
126
|
+
size: SIZE_TOKEN,
|
|
127
|
+
}),
|
|
128
|
+
},
|
|
129
|
+
],
|
|
130
|
+
]
|
|
131
|
+
: []));
|
|
132
|
+
const uploadTemplateId = row?.scope === "tenant" ? row.id : null;
|
|
133
|
+
const takenKeys = resources.map((resource) => resource.key);
|
|
134
|
+
const removeResource = async (key) => {
|
|
135
|
+
if (uploadTemplateId === null)
|
|
136
|
+
return;
|
|
137
|
+
setResourceError(null);
|
|
138
|
+
const result = await resourceRemove.mutate({ templateId: uploadTemplateId, key });
|
|
139
|
+
if (result.isSuccess)
|
|
140
|
+
await onRefreshRow();
|
|
141
|
+
else
|
|
142
|
+
setResourceError(dispatcherErrorText(result.error, t));
|
|
143
|
+
};
|
|
144
|
+
const sendTestMail = () => {
|
|
145
|
+
void testMail.send({
|
|
146
|
+
slug,
|
|
147
|
+
locale,
|
|
148
|
+
content: draft.body,
|
|
149
|
+
meta: buildMeta(draft),
|
|
150
|
+
variableSchema,
|
|
151
|
+
includeAttachments,
|
|
152
|
+
prefixSubject,
|
|
153
|
+
});
|
|
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);
|
|
157
|
+
const names = placeholderNames(validation ?? null);
|
|
158
|
+
const vocabulary = {
|
|
159
|
+
names,
|
|
160
|
+
labels: {
|
|
161
|
+
...systemLabelMap(t, validation?.systemPlaceholders ?? []),
|
|
162
|
+
...variableLabelMap(validation?.variables ?? {}),
|
|
163
|
+
},
|
|
164
|
+
system: validation?.systemPlaceholders ?? [],
|
|
165
|
+
};
|
|
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: {
|
|
178
|
+
slug,
|
|
179
|
+
kind: "mail",
|
|
180
|
+
locale,
|
|
181
|
+
content: settled.body,
|
|
182
|
+
meta: buildMeta(settled),
|
|
183
|
+
variableSchema,
|
|
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: {
|
|
192
|
+
templateId: uploadTemplateId,
|
|
193
|
+
takenKeys,
|
|
194
|
+
onUploaded: (uploaded) => {
|
|
195
|
+
if (uploaded.purpose === "image")
|
|
196
|
+
insert.insertResource(uploaded.key);
|
|
197
|
+
void onRefreshRow();
|
|
198
|
+
},
|
|
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: () => {
|
|
200
|
+
reset();
|
|
201
|
+
setDialog({ kind: "none" });
|
|
202
|
+
}, onRemove: removeResource, onLeaveSave: async (target) => {
|
|
203
|
+
if (await persist(false))
|
|
204
|
+
perform(target);
|
|
205
|
+
}, onLeaveDiscard: (target) => {
|
|
206
|
+
setDialog({ kind: "none" });
|
|
207
|
+
perform(target);
|
|
208
|
+
} })] }));
|
|
209
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
// @runtime client
|
|
3
|
+
import { TemplateResolverQueries } from "@cosmicdrift/kumiko-bundled-features/template-resolver/web";
|
|
4
|
+
import { useNav, useQuery, useUserRoles } from "@cosmicdrift/kumiko-renderer";
|
|
5
|
+
import { LoadingState } from "@cosmicdrift/kumiko-renderer-web";
|
|
6
|
+
import { useState } from "react";
|
|
7
|
+
import { DEFAULT_TEMPLATE_LOCALES } from "../constants.js";
|
|
8
|
+
import { MailEditorBody } from "./mail-editor-body.js";
|
|
9
|
+
import { useOverviewRows } from "./use-overview-rows.js";
|
|
10
|
+
function defaultLocaleOf(appLocales, rowLocales) {
|
|
11
|
+
const preferred = appLocales[0] ?? DEFAULT_TEMPLATE_LOCALES[0] ?? "de";
|
|
12
|
+
if (rowLocales.length === 0 || rowLocales.includes(preferred))
|
|
13
|
+
return preferred;
|
|
14
|
+
return rowLocales[0] ?? preferred;
|
|
15
|
+
}
|
|
16
|
+
function fallbackLocaleOf(locale, rowLocales) {
|
|
17
|
+
const others = rowLocales.filter((candidate) => candidate !== locale);
|
|
18
|
+
const preferred = DEFAULT_TEMPLATE_LOCALES[0];
|
|
19
|
+
return (preferred !== undefined && others.includes(preferred) ? preferred : others[0]) ?? null;
|
|
20
|
+
}
|
|
21
|
+
function shownSummaryOf(summaries, slug, isSystemAdmin) {
|
|
22
|
+
const ofSlug = summaries.filter((summary) => summary.slug === slug);
|
|
23
|
+
const tenant = ofSlug.find((summary) => summary.scope === "tenant");
|
|
24
|
+
const system = ofSlug.find((summary) => summary.scope === "system");
|
|
25
|
+
return (isSystemAdmin ? system : (tenant ?? system)) ?? null;
|
|
26
|
+
}
|
|
27
|
+
function MailEditorData({ slug, locale, locales, overviewRow, isSystemAdmin, onLocaleChange, refreshOverview, }) {
|
|
28
|
+
const [generation, setGeneration] = useState(0);
|
|
29
|
+
const list = useQuery(TemplateResolverQueries.list, {
|
|
30
|
+
kind: "mail",
|
|
31
|
+
locale,
|
|
32
|
+
includeSystem: true,
|
|
33
|
+
});
|
|
34
|
+
const shown = shownSummaryOf(list.data ?? [], slug, isSystemAdmin);
|
|
35
|
+
const stored = useQuery(TemplateResolverQueries.findById, { id: shown?.id ?? "" }, { enabled: shown !== null });
|
|
36
|
+
const row = stored.data !== null && stored.data.id === shown?.id ? stored.data : null;
|
|
37
|
+
// A changed row id keeps the previous row in `stored` until the new one arrives.
|
|
38
|
+
if (list.data === null || (shown !== null && row === null))
|
|
39
|
+
return _jsx(LoadingState, {});
|
|
40
|
+
const reload = async () => {
|
|
41
|
+
await Promise.all([list.refetch(), refreshOverview()]);
|
|
42
|
+
await stored.refetch();
|
|
43
|
+
setGeneration((current) => current + 1);
|
|
44
|
+
};
|
|
45
|
+
const rowLocales = overviewRow?.locales ?? [];
|
|
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 () => {
|
|
47
|
+
await stored.refetch();
|
|
48
|
+
} }, `${locale}|${generation}|${row?.id ?? "none"}`));
|
|
49
|
+
}
|
|
50
|
+
function MailEditorEntry({ appLocales }) {
|
|
51
|
+
const nav = useNav();
|
|
52
|
+
const roles = useUserRoles();
|
|
53
|
+
const slug = nav.searchParams["slug"] ?? "";
|
|
54
|
+
const overview = useOverviewRows("mail", slug);
|
|
55
|
+
const [picked, setPicked] = useState(nav.searchParams["locale"] ?? null);
|
|
56
|
+
if (!overview.loaded)
|
|
57
|
+
return _jsx(LoadingState, {});
|
|
58
|
+
const overviewRow = overview.rows.find((candidate) => candidate.slug === slug) ?? null;
|
|
59
|
+
const rowLocales = overviewRow?.locales ?? [];
|
|
60
|
+
const locale = picked ?? defaultLocaleOf(appLocales, rowLocales);
|
|
61
|
+
const locales = [...new Set([...appLocales, ...rowLocales])];
|
|
62
|
+
return (_jsx(MailEditorData, { slug: slug, locale: locale, locales: locales, overviewRow: overviewRow, isSystemAdmin: roles?.includes("SystemAdmin") === true, onLocaleChange: setPicked, refreshOverview: overview.refetch }, locale));
|
|
63
|
+
}
|
|
64
|
+
export function createMailEditor(appLocales) {
|
|
65
|
+
return function MailEditor() {
|
|
66
|
+
return _jsx(MailEditorEntry, { appLocales: appLocales });
|
|
67
|
+
};
|
|
68
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { MailValidation } from "./mail-types.js";
|
|
2
|
+
type Translate = (key: string, params?: Readonly<Record<string, unknown>>) => string;
|
|
3
|
+
export declare function footerMessage(t: Translate, validation: MailValidation, previewMode: boolean): {
|
|
4
|
+
readonly tone: "bad" | "warn" | "ok";
|
|
5
|
+
readonly text: string;
|
|
6
|
+
};
|
|
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>>;
|
|
10
|
+
export {};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
function issueText(t, issue) {
|
|
2
|
+
const translated = t(issue.i18nKey, issue.params);
|
|
3
|
+
return translated === issue.i18nKey ? issue.code : translated;
|
|
4
|
+
}
|
|
5
|
+
function missingLabels(validation) {
|
|
6
|
+
return validation.missingRequired
|
|
7
|
+
.map((name) => validation.variables[name]?.label ?? name)
|
|
8
|
+
.join(", ");
|
|
9
|
+
}
|
|
10
|
+
// The footer sentence: missing required placeholders first, then other blocking issues,
|
|
11
|
+
// then hints (not-yet-uploaded resources), else "ready".
|
|
12
|
+
export function footerMessage(t, validation, previewMode) {
|
|
13
|
+
if (validation.missingRequired.length > 0) {
|
|
14
|
+
return {
|
|
15
|
+
tone: "bad",
|
|
16
|
+
text: t(previewMode
|
|
17
|
+
? "documentDesigner.mail.footer.missingPreview"
|
|
18
|
+
: "documentDesigner.mail.footer.missing", { names: missingLabels(validation) }),
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
const blocking = [...validation.issues, ...validation.publishIssues][0];
|
|
22
|
+
if (blocking !== undefined)
|
|
23
|
+
return { tone: "bad", text: issueText(t, blocking) };
|
|
24
|
+
const warning = validation.warnings[0];
|
|
25
|
+
if (warning !== undefined) {
|
|
26
|
+
const translated = t(warning.i18nKey, { detail: warning.detail });
|
|
27
|
+
return { tone: "warn", text: translated === warning.i18nKey ? warning.code : translated };
|
|
28
|
+
}
|
|
29
|
+
return { tone: "ok", text: t("documentDesigner.mail.footer.ok") };
|
|
30
|
+
}
|
|
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;
|
|
43
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import type { LinkedResourceView, MailValidation } from "./mail-types.js";
|
|
3
|
+
export type PlaceholderSource = Pick<MailValidation, "variables" | "used" | "missingRequired" | "systemPlaceholders">;
|
|
4
|
+
export declare function PlaceholderSection({ validation, onInsert, idPrefix, }: {
|
|
5
|
+
readonly validation: PlaceholderSource | null;
|
|
6
|
+
readonly onInsert: (name: string) => void;
|
|
7
|
+
readonly idPrefix?: string;
|
|
8
|
+
}): ReactNode;
|
|
9
|
+
export declare function SystemPlaceholderSection({ validation, onInsert, idPrefix, isLayoutOwned, }: {
|
|
10
|
+
readonly validation: PlaceholderSource | null;
|
|
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;
|
|
15
|
+
}): ReactNode;
|
|
16
|
+
export declare function ResourceSection({ resources, pdfTitles, canUpload, onUpload, onInsert, onRemove, }: {
|
|
17
|
+
readonly resources: readonly LinkedResourceView[];
|
|
18
|
+
readonly pdfTitles: ReadonlyMap<string, string>;
|
|
19
|
+
readonly canUpload: boolean;
|
|
20
|
+
readonly onUpload: () => void;
|
|
21
|
+
readonly onInsert: (key: string) => void;
|
|
22
|
+
readonly onRemove: (key: string) => void;
|
|
23
|
+
}): ReactNode;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
// @runtime client
|
|
3
|
+
import { usePrimitives, useTranslation } from "@cosmicdrift/kumiko-renderer";
|
|
4
|
+
import { LAYOUT_OWNED_PLACEHOLDERS, PLACEHOLDER_SYMBOL, SYSTEM_PLACEHOLDER_TYPE, systemPlaceholderLabel, } from "./mail-draft.js";
|
|
5
|
+
import { RailHint, RailRow, RailSection } from "./rail.js";
|
|
6
|
+
function placeholderStatus(t, validation, 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" };
|
|
11
|
+
}
|
|
12
|
+
export function PlaceholderSection({ validation, onInsert, idPrefix = "mail", }) {
|
|
13
|
+
const t = useTranslation();
|
|
14
|
+
const entries = Object.entries(validation?.variables ?? {});
|
|
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") })] }));
|
|
20
|
+
}
|
|
21
|
+
export function SystemPlaceholderSection({ validation, onInsert, idPrefix = "mail", isLayoutOwned = (name) => LAYOUT_OWNED_PLACEHOLDERS.has(name), }) {
|
|
22
|
+
const t = useTranslation();
|
|
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") })] }));
|
|
28
|
+
}
|
|
29
|
+
function resourceRow(t, resource, pdfTitles) {
|
|
30
|
+
switch (resource.kind) {
|
|
31
|
+
case "image":
|
|
32
|
+
return { symbol: "▣", label: resource.key, meta: t("documentDesigner.mail.resource.inline") };
|
|
33
|
+
case "file":
|
|
34
|
+
return {
|
|
35
|
+
symbol: "⎘",
|
|
36
|
+
label: resource.fileName,
|
|
37
|
+
meta: t("documentDesigner.mail.resource.attachment"),
|
|
38
|
+
};
|
|
39
|
+
case "pdf":
|
|
40
|
+
return {
|
|
41
|
+
symbol: "⎘",
|
|
42
|
+
label: resource.fileName,
|
|
43
|
+
meta: t("documentDesigner.mail.resource.fromPdf", {
|
|
44
|
+
title: pdfTitles.get(resource.templateSlug) ?? resource.templateSlug,
|
|
45
|
+
}),
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
export function ResourceSection({ resources, pdfTitles, canUpload, onUpload, onInsert, onRemove, }) {
|
|
50
|
+
const t = useTranslation();
|
|
51
|
+
const { Button } = usePrimitives();
|
|
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) => {
|
|
53
|
+
const row = resourceRow(t, resource, pdfTitles);
|
|
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));
|
|
55
|
+
}), _jsx(RailHint, { children: t("documentDesigner.mail.resource.hint") })] }));
|
|
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
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
import type { TemplateRowView } from "./mail-types.js";
|
|
3
|
+
type Translate = (key: string, params?: Readonly<Record<string, unknown>>) => string;
|
|
4
|
+
export declare function languageName(t: Translate, locale: string): string;
|
|
5
|
+
export declare function MailOriginBar({ row, systemVersion, locale, isSystemAdmin, creating, controls, onReload, onResetError, }: {
|
|
6
|
+
readonly row: TemplateRowView | null;
|
|
7
|
+
readonly systemVersion: number | null;
|
|
8
|
+
readonly locale: string;
|
|
9
|
+
readonly isSystemAdmin: boolean;
|
|
10
|
+
readonly creating: boolean;
|
|
11
|
+
readonly controls: ReactNode;
|
|
12
|
+
readonly onReload: () => void;
|
|
13
|
+
readonly onResetError: (message: string) => void;
|
|
14
|
+
}): ReactNode;
|
|
15
|
+
export {};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { Fragment as _Fragment, 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, formatWhen, useMutation, usePrimitives, useTranslation, } from "@cosmicdrift/kumiko-renderer";
|
|
5
|
+
import { useState } from "react";
|
|
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";
|
|
10
|
+
export function languageName(t, locale) {
|
|
11
|
+
const key = `documentDesigner.mail.language.${locale}`;
|
|
12
|
+
const translated = t(key);
|
|
13
|
+
return translated === key ? locale : translated;
|
|
14
|
+
}
|
|
15
|
+
function statusSentence(t, row) {
|
|
16
|
+
const besideActive = draftBesideActiveSentence(t, row);
|
|
17
|
+
if (besideActive !== null)
|
|
18
|
+
return besideActive;
|
|
19
|
+
if (row.status === "active") {
|
|
20
|
+
return t("documentDesigner.mail.origin.activeSince", {
|
|
21
|
+
when: row.modifiedAt === null ? "–" : formatWhen(row.modifiedAt),
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
return t(row.status === "archived"
|
|
25
|
+
? "documentDesigner.mail.origin.archived"
|
|
26
|
+
: "documentDesigner.mail.origin.notPublished");
|
|
27
|
+
}
|
|
28
|
+
function MissingLocaleBar({ locale, creating, controls, }) {
|
|
29
|
+
const t = useTranslation();
|
|
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) }) }));
|
|
31
|
+
}
|
|
32
|
+
function TenantOriginBar({ row, systemVersion, controls, onReset, onResetError, }) {
|
|
33
|
+
const t = useTranslation();
|
|
34
|
+
const { Button, Dialog } = usePrimitives();
|
|
35
|
+
const [diffOpen, setDiffOpen] = useState(false);
|
|
36
|
+
const [resetOpen, setResetOpen] = useState(false);
|
|
37
|
+
const resetToSystem = useMutation(TemplateResolverHandlers.resetToSystem);
|
|
38
|
+
const hasSystem = systemVersion !== null;
|
|
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)] }));
|
|
48
|
+
}
|
|
49
|
+
export function MailOriginBar({ row, systemVersion, locale, isSystemAdmin, creating, controls, onReload, onResetError, }) {
|
|
50
|
+
const t = useTranslation();
|
|
51
|
+
if (row === null) {
|
|
52
|
+
return _jsx(MissingLocaleBar, { locale: locale, creating: creating, controls: controls });
|
|
53
|
+
}
|
|
54
|
+
if (row.scope === "tenant") {
|
|
55
|
+
return (_jsx(TenantOriginBar, { row: row, systemVersion: systemVersion, controls: controls, onReset: onReload, onResetError: onResetError }));
|
|
56
|
+
}
|
|
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 }) }));
|
|
60
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
import type { OverviewRowView } from "./overview-row.js";
|
|
3
|
+
export declare function PdfLinkModal({ open, onOpenChange, templates, ...form }: {
|
|
4
|
+
readonly open: boolean;
|
|
5
|
+
readonly onOpenChange: (open: boolean) => void;
|
|
6
|
+
readonly templates: readonly OverviewRowView[];
|
|
7
|
+
readonly variableLabels: Readonly<Record<string, string>>;
|
|
8
|
+
readonly templateId: string;
|
|
9
|
+
readonly takenKeys: readonly string[];
|
|
10
|
+
readonly onLinked: () => void;
|
|
11
|
+
}): ReactNode;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
// @runtime client
|
|
3
|
+
import { PlaceholderLineEditor, TemplateResolverHandlers, TemplateResolverQueries, } from "@cosmicdrift/kumiko-bundled-features/template-resolver/web";
|
|
4
|
+
import { dispatcherErrorText, useMutation, usePrimitives, useQuery, useTranslation, } from "@cosmicdrift/kumiko-renderer";
|
|
5
|
+
import { EmptyState } from "@cosmicdrift/kumiko-renderer-web";
|
|
6
|
+
import { useState } from "react";
|
|
7
|
+
import { lastSlugSegment, parseJsonRecord, uniqueResourceKey } from "./mail-draft.js";
|
|
8
|
+
function pdfRowIdOf(summaries, slug) {
|
|
9
|
+
const ofSlug = (summaries ?? []).filter((row) => row.slug === slug);
|
|
10
|
+
return (ofSlug.find((row) => row.scope === "tenant") ?? ofSlug[0])?.id ?? null;
|
|
11
|
+
}
|
|
12
|
+
function PdfLinkForm({ templates, variableLabels, templateId, takenKeys, onLinked, }) {
|
|
13
|
+
const t = useTranslation();
|
|
14
|
+
const { Banner, Button, Field, Input } = usePrimitives();
|
|
15
|
+
const [slug, setSlug] = useState(templates[0]?.slug ?? "");
|
|
16
|
+
const [editedName, setEditedName] = useState(null);
|
|
17
|
+
const [error, setError] = useState(null);
|
|
18
|
+
const linkPdf = useMutation(TemplateResolverHandlers.resourceLinkPdf);
|
|
19
|
+
const summaries = useQuery(TemplateResolverQueries.list, {
|
|
20
|
+
kind: "document-pdf",
|
|
21
|
+
includeSystem: true,
|
|
22
|
+
});
|
|
23
|
+
const rowId = pdfRowIdOf(summaries.data, slug);
|
|
24
|
+
const row = useQuery(TemplateResolverQueries.findById, { id: rowId ?? "" }, { enabled: rowId !== null });
|
|
25
|
+
const metaName = parseJsonRecord(row.data?.meta)["fileName"];
|
|
26
|
+
const defaultName = typeof metaName === "string" && metaName !== "" ? metaName : `${lastSlugSegment(slug)}.pdf`;
|
|
27
|
+
const fileName = editedName ?? defaultName;
|
|
28
|
+
const link = async () => {
|
|
29
|
+
setError(null);
|
|
30
|
+
const result = await linkPdf.mutate({
|
|
31
|
+
templateId,
|
|
32
|
+
key: uniqueResourceKey(lastSlugSegment(slug), takenKeys),
|
|
33
|
+
templateSlug: slug,
|
|
34
|
+
fileName,
|
|
35
|
+
});
|
|
36
|
+
if (!result.isSuccess) {
|
|
37
|
+
setError(dispatcherErrorText(result.error, t));
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
onLinked();
|
|
41
|
+
};
|
|
42
|
+
return (_jsxs("div", { className: "flex flex-col gap-4", children: [error !== null && (_jsx(Banner, { variant: "error", testId: "mail-pdf-link-error", children: error })), _jsx(Field, { id: "mail-pdf-template", label: t("documentDesigner.mail.pdfLink.template"), children: _jsx(Input, { kind: "select", id: "mail-pdf-template", name: "mail-pdf-template", value: slug, onChange: (next) => {
|
|
43
|
+
setSlug(next);
|
|
44
|
+
setEditedName(null);
|
|
45
|
+
}, options: templates.map((template) => ({ value: template.slug, label: template.title })) }) }), _jsx(Field, { id: "mail-pdf-file-name", label: t("documentDesigner.mail.pdfLink.fileName"), description: t("documentDesigner.mail.pdfLink.fileNameHint"), children: _jsx(PlaceholderLineEditor, { id: "mail-pdf-file-name", value: fileName, onChange: setEditedName, variableLabels: variableLabels, "aria-label": t("documentDesigner.mail.pdfLink.fileName") }) }), _jsx("div", { className: "flex justify-end", children: _jsx(Button, { disabled: slug === "" || fileName === "" || linkPdf.pending, loading: linkPdf.pending, onClick: link, testId: "mail-pdf-link-confirm", children: t("documentDesigner.mail.pdfLink.confirm") }) })] }));
|
|
46
|
+
}
|
|
47
|
+
export function PdfLinkModal({ open, onOpenChange, templates, ...form }) {
|
|
48
|
+
const t = useTranslation();
|
|
49
|
+
const { Modal } = usePrimitives();
|
|
50
|
+
return (_jsx(Modal, { open: open, onOpenChange: onOpenChange, title: t("documentDesigner.mail.pdfLink.title"), testId: "mail-pdf-link-modal", children: templates.length === 0 ? (_jsx(EmptyState, { title: t("documentDesigner.mail.pdfLink.none") })) : (_jsx(PdfLinkForm, { templates: templates, ...form, onLinked: () => {
|
|
51
|
+
onOpenChange(false);
|
|
52
|
+
form.onLinked();
|
|
53
|
+
} })) }));
|
|
54
|
+
}
|