@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.
Files changed (42) hide show
  1. package/dist/web/layout-editor-body.d.ts +3 -14
  2. package/dist/web/layout-editor-body.js +25 -172
  3. package/dist/web/layout-editor-center.d.ts +7 -0
  4. package/dist/web/layout-editor-center.js +23 -0
  5. package/dist/web/layout-editor-logic.d.ts +97 -0
  6. package/dist/web/layout-editor-logic.js +143 -0
  7. package/dist/web/layout-editor-paper.d.ts +4 -0
  8. package/dist/web/layout-editor-paper.js +24 -0
  9. package/dist/web/mail-editor-body.d.ts +4 -16
  10. package/dist/web/mail-editor-body.js +27 -218
  11. package/dist/web/mail-editor-logic.d.ts +70 -0
  12. package/dist/web/mail-editor-logic.js +81 -0
  13. package/dist/web/mail-editor-model.d.ts +105 -0
  14. package/dist/web/mail-editor-model.js +114 -0
  15. package/dist/web/mail-editor-render.d.ts +11 -0
  16. package/dist/web/mail-editor-render.js +51 -0
  17. package/dist/web/mail-preview-render.d.ts +5 -0
  18. package/dist/web/mail-preview-render.js +22 -0
  19. package/dist/web/pdf-editor-actions.d.ts +27 -0
  20. package/dist/web/pdf-editor-actions.js +146 -0
  21. package/dist/web/pdf-editor-chrome.d.ts +6 -0
  22. package/dist/web/pdf-editor-chrome.js +60 -0
  23. package/dist/web/pdf-editor-context.d.ts +22 -0
  24. package/dist/web/pdf-editor-context.js +1 -0
  25. package/dist/web/pdf-editor-draft.d.ts +23 -0
  26. package/dist/web/pdf-editor-draft.js +33 -0
  27. package/dist/web/pdf-editor-panes.d.ts +6 -0
  28. package/dist/web/pdf-editor-panes.js +40 -0
  29. package/dist/web/pdf-editor-view.d.ts +52 -0
  30. package/dist/web/pdf-editor-view.js +89 -0
  31. package/dist/web/pdf-template-editor.d.ts +1 -1
  32. package/dist/web/pdf-template-editor.js +40 -342
  33. package/dist/web/use-layout-editor-state.d.ts +62 -0
  34. package/dist/web/use-layout-editor-state.js +155 -0
  35. package/dist/web/use-mail-editor-parts.d.ts +42 -0
  36. package/dist/web/use-mail-editor-parts.js +74 -0
  37. package/dist/web/use-mail-editor-state.d.ts +89 -0
  38. package/dist/web/use-mail-editor-state.js +55 -0
  39. package/dist/web/use-pdf-editor-state.d.ts +77 -0
  40. package/dist/web/use-pdf-editor-state.js +161 -0
  41. package/package.json +6 -6
  42. package/src/changes.json +18 -0
