@cosmicdrift/kumiko-document-designer 0.389.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/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
|
@@ -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;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { PdfPreview } from "@cosmicdrift/kumiko-renderer/web";
|
|
3
|
+
import { useEffect } from "react";
|
|
4
|
+
import { PlaceholderSection, SystemPlaceholderSection } from "./mail-left-rail.js";
|
|
5
|
+
import { PageCanvas } from "./page-canvas.js";
|
|
6
|
+
import { PdfResourceSection } from "./pdf-left-rail.js";
|
|
7
|
+
import { SettingsPanel } from "./settings-panel.js";
|
|
8
|
+
// Opening the preview prints the page without a click; a failed print is not retried for the same draft.
|
|
9
|
+
function RunOnMount({ run }) {
|
|
10
|
+
useEffect(() => {
|
|
11
|
+
run();
|
|
12
|
+
}, [run]);
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
export function renderPdfLeftRail(c) {
|
|
16
|
+
const { s, view, detail, actions } = c;
|
|
17
|
+
const { insert, filesAvailable, scope } = s.host;
|
|
18
|
+
return (_jsxs(_Fragment, { children: [_jsx(PlaceholderSection, { validation: view.placeholderSource, onInsert: insert.insertPlaceholder, idPrefix: "pdf" }), _jsx(SystemPlaceholderSection, { validation: view.placeholderSource, onInsert: insert.insertPlaceholder, idPrefix: "pdf", isLayoutOwned: view.layoutFills }), _jsx(PdfResourceSection, { resourceKeys: view.resourceKeys, canUpload: detail.scope === scope, filesAvailable: filesAvailable, layoutName: view.layoutName, onInsert: insert.insertResource, onUpload: actions.uploadResource })] }));
|
|
19
|
+
}
|
|
20
|
+
export function renderPdfEditCanvas(c) {
|
|
21
|
+
const { s, view, current, actions } = c;
|
|
22
|
+
const { insert } = s.host;
|
|
23
|
+
const { vocabulary } = s;
|
|
24
|
+
return (_jsx("div", { className: "flex min-h-0 min-w-0 flex-1 flex-col", onFocusCapture: insert.markFocused("body"), children: _jsx(PageCanvas, { meta: current.draft.meta, content: current.draft.content, onContentChange: (content) => actions.updateDraft({ ...current.draft, content }), regions: s.server.layout?.regions ?? null, layoutName: view.layoutName, resourceSrc: view.layoutResourceSrc, variableLabels: vocabulary.variableLabels, systemVariables: vocabulary.systemVariables, systemLabels: vocabulary.systemLabels, variableNames: vocabulary.variableNames, imageVariables: vocabulary.imageVariables, resourcePreviews: vocabulary.resourcePreviews, onEditorReady: insert.register("body") }) }));
|
|
25
|
+
}
|
|
26
|
+
function renderStalePreview(c) {
|
|
27
|
+
const { t, Button, s, view, actions } = c;
|
|
28
|
+
const { busy } = s.ui;
|
|
29
|
+
return (_jsxs("div", { className: "flex flex-col items-start gap-3 p-6", "data-testid": "document-designer-preview-stale", children: [view.pdfAvailable && busy === null && _jsx(RunOnMount, { run: actions.printOnce }), _jsx("p", { className: "text-sm text-muted-foreground", children: busy === "pdf"
|
|
30
|
+
? t("documentDesigner.preview.loading")
|
|
31
|
+
: t("documentDesigner.preview.stale") }), _jsx(Button, { variant: "secondary", disabled: !view.pdfAvailable || busy !== null, onClick: () => void actions.generatePdf(), children: t("documentDesigner.actions.generatePdf") })] }));
|
|
32
|
+
}
|
|
33
|
+
export function renderPdfPreviewPane(c) {
|
|
34
|
+
const { generated } = c.s.ui;
|
|
35
|
+
return (_jsx("div", { className: "min-h-0 flex-1 overflow-auto", children: generated !== null && c.view.generatedFresh ? (_jsx(PdfPreview, { source: { base64: generated.result.pdfBase64 }, testId: "document-designer-pdf-preview" })) : (renderStalePreview(c)) }));
|
|
36
|
+
}
|
|
37
|
+
export function renderPdfSettings(c) {
|
|
38
|
+
const { s, view, current, detail, actions } = c;
|
|
39
|
+
return (_jsx(SettingsPanel, { meta: current.draft.meta, onMetaChange: (meta) => actions.updateDraft({ ...current.draft, meta }), layouts: s.server.layouts.rows, locale: s.doc.locale, version: { id: detail.id, version: detail.version, status: detail.status }, vocabulary: view.vocabulary, insert: s.host.insert, draftKey: `${current.key}:${s.ui.draftEpoch}` }));
|
|
40
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { EditorState } from "./pdf-editor-draft.ts";
|
|
2
|
+
import { type TemplateDetail } from "./types.ts";
|
|
3
|
+
import type { PdfEditorState } from "./use-pdf-editor-state.ts";
|
|
4
|
+
type Translate = (key: string, params?: Readonly<Record<string, unknown>>) => string;
|
|
5
|
+
export declare function buildPdfView(input: {
|
|
6
|
+
readonly t: Translate;
|
|
7
|
+
readonly s: PdfEditorState;
|
|
8
|
+
readonly slug: string;
|
|
9
|
+
readonly current: EditorState;
|
|
10
|
+
readonly detail: TemplateDetail;
|
|
11
|
+
}): {
|
|
12
|
+
dirty: boolean;
|
|
13
|
+
generatedFresh: boolean;
|
|
14
|
+
canPublish: boolean;
|
|
15
|
+
pdfAvailable: boolean;
|
|
16
|
+
unavailableMessage: string;
|
|
17
|
+
pdfUnavailableNote: boolean;
|
|
18
|
+
layoutName: string;
|
|
19
|
+
layoutResourceSrc: (key: string) => string | undefined;
|
|
20
|
+
placeholderSource: {
|
|
21
|
+
canSave: boolean;
|
|
22
|
+
canPublish: boolean;
|
|
23
|
+
issues: readonly import("@cosmicdrift/kumiko-framework/errors").FieldIssue[];
|
|
24
|
+
publishIssues: readonly import("@cosmicdrift/kumiko-framework/errors").FieldIssue[];
|
|
25
|
+
used: readonly string[];
|
|
26
|
+
unknown: readonly string[];
|
|
27
|
+
missingRequired: readonly string[];
|
|
28
|
+
warnings: readonly import("./types.ts").ValidationWarning[];
|
|
29
|
+
variables: Readonly<Record<string, import("./mail-types.ts").MailVariable>>;
|
|
30
|
+
systemPlaceholders: readonly string[];
|
|
31
|
+
} | null;
|
|
32
|
+
layoutFills: (name: string) => boolean;
|
|
33
|
+
resourceKeys: string[];
|
|
34
|
+
title: string;
|
|
35
|
+
systemVersion: number | null;
|
|
36
|
+
vocabulary: {
|
|
37
|
+
names: string[];
|
|
38
|
+
labels: {
|
|
39
|
+
[x: string]: string;
|
|
40
|
+
};
|
|
41
|
+
system: readonly string[];
|
|
42
|
+
};
|
|
43
|
+
footer: {
|
|
44
|
+
readonly tone: import("./editor-shell.tsx").FooterTone;
|
|
45
|
+
readonly text: string;
|
|
46
|
+
} | null;
|
|
47
|
+
showOverrideLinks: boolean;
|
|
48
|
+
originText: string;
|
|
49
|
+
besideActiveSentence: string | null;
|
|
50
|
+
};
|
|
51
|
+
export type PdfView = ReturnType<typeof buildPdfView>;
|
|
52
|
+
export {};
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { fileUrl } from "./file-name.js";
|
|
2
|
+
import { snapshotOf } from "./pdf-editor-draft.js";
|
|
3
|
+
import { pdfFooterMessage } from "./pdf-footer.js";
|
|
4
|
+
import { visibleSystemPlaceholders } from "./pdf-left-rail.js";
|
|
5
|
+
import { draftBesideActiveSentence } from "./status-label.js";
|
|
6
|
+
import { parseJsonObject } from "./template-rows.js";
|
|
7
|
+
import { DEFAULT_LAYOUT_SLUG } from "./types.js";
|
|
8
|
+
import { templateTitle, templateTitleText } from "./used-by-label.js";
|
|
9
|
+
function originTextOf(t, s, detail, systemVersion, showOverrideLinks) {
|
|
10
|
+
const overrides = s.server.overridesQuery.data;
|
|
11
|
+
if (showOverrideLinks) {
|
|
12
|
+
const overrideCount = (overrides?.activeOverrides ?? 0) + (overrides?.draftOverrides ?? 0);
|
|
13
|
+
return overrides?.totalTenants == null
|
|
14
|
+
? t("documentDesigner.originBar.noTotal", { count: overrideCount })
|
|
15
|
+
: t("documentDesigner.originBar.withTotal", {
|
|
16
|
+
count: overrideCount,
|
|
17
|
+
total: overrides.totalTenants,
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
if (detail.scope === "system")
|
|
21
|
+
return t("documentDesigner.mail.origin.system", { version: detail.version });
|
|
22
|
+
return systemVersion === null
|
|
23
|
+
? t("documentDesigner.mail.origin.tenantOnly")
|
|
24
|
+
: t("documentDesigner.mail.origin.overrides", { version: systemVersion });
|
|
25
|
+
}
|
|
26
|
+
function footerOf(t, s) {
|
|
27
|
+
const { validation } = s.server;
|
|
28
|
+
const { generated } = s.ui;
|
|
29
|
+
if (validation === undefined || validation === null)
|
|
30
|
+
return null; // skip: nothing validated yet
|
|
31
|
+
return pdfFooterMessage(t, validation, generated === null
|
|
32
|
+
? null
|
|
33
|
+
: {
|
|
34
|
+
pages: generated.result.pageCount,
|
|
35
|
+
sizeBytes: generated.result.sizeBytes,
|
|
36
|
+
durationMs: generated.result.durationMs,
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
function layoutNameOf(t, s, layoutSlugShown) {
|
|
40
|
+
const layoutTitle = s.server.layouts.rows.find((row) => row.slug === layoutSlugShown)?.title;
|
|
41
|
+
return layoutTitle === undefined ? layoutSlugShown : templateTitleText(t, layoutTitle);
|
|
42
|
+
}
|
|
43
|
+
function placeholderSourceOf(s) {
|
|
44
|
+
const { validation } = s.server;
|
|
45
|
+
return validation === undefined || validation === null
|
|
46
|
+
? null
|
|
47
|
+
: {
|
|
48
|
+
...validation,
|
|
49
|
+
variables: s.vocabulary.variables,
|
|
50
|
+
systemPlaceholders: visibleSystemPlaceholders(validation.systemPlaceholders),
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
export function buildPdfView(input) {
|
|
54
|
+
const { t, s, slug, current, detail } = input;
|
|
55
|
+
const { layout, validation, statusQuery, brandingLogoFile } = s.server;
|
|
56
|
+
const { generated } = s.ui;
|
|
57
|
+
const pdfAvailable = statusQuery.error === null && statusQuery.data?.available === true;
|
|
58
|
+
const unavailableMessage = statusQuery.data?.available === false ? (statusQuery.data.message ?? "") : "";
|
|
59
|
+
const layoutSlugShown = layout?.slug ?? DEFAULT_LAYOUT_SLUG;
|
|
60
|
+
const layoutName = layoutNameOf(t, s, layoutSlugShown);
|
|
61
|
+
const overviewRow = s.host.overview.rows.find((candidate) => candidate.slug === slug);
|
|
62
|
+
const systemVersion = overviewRow?.systemVersion ?? null;
|
|
63
|
+
const showOverrideLinks = s.server.showOrigin && detail.scope === "system";
|
|
64
|
+
return {
|
|
65
|
+
dirty: snapshotOf(current.draft) !== current.snapshot,
|
|
66
|
+
generatedFresh: generated !== null && generated.forSnapshot === snapshotOf(current.draft),
|
|
67
|
+
canPublish: validation?.canPublish === true,
|
|
68
|
+
pdfAvailable,
|
|
69
|
+
unavailableMessage,
|
|
70
|
+
pdfUnavailableNote: !pdfAvailable && statusQuery.loading === false,
|
|
71
|
+
layoutName,
|
|
72
|
+
layoutResourceSrc: (key) => key === "logo" && brandingLogoFile !== "" ? fileUrl(brandingLogoFile) : undefined,
|
|
73
|
+
placeholderSource: placeholderSourceOf(s),
|
|
74
|
+
layoutFills: (name) => layout !== null &&
|
|
75
|
+
(layout.html.includes(`data-variable="${name}"`) || layout.html.includes(`{{${name}}}`)),
|
|
76
|
+
resourceKeys: Object.keys(parseJsonObject(detail.linkedResources)),
|
|
77
|
+
title: overviewRow === undefined ? slug : templateTitle(t, overviewRow),
|
|
78
|
+
systemVersion,
|
|
79
|
+
vocabulary: {
|
|
80
|
+
names: s.vocabulary.variableNames,
|
|
81
|
+
labels: s.vocabulary.variableLabels,
|
|
82
|
+
system: s.vocabulary.systemVariables,
|
|
83
|
+
},
|
|
84
|
+
footer: footerOf(t, s),
|
|
85
|
+
showOverrideLinks,
|
|
86
|
+
originText: originTextOf(t, s, detail, systemVersion, showOverrideLinks),
|
|
87
|
+
besideActiveSentence: draftBesideActiveSentence(t, detail),
|
|
88
|
+
};
|
|
89
|
+
}
|