@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,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
|
+
}
|
|
@@ -1,159 +1,21 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { useDispatcher, usePrimitives,
|
|
4
|
-
import { ErrorState, LoadingState
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import { PlaceholderSection, SystemPlaceholderSection } from "./mail-left-rail.js";
|
|
14
|
-
import { OverridesDrawer } from "./overrides-drawer.js";
|
|
15
|
-
import { PageCanvas } from "./page-canvas.js";
|
|
16
|
-
import { pdfFooterMessage } from "./pdf-footer.js";
|
|
17
|
-
import { PdfResourceSection, visibleSystemPlaceholders } from "./pdf-left-rail.js";
|
|
18
|
-
import { EMPTY_PDF_META, parsePdfMeta, serializePdfMeta } from "./pdf-meta.js";
|
|
19
|
-
import { SettingsPanel } from "./settings-panel.js";
|
|
20
|
-
import { draftBesideActiveSentence } from "./status-label.js";
|
|
21
|
-
import { localesOf, parseJsonObject, pickRow, resolveLocale } from "./template-rows.js";
|
|
22
|
-
import { DEFAULT_LAYOUT_SLUG, DOCUMENT_PDF_KIND, PDF_LAYOUT_KIND, } from "./types.js";
|
|
23
|
-
import { useFilesCapability } from "./use-files-capability.js";
|
|
24
|
-
import { useInsertTargets } from "./use-insert-targets.js";
|
|
25
|
-
import { useOverviewRows } from "./use-overview-rows.js";
|
|
26
|
-
import { templateTitle, templateTitleText } from "./used-by-label.js";
|
|
27
|
-
const SETTLE_MS = 500;
|
|
28
|
-
const NO_VARIABLES = {};
|
|
29
|
-
// Only the label of an images variable is needed, for the Images block and its picker.
|
|
30
|
-
function imageLabelSource(variables) {
|
|
31
|
-
const result = {};
|
|
32
|
-
for (const [name, info] of Object.entries(variables)) {
|
|
33
|
-
if (info.type === "images" && info.label !== undefined) {
|
|
34
|
-
result[name] = { type: "text", label: info.label };
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
return result;
|
|
38
|
-
}
|
|
39
|
-
function textVariablesOf(variables) {
|
|
40
|
-
const result = {};
|
|
41
|
-
for (const [name, info] of Object.entries(variables)) {
|
|
42
|
-
if (info.type === "images")
|
|
43
|
-
continue;
|
|
44
|
-
const { item, ...rest } = info;
|
|
45
|
-
const columns = Object.entries(item ?? {}).flatMap(([column, def]) => def.type === "images" ? [] : [[column, { ...def, type: def.type }]]);
|
|
46
|
-
result[name] = {
|
|
47
|
-
...rest,
|
|
48
|
-
type: info.type,
|
|
49
|
-
...(item !== undefined && { item: Object.fromEntries(columns) }),
|
|
50
|
-
};
|
|
51
|
-
}
|
|
52
|
-
return result;
|
|
53
|
-
}
|
|
54
|
-
// Opening the preview prints the page without a click; a failed print is not retried for the same draft.
|
|
55
|
-
function RunOnMount({ run }) {
|
|
56
|
-
useEffect(() => {
|
|
57
|
-
run();
|
|
58
|
-
}, [run]);
|
|
59
|
-
return null;
|
|
60
|
-
}
|
|
61
|
-
const draftFromDetail = (detail) => ({
|
|
62
|
-
content: detail.content,
|
|
63
|
-
meta: parsePdfMeta(detail.meta),
|
|
64
|
-
});
|
|
65
|
-
const snapshotOf = (draft) => JSON.stringify({ content: draft.content, meta: serializePdfMeta(draft.meta) });
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { TemplateResolverQueries } from "@cosmicdrift/kumiko-bundled-features/template-resolver/web";
|
|
3
|
+
import { useDispatcher, usePrimitives, useTranslation } from "@cosmicdrift/kumiko-renderer";
|
|
4
|
+
import { ErrorState, LoadingState } from "@cosmicdrift/kumiko-renderer/web";
|
|
5
|
+
import { EditorBody, EditorFrame } from "./editor-shell.js";
|
|
6
|
+
import { describeError } from "./errors.js";
|
|
7
|
+
import { buildPdfActions } from "./pdf-editor-actions.js";
|
|
8
|
+
import { renderPdfFooter, renderPdfHeader, renderPdfInfoBar, renderPdfLocaleDialog, } from "./pdf-editor-chrome.js";
|
|
9
|
+
import { renderPdfEditCanvas, renderPdfLeftRail, renderPdfPreviewPane, renderPdfSettings, } from "./pdf-editor-panes.js";
|
|
10
|
+
import { buildPdfView } from "./pdf-editor-view.js";
|
|
11
|
+
import { DOCUMENT_PDF_KIND } from "./types.js";
|
|
12
|
+
import { usePdfEditorState } from "./use-pdf-editor-state.js";
|
|
66
13
|
export function PdfTemplateEditor({ slug, locale: requestedLocale, onLocaleChange, }) {
|
|
67
14
|
const t = useTranslation();
|
|
68
15
|
const dispatcher = useDispatcher();
|
|
69
16
|
const { Button, Banner, Dialog } = usePrimitives();
|
|
70
|
-
const
|
|
71
|
-
const
|
|
72
|
-
const overview = useOverviewRows(DOCUMENT_PDF_KIND);
|
|
73
|
-
const roles = useUserRoles();
|
|
74
|
-
const scope = roles?.includes("SystemAdmin") === true ? "system" : "tenant";
|
|
75
|
-
const rowsQuery = useQuery(TemplateResolverQueries.list, {
|
|
76
|
-
kind: DOCUMENT_PDF_KIND,
|
|
77
|
-
});
|
|
78
|
-
const rows = rowsQuery.data ?? [];
|
|
79
|
-
const locales = localesOf(rows, slug);
|
|
80
|
-
const locale = resolveLocale(requestedLocale, locales);
|
|
81
|
-
const row = pickRow(rows, slug, locale, scope);
|
|
82
|
-
const detailQuery = useQuery(TemplateResolverQueries.findById, { id: row?.id ?? "" }, { enabled: row !== undefined });
|
|
83
|
-
const detail = detailQuery.data !== null &&
|
|
84
|
-
detailQuery.data?.slug === slug &&
|
|
85
|
-
detailQuery.data.locale === locale
|
|
86
|
-
? detailQuery.data
|
|
87
|
-
: null;
|
|
88
|
-
const [state, setState] = useState(null);
|
|
89
|
-
if (detail !== null && (state === null || state.key !== detail.id)) {
|
|
90
|
-
const draft = draftFromDetail(detail);
|
|
91
|
-
setState({ key: detail.id, draft, settled: draft, snapshot: snapshotOf(draft) });
|
|
92
|
-
}
|
|
93
|
-
const current = state !== null && detail !== null && state.key === detail.id ? state : null;
|
|
94
|
-
const settleTimer = useRef(null);
|
|
95
|
-
// A pending settle must not fire into an unmounted editor.
|
|
96
|
-
useEffect(() => () => {
|
|
97
|
-
if (settleTimer.current !== null)
|
|
98
|
-
clearTimeout(settleTimer.current);
|
|
99
|
-
}, []);
|
|
100
|
-
const [overridesOpen, setOverridesOpen] = useState(false);
|
|
101
|
-
const [draftEpoch, setDraftEpoch] = useState(0);
|
|
102
|
-
const [mode, setMode] = useState("edit");
|
|
103
|
-
const [busy, setBusy] = useState(null);
|
|
104
|
-
const [actionError, setActionError] = useState(null);
|
|
105
|
-
const [generated, setGenerated] = useState(null);
|
|
106
|
-
const autoPrintedFor = useRef(null);
|
|
107
|
-
const [pendingLocale, setPendingLocale] = useState(null);
|
|
108
|
-
const variableSchema = useMemo(() => parseJsonObject(detail?.variableSchema ?? null), [detail?.variableSchema]);
|
|
109
|
-
const settledMeta = current === null ? null : serializePdfMeta(current.settled.meta);
|
|
110
|
-
const validationQuery = useQuery(TemplateResolverQueries.validate, {
|
|
111
|
-
slug,
|
|
112
|
-
kind: DOCUMENT_PDF_KIND,
|
|
113
|
-
contentFormat: "html",
|
|
114
|
-
content: current?.settled.content ?? "",
|
|
115
|
-
meta: settledMeta ?? {},
|
|
116
|
-
variableSchema,
|
|
117
|
-
locale,
|
|
118
|
-
scope,
|
|
119
|
-
}, { enabled: current !== null });
|
|
120
|
-
const validation = validationQuery.data;
|
|
121
|
-
const layoutSlug = current?.settled.meta.layoutSlug ?? "";
|
|
122
|
-
const previewQuery = useQuery(TemplateResolverQueries.preview, {
|
|
123
|
-
slug,
|
|
124
|
-
kind: DOCUMENT_PDF_KIND,
|
|
125
|
-
locale,
|
|
126
|
-
content: detail?.content ?? "",
|
|
127
|
-
meta: layoutSlug === "" ? {} : { layoutSlug },
|
|
128
|
-
variableSchema,
|
|
129
|
-
}, { enabled: detail !== null });
|
|
130
|
-
const layout = previewQuery.error === null ? (previewQuery.data?.layout ?? null) : null;
|
|
131
|
-
const layouts = useOverviewRows(PDF_LAYOUT_KIND);
|
|
132
|
-
// The built-in "logo" resource of a layout is the tenant's branding logo.
|
|
133
|
-
const branding = useQuery(BRANDING_QUERY_QN, {});
|
|
134
|
-
const brandingLogoFile = branding.data?.logoFile ?? "";
|
|
135
|
-
const statusQuery = useQuery(RendererPuppeteerClientQueries.status, {});
|
|
136
|
-
const pdfAvailable = statusQuery.error === null && statusQuery.data?.available === true;
|
|
137
|
-
const unavailableMessage = statusQuery.data?.available === false ? (statusQuery.data.message ?? "") : "";
|
|
138
|
-
const showOrigin = scope === "system";
|
|
139
|
-
const overridesQuery = useQuery(TemplateResolverQueries.tenantOverrides, { slug, kind: DOCUMENT_PDF_KIND, locale }, { enabled: showOrigin && detail !== null });
|
|
140
|
-
const allVariables = validation?.variables ?? NO_VARIABLES;
|
|
141
|
-
// Photos are record data, never an inline placeholder: images variables only feed the Images block.
|
|
142
|
-
const variables = useMemo(() => textVariablesOf(allVariables), [allVariables]);
|
|
143
|
-
const imageVariables = useMemo(() => Object.keys(allVariables).filter((name) => allVariables[name]?.type === "images"), [allVariables]);
|
|
144
|
-
const systemVariables = useMemo(() => visibleSystemPlaceholders(validation?.systemPlaceholders ?? []), [validation?.systemPlaceholders]);
|
|
145
|
-
const systemLabels = useMemo(() => systemLabelMap(t, systemVariables), [t, systemVariables]);
|
|
146
|
-
const variableLabels = useMemo(() => ({
|
|
147
|
-
...systemLabels,
|
|
148
|
-
...variableLabelMap(t, variables),
|
|
149
|
-
...variableLabelMap(t, imageLabelSource(allVariables)),
|
|
150
|
-
}), [systemLabels, variables, allVariables, t]);
|
|
151
|
-
const variableNames = useMemo(() => Object.keys(variables), [variables]);
|
|
152
|
-
const linkedResources = detail?.linkedResources ?? null;
|
|
153
|
-
// The editor shows resource images from the stored copies; no extra request is made here.
|
|
154
|
-
const resourcePreviews = useMemo(() => Object.fromEntries(Object.entries(parseJsonObject(linkedResources)).flatMap(([key, fileRefId]) => typeof fileRefId === "string"
|
|
155
|
-
? [[key, { src: `/api/files/${encodeURIComponent(fileRefId)}`, caption: key }]]
|
|
156
|
-
: [])), [linkedResources]);
|
|
17
|
+
const s = usePdfEditorState({ slug, requestedLocale }, t);
|
|
18
|
+
const { rowsQuery, row, current, detail, detailQuery } = s.doc;
|
|
157
19
|
if (rowsQuery.loading && rowsQuery.data === null)
|
|
158
20
|
return _jsx(LoadingState, {});
|
|
159
21
|
if (rowsQuery.error !== null)
|
|
@@ -163,196 +25,32 @@ export function PdfTemplateEditor({ slug, locale: requestedLocale, onLocaleChang
|
|
|
163
25
|
if (current === null || detail === null) {
|
|
164
26
|
return detailQuery.error !== null ? _jsx(ErrorState, { error: detailQuery.error }) : _jsx(LoadingState, {});
|
|
165
27
|
}
|
|
166
|
-
const
|
|
167
|
-
const
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
const parentTemplateId = detail?.scope === "system" ? detail.id : detail?.parentTemplateId;
|
|
190
|
-
const result = await dispatcher.write(scope === "system"
|
|
191
|
-
? TemplateResolverHandlers.upsertSystem
|
|
192
|
-
: TemplateResolverHandlers.upsertTenant, {
|
|
193
|
-
slug,
|
|
194
|
-
kind: DOCUMENT_PDF_KIND,
|
|
195
|
-
locale,
|
|
196
|
-
content: draft.content,
|
|
197
|
-
contentFormat: "html",
|
|
198
|
-
variableSchema,
|
|
199
|
-
meta: serializePdfMeta(draft.meta),
|
|
200
|
-
...(scope === "tenant" && parentTemplateId != null && { parentTemplateId }),
|
|
201
|
-
});
|
|
202
|
-
if (!result.isSuccess) {
|
|
203
|
-
setActionError(describeError(t, result.error));
|
|
204
|
-
return null;
|
|
205
|
-
}
|
|
206
|
-
setState((previous) => previous === null ? previous : { ...previous, snapshot: snapshotOf(draft) });
|
|
207
|
-
return result.data;
|
|
208
|
-
}
|
|
209
|
-
async function save() {
|
|
210
|
-
setBusy("save");
|
|
211
|
-
setActionError(null);
|
|
212
|
-
const saved = await persist();
|
|
213
|
-
if (saved !== null)
|
|
214
|
-
await reload();
|
|
215
|
-
setBusy(null);
|
|
216
|
-
return saved !== null;
|
|
217
|
-
}
|
|
218
|
-
async function publish() {
|
|
219
|
-
setBusy("publish");
|
|
220
|
-
setActionError(null);
|
|
221
|
-
// A system save activates immediately; a tenant row needs a save first when it
|
|
222
|
-
// does not exist yet or carries unsaved edits, then an explicit publish.
|
|
223
|
-
const needsSave = dirty || detail?.scope === "system";
|
|
224
|
-
const saved = needsSave ? await persist() : null;
|
|
225
|
-
const saveFailed = needsSave && saved === null;
|
|
226
|
-
// upsertSystem already activates, so only a tenant row needs the explicit publish.
|
|
227
|
-
const alreadyActivatedBySave = scope === "system" && saved !== null;
|
|
228
|
-
const templateId = saved?.id ?? detail?.id ?? null;
|
|
229
|
-
if (templateId !== null && !saveFailed && !alreadyActivatedBySave) {
|
|
230
|
-
const result = await dispatcher.write(scope === "system"
|
|
231
|
-
? TemplateResolverHandlers.publishSystem
|
|
232
|
-
: TemplateResolverHandlers.publish, { id: templateId });
|
|
233
|
-
if (!result.isSuccess && !isAlreadyActive(result.error))
|
|
234
|
-
setActionError(describeError(t, result.error));
|
|
235
|
-
}
|
|
236
|
-
await reload();
|
|
237
|
-
setBusy(null);
|
|
238
|
-
}
|
|
239
|
-
async function generatePdf() {
|
|
240
|
-
if (current === null)
|
|
241
|
-
return;
|
|
242
|
-
setMode("preview");
|
|
243
|
-
setBusy("pdf");
|
|
244
|
-
setActionError(null);
|
|
245
|
-
const forSnapshot = snapshotOf(current.draft);
|
|
246
|
-
const result = await dispatcher.query(RendererPuppeteerClientQueries.previewPdf, {
|
|
247
|
-
slug,
|
|
248
|
-
locale,
|
|
249
|
-
content: current.draft.content,
|
|
250
|
-
meta: serializePdfMeta(current.draft.meta),
|
|
251
|
-
variableSchema,
|
|
252
|
-
});
|
|
253
|
-
if (result.isSuccess)
|
|
254
|
-
setGenerated({ forSnapshot, result: result.data });
|
|
255
|
-
else
|
|
256
|
-
setActionError(describeError(t, result.error));
|
|
257
|
-
setBusy(null);
|
|
258
|
-
}
|
|
259
|
-
function printOnce() {
|
|
260
|
-
if (current === null || autoPrintedFor.current === snapshotOf(current.draft))
|
|
261
|
-
return;
|
|
262
|
-
autoPrintedFor.current = snapshotOf(current.draft);
|
|
263
|
-
void generatePdf();
|
|
264
|
-
}
|
|
265
|
-
async function uploadResource(file) {
|
|
266
|
-
const sourceFileRefId = await uploadFileRef(file);
|
|
267
|
-
const result = await dispatcher.write(TemplateResolverHandlers.resourceUpload, {
|
|
268
|
-
templateId: detail?.id,
|
|
269
|
-
sourceFileRefId,
|
|
270
|
-
});
|
|
271
|
-
if (!result.isSuccess)
|
|
272
|
-
throw new Error(describeError(t, result.error));
|
|
273
|
-
await detailQuery.refetch();
|
|
274
|
-
}
|
|
275
|
-
function requestLocale(next) {
|
|
276
|
-
if (next === locale)
|
|
277
|
-
return;
|
|
278
|
-
if (dirty)
|
|
279
|
-
setPendingLocale(next);
|
|
280
|
-
else
|
|
281
|
-
onLocaleChange?.(next);
|
|
282
|
-
}
|
|
283
|
-
const canPublish = validation?.canPublish === true;
|
|
284
|
-
const layoutSlugShown = layout?.slug ?? DEFAULT_LAYOUT_SLUG;
|
|
285
|
-
const layoutTitle = layouts.rows.find((row) => row.slug === layoutSlugShown)?.title;
|
|
286
|
-
const layoutName = layoutTitle === undefined ? layoutSlugShown : templateTitleText(t, layoutTitle);
|
|
287
|
-
const layoutResourceSrc = (key) => key === "logo" && brandingLogoFile !== "" ? fileUrl(brandingLogoFile) : undefined;
|
|
288
|
-
const placeholderSource = validation === undefined || validation === null
|
|
289
|
-
? null
|
|
290
|
-
: {
|
|
291
|
-
...validation,
|
|
292
|
-
variables,
|
|
293
|
-
systemPlaceholders: visibleSystemPlaceholders(validation.systemPlaceholders),
|
|
294
|
-
};
|
|
295
|
-
const layoutFills = (name) => layout !== null &&
|
|
296
|
-
(layout.html.includes(`data-variable="${name}"`) || layout.html.includes(`{{${name}}}`));
|
|
297
|
-
const overrideCount = (overridesQuery.data?.activeOverrides ?? 0) + (overridesQuery.data?.draftOverrides ?? 0);
|
|
298
|
-
const resourceKeys = Object.keys(parseJsonObject(detail.linkedResources));
|
|
299
|
-
const overviewRow = overview.rows.find((candidate) => candidate.slug === slug);
|
|
300
|
-
const title = overviewRow === undefined ? slug : templateTitle(t, overviewRow);
|
|
301
|
-
const systemVersion = overviewRow?.systemVersion ?? null;
|
|
302
|
-
const vocabulary = {
|
|
303
|
-
names: variableNames,
|
|
304
|
-
labels: variableLabels,
|
|
305
|
-
system: systemVariables,
|
|
28
|
+
const view = buildPdfView({ t, s, slug, current, detail });
|
|
29
|
+
const actions = buildPdfActions({
|
|
30
|
+
s,
|
|
31
|
+
t,
|
|
32
|
+
dispatcher,
|
|
33
|
+
slug,
|
|
34
|
+
current,
|
|
35
|
+
detail,
|
|
36
|
+
dirty: view.dirty,
|
|
37
|
+
onLocaleChange,
|
|
38
|
+
});
|
|
39
|
+
const context = {
|
|
40
|
+
t,
|
|
41
|
+
Button,
|
|
42
|
+
Dialog,
|
|
43
|
+
slug,
|
|
44
|
+
s,
|
|
45
|
+
actions,
|
|
46
|
+
view,
|
|
47
|
+
current,
|
|
48
|
+
detail,
|
|
49
|
+
onLocaleChange,
|
|
50
|
+
exportBundle,
|
|
306
51
|
};
|
|
307
|
-
const
|
|
308
|
-
|
|
309
|
-
: pdfFooterMessage(t, validation, generated === null
|
|
310
|
-
? null
|
|
311
|
-
: {
|
|
312
|
-
pages: generated.result.pageCount,
|
|
313
|
-
sizeBytes: generated.result.sizeBytes,
|
|
314
|
-
durationMs: generated.result.durationMs,
|
|
315
|
-
});
|
|
316
|
-
const showOverrideLinks = showOrigin && detail.scope === "system";
|
|
317
|
-
const originText = showOverrideLinks
|
|
318
|
-
? overridesQuery.data?.totalTenants == null
|
|
319
|
-
? t("documentDesigner.originBar.noTotal", { count: overrideCount })
|
|
320
|
-
: t("documentDesigner.originBar.withTotal", {
|
|
321
|
-
count: overrideCount,
|
|
322
|
-
total: overridesQuery.data.totalTenants,
|
|
323
|
-
})
|
|
324
|
-
: detail.scope === "system"
|
|
325
|
-
? t("documentDesigner.mail.origin.system", { version: detail.version })
|
|
326
|
-
: systemVersion === null
|
|
327
|
-
? t("documentDesigner.mail.origin.tenantOnly")
|
|
328
|
-
: t("documentDesigner.mail.origin.overrides", { version: systemVersion });
|
|
329
|
-
const besideActiveSentence = draftBesideActiveSentence(t, detail);
|
|
330
|
-
const pdfUnavailableNote = !pdfAvailable && statusQuery.loading === false;
|
|
331
|
-
return (_jsxs(EditorFrame, { testId: "pdf-template-editor", children: [_jsx(EditorHeader, { title: title, stored: { status: detail.status, version: detail.version }, actions: {
|
|
332
|
-
busy: busy === "save",
|
|
333
|
-
canSave: busy === null && validation?.canSave !== false,
|
|
334
|
-
canPublish: busy === null && canPublish,
|
|
335
|
-
onSave: () => void save(),
|
|
336
|
-
onPublish: () => void publish(),
|
|
337
|
-
}, children: _jsx(EditorModeSwitch, { testIdPrefix: "pdf", mode: mode, onMode: setMode }) }), _jsxs(EditorInfoBar, { tone: pdfUnavailableNote ? "warn" : detail.scope === "tenant" ? "active" : "muted", testId: "pdf-origin-bar", origin: originOf(detail.scope === "system", systemVersion !== null), controls: _jsxs(_Fragment, { children: [_jsx(EditorLocaleSwitch, { testIdPrefix: "pdf", locales: locales.length > 0 ? locales : [locale], locale: locale, onLocale: requestLocale }), mode === "preview" && (_jsx(Button, { variant: "secondary", disabled: !pdfAvailable || busy !== null, loading: busy === "pdf", onClick: () => void generatePdf(), testId: "pdf-generate-header", children: t("documentDesigner.actions.generatePdf") }))] }), overlays: _jsx(OverridesDrawer, { open: overridesOpen, onOpenChange: setOverridesOpen, slug: slug, locale: locale, overrides: overridesQuery.data }), links: showOverrideLinks && (_jsxs(_Fragment, { children: [_jsx(Button, { variant: "link", onClick: () => void exportBundle(), children: t("documentDesigner.originBar.export") }), _jsx(Button, { variant: "link", onClick: () => setOverridesOpen(true), children: t("documentDesigner.originBar.showOverrides") })] })), children: [originText, besideActiveSentence !== null && ` ${besideActiveSentence}`, pdfUnavailableNote && (_jsxs(_Fragment, { children: [" ", _jsx("span", { "data-testid": "pdf-unavailable", children: unavailableMessage === ""
|
|
338
|
-
? t("documentDesigner.pdfUnavailableNoReason")
|
|
339
|
-
: t("documentDesigner.pdfUnavailable", { message: unavailableMessage }) })] }))] }), _jsx(EditorBody, { left: mode === "edit" ? (_jsxs(_Fragment, { children: [_jsx(PlaceholderSection, { validation: placeholderSource, onInsert: insert.insertPlaceholder, idPrefix: "pdf" }), _jsx(SystemPlaceholderSection, { validation: placeholderSource, onInsert: insert.insertPlaceholder, idPrefix: "pdf", isLayoutOwned: layoutFills }), _jsx(PdfResourceSection, { resourceKeys: resourceKeys, canUpload: detail.scope === scope, filesAvailable: filesAvailable, layoutName: layoutName, onInsert: insert.insertResource, onUpload: uploadResource })] })) : undefined, center: mode === "edit" ? (_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) => updateDraft({ ...current.draft, content }), regions: layout?.regions ?? null, layoutName: layoutName, resourceSrc: layoutResourceSrc, variableLabels: variableLabels, systemVariables: systemVariables, systemLabels: systemLabels, variableNames: variableNames, imageVariables: imageVariables, resourcePreviews: resourcePreviews, onEditorReady: insert.register("body") }) })) : (_jsx("div", { className: "min-h-0 flex-1 overflow-auto", children: generated !== null && generatedFresh ? (_jsx(PdfPreview, { source: { base64: generated.result.pdfBase64 }, testId: "document-designer-pdf-preview" })) : (_jsxs("div", { className: "flex flex-col items-start gap-3 p-6", "data-testid": "document-designer-preview-stale", children: [pdfAvailable && busy === null && _jsx(RunOnMount, { run: printOnce }), _jsx("p", { className: "text-sm text-muted-foreground", children: busy === "pdf"
|
|
340
|
-
? t("documentDesigner.preview.loading")
|
|
341
|
-
: t("documentDesigner.preview.stale") }), _jsx(Button, { variant: "secondary", disabled: !pdfAvailable || busy !== null, onClick: () => void generatePdf(), children: t("documentDesigner.actions.generatePdf") })] })) })), right: _jsx(SettingsPanel, { meta: current.draft.meta, onMetaChange: (meta) => updateDraft({ ...current.draft, meta }), layouts: layouts.rows, locale: locale, version: { id: detail.id, version: detail.version, status: detail.status }, vocabulary: vocabulary, insert: insert, draftKey: `${current.key}:${draftEpoch}` }) }), _jsx(EditorFooter, { testIdPrefix: "pdf-editor", message: footer, saveError: actionError, modifiedAt: detail.modifiedAt ?? null, dirty: dirty, busy: busy === "save", canSave: busy === null && validation?.canSave !== false, canPublish: busy === null && canPublish, onDiscard: () => resetDraft(detail), onSave: () => void save(), onPublish: () => void publish() }), _jsx(Dialog, { open: pendingLocale !== null, onOpenChange: (open) => {
|
|
342
|
-
if (!open)
|
|
343
|
-
setPendingLocale(null);
|
|
344
|
-
}, title: t("documentDesigner.localeDialog.title"), description: t("documentDesigner.localeDialog.description"), confirmLabel: t("documentDesigner.localeDialog.save"), cancelLabel: t("documentDesigner.localeDialog.cancel"), onConfirm: async () => {
|
|
345
|
-
const target = pendingLocale;
|
|
346
|
-
if (target !== null && (await save()))
|
|
347
|
-
onLocaleChange?.(target);
|
|
348
|
-
setPendingLocale(null);
|
|
349
|
-
}, children: _jsx(Button, { variant: "danger-ghost", onClick: () => {
|
|
350
|
-
const target = pendingLocale;
|
|
351
|
-
setPendingLocale(null);
|
|
352
|
-
resetDraft(detail);
|
|
353
|
-
if (target !== null)
|
|
354
|
-
onLocaleChange?.(target);
|
|
355
|
-
}, children: t("documentDesigner.localeDialog.discard") }) })] }));
|
|
52
|
+
const editing = s.ui.mode === "edit";
|
|
53
|
+
return (_jsxs(EditorFrame, { testId: "pdf-template-editor", children: [renderPdfHeader(context), renderPdfInfoBar(context), _jsx(EditorBody, { left: editing ? renderPdfLeftRail(context) : undefined, center: editing ? renderPdfEditCanvas(context) : renderPdfPreviewPane(context), right: renderPdfSettings(context) }), renderPdfFooter(context), renderPdfLocaleDialog(context)] }));
|
|
356
54
|
async function exportBundle() {
|
|
357
55
|
const result = await dispatcher.query(TemplateResolverQueries.exportBundle, {
|
|
358
56
|
slug,
|
|
@@ -360,7 +58,7 @@ export function PdfTemplateEditor({ slug, locale: requestedLocale, onLocaleChang
|
|
|
360
58
|
scope: "system",
|
|
361
59
|
});
|
|
362
60
|
if (!result.isSuccess) {
|
|
363
|
-
setActionError(describeError(t, result.error));
|
|
61
|
+
s.ui.setActionError(describeError(t, result.error));
|
|
364
62
|
return;
|
|
365
63
|
}
|
|
366
64
|
const url = URL.createObjectURL(new Blob([JSON.stringify(result.data, null, 2)], { type: "application/json" }));
|