@@ -0,0 +1,42 @@
1
+ import type { EditorMode } from "./editor-header.tsx";
2
+ import type { ConfirmDialog } from "./mail-dialogs.tsx";
3
+ import type { MailEditorBodyProps } from "./mail-editor-logic.ts";
4
+ export declare function useMailUiState(row: MailEditorBodyProps["row"]): {
5
+ baseline: import("./mail-types.ts").MailDraft;
6
+ draft: import("./mail-types.ts").MailDraft;
7
+ setDraft: import("react").Dispatch<import("react").SetStateAction<import("./mail-types.ts").MailDraft>>;
8
+ settled: import("./mail-types.ts").MailDraft;
9
+ settle: (next: import("./mail-types.ts").MailDraft) => void;
10
+ mode: EditorMode;
11
+ setMode: import("react").Dispatch<import("react").SetStateAction<EditorMode>>;
12
+ creating: boolean;
13
+ setCreating: import("react").Dispatch<import("react").SetStateAction<boolean>>;
14
+ dialog: ConfirmDialog;
15
+ setDialog: import("react").Dispatch<import("react").SetStateAction<ConfirmDialog>>;
16
+ uploadOpen: boolean;
17
+ setUploadOpen: import("react").Dispatch<import("react").SetStateAction<boolean>>;
18
+ pdfOpen: boolean;
19
+ setPdfOpen: import("react").Dispatch<import("react").SetStateAction<boolean>>;
20
+ resourceError: string | null;
21
+ setResourceError: import("react").Dispatch<import("react").SetStateAction<string | null>>;
22
+ resetError: string | null;
23
+ setResetError: import("react").Dispatch<import("react").SetStateAction<string | null>>;
24
+ editorEpoch: number;
25
+ setEditorEpoch: import("react").Dispatch<import("react").SetStateAction<number>>;
26
+ includeAttachments: boolean;
27
+ setIncludeAttachments: import("react").Dispatch<import("react").SetStateAction<boolean>>;
28
+ prefixSubject: boolean;
29
+ setPrefixSubject: import("react").Dispatch<import("react").SetStateAction<boolean>>;
30
+ };
31
+ export type MailUiState = ReturnType<typeof useMailUiState>;
32
+ export type MailServices = ReturnType<typeof useMailServices>;
33
+ export declare function useMailServices(props: MailEditorBodyProps): {
34
+ insert: import("./use-insert-targets.ts").InsertTargets;
35
+ persistence: import("./use-mail-persistence.ts").MailPersistence;
36
+ testMail: import("./use-test-mail.ts").TestMail;
37
+ own: import("./use-own-address.ts").OwnAddress;
38
+ filesAvailable: boolean;
39
+ resourceRemove: import("@cosmicdrift/kumiko-renderer").UseMutationResult<unknown>;
40
+ layouts: import("./use-overview-rows.ts").OverviewRows;
41
+ pdfTemplates: import("./use-overview-rows.ts").OverviewRows;
42
+ };
@@ -0,0 +1,74 @@
1
+ import { TemplateResolverHandlers } from "@cosmicdrift/kumiko-bundled-features/template-resolver/web";
2
+ import { useMutation } from "@cosmicdrift/kumiko-renderer";
3
+ import { useState } from "react";
4
+ import { loadedMailOf } from "./mail-draft.js";
5
+ import { useFilesCapability } from "./use-files-capability.js";
6
+ import { useInsertTargets } from "./use-insert-targets.js";
7
+ import { useMailPersistence } from "./use-mail-persistence.js";
8
+ import { useOverviewRows } from "./use-overview-rows.js";
9
+ import { useOwnAddress } from "./use-own-address.js";
10
+ import { useSettledValue } from "./use-settled-value.js";
11
+ import { useTestMail } from "./use-test-mail.js";
12
+ export function useMailUiState(row) {
13
+ const [baseline] = useState(() => loadedMailOf(row));
14
+ const [draft, setDraft] = useState(baseline);
15
+ const [settled, settle] = useSettledValue(baseline);
16
+ const [mode, setMode] = useState("edit");
17
+ const [creating, setCreating] = useState(false);
18
+ const [dialog, setDialog] = useState({ kind: "none" });
19
+ const [uploadOpen, setUploadOpen] = useState(false);
20
+ const [pdfOpen, setPdfOpen] = useState(false);
21
+ const [resourceError, setResourceError] = useState(null);
22
+ const [resetError, setResetError] = useState(null);
23
+ const [editorEpoch, setEditorEpoch] = useState(0);
24
+ const [includeAttachments, setIncludeAttachments] = useState(true);
25
+ const [prefixSubject, setPrefixSubject] = useState(true);
26
+ return {
27
+ baseline,
28
+ draft,
29
+ setDraft,
30
+ settled,
31
+ settle,
32
+ mode,
33
+ setMode,
34
+ creating,
35
+ setCreating,
36
+ dialog,
37
+ setDialog,
38
+ uploadOpen,
39
+ setUploadOpen,
40
+ pdfOpen,
41
+ setPdfOpen,
42
+ resourceError,
43
+ setResourceError,
44
+ resetError,
45
+ setResetError,
46
+ editorEpoch,
47
+ setEditorEpoch,
48
+ includeAttachments,
49
+ setIncludeAttachments,
50
+ prefixSubject,
51
+ setPrefixSubject,
52
+ };
53
+ }
54
+ export function useMailServices(props) {
55
+ const { slug, locale, title, isSystemAdmin, row } = props;
56
+ const insert = useInsertTargets();
57
+ const persistence = useMailPersistence({ slug, locale, title, isSystemAdmin, row });
58
+ const testMail = useTestMail();
59
+ const own = useOwnAddress();
60
+ const filesAvailable = useFilesCapability();
61
+ const resourceRemove = useMutation(TemplateResolverHandlers.resourceRemove);
62
+ const layouts = useOverviewRows("mail-layout");
63
+ const pdfTemplates = useOverviewRows("document-pdf");
64
+ return {
65
+ insert,
66
+ persistence,
67
+ testMail,
68
+ own,
69
+ filesAvailable,
70
+ resourceRemove,
71
+ layouts,
72
+ pdfTemplates,
73
+ };
74
+ }
@@ -0,0 +1,89 @@
1
+ import type { MailEditorBodyProps } from "./mail-editor-logic.ts";
2
+ import type { MailValidation, PickedValues } from "./mail-types.ts";
3
+ type Translate = (key: string, params?: Readonly<Record<string, unknown>>) => string;
4
+ export declare function useMailEditorState(props: MailEditorBodyProps, t: Translate): {
5
+ update: (patch: Partial<import("./mail-types.ts").MailDraft>) => void;
6
+ reset: () => void;
7
+ saveAndReload: (publish: boolean) => Promise<void>;
8
+ perform: (target: import("./mail-dialogs.tsx").LeaveTarget) => void;
9
+ leave: (target: import("./mail-dialogs.tsx").LeaveTarget) => void;
10
+ leaveSave: (target: import("./mail-dialogs.tsx").LeaveTarget) => Promise<void>;
11
+ removeResource: (key: string) => Promise<void>;
12
+ sendTestMail: () => void;
13
+ ui: {
14
+ baseline: import("./mail-types.ts").MailDraft;
15
+ draft: import("./mail-types.ts").MailDraft;
16
+ setDraft: import("react").Dispatch<import("react").SetStateAction<import("./mail-types.ts").MailDraft>>;
17
+ settled: import("./mail-types.ts").MailDraft;
18
+ settle: (next: import("./mail-types.ts").MailDraft) => void;
19
+ mode: import("./editor-header.tsx").EditorMode;
20
+ setMode: import("react").Dispatch<import("react").SetStateAction<import("./editor-header.tsx").EditorMode>>;
21
+ creating: boolean;
22
+ setCreating: import("react").Dispatch<import("react").SetStateAction<boolean>>;
23
+ dialog: import("./mail-dialogs.tsx").ConfirmDialog;
24
+ setDialog: import("react").Dispatch<import("react").SetStateAction<import("./mail-dialogs.tsx").ConfirmDialog>>;
25
+ uploadOpen: boolean;
26
+ setUploadOpen: import("react").Dispatch<import("react").SetStateAction<boolean>>;
27
+ pdfOpen: boolean;
28
+ setPdfOpen: import("react").Dispatch<import("react").SetStateAction<boolean>>;
29
+ resourceError: string | null;
30
+ setResourceError: import("react").Dispatch<import("react").SetStateAction<string | null>>;
31
+ resetError: string | null;
32
+ setResetError: import("react").Dispatch<import("react").SetStateAction<string | null>>;
33
+ editorEpoch: number;
34
+ setEditorEpoch: import("react").Dispatch<import("react").SetStateAction<number>>;
35
+ includeAttachments: boolean;
36
+ setIncludeAttachments: import("react").Dispatch<import("react").SetStateAction<boolean>>;
37
+ prefixSubject: boolean;
38
+ setPrefixSubject: import("react").Dispatch<import("react").SetStateAction<boolean>>;
39
+ };
40
+ services: {
41
+ insert: import("./use-insert-targets.ts").InsertTargets;
42
+ persistence: import("./use-mail-persistence.ts").MailPersistence;
43
+ testMail: import("./use-test-mail.ts").TestMail;
44
+ own: import("./use-own-address.ts").OwnAddress;
45
+ filesAvailable: boolean;
46
+ resourceRemove: import("@cosmicdrift/kumiko-renderer").UseMutationResult<unknown>;
47
+ layouts: import("./use-overview-rows.ts").OverviewRows;
48
+ pdfTemplates: import("./use-overview-rows.ts").OverviewRows;
49
+ };
50
+ validation: MailValidation | null;
51
+ picked: Readonly<Record<string, unknown>> | null;
52
+ setPicked: (next: PickedValues | null) => void;
53
+ pickedVariables: Readonly<Record<string, unknown>> | null;
54
+ variableSchema: Readonly<Record<string, unknown>>;
55
+ locked: boolean;
56
+ dirty: boolean;
57
+ busy: boolean;
58
+ canSave: boolean;
59
+ canPublish: boolean;
60
+ resources: readonly import("./mail-types.ts").LinkedResourceView[];
61
+ pdfTitles: Map<string, string>;
62
+ uploadTemplateId: string | null;
63
+ takenKeys: string[];
64
+ attachments: ({
65
+ readonly kind: "file";
66
+ readonly key: string;
67
+ readonly fileRefId: string;
68
+ readonly fileName: string;
69
+ } | {
70
+ readonly kind: "pdf";
71
+ readonly key: string;
72
+ readonly templateSlug: string;
73
+ readonly fileName: string;
74
+ })[];
75
+ resourcePreviews: {
76
+ [k: string]: {
77
+ src: string;
78
+ caption: string;
79
+ };
80
+ };
81
+ railValidation: MailValidation | null;
82
+ vocabulary: import("./mail-edit-center.tsx").EditorVocabulary;
83
+ footer: {
84
+ readonly tone: "bad" | "warn" | "ok";
85
+ readonly text: string;
86
+ } | null;
87
+ fieldErrors: Partial<Record<"previewText" | "subject", string>>;
88
+ };
89
+ export {};
@@ -0,0 +1,55 @@
1
+ import { TemplateResolverQueries } from "@cosmicdrift/kumiko-bundled-features/template-resolver/web";
2
+ import { useQuery, useUnsavedInputSource } from "@cosmicdrift/kumiko-renderer";
3
+ import { useEffect, useRef, useState } from "react";
4
+ import { buildMeta, isDraftDirty, parseJsonRecord, pickDeclaredVariables } from "./mail-draft.js";
5
+ import { buildMailModel } from "./mail-editor-model.js";
6
+ import { useMailServices, useMailUiState } from "./use-mail-editor-parts.js";
7
+ export function useMailEditorState(props, t) {
8
+ const { slug, locale, isSystemAdmin, row } = props;
9
+ const ui = useMailUiState(row);
10
+ const [picked, setPicked] = useState(null);
11
+ const services = useMailServices(props);
12
+ const locked = row === null && !ui.creating;
13
+ const variableSchema = parseJsonRecord(row?.variableSchema);
14
+ const validation = useQuery(TemplateResolverQueries.validate, {
15
+ slug,
16
+ kind: "mail",
17
+ contentFormat: "html",
18
+ content: ui.settled.body,
19
+ meta: buildMeta(ui.settled),
20
+ variableSchema,
21
+ locale,
22
+ scope: isSystemAdmin ? "system" : "tenant",
23
+ }).data;
24
+ const pickedVariables = pickDeclaredVariables(picked, validation?.variables);
25
+ // A pick belongs to the variables it was made for: after the declared set changes it is dropped,
26
+ // so a re-added variable does not revive an old record value.
27
+ const declaredVariables = validation?.variables;
28
+ const declaredVariableNames = declaredVariables === undefined ? null : Object.keys(declaredVariables).sort().join("|");
29
+ const previousDeclaredNames = useRef(declaredVariableNames);
30
+ useEffect(() => {
31
+ // skip: validation has not loaded, so there is no declared set to compare
32
+ if (declaredVariableNames === null)
33
+ return;
34
+ if (previousDeclaredNames.current !== null &&
35
+ previousDeclaredNames.current !== declaredVariableNames) {
36
+ setPicked(null);
37
+ }
38
+ previousDeclaredNames.current = declaredVariableNames;
39
+ }, [declaredVariableNames]);
40
+ const dirty = isDraftDirty(ui.draft, ui.baseline);
41
+ useUnsavedInputSource(() => dirty);
42
+ return buildMailModel({
43
+ props,
44
+ t,
45
+ ui,
46
+ picked,
47
+ setPicked,
48
+ services,
49
+ validation,
50
+ variableSchema,
51
+ pickedVariables,
52
+ locked,
53
+ dirty,
54
+ });
55
+ }
@@ -0,0 +1,77 @@
1
+ import type { EditorMode } from "./editor-header.tsx";
2
+ import { type EditorState, type GeneratedPdf } from "./pdf-editor-draft.ts";
3
+ import { type PuppeteerStatus, type TemplateDetail, type TemplateListRow, type TemplateScope, type TenantOverrides, type ValidationResult } from "./types.ts";
4
+ type Translate = (key: string, params?: Readonly<Record<string, unknown>>) => string;
5
+ export declare function usePdfEditorState(input: {
6
+ readonly slug: string;
7
+ readonly requestedLocale: string | undefined;
8
+ }, t: Translate): {
9
+ host: {
10
+ insert: import("./use-insert-targets.ts").InsertTargets;
11
+ filesAvailable: boolean;
12
+ overview: import("./use-overview-rows.ts").OverviewRows;
13
+ scope: TemplateScope;
14
+ };
15
+ doc: {
16
+ rowsQuery: import("@cosmicdrift/kumiko-renderer").UseQueryResult<readonly TemplateListRow[]>;
17
+ locales: readonly string[];
18
+ locale: string;
19
+ row: TemplateListRow | undefined;
20
+ detailQuery: import("@cosmicdrift/kumiko-renderer").UseQueryResult<TemplateDetail | null>;
21
+ detail: TemplateDetail | null;
22
+ setState: import("react").Dispatch<import("react").SetStateAction<EditorState | null>>;
23
+ current: EditorState | null;
24
+ };
25
+ settleTimer: import("react").RefObject<NodeJS.Timeout | null>;
26
+ ui: {
27
+ overridesOpen: boolean;
28
+ setOverridesOpen: import("react").Dispatch<import("react").SetStateAction<boolean>>;
29
+ draftEpoch: number;
30
+ setDraftEpoch: import("react").Dispatch<import("react").SetStateAction<number>>;
31
+ mode: EditorMode;
32
+ setMode: import("react").Dispatch<import("react").SetStateAction<EditorMode>>;
33
+ busy: "pdf" | "publish" | "save" | null;
34
+ setBusy: import("react").Dispatch<import("react").SetStateAction<"pdf" | "publish" | "save" | null>>;
35
+ actionError: string | null;
36
+ setActionError: import("react").Dispatch<import("react").SetStateAction<string | null>>;
37
+ generated: GeneratedPdf | null;
38
+ setGenerated: import("react").Dispatch<import("react").SetStateAction<GeneratedPdf | null>>;
39
+ autoPrintedFor: import("react").RefObject<string | null>;
40
+ pendingLocale: string | null;
41
+ setPendingLocale: import("react").Dispatch<import("react").SetStateAction<string | null>>;
42
+ };
43
+ server: {
44
+ variableSchema: Record<string, unknown>;
45
+ validationQuery: import("@cosmicdrift/kumiko-renderer").UseQueryResult<ValidationResult>;
46
+ validation: ValidationResult | null;
47
+ layout: {
48
+ readonly slug: string;
49
+ readonly scope: TemplateScope;
50
+ readonly html: string;
51
+ readonly regions: import("./types.ts").LayoutRegionsHtml;
52
+ } | null;
53
+ layouts: import("./use-overview-rows.ts").OverviewRows;
54
+ brandingLogoFile: string;
55
+ statusQuery: import("@cosmicdrift/kumiko-renderer").UseQueryResult<PuppeteerStatus>;
56
+ showOrigin: boolean;
57
+ overridesQuery: import("@cosmicdrift/kumiko-renderer").UseQueryResult<TenantOverrides>;
58
+ };
59
+ vocabulary: {
60
+ variables: Readonly<Record<string, import("./mail-types.ts").MailVariable>>;
61
+ imageVariables: string[];
62
+ systemVariables: readonly string[];
63
+ systemLabels: Readonly<Record<string, string>>;
64
+ variableLabels: {
65
+ [x: string]: string;
66
+ };
67
+ variableNames: string[];
68
+ resourcePreviews: {
69
+ [k: string]: {
70
+ src: string;
71
+ caption: string;
72
+ };
73
+ };
74
+ };
75
+ };
76
+ export type PdfEditorState = ReturnType<typeof usePdfEditorState>;
77
+ export {};
@@ -0,0 +1,161 @@
1
+ import { TemplateResolverQueries } from "@cosmicdrift/kumiko-bundled-features/template-resolver/web";
2
+ import { useQuery, useUserRoles } from "@cosmicdrift/kumiko-renderer";
3
+ import { RendererPuppeteerClientQueries } from "@cosmicdrift/kumiko-renderer-puppeteer-client/qualified-names";
4
+ import { useEffect, useMemo, useRef, useState } from "react";
5
+ import { BRANDING_QUERY_QN } from "./constants.js";
6
+ import { systemLabelMap, variableLabelMap } from "./mail-draft.js";
7
+ import { draftFromDetail, imageLabelSource, NO_VARIABLES, snapshotOf, textVariablesOf, } from "./pdf-editor-draft.js";
8
+ import { visibleSystemPlaceholders } from "./pdf-left-rail.js";
9
+ import { serializePdfMeta } from "./pdf-meta.js";
10
+ import { localesOf, parseJsonObject, pickRow, resolveLocale } from "./template-rows.js";
11
+ import { DOCUMENT_PDF_KIND, PDF_LAYOUT_KIND, } from "./types.js";
12
+ import { useFilesCapability } from "./use-files-capability.js";
13
+ import { useInsertTargets } from "./use-insert-targets.js";
14
+ import { useOverviewRows } from "./use-overview-rows.js";
15
+ function usePdfHostServices() {
16
+ const insert = useInsertTargets();
17
+ const filesAvailable = useFilesCapability();
18
+ const overview = useOverviewRows(DOCUMENT_PDF_KIND);
19
+ const roles = useUserRoles();
20
+ const scope = roles?.includes("SystemAdmin") === true ? "system" : "tenant";
21
+ return { insert, filesAvailable, overview, scope };
22
+ }
23
+ function usePdfDocument(slug, requestedLocale, scope) {
24
+ const rowsQuery = useQuery(TemplateResolverQueries.list, {
25
+ kind: DOCUMENT_PDF_KIND,
26
+ });
27
+ const rows = rowsQuery.data ?? [];
28
+ const locales = localesOf(rows, slug);
29
+ const locale = resolveLocale(requestedLocale, locales);
30
+ const row = pickRow(rows, slug, locale, scope);
31
+ const detailQuery = useQuery(TemplateResolverQueries.findById, { id: row?.id ?? "" }, { enabled: row !== undefined });
32
+ // useQuery keeps the previous key's data while the next one loads, so a scope or locale
33
+ // switch would otherwise hand the old row to save and publish.
34
+ const detail = row !== undefined && detailQuery.data?.id === row.id ? detailQuery.data : null;
35
+ const [state, setState] = useState(null);
36
+ if (detail !== null && (state === null || state.key !== detail.id)) {
37
+ const draft = draftFromDetail(detail);
38
+ setState({ key: detail.id, draft, settled: draft, snapshot: snapshotOf(draft) });
39
+ }
40
+ const current = state !== null && detail !== null && state.key === detail.id ? state : null;
41
+ return { rowsQuery, locales, locale, row, detailQuery, detail, setState, current };
42
+ }
43
+ function useSettleTimer() {
44
+ const settleTimer = useRef(null);
45
+ // A pending settle must not fire into an unmounted editor.
46
+ useEffect(() => () => {
47
+ if (settleTimer.current !== null)
48
+ clearTimeout(settleTimer.current);
49
+ }, []);
50
+ return settleTimer;
51
+ }
52
+ function usePdfUiState() {
53
+ const [overridesOpen, setOverridesOpen] = useState(false);
54
+ const [draftEpoch, setDraftEpoch] = useState(0);
55
+ const [mode, setMode] = useState("edit");
56
+ const [busy, setBusy] = useState(null);
57
+ const [actionError, setActionError] = useState(null);
58
+ const [generated, setGenerated] = useState(null);
59
+ const autoPrintedFor = useRef(null);
60
+ const [pendingLocale, setPendingLocale] = useState(null);
61
+ return {
62
+ overridesOpen,
63
+ setOverridesOpen,
64
+ draftEpoch,
65
+ setDraftEpoch,
66
+ mode,
67
+ setMode,
68
+ busy,
69
+ setBusy,
70
+ actionError,
71
+ setActionError,
72
+ generated,
73
+ setGenerated,
74
+ autoPrintedFor,
75
+ pendingLocale,
76
+ setPendingLocale,
77
+ };
78
+ }
79
+ function usePdfServerQueries(input) {
80
+ const { slug, scope } = input;
81
+ const { locale, detail, current } = input.doc;
82
+ const variableSchema = useMemo(() => parseJsonObject(detail?.variableSchema ?? null), [detail?.variableSchema]);
83
+ const settledMeta = current === null ? null : serializePdfMeta(current.settled.meta);
84
+ const validationQuery = useQuery(TemplateResolverQueries.validate, {
85
+ slug,
86
+ kind: DOCUMENT_PDF_KIND,
87
+ contentFormat: "html",
88
+ content: current?.settled.content ?? "",
89
+ meta: settledMeta ?? {},
90
+ variableSchema,
91
+ locale,
92
+ scope,
93
+ }, { enabled: current !== null });
94
+ const validation = validationQuery.data;
95
+ const layoutSlug = current?.settled.meta.layoutSlug ?? "";
96
+ const previewQuery = useQuery(TemplateResolverQueries.preview, {
97
+ slug,
98
+ kind: DOCUMENT_PDF_KIND,
99
+ locale,
100
+ content: detail?.content ?? "",
101
+ meta: layoutSlug === "" ? {} : { layoutSlug },
102
+ variableSchema,
103
+ }, { enabled: detail !== null });
104
+ const layout = previewQuery.error === null ? (previewQuery.data?.layout ?? null) : null;
105
+ const layouts = useOverviewRows(PDF_LAYOUT_KIND);
106
+ // The built-in "logo" resource of a layout is the tenant's branding logo.
107
+ const branding = useQuery(BRANDING_QUERY_QN, {});
108
+ const brandingLogoFile = branding.data?.logoFile ?? "";
109
+ const statusQuery = useQuery(RendererPuppeteerClientQueries.status, {});
110
+ const showOrigin = scope === "system";
111
+ const overridesQuery = useQuery(TemplateResolverQueries.tenantOverrides, { slug, kind: DOCUMENT_PDF_KIND, locale }, { enabled: showOrigin && detail !== null });
112
+ return {
113
+ variableSchema,
114
+ validationQuery,
115
+ validation,
116
+ layout,
117
+ layouts,
118
+ brandingLogoFile,
119
+ statusQuery,
120
+ showOrigin,
121
+ overridesQuery,
122
+ };
123
+ }
124
+ function usePdfVocabulary(t, validation, detail) {
125
+ const allVariables = validation?.variables ?? NO_VARIABLES;
126
+ // Photos are record data, never an inline placeholder: images variables only feed the Images block.
127
+ const variables = useMemo(() => textVariablesOf(allVariables), [allVariables]);
128
+ const imageVariables = useMemo(() => Object.keys(allVariables).filter((name) => allVariables[name]?.type === "images"), [allVariables]);
129
+ const systemVariables = useMemo(() => visibleSystemPlaceholders(validation?.systemPlaceholders ?? []), [validation?.systemPlaceholders]);
130
+ const systemLabels = useMemo(() => systemLabelMap(t, systemVariables), [t, systemVariables]);
131
+ const variableLabels = useMemo(() => ({
132
+ ...systemLabels,
133
+ ...variableLabelMap(t, variables),
134
+ ...variableLabelMap(t, imageLabelSource(allVariables)),
135
+ }), [systemLabels, variables, allVariables, t]);
136
+ const variableNames = useMemo(() => Object.keys(variables), [variables]);
137
+ const linkedResources = detail?.linkedResources ?? null;
138
+ // The editor shows resource images from the stored copies; no extra request is made here.
139
+ const resourcePreviews = useMemo(() => Object.fromEntries(Object.entries(parseJsonObject(linkedResources)).flatMap(([key, fileRefId]) => typeof fileRefId === "string"
140
+ ? [[key, { src: `/api/files/${encodeURIComponent(fileRefId)}`, caption: key }]]
141
+ : [])), [linkedResources]);
142
+ return {
143
+ variables,
144
+ imageVariables,
145
+ systemVariables,
146
+ systemLabels,
147
+ variableLabels,
148
+ variableNames,
149
+ resourcePreviews,
150
+ };
151
+ }
152
+ export function usePdfEditorState(input, t) {
153
+ const { slug, requestedLocale } = input;
154
+ const host = usePdfHostServices();
155
+ const doc = usePdfDocument(slug, requestedLocale, host.scope);
156
+ const settleTimer = useSettleTimer();
157
+ const ui = usePdfUiState();
158
+ const server = usePdfServerQueries({ slug, scope: host.scope, doc });
159
+ const vocabulary = usePdfVocabulary(t, server.validation, doc.detail);
160
+ return { host, doc, settleTimer, ui, server, vocabulary };
161
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cosmicdrift/kumiko-document-designer",
3
- "version": "0.389.0",
3
+ "version": "0.390.0",
4
4
  "description": "Document designer for Kumiko: template overview, layout editor (mail layout, PDF letterhead) and the PDF template editor.",
5
5
  "license": "MIT",
6
6
  "author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
@@ -20,11 +20,11 @@
20
20
  "./web/styles.css": "./src/web/styles.css"
21
21
  },
