@cosmicdrift/kumiko-document-designer 0.363.0 → 0.365.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/constants.d.ts +3 -0
- package/dist/constants.js +4 -0
- package/dist/i18n.js +208 -50
- package/dist/web/cells.js +4 -3
- package/dist/web/editor-header.d.ts +37 -0
- package/dist/web/editor-header.js +43 -0
- package/dist/web/editor-locale.d.ts +1 -0
- package/dist/web/editor-locale.js +15 -0
- package/dist/web/editor-shell.d.ts +40 -0
- package/dist/web/editor-shell.js +36 -0
- package/dist/web/i18n.js +35 -59
- package/dist/web/layout-editor-body.d.ts +2 -2
- package/dist/web/layout-editor-body.js +101 -52
- package/dist/web/layout-editor.js +14 -7
- package/dist/web/layout-html.d.ts +2 -0
- package/dist/web/layout-html.js +13 -1
- package/dist/web/layout-meta.d.ts +29 -0
- package/dist/web/layout-meta.js +32 -0
- package/dist/web/layout-rails.d.ts +15 -5
- package/dist/web/layout-rails.js +61 -68
- package/dist/web/layout-types.d.ts +4 -0
- package/dist/web/layout-users.d.ts +1 -0
- package/dist/web/layout-users.js +4 -1
- package/dist/web/mail-dialogs.d.ts +1 -6
- package/dist/web/mail-dialogs.js +2 -2
- package/dist/web/mail-draft.d.ts +8 -6
- package/dist/web/mail-draft.js +45 -11
- package/dist/web/mail-edit-center.d.ts +3 -1
- package/dist/web/mail-edit-center.js +5 -4
- package/dist/web/mail-editor-body.d.ts +0 -1
- package/dist/web/mail-editor-body.js +60 -45
- package/dist/web/mail-editor.js +9 -14
- package/dist/web/mail-footer.d.ts +3 -14
- package/dist/web/mail-footer.js +12 -13
- package/dist/web/mail-left-rail.d.ts +9 -4
- package/dist/web/mail-left-rail.js +18 -15
- package/dist/web/mail-missing-locale.d.ts +10 -0
- package/dist/web/mail-missing-locale.js +45 -0
- package/dist/web/mail-origin-bar.d.ts +3 -5
- package/dist/web/mail-origin-bar.js +26 -50
- package/dist/web/mail-preview-side.d.ts +12 -5
- package/dist/web/mail-preview-side.js +14 -5
- package/dist/web/mail-preview.d.ts +9 -5
- package/dist/web/mail-preview.js +24 -13
- package/dist/web/mail-right-rail.d.ts +8 -0
- package/dist/web/mail-right-rail.js +65 -14
- package/dist/web/mail-types.d.ts +10 -1
- package/dist/web/mail-upload-modal.js +12 -2
- package/dist/web/mail-versions.js +3 -3
- package/dist/web/origin-bar.d.ts +11 -2
- package/dist/web/origin-bar.js +46 -24
- package/dist/web/overrides-drawer.d.ts +3 -2
- package/dist/web/overrides-drawer.js +87 -11
- package/dist/web/overview-row.d.ts +2 -0
- package/dist/web/overview-row.js +2 -1
- package/dist/web/page-canvas.d.ts +2 -1
- package/dist/web/page-canvas.js +23 -13
- package/dist/web/pdf-footer.d.ts +13 -0
- package/dist/web/pdf-footer.js +34 -0
- package/dist/web/pdf-left-rail.d.ts +9 -0
- package/dist/web/pdf-left-rail.js +16 -0
- package/dist/web/pdf-meta.d.ts +3 -5
- package/dist/web/pdf-meta.js +10 -16
- package/dist/web/pdf-preview.js +21 -8
- package/dist/web/pdf-template-editor.js +91 -76
- package/dist/web/rail.d.ts +13 -1
- package/dist/web/rail.js +15 -7
- package/dist/web/relative-time.d.ts +1 -0
- package/dist/web/relative-time.js +18 -0
- package/dist/web/settings-panel.d.ts +11 -5
- package/dist/web/settings-panel.js +24 -13
- package/dist/web/status-label.d.ts +7 -0
- package/dist/web/status-label.js +8 -0
- package/dist/web/types.d.ts +23 -0
- package/dist/web/use-debounced-value.d.ts +1 -0
- package/dist/web/use-debounced-value.js +11 -0
- package/dist/web/use-insert-targets.d.ts +2 -2
- package/dist/web/use-insert-targets.js +4 -1
- package/dist/web/use-mail-persistence.d.ts +2 -2
- package/dist/web/use-mail-persistence.js +4 -4
- package/dist/web/use-own-address.d.ts +5 -0
- package/dist/web/use-own-address.js +14 -0
- package/dist/web/used-by-label.d.ts +1 -0
- package/dist/web/used-by-label.js +11 -0
- package/package.json +8 -8
- package/src/changes.json +48 -0
- package/dist/web/mail-header.d.ts +0 -23
- package/dist/web/mail-header.js +0 -16
- package/dist/web/placeholder-panel.d.ts +0 -21
- package/dist/web/placeholder-panel.js +0 -35
|
@@ -3,36 +3,48 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
|
|
|
3
3
|
import { TemplateResolverHandlers, TemplateResolverQueries, } from "@cosmicdrift/kumiko-bundled-features/template-resolver/web";
|
|
4
4
|
import { useDispatcher, usePrimitives, useQuery, useTranslation, useUserRoles, } from "@cosmicdrift/kumiko-renderer";
|
|
5
5
|
import { RendererPuppeteerClientQueries } from "@cosmicdrift/kumiko-renderer-puppeteer-client/qualified-names";
|
|
6
|
-
import { ErrorState, LoadingState,
|
|
7
|
-
import {
|
|
6
|
+
import { ErrorState, LoadingState, uploadFileRef } from "@cosmicdrift/kumiko-renderer-web";
|
|
7
|
+
import { useEffect, useMemo, useRef, useState } from "react";
|
|
8
|
+
import { BRANDING_QUERY_QN } from "../constants.js";
|
|
9
|
+
import { EditorHeader, EditorLocaleSwitch, EditorModeSwitch, originOf, } from "./editor-header.js";
|
|
10
|
+
import { EditorBody, EditorFooter, EditorFrame, EditorInfoBar } from "./editor-shell.js";
|
|
8
11
|
import { describeError, isAlreadyActive } from "./errors.js";
|
|
12
|
+
import { fileUrl } from "./file-name.js";
|
|
13
|
+
import { systemLabelMap, variableLabelMap } from "./mail-draft.js";
|
|
14
|
+
import { PlaceholderSection, SystemPlaceholderSection } from "./mail-left-rail.js";
|
|
9
15
|
import { OverridesDrawer } from "./overrides-drawer.js";
|
|
10
16
|
import { PageCanvas } from "./page-canvas.js";
|
|
17
|
+
import { pdfFooterMessage } from "./pdf-footer.js";
|
|
18
|
+
import { PdfResourceSection, visibleSystemPlaceholders } from "./pdf-left-rail.js";
|
|
11
19
|
import { EMPTY_PDF_META, parsePdfMeta, serializePdfMeta } from "./pdf-meta.js";
|
|
12
20
|
import { PdfPreview } from "./pdf-preview.js";
|
|
13
|
-
import { PlaceholderPanel, visibleSystemPlaceholders, } from "./placeholder-panel.js";
|
|
14
21
|
import { SettingsPanel } from "./settings-panel.js";
|
|
22
|
+
import { draftBesideActiveSentence } from "./status-label.js";
|
|
15
23
|
import { localesOf, parseJsonObject, pickRow, resolveLocale } from "./template-rows.js";
|
|
16
24
|
import { DEFAULT_LAYOUT_SLUG, DOCUMENT_PDF_KIND, PDF_LAYOUT_KIND, } from "./types.js";
|
|
25
|
+
import { useInsertTargets } from "./use-insert-targets.js";
|
|
26
|
+
import { useOverviewRows } from "./use-overview-rows.js";
|
|
27
|
+
import { templateTitle } from "./used-by-label.js";
|
|
17
28
|
const SETTLE_MS = 500;
|
|
18
29
|
const NO_VARIABLES = {};
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
30
|
+
// Opening the preview prints the page without a click; a failed print is not retried for the same draft.
|
|
31
|
+
function RunOnMount({ run }) {
|
|
32
|
+
useEffect(() => {
|
|
33
|
+
run();
|
|
34
|
+
}, [run]);
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
22
37
|
const draftFromDetail = (detail) => ({
|
|
23
38
|
content: detail.content,
|
|
24
39
|
meta: parsePdfMeta(detail.meta),
|
|
25
40
|
});
|
|
26
41
|
const snapshotOf = (draft) => JSON.stringify({ content: draft.content, meta: serializePdfMeta(draft.meta) });
|
|
27
|
-
function formatSize(bytes) {
|
|
28
|
-
return bytes < 1024 * 1024
|
|
29
|
-
? `${Math.max(1, Math.round(bytes / 1024))} KB`
|
|
30
|
-
: `${(bytes / 1024 / 1024).toFixed(1)} MB`;
|
|
31
|
-
}
|
|
32
42
|
export function PdfTemplateEditor({ slug, locale: requestedLocale, onLocaleChange, }) {
|
|
33
43
|
const t = useTranslation();
|
|
34
44
|
const dispatcher = useDispatcher();
|
|
35
45
|
const { Button, Banner, Dialog } = usePrimitives();
|
|
46
|
+
const insert = useInsertTargets();
|
|
47
|
+
const overview = useOverviewRows(DOCUMENT_PDF_KIND);
|
|
36
48
|
const roles = useUserRoles();
|
|
37
49
|
const scope = roles?.includes("SystemAdmin") === true ? "system" : "tenant";
|
|
38
50
|
const rowsQuery = useQuery(TemplateResolverQueries.list, {
|
|
@@ -61,11 +73,12 @@ export function PdfTemplateEditor({ slug, locale: requestedLocale, onLocaleChang
|
|
|
61
73
|
clearTimeout(settleTimer.current);
|
|
62
74
|
}, []);
|
|
63
75
|
const [overridesOpen, setOverridesOpen] = useState(false);
|
|
64
|
-
const
|
|
76
|
+
const [draftEpoch, setDraftEpoch] = useState(0);
|
|
65
77
|
const [mode, setMode] = useState("edit");
|
|
66
78
|
const [busy, setBusy] = useState(null);
|
|
67
79
|
const [actionError, setActionError] = useState(null);
|
|
68
80
|
const [generated, setGenerated] = useState(null);
|
|
81
|
+
const autoPrintedFor = useRef(null);
|
|
69
82
|
const [pendingLocale, setPendingLocale] = useState(null);
|
|
70
83
|
const variableSchema = useMemo(() => parseJsonObject(detail?.variableSchema ?? null), [detail?.variableSchema]);
|
|
71
84
|
const settledMeta = current === null ? null : serializePdfMeta(current.settled.meta);
|
|
@@ -90,35 +103,25 @@ export function PdfTemplateEditor({ slug, locale: requestedLocale, onLocaleChang
|
|
|
90
103
|
variableSchema,
|
|
91
104
|
}, { enabled: detail !== null });
|
|
92
105
|
const layout = previewQuery.error === null ? (previewQuery.data?.layout ?? null) : null;
|
|
93
|
-
const
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
const layoutSlugs = [
|
|
98
|
-
...new Set((layoutsQuery.data ?? []).map((layoutRow) => layoutRow.slug)),
|
|
99
|
-
].sort();
|
|
106
|
+
const layouts = useOverviewRows(PDF_LAYOUT_KIND);
|
|
107
|
+
// The built-in "logo" resource of a layout is the tenant's branding logo.
|
|
108
|
+
const branding = useQuery(BRANDING_QUERY_QN, {});
|
|
109
|
+
const brandingLogoFile = branding.data?.logoFile ?? "";
|
|
100
110
|
const statusQuery = useQuery(RendererPuppeteerClientQueries.status, {});
|
|
101
111
|
const pdfAvailable = statusQuery.error === null && statusQuery.data?.available === true;
|
|
102
112
|
const unavailableMessage = statusQuery.data?.available === false ? (statusQuery.data.message ?? "") : "";
|
|
103
113
|
const showOrigin = scope === "system";
|
|
104
114
|
const overridesQuery = useQuery(TemplateResolverQueries.tenantOverrides, { slug, kind: DOCUMENT_PDF_KIND, locale }, { enabled: showOrigin && detail !== null });
|
|
105
115
|
const variables = validation?.variables ?? NO_VARIABLES;
|
|
106
|
-
const systemLabel = useCallback((name) => {
|
|
107
|
-
const label = t(`documentDesigner.system.${name}`);
|
|
108
|
-
return label === `documentDesigner.system.${name}` ? name : label;
|
|
109
|
-
}, [t]);
|
|
110
|
-
const variableLabels = useMemo(() => Object.fromEntries(Object.entries(variables).flatMap(([name, info]) => info.label === undefined ? [] : [[name, info.label]])), [variables]);
|
|
111
|
-
const systemLabels = useMemo(() => Object.fromEntries(SYSTEM_LABEL_NAMES.map((name) => [name, systemLabel(name)])), [systemLabel]);
|
|
112
116
|
const systemVariables = useMemo(() => visibleSystemPlaceholders(validation?.systemPlaceholders ?? []), [validation?.systemPlaceholders]);
|
|
117
|
+
const systemLabels = useMemo(() => systemLabelMap(t, systemVariables), [t, systemVariables]);
|
|
118
|
+
const variableLabels = useMemo(() => ({ ...systemLabels, ...variableLabelMap(variables) }), [systemLabels, variables]);
|
|
113
119
|
const variableNames = useMemo(() => Object.keys(variables), [variables]);
|
|
114
120
|
const linkedResources = detail?.linkedResources ?? null;
|
|
115
121
|
// The editor shows resource images from the stored copies; no extra request is made here.
|
|
116
122
|
const resourcePreviews = useMemo(() => Object.fromEntries(Object.entries(parseJsonObject(linkedResources)).flatMap(([key, fileRefId]) => typeof fileRefId === "string"
|
|
117
123
|
? [[key, { src: `/api/files/${encodeURIComponent(fileRefId)}`, caption: key }]]
|
|
118
124
|
: [])), [linkedResources]);
|
|
119
|
-
const onEditorReady = useCallback((handle) => {
|
|
120
|
-
editorHandle.current = handle;
|
|
121
|
-
}, []);
|
|
122
125
|
if (rowsQuery.loading && rowsQuery.data === null)
|
|
123
126
|
return _jsx(LoadingState, {});
|
|
124
127
|
if (rowsQuery.error !== null)
|
|
@@ -143,6 +146,7 @@ export function PdfTemplateEditor({ slug, locale: requestedLocale, onLocaleChang
|
|
|
143
146
|
if (settleTimer.current !== null)
|
|
144
147
|
clearTimeout(settleTimer.current);
|
|
145
148
|
setState({ key: source.id, draft, settled: draft, snapshot: snapshotOf(draft) });
|
|
149
|
+
setDraftEpoch((epoch) => epoch + 1);
|
|
146
150
|
setActionError(null);
|
|
147
151
|
}
|
|
148
152
|
async function reload() {
|
|
@@ -220,10 +224,11 @@ export function PdfTemplateEditor({ slug, locale: requestedLocale, onLocaleChang
|
|
|
220
224
|
setActionError(describeError(t, result.error));
|
|
221
225
|
setBusy(null);
|
|
222
226
|
}
|
|
223
|
-
function
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
+
function printOnce() {
|
|
228
|
+
if (current === null || autoPrintedFor.current === snapshotOf(current.draft))
|
|
229
|
+
return;
|
|
230
|
+
autoPrintedFor.current = snapshotOf(current.draft);
|
|
231
|
+
void generatePdf();
|
|
227
232
|
}
|
|
228
233
|
async function uploadResource(file) {
|
|
229
234
|
const sourceFileRefId = await uploadFileRef(file);
|
|
@@ -243,53 +248,63 @@ export function PdfTemplateEditor({ slug, locale: requestedLocale, onLocaleChang
|
|
|
243
248
|
else
|
|
244
249
|
onLocaleChange?.(next);
|
|
245
250
|
}
|
|
246
|
-
const used = validation?.used ?? [];
|
|
247
|
-
const missingRequired = validation?.missingRequired ?? [];
|
|
248
|
-
const layoutName = layout?.slug ?? DEFAULT_LAYOUT_SLUG;
|
|
249
|
-
const systemNames = visibleSystemPlaceholders(validation?.systemPlaceholders ?? []).filter((name) => !MERGED_SYSTEM_PLACEHOLDERS.has(name));
|
|
250
|
-
const systemRows = systemNames.map((name) => ({
|
|
251
|
-
name,
|
|
252
|
-
label: systemLabel(name),
|
|
253
|
-
status: used.includes(name)
|
|
254
|
-
? "used"
|
|
255
|
-
: layout !== null &&
|
|
256
|
-
(layout.html.includes(`data-variable="${name}"`) || layout.html.includes(`{{${name}}}`))
|
|
257
|
-
? "layout"
|
|
258
|
-
: "unused",
|
|
259
|
-
}));
|
|
260
|
-
const overrideCount = (overridesQuery.data?.activeOverrides ?? 0) + (overridesQuery.data?.draftOverrides ?? 0);
|
|
261
251
|
const canPublish = validation?.canPublish === true;
|
|
262
|
-
const
|
|
252
|
+
const layoutSlugShown = layout?.slug ?? DEFAULT_LAYOUT_SLUG;
|
|
253
|
+
const layoutName = layouts.rows.find((row) => row.slug === layoutSlugShown)?.title ?? layoutSlugShown;
|
|
254
|
+
const layoutResourceSrc = (key) => key === "logo" && brandingLogoFile !== "" ? fileUrl(brandingLogoFile) : undefined;
|
|
255
|
+
const placeholderSource = validation === undefined || validation === null
|
|
256
|
+
? null
|
|
257
|
+
: {
|
|
258
|
+
...validation,
|
|
259
|
+
systemPlaceholders: visibleSystemPlaceholders(validation.systemPlaceholders),
|
|
260
|
+
};
|
|
261
|
+
const layoutFills = (name) => layout !== null &&
|
|
262
|
+
(layout.html.includes(`data-variable="${name}"`) || layout.html.includes(`{{${name}}}`));
|
|
263
|
+
const overrideCount = (overridesQuery.data?.activeOverrides ?? 0) + (overridesQuery.data?.draftOverrides ?? 0);
|
|
263
264
|
const resourceKeys = Object.keys(parseJsonObject(detail.linkedResources));
|
|
264
|
-
const
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
265
|
+
const overviewRow = overview.rows.find((candidate) => candidate.slug === slug);
|
|
266
|
+
const title = overviewRow === undefined ? slug : templateTitle(t, overviewRow);
|
|
267
|
+
const systemVersion = overviewRow?.systemVersion ?? null;
|
|
268
|
+
const vocabulary = {
|
|
269
|
+
names: variableNames,
|
|
270
|
+
labels: variableLabels,
|
|
271
|
+
system: systemVariables,
|
|
272
|
+
};
|
|
273
|
+
const footer = validation === undefined || validation === null
|
|
274
|
+
? null
|
|
275
|
+
: pdfFooterMessage(t, validation, generated === null
|
|
276
|
+
? null
|
|
277
|
+
: {
|
|
278
|
+
pages: generated.result.pageCount,
|
|
279
|
+
sizeBytes: generated.result.sizeBytes,
|
|
280
|
+
durationMs: generated.result.durationMs,
|
|
281
|
+
});
|
|
282
|
+
const showOverrideLinks = showOrigin && detail.scope === "system";
|
|
283
|
+
const originText = showOverrideLinks
|
|
284
|
+
? overridesQuery.data?.totalTenants == null
|
|
285
|
+
? t("documentDesigner.originBar.noTotal", { count: overrideCount })
|
|
286
|
+
: t("documentDesigner.originBar.withTotal", {
|
|
287
|
+
count: overrideCount,
|
|
288
|
+
total: overridesQuery.data.totalTenants,
|
|
289
|
+
})
|
|
290
|
+
: detail.scope === "system"
|
|
291
|
+
? t("documentDesigner.mail.origin.system", { version: detail.version })
|
|
292
|
+
: systemVersion === null
|
|
293
|
+
? t("documentDesigner.mail.origin.tenantOnly")
|
|
294
|
+
: t("documentDesigner.mail.origin.overrides", { version: systemVersion });
|
|
295
|
+
const besideActiveSentence = draftBesideActiveSentence(t, detail);
|
|
296
|
+
const pdfUnavailableNote = !pdfAvailable && statusQuery.loading === false;
|
|
297
|
+
return (_jsxs(EditorFrame, { testId: "pdf-template-editor", children: [_jsx(EditorHeader, { title: title, stored: { status: detail.status, version: detail.version }, actions: {
|
|
298
|
+
busy: busy === "save",
|
|
299
|
+
canSave: busy === null && validation?.canSave !== false,
|
|
300
|
+
canPublish: busy === null && canPublish,
|
|
301
|
+
onSave: () => void save(),
|
|
302
|
+
onPublish: () => void publish(),
|
|
303
|
+
}, 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 === ""
|
|
304
|
+
? t("documentDesigner.pdfUnavailableNoReason")
|
|
305
|
+
: 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, 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, resourcePreviews: resourcePreviews, onEditorReady: insert.register("body") }) })) : (_jsx("div", { className: "min-h-0 flex-1 overflow-auto", children: generated !== null && generatedFresh ? (_jsx(PdfPreview, { pdfBase64: generated.result.pdfBase64 })) : (_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"
|
|
281
306
|
? t("documentDesigner.preview.loading")
|
|
282
|
-
: t("documentDesigner.preview.stale") }), _jsx(Button, { variant: "secondary", disabled: !pdfAvailable || busy !== null, onClick: () => void generatePdf(), children: t("documentDesigner.actions.generatePdf") })] })), _jsx(SettingsPanel, { meta: current.draft.meta, onMetaChange: (meta) => updateDraft({ ...current.draft, meta }),
|
|
283
|
-
version: detail.version,
|
|
284
|
-
status: detail.status,
|
|
285
|
-
modifiedAt: detail.modifiedAt,
|
|
286
|
-
} })] }), _jsxs("footer", { className: "flex flex-wrap items-start justify-between gap-3 border-t border-border pt-3", children: [_jsxs("ul", { className: "m-0 flex list-none flex-col gap-1 p-0 text-sm", "data-testid": "document-designer-validation", children: [missingRequired.length === 0 && validation !== undefined && validation !== null && (_jsx("li", { className: "text-status-ok", children: t("documentDesigner.validation.ok") })), missingRequired.map((name) => (_jsx("li", { className: "text-status-bad", children: t("documentDesigner.validation.missingRequired", {
|
|
287
|
-
name: variables[name]?.label ?? name,
|
|
288
|
-
}) }, name))), issueTexts.map((text) => (_jsx("li", { className: "text-status-bad", children: text }, text))), (validation?.warnings ?? []).map((warning) => (_jsx("li", { className: "text-status-warn", children: t(warning.i18nKey, { detail: warning.detail }) }, `${warning.code}:${warning.detail}`))), generated !== null && (_jsx("li", { className: "text-muted-foreground", children: t("documentDesigner.validation.lastProbe", {
|
|
289
|
-
pages: generated.result.pageCount,
|
|
290
|
-
size: formatSize(generated.result.sizeBytes),
|
|
291
|
-
seconds: (generated.result.durationMs / 1000).toFixed(1),
|
|
292
|
-
}) }))] }), _jsx("div", { className: "flex items-center gap-2", children: actions })] }), _jsx(Dialog, { open: pendingLocale !== null, onOpenChange: (open) => {
|
|
307
|
+
: 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) => {
|
|
293
308
|
if (!open)
|
|
294
309
|
setPendingLocale(null);
|
|
295
310
|
}, title: t("documentDesigner.localeDialog.title"), description: t("documentDesigner.localeDialog.description"), confirmLabel: t("documentDesigner.localeDialog.save"), cancelLabel: t("documentDesigner.localeDialog.cancel"), onConfirm: async () => {
|
package/dist/web/rail.d.ts
CHANGED
|
@@ -5,10 +5,21 @@ export declare function RailSection({ title, action, testId, children, }: {
|
|
|
5
5
|
readonly testId?: string;
|
|
6
6
|
readonly children: ReactNode;
|
|
7
7
|
}): ReactNode;
|
|
8
|
-
export declare function
|
|
8
|
+
export declare function RailField({ children }: {
|
|
9
|
+
readonly children: ReactNode;
|
|
10
|
+
}): ReactNode;
|
|
11
|
+
declare const TRAILING_TONE: {
|
|
12
|
+
readonly ok: "text-status-ok";
|
|
13
|
+
readonly bad: "text-status-bad";
|
|
14
|
+
readonly muted: "text-muted-foreground";
|
|
15
|
+
};
|
|
16
|
+
export declare function RailRow({ symbol, label, required, trailing, trailingTone, action, bad, testId, onPress, }: {
|
|
9
17
|
readonly symbol?: string;
|
|
10
18
|
readonly label: string;
|
|
19
|
+
readonly required?: boolean;
|
|
11
20
|
readonly trailing?: ReactNode;
|
|
21
|
+
readonly trailingTone?: keyof typeof TRAILING_TONE;
|
|
22
|
+
readonly action?: ReactNode;
|
|
12
23
|
readonly bad?: boolean;
|
|
13
24
|
readonly testId?: string;
|
|
14
25
|
readonly onPress?: () => void;
|
|
@@ -16,3 +27,4 @@ export declare function RailRow({ symbol, label, trailing, bad, testId, onPress,
|
|
|
16
27
|
export declare function RailHint({ children }: {
|
|
17
28
|
readonly children: ReactNode;
|
|
18
29
|
}): ReactNode;
|
|
30
|
+
export {};
|
package/dist/web/rail.js
CHANGED
|
@@ -1,14 +1,22 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
// @runtime client
|
|
3
|
-
import { usePrimitives } from "@cosmicdrift/kumiko-renderer";
|
|
4
3
|
import { cn } from "@cosmicdrift/kumiko-renderer-web";
|
|
5
4
|
export function RailSection({ title, action, testId, children, }) {
|
|
6
|
-
return (_jsxs("section", { className: "flex flex-col
|
|
5
|
+
return (_jsxs("section", { className: "flex flex-col border-b pb-1", "data-testid": testId, children: [_jsxs("div", { className: "flex items-center gap-2 px-3.5 pt-2.5 pb-1", children: [_jsx("span", { className: "text-xs font-medium uppercase tracking-wider text-muted-foreground", children: title }), _jsx("div", { className: "grow" }), action] }), children] }));
|
|
7
6
|
}
|
|
8
|
-
export function
|
|
9
|
-
|
|
10
|
-
|
|
7
|
+
export function RailField({ children }) {
|
|
8
|
+
return _jsx("div", { className: "flex flex-col gap-3 px-3.5 py-2", children: children });
|
|
9
|
+
}
|
|
10
|
+
const TRAILING_TONE = {
|
|
11
|
+
ok: "text-status-ok",
|
|
12
|
+
bad: "text-status-bad",
|
|
13
|
+
muted: "text-muted-foreground",
|
|
14
|
+
};
|
|
15
|
+
export function RailRow({ symbol, label, required, trailing, trailingTone = "muted", action, bad, testId, onPress, }) {
|
|
16
|
+
const content = (_jsxs(_Fragment, { children: [symbol !== undefined && (_jsx("span", { className: "inline-flex h-[18px] w-[22px] shrink-0 items-center justify-center rounded-sm bg-muted font-mono text-xs text-muted-foreground", children: symbol })), _jsxs("span", { className: cn("min-w-0 grow break-words text-sm", trailing !== undefined && "max-w-[65%] shrink-0", bad === true && "text-status-bad"), children: [label, required === true && _jsx("span", { className: "font-semibold text-status-bad", children: " *" })] }), trailing !== undefined && (_jsx("span", { className: cn("min-w-0 truncate text-right text-xs", bad === true ? "text-status-bad" : TRAILING_TONE[trailingTone]), title: typeof trailing === "string" ? trailing : undefined, children: trailing }))] }));
|
|
17
|
+
const rowClass = "flex min-h-10 min-w-0 grow items-center gap-2 px-3.5 text-left";
|
|
18
|
+
return (_jsxs("div", { className: cn("flex items-center border-b", bad === true && "bg-status-bad-surface"), "data-testid": testId, "data-bad": bad === true ? "true" : undefined, children: [onPress === undefined ? (_jsx("div", { className: rowClass, children: content })) : (_jsx("button", { type: "button", onClick: onPress, className: cn(rowClass, "hover:bg-muted"), ...(testId !== undefined && { "data-testid": `${testId}-insert` }), children: content })), action !== undefined && _jsx("div", { className: "shrink-0 pr-2", children: action })] }));
|
|
11
19
|
}
|
|
12
20
|
export function RailHint({ children }) {
|
|
13
|
-
return _jsx("p", { className: "px-
|
|
21
|
+
return _jsx("p", { className: "px-3.5 pt-1 pb-2 text-xs text-muted-foreground", children: children });
|
|
14
22
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function formatRelativeWhen(value: string, now?: number): string;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
const UNITS = [
|
|
2
|
+
["day", 86_400_000],
|
|
3
|
+
["hour", 3_600_000],
|
|
4
|
+
["minute", 60_000],
|
|
5
|
+
];
|
|
6
|
+
// Falls back to the raw value for an unparseable timestamp, like formatWhen.
|
|
7
|
+
export function formatRelativeWhen(value, now = Date.now()) {
|
|
8
|
+
const then = Date.parse(value);
|
|
9
|
+
if (Number.isNaN(then))
|
|
10
|
+
return value;
|
|
11
|
+
const formatter = new Intl.RelativeTimeFormat(undefined, { numeric: "auto" });
|
|
12
|
+
const elapsed = then - now;
|
|
13
|
+
for (const [unit, size] of UNITS) {
|
|
14
|
+
if (Math.abs(elapsed) >= size)
|
|
15
|
+
return formatter.format(Math.round(elapsed / size), unit);
|
|
16
|
+
}
|
|
17
|
+
return formatter.format(0, "minute");
|
|
18
|
+
}
|
|
@@ -1,17 +1,23 @@
|
|
|
1
1
|
import type { ReactNode } from "react";
|
|
2
|
+
import type { EditorVocabulary } from "./mail-edit-center.js";
|
|
3
|
+
import type { OverviewRowView } from "./overview-row.js";
|
|
2
4
|
import { type PdfMetaDraft } from "./pdf-meta.js";
|
|
3
|
-
import type
|
|
5
|
+
import { type TemplateStatus } from "./types.js";
|
|
6
|
+
import type { InsertTargets } from "./use-insert-targets.js";
|
|
4
7
|
export type VersionInfo = {
|
|
8
|
+
readonly id: string;
|
|
5
9
|
readonly version: number;
|
|
6
10
|
readonly status: TemplateStatus;
|
|
7
|
-
readonly modifiedAt: string | null;
|
|
8
11
|
};
|
|
9
12
|
export type SettingsPanelProps = {
|
|
10
13
|
readonly meta: PdfMetaDraft;
|
|
11
14
|
readonly onMetaChange: (next: PdfMetaDraft) => void;
|
|
12
|
-
|
|
13
|
-
readonly layoutSlugs: readonly string[];
|
|
15
|
+
readonly layouts: readonly OverviewRowView[];
|
|
14
16
|
readonly locale: string;
|
|
15
17
|
readonly version: VersionInfo;
|
|
18
|
+
readonly vocabulary: EditorVocabulary;
|
|
19
|
+
readonly insert: InsertTargets;
|
|
20
|
+
/** Changes when the draft is replaced from outside, so the single-line editors re-read it. */
|
|
21
|
+
readonly draftKey: string;
|
|
16
22
|
};
|
|
17
|
-
export declare function SettingsPanel({ meta, onMetaChange,
|
|
23
|
+
export declare function SettingsPanel({ meta, onMetaChange, layouts, locale, version, vocabulary, insert, draftKey, }: SettingsPanelProps): ReactNode;
|
|
@@ -1,25 +1,36 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
// @runtime client
|
|
3
|
+
import { PDF_ORIENTATIONS, PDF_PAGE_FORMATS, PlaceholderLineEditor, } from "@cosmicdrift/kumiko-bundled-features/template-resolver/web";
|
|
3
4
|
import { usePrimitives, useTranslation } from "@cosmicdrift/kumiko-renderer";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
5
|
+
import { languageName } from "./mail-origin-bar.js";
|
|
6
|
+
import { layoutOptionLabel } from "./mail-right-rail.js";
|
|
7
|
+
import { VersionsSection } from "./mail-versions.js";
|
|
8
|
+
import { MARGIN_SIDES } from "./pdf-meta.js";
|
|
9
|
+
import { RailField, RailHint, RailSection } from "./rail.js";
|
|
10
|
+
import { DEFAULT_LAYOUT_SLUG } from "./types.js";
|
|
6
11
|
const SIDE_LABEL_KEYS = {
|
|
7
12
|
top: "documentDesigner.settings.marginTop",
|
|
8
13
|
right: "documentDesigner.settings.marginRight",
|
|
9
14
|
bottom: "documentDesigner.settings.marginBottom",
|
|
10
15
|
left: "documentDesigner.settings.marginLeft",
|
|
11
16
|
};
|
|
12
|
-
export function SettingsPanel({ meta, onMetaChange,
|
|
17
|
+
export function SettingsPanel({ meta, onMetaChange, layouts, locale, version, vocabulary, insert, draftKey, }) {
|
|
13
18
|
const t = useTranslation();
|
|
14
|
-
const { Field, Input } = usePrimitives();
|
|
19
|
+
const { Field, Input, Text } = usePrimitives();
|
|
15
20
|
const patch = (changes) => onMetaChange({ ...meta, ...changes });
|
|
16
|
-
return (_jsxs("div", {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
21
|
+
return (_jsxs("div", { "data-testid": "document-designer-settings", children: [_jsx(RailSection, { title: t("documentDesigner.settings.page"), testId: "pdf-editor-document", children: _jsxs(RailField, { children: [_jsx(Field, { id: "dd-layout", label: t("documentDesigner.settings.layout"), children: _jsx(Input, { kind: "select", id: "dd-layout", name: "layoutSlug", value: meta.layoutSlug === "" ? DEFAULT_LAYOUT_SLUG : meta.layoutSlug, onChange: (layoutSlug) => patch({ layoutSlug }), display: "dropdown", options: layouts.map((layout) => ({
|
|
22
|
+
value: layout.slug,
|
|
23
|
+
label: layoutOptionLabel(t, layout),
|
|
24
|
+
})) }) }), _jsx(Field, { id: "dd-file-name", label: t("documentDesigner.settings.fileName"), children: _jsx(Input, { kind: "text", id: "dd-file-name", name: "fileName", value: meta.fileName, onChange: (fileName) => patch({ fileName }), placeholder: "rechnung-{{number}}.pdf" }) }), _jsxs("div", { className: "grid grid-cols-[minmax(0,1fr)_minmax(0,1.5fr)] gap-2", children: [_jsx(Field, { id: "dd-page-format", label: t("documentDesigner.settings.pageFormat"), children: _jsx(Input, { kind: "select", id: "dd-page-format", name: "pageFormat", value: meta.pageFormat, onChange: (pageFormat) => {
|
|
25
|
+
const next = PDF_PAGE_FORMATS.find((format) => format === pageFormat);
|
|
26
|
+
if (next !== undefined)
|
|
27
|
+
patch({ pageFormat: next });
|
|
28
|
+
}, display: "dropdown", options: PDF_PAGE_FORMATS.map((value) => ({ value, label: value })) }) }), _jsx(Field, { id: "dd-orientation", label: t("documentDesigner.settings.orientation"), children: _jsx(Input, { kind: "select", id: "dd-orientation", name: "orientation", value: meta.orientation, onChange: (orientation) => {
|
|
29
|
+
const next = PDF_ORIENTATIONS.find((candidate) => candidate === orientation);
|
|
30
|
+
if (next !== undefined)
|
|
31
|
+
patch({ orientation: next });
|
|
32
|
+
}, display: "dropdown", options: PDF_ORIENTATIONS.map((value) => ({
|
|
33
|
+
value,
|
|
34
|
+
label: t(`documentDesigner.settings.${value}`),
|
|
35
|
+
})) }) })] }), _jsxs("fieldset", { className: "m-0 flex min-w-0 flex-col gap-1 border-0 p-0", children: [_jsx("legend", { className: "p-0 text-sm font-medium", children: t("documentDesigner.settings.margins") }), _jsx("div", { className: "grid grid-cols-4 gap-1", children: MARGIN_SIDES.map((side) => (_jsx(Field, { id: `dd-margin-${side}`, label: t(SIDE_LABEL_KEYS[side]), hideLabel: true, children: _jsx(Input, { kind: "number", id: `dd-margin-${side}`, name: `margin-${side}`, value: meta.margins[side], onChange: (value) => patch({ margins: { ...meta.margins, [side]: value ?? "" } }), placeholder: "15" }) }, side))) })] }), _jsx(Field, { id: "dd-print-background", label: t("documentDesigner.settings.printBackground"), layout: "inline", children: _jsx(Input, { kind: "boolean", id: "dd-print-background", name: "printBackground", value: meta.printBackground, onChange: (printBackground) => patch({ printBackground }) }) }), _jsx(Field, { id: "dd-layout-header-footer", label: t("documentDesigner.settings.layoutHeaderFooter"), layout: "inline", children: _jsx(Input, { kind: "boolean", id: "dd-layout-header-footer", name: "layoutHeaderFooter", value: meta.layoutHeaderFooter, onChange: (layoutHeaderFooter) => patch({ layoutHeaderFooter }) }) })] }) }), _jsxs(RailSection, { title: t("documentDesigner.settings.metadata"), testId: "pdf-editor-metadata", children: [_jsxs(RailField, { children: [_jsx("div", { onFocusCapture: insert.markFocused("title"), "data-testid": "pdf-meta-title", children: _jsx(Field, { id: "dd-meta-title", label: t("documentDesigner.settings.metaTitle"), children: _jsx(PlaceholderLineEditor, { id: "dd-meta-title", value: meta.title, onChange: (title) => patch({ title }), variableLabels: vocabulary.labels, systemVariables: vocabulary.system, onEditorReady: insert.register("title"), "aria-label": t("documentDesigner.settings.metaTitle") }, `title:${draftKey}`) }) }), _jsx("div", { onFocusCapture: insert.markFocused("author"), "data-testid": "pdf-meta-author", children: _jsx(Field, { id: "dd-meta-author", label: t("documentDesigner.settings.metaAuthor"), children: _jsx(PlaceholderLineEditor, { id: "dd-meta-author", value: meta.author, onChange: (author) => patch({ author }), variableLabels: vocabulary.labels, systemVariables: vocabulary.system, onEditorReady: insert.register("author"), "aria-label": t("documentDesigner.settings.metaAuthor") }, `author:${draftKey}`) }) }), _jsxs("div", { className: "flex flex-col gap-1", "data-testid": "pdf-meta-language", children: [_jsx("span", { className: "text-sm font-medium", children: t("documentDesigner.settings.metaLanguage") }), _jsx(Text, { children: `${languageName(t, locale)} (${locale})` })] })] }), _jsx(RailHint, { children: t("documentDesigner.settings.metaLanguageHint") })] }), _jsx(RailSection, { title: t("documentDesigner.settings.storage"), testId: "pdf-editor-storage", children: _jsx(RailHint, { children: t("documentDesigner.settings.storageHint") }) }), _jsx(VersionsSection, { rowId: version.id, currentVersion: version.version, currentStatus: version.status })] }));
|
|
25
36
|
}
|
|
@@ -3,5 +3,12 @@ import type { OverviewRowView } from "./overview-row.js";
|
|
|
3
3
|
type Translate = (key: string, params?: Readonly<Record<string, unknown>>) => string;
|
|
4
4
|
export declare function statusTone(status: OverviewRowView["status"]): StatusBadgeTone;
|
|
5
5
|
export declare function statusLabel(t: Translate, status: OverviewRowView["status"], version: number): string;
|
|
6
|
+
export declare function draftBesideActiveSentence(t: Translate, row: {
|
|
7
|
+
readonly status: string;
|
|
8
|
+
readonly version: number;
|
|
9
|
+
readonly active?: {
|
|
10
|
+
readonly version: number;
|
|
11
|
+
} | null;
|
|
12
|
+
}): string | null;
|
|
6
13
|
export declare function asRowStatus(status: string): OverviewRowView["status"];
|
|
7
14
|
export {};
|
package/dist/web/status-label.js
CHANGED
|
@@ -13,6 +13,14 @@ export function statusLabel(t, status, version) {
|
|
|
13
13
|
? "documentDesigner.status.activeVersion"
|
|
14
14
|
: "documentDesigner.status.draftVersion", { version });
|
|
15
15
|
}
|
|
16
|
+
export function draftBesideActiveSentence(t, row) {
|
|
17
|
+
if (asRowStatus(row.status) !== "draft" || row.active == null)
|
|
18
|
+
return null;
|
|
19
|
+
return t("documentDesigner.origin.draftBesideActive", {
|
|
20
|
+
active: row.active.version,
|
|
21
|
+
draft: row.version,
|
|
22
|
+
});
|
|
23
|
+
}
|
|
16
24
|
export function asRowStatus(status) {
|
|
17
25
|
return status === "active" || status === "archived" ? status : "draft";
|
|
18
26
|
}
|
package/dist/web/types.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { FieldIssue } from "@cosmicdrift/kumiko-headless";
|
|
2
|
+
import type { TemplateDiff } from "./layout-types.js";
|
|
2
3
|
export type TemplateScope = "system" | "tenant";
|
|
3
4
|
export type TemplateStatus = "draft" | "active" | "archived";
|
|
4
5
|
export declare const DOCUMENT_PDF_KIND: "document-pdf";
|
|
@@ -31,12 +32,22 @@ export type TemplateDetail = {
|
|
|
31
32
|
readonly parentTemplateId: string | null;
|
|
32
33
|
readonly status: TemplateStatus;
|
|
33
34
|
readonly modifiedAt: string | null;
|
|
35
|
+
readonly active?: ServedVersionView | null;
|
|
36
|
+
};
|
|
37
|
+
/** The version the resolver serves while a draft is open beside it. */
|
|
38
|
+
export type ServedVersionView = {
|
|
39
|
+
readonly version: number;
|
|
40
|
+
readonly since: string;
|
|
34
41
|
};
|
|
35
42
|
export type VariableType = "text" | "link" | "money" | "date" | "list";
|
|
36
43
|
export type VariableInfo = {
|
|
37
44
|
readonly type: VariableType;
|
|
38
45
|
readonly label?: string;
|
|
39
46
|
readonly required?: boolean;
|
|
47
|
+
readonly item?: Readonly<Record<string, {
|
|
48
|
+
readonly type: VariableType;
|
|
49
|
+
readonly label?: string;
|
|
50
|
+
}>>;
|
|
40
51
|
};
|
|
41
52
|
export type ValidationWarning = {
|
|
42
53
|
readonly code: string;
|
|
@@ -82,10 +93,22 @@ export type PdfPreviewResult = {
|
|
|
82
93
|
readonly sizeBytes: number;
|
|
83
94
|
readonly durationMs: number;
|
|
84
95
|
};
|
|
96
|
+
export type TenantOverrideRow = {
|
|
97
|
+
readonly tenantId: string;
|
|
98
|
+
readonly tenantName: string;
|
|
99
|
+
readonly templateId: string;
|
|
100
|
+
readonly locale: string;
|
|
101
|
+
readonly status: "active" | "draft";
|
|
102
|
+
readonly version: number;
|
|
103
|
+
readonly changedAt: string;
|
|
104
|
+
readonly changedBy: string | null;
|
|
105
|
+
};
|
|
85
106
|
export type TenantOverrides = {
|
|
86
107
|
readonly activeOverrides: number;
|
|
87
108
|
readonly draftOverrides: number;
|
|
88
109
|
readonly totalTenants: number | null;
|
|
110
|
+
readonly rows: readonly TenantOverrideRow[];
|
|
111
|
+
readonly diff: TemplateDiff | null;
|
|
89
112
|
};
|
|
90
113
|
export type SaveResult = {
|
|
91
114
|
readonly id: string;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function useDebouncedValue<T>(value: T, delayMs: number): T;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { useEffect, useState } from "react";
|
|
2
|
+
// Unlike useDeferredValue this waits for a quiet period, so a burst of
|
|
3
|
+
// keystrokes triggers one server round-trip instead of one per keystroke.
|
|
4
|
+
export function useDebouncedValue(value, delayMs) {
|
|
5
|
+
const [debounced, setDebounced] = useState(value);
|
|
6
|
+
useEffect(() => {
|
|
7
|
+
const timer = setTimeout(() => setDebounced(value), delayMs);
|
|
8
|
+
return () => clearTimeout(timer);
|
|
9
|
+
}, [value, delayMs]);
|
|
10
|
+
return debounced;
|
|
11
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { DocumentEditorHandle } from "@cosmicdrift/kumiko-bundled-features/template-resolver/web";
|
|
2
|
-
export type InsertField = "subject" | "previewText" | "body";
|
|
2
|
+
export type InsertField = "subject" | "previewText" | "headerChip" | "footerNote" | "body" | "title" | "author";
|
|
3
3
|
export type InsertTargets = {
|
|
4
|
-
readonly register: (field: InsertField) => (handle: DocumentEditorHandle) => void;
|
|
4
|
+
readonly register: (field: InsertField) => (handle: DocumentEditorHandle | null) => void;
|
|
5
5
|
readonly markFocused: (field: InsertField) => () => void;
|
|
6
6
|
readonly insertPlaceholder: (name: string) => void;
|
|
7
7
|
readonly insertResource: (key: string) => void;
|
|
@@ -6,7 +6,10 @@ export function useInsertTargets() {
|
|
|
6
6
|
const lastFocused = useRef("body");
|
|
7
7
|
return {
|
|
8
8
|
register: (field) => (handle) => {
|
|
9
|
-
|
|
9
|
+
if (handle === null)
|
|
10
|
+
delete handles.current[field];
|
|
11
|
+
else
|
|
12
|
+
handles.current[field] = handle;
|
|
10
13
|
},
|
|
11
14
|
markFocused: (field) => () => {
|
|
12
15
|
lastFocused.current = field;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { MailDraft, TemplateRowView } from "./mail-types.js";
|
|
2
|
+
export type SaveOutcome = "saved" | "saved-not-published" | "failed";
|
|
2
3
|
export type MailPersistence = {
|
|
3
|
-
readonly save: (draft: MailDraft, publish: boolean) => Promise<
|
|
4
|
+
readonly save: (draft: MailDraft, publish: boolean) => Promise<SaveOutcome>;
|
|
4
5
|
readonly busy: boolean;
|
|
5
6
|
readonly error: string | null;
|
|
6
7
|
readonly clearError: () => void;
|
|
@@ -11,5 +12,4 @@ export declare function useMailPersistence(args: {
|
|
|
11
12
|
readonly title: string;
|
|
12
13
|
readonly isSystemAdmin: boolean;
|
|
13
14
|
readonly row: TemplateRowView | null;
|
|
14
|
-
readonly sender: unknown;
|
|
15
15
|
}): MailPersistence;
|
|
@@ -21,7 +21,7 @@ export function useMailPersistence(args) {
|
|
|
21
21
|
title: args.title,
|
|
22
22
|
content: draft.body,
|
|
23
23
|
contentFormat: "html",
|
|
24
|
-
meta: buildMeta(draft
|
|
24
|
+
meta: buildMeta(draft),
|
|
25
25
|
variableSchema: parseJsonRecord(args.row?.variableSchema),
|
|
26
26
|
};
|
|
27
27
|
const saved = args.isSystemAdmin
|
|
@@ -29,16 +29,16 @@ export function useMailPersistence(args) {
|
|
|
29
29
|
: await upsertTenant.mutate({ ...base, status: publish ? "active" : "draft" });
|
|
30
30
|
if (!saved.isSuccess) {
|
|
31
31
|
setError(errorTextWithFieldIssues(saved.error, t));
|
|
32
|
-
return
|
|
32
|
+
return "failed";
|
|
33
33
|
}
|
|
34
34
|
if (args.isSystemAdmin && publish) {
|
|
35
35
|
const published = await publishSystem.mutate({ id: saved.data.id });
|
|
36
36
|
if (!published.isSuccess) {
|
|
37
37
|
setError(errorTextWithFieldIssues(published.error, t));
|
|
38
|
-
return
|
|
38
|
+
return "saved-not-published";
|
|
39
39
|
}
|
|
40
40
|
}
|
|
41
|
-
return
|
|
41
|
+
return "saved";
|
|
42
42
|
};
|
|
43
43
|
return {
|
|
44
44
|
save,
|