@cosmicdrift/kumiko-document-designer 0.0.0-stage → 0.364.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/LICENSE +57 -0
- package/README.md +37 -2
- package/dist/constants.d.ts +27 -0
- package/dist/constants.js +37 -0
- package/dist/feature.d.ts +2 -0
- package/dist/feature.js +85 -0
- package/dist/i18n.d.ts +6 -0
- package/dist/i18n.js +764 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/list-definition.d.ts +6 -0
- package/dist/list-definition.js +109 -0
- package/dist/web/cells.d.ts +10 -0
- package/dist/web/cells.js +87 -0
- package/dist/web/client-plugin.d.ts +12 -0
- package/dist/web/client-plugin.js +43 -0
- package/dist/web/compose-layout.d.ts +6 -0
- package/dist/web/compose-layout.js +13 -0
- package/dist/web/editor-header.d.ts +37 -0
- package/dist/web/editor-header.js +43 -0
- package/dist/web/editor-shell.d.ts +40 -0
- package/dist/web/editor-shell.js +36 -0
- package/dist/web/errors.d.ts +6 -0
- package/dist/web/errors.js +24 -0
- package/dist/web/field-issue-text.d.ts +4 -0
- package/dist/web/field-issue-text.js +12 -0
- package/dist/web/file-name.d.ts +2 -0
- package/dist/web/file-name.js +17 -0
- package/dist/web/i18n.d.ts +2 -0
- package/dist/web/i18n.js +146 -0
- package/dist/web/index.d.ts +6 -0
- package/dist/web/index.js +7 -0
- package/dist/web/layout-editor-body.d.ts +14 -0
- package/dist/web/layout-editor-body.js +187 -0
- package/dist/web/layout-editor.d.ts +2 -0
- package/dist/web/layout-editor.js +43 -0
- package/dist/web/layout-html.d.ts +9 -0
- package/dist/web/layout-html.js +93 -0
- package/dist/web/layout-meta.d.ts +29 -0
- package/dist/web/layout-meta.js +32 -0
- package/dist/web/layout-rails.d.ts +28 -0
- package/dist/web/layout-rails.js +100 -0
- package/dist/web/layout-types.d.ts +66 -0
- package/dist/web/layout-types.js +1 -0
- package/dist/web/layout-users.d.ts +4 -0
- package/dist/web/layout-users.js +11 -0
- package/dist/web/mail-dialogs.d.ts +24 -0
- package/dist/web/mail-dialogs.js +15 -0
- package/dist/web/mail-draft.d.ts +19 -0
- package/dist/web/mail-draft.js +154 -0
- package/dist/web/mail-edit-center.d.ts +19 -0
- package/dist/web/mail-edit-center.js +12 -0
- package/dist/web/mail-editor-body.d.ts +17 -0
- package/dist/web/mail-editor-body.js +209 -0
- package/dist/web/mail-editor.d.ts +2 -0
- package/dist/web/mail-editor.js +68 -0
- package/dist/web/mail-footer.d.ts +10 -0
- package/dist/web/mail-footer.js +43 -0
- package/dist/web/mail-left-rail.d.ts +23 -0
- package/dist/web/mail-left-rail.js +56 -0
- 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 +15 -0
- package/dist/web/mail-origin-bar.js +60 -0
- package/dist/web/mail-pdf-link-modal.d.ts +11 -0
- package/dist/web/mail-pdf-link-modal.js +54 -0
- package/dist/web/mail-preview-side.d.ts +19 -0
- package/dist/web/mail-preview-side.js +57 -0
- package/dist/web/mail-preview.d.ts +24 -0
- package/dist/web/mail-preview.js +54 -0
- package/dist/web/mail-right-rail.d.ts +19 -0
- package/dist/web/mail-right-rail.js +57 -0
- package/dist/web/mail-types.d.ts +117 -0
- package/dist/web/mail-types.js +1 -0
- package/dist/web/mail-upload-modal.d.ts +16 -0
- package/dist/web/mail-upload-modal.js +56 -0
- package/dist/web/mail-versions.d.ts +6 -0
- package/dist/web/mail-versions.js +43 -0
- package/dist/web/origin-bar.d.ts +22 -0
- package/dist/web/origin-bar.js +104 -0
- package/dist/web/overrides-drawer.d.ts +10 -0
- package/dist/web/overrides-drawer.js +95 -0
- package/dist/web/overview-row.d.ts +23 -0
- package/dist/web/overview-row.js +48 -0
- package/dist/web/page-canvas.d.ts +19 -0
- package/dist/web/page-canvas.js +48 -0
- 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 +21 -0
- package/dist/web/pdf-meta.js +95 -0
- package/dist/web/pdf-preview.d.ts +4 -0
- package/dist/web/pdf-preview.js +112 -0
- package/dist/web/pdf-template-editor.d.ts +8 -0
- package/dist/web/pdf-template-editor.js +340 -0
- package/dist/web/preview-record-picker.d.ts +12 -0
- package/dist/web/preview-record-picker.js +7 -0
- package/dist/web/rail.d.ts +30 -0
- package/dist/web/rail.js +22 -0
- package/dist/web/relative-time.d.ts +1 -0
- package/dist/web/relative-time.js +18 -0
- package/dist/web/settings-panel.d.ts +23 -0
- package/dist/web/settings-panel.js +36 -0
- package/dist/web/status-label.d.ts +14 -0
- package/dist/web/status-label.js +26 -0
- package/dist/web/template-rows.d.ts +9 -0
- package/dist/web/template-rows.js +34 -0
- package/dist/web/types.d.ts +126 -0
- package/dist/web/types.js +3 -0
- package/dist/web/use-insert-targets.d.ts +9 -0
- package/dist/web/use-insert-targets.js +17 -0
- package/dist/web/use-mail-persistence.d.ts +14 -0
- package/dist/web/use-mail-persistence.js +49 -0
- package/dist/web/use-overview-rows.d.ts +7 -0
- package/dist/web/use-overview-rows.js +19 -0
- package/dist/web/use-own-address.d.ts +5 -0
- package/dist/web/use-own-address.js +14 -0
- package/dist/web/use-settled-value.d.ts +2 -0
- package/dist/web/use-settled-value.js +18 -0
- package/dist/web/use-test-mail.d.ts +15 -0
- package/dist/web/use-test-mail.js +27 -0
- package/dist/web/used-by-label.d.ts +9 -0
- package/dist/web/used-by-label.js +32 -0
- package/package.json +63 -4
- package/src/changes.json +44 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
export { DEFAULT_TEMPLATE_LOCALES, DOCUMENT_DESIGNER_FEATURE, LAYOUT_EDITOR_SCREEN_ID, MAIL_EDITOR_SCREEN_ID, PDF_TEMPLATE_EDITOR_SCREEN_ID, PLATFORM_TEMPLATES_NAV_ID, PLATFORM_TEMPLATES_SCREEN_ID, TEMPLATE_EDITOR_SCREENS, TEMPLATES_NAV_ID, TEMPLATES_SCREEN_ID, type TemplateEditorKind, } from "./constants.js";
|
|
2
|
+
export { createDocumentDesignerFeature } from "./feature.js";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
export { DEFAULT_TEMPLATE_LOCALES, DOCUMENT_DESIGNER_FEATURE, LAYOUT_EDITOR_SCREEN_ID, MAIL_EDITOR_SCREEN_ID, PDF_TEMPLATE_EDITOR_SCREEN_ID, PLATFORM_TEMPLATES_NAV_ID, PLATFORM_TEMPLATES_SCREEN_ID, TEMPLATE_EDITOR_SCREENS, TEMPLATES_NAV_ID, TEMPLATES_SCREEN_ID, } from "./constants.js";
|
|
2
|
+
export { createDocumentDesignerFeature } from "./feature.js";
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { type ListColumnSpec, type ListFacetSpec, type RowAction } from "@cosmicdrift/kumiko-framework/engine";
|
|
2
|
+
export declare function templateColumns(options: {
|
|
3
|
+
readonly platform: boolean;
|
|
4
|
+
}): ListColumnSpec[];
|
|
5
|
+
export declare const TEMPLATE_FACETS: readonly ListFacetSpec[];
|
|
6
|
+
export declare function templateRowActions(): RowAction[];
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { i18nKey, } from "@cosmicdrift/kumiko-framework/engine";
|
|
2
|
+
import { DocumentDesignerCells, TEMPLATE_EDITOR_SCREENS, } from "./constants.js";
|
|
3
|
+
function cell(component) {
|
|
4
|
+
return { react: { __component: component } };
|
|
5
|
+
}
|
|
6
|
+
const BASE_COLUMNS = [
|
|
7
|
+
{
|
|
8
|
+
field: "title",
|
|
9
|
+
label: i18nKey("documentDesigner.col.template"),
|
|
10
|
+
renderer: cell(DocumentDesignerCells.title),
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
field: "kindGroup",
|
|
14
|
+
label: i18nKey("documentDesigner.col.kind"),
|
|
15
|
+
renderer: cell(DocumentDesignerCells.kind),
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
field: "origin",
|
|
19
|
+
label: i18nKey("documentDesigner.col.origin"),
|
|
20
|
+
renderer: cell(DocumentDesignerCells.origin),
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
field: "locales",
|
|
24
|
+
label: i18nKey("documentDesigner.col.locales"),
|
|
25
|
+
hideOnNarrow: true,
|
|
26
|
+
renderer: cell(DocumentDesignerCells.locales),
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
field: "status",
|
|
30
|
+
label: i18nKey("documentDesigner.col.status"),
|
|
31
|
+
renderer: cell(DocumentDesignerCells.status),
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
field: "usedBy",
|
|
35
|
+
label: i18nKey("documentDesigner.col.usedBy"),
|
|
36
|
+
hideOnNarrow: true,
|
|
37
|
+
renderer: cell(DocumentDesignerCells.usedBy),
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
field: "modifiedAt",
|
|
41
|
+
label: i18nKey("documentDesigner.col.modifiedAt"),
|
|
42
|
+
hideOnNarrow: true,
|
|
43
|
+
renderer: cell(DocumentDesignerCells.modifiedAt),
|
|
44
|
+
},
|
|
45
|
+
];
|
|
46
|
+
const OVERRIDDEN_BY_COLUMN = {
|
|
47
|
+
field: "overriddenBy",
|
|
48
|
+
label: i18nKey("documentDesigner.col.overriddenBy"),
|
|
49
|
+
virtual: true,
|
|
50
|
+
hideOnNarrow: true,
|
|
51
|
+
renderer: cell(DocumentDesignerCells.overriddenBy),
|
|
52
|
+
};
|
|
53
|
+
export function templateColumns(options) {
|
|
54
|
+
return options.platform ? [...BASE_COLUMNS, OVERRIDDEN_BY_COLUMN] : [...BASE_COLUMNS];
|
|
55
|
+
}
|
|
56
|
+
export const TEMPLATE_FACETS = [
|
|
57
|
+
{
|
|
58
|
+
field: "kindGroup",
|
|
59
|
+
type: "select",
|
|
60
|
+
label: i18nKey("documentDesigner.facet.kind"),
|
|
61
|
+
options: [
|
|
62
|
+
{ value: "mail", label: i18nKey("documentDesigner.kind.mail") },
|
|
63
|
+
{ value: "pdf", label: i18nKey("documentDesigner.kind.pdf") },
|
|
64
|
+
{ value: "layout", label: i18nKey("documentDesigner.kind.layout") },
|
|
65
|
+
],
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
field: "origin",
|
|
69
|
+
type: "select",
|
|
70
|
+
label: i18nKey("documentDesigner.facet.origin"),
|
|
71
|
+
options: [
|
|
72
|
+
{ value: "system", label: i18nKey("documentDesigner.source.system") },
|
|
73
|
+
{ value: "tenant", label: i18nKey("documentDesigner.source.tenant") },
|
|
74
|
+
],
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
field: "status",
|
|
78
|
+
type: "select",
|
|
79
|
+
label: i18nKey("documentDesigner.facet.status"),
|
|
80
|
+
options: [
|
|
81
|
+
{ value: "active", label: i18nKey("documentDesigner.status.active") },
|
|
82
|
+
{ value: "draft", label: i18nKey("documentDesigner.status.draft") },
|
|
83
|
+
{ value: "archived", label: i18nKey("documentDesigner.status.archived") },
|
|
84
|
+
],
|
|
85
|
+
},
|
|
86
|
+
];
|
|
87
|
+
const EDITABLE_KINDS = [
|
|
88
|
+
"mail",
|
|
89
|
+
"document-pdf",
|
|
90
|
+
"mail-layout",
|
|
91
|
+
"pdf-layout",
|
|
92
|
+
];
|
|
93
|
+
export function templateRowActions() {
|
|
94
|
+
return EDITABLE_KINDS.flatMap((kind) => {
|
|
95
|
+
const screen = TEMPLATE_EDITOR_SCREENS[kind];
|
|
96
|
+
if (screen === undefined)
|
|
97
|
+
return [];
|
|
98
|
+
return [
|
|
99
|
+
{
|
|
100
|
+
kind: "navigate",
|
|
101
|
+
id: `edit-${kind}`,
|
|
102
|
+
label: i18nKey("documentDesigner.action.edit"),
|
|
103
|
+
screen,
|
|
104
|
+
params: { pick: ["slug", "kind"] },
|
|
105
|
+
visible: { field: "kind", eq: kind },
|
|
106
|
+
},
|
|
107
|
+
];
|
|
108
|
+
});
|
|
109
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type ColumnRendererComponent, type ColumnRendererProps } from "@cosmicdrift/kumiko-renderer";
|
|
2
|
+
import type { ReactNode } from "react";
|
|
3
|
+
export declare function TitleCell({ row }: ColumnRendererProps): ReactNode;
|
|
4
|
+
export declare function KindCell({ row }: ColumnRendererProps): ReactNode;
|
|
5
|
+
export declare function OriginCell({ row }: ColumnRendererProps): ReactNode;
|
|
6
|
+
export declare function createLocalesCell(appLocales: readonly string[]): ColumnRendererComponent;
|
|
7
|
+
export declare function StatusCell({ row }: ColumnRendererProps): ReactNode;
|
|
8
|
+
export declare function UsedByCell({ row }: ColumnRendererProps): ReactNode;
|
|
9
|
+
export declare function ModifiedCell({ row }: ColumnRendererProps): ReactNode;
|
|
10
|
+
export declare function OverriddenByCell({ row }: ColumnRendererProps): ReactNode;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
// @runtime client
|
|
3
|
+
import { TemplateResolverQueries } from "@cosmicdrift/kumiko-bundled-features/template-resolver/web";
|
|
4
|
+
import { toInstant } from "@cosmicdrift/kumiko-headless";
|
|
5
|
+
import { formatWhen, useQuery, useTranslation, } from "@cosmicdrift/kumiko-renderer";
|
|
6
|
+
import { StatusBadge } from "@cosmicdrift/kumiko-renderer-web";
|
|
7
|
+
import { parseOverviewRow } from "./overview-row.js";
|
|
8
|
+
import { statusLabel, statusTone } from "./status-label.js";
|
|
9
|
+
import { usedByLabel } from "./used-by-label.js";
|
|
10
|
+
const EMPTY_MARK = "–";
|
|
11
|
+
function withRow(row, render) {
|
|
12
|
+
const parsed = parseOverviewRow(row);
|
|
13
|
+
return parsed === null ? EMPTY_MARK : render(parsed);
|
|
14
|
+
}
|
|
15
|
+
export function TitleCell({ row }) {
|
|
16
|
+
return withRow(row, (r) => (_jsxs("div", { children: [_jsx("div", { className: "font-medium", children: r.title }), _jsx("span", { className: "block max-w-40 truncate font-mono text-xs text-muted-foreground", title: r.slug, children: r.slug })] })));
|
|
17
|
+
}
|
|
18
|
+
const KIND_KEY = {
|
|
19
|
+
mail: "documentDesigner.kind.mail",
|
|
20
|
+
"document-pdf": "documentDesigner.kind.pdf",
|
|
21
|
+
"mail-layout": "documentDesigner.kind.layout",
|
|
22
|
+
"pdf-layout": "documentDesigner.kind.layout",
|
|
23
|
+
};
|
|
24
|
+
export function KindCell({ row }) {
|
|
25
|
+
const t = useTranslation();
|
|
26
|
+
return withRow(row, (r) => t(KIND_KEY[r.kind]));
|
|
27
|
+
}
|
|
28
|
+
export function OriginCell({ row }) {
|
|
29
|
+
const t = useTranslation();
|
|
30
|
+
return withRow(row, (r) => (_jsx(StatusBadge, { tone: r.origin === "tenant" ? "accent" : "muted", testId: "document-designer-origin", children: t(r.origin === "tenant" ? "documentDesigner.source.tenant" : "documentDesigner.source.system") })));
|
|
31
|
+
}
|
|
32
|
+
export function createLocalesCell(appLocales) {
|
|
33
|
+
return function LocalesCell({ row }) {
|
|
34
|
+
return withRow(row, (r) => {
|
|
35
|
+
if (r.kind === "mail-layout" || r.kind === "pdf-layout")
|
|
36
|
+
return EMPTY_MARK;
|
|
37
|
+
const present = new Set(r.locales);
|
|
38
|
+
const shown = [...appLocales, ...r.locales.filter((l) => !appLocales.includes(l))];
|
|
39
|
+
return (_jsx("span", { className: "inline-flex gap-1", children: shown.map((locale) => (_jsx("span", { "data-testid": `document-designer-locale-${locale}`, "data-present": present.has(locale), className: present.has(locale)
|
|
40
|
+
? "rounded border border-transparent bg-muted px-1.5 text-xs font-medium uppercase"
|
|
41
|
+
: "rounded border border-dashed px-1.5 text-xs uppercase text-muted-foreground", children: locale }, locale))) }));
|
|
42
|
+
});
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
export function StatusCell({ row }) {
|
|
46
|
+
const t = useTranslation();
|
|
47
|
+
return withRow(row, (r) => (_jsxs("span", { className: "flex flex-col items-start gap-1", children: [_jsx(StatusBadge, { tone: statusTone(r.status), testId: "document-designer-status", children: statusLabel(t, r.status, r.version) }), r.status === "active" && r.draftVersion !== null && (_jsx(StatusBadge, { tone: statusTone("draft"), testId: "document-designer-draft-status", children: statusLabel(t, "draft", r.draftVersion) }))] })));
|
|
48
|
+
}
|
|
49
|
+
export function UsedByCell({ row }) {
|
|
50
|
+
const t = useTranslation();
|
|
51
|
+
return withRow(row, (r) => {
|
|
52
|
+
const label = usedByLabel(t, r);
|
|
53
|
+
return (_jsx("span", { className: "block max-w-28 truncate", title: label, children: label }));
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
// Date only keeps the column narrow enough for the overview to fit without
|
|
57
|
+
// horizontal scroll; the full timestamp stays reachable via the tooltip.
|
|
58
|
+
function formatModifiedDate(value) {
|
|
59
|
+
try {
|
|
60
|
+
return toInstant(value).toLocaleString(undefined, { dateStyle: "medium" });
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
return value;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
export function ModifiedCell({ row }) {
|
|
67
|
+
return withRow(row, (r) => r.modifiedAt === "" ? (EMPTY_MARK) : (_jsx("span", { className: "whitespace-nowrap", title: formatWhen(r.modifiedAt), children: formatModifiedDate(r.modifiedAt) })));
|
|
68
|
+
}
|
|
69
|
+
function OverriddenByCount({ slug, kind, }) {
|
|
70
|
+
const t = useTranslation();
|
|
71
|
+
const { data } = useQuery(TemplateResolverQueries.tenantOverrides, {
|
|
72
|
+
slug,
|
|
73
|
+
kind,
|
|
74
|
+
});
|
|
75
|
+
if (data === null)
|
|
76
|
+
return EMPTY_MARK;
|
|
77
|
+
const label = data.totalTenants === null
|
|
78
|
+
? t("documentDesigner.overriddenBy.tenants", { active: data.activeOverrides })
|
|
79
|
+
: t("documentDesigner.overriddenBy.ofTotal", {
|
|
80
|
+
active: data.activeOverrides,
|
|
81
|
+
total: data.totalTenants,
|
|
82
|
+
});
|
|
83
|
+
return _jsx("span", { className: "whitespace-nowrap", children: label });
|
|
84
|
+
}
|
|
85
|
+
export function OverriddenByCell({ row }) {
|
|
86
|
+
return withRow(row, (r) => r.origin === "system" ? _jsx(OverriddenByCount, { slug: r.slug, kind: r.kind }) : EMPTY_MARK);
|
|
87
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { ClientFeatureDefinition } from "@cosmicdrift/kumiko-renderer-web";
|
|
2
|
+
import type { ReactNode } from "react";
|
|
3
|
+
/**
|
|
4
|
+
* Custom screen body: the template comes from the URL (`?slug=...&locale=...`),
|
|
5
|
+
* because custom screens receive no props.
|
|
6
|
+
*/
|
|
7
|
+
export declare function PdfTemplateEditorScreen(): ReactNode;
|
|
8
|
+
export type DocumentDesignerClientOptions = {
|
|
9
|
+
/** Locales shown as boxes in the overview's languages column; templates in other locales are appended. */
|
|
10
|
+
readonly appLocales?: readonly string[];
|
|
11
|
+
};
|
|
12
|
+
export declare function documentDesignerClient(options?: DocumentDesignerClientOptions): ClientFeatureDefinition;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
// @runtime client
|
|
3
|
+
import { textBlocksClient } from "@cosmicdrift/kumiko-bundled-features/template-resolver/web";
|
|
4
|
+
import { mergeTranslations, translationsByLocaleFromKeys, useNav, } from "@cosmicdrift/kumiko-renderer";
|
|
5
|
+
import { DEFAULT_TEMPLATE_LOCALES, DOCUMENT_DESIGNER_FEATURE, DocumentDesignerCells, LAYOUT_EDITOR_SCREEN_ID, MAIL_EDITOR_SCREEN_ID, PDF_TEMPLATE_EDITOR_SCREEN_ID, } from "../constants.js";
|
|
6
|
+
import { DOCUMENT_DESIGNER_I18N } from "../i18n.js";
|
|
7
|
+
import { createLocalesCell, KindCell, ModifiedCell, OriginCell, OverriddenByCell, StatusCell, TitleCell, UsedByCell, } from "./cells.js";
|
|
8
|
+
import { documentDesignerTranslations } from "./i18n.js";
|
|
9
|
+
import { LayoutEditor } from "./layout-editor.js";
|
|
10
|
+
import { createMailEditor } from "./mail-editor.js";
|
|
11
|
+
import { PdfTemplateEditor } from "./pdf-template-editor.js";
|
|
12
|
+
/**
|
|
13
|
+
* Custom screen body: the template comes from the URL (`?slug=...&locale=...`),
|
|
14
|
+
* because custom screens receive no props.
|
|
15
|
+
*/
|
|
16
|
+
export function PdfTemplateEditorScreen() {
|
|
17
|
+
const { searchParams, setSearchParams } = useNav();
|
|
18
|
+
const slug = searchParams["slug"];
|
|
19
|
+
if (slug === undefined || slug === "")
|
|
20
|
+
return null;
|
|
21
|
+
return (_jsx(PdfTemplateEditor, { slug: slug, ...(searchParams["locale"] !== undefined && { locale: searchParams["locale"] }), onLocaleChange: (locale) => setSearchParams({ locale }) }));
|
|
22
|
+
}
|
|
23
|
+
export function documentDesignerClient(options = {}) {
|
|
24
|
+
return {
|
|
25
|
+
name: DOCUMENT_DESIGNER_FEATURE,
|
|
26
|
+
columnRenderers: {
|
|
27
|
+
[DocumentDesignerCells.title]: TitleCell,
|
|
28
|
+
[DocumentDesignerCells.kind]: KindCell,
|
|
29
|
+
[DocumentDesignerCells.origin]: OriginCell,
|
|
30
|
+
[DocumentDesignerCells.locales]: createLocalesCell(options.appLocales ?? DEFAULT_TEMPLATE_LOCALES),
|
|
31
|
+
[DocumentDesignerCells.status]: StatusCell,
|
|
32
|
+
[DocumentDesignerCells.usedBy]: UsedByCell,
|
|
33
|
+
[DocumentDesignerCells.modifiedAt]: ModifiedCell,
|
|
34
|
+
[DocumentDesignerCells.overriddenBy]: OverriddenByCell,
|
|
35
|
+
},
|
|
36
|
+
components: {
|
|
37
|
+
[LAYOUT_EDITOR_SCREEN_ID]: LayoutEditor,
|
|
38
|
+
[PDF_TEMPLATE_EDITOR_SCREEN_ID]: PdfTemplateEditorScreen,
|
|
39
|
+
[MAIL_EDITOR_SCREEN_ID]: createMailEditor(options.appLocales ?? DEFAULT_TEMPLATE_LOCALES),
|
|
40
|
+
},
|
|
41
|
+
translations: mergeTranslations(mergeTranslations(textBlocksClient().translations ?? {}, documentDesignerTranslations), translationsByLocaleFromKeys(DOCUMENT_DESIGNER_I18N)),
|
|
42
|
+
};
|
|
43
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// An untouched rich-text editor reports an empty paragraph rather than "".
|
|
2
|
+
const EMPTY_REGION = /^(\s|<p>(\s|<br\s*\/?>)*<\/p>)*$/;
|
|
3
|
+
// Same top-level div contract the server reads with readLayoutRegions: empty regions are
|
|
4
|
+
// left out, the content slot is always present.
|
|
5
|
+
export function composeLayoutHtml(regions) {
|
|
6
|
+
const region = (name, regionHtml) => EMPTY_REGION.test(regionHtml) ? "" : `<div data-${name}>${regionHtml}</div>`;
|
|
7
|
+
return [
|
|
8
|
+
region("header", regions.header),
|
|
9
|
+
"<div data-content></div>",
|
|
10
|
+
region("signature", regions.signature),
|
|
11
|
+
region("footer", regions.footer),
|
|
12
|
+
].join("");
|
|
13
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
export type EditorMode = "edit" | "preview";
|
|
3
|
+
/** "override" is a tenant row that replaces a system version; "tenant" has no system counterpart. */
|
|
4
|
+
export type EditorOrigin = "system" | "override" | "tenant";
|
|
5
|
+
export declare function originOf(isSystemRow: boolean, hasSystemVersion: boolean): EditorOrigin;
|
|
6
|
+
export declare function OriginBadge({ origin }: {
|
|
7
|
+
readonly origin: EditorOrigin;
|
|
8
|
+
}): ReactNode;
|
|
9
|
+
export declare function EditorModeSwitch({ testIdPrefix, mode, onMode, }: {
|
|
10
|
+
readonly testIdPrefix: string;
|
|
11
|
+
readonly mode: EditorMode;
|
|
12
|
+
readonly onMode: (mode: EditorMode) => void;
|
|
13
|
+
}): ReactNode;
|
|
14
|
+
export declare function EditorLocaleSwitch({ testIdPrefix, locales, locale, onLocale, }: {
|
|
15
|
+
readonly testIdPrefix: string;
|
|
16
|
+
readonly locales: readonly string[];
|
|
17
|
+
readonly locale: string;
|
|
18
|
+
readonly onLocale: (locale: string) => void;
|
|
19
|
+
}): ReactNode;
|
|
20
|
+
export type StoredVersion = {
|
|
21
|
+
readonly status: string;
|
|
22
|
+
readonly version: number;
|
|
23
|
+
};
|
|
24
|
+
export type EditorSaveActions = {
|
|
25
|
+
readonly busy: boolean;
|
|
26
|
+
readonly canSave: boolean;
|
|
27
|
+
readonly canPublish: boolean;
|
|
28
|
+
readonly onSave: () => void;
|
|
29
|
+
readonly onPublish: () => void;
|
|
30
|
+
};
|
|
31
|
+
export declare function EditorHeader({ title, stored, actions, children, }: {
|
|
32
|
+
readonly title: string;
|
|
33
|
+
readonly stored: StoredVersion | null;
|
|
34
|
+
readonly actions: EditorSaveActions;
|
|
35
|
+
/** View controls left of the save actions (mode or kind switch). */
|
|
36
|
+
readonly children: ReactNode;
|
|
37
|
+
}): ReactNode;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
// @runtime client
|
|
3
|
+
import { usePrimitives, useTranslation } from "@cosmicdrift/kumiko-renderer";
|
|
4
|
+
import { ModeSwitch, StatusBadge } from "@cosmicdrift/kumiko-renderer-web";
|
|
5
|
+
import { asRowStatus, statusLabel, statusTone } from "./status-label.js";
|
|
6
|
+
const ORIGIN_LABEL = {
|
|
7
|
+
system: "documentDesigner.origin.system",
|
|
8
|
+
override: "documentDesigner.origin.override",
|
|
9
|
+
tenant: "documentDesigner.source.tenant",
|
|
10
|
+
};
|
|
11
|
+
export function originOf(isSystemRow, hasSystemVersion) {
|
|
12
|
+
return isSystemRow ? "system" : hasSystemVersion ? "override" : "tenant";
|
|
13
|
+
}
|
|
14
|
+
export function OriginBadge({ origin }) {
|
|
15
|
+
const t = useTranslation();
|
|
16
|
+
return (_jsx(StatusBadge, { tone: origin === "system" ? "muted" : "accent", testId: "document-designer-editor-origin", children: t(ORIGIN_LABEL[origin]) }));
|
|
17
|
+
}
|
|
18
|
+
export function EditorModeSwitch({ testIdPrefix, mode, onMode, }) {
|
|
19
|
+
const t = useTranslation();
|
|
20
|
+
return (_jsx(ModeSwitch, { value: mode, onChange: onMode, ariaLabel: t("documentDesigner.mail.header.modeSwitch"), testId: `${testIdPrefix}-mode-switch`, options: [
|
|
21
|
+
{ value: "edit", label: t("documentDesigner.mail.header.edit") },
|
|
22
|
+
{ value: "preview", label: t("documentDesigner.mail.header.preview") },
|
|
23
|
+
] }));
|
|
24
|
+
}
|
|
25
|
+
export function EditorLocaleSwitch({ testIdPrefix, locales, locale, onLocale, }) {
|
|
26
|
+
const t = useTranslation();
|
|
27
|
+
return (_jsx(ModeSwitch, { value: locale, onChange: onLocale, ariaLabel: t("documentDesigner.mail.header.localeSwitch"), testId: `${testIdPrefix}-locale-switch`, options: locales.map((code) => ({ value: code, label: code })) }));
|
|
28
|
+
}
|
|
29
|
+
function StoredStatus({ stored }) {
|
|
30
|
+
const t = useTranslation();
|
|
31
|
+
const status = asRowStatus(stored.status);
|
|
32
|
+
return (_jsx(StatusBadge, { tone: statusTone(status), testId: "document-designer-editor-status", children: statusLabel(t, status, stored.version) }));
|
|
33
|
+
}
|
|
34
|
+
// Title, status and the save/publish actions live in the shell header. The origin badge, the
|
|
35
|
+
// locale switch and the secondary action sit in the info bar below: at 1280px next to the
|
|
36
|
+
// sidebar, assistant and theme toggle the header has no room for them and the title would clip.
|
|
37
|
+
export function EditorHeader({ title, stored, actions, children, }) {
|
|
38
|
+
const t = useTranslation();
|
|
39
|
+
const { Button, PageHeader } = usePrimitives();
|
|
40
|
+
if (PageHeader === undefined)
|
|
41
|
+
return null;
|
|
42
|
+
return (_jsx(PageHeader, { title: title, status: stored !== null && _jsx(StoredStatus, { stored: stored }), actions: _jsxs("span", { className: "flex items-center gap-2 [&_button]:px-2.5 [&_button]:text-sm", children: [children, _jsx(Button, { variant: "secondary", size: "sm", disabled: !actions.canSave, loading: actions.busy, onClick: actions.onSave, testId: "document-designer-header-save", children: t("documentDesigner.editor.save") }), _jsx(Button, { size: "sm", disabled: !actions.canPublish, onClick: actions.onPublish, testId: "document-designer-header-publish", children: t("documentDesigner.editor.publish") })] }) }));
|
|
43
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import { type EditorOrigin } from "./editor-header.js";
|
|
3
|
+
export type InfoBarTone = "active" | "muted" | "warn";
|
|
4
|
+
export type FooterTone = "bad" | "warn" | "ok";
|
|
5
|
+
export declare function EditorFrame({ testId, children, }: {
|
|
6
|
+
readonly testId: string;
|
|
7
|
+
readonly children: ReactNode;
|
|
8
|
+
}): ReactNode;
|
|
9
|
+
export declare function EditorInfoBar({ tone, testId, origin, controls, links, overlays, children, }: {
|
|
10
|
+
readonly tone: InfoBarTone;
|
|
11
|
+
readonly testId: string;
|
|
12
|
+
readonly origin?: EditorOrigin | null;
|
|
13
|
+
/** Right side, before the links: locale switch and the editor's secondary action. */
|
|
14
|
+
readonly controls?: ReactNode;
|
|
15
|
+
readonly links?: ReactNode;
|
|
16
|
+
readonly overlays?: ReactNode;
|
|
17
|
+
readonly children: ReactNode;
|
|
18
|
+
}): ReactNode;
|
|
19
|
+
export declare function EditorBody({ left, center, right, testId, }: {
|
|
20
|
+
readonly left?: ReactNode;
|
|
21
|
+
readonly center: ReactNode;
|
|
22
|
+
readonly right: ReactNode;
|
|
23
|
+
readonly testId?: string;
|
|
24
|
+
}): ReactNode;
|
|
25
|
+
export declare function EditorFooter({ testIdPrefix, message, saveError, modifiedAt, dirty, busy, canSave, canPublish, onDiscard, onSave, onPublish, }: {
|
|
26
|
+
readonly testIdPrefix: string;
|
|
27
|
+
readonly message: {
|
|
28
|
+
readonly tone: FooterTone;
|
|
29
|
+
readonly text: string;
|
|
30
|
+
} | null;
|
|
31
|
+
readonly saveError: string | null;
|
|
32
|
+
readonly modifiedAt: string | null;
|
|
33
|
+
readonly dirty: boolean;
|
|
34
|
+
readonly busy: boolean;
|
|
35
|
+
readonly canSave: boolean;
|
|
36
|
+
readonly canPublish: boolean;
|
|
37
|
+
readonly onDiscard: () => void;
|
|
38
|
+
readonly onSave: () => void;
|
|
39
|
+
readonly onPublish: () => void;
|
|
40
|
+
}): ReactNode;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
// @runtime client
|
|
3
|
+
import { usePrimitives, useTranslation } from "@cosmicdrift/kumiko-renderer";
|
|
4
|
+
import { cn } from "@cosmicdrift/kumiko-renderer-web";
|
|
5
|
+
import { OriginBadge } from "./editor-header.js";
|
|
6
|
+
import { formatRelativeWhen } from "./relative-time.js";
|
|
7
|
+
const INFO_BAR_TONE = {
|
|
8
|
+
active: "bg-primary/10",
|
|
9
|
+
muted: "bg-muted",
|
|
10
|
+
warn: "bg-status-warn-surface",
|
|
11
|
+
};
|
|
12
|
+
const FOOTER_TONE = {
|
|
13
|
+
ok: { text: "text-status-ok", glyph: "✓" },
|
|
14
|
+
warn: { text: "text-status-warn", glyph: "!" },
|
|
15
|
+
bad: { text: "text-status-bad", glyph: "!" },
|
|
16
|
+
};
|
|
17
|
+
// Height below the shell header: the rails and the center scroll on their own.
|
|
18
|
+
export function EditorFrame({ testId, children, }) {
|
|
19
|
+
return (_jsx("div", { className: "flex h-[calc(100svh-var(--shell-header-height))] min-h-[600px] flex-col", "data-testid": testId, children: children }));
|
|
20
|
+
}
|
|
21
|
+
export function EditorInfoBar({ tone, testId, origin, controls, links, overlays, children, }) {
|
|
22
|
+
return (_jsxs(_Fragment, { children: [_jsxs("div", { className: cn("flex items-start gap-4 border-b px-6 py-2 text-sm", INFO_BAR_TONE[tone]), "data-testid": testId, "data-variant": tone === "warn" ? "warn" : undefined, children: [_jsxs("span", { className: "min-w-0 flex-1", children: [origin != null && _jsx(OriginBadge, { origin: origin }), " ", children] }), controls !== undefined && (_jsx("div", { className: "flex shrink-0 items-center gap-2", children: controls })), links !== undefined && _jsx("div", { className: "flex shrink-0 items-center gap-4", children: links })] }), overlays] }));
|
|
23
|
+
}
|
|
24
|
+
export function EditorBody({ left, center, right, testId, }) {
|
|
25
|
+
return (_jsxs("div", { className: cn("grid min-h-0 flex-1", left === undefined
|
|
26
|
+
? "grid-cols-[minmax(0,1fr)_280px]"
|
|
27
|
+
: "grid-cols-[240px_minmax(0,1fr)_280px]"), "data-testid": testId, children: [left !== undefined && (_jsx("aside", { className: "min-h-0 overflow-y-auto border-r bg-background", children: left })), _jsx("div", { className: "flex min-h-0 min-w-0 flex-col overflow-hidden bg-muted", children: center }), _jsx("aside", { className: "min-h-0 overflow-y-auto border-l bg-background", children: right })] }));
|
|
28
|
+
}
|
|
29
|
+
export function EditorFooter({ testIdPrefix, message, saveError, modifiedAt, dirty, busy, canSave, canPublish, onDiscard, onSave, onPublish, }) {
|
|
30
|
+
const t = useTranslation();
|
|
31
|
+
const { Banner, Button, Text } = usePrimitives();
|
|
32
|
+
const tone = message === null ? null : FOOTER_TONE[message.tone];
|
|
33
|
+
return (_jsxs("div", { className: "flex items-center gap-4 border-t bg-background px-6 py-3", "data-testid": `${testIdPrefix}-footer`, children: [_jsxs("div", { className: "min-w-0 flex-1 text-sm", children: [saveError !== null ? (_jsx(Banner, { variant: "error", testId: `${testIdPrefix}-save-error`, children: saveError })) : (_jsx("span", { className: cn(tone === null ? "text-muted-foreground" : tone.text), "data-testid": `${testIdPrefix}-footer-message`, "data-tone": message?.tone, children: message === null
|
|
34
|
+
? t("documentDesigner.editor.footer.checking")
|
|
35
|
+
: `${tone?.glyph ?? ""} ${message.text}` })), modifiedAt !== null && (_jsxs(_Fragment, { children: [" ", _jsx(Text, { variant: "muted", testId: `${testIdPrefix}-saved-at`, children: t("documentDesigner.mail.footer.savedAt", { when: formatRelativeWhen(modifiedAt) }) })] }))] }), _jsxs("div", { className: "flex shrink-0 items-center gap-3", children: [_jsx(Button, { variant: "ghost", disabled: !dirty, onClick: onDiscard, testId: "document-designer-discard", children: t("documentDesigner.editor.discard") }), _jsx(Button, { variant: "secondary", disabled: !canSave, loading: busy, onClick: onSave, testId: "document-designer-save", children: t("documentDesigner.editor.save") }), _jsx(Button, { disabled: !canPublish, onClick: onPublish, testId: "document-designer-publish", children: t("documentDesigner.editor.publish") })] })] }));
|
|
36
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { DispatcherError } from "@cosmicdrift/kumiko-headless";
|
|
2
|
+
type Translate = (key: string, params?: Readonly<Record<string, unknown>>) => string;
|
|
3
|
+
export declare function isAlreadyActive(error: DispatcherError): boolean;
|
|
4
|
+
/** Plain-language text for a failed call; falls back to the server message when no translation exists. */
|
|
5
|
+
export declare function describeError(t: Translate, error: DispatcherError): string;
|
|
6
|
+
export {};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// @runtime client
|
|
2
|
+
import { TemplateResolverErrors } from "@cosmicdrift/kumiko-bundled-features/template-resolver/web";
|
|
3
|
+
import { propertyOf } from "@cosmicdrift/kumiko-types/type-guards";
|
|
4
|
+
function reasonOf(error) {
|
|
5
|
+
const reason = propertyOf(error.details, "reason");
|
|
6
|
+
return typeof reason === "string" ? reason : undefined;
|
|
7
|
+
}
|
|
8
|
+
export function isAlreadyActive(error) {
|
|
9
|
+
return (error.code === TemplateResolverErrors.alreadyActive ||
|
|
10
|
+
reasonOf(error) === TemplateResolverErrors.alreadyActive);
|
|
11
|
+
}
|
|
12
|
+
const REASON_KEYS = {
|
|
13
|
+
[TemplateResolverErrors.resourceTooLarge]: "documentDesigner.error.resourceTooLarge",
|
|
14
|
+
[TemplateResolverErrors.resourceUnsupportedImage]: "documentDesigner.error.resourceUnsupportedImage",
|
|
15
|
+
};
|
|
16
|
+
/** Plain-language text for a failed call; falls back to the server message when no translation exists. */
|
|
17
|
+
export function describeError(t, error) {
|
|
18
|
+
const reason = reasonOf(error);
|
|
19
|
+
const reasonKey = reason === undefined ? undefined : REASON_KEYS[reason];
|
|
20
|
+
if (reasonKey !== undefined)
|
|
21
|
+
return t(reasonKey);
|
|
22
|
+
const translated = t(error.i18nKey, error.i18nParams);
|
|
23
|
+
return translated === error.i18nKey ? error.message : translated;
|
|
24
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { DispatcherError } from "@cosmicdrift/kumiko-headless";
|
|
2
|
+
type Translate = (key: string, params?: Readonly<Record<string, unknown>>) => string;
|
|
3
|
+
export declare function errorTextWithFieldIssues(error: DispatcherError, translate: Translate): string;
|
|
4
|
+
export {};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// @runtime client
|
|
2
|
+
import { dispatcherErrorText } from "@cosmicdrift/kumiko-renderer";
|
|
3
|
+
// A validation_error alone says nothing; the server names the offending field
|
|
4
|
+
// and reason per issue, so the user gets those as text.
|
|
5
|
+
export function errorTextWithFieldIssues(error, translate) {
|
|
6
|
+
const headline = dispatcherErrorText(error, translate);
|
|
7
|
+
const issues = (error.details?.fields ?? []).map((issue) => {
|
|
8
|
+
const text = translate(issue.i18nKey, issue.params);
|
|
9
|
+
return text === issue.i18nKey ? `${issue.path}: ${issue.code}` : text;
|
|
10
|
+
});
|
|
11
|
+
return issues.length === 0 ? headline : `${headline} ${issues.join(" ")}`;
|
|
12
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// @runtime client
|
|
2
|
+
// The upload input only hands back the FileRef id; resources are keyed and named after the file.
|
|
3
|
+
export async function fetchStoredFileName(fileRefId) {
|
|
4
|
+
try {
|
|
5
|
+
const res = await fetch(`/api/files/${encodeURIComponent(fileRefId)}/meta`);
|
|
6
|
+
if (!res.ok)
|
|
7
|
+
return null;
|
|
8
|
+
const meta = await res.json();
|
|
9
|
+
if (typeof meta !== "object" || meta === null || !("fileName" in meta))
|
|
10
|
+
return null;
|
|
11
|
+
return typeof meta.fileName === "string" ? meta.fileName : null;
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
export const fileUrl = (fileRefId) => `/api/files/${encodeURIComponent(fileRefId)}`;
|