22
22
  "dependencies": {
23
- "@cosmicdrift/kumiko-bundled-features": "0.389.0",
24
- "@cosmicdrift/kumiko-framework": "0.389.0",
25
- "@cosmicdrift/kumiko-renderer": "0.389.0",
26
- "@cosmicdrift/kumiko-renderer-puppeteer-client": "0.389.0",
27
- "@cosmicdrift/kumiko-types": "0.389.0",
23
+ "@cosmicdrift/kumiko-bundled-features": "0.390.0",
24
+ "@cosmicdrift/kumiko-framework": "0.390.0",
25
+ "@cosmicdrift/kumiko-renderer": "0.390.0",
26
+ "@cosmicdrift/kumiko-renderer-puppeteer-client": "0.390.0",
27
+ "@cosmicdrift/kumiko-types": "0.390.0",
28
28
  "react": "^19.2.6"
29
29
  },
30
30
  "devDependencies": {
package/src/changes.json CHANGED
@@ -1,4 +1,22 @@
1
1
  [
2
+ {
3
+ "version": "0.390.0",
4
+ "type": "improvement",
5
+ "title": "PDF editor actions drop unreachable null checks",
6
+ "detail": "generatePdf, printOnce and the save path of the PDF template editor no longer check for a missing editor state; the editor only builds its actions once the state is loaded, and the type already excludes null. A new test pins the publishSystem path for a SystemAdmin who holds a clean tenant template."
7
+ },
8
+ {
9
+ "version": "0.390.0",
10
+ "type": "fix",
11
+ "title": "The PDF editor no longer saves or publishes the previous row after a scope switch",
12
+ "detail": "When the host scope changed while the editor was open, for example once the SystemAdmin role loaded, the editor kept showing the tenant copy until the system row arrived. Save then wrote the tenant content as the system template, and publish sent the tenant row to publishSystem. The editor now shows the loading state until the detail of the newly picked row has loaded."
13
+ },
14
+ {
15
+ "version": "0.390.0",
16
+ "type": "improvement",
17
+ "title": "Split PDF, layout and mail editors into hooks and render functions",
18
+ "detail": "PdfTemplateEditor, LayoutEditorBody and MailEditorBody compose focused state hooks, pure logic modules and render functions for chrome, panes and previews. Rendered markup and behavior are unchanged."
19
+ },
2
20
  {
3
21
  "version": "0.389.0",
4
22
  "type": "fix",