@cosmicdrift/kumiko-document-designer 0.389.0 → 0.390.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/web/layout-editor-body.d.ts +3 -14
- package/dist/web/layout-editor-body.js +25 -172
- package/dist/web/layout-editor-center.d.ts +7 -0
- package/dist/web/layout-editor-center.js +23 -0
- package/dist/web/layout-editor-logic.d.ts +97 -0
- package/dist/web/layout-editor-logic.js +143 -0
- package/dist/web/layout-editor-paper.d.ts +4 -0
- package/dist/web/layout-editor-paper.js +24 -0
- package/dist/web/mail-editor-body.d.ts +4 -16
- package/dist/web/mail-editor-body.js +27 -218
- package/dist/web/mail-editor-logic.d.ts +70 -0
- package/dist/web/mail-editor-logic.js +81 -0
- package/dist/web/mail-editor-model.d.ts +105 -0
- package/dist/web/mail-editor-model.js +114 -0
- package/dist/web/mail-editor-render.d.ts +11 -0
- package/dist/web/mail-editor-render.js +51 -0
- package/dist/web/mail-preview-render.d.ts +5 -0
- package/dist/web/mail-preview-render.js +22 -0
- package/dist/web/pdf-editor-actions.d.ts +27 -0
- package/dist/web/pdf-editor-actions.js +146 -0
- package/dist/web/pdf-editor-chrome.d.ts +6 -0
- package/dist/web/pdf-editor-chrome.js +60 -0
- package/dist/web/pdf-editor-context.d.ts +22 -0
- package/dist/web/pdf-editor-context.js +1 -0
- package/dist/web/pdf-editor-draft.d.ts +23 -0
- package/dist/web/pdf-editor-draft.js +33 -0
- package/dist/web/pdf-editor-panes.d.ts +6 -0
- package/dist/web/pdf-editor-panes.js +40 -0
- package/dist/web/pdf-editor-view.d.ts +52 -0
- package/dist/web/pdf-editor-view.js +89 -0
- package/dist/web/pdf-template-editor.d.ts +1 -1
- package/dist/web/pdf-template-editor.js +40 -342
- package/dist/web/use-layout-editor-state.d.ts +62 -0
- package/dist/web/use-layout-editor-state.js +155 -0
- package/dist/web/use-mail-editor-parts.d.ts +42 -0
- package/dist/web/use-mail-editor-parts.js +74 -0
- package/dist/web/use-mail-editor-state.d.ts +89 -0
- package/dist/web/use-mail-editor-state.js +55 -0
- package/dist/web/use-pdf-editor-state.d.ts +77 -0
- package/dist/web/use-pdf-editor-state.js +161 -0
- package/package.json +6 -6
- package/src/changes.json +18 -0
|
@@ -1,230 +1,39 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import { EditorHeader, EditorLocaleSwitch, EditorModeSwitch, originOf, } from "./editor-header.js";
|
|
6
|
-
import { EditorBody, EditorFooter, EditorFrame } from "./editor-shell.js";
|
|
7
|
-
import { fileUrl } from "./file-name.js";
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { usePrimitives, useTranslation } from "@cosmicdrift/kumiko-renderer";
|
|
3
|
+
import { EditorHeader, EditorModeSwitch, originOf } from "./editor-header.js";
|
|
4
|
+
import { EditorFooter, EditorFrame } from "./editor-shell.js";
|
|
8
5
|
import { MailConfirmDialogs } from "./mail-dialogs.js";
|
|
9
|
-
import {
|
|
10
|
-
import { MailEditCenter } from "./mail-edit-center.js";
|
|
11
|
-
import { footerMessage, metaFieldMessages } from "./mail-footer.js";
|
|
12
|
-
import { PlaceholderSection, ResourceSection, SystemPlaceholderSection, } from "./mail-left-rail.js";
|
|
13
|
-
import { MissingLocaleCanvas } from "./mail-missing-locale.js";
|
|
6
|
+
import { renderMailBanners, renderMailControls, renderMailEditBody, renderMailModals, } from "./mail-editor-render.js";
|
|
14
7
|
import { MailOriginBar } from "./mail-origin-bar.js";
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
17
|
-
|
|
18
|
-
import { UploadModal } from "./mail-upload-modal.js";
|
|
19
|
-
import { VersionsSection } from "./mail-versions.js";
|
|
20
|
-
import { useFilesCapability } from "./use-files-capability.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
|
-
import { templateTitleText } from "./used-by-label.js";
|
|
28
|
-
// The editor replaces this token with the decoded image size once the preview image loaded.
|
|
29
|
-
const SIZE_TOKEN = "{size}";
|
|
30
|
-
function uniqueValues(values) {
|
|
31
|
-
return [...new Set(values)];
|
|
32
|
-
}
|
|
33
|
-
export function placeholderNames(validation) {
|
|
34
|
-
if (validation === null)
|
|
35
|
-
return [];
|
|
36
|
-
const own = Object.entries(validation.variables).flatMap(([name, variable]) => [
|
|
37
|
-
name,
|
|
38
|
-
...Object.keys(variable.item ?? {}).map((item) => `${name}.${item}`),
|
|
39
|
-
]);
|
|
40
|
-
return uniqueValues([...own, ...validation.systemPlaceholders]);
|
|
41
|
-
}
|
|
8
|
+
import { renderMailPreview } from "./mail-preview-render.js";
|
|
9
|
+
import { useMailEditorState } from "./use-mail-editor-state.js";
|
|
10
|
+
export { placeholderNames } from "./mail-editor-logic.js";
|
|
42
11
|
export function MailEditorBody(props) {
|
|
43
|
-
const {
|
|
12
|
+
const { locale, title, isSystemAdmin, row, onReload } = props;
|
|
44
13
|
const t = useTranslation();
|
|
45
14
|
const { Banner } = usePrimitives();
|
|
46
|
-
const
|
|
47
|
-
const
|
|
48
|
-
const
|
|
49
|
-
const
|
|
50
|
-
const
|
|
51
|
-
const [dialog, setDialog] = useState({ kind: "none" });
|
|
52
|
-
const [uploadOpen, setUploadOpen] = useState(false);
|
|
53
|
-
const [pdfOpen, setPdfOpen] = useState(false);
|
|
54
|
-
const [resourceError, setResourceError] = useState(null);
|
|
55
|
-
const [resetError, setResetError] = useState(null);
|
|
56
|
-
const [editorEpoch, setEditorEpoch] = useState(0);
|
|
57
|
-
const [includeAttachments, setIncludeAttachments] = useState(true);
|
|
58
|
-
const [prefixSubject, setPrefixSubject] = useState(true);
|
|
59
|
-
const [picked, setPicked] = useState(null);
|
|
60
|
-
const insert = useInsertTargets();
|
|
61
|
-
const persistence = useMailPersistence({
|
|
62
|
-
slug,
|
|
63
|
-
locale,
|
|
64
|
-
title,
|
|
65
|
-
isSystemAdmin,
|
|
66
|
-
row,
|
|
67
|
-
});
|
|
68
|
-
const testMail = useTestMail();
|
|
69
|
-
const own = useOwnAddress();
|
|
70
|
-
const filesAvailable = useFilesCapability();
|
|
71
|
-
const resourceRemove = useMutation(TemplateResolverHandlers.resourceRemove);
|
|
72
|
-
const layouts = useOverviewRows("mail-layout");
|
|
73
|
-
const pdfTemplates = useOverviewRows("document-pdf");
|
|
74
|
-
const locked = row === null && !creating;
|
|
75
|
-
const variableSchema = parseJsonRecord(row?.variableSchema);
|
|
76
|
-
const validation = useQuery(TemplateResolverQueries.validate, {
|
|
77
|
-
slug,
|
|
78
|
-
kind: "mail",
|
|
79
|
-
contentFormat: "html",
|
|
80
|
-
content: settled.body,
|
|
81
|
-
meta: buildMeta(settled),
|
|
82
|
-
variableSchema,
|
|
83
|
-
locale,
|
|
84
|
-
scope: isSystemAdmin ? "system" : "tenant",
|
|
85
|
-
}).data;
|
|
86
|
-
const pickedVariables = pickDeclaredVariables(picked, validation?.variables);
|
|
87
|
-
// A pick belongs to the variables it was made for: after the declared set changes it is dropped,
|
|
88
|
-
// so a re-added variable does not revive an old record value.
|
|
89
|
-
const declaredVariables = validation?.variables;
|
|
90
|
-
const declaredVariableNames = declaredVariables === undefined ? null : Object.keys(declaredVariables).sort().join("|");
|
|
91
|
-
const previousDeclaredNames = useRef(declaredVariableNames);
|
|
92
|
-
useEffect(() => {
|
|
93
|
-
if (declaredVariableNames === null)
|
|
94
|
-
return;
|
|
95
|
-
if (previousDeclaredNames.current !== null &&
|
|
96
|
-
previousDeclaredNames.current !== declaredVariableNames) {
|
|
97
|
-
setPicked(null);
|
|
98
|
-
}
|
|
99
|
-
previousDeclaredNames.current = declaredVariableNames;
|
|
100
|
-
}, [declaredVariableNames]);
|
|
101
|
-
const update = (patch) => {
|
|
102
|
-
const next = { ...draft, ...patch };
|
|
103
|
-
setDraft(next);
|
|
104
|
-
settle(next);
|
|
105
|
-
};
|
|
106
|
-
const reset = () => {
|
|
107
|
-
setDraft(baseline);
|
|
108
|
-
settle(baseline);
|
|
109
|
-
setEditorEpoch((epoch) => epoch + 1);
|
|
110
|
-
persistence.clearError();
|
|
111
|
-
};
|
|
112
|
-
const dirty = isDraftDirty(draft, baseline);
|
|
113
|
-
useUnsavedInputSource(() => dirty);
|
|
114
|
-
const busy = persistence.busy;
|
|
115
|
-
const canSave = !locked && !busy && validation?.canSave === true && (dirty || creating);
|
|
116
|
-
const canPublish = !locked &&
|
|
117
|
-
!busy &&
|
|
118
|
-
validation?.canPublish === true &&
|
|
119
|
-
(dirty || creating || row?.status !== "active");
|
|
120
|
-
const saveAndReload = async (publish) => {
|
|
121
|
-
if ((await persistence.save(draft, publish)) !== "failed")
|
|
122
|
-
await onReload();
|
|
123
|
-
};
|
|
124
|
-
const saveDraft = () => void saveAndReload(false);
|
|
125
|
-
const perform = (target) => props.onLocaleChange(target.locale);
|
|
126
|
-
const leave = (target) => {
|
|
127
|
-
if (target.locale === locale)
|
|
128
|
-
return;
|
|
129
|
-
if (dirty)
|
|
130
|
-
setDialog({ kind: "leave", target });
|
|
131
|
-
else
|
|
132
|
-
perform(target);
|
|
133
|
-
};
|
|
134
|
-
const resources = parseLinkedResources(row?.linkedResources);
|
|
135
|
-
const pdfTitles = new Map(pdfTemplates.rows.map((template) => [template.slug, templateTitleText(t, template.title)]));
|
|
136
|
-
const resourcePreviews = Object.fromEntries(resources.flatMap((resource) => resource.kind === "image"
|
|
137
|
-
? [
|
|
138
|
-
[
|
|
139
|
-
resource.key,
|
|
140
|
-
{
|
|
141
|
-
src: fileUrl(resource.fileRefId),
|
|
142
|
-
caption: t("documentDesigner.mail.resource.caption", {
|
|
143
|
-
key: resource.key,
|
|
144
|
-
size: SIZE_TOKEN,
|
|
145
|
-
}),
|
|
146
|
-
},
|
|
147
|
-
],
|
|
148
|
-
]
|
|
149
|
-
: []));
|
|
150
|
-
const uploadTemplateId = row?.scope === "tenant" ? row.id : null;
|
|
151
|
-
const takenKeys = resources.map((resource) => resource.key);
|
|
152
|
-
const removeResource = async (key) => {
|
|
153
|
-
if (uploadTemplateId === null)
|
|
154
|
-
return;
|
|
155
|
-
setResourceError(null);
|
|
156
|
-
const result = await resourceRemove.mutate({ templateId: uploadTemplateId, key });
|
|
157
|
-
if (result.isSuccess)
|
|
158
|
-
await onRefreshRow();
|
|
159
|
-
else
|
|
160
|
-
setResourceError(dispatcherErrorText(result.error, t));
|
|
161
|
-
};
|
|
162
|
-
const sendTestMail = () => {
|
|
163
|
-
void testMail.send({
|
|
164
|
-
slug,
|
|
165
|
-
locale,
|
|
166
|
-
content: draft.body,
|
|
167
|
-
meta: buildMeta(draft),
|
|
168
|
-
variableSchema,
|
|
169
|
-
includeAttachments,
|
|
170
|
-
prefixSubject,
|
|
171
|
-
...(pickedVariables !== null && { variables: pickedVariables }),
|
|
172
|
-
});
|
|
173
|
-
};
|
|
174
|
-
// Nothing is written yet in a missing language, so no placeholder may read as missing.
|
|
175
|
-
const railValidation = locked && validation != null ? { ...validation, missingRequired: [] } : (validation ?? null);
|
|
176
|
-
const names = placeholderNames(validation ?? null);
|
|
177
|
-
const vocabulary = {
|
|
178
|
-
names,
|
|
179
|
-
labels: {
|
|
180
|
-
...systemLabelMap(t, validation?.systemPlaceholders ?? []),
|
|
181
|
-
...variableLabelMap(t, validation?.variables ?? {}),
|
|
182
|
-
},
|
|
183
|
-
system: validation?.systemPlaceholders ?? [],
|
|
184
|
-
};
|
|
185
|
-
const attachments = resources.filter((resource) => resource.kind !== "image");
|
|
186
|
-
const footer = locked || validation === undefined || validation === null
|
|
187
|
-
? null
|
|
188
|
-
: footerMessage(t, validation, mode === "preview" && testMail.availability.canSend);
|
|
189
|
-
const fieldErrors = locked ? {} : metaFieldMessages(t, validation ?? null);
|
|
190
|
-
const publish = () => void saveAndReload(true);
|
|
191
|
-
const controls = (_jsx(EditorLocaleSwitch, { testIdPrefix: "mail", locales: props.locales, locale: locale, onLocale: (next) => leave({ kind: "locale", locale: next }) }));
|
|
15
|
+
const m = useMailEditorState(props, t);
|
|
16
|
+
const { mode, setMode, setDialog, setResetError } = m.ui;
|
|
17
|
+
const saveDraft = () => void m.saveAndReload(false);
|
|
18
|
+
const publish = () => void m.saveAndReload(true);
|
|
19
|
+
const controls = renderMailControls(props, m);
|
|
192
20
|
const origin = row === null ? null : originOf(row.scope !== "tenant", props.systemVersion !== null);
|
|
193
|
-
return (_jsxs(EditorFrame, { testId: "document-designer-mail-editor", children: [_jsx(EditorHeader, { title: title, stored: row === null ? null : { status: row.status, version: row.version }, actions: {
|
|
21
|
+
return (_jsxs(EditorFrame, { testId: "document-designer-mail-editor", children: [_jsx(EditorHeader, { title: title, stored: row === null ? null : { status: row.status, version: row.version }, actions: {
|
|
22
|
+
busy: m.busy,
|
|
23
|
+
canSave: m.canSave,
|
|
24
|
+
canPublish: m.canPublish,
|
|
25
|
+
onSave: saveDraft,
|
|
26
|
+
onPublish: publish,
|
|
27
|
+
}, children: _jsx(EditorModeSwitch, { testIdPrefix: "mail", mode: mode, onMode: setMode }) }), mode === "edit" && (_jsx(MailOriginBar, { row: row, systemVersion: props.systemVersion, locale: locale, isSystemAdmin: isSystemAdmin, creating: m.ui.creating, controls: controls, onReload: () => {
|
|
194
28
|
setResetError(null);
|
|
195
29
|
void onReload();
|
|
196
|
-
}, onResetError: setResetError })),
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
content: settled.body,
|
|
201
|
-
meta: buildMeta(settled),
|
|
202
|
-
variableSchema,
|
|
203
|
-
...(pickedVariables !== null && { variables: pickedVariables }),
|
|
204
|
-
}, picked: picked, onPick: setPicked, validation: validation ?? null, slug: slug, locale: locale, fromName: settled.senderName, own: own, origin: origin, controls: controls, testMail: {
|
|
205
|
-
availability: testMail.availability,
|
|
206
|
-
pending: testMail.pending,
|
|
207
|
-
onSend: sendTestMail,
|
|
208
|
-
includeAttachments,
|
|
209
|
-
onIncludeAttachments: setIncludeAttachments,
|
|
210
|
-
prefixSubject,
|
|
211
|
-
onPrefixSubject: setPrefixSubject,
|
|
212
|
-
} })), !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: {
|
|
213
|
-
templateId: uploadTemplateId,
|
|
214
|
-
takenKeys,
|
|
215
|
-
onUploaded: (uploaded) => {
|
|
216
|
-
if (uploaded.purpose === "image")
|
|
217
|
-
insert.insertResource(uploaded.key);
|
|
218
|
-
void onRefreshRow();
|
|
219
|
-
},
|
|
220
|
-
} }), _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: () => {
|
|
221
|
-
reset();
|
|
30
|
+
}, onResetError: setResetError })), renderMailBanners(Banner, t, m), mode === "edit"
|
|
31
|
+
? renderMailEditBody(props, m)
|
|
32
|
+
: renderMailPreview(props, m, controls, origin), !m.locked && (_jsx(EditorFooter, { testIdPrefix: "mail-editor", message: m.footer, saveError: m.services.persistence.error, modifiedAt: row?.modifiedAt ?? null, dirty: m.dirty, busy: m.busy, canSave: m.canSave, canPublish: m.canPublish, onDiscard: () => setDialog({ kind: "discard" }), onSave: saveDraft, onPublish: publish })), renderMailModals(props, m), _jsx(MailConfirmDialogs, { dialog: m.ui.dialog, onClose: () => setDialog({ kind: "none" }), onDiscard: () => {
|
|
33
|
+
m.reset();
|
|
222
34
|
setDialog({ kind: "none" });
|
|
223
|
-
}, onRemove: removeResource, onLeaveSave:
|
|
224
|
-
if ((await persistence.save(draft, false)) === "saved")
|
|
225
|
-
perform(target);
|
|
226
|
-
}, onLeaveDiscard: (target) => {
|
|
35
|
+
}, onRemove: m.removeResource, onLeaveSave: m.leaveSave, onLeaveDiscard: (target) => {
|
|
227
36
|
setDialog({ kind: "none" });
|
|
228
|
-
perform(target);
|
|
37
|
+
m.perform(target);
|
|
229
38
|
} })] }));
|
|
230
39
|
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import type { EditorVocabulary } from "./mail-edit-center.tsx";
|
|
2
|
+
import type { LinkedResourceView, MailDraft, MailValidation, PickedValues, TemplateRowView } from "./mail-types.ts";
|
|
3
|
+
type Translate = (key: string, params?: Readonly<Record<string, unknown>>) => string;
|
|
4
|
+
export type MailEditorBodyProps = {
|
|
5
|
+
readonly slug: string;
|
|
6
|
+
readonly locale: string;
|
|
7
|
+
readonly locales: readonly string[];
|
|
8
|
+
readonly title: string;
|
|
9
|
+
readonly isSystemAdmin: boolean;
|
|
10
|
+
readonly row: TemplateRowView | null;
|
|
11
|
+
readonly systemVersion: number | null;
|
|
12
|
+
readonly fallbackLocale: string | null;
|
|
13
|
+
readonly onLocaleChange: (locale: string) => void;
|
|
14
|
+
readonly onReload: () => Promise<void>;
|
|
15
|
+
readonly onRefreshRow: () => Promise<void>;
|
|
16
|
+
};
|
|
17
|
+
export declare function placeholderNames(validation: MailValidation | null): string[];
|
|
18
|
+
export declare function mailSaveFlags(input: {
|
|
19
|
+
readonly locked: boolean;
|
|
20
|
+
readonly busy: boolean;
|
|
21
|
+
readonly validation: MailValidation | null;
|
|
22
|
+
readonly dirty: boolean;
|
|
23
|
+
readonly creating: boolean;
|
|
24
|
+
readonly row: TemplateRowView | null;
|
|
25
|
+
}): {
|
|
26
|
+
readonly canSave: boolean;
|
|
27
|
+
readonly canPublish: boolean;
|
|
28
|
+
};
|
|
29
|
+
export declare function resourcePreviewsOfMail(resources: readonly LinkedResourceView[], t: Translate): {
|
|
30
|
+
[k: string]: {
|
|
31
|
+
src: string;
|
|
32
|
+
caption: string;
|
|
33
|
+
};
|
|
34
|
+
};
|
|
35
|
+
export declare function railValidationOf(locked: boolean, validation: MailValidation | null): MailValidation | null;
|
|
36
|
+
export declare function vocabularyOf(t: Translate, validation: MailValidation | null): EditorVocabulary;
|
|
37
|
+
export declare function testMailPayload(input: {
|
|
38
|
+
readonly slug: string;
|
|
39
|
+
readonly locale: string;
|
|
40
|
+
readonly draft: MailDraft;
|
|
41
|
+
readonly variableSchema: Readonly<Record<string, unknown>>;
|
|
42
|
+
readonly includeAttachments: boolean;
|
|
43
|
+
readonly prefixSubject: boolean;
|
|
44
|
+
readonly pickedVariables: PickedValues | null;
|
|
45
|
+
}): {
|
|
46
|
+
slug: string;
|
|
47
|
+
locale: string;
|
|
48
|
+
content: string;
|
|
49
|
+
meta: Record<string, unknown>;
|
|
50
|
+
variableSchema: Readonly<Record<string, unknown>>;
|
|
51
|
+
includeAttachments: boolean;
|
|
52
|
+
prefixSubject: boolean;
|
|
53
|
+
variables?: Readonly<Record<string, unknown>> | undefined;
|
|
54
|
+
};
|
|
55
|
+
export declare function previewPayload(input: {
|
|
56
|
+
readonly slug: string;
|
|
57
|
+
readonly locale: string;
|
|
58
|
+
readonly settled: MailDraft;
|
|
59
|
+
readonly variableSchema: Readonly<Record<string, unknown>>;
|
|
60
|
+
readonly pickedVariables: PickedValues | null;
|
|
61
|
+
}): {
|
|
62
|
+
slug: string;
|
|
63
|
+
kind: "mail";
|
|
64
|
+
locale: string;
|
|
65
|
+
content: string;
|
|
66
|
+
meta: Record<string, unknown>;
|
|
67
|
+
variableSchema: Readonly<Record<string, unknown>>;
|
|
68
|
+
variables?: Readonly<Record<string, unknown>> | undefined;
|
|
69
|
+
};
|
|
70
|
+
export {};
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { fileUrl } from "./file-name.js";
|
|
2
|
+
import { buildMeta, systemLabelMap, variableLabelMap } from "./mail-draft.js";
|
|
3
|
+
// The editor replaces this token with the decoded image size once the preview image loaded.
|
|
4
|
+
const SIZE_TOKEN = "{size}";
|
|
5
|
+
function uniqueValues(values) {
|
|
6
|
+
return [...new Set(values)];
|
|
7
|
+
}
|
|
8
|
+
export function placeholderNames(validation) {
|
|
9
|
+
if (validation === null)
|
|
10
|
+
return [];
|
|
11
|
+
const own = Object.entries(validation.variables).flatMap(([name, variable]) => [
|
|
12
|
+
name,
|
|
13
|
+
...Object.keys(variable.item ?? {}).map((item) => `${name}.${item}`),
|
|
14
|
+
]);
|
|
15
|
+
return uniqueValues([...own, ...validation.systemPlaceholders]);
|
|
16
|
+
}
|
|
17
|
+
export function mailSaveFlags(input) {
|
|
18
|
+
const { locked, busy, validation, dirty, creating, row } = input;
|
|
19
|
+
return {
|
|
20
|
+
canSave: !locked && !busy && validation?.canSave === true && (dirty || creating),
|
|
21
|
+
canPublish: !locked &&
|
|
22
|
+
!busy &&
|
|
23
|
+
validation?.canPublish === true &&
|
|
24
|
+
(dirty || creating || row?.status !== "active"),
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
export function resourcePreviewsOfMail(resources, t) {
|
|
28
|
+
return Object.fromEntries(resources.flatMap((resource) => resource.kind === "image"
|
|
29
|
+
? [
|
|
30
|
+
[
|
|
31
|
+
resource.key,
|
|
32
|
+
{
|
|
33
|
+
src: fileUrl(resource.fileRefId),
|
|
34
|
+
caption: t("documentDesigner.mail.resource.caption", {
|
|
35
|
+
key: resource.key,
|
|
36
|
+
size: SIZE_TOKEN,
|
|
37
|
+
}),
|
|
38
|
+
},
|
|
39
|
+
],
|
|
40
|
+
]
|
|
41
|
+
: []));
|
|
42
|
+
}
|
|
43
|
+
// Nothing is written yet in a missing language, so no placeholder may read as missing.
|
|
44
|
+
export function railValidationOf(locked, validation) {
|
|
45
|
+
return locked && validation != null
|
|
46
|
+
? { ...validation, missingRequired: [] }
|
|
47
|
+
: (validation ?? null);
|
|
48
|
+
}
|
|
49
|
+
export function vocabularyOf(t, validation) {
|
|
50
|
+
return {
|
|
51
|
+
names: placeholderNames(validation ?? null),
|
|
52
|
+
labels: {
|
|
53
|
+
...systemLabelMap(t, validation?.systemPlaceholders ?? []),
|
|
54
|
+
...variableLabelMap(t, validation?.variables ?? {}),
|
|
55
|
+
},
|
|
56
|
+
system: validation?.systemPlaceholders ?? [],
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
export function testMailPayload(input) {
|
|
60
|
+
return {
|
|
61
|
+
slug: input.slug,
|
|
62
|
+
locale: input.locale,
|
|
63
|
+
content: input.draft.body,
|
|
64
|
+
meta: buildMeta(input.draft),
|
|
65
|
+
variableSchema: input.variableSchema,
|
|
66
|
+
includeAttachments: input.includeAttachments,
|
|
67
|
+
prefixSubject: input.prefixSubject,
|
|
68
|
+
...(input.pickedVariables !== null && { variables: input.pickedVariables }),
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
export function previewPayload(input) {
|
|
72
|
+
return {
|
|
73
|
+
slug: input.slug,
|
|
74
|
+
kind: "mail",
|
|
75
|
+
locale: input.locale,
|
|
76
|
+
content: input.settled.body,
|
|
77
|
+
meta: buildMeta(input.settled),
|
|
78
|
+
variableSchema: input.variableSchema,
|
|
79
|
+
...(input.pickedVariables !== null && { variables: input.pickedVariables }),
|
|
80
|
+
};
|
|
81
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import type { LeaveTarget } from "./mail-dialogs.tsx";
|
|
2
|
+
import { type MailEditorBodyProps } from "./mail-editor-logic.ts";
|
|
3
|
+
import type { MailDraft, MailValidation, PickedValues } from "./mail-types.ts";
|
|
4
|
+
import type { MailServices, MailUiState } from "./use-mail-editor-parts.ts";
|
|
5
|
+
type Translate = (key: string, params?: Readonly<Record<string, unknown>>) => string;
|
|
6
|
+
export type MailModelInput = {
|
|
7
|
+
readonly props: MailEditorBodyProps;
|
|
8
|
+
readonly t: Translate;
|
|
9
|
+
readonly ui: MailUiState;
|
|
10
|
+
readonly picked: PickedValues | null;
|
|
11
|
+
readonly setPicked: (next: PickedValues | null) => void;
|
|
12
|
+
readonly services: MailServices;
|
|
13
|
+
readonly validation: MailValidation | null;
|
|
14
|
+
readonly variableSchema: Readonly<Record<string, unknown>>;
|
|
15
|
+
readonly pickedVariables: PickedValues | null;
|
|
16
|
+
readonly locked: boolean;
|
|
17
|
+
readonly dirty: boolean;
|
|
18
|
+
};
|
|
19
|
+
export declare function buildMailModel(input: MailModelInput): {
|
|
20
|
+
update: (patch: Partial<MailDraft>) => void;
|
|
21
|
+
reset: () => void;
|
|
22
|
+
saveAndReload: (publish: boolean) => Promise<void>;
|
|
23
|
+
perform: (target: LeaveTarget) => void;
|
|
24
|
+
leave: (target: LeaveTarget) => void;
|
|
25
|
+
leaveSave: (target: LeaveTarget) => Promise<void>;
|
|
26
|
+
removeResource: (key: string) => Promise<void>;
|
|
27
|
+
sendTestMail: () => void;
|
|
28
|
+
ui: {
|
|
29
|
+
baseline: MailDraft;
|
|
30
|
+
draft: MailDraft;
|
|
31
|
+
setDraft: import("react").Dispatch<import("react").SetStateAction<MailDraft>>;
|
|
32
|
+
settled: MailDraft;
|
|
33
|
+
settle: (next: MailDraft) => void;
|
|
34
|
+
mode: import("./editor-header.tsx").EditorMode;
|
|
35
|
+
setMode: import("react").Dispatch<import("react").SetStateAction<import("./editor-header.tsx").EditorMode>>;
|
|
36
|
+
creating: boolean;
|
|
37
|
+
setCreating: import("react").Dispatch<import("react").SetStateAction<boolean>>;
|
|
38
|
+
dialog: import("./mail-dialogs.tsx").ConfirmDialog;
|
|
39
|
+
setDialog: import("react").Dispatch<import("react").SetStateAction<import("./mail-dialogs.tsx").ConfirmDialog>>;
|
|
40
|
+
uploadOpen: boolean;
|
|
41
|
+
setUploadOpen: import("react").Dispatch<import("react").SetStateAction<boolean>>;
|
|
42
|
+
pdfOpen: boolean;
|
|
43
|
+
setPdfOpen: import("react").Dispatch<import("react").SetStateAction<boolean>>;
|
|
44
|
+
resourceError: string | null;
|
|
45
|
+
setResourceError: import("react").Dispatch<import("react").SetStateAction<string | null>>;
|
|
46
|
+
resetError: string | null;
|
|
47
|
+
setResetError: import("react").Dispatch<import("react").SetStateAction<string | null>>;
|
|
48
|
+
editorEpoch: number;
|
|
49
|
+
setEditorEpoch: import("react").Dispatch<import("react").SetStateAction<number>>;
|
|
50
|
+
includeAttachments: boolean;
|
|
51
|
+
setIncludeAttachments: import("react").Dispatch<import("react").SetStateAction<boolean>>;
|
|
52
|
+
prefixSubject: boolean;
|
|
53
|
+
setPrefixSubject: import("react").Dispatch<import("react").SetStateAction<boolean>>;
|
|
54
|
+
};
|
|
55
|
+
services: {
|
|
56
|
+
insert: import("./use-insert-targets.ts").InsertTargets;
|
|
57
|
+
persistence: import("./use-mail-persistence.ts").MailPersistence;
|
|
58
|
+
testMail: import("./use-test-mail.ts").TestMail;
|
|
59
|
+
own: import("./use-own-address.ts").OwnAddress;
|
|
60
|
+
filesAvailable: boolean;
|
|
61
|
+
resourceRemove: import("@cosmicdrift/kumiko-renderer").UseMutationResult<unknown>;
|
|
62
|
+
layouts: import("./use-overview-rows.ts").OverviewRows;
|
|
63
|
+
pdfTemplates: import("./use-overview-rows.ts").OverviewRows;
|
|
64
|
+
};
|
|
65
|
+
validation: MailValidation | null;
|
|
66
|
+
picked: Readonly<Record<string, unknown>> | null;
|
|
67
|
+
setPicked: (next: PickedValues | null) => void;
|
|
68
|
+
pickedVariables: Readonly<Record<string, unknown>> | null;
|
|
69
|
+
variableSchema: Readonly<Record<string, unknown>>;
|
|
70
|
+
locked: boolean;
|
|
71
|
+
dirty: boolean;
|
|
72
|
+
busy: boolean;
|
|
73
|
+
canSave: boolean;
|
|
74
|
+
canPublish: boolean;
|
|
75
|
+
resources: readonly import("./mail-types.ts").LinkedResourceView[];
|
|
76
|
+
pdfTitles: Map<string, string>;
|
|
77
|
+
uploadTemplateId: string | null;
|
|
78
|
+
takenKeys: string[];
|
|
79
|
+
attachments: ({
|
|
80
|
+
readonly kind: "file";
|
|
81
|
+
readonly key: string;
|
|
82
|
+
readonly fileRefId: string;
|
|
83
|
+
readonly fileName: string;
|
|
84
|
+
} | {
|
|
85
|
+
readonly kind: "pdf";
|
|
86
|
+
readonly key: string;
|
|
87
|
+
readonly templateSlug: string;
|
|
88
|
+
readonly fileName: string;
|
|
89
|
+
})[];
|
|
90
|
+
resourcePreviews: {
|
|
91
|
+
[k: string]: {
|
|
92
|
+
src: string;
|
|
93
|
+
caption: string;
|
|
94
|
+
};
|
|
95
|
+
};
|
|
96
|
+
railValidation: MailValidation | null;
|
|
97
|
+
vocabulary: import("./mail-edit-center.tsx").EditorVocabulary;
|
|
98
|
+
footer: {
|
|
99
|
+
readonly tone: "bad" | "warn" | "ok";
|
|
100
|
+
readonly text: string;
|
|
101
|
+
} | null;
|
|
102
|
+
fieldErrors: Partial<Record<"previewText" | "subject", string>>;
|
|
103
|
+
};
|
|
104
|
+
export type MailEditorModel = ReturnType<typeof buildMailModel>;
|
|
105
|
+
export {};
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { dispatcherErrorText } from "@cosmicdrift/kumiko-renderer";
|
|
2
|
+
import { parseLinkedResources } from "./mail-draft.js";
|
|
3
|
+
import { mailSaveFlags, railValidationOf, resourcePreviewsOfMail, testMailPayload, vocabularyOf, } from "./mail-editor-logic.js";
|
|
4
|
+
import { footerMessage, metaFieldMessages } from "./mail-footer.js";
|
|
5
|
+
import { templateTitleText } from "./used-by-label.js";
|
|
6
|
+
function mailActions(input) {
|
|
7
|
+
const { props, ui, services } = input;
|
|
8
|
+
const { draft, baseline, settle } = ui;
|
|
9
|
+
const { persistence } = services;
|
|
10
|
+
const update = (patch) => {
|
|
11
|
+
const next = { ...draft, ...patch };
|
|
12
|
+
ui.setDraft(next);
|
|
13
|
+
settle(next);
|
|
14
|
+
};
|
|
15
|
+
const reset = () => {
|
|
16
|
+
ui.setDraft(baseline);
|
|
17
|
+
settle(baseline);
|
|
18
|
+
ui.setEditorEpoch((epoch) => epoch + 1);
|
|
19
|
+
persistence.clearError();
|
|
20
|
+
};
|
|
21
|
+
const saveAndReload = async (publish) => {
|
|
22
|
+
if ((await persistence.save(draft, publish)) !== "failed")
|
|
23
|
+
await props.onReload();
|
|
24
|
+
};
|
|
25
|
+
const perform = (target) => props.onLocaleChange(target.locale);
|
|
26
|
+
const leave = (target) => {
|
|
27
|
+
// skip: the target locale is already open
|
|
28
|
+
if (target.locale === props.locale)
|
|
29
|
+
return;
|
|
30
|
+
if (input.dirty)
|
|
31
|
+
ui.setDialog({ kind: "leave", target });
|
|
32
|
+
else
|
|
33
|
+
perform(target);
|
|
34
|
+
};
|
|
35
|
+
const leaveSave = async (target) => {
|
|
36
|
+
if ((await persistence.save(draft, false)) === "saved")
|
|
37
|
+
perform(target);
|
|
38
|
+
};
|
|
39
|
+
return { update, reset, saveAndReload, perform, leave, leaveSave };
|
|
40
|
+
}
|
|
41
|
+
function resourceActions(input, uploadTemplateId) {
|
|
42
|
+
const { props, t, ui, services } = input;
|
|
43
|
+
const removeResource = async (key) => {
|
|
44
|
+
// skip: only tenant rows own removable resources
|
|
45
|
+
if (uploadTemplateId === null)
|
|
46
|
+
return;
|
|
47
|
+
ui.setResourceError(null);
|
|
48
|
+
const result = await services.resourceRemove.mutate({ templateId: uploadTemplateId, key });
|
|
49
|
+
if (result.isSuccess)
|
|
50
|
+
await props.onRefreshRow();
|
|
51
|
+
else
|
|
52
|
+
ui.setResourceError(dispatcherErrorText(result.error, t));
|
|
53
|
+
};
|
|
54
|
+
const sendTestMail = () => {
|
|
55
|
+
void services.testMail.send(testMailPayload({
|
|
56
|
+
slug: props.slug,
|
|
57
|
+
locale: props.locale,
|
|
58
|
+
draft: ui.draft,
|
|
59
|
+
variableSchema: input.variableSchema,
|
|
60
|
+
includeAttachments: ui.includeAttachments,
|
|
61
|
+
prefixSubject: ui.prefixSubject,
|
|
62
|
+
pickedVariables: input.pickedVariables,
|
|
63
|
+
}));
|
|
64
|
+
};
|
|
65
|
+
return { removeResource, sendTestMail };
|
|
66
|
+
}
|
|
67
|
+
export function buildMailModel(input) {
|
|
68
|
+
const { props, t, ui, services, validation, locked } = input;
|
|
69
|
+
const { row } = props;
|
|
70
|
+
const busy = services.persistence.busy;
|
|
71
|
+
const { canSave, canPublish } = mailSaveFlags({
|
|
72
|
+
locked,
|
|
73
|
+
busy,
|
|
74
|
+
validation,
|
|
75
|
+
dirty: input.dirty,
|
|
76
|
+
creating: ui.creating,
|
|
77
|
+
row,
|
|
78
|
+
});
|
|
79
|
+
const resources = parseLinkedResources(row?.linkedResources);
|
|
80
|
+
const pdfTitles = new Map(services.pdfTemplates.rows.map((template) => [
|
|
81
|
+
template.slug,
|
|
82
|
+
templateTitleText(t, template.title),
|
|
83
|
+
]));
|
|
84
|
+
const uploadTemplateId = row?.scope === "tenant" ? row.id : null;
|
|
85
|
+
const footer = locked || validation === undefined || validation === null
|
|
86
|
+
? null
|
|
87
|
+
: footerMessage(t, validation, ui.mode === "preview" && services.testMail.availability.canSend);
|
|
88
|
+
return {
|
|
89
|
+
ui,
|
|
90
|
+
services,
|
|
91
|
+
validation,
|
|
92
|
+
picked: input.picked,
|
|
93
|
+
setPicked: input.setPicked,
|
|
94
|
+
pickedVariables: input.pickedVariables,
|
|
95
|
+
variableSchema: input.variableSchema,
|
|
96
|
+
locked,
|
|
97
|
+
dirty: input.dirty,
|
|
98
|
+
busy,
|
|
99
|
+
canSave,
|
|
100
|
+
canPublish,
|
|
101
|
+
...mailActions(input),
|
|
102
|
+
...resourceActions(input, uploadTemplateId),
|
|
103
|
+
resources,
|
|
104
|
+
pdfTitles,
|
|
105
|
+
uploadTemplateId,
|
|
106
|
+
takenKeys: resources.map((resource) => resource.key),
|
|
107
|
+
attachments: resources.filter((resource) => resource.kind !== "image"),
|
|
108
|
+
resourcePreviews: resourcePreviewsOfMail(resources, t),
|
|
109
|
+
railValidation: railValidationOf(locked, validation),
|
|
110
|
+
vocabulary: vocabularyOf(t, validation),
|
|
111
|
+
footer,
|
|
112
|
+
fieldErrors: locked ? {} : metaFieldMessages(t, validation),
|
|
113
|
+
};
|
|
114
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { usePrimitives } from "@cosmicdrift/kumiko-renderer";
|
|
2
|
+
import type { ReactNode } from "react";
|
|
3
|
+
import type { MailEditorBodyProps } from "./mail-editor-logic.ts";
|
|
4
|
+
import type { MailEditorModel } from "./mail-editor-model.ts";
|
|
5
|
+
type Translate = (key: string, params?: Readonly<Record<string, unknown>>) => string;
|
|
6
|
+
type Banner = ReturnType<typeof usePrimitives>["Banner"];
|
|
7
|
+
export declare function renderMailControls(props: MailEditorBodyProps, m: MailEditorModel): ReactNode;
|
|
8
|
+
export declare function renderMailBanners(Banner: Banner, t: Translate, m: MailEditorModel): ReactNode;
|
|
9
|
+
export declare function renderMailEditBody(props: MailEditorBodyProps, m: MailEditorModel): ReactNode;
|
|
10
|
+
export declare function renderMailModals(props: MailEditorBodyProps, m: MailEditorModel): ReactNode;
|
|
11
|
+
export {};
|