@cosmicdrift/kumiko-document-designer 0.388.0 → 0.390.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/feature.js +11 -3
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/list-definition.d.ts +3 -1
- package/dist/list-definition.js +10 -3
- package/dist/web/client-plugin.js +6 -2
- package/dist/web/constants.d.ts +1 -0
- package/dist/web/constants.js +1 -0
- package/dist/web/i18n.js +1 -0
- 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/layout-editor.d.ts +3 -1
- package/dist/web/layout-editor.js +7 -3
- package/dist/web/mail-editor-body.d.ts +4 -16
- package/dist/web/mail-editor-body.js +27 -204
- 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 +30 -0
|
@@ -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 {};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { EditorLocaleSwitch } from "./editor-header.js";
|
|
3
|
+
import { EditorBody } from "./editor-shell.js";
|
|
4
|
+
import { MailEditCenter } from "./mail-edit-center.js";
|
|
5
|
+
import { PlaceholderSection, ResourceSection, SystemPlaceholderSection, } from "./mail-left-rail.js";
|
|
6
|
+
import { MissingLocaleCanvas } from "./mail-missing-locale.js";
|
|
7
|
+
import { PdfLinkModal } from "./mail-pdf-link-modal.js";
|
|
8
|
+
import { AttachmentSection, ChromeSection, PropertiesSection, UsageSection, } from "./mail-right-rail.js";
|
|
9
|
+
import { UploadModal } from "./mail-upload-modal.js";
|
|
10
|
+
import { VersionsSection } from "./mail-versions.js";
|
|
11
|
+
export function renderMailControls(props, m) {
|
|
12
|
+
return (_jsx(EditorLocaleSwitch, { testIdPrefix: "mail", locales: props.locales, locale: props.locale, onLocale: (next) => m.leave({ kind: "locale", locale: next }) }));
|
|
13
|
+
}
|
|
14
|
+
export function renderMailBanners(Banner, t, m) {
|
|
15
|
+
const { testMail } = m.services;
|
|
16
|
+
const { resetError, resourceError } = m.ui;
|
|
17
|
+
return (_jsxs(_Fragment, { children: [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 }))] }));
|
|
18
|
+
}
|
|
19
|
+
function renderLeftRail(m) {
|
|
20
|
+
const { insert, filesAvailable } = m.services;
|
|
21
|
+
const { setUploadOpen, setDialog } = m.ui;
|
|
22
|
+
return (_jsxs(_Fragment, { children: [_jsx(PlaceholderSection, { validation: m.railValidation, onInsert: insert.insertPlaceholder }), _jsx(SystemPlaceholderSection, { validation: m.railValidation, onInsert: insert.insertPlaceholder }), _jsx(ResourceSection, { resources: m.resources, pdfTitles: m.pdfTitles, canUpload: m.uploadTemplateId !== null, filesAvailable: filesAvailable, onUpload: () => setUploadOpen(true), onInsert: insert.insertResource, onRemove: (key) => setDialog({ kind: "remove", key }) })] }));
|
|
23
|
+
}
|
|
24
|
+
function renderCenterCanvas(props, m) {
|
|
25
|
+
if (m.locked) {
|
|
26
|
+
return (_jsx(MissingLocaleCanvas, { slug: props.slug, locale: props.locale, fallbackLocale: props.fallbackLocale, copyScope: props.isSystemAdmin ? "system" : "tenant", onCreateBlank: () => m.ui.setCreating(true), onCopied: () => void props.onReload() }));
|
|
27
|
+
}
|
|
28
|
+
return (_jsx(MailEditCenter, { draft: m.ui.draft, vocabulary: m.vocabulary, resourcePreviews: m.resourcePreviews, insert: m.services.insert, readOnly: false, fieldErrors: m.fieldErrors, onChange: m.update }, m.ui.editorEpoch));
|
|
29
|
+
}
|
|
30
|
+
function renderRightRail(props, m) {
|
|
31
|
+
const { row } = props;
|
|
32
|
+
return (_jsxs(_Fragment, { children: [_jsx(PropertiesSection, { slug: props.slug, draft: m.ui.draft, layouts: m.services.layouts.rows, onChange: m.update }), _jsx(ChromeSection, { draft: m.ui.draft, vocabulary: m.vocabulary, insert: m.services.insert, onChange: m.update }), _jsx(AttachmentSection, { attachments: m.attachments, pdfTitles: m.pdfTitles, canEdit: m.uploadTemplateId !== null, onAddPdf: () => m.ui.setPdfOpen(true), onRemove: (key) => m.ui.setDialog({ kind: "remove", key }) }), _jsx(UsageSection, { slug: props.slug }), _jsx(VersionsSection, { rowId: row?.id ?? null, currentVersion: row?.version ?? 0, currentStatus: row?.status ?? "draft" })] }));
|
|
33
|
+
}
|
|
34
|
+
export function renderMailEditBody(props, m) {
|
|
35
|
+
return (_jsx(EditorBody, { left: renderLeftRail(m), center: renderCenterCanvas(props, m), right: renderRightRail(props, m) }));
|
|
36
|
+
}
|
|
37
|
+
export function renderMailModals(props, m) {
|
|
38
|
+
const { uploadTemplateId, takenKeys } = m;
|
|
39
|
+
if (uploadTemplateId === null)
|
|
40
|
+
return false; // skip: only tenant rows accept uploads
|
|
41
|
+
const { insert, pdfTemplates } = m.services;
|
|
42
|
+
return (_jsxs(_Fragment, { children: [_jsx(UploadModal, { open: m.ui.uploadOpen, onOpenChange: m.ui.setUploadOpen, target: {
|
|
43
|
+
templateId: uploadTemplateId,
|
|
44
|
+
takenKeys,
|
|
45
|
+
onUploaded: (uploaded) => {
|
|
46
|
+
if (uploaded.purpose === "image")
|
|
47
|
+
insert.insertResource(uploaded.key);
|
|
48
|
+
void props.onRefreshRow();
|
|
49
|
+
},
|
|
50
|
+
} }), _jsx(PdfLinkModal, { open: m.ui.pdfOpen, onOpenChange: m.ui.setPdfOpen, templates: pdfTemplates.rows, variableLabels: m.vocabulary.labels, templateId: uploadTemplateId, takenKeys: takenKeys, onLinked: () => void props.onRefreshRow() })] }));
|
|
51
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import type { MailEditorBodyProps } from "./mail-editor-logic.ts";
|
|
3
|
+
import type { MailEditorModel } from "./mail-editor-model.ts";
|
|
4
|
+
import { MailPreviewMode } from "./mail-preview.tsx";
|
|
5
|
+
export declare function renderMailPreview(props: MailEditorBodyProps, m: MailEditorModel, controls: ReactNode, origin: React.ComponentProps<typeof MailPreviewMode>["origin"]): ReactNode;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { previewPayload } from "./mail-editor-logic.js";
|
|
3
|
+
import { MailPreviewMode } from "./mail-preview.js";
|
|
4
|
+
export function renderMailPreview(props, m, controls, origin) {
|
|
5
|
+
const { ui, services } = m;
|
|
6
|
+
const { testMail } = services;
|
|
7
|
+
return (_jsx(MailPreviewMode, { payload: previewPayload({
|
|
8
|
+
slug: props.slug,
|
|
9
|
+
locale: props.locale,
|
|
10
|
+
settled: ui.settled,
|
|
11
|
+
variableSchema: m.variableSchema,
|
|
12
|
+
pickedVariables: m.pickedVariables,
|
|
13
|
+
}), picked: m.picked, onPick: m.setPicked, validation: m.validation ?? null, slug: props.slug, locale: props.locale, fromName: ui.settled.senderName, own: services.own, origin: origin, controls: controls, testMail: {
|
|
14
|
+
availability: testMail.availability,
|
|
15
|
+
pending: testMail.pending,
|
|
16
|
+
onSend: m.sendTestMail,
|
|
17
|
+
includeAttachments: ui.includeAttachments,
|
|
18
|
+
onIncludeAttachments: ui.setIncludeAttachments,
|
|
19
|
+
prefixSubject: ui.prefixSubject,
|
|
20
|
+
onPrefixSubject: ui.setPrefixSubject,
|
|
21
|
+
} }));
|
|
22
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { useDispatcher } from "@cosmicdrift/kumiko-renderer";
|
|
2
|
+
import { type Draft, type EditorState } from "./pdf-editor-draft.ts";
|
|
3
|
+
import { type TemplateDetail } from "./types.ts";
|
|
4
|
+
import type { PdfEditorState } from "./use-pdf-editor-state.ts";
|
|
5
|
+
type Translate = (key: string, params?: Readonly<Record<string, unknown>>) => string;
|
|
6
|
+
export type PdfActionsInput = {
|
|
7
|
+
readonly s: PdfEditorState;
|
|
8
|
+
readonly t: Translate;
|
|
9
|
+
readonly dispatcher: ReturnType<typeof useDispatcher>;
|
|
10
|
+
readonly slug: string;
|
|
11
|
+
readonly current: EditorState;
|
|
12
|
+
readonly detail: TemplateDetail;
|
|
13
|
+
readonly dirty: boolean;
|
|
14
|
+
readonly onLocaleChange: ((locale: string) => void) | undefined;
|
|
15
|
+
};
|
|
16
|
+
export declare function buildPdfActions(input: PdfActionsInput): {
|
|
17
|
+
updateDraft: (next: Draft) => void;
|
|
18
|
+
resetDraft: (source: TemplateDetail) => void;
|
|
19
|
+
save: () => Promise<boolean>;
|
|
20
|
+
publish: () => Promise<void>;
|
|
21
|
+
generatePdf: () => Promise<void>;
|
|
22
|
+
printOnce: () => void;
|
|
23
|
+
uploadResource: (file: File) => Promise<void>;
|
|
24
|
+
requestLocale: (next: string) => void;
|
|
25
|
+
};
|
|
26
|
+
export type PdfActions = ReturnType<typeof buildPdfActions>;
|
|
27
|
+
export {};
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { TemplateResolverHandlers } from "@cosmicdrift/kumiko-bundled-features/template-resolver/web";
|
|
2
|
+
import { uploadFileRef } from "@cosmicdrift/kumiko-renderer/web";
|
|
3
|
+
import { RendererPuppeteerClientQueries } from "@cosmicdrift/kumiko-renderer-puppeteer-client/qualified-names";
|
|
4
|
+
import { describeError, isAlreadyActive } from "./errors.js";
|
|
5
|
+
import { draftFromDetail, SETTLE_MS, snapshotOf, } from "./pdf-editor-draft.js";
|
|
6
|
+
import { serializePdfMeta } from "./pdf-meta.js";
|
|
7
|
+
import { DOCUMENT_PDF_KIND, } from "./types.js";
|
|
8
|
+
function draftActions(input) {
|
|
9
|
+
const { s } = input;
|
|
10
|
+
const { setState } = s.doc;
|
|
11
|
+
const { settleTimer } = s;
|
|
12
|
+
function updateDraft(next) {
|
|
13
|
+
setState((previous) => (previous === null ? previous : { ...previous, draft: next }));
|
|
14
|
+
if (settleTimer.current !== null)
|
|
15
|
+
clearTimeout(settleTimer.current);
|
|
16
|
+
settleTimer.current = setTimeout(() => {
|
|
17
|
+
setState((previous) => previous === null ? previous : { ...previous, settled: previous.draft });
|
|
18
|
+
}, SETTLE_MS);
|
|
19
|
+
}
|
|
20
|
+
function resetDraft(source) {
|
|
21
|
+
const draft = draftFromDetail(source);
|
|
22
|
+
if (settleTimer.current !== null)
|
|
23
|
+
clearTimeout(settleTimer.current);
|
|
24
|
+
setState({ key: source.id, draft, settled: draft, snapshot: snapshotOf(draft) });
|
|
25
|
+
s.ui.setDraftEpoch((epoch) => epoch + 1);
|
|
26
|
+
s.ui.setActionError(null);
|
|
27
|
+
}
|
|
28
|
+
return { updateDraft, resetDraft };
|
|
29
|
+
}
|
|
30
|
+
function writeActions(input) {
|
|
31
|
+
const { s, t, dispatcher, slug, current, detail, dirty } = input;
|
|
32
|
+
const { locale, rowsQuery, detailQuery, setState } = s.doc;
|
|
33
|
+
const { variableSchema, validationQuery } = s.server;
|
|
34
|
+
const { scope } = s.host;
|
|
35
|
+
const { setBusy, setActionError } = s.ui;
|
|
36
|
+
async function reload() {
|
|
37
|
+
await Promise.all([rowsQuery.refetch(), detailQuery.refetch(), validationQuery.refetch()]);
|
|
38
|
+
}
|
|
39
|
+
async function persist() {
|
|
40
|
+
const { draft } = current;
|
|
41
|
+
const parentTemplateId = detail?.scope === "system" ? detail.id : detail?.parentTemplateId;
|
|
42
|
+
const result = await dispatcher.write(scope === "system"
|
|
43
|
+
? TemplateResolverHandlers.upsertSystem
|
|
44
|
+
: TemplateResolverHandlers.upsertTenant, {
|
|
45
|
+
slug,
|
|
46
|
+
kind: DOCUMENT_PDF_KIND,
|
|
47
|
+
locale,
|
|
48
|
+
content: draft.content,
|
|
49
|
+
contentFormat: "html",
|
|
50
|
+
variableSchema,
|
|
51
|
+
meta: serializePdfMeta(draft.meta),
|
|
52
|
+
...(scope === "tenant" && parentTemplateId != null && { parentTemplateId }),
|
|
53
|
+
});
|
|
54
|
+
if (!result.isSuccess) {
|
|
55
|
+
setActionError(describeError(t, result.error));
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
setState((previous) => previous === null ? previous : { ...previous, snapshot: snapshotOf(draft) });
|
|
59
|
+
return result.data;
|
|
60
|
+
}
|
|
61
|
+
async function save() {
|
|
62
|
+
setBusy("save");
|
|
63
|
+
setActionError(null);
|
|
64
|
+
const saved = await persist();
|
|
65
|
+
if (saved !== null)
|
|
66
|
+
await reload();
|
|
67
|
+
setBusy(null);
|
|
68
|
+
return saved !== null;
|
|
69
|
+
}
|
|
70
|
+
async function publish() {
|
|
71
|
+
setBusy("publish");
|
|
72
|
+
setActionError(null);
|
|
73
|
+
// A system save activates immediately; a tenant row needs a save first when it
|
|
74
|
+
// does not exist yet or carries unsaved edits, then an explicit publish.
|
|
75
|
+
const needsSave = dirty || detail?.scope === "system";
|
|
76
|
+
const saved = needsSave ? await persist() : null;
|
|
77
|
+
const saveFailed = needsSave && saved === null;
|
|
78
|
+
// upsertSystem already activates, so only a tenant row needs the explicit publish.
|
|
79
|
+
const alreadyActivatedBySave = scope === "system" && saved !== null;
|
|
80
|
+
const templateId = saved?.id ?? detail?.id ?? null;
|
|
81
|
+
if (templateId !== null && !saveFailed && !alreadyActivatedBySave) {
|
|
82
|
+
const result = await dispatcher.write(scope === "system"
|
|
83
|
+
? TemplateResolverHandlers.publishSystem
|
|
84
|
+
: TemplateResolverHandlers.publish, { id: templateId });
|
|
85
|
+
if (!result.isSuccess && !isAlreadyActive(result.error))
|
|
86
|
+
setActionError(describeError(t, result.error));
|
|
87
|
+
}
|
|
88
|
+
await reload();
|
|
89
|
+
setBusy(null);
|
|
90
|
+
}
|
|
91
|
+
return { save, publish };
|
|
92
|
+
}
|
|
93
|
+
function pdfActions(input) {
|
|
94
|
+
const { s, t, dispatcher, slug, current, detail, dirty, onLocaleChange } = input;
|
|
95
|
+
const { locale, detailQuery } = s.doc;
|
|
96
|
+
const { variableSchema } = s.server;
|
|
97
|
+
const { setMode, setBusy, setActionError, setGenerated, autoPrintedFor, setPendingLocale } = s.ui;
|
|
98
|
+
async function generatePdf() {
|
|
99
|
+
setMode("preview");
|
|
100
|
+
setBusy("pdf");
|
|
101
|
+
setActionError(null);
|
|
102
|
+
const forSnapshot = snapshotOf(current.draft);
|
|
103
|
+
const result = await dispatcher.query(RendererPuppeteerClientQueries.previewPdf, {
|
|
104
|
+
slug,
|
|
105
|
+
locale,
|
|
106
|
+
content: current.draft.content,
|
|
107
|
+
meta: serializePdfMeta(current.draft.meta),
|
|
108
|
+
variableSchema,
|
|
109
|
+
});
|
|
110
|
+
if (result.isSuccess)
|
|
111
|
+
setGenerated({ forSnapshot, result: result.data });
|
|
112
|
+
else
|
|
113
|
+
setActionError(describeError(t, result.error));
|
|
114
|
+
setBusy(null);
|
|
115
|
+
}
|
|
116
|
+
function printOnce() {
|
|
117
|
+
// skip: this draft was already printed
|
|
118
|
+
if (autoPrintedFor.current === snapshotOf(current.draft))
|
|
119
|
+
return;
|
|
120
|
+
autoPrintedFor.current = snapshotOf(current.draft);
|
|
121
|
+
void generatePdf();
|
|
122
|
+
}
|
|
123
|
+
async function uploadResource(file) {
|
|
124
|
+
const sourceFileRefId = await uploadFileRef(file);
|
|
125
|
+
const result = await dispatcher.write(TemplateResolverHandlers.resourceUpload, {
|
|
126
|
+
templateId: detail?.id,
|
|
127
|
+
sourceFileRefId,
|
|
128
|
+
});
|
|
129
|
+
if (!result.isSuccess)
|
|
130
|
+
throw new Error(describeError(t, result.error));
|
|
131
|
+
await detailQuery.refetch();
|
|
132
|
+
}
|
|
133
|
+
function requestLocale(next) {
|
|
134
|
+
// skip: the requested locale is already open
|
|
135
|
+
if (next === locale)
|
|
136
|
+
return;
|
|
137
|
+
if (dirty)
|
|
138
|
+
setPendingLocale(next);
|
|
139
|
+
else
|
|
140
|
+
onLocaleChange?.(next);
|
|
141
|
+
}
|
|
142
|
+
return { generatePdf, printOnce, uploadResource, requestLocale };
|
|
143
|
+
}
|
|
144
|
+
export function buildPdfActions(input) {
|
|
145
|
+
return { ...draftActions(input), ...writeActions(input), ...pdfActions(input) };
|
|
146
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import type { PdfRenderContext } from "./pdf-editor-context.ts";
|
|
3
|
+
export declare function renderPdfHeader(c: PdfRenderContext): ReactNode;
|
|
4
|
+
export declare function renderPdfInfoBar(c: PdfRenderContext): ReactNode;
|
|
5
|
+
export declare function renderPdfFooter(c: PdfRenderContext): ReactNode;
|
|
6
|
+
export declare function renderPdfLocaleDialog(c: PdfRenderContext): ReactNode;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { EditorHeader, EditorLocaleSwitch, EditorModeSwitch, originOf } from "./editor-header.js";
|
|
3
|
+
import { EditorFooter, EditorInfoBar } from "./editor-shell.js";
|
|
4
|
+
import { OverridesDrawer } from "./overrides-drawer.js";
|
|
5
|
+
export function renderPdfHeader(c) {
|
|
6
|
+
const { s, view, detail, actions } = c;
|
|
7
|
+
const { busy, mode, setMode } = s.ui;
|
|
8
|
+
return (_jsx(EditorHeader, { title: view.title, stored: { status: detail.status, version: detail.version }, actions: {
|
|
9
|
+
busy: busy === "save",
|
|
10
|
+
canSave: busy === null && s.server.validation?.canSave !== false,
|
|
11
|
+
canPublish: busy === null && view.canPublish,
|
|
12
|
+
onSave: () => void actions.save(),
|
|
13
|
+
onPublish: () => void actions.publish(),
|
|
14
|
+
}, children: _jsx(EditorModeSwitch, { testIdPrefix: "pdf", mode: mode, onMode: setMode }) }));
|
|
15
|
+
}
|
|
16
|
+
function renderInfoControls(c) {
|
|
17
|
+
const { t, Button, s, view, actions } = c;
|
|
18
|
+
const { busy, mode } = s.ui;
|
|
19
|
+
const { locale, locales } = s.doc;
|
|
20
|
+
return (_jsxs(_Fragment, { children: [_jsx(EditorLocaleSwitch, { testIdPrefix: "pdf", locales: locales.length > 0 ? locales : [locale], locale: locale, onLocale: actions.requestLocale }), mode === "preview" && (_jsx(Button, { variant: "secondary", disabled: !view.pdfAvailable || busy !== null, loading: busy === "pdf", onClick: () => void actions.generatePdf(), testId: "pdf-generate-header", children: t("documentDesigner.actions.generatePdf") }))] }));
|
|
21
|
+
}
|
|
22
|
+
function renderOverrideLinks(c) {
|
|
23
|
+
const { t, Button, s, view } = c;
|
|
24
|
+
return (view.showOverrideLinks && (_jsxs(_Fragment, { children: [_jsx(Button, { variant: "link", onClick: () => void c.exportBundle(), children: t("documentDesigner.originBar.export") }), _jsx(Button, { variant: "link", onClick: () => s.ui.setOverridesOpen(true), children: t("documentDesigner.originBar.showOverrides") })] })));
|
|
25
|
+
}
|
|
26
|
+
function renderUnavailableNote(c) {
|
|
27
|
+
const { t, view } = c;
|
|
28
|
+
return (view.pdfUnavailableNote && (_jsxs(_Fragment, { children: [" ", _jsx("span", { "data-testid": "pdf-unavailable", children: view.unavailableMessage === ""
|
|
29
|
+
? t("documentDesigner.pdfUnavailableNoReason")
|
|
30
|
+
: t("documentDesigner.pdfUnavailable", { message: view.unavailableMessage }) })] })));
|
|
31
|
+
}
|
|
32
|
+
export function renderPdfInfoBar(c) {
|
|
33
|
+
const { s, view, detail, slug } = c;
|
|
34
|
+
const { overridesOpen, setOverridesOpen } = s.ui;
|
|
35
|
+
return (_jsxs(EditorInfoBar, { tone: view.pdfUnavailableNote ? "warn" : detail.scope === "tenant" ? "active" : "muted", testId: "pdf-origin-bar", origin: originOf(detail.scope === "system", view.systemVersion !== null), controls: renderInfoControls(c), overlays: _jsx(OverridesDrawer, { open: overridesOpen, onOpenChange: setOverridesOpen, slug: slug, locale: s.doc.locale, overrides: s.server.overridesQuery.data }), links: renderOverrideLinks(c), children: [view.originText, view.besideActiveSentence !== null && ` ${view.besideActiveSentence}`, renderUnavailableNote(c)] }));
|
|
36
|
+
}
|
|
37
|
+
export function renderPdfFooter(c) {
|
|
38
|
+
const { s, view, detail, actions } = c;
|
|
39
|
+
const { busy } = s.ui;
|
|
40
|
+
return (_jsx(EditorFooter, { testIdPrefix: "pdf-editor", message: view.footer, saveError: s.ui.actionError, modifiedAt: detail.modifiedAt ?? null, dirty: view.dirty, busy: busy === "save", canSave: busy === null && s.server.validation?.canSave !== false, canPublish: busy === null && view.canPublish, onDiscard: () => actions.resetDraft(detail), onSave: () => void actions.save(), onPublish: () => void actions.publish() }));
|
|
41
|
+
}
|
|
42
|
+
export function renderPdfLocaleDialog(c) {
|
|
43
|
+
const { t, Button, Dialog, s, actions, detail, onLocaleChange } = c;
|
|
44
|
+
const { pendingLocale, setPendingLocale } = s.ui;
|
|
45
|
+
return (_jsx(Dialog, { open: pendingLocale !== null, onOpenChange: (open) => {
|
|
46
|
+
if (!open)
|
|
47
|
+
setPendingLocale(null);
|
|
48
|
+
}, title: t("documentDesigner.localeDialog.title"), description: t("documentDesigner.localeDialog.description"), confirmLabel: t("documentDesigner.localeDialog.save"), cancelLabel: t("documentDesigner.localeDialog.cancel"), onConfirm: async () => {
|
|
49
|
+
const target = pendingLocale;
|
|
50
|
+
if (target !== null && (await actions.save()))
|
|
51
|
+
onLocaleChange?.(target);
|
|
52
|
+
setPendingLocale(null);
|
|
53
|
+
}, children: _jsx(Button, { variant: "danger-ghost", onClick: () => {
|
|
54
|
+
const target = pendingLocale;
|
|
55
|
+
setPendingLocale(null);
|
|
56
|
+
actions.resetDraft(detail);
|
|
57
|
+
if (target !== null)
|
|
58
|
+
onLocaleChange?.(target);
|
|
59
|
+
}, children: t("documentDesigner.localeDialog.discard") }) }));
|
|
60
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { usePrimitives } from "@cosmicdrift/kumiko-renderer";
|
|
2
|
+
import type { PdfActions } from "./pdf-editor-actions.ts";
|
|
3
|
+
import type { EditorState } from "./pdf-editor-draft.ts";
|
|
4
|
+
import type { PdfView } from "./pdf-editor-view.ts";
|
|
5
|
+
import type { TemplateDetail } from "./types.ts";
|
|
6
|
+
import type { PdfEditorState } from "./use-pdf-editor-state.ts";
|
|
7
|
+
type Translate = (key: string, params?: Readonly<Record<string, unknown>>) => string;
|
|
8
|
+
type Primitives = ReturnType<typeof usePrimitives>;
|
|
9
|
+
export type PdfRenderContext = {
|
|
10
|
+
readonly t: Translate;
|
|
11
|
+
readonly Button: Primitives["Button"];
|
|
12
|
+
readonly Dialog: Primitives["Dialog"];
|
|
13
|
+
readonly slug: string;
|
|
14
|
+
readonly s: PdfEditorState;
|
|
15
|
+
readonly actions: PdfActions;
|
|
16
|
+
readonly view: PdfView;
|
|
17
|
+
readonly current: EditorState;
|
|
18
|
+
readonly detail: TemplateDetail;
|
|
19
|
+
readonly onLocaleChange: ((locale: string) => void) | undefined;
|
|
20
|
+
readonly exportBundle: () => Promise<void>;
|
|
21
|
+
};
|
|
22
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { MailVariable } from "./mail-types.ts";
|
|
2
|
+
import { type PdfMetaDraft } from "./pdf-meta.ts";
|
|
3
|
+
import type { PdfPreviewResult, TemplateDetail, VariableInfo } from "./types.ts";
|
|
4
|
+
export declare const SETTLE_MS = 500;
|
|
5
|
+
export declare const NO_VARIABLES: Readonly<Record<string, VariableInfo>>;
|
|
6
|
+
export type Draft = {
|
|
7
|
+
readonly content: string;
|
|
8
|
+
readonly meta: PdfMetaDraft;
|
|
9
|
+
};
|
|
10
|
+
export type EditorState = {
|
|
11
|
+
readonly key: string;
|
|
12
|
+
readonly draft: Draft;
|
|
13
|
+
readonly settled: Draft;
|
|
14
|
+
readonly snapshot: string;
|
|
15
|
+
};
|
|
16
|
+
export type GeneratedPdf = {
|
|
17
|
+
readonly forSnapshot: string;
|
|
18
|
+
readonly result: PdfPreviewResult;
|
|
19
|
+
};
|
|
20
|
+
export declare const draftFromDetail: (detail: TemplateDetail) => Draft;
|
|
21
|
+
export declare const snapshotOf: (draft: Draft) => string;
|
|
22
|
+
export declare function imageLabelSource(variables: Readonly<Record<string, VariableInfo>>): Readonly<Record<string, MailVariable>>;
|
|
23
|
+
export declare function textVariablesOf(variables: Readonly<Record<string, VariableInfo>>): Readonly<Record<string, MailVariable>>;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { parsePdfMeta, serializePdfMeta } from "./pdf-meta.js";
|
|
2
|
+
export const SETTLE_MS = 500;
|
|
3
|
+
export const NO_VARIABLES = {};
|
|
4
|
+
export const draftFromDetail = (detail) => ({
|
|
5
|
+
content: detail.content,
|
|
6
|
+
meta: parsePdfMeta(detail.meta),
|
|
7
|
+
});
|
|
8
|
+
export const snapshotOf = (draft) => JSON.stringify({ content: draft.content, meta: serializePdfMeta(draft.meta) });
|
|
9
|
+
// Only the label of an images variable is needed, for the Images block and its picker.
|
|
10
|
+
export function imageLabelSource(variables) {
|
|
11
|
+
const result = {};
|
|
12
|
+
for (const [name, info] of Object.entries(variables)) {
|
|
13
|
+
if (info.type === "images" && info.label !== undefined) {
|
|
14
|
+
result[name] = { type: "text", label: info.label };
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
return result;
|
|
18
|
+
}
|
|
19
|
+
export function textVariablesOf(variables) {
|
|
20
|
+
const result = {};
|
|
21
|
+
for (const [name, info] of Object.entries(variables)) {
|
|
22
|
+
if (info.type === "images")
|
|
23
|
+
continue;
|
|
24
|
+
const { item, ...rest } = info;
|
|
25
|
+
const columns = Object.entries(item ?? {}).flatMap(([column, def]) => def.type === "images" ? [] : [[column, { ...def, type: def.type }]]);
|
|
26
|
+
result[name] = {
|
|
27
|
+
...rest,
|
|
28
|
+
type: info.type,
|
|
29
|
+
...(item !== undefined && { item: Object.fromEntries(columns) }),
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
return result;
|
|
33
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
import type { PdfRenderContext } from "./pdf-editor-context.ts";
|
|
3
|
+
export declare function renderPdfLeftRail(c: PdfRenderContext): ReactNode;
|
|
4
|
+
export declare function renderPdfEditCanvas(c: PdfRenderContext): ReactNode;
|
|
5
|
+
export declare function renderPdfPreviewPane(c: PdfRenderContext): ReactNode;
|
|
6
|
+
export declare function renderPdfSettings(c: PdfRenderContext): ReactNode;
|