@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
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
// @runtime client
|
|
3
|
+
import { useAppFeatures, useNav, usePrimitives, useTranslation, } from "@cosmicdrift/kumiko-renderer";
|
|
4
|
+
import { BRANDING_SCREEN_QN } from "../constants.js";
|
|
5
|
+
import { isHexColor, LAYOUT_ACCENT_LINES, LAYOUT_BUTTON_STYLES, LAYOUT_FONTS, LAYOUT_WIDTHS, } from "./layout-meta.js";
|
|
6
|
+
import { layoutUsers } from "./layout-users.js";
|
|
7
|
+
import { PLACEHOLDER_SYMBOL, SYSTEM_PLACEHOLDER_TYPE, systemPlaceholderLabel, } from "./mail-draft.js";
|
|
8
|
+
import { RailField, RailHint, RailRow, RailSection } from "./rail.js";
|
|
9
|
+
import { statusLabel } from "./status-label.js";
|
|
10
|
+
const BRANDING_SCREEN_SHORT_ID = BRANDING_SCREEN_QN.slice(BRANDING_SCREEN_QN.lastIndexOf(":") + 1);
|
|
11
|
+
function logoName(logoUrl) {
|
|
12
|
+
const path = logoUrl.split("?")[0] ?? "";
|
|
13
|
+
return path.slice(path.lastIndexOf("/") + 1);
|
|
14
|
+
}
|
|
15
|
+
function hostOf(siteUrl) {
|
|
16
|
+
return siteUrl.replace(/^https?:\/\//, "").replace(/\/$/, "");
|
|
17
|
+
}
|
|
18
|
+
// The color is user data, so it goes into a style attribute instead of a class.
|
|
19
|
+
function ColorSwatch({ color }) {
|
|
20
|
+
return (_jsx("span", { "data-testid": "document-designer-accent-swatch", className: "inline-block size-3.5 shrink-0 rounded-sm border", style: { backgroundColor: color } }));
|
|
21
|
+
}
|
|
22
|
+
export function BrandingRail({ data, unavailable, }) {
|
|
23
|
+
const t = useTranslation();
|
|
24
|
+
const nav = useNav();
|
|
25
|
+
const features = useAppFeatures();
|
|
26
|
+
const { Link } = usePrimitives();
|
|
27
|
+
const hasBrandingScreen = features.some((feature) => feature.featureName === "managed-pages" &&
|
|
28
|
+
feature.screens.some((screen) => screen.id === BRANDING_SCREEN_SHORT_ID));
|
|
29
|
+
const notSet = t("documentDesigner.branding.notSet");
|
|
30
|
+
const value = (text) => text === undefined || text === "" ? notSet : text;
|
|
31
|
+
const editLink = hasBrandingScreen ? (_jsx(Link, { href: nav.hrefFor({ screenId: BRANDING_SCREEN_SHORT_ID }), onPress: () => nav.navigate({ screenId: BRANDING_SCREEN_SHORT_ID }), testId: "document-designer-branding-edit", children: t("documentDesigner.branding.edit") })) : undefined;
|
|
32
|
+
if (unavailable) {
|
|
33
|
+
return (_jsx(RailSection, { title: t("documentDesigner.branding.title"), testId: "document-designer-branding", children: _jsx(RailHint, { children: t("documentDesigner.branding.unavailable") }) }));
|
|
34
|
+
}
|
|
35
|
+
const accent = data?.accentColor ?? "";
|
|
36
|
+
const logo = data?.logoUrl !== undefined && data.logoUrl !== ""
|
|
37
|
+
? logoName(data.logoUrl)
|
|
38
|
+
: data?.logoFile !== undefined && data.logoFile !== ""
|
|
39
|
+
? t("documentDesigner.branding.logoUploaded")
|
|
40
|
+
: notSet;
|
|
41
|
+
return (_jsxs(RailSection, { title: t("documentDesigner.branding.title"), action: editLink, testId: "document-designer-branding", children: [_jsx(RailRow, { label: t("documentDesigner.branding.productName"), trailing: value(data?.title) }), _jsx(RailRow, { label: t("documentDesigner.branding.accentColor"), trailing: isHexColor(accent) ? (_jsxs("span", { className: "inline-flex items-center gap-1.5", children: [_jsx(ColorSwatch, { color: accent }), _jsx("code", { children: accent })] })) : (value(accent)) }), _jsx(RailRow, { label: t("documentDesigner.branding.logo"), trailing: logo }), _jsx(RailRow, { label: t("documentDesigner.branding.website"), trailing: data?.siteUrl === undefined || data.siteUrl === "" ? notSet : hostOf(data.siteUrl) }), _jsx(RailHint, { children: t("documentDesigner.branding.hint") })] }));
|
|
42
|
+
}
|
|
43
|
+
export function PlaceholderRail({ validation, }) {
|
|
44
|
+
const t = useTranslation();
|
|
45
|
+
const used = new Set(validation?.used ?? []);
|
|
46
|
+
return (_jsxs(RailSection, { title: t("documentDesigner.layoutPlaceholders.title"), testId: "document-designer-placeholders", children: [_jsx(RailRow, { symbol: "\u229E", label: t("documentDesigner.placeholders.content"), required: true, trailing: "\u2713", trailingTone: "ok" }), (validation?.systemPlaceholders ?? []).map((name) => (_jsx(RailRow, { testId: `document-designer-placeholder-${name}`, symbol: PLACEHOLDER_SYMBOL[SYSTEM_PLACEHOLDER_TYPE[name] ?? "text"], label: systemPlaceholderLabel(t, name), trailing: used.has(name) ? "✓" : "–", trailingTone: used.has(name) ? "ok" : "muted" }, name)))] }));
|
|
47
|
+
}
|
|
48
|
+
// Both renderers always run for their kind; the rows say which one produces the output.
|
|
49
|
+
export function RendererRail({ kind }) {
|
|
50
|
+
const t = useTranslation();
|
|
51
|
+
return (_jsx(RailSection, { title: t("documentDesigner.renderer.title"), testId: "document-designer-renderer", children: _jsx(RailRow, { label: t(kind === "mail-layout"
|
|
52
|
+
? "documentDesigner.renderer.mjml"
|
|
53
|
+
: "documentDesigner.renderer.chromium"), trailing: t("documentDesigner.renderer.active"), trailingTone: "ok" }) }));
|
|
54
|
+
}
|
|
55
|
+
export function AppearanceRail({ appearance, onChange, fromMjml, }) {
|
|
56
|
+
const t = useTranslation();
|
|
57
|
+
const { Field, Input } = usePrimitives();
|
|
58
|
+
const patch = (changes) => onChange({ ...appearance, ...changes });
|
|
59
|
+
const backgroundInvalid = !isHexColor(appearance.outerBackground);
|
|
60
|
+
return (_jsxs(RailSection, { title: t("documentDesigner.appearance.title"), testId: "document-designer-appearance", children: [fromMjml ? (_jsx(RailHint, { children: t("documentDesigner.appearance.fromMjml") })) : (_jsxs(RailField, { children: [_jsx(Field, { id: "dd-layout-width", label: t("documentDesigner.appearance.width"), children: _jsx(Input, { kind: "select", id: "dd-layout-width", name: "width", value: String(appearance.width), onChange: (value) => {
|
|
61
|
+
const width = LAYOUT_WIDTHS.find((candidate) => String(candidate) === value);
|
|
62
|
+
if (width !== undefined)
|
|
63
|
+
patch({ width });
|
|
64
|
+
}, display: "dropdown", options: LAYOUT_WIDTHS.map((width) => ({
|
|
65
|
+
value: String(width),
|
|
66
|
+
label: `${width} px`,
|
|
67
|
+
})) }) }), _jsx(Field, { id: "dd-layout-font", label: t("documentDesigner.appearance.font"), children: _jsx(Input, { kind: "select", id: "dd-layout-font", name: "font", value: appearance.font, onChange: (value) => {
|
|
68
|
+
const font = LAYOUT_FONTS.find((candidate) => candidate === value);
|
|
69
|
+
if (font !== undefined)
|
|
70
|
+
patch({ font });
|
|
71
|
+
}, display: "dropdown", options: LAYOUT_FONTS.map((font) => ({
|
|
72
|
+
value: font,
|
|
73
|
+
label: t(`documentDesigner.appearance.font.${font}`),
|
|
74
|
+
})) }) }), _jsxs(Field, { id: "dd-layout-background", label: t("documentDesigner.appearance.background"), children: [_jsxs("div", { className: "flex items-center gap-2", children: [!backgroundInvalid && _jsx(ColorSwatch, { color: appearance.outerBackground }), _jsx(Input, { kind: "text", id: "dd-layout-background", name: "outerBackground", value: appearance.outerBackground, onChange: (outerBackground) => patch({ outerBackground }), hasError: backgroundInvalid })] }), backgroundInvalid && (_jsx("p", { role: "alert", className: "text-xs text-status-bad", children: t("documentDesigner.appearance.backgroundInvalid") }))] }), _jsx(Field, { id: "dd-layout-accent", label: t("documentDesigner.appearance.accentLine"), children: _jsx(Input, { kind: "select", id: "dd-layout-accent", name: "accentLine", value: appearance.accentLine, onChange: (value) => {
|
|
75
|
+
const accentLine = LAYOUT_ACCENT_LINES.find((candidate) => candidate === value);
|
|
76
|
+
if (accentLine !== undefined)
|
|
77
|
+
patch({ accentLine });
|
|
78
|
+
}, display: "dropdown", options: LAYOUT_ACCENT_LINES.map((accentLine) => ({
|
|
79
|
+
value: accentLine,
|
|
80
|
+
label: t(`documentDesigner.appearance.accentLine.${accentLine}`),
|
|
81
|
+
})) }) }), _jsx(Field, { id: "dd-layout-button", label: t("documentDesigner.appearance.buttonStyle"), children: _jsx(Input, { kind: "select", id: "dd-layout-button", name: "buttonStyle", value: appearance.buttonStyle, onChange: (value) => {
|
|
82
|
+
const buttonStyle = LAYOUT_BUTTON_STYLES.find((candidate) => candidate === value);
|
|
83
|
+
if (buttonStyle !== undefined)
|
|
84
|
+
patch({ buttonStyle });
|
|
85
|
+
}, display: "dropdown", options: LAYOUT_BUTTON_STYLES.map((buttonStyle) => ({
|
|
86
|
+
value: buttonStyle,
|
|
87
|
+
label: t(`documentDesigner.appearance.buttonStyle.${buttonStyle}`),
|
|
88
|
+
})) }) })] })), !fromMjml && _jsx(RailHint, { children: t("documentDesigner.appearance.fontHint") })] }));
|
|
89
|
+
}
|
|
90
|
+
export function UsedByRail({ layoutSlug, kind, rows, }) {
|
|
91
|
+
const t = useTranslation();
|
|
92
|
+
const users = layoutUsers(rows, kind, layoutSlug);
|
|
93
|
+
return (_jsx(RailSection, { title: t("documentDesigner.usedBy.title"), testId: "document-designer-used-by", children: users.length === 0 ? (_jsx(RailHint, { children: t("documentDesigner.usedBy.none") })) : (users.map((row) => (_jsx(RailRow, { testId: `used-by-${row.id}`, label: row.title, trailing: row.origin === "system"
|
|
94
|
+
? t("documentDesigner.source.system")
|
|
95
|
+
: statusLabel(t, row.status, row.version) }, row.id)))) }));
|
|
96
|
+
}
|
|
97
|
+
export function MjmlSourceRail({ mjmlSource }) {
|
|
98
|
+
const t = useTranslation();
|
|
99
|
+
return (_jsxs(RailSection, { title: t("documentDesigner.mjml.title"), testId: "document-designer-mjml-preview", children: [mjmlSource !== null && (_jsx("pre", { className: "mx-3.5 mt-1 mb-2 max-h-56 overflow-auto whitespace-pre rounded-sm border bg-muted p-2.5 font-mono text-xs text-muted-foreground", children: mjmlSource })), _jsx(RailHint, { children: t("documentDesigner.mjml.hint") })] }));
|
|
100
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import type { LayoutMeta } from "./layout-meta.js";
|
|
2
|
+
import type { ServedVersionView } from "./types.js";
|
|
3
|
+
export type LayoutKind = "mail-layout" | "pdf-layout";
|
|
4
|
+
export type ValidationIssue = {
|
|
5
|
+
readonly path: string;
|
|
6
|
+
readonly code: string;
|
|
7
|
+
readonly i18nKey: string;
|
|
8
|
+
readonly params?: Readonly<Record<string, unknown>>;
|
|
9
|
+
};
|
|
10
|
+
export type LayoutValidation = {
|
|
11
|
+
readonly canSave: boolean;
|
|
12
|
+
readonly canPublish: boolean;
|
|
13
|
+
readonly issues: readonly ValidationIssue[];
|
|
14
|
+
readonly publishIssues: readonly ValidationIssue[];
|
|
15
|
+
readonly used: readonly string[];
|
|
16
|
+
readonly systemPlaceholders: readonly string[];
|
|
17
|
+
};
|
|
18
|
+
export type BrandingView = {
|
|
19
|
+
readonly title: string;
|
|
20
|
+
readonly accentColor: string;
|
|
21
|
+
readonly logoUrl: string;
|
|
22
|
+
readonly logoFile: string;
|
|
23
|
+
readonly siteUrl: string;
|
|
24
|
+
};
|
|
25
|
+
export type TenantOverrideCounts = {
|
|
26
|
+
readonly activeOverrides: number;
|
|
27
|
+
readonly draftOverrides: number;
|
|
28
|
+
readonly totalTenants: number | null;
|
|
29
|
+
};
|
|
30
|
+
export type TemplateDiff = {
|
|
31
|
+
readonly identical: boolean;
|
|
32
|
+
readonly blocks: readonly {
|
|
33
|
+
readonly change: "same" | "added" | "removed";
|
|
34
|
+
readonly content: string;
|
|
35
|
+
}[];
|
|
36
|
+
readonly meta: readonly {
|
|
37
|
+
readonly key: string;
|
|
38
|
+
readonly tenant?: unknown;
|
|
39
|
+
readonly system?: unknown;
|
|
40
|
+
}[];
|
|
41
|
+
};
|
|
42
|
+
export type UpsertResponse = {
|
|
43
|
+
readonly id: string;
|
|
44
|
+
};
|
|
45
|
+
export type LayoutSourceRow = {
|
|
46
|
+
readonly id: string;
|
|
47
|
+
readonly version: number;
|
|
48
|
+
readonly status: string;
|
|
49
|
+
readonly locale: string;
|
|
50
|
+
readonly active?: ServedVersionView | null;
|
|
51
|
+
};
|
|
52
|
+
export type LayoutSourceView = {
|
|
53
|
+
readonly tenant: LayoutSourceRow | null;
|
|
54
|
+
readonly system: (LayoutSourceRow & {
|
|
55
|
+
readonly contentFormat: string;
|
|
56
|
+
}) | null;
|
|
57
|
+
readonly regions: {
|
|
58
|
+
readonly header: string;
|
|
59
|
+
readonly signature: string;
|
|
60
|
+
readonly footer: string;
|
|
61
|
+
};
|
|
62
|
+
readonly effectiveFormat: "html" | "mjml" | null;
|
|
63
|
+
readonly mjmlSource: string | null;
|
|
64
|
+
readonly meta: LayoutMeta;
|
|
65
|
+
readonly locale: string;
|
|
66
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { LayoutKind } from "./layout-types.js";
|
|
2
|
+
import type { OverviewRowView } from "./overview-row.js";
|
|
3
|
+
export declare function layoutUserKind(kind: LayoutKind): "mail" | "document-pdf";
|
|
4
|
+
export declare function layoutUsers(rows: readonly OverviewRowView[], kind: LayoutKind, layoutSlug: string): readonly OverviewRowView[];
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// @runtime client
|
|
2
|
+
import { DEFAULT_LAYOUT_SLUG } from "../constants.js";
|
|
3
|
+
export function layoutUserKind(kind) {
|
|
4
|
+
return kind === "mail-layout" ? "mail" : "document-pdf";
|
|
5
|
+
}
|
|
6
|
+
// Templates whose content is wrapped by this layout. A template without an explicit
|
|
7
|
+
// layoutSlug uses the default layout.
|
|
8
|
+
export function layoutUsers(rows, kind, layoutSlug) {
|
|
9
|
+
const documentKind = layoutUserKind(kind);
|
|
10
|
+
return rows.filter((row) => row.kind === documentKind && (row.layoutSlug ?? DEFAULT_LAYOUT_SLUG) === layoutSlug);
|
|
11
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
export type LeaveTarget = {
|
|
3
|
+
readonly kind: "locale";
|
|
4
|
+
readonly locale: string;
|
|
5
|
+
};
|
|
6
|
+
export type ConfirmDialog = {
|
|
7
|
+
readonly kind: "none";
|
|
8
|
+
} | {
|
|
9
|
+
readonly kind: "discard";
|
|
10
|
+
} | {
|
|
11
|
+
readonly kind: "remove";
|
|
12
|
+
readonly key: string;
|
|
13
|
+
} | {
|
|
14
|
+
readonly kind: "leave";
|
|
15
|
+
readonly target: LeaveTarget;
|
|
16
|
+
};
|
|
17
|
+
export declare function MailConfirmDialogs({ dialog, onClose, onDiscard, onRemove, onLeaveSave, onLeaveDiscard, }: {
|
|
18
|
+
readonly dialog: ConfirmDialog;
|
|
19
|
+
readonly onClose: () => void;
|
|
20
|
+
readonly onDiscard: () => void;
|
|
21
|
+
readonly onRemove: (key: string) => Promise<void>;
|
|
22
|
+
readonly onLeaveSave: (target: LeaveTarget) => Promise<void>;
|
|
23
|
+
readonly onLeaveDiscard: (target: LeaveTarget) => void;
|
|
24
|
+
}): ReactNode;
|
|
@@ -0,0 +1,15 @@
|
|
|
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
|
+
export function MailConfirmDialogs({ dialog, onClose, onDiscard, onRemove, onLeaveSave, onLeaveDiscard, }) {
|
|
5
|
+
const t = useTranslation();
|
|
6
|
+
const { Button, Dialog } = usePrimitives();
|
|
7
|
+
const closeUnless = (open) => {
|
|
8
|
+
if (!open)
|
|
9
|
+
onClose();
|
|
10
|
+
};
|
|
11
|
+
return (_jsxs(_Fragment, { children: [_jsx(Dialog, { open: dialog.kind === "discard", onOpenChange: closeUnless, title: t("documentDesigner.mail.discard.title"), description: t("documentDesigner.mail.discard.description"), confirmLabel: t("documentDesigner.mail.discard.confirm"), variant: "danger", testId: "mail-discard-dialog", onConfirm: onDiscard }), _jsx(Dialog, { open: dialog.kind === "remove", onOpenChange: closeUnless, title: t("documentDesigner.mail.remove.title"), description: t("documentDesigner.mail.remove.description"), confirmLabel: t("documentDesigner.mail.remove.confirm"), variant: "danger", testId: "mail-remove-dialog", onConfirm: () => (dialog.kind === "remove" ? onRemove(dialog.key) : Promise.resolve()) }), _jsx(Dialog, { open: dialog.kind === "leave", onOpenChange: closeUnless, title: t("documentDesigner.mail.leave.title"), description: t("documentDesigner.mail.leave.description"), confirmLabel: t("documentDesigner.mail.leave.save"), testId: "mail-leave-dialog", onConfirm: () => (dialog.kind === "leave" ? onLeaveSave(dialog.target) : Promise.resolve()), children: _jsx(Button, { variant: "danger-ghost", onClick: () => {
|
|
12
|
+
if (dialog.kind === "leave")
|
|
13
|
+
onLeaveDiscard(dialog.target);
|
|
14
|
+
}, testId: "mail-leave-discard", children: t("documentDesigner.mail.leave.discard") }) })] }));
|
|
15
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { LinkedResourceView, MailDraft, MailVariable, PlaceholderType, TemplateRowView } from "./mail-types.js";
|
|
2
|
+
export declare const EMPTY_DRAFT: MailDraft;
|
|
3
|
+
export declare const PLACEHOLDER_SYMBOL: Readonly<Record<PlaceholderType, string>>;
|
|
4
|
+
export declare const LAYOUT_OWNED_PLACEHOLDERS: ReadonlySet<string>;
|
|
5
|
+
export declare const SYSTEM_PLACEHOLDER_TYPE: Readonly<Record<string, PlaceholderType>>;
|
|
6
|
+
type Translate = (key: string, params?: Readonly<Record<string, unknown>>) => string;
|
|
7
|
+
export declare function systemPlaceholderLabel(t: Translate, name: string): string;
|
|
8
|
+
export declare function systemLabelMap(t: Translate, names: readonly string[]): Readonly<Record<string, string>>;
|
|
9
|
+
export declare function parseJsonRecord(value: unknown): Readonly<Record<string, unknown>>;
|
|
10
|
+
export declare function loadedMailOf(row: TemplateRowView | null): MailDraft;
|
|
11
|
+
export declare function buildMeta(draft: MailDraft): Record<string, unknown>;
|
|
12
|
+
export declare function isDraftDirty(current: MailDraft, saved: MailDraft): boolean;
|
|
13
|
+
export declare function parseLinkedResources(value: unknown): readonly LinkedResourceView[];
|
|
14
|
+
export declare function uniqueResourceKey(source: string, taken: readonly string[]): string;
|
|
15
|
+
export declare function lastSlugSegment(slug: string): string;
|
|
16
|
+
export declare function formatKilobytes(bytes: number): string;
|
|
17
|
+
export declare function variableLabelMap(variables: Readonly<Record<string, MailVariable>>): Record<string, string>;
|
|
18
|
+
export declare function displayValue(t: (key: string, params?: Readonly<Record<string, unknown>>) => string, value: unknown): string;
|
|
19
|
+
export {};
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
export const EMPTY_DRAFT = {
|
|
2
|
+
subject: "",
|
|
3
|
+
previewText: "",
|
|
4
|
+
layoutSlug: "",
|
|
5
|
+
category: "",
|
|
6
|
+
senderName: "",
|
|
7
|
+
replyTo: "",
|
|
8
|
+
body: "",
|
|
9
|
+
};
|
|
10
|
+
export const PLACEHOLDER_SYMBOL = {
|
|
11
|
+
text: "T",
|
|
12
|
+
money: "€",
|
|
13
|
+
date: "D",
|
|
14
|
+
link: "↗",
|
|
15
|
+
list: "≡",
|
|
16
|
+
};
|
|
17
|
+
// Filled in by the layout around the content, so a template that never uses them is fine.
|
|
18
|
+
export const LAYOUT_OWNED_PLACEHOLDERS = new Set([
|
|
19
|
+
"imprint",
|
|
20
|
+
"unsubscribeUrl",
|
|
21
|
+
]);
|
|
22
|
+
export const SYSTEM_PLACEHOLDER_TYPE = {
|
|
23
|
+
imprint: "link",
|
|
24
|
+
unsubscribeUrl: "link",
|
|
25
|
+
};
|
|
26
|
+
export function systemPlaceholderLabel(t, name) {
|
|
27
|
+
const key = `documentDesigner.placeholder.${name}`;
|
|
28
|
+
const translated = t(key);
|
|
29
|
+
return translated === key ? name : translated;
|
|
30
|
+
}
|
|
31
|
+
// Chips in the editors show these instead of the raw system names.
|
|
32
|
+
export function systemLabelMap(t, names) {
|
|
33
|
+
return Object.fromEntries(names.map((name) => [name, systemPlaceholderLabel(t, name)]));
|
|
34
|
+
}
|
|
35
|
+
function isRecord(value) {
|
|
36
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
37
|
+
}
|
|
38
|
+
// Stored JSON columns come back as a JSON string or an already parsed object.
|
|
39
|
+
export function parseJsonRecord(value) {
|
|
40
|
+
if (isRecord(value))
|
|
41
|
+
return value;
|
|
42
|
+
if (typeof value !== "string" || value === "")
|
|
43
|
+
return {};
|
|
44
|
+
try {
|
|
45
|
+
const parsed = JSON.parse(value);
|
|
46
|
+
return isRecord(parsed) ? parsed : {};
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return {};
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
function categoryOf(value) {
|
|
53
|
+
return value === "transactional" || value === "marketing" ? value : "";
|
|
54
|
+
}
|
|
55
|
+
function textOf(value) {
|
|
56
|
+
return typeof value === "string" ? value : "";
|
|
57
|
+
}
|
|
58
|
+
export function loadedMailOf(row) {
|
|
59
|
+
if (row === null)
|
|
60
|
+
return EMPTY_DRAFT;
|
|
61
|
+
const meta = parseJsonRecord(row.meta);
|
|
62
|
+
return {
|
|
63
|
+
subject: textOf(meta["subject"]),
|
|
64
|
+
previewText: textOf(meta["previewText"]),
|
|
65
|
+
layoutSlug: textOf(meta["layoutSlug"]),
|
|
66
|
+
category: categoryOf(meta["category"]),
|
|
67
|
+
senderName: textOf(meta["senderName"]),
|
|
68
|
+
replyTo: textOf(meta["replyTo"]),
|
|
69
|
+
body: row.content,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
export function buildMeta(draft) {
|
|
73
|
+
return {
|
|
74
|
+
subject: draft.subject,
|
|
75
|
+
...(draft.previewText !== "" && { previewText: draft.previewText }),
|
|
76
|
+
...(draft.layoutSlug !== "" && { layoutSlug: draft.layoutSlug }),
|
|
77
|
+
...(draft.category !== "" && { category: draft.category }),
|
|
78
|
+
...(draft.senderName !== "" && { senderName: draft.senderName }),
|
|
79
|
+
...(draft.replyTo !== "" && { replyTo: draft.replyTo }),
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
export function isDraftDirty(current, saved) {
|
|
83
|
+
return (current.subject !== saved.subject ||
|
|
84
|
+
current.previewText !== saved.previewText ||
|
|
85
|
+
current.layoutSlug !== saved.layoutSlug ||
|
|
86
|
+
current.category !== saved.category ||
|
|
87
|
+
current.senderName !== saved.senderName ||
|
|
88
|
+
current.replyTo !== saved.replyTo ||
|
|
89
|
+
current.body !== saved.body);
|
|
90
|
+
}
|
|
91
|
+
export function parseLinkedResources(value) {
|
|
92
|
+
return Object.entries(parseJsonRecord(value)).flatMap(([key, entry]) => {
|
|
93
|
+
if (typeof entry === "string")
|
|
94
|
+
return [{ kind: "image", key, fileRefId: entry }];
|
|
95
|
+
if (!isRecord(entry))
|
|
96
|
+
return [];
|
|
97
|
+
const fileName = textOf(entry["fileName"]);
|
|
98
|
+
if (entry["kind"] === "file") {
|
|
99
|
+
return [{ kind: "file", key, fileRefId: textOf(entry["fileRefId"]), fileName }];
|
|
100
|
+
}
|
|
101
|
+
if (entry["kind"] === "rendered-pdf") {
|
|
102
|
+
return [{ kind: "pdf", key, templateSlug: textOf(entry["templateSlug"]), fileName }];
|
|
103
|
+
}
|
|
104
|
+
return [];
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
const KEY_MAX_LENGTH = 64;
|
|
108
|
+
// Resource keys allow letters, digits, dot, dash and underscore; a taken key gets a numeric suffix.
|
|
109
|
+
export function uniqueResourceKey(source, taken) {
|
|
110
|
+
const withoutExtension = source.replace(/\.[A-Za-z0-9]{1,5}$/, "");
|
|
111
|
+
const base = withoutExtension
|
|
112
|
+
.toLowerCase()
|
|
113
|
+
.replace(/[^a-z0-9._-]+/g, "-")
|
|
114
|
+
.replace(/^-+|-+$/g, "")
|
|
115
|
+
.slice(0, KEY_MAX_LENGTH - 4) || "resource";
|
|
116
|
+
if (!taken.includes(base))
|
|
117
|
+
return base;
|
|
118
|
+
for (let n = 2;; n += 1) {
|
|
119
|
+
const candidate = `${base}-${n}`;
|
|
120
|
+
if (!taken.includes(candidate))
|
|
121
|
+
return candidate;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
export function lastSlugSegment(slug) {
|
|
125
|
+
const parts = slug.split(":");
|
|
126
|
+
return parts[parts.length - 1] ?? slug;
|
|
127
|
+
}
|
|
128
|
+
export function formatKilobytes(bytes) {
|
|
129
|
+
return `${Math.max(1, Math.round(bytes / 1024))} KB`;
|
|
130
|
+
}
|
|
131
|
+
// Labels for the chip editors; list item fields are addressed as "list.item".
|
|
132
|
+
export function variableLabelMap(variables) {
|
|
133
|
+
const labels = {};
|
|
134
|
+
for (const [name, variable] of Object.entries(variables)) {
|
|
135
|
+
if (variable.label !== undefined)
|
|
136
|
+
labels[name] = variable.label;
|
|
137
|
+
for (const [itemName, item] of Object.entries(variable.item ?? {})) {
|
|
138
|
+
if (item.label !== undefined)
|
|
139
|
+
labels[`${name}.${itemName}`] = item.label;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return labels;
|
|
143
|
+
}
|
|
144
|
+
export function displayValue(t, value) {
|
|
145
|
+
if (Array.isArray(value))
|
|
146
|
+
return t("documentDesigner.mail.preview.entries", { count: value.length });
|
|
147
|
+
if (isRecord(value)) {
|
|
148
|
+
const { amount, currency } = value;
|
|
149
|
+
if (typeof amount === "number" && typeof currency === "string")
|
|
150
|
+
return `${amount} ${currency}`;
|
|
151
|
+
return JSON.stringify(value);
|
|
152
|
+
}
|
|
153
|
+
return String(value ?? "");
|
|
154
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { type DocumentResourcePreview } from "@cosmicdrift/kumiko-bundled-features/template-resolver/web";
|
|
2
|
+
import type { ReactNode } from "react";
|
|
3
|
+
import type { MailMetaField } from "./mail-footer.js";
|
|
4
|
+
import type { MailDraft } from "./mail-types.js";
|
|
5
|
+
import type { InsertTargets } from "./use-insert-targets.js";
|
|
6
|
+
export type EditorVocabulary = {
|
|
7
|
+
readonly names: readonly string[];
|
|
8
|
+
readonly labels: Readonly<Record<string, string>>;
|
|
9
|
+
readonly system: readonly string[];
|
|
10
|
+
};
|
|
11
|
+
export declare function MailEditCenter({ draft, vocabulary, resourcePreviews, insert, readOnly, fieldErrors, onChange, }: {
|
|
12
|
+
readonly draft: MailDraft;
|
|
13
|
+
readonly vocabulary: EditorVocabulary;
|
|
14
|
+
readonly resourcePreviews: Readonly<Record<string, DocumentResourcePreview>>;
|
|
15
|
+
readonly insert: InsertTargets;
|
|
16
|
+
readonly readOnly: boolean;
|
|
17
|
+
readonly fieldErrors: Partial<Record<MailMetaField, string>>;
|
|
18
|
+
readonly onChange: (patch: Partial<MailDraft>) => void;
|
|
19
|
+
}): ReactNode;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
// @runtime client
|
|
3
|
+
import { DocumentContentEditor, MAIL_DOCUMENT_BLOCKS, PlaceholderLineEditor, } from "@cosmicdrift/kumiko-bundled-features/template-resolver/web";
|
|
4
|
+
import { useTranslation } from "@cosmicdrift/kumiko-renderer";
|
|
5
|
+
const renderMailPaper = (editable) => (_jsx("div", { className: "mx-auto w-full max-w-[600px] bg-card px-10 py-7 shadow-md", children: editable }));
|
|
6
|
+
function MetaLine({ label, testId, error, onFocusCapture, children, }) {
|
|
7
|
+
return (_jsxs("div", { className: "grid min-h-10 grid-cols-[84px_minmax(0,1fr)] items-center border-b bg-background px-4", "data-testid": testId, onFocusCapture: onFocusCapture, children: [_jsx("span", { className: "text-xs text-muted-foreground", children: label }), _jsxs("div", { className: "flex min-w-0 items-center gap-3", children: [_jsx("div", { className: "min-w-0 flex-1", children: children }), error !== undefined && (_jsx("span", { className: "shrink-0 text-xs text-status-bad", role: "alert", children: error }))] })] }));
|
|
8
|
+
}
|
|
9
|
+
export function MailEditCenter({ draft, vocabulary, resourcePreviews, insert, readOnly, fieldErrors, onChange, }) {
|
|
10
|
+
const t = useTranslation();
|
|
11
|
+
return (_jsxs("div", { className: "flex min-h-0 min-w-0 flex-1 flex-col", "data-testid": "mail-editor-center", children: [_jsx(MetaLine, { label: t("documentDesigner.mail.meta.subject"), testId: "mail-meta-subject", error: fieldErrors.subject, onFocusCapture: insert.markFocused("subject"), children: _jsx(PlaceholderLineEditor, { id: "mail-subject", value: draft.subject, onChange: (subject) => onChange({ subject }), variableLabels: vocabulary.labels, systemVariables: vocabulary.system, readOnly: readOnly, flat: true, onEditorReady: insert.register("subject"), "aria-label": t("documentDesigner.mail.meta.subject") }) }), _jsx(MetaLine, { label: t("documentDesigner.mail.meta.previewText"), testId: "mail-meta-preview-text", error: fieldErrors.previewText, onFocusCapture: insert.markFocused("previewText"), children: _jsx(PlaceholderLineEditor, { id: "mail-preview-text", value: draft.previewText, onChange: (previewText) => onChange({ previewText }), variableLabels: vocabulary.labels, systemVariables: vocabulary.system, readOnly: readOnly, flat: true, onEditorReady: insert.register("previewText"), "aria-label": t("documentDesigner.mail.meta.previewText") }) }), _jsx("div", { className: "flex min-h-0 flex-1 flex-col", onFocusCapture: insert.markFocused("body"), "data-testid": "mail-content", children: _jsx(DocumentContentEditor, { id: "mail-body", value: draft.body, onChange: (body) => onChange({ body }), variables: vocabulary.names, readOnly: readOnly, blocks: MAIL_DOCUMENT_BLOCKS, variableLabels: vocabulary.labels, systemVariables: vocabulary.system, resourcePreviews: resourcePreviews, chrome: "canvas", renderPaper: renderMailPaper, onEditorReady: insert.register("body") }) })] }));
|
|
12
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
import type { MailValidation, TemplateRowView } from "./mail-types.js";
|
|
3
|
+
export type MailEditorBodyProps = {
|
|
4
|
+
readonly slug: string;
|
|
5
|
+
readonly locale: string;
|
|
6
|
+
readonly locales: readonly string[];
|
|
7
|
+
readonly title: string;
|
|
8
|
+
readonly isSystemAdmin: boolean;
|
|
9
|
+
readonly row: TemplateRowView | null;
|
|
10
|
+
readonly systemVersion: number | null;
|
|
11
|
+
readonly fallbackLocale: string | null;
|
|
12
|
+
readonly onLocaleChange: (locale: string) => void;
|
|
13
|
+
readonly onReload: () => Promise<void>;
|
|
14
|
+
readonly onRefreshRow: () => Promise<void>;
|
|
15
|
+
};
|
|
16
|
+
export declare function placeholderNames(validation: MailValidation | null): string[];
|
|
17
|
+
export declare function MailEditorBody(props: MailEditorBodyProps): ReactNode;
|