@cosmicdrift/kumiko-document-designer 0.387.0 → 0.389.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/feature.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { access, defineFeature, i18nKey, } from "@cosmicdrift/kumiko-framework/engine";
2
2
  import { TEMPLATE_FACETS, templateColumns, templateRowActions } from "./list-definition.js";
3
- import { DOCUMENT_DESIGNER_FEATURE, LAYOUT_EDITOR_SCREEN_ID, MAIL_EDITOR_SCREEN_ID, PDF_TEMPLATE_EDITOR_SCREEN_ID, PLATFORM_TEMPLATES_NAV_ID, PLATFORM_TEMPLATES_SCREEN_ID, TEMPLATES_NAV_ID, TEMPLATES_SCREEN_ID, } from "./web/constants.js";
3
+ import { DOCUMENT_DESIGNER_FEATURE, LAYOUT_EDITOR_SCREEN_ID, MAIL_EDITOR_SCREEN_ID, PDF_TEMPLATE_EDITOR_SCREEN_ID, PLATFORM_LAYOUT_EDITOR_SCREEN_ID, PLATFORM_TEMPLATES_NAV_ID, PLATFORM_TEMPLATES_SCREEN_ID, TEMPLATES_NAV_ID, TEMPLATES_SCREEN_ID, } from "./web/constants.js";
4
4
  import { DOCUMENT_DESIGNER_I18N } from "./web/i18n.js";
5
5
  const OVERVIEW_QUERY = "template-resolver:query:overview";
6
6
  export function createDocumentDesignerFeature(options = {}) {
@@ -20,7 +20,7 @@ export function createDocumentDesignerFeature(options = {}) {
20
20
  query: OVERVIEW_QUERY,
21
21
  columns: templateColumns({ platform: false }),
22
22
  facets: [...TEMPLATE_FACETS],
23
- rowActions: templateRowActions(),
23
+ rowActions: templateRowActions({ platform: false }),
24
24
  searchable: true,
25
25
  searchPlaceholder: i18nKey("documentDesigner.search"),
26
26
  pagination: "pages",
@@ -41,7 +41,7 @@ export function createDocumentDesignerFeature(options = {}) {
41
41
  query: OVERVIEW_QUERY,
42
42
  columns: templateColumns({ platform: true }),
43
43
  facets: [...TEMPLATE_FACETS],
44
- rowActions: templateRowActions(),
44
+ rowActions: templateRowActions({ platform: true }),
45
45
  searchable: true,
46
46
  searchPlaceholder: i18nKey("documentDesigner.search"),
47
47
  pagination: "pages",
@@ -64,6 +64,14 @@ export function createDocumentDesignerFeature(options = {}) {
64
64
  dormant: true,
65
65
  access: { roles: access.roles(...editorRoles) },
66
66
  });
67
+ r.screen({
68
+ id: PLATFORM_LAYOUT_EDITOR_SCREEN_ID,
69
+ type: "custom",
70
+ renderer: { react: { __component: PLATFORM_LAYOUT_EDITOR_SCREEN_ID } },
71
+ description: "documentDesigner.screen.layoutEditor.description",
72
+ dormant: true,
73
+ access: { roles: access.systemAdmin },
74
+ });
67
75
  r.screen({
68
76
  id: PDF_TEMPLATE_EDITOR_SCREEN_ID,
69
77
  type: "custom",
package/dist/index.d.ts CHANGED
@@ -1,2 +1,2 @@
1
1
  export { createDocumentDesignerFeature, type DocumentDesignerOptions } from "./feature.ts";
2
- 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 "./web/constants.ts";
2
+ export { DEFAULT_TEMPLATE_LOCALES, DOCUMENT_DESIGNER_FEATURE, LAYOUT_EDITOR_SCREEN_ID, MAIL_EDITOR_SCREEN_ID, PDF_TEMPLATE_EDITOR_SCREEN_ID, PLATFORM_LAYOUT_EDITOR_SCREEN_ID, PLATFORM_TEMPLATES_NAV_ID, PLATFORM_TEMPLATES_SCREEN_ID, TEMPLATE_EDITOR_SCREENS, TEMPLATES_NAV_ID, TEMPLATES_SCREEN_ID, type TemplateEditorKind, } from "./web/constants.ts";
package/dist/index.js CHANGED
@@ -1,2 +1,2 @@
1
1
  export { createDocumentDesignerFeature } from "./feature.js";
2
- 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 "./web/constants.js";
2
+ export { DEFAULT_TEMPLATE_LOCALES, DOCUMENT_DESIGNER_FEATURE, LAYOUT_EDITOR_SCREEN_ID, MAIL_EDITOR_SCREEN_ID, PDF_TEMPLATE_EDITOR_SCREEN_ID, PLATFORM_LAYOUT_EDITOR_SCREEN_ID, PLATFORM_TEMPLATES_NAV_ID, PLATFORM_TEMPLATES_SCREEN_ID, TEMPLATE_EDITOR_SCREENS, TEMPLATES_NAV_ID, TEMPLATES_SCREEN_ID, } from "./web/constants.js";
@@ -3,4 +3,6 @@ export declare function templateColumns(options: {
3
3
  readonly platform: boolean;
4
4
  }): ListColumnSpec[];
5
5
  export declare const TEMPLATE_FACETS: readonly ListFacetSpec[];
6
- export declare function templateRowActions(): RowAction[];
6
+ export declare function templateRowActions(options: {
7
+ readonly platform: boolean;
8
+ }): RowAction[];
@@ -1,5 +1,6 @@
1
+ import { TemplateResolverHandlers } from "@cosmicdrift/kumiko-bundled-features/template-resolver";
1
2
  import { i18nKey, } from "@cosmicdrift/kumiko-framework/engine";
2
- import { DocumentDesignerCells, TEMPLATE_EDITOR_SCREENS, } from "./web/constants.js";
3
+ import { DocumentDesignerCells, PLATFORM_LAYOUT_EDITOR_SCREEN_ID, TEMPLATE_EDITOR_SCREENS, } from "./web/constants.js";
3
4
  function cell(component) {
4
5
  return { react: { __component: component } };
5
6
  }
@@ -84,26 +85,32 @@ export const TEMPLATE_FACETS = [
84
85
  ],
85
86
  },
86
87
  ];
88
+ function isLayoutKind(kind) {
89
+ return kind === "mail-layout" || kind === "pdf-layout";
90
+ }
87
91
  const EDITABLE_KINDS = [
88
92
  "mail",
89
93
  "document-pdf",
90
94
  "mail-layout",
91
95
  "pdf-layout",
92
96
  ];
93
- const RESET_TO_SYSTEM_HANDLER = "template-resolver:write:reset-to-system";
94
97
  const RESET_LOCKED_OVERRIDE_ACTION = {
95
98
  id: "reset-locked-override",
96
99
  label: i18nKey("documentDesigner.action.resetLockedOverride"),
97
- handler: RESET_TO_SYSTEM_HANDLER,
100
+ handler: TemplateResolverHandlers.resetToSystem,
98
101
  payload: { map: { id: "lockedOverrideId" } },
99
102
  confirm: i18nKey("documentDesigner.action.resetLockedOverride.confirm"),
100
103
  confirmLabel: i18nKey("documentDesigner.action.resetLockedOverride.confirmLabel"),
101
104
  style: "danger",
102
105
  visible: { field: "canResetLockedOverride", eq: true },
103
106
  };
104
- export function templateRowActions() {
107
+ // The screen the row is opened from fixes the layout scope: the platform overview edits the system
108
+ // layout, the tenant overview always the tenant's own, even for a SystemAdmin on a system-origin row.
109
+ export function templateRowActions(options) {
105
110
  const editActions = EDITABLE_KINDS.flatMap((kind) => {
106
- const screen = TEMPLATE_EDITOR_SCREENS[kind];
111
+ const screen = options.platform && isLayoutKind(kind)
112
+ ? PLATFORM_LAYOUT_EDITOR_SCREEN_ID
113
+ : TEMPLATE_EDITOR_SCREENS[kind];
107
114
  if (screen === undefined)
108
115
  return [];
109
116
  return [
package/dist/web/cells.js CHANGED
@@ -11,7 +11,7 @@ function withRow(row, render) {
11
11
  }
12
12
  export function TitleCell({ row }) {
13
13
  const t = useTranslation();
14
- return withRow(row, (r) => (_jsxs("div", { children: [_jsx("div", { className: "max-w-48 whitespace-normal font-medium", children: templateTitle(t, r) }), _jsx("span", { className: "block max-w-40 truncate font-mono text-xs text-muted-foreground", title: r.slug, children: r.slug })] })));
14
+ return withRow(row, (r) => (_jsxs("div", { children: [_jsx("div", { className: "max-w-48 whitespace-normal font-medium [overflow-wrap:anywhere]", children: templateTitle(t, r) }), _jsx("span", { className: "block max-w-28 truncate font-mono text-xs text-muted-foreground", title: r.slug, children: r.slug })] })));
15
15
  }
16
16
  const KIND_KEY = {
17
17
  mail: "documentDesigner.kind.mail",
@@ -4,14 +4,17 @@ import { mergeTranslations, translationsByLocaleFromKeys, useNav, } from "@cosmi
4
4
  import { LoadingState } from "@cosmicdrift/kumiko-renderer/web";
5
5
  import { lazy, Suspense } from "react";
6
6
  import { createLocalesCell, KindCell, ModifiedCell, OriginCell, OverriddenByCell, StatusCell, TitleCell, UsedByCell, } from "./cells.js";
7
- import { DEFAULT_TEMPLATE_LOCALES, DOCUMENT_DESIGNER_FEATURE, DocumentDesignerCells, LAYOUT_EDITOR_SCREEN_ID, MAIL_EDITOR_SCREEN_ID, PDF_TEMPLATE_EDITOR_SCREEN_ID, } from "./constants.js";
7
+ import { DEFAULT_TEMPLATE_LOCALES, DOCUMENT_DESIGNER_FEATURE, DocumentDesignerCells, LAYOUT_EDITOR_SCREEN_ID, MAIL_EDITOR_SCREEN_ID, PDF_TEMPLATE_EDITOR_SCREEN_ID, PLATFORM_LAYOUT_EDITOR_SCREEN_ID, } from "./constants.js";
8
8
  import { documentDesignerTranslations } from "./default-translations.js";
9
9
  import { DOCUMENT_DESIGNER_I18N } from "./i18n.js";
10
10
  // The editors pull tiptap, MJML preview and the page canvas; load them on first mount.
11
11
  const LazyLayoutEditor = lazy(() => import("./layout-editor.js").then((module) => ({ default: module.LayoutEditor })));
12
12
  const LazyPdfTemplateEditor = lazy(() => import("./pdf-template-editor.js").then((module) => ({ default: module.PdfTemplateEditor })));
13
13
  function LayoutEditorScreen() {
14
- return (_jsx(Suspense, { fallback: _jsx(LoadingState, {}), children: _jsx(LazyLayoutEditor, {}) }));
14
+ return (_jsx(Suspense, { fallback: _jsx(LoadingState, {}), children: _jsx(LazyLayoutEditor, { scope: "tenant" }) }));
15
+ }
16
+ function PlatformLayoutEditorScreen() {
17
+ return (_jsx(Suspense, { fallback: _jsx(LoadingState, {}), children: _jsx(LazyLayoutEditor, { scope: "system" }) }));
15
18
  }
16
19
  function createLazyMailEditor(appLocales) {
17
20
  const LazyMailEditor = lazy(() => import("./mail-editor.js").then((module) => ({
@@ -47,6 +50,7 @@ export function documentDesignerClient(options = {}) {
47
50
  },
48
51
  components: {
49
52
  [LAYOUT_EDITOR_SCREEN_ID]: LayoutEditorScreen,
53
+ [PLATFORM_LAYOUT_EDITOR_SCREEN_ID]: PlatformLayoutEditorScreen,
50
54
  [PDF_TEMPLATE_EDITOR_SCREEN_ID]: PdfTemplateEditorScreen,
51
55
  [MAIL_EDITOR_SCREEN_ID]: createLazyMailEditor(options.appLocales ?? DEFAULT_TEMPLATE_LOCALES),
52
56
  },
@@ -2,6 +2,7 @@ export declare const DOCUMENT_DESIGNER_FEATURE: "document-designer";
2
2
  export declare const TEMPLATES_SCREEN_ID: "templates";
3
3
  export declare const PLATFORM_TEMPLATES_SCREEN_ID: "platform-templates";
4
4
  export declare const LAYOUT_EDITOR_SCREEN_ID: "layout-editor";
5
+ export declare const PLATFORM_LAYOUT_EDITOR_SCREEN_ID: "platform-layout-editor";
5
6
  export declare const PDF_TEMPLATE_EDITOR_SCREEN_ID: "pdf-template-editor";
6
7
  export declare const MAIL_EDITOR_SCREEN_ID: "mail-editor";
7
8
  export declare const TEMPLATES_NAV_ID: "templates";
@@ -3,6 +3,7 @@ export const DOCUMENT_DESIGNER_FEATURE = "document-designer";
3
3
  export const TEMPLATES_SCREEN_ID = "templates";
4
4
  export const PLATFORM_TEMPLATES_SCREEN_ID = "platform-templates";
5
5
  export const LAYOUT_EDITOR_SCREEN_ID = "layout-editor";
6
+ export const PLATFORM_LAYOUT_EDITOR_SCREEN_ID = "platform-layout-editor";
6
7
  export const PDF_TEMPLATE_EDITOR_SCREEN_ID = "pdf-template-editor";
7
8
  export const MAIL_EDITOR_SCREEN_ID = "mail-editor";
8
9
  export const TEMPLATES_NAV_ID = "templates";
@@ -13,8 +13,14 @@ export const documentDesignerTranslations = {
13
13
  "documentDesigner.localeDialog.title": "Unsaved changes",
14
14
  "documentDesigner.notFound": 'Template "{slug}" was not found.',
15
15
  "documentDesigner.originBar.export": "Export as bundle",
16
- "documentDesigner.originBar.noTotal": "System template for all tenants. {count} tenants have their own version and do not receive changes made here.",
17
- "documentDesigner.originBar.withTotal": "System template for all tenants. {count} of {total} tenants have their own version and do not receive changes made here.",
16
+ "documentDesigner.originBar.noTotal": {
17
+ one: "System template for all tenants. {count} tenant has its own version and does not receive changes made here.",
18
+ other: "System template for all tenants. {count} tenants have their own version and do not receive changes made here.",
19
+ },
20
+ "documentDesigner.originBar.withTotal": {
21
+ one: "System template for all tenants. {count} of {total} tenants has its own version and does not receive changes made here.",
22
+ other: "System template for all tenants. {count} of {total} tenants have their own version and do not receive changes made here.",
23
+ },
18
24
  "documentDesigner.originBar.showOverrides": "Show tenant customisations",
19
25
  "documentDesigner.overrides.action.diff": "Show difference",
20
26
  "documentDesigner.overrides.action.open": "Open",
@@ -27,7 +33,10 @@ export const documentDesignerTranslations = {
27
33
  "documentDesigner.overrides.empty": "No tenant has customised this template. Changes here apply to everyone.",
28
34
  "documentDesigner.overrides.status.active": "Customised",
29
35
  "documentDesigner.overrides.status.draft": "Draft",
30
- "documentDesigner.overrides.summary": "{count} tenants have their own version.",
36
+ "documentDesigner.overrides.summary": {
37
+ one: "{count} tenant has its own version.",
38
+ other: "{count} tenants have their own version.",
39
+ },
31
40
  "documentDesigner.overrides.title": "Tenant customisations",
32
41
  "documentDesigner.overrides.usedBy": "Used by",
33
42
  "documentDesigner.overrides.usedByEmpty": "No registered notification uses this template directly.",
@@ -85,8 +94,14 @@ export const documentDesignerTranslations = {
85
94
  "documentDesigner.localeDialog.title": "Ungespeicherte Änderungen",
86
95
  "documentDesigner.notFound": 'Die Vorlage "{slug}" wurde nicht gefunden.',
87
96
  "documentDesigner.originBar.export": "Als Bundle exportieren",
88
- "documentDesigner.originBar.noTotal": "System-Vorlage für alle Mandanten. {count} Mandanten haben eine eigene Fassung und bekommen Änderungen hier nicht.",
89
- "documentDesigner.originBar.withTotal": "System-Vorlage für alle Mandanten. {count} von {total} Mandanten haben eine eigene Fassung und bekommen Änderungen hier nicht.",
97
+ "documentDesigner.originBar.noTotal": {
98
+ one: "System-Vorlage für alle Mandanten. {count} Mandant hat eine eigene Fassung und bekommt Änderungen hier nicht.",
99
+ other: "System-Vorlage für alle Mandanten. {count} Mandanten haben eine eigene Fassung und bekommen Änderungen hier nicht.",
100
+ },
101
+ "documentDesigner.originBar.withTotal": {
102
+ one: "System-Vorlage für alle Mandanten. {count} von {total} Mandanten hat eine eigene Fassung und bekommt Änderungen hier nicht.",
103
+ other: "System-Vorlage für alle Mandanten. {count} von {total} Mandanten haben eine eigene Fassung und bekommen Änderungen hier nicht.",
104
+ },
90
105
  "documentDesigner.originBar.showOverrides": "Anpassungen der Mandanten anzeigen",
91
106
  "documentDesigner.overrides.action.diff": "Unterschied anzeigen",
92
107
  "documentDesigner.overrides.action.open": "Öffnen",
@@ -99,7 +114,10 @@ export const documentDesignerTranslations = {
99
114
  "documentDesigner.overrides.empty": "Kein Mandant hat diese Vorlage angepasst. Änderungen hier gelten für alle.",
100
115
  "documentDesigner.overrides.status.active": "Angepasst",
101
116
  "documentDesigner.overrides.status.draft": "Entwurf",
102
- "documentDesigner.overrides.summary": "{count} Mandanten haben eine eigene Fassung.",
117
+ "documentDesigner.overrides.summary": {
118
+ one: "{count} Mandant hat eine eigene Fassung.",
119
+ other: "{count} Mandanten haben eine eigene Fassung.",
120
+ },
103
121
  "documentDesigner.overrides.title": "Anpassungen der Mandanten",
104
122
  "documentDesigner.overrides.usedBy": "Wird verwendet von",
105
123
  "documentDesigner.overrides.usedByEmpty": "Keine registrierte Benachrichtigung nutzt diese Vorlage direkt.",
@@ -157,8 +175,14 @@ export const documentDesignerTranslations = {
157
175
  "documentDesigner.localeDialog.title": "Cambios sin guardar",
158
176
  "documentDesigner.notFound": "No se encontró la plantilla «{slug}».",
159
177
  "documentDesigner.originBar.export": "Exportar como paquete",
160
- "documentDesigner.originBar.noTotal": "Plantilla del sistema para todas las organizaciones. {count} organizaciones tienen su propia versión y no reciben los cambios que hagas aquí.",
161
- "documentDesigner.originBar.withTotal": "Plantilla del sistema para todas las organizaciones. {count} de {total} organizaciones tienen su propia versión y no reciben los cambios que hagas aquí.",
178
+ "documentDesigner.originBar.noTotal": {
179
+ one: "Plantilla del sistema para todas las organizaciones. {count} organización tiene su propia versión y no recibe los cambios que hagas aquí.",
180
+ other: "Plantilla del sistema para todas las organizaciones. {count} organizaciones tienen su propia versión y no reciben los cambios que hagas aquí.",
181
+ },
182
+ "documentDesigner.originBar.withTotal": {
183
+ one: "Plantilla del sistema para todas las organizaciones. {count} de {total} organizaciones tiene su propia versión y no recibe los cambios que hagas aquí.",
184
+ other: "Plantilla del sistema para todas las organizaciones. {count} de {total} organizaciones tienen su propia versión y no reciben los cambios que hagas aquí.",
185
+ },
162
186
  "documentDesigner.originBar.showOverrides": "Ver personalizaciones de las organizaciones",
163
187
  "documentDesigner.overrides.action.diff": "Ver diferencias",
164
188
  "documentDesigner.overrides.action.open": "Abrir",
@@ -171,7 +195,10 @@ export const documentDesignerTranslations = {
171
195
  "documentDesigner.overrides.empty": "Ninguna organización ha personalizado esta plantilla. Los cambios que hagas aquí se aplican a todas.",
172
196
  "documentDesigner.overrides.status.active": "Personalizada",
173
197
  "documentDesigner.overrides.status.draft": "Borrador",
174
- "documentDesigner.overrides.summary": "{count} organizaciones tienen su propia versión.",
198
+ "documentDesigner.overrides.summary": {
199
+ one: "{count} organización tiene su propia versión.",
200
+ other: "{count} organizaciones tienen su propia versión.",
201
+ },
175
202
  "documentDesigner.overrides.title": "Personalizaciones de las organizaciones",
176
203
  "documentDesigner.overrides.usedBy": "Se usa en",
177
204
  "documentDesigner.overrides.usedByEmpty": "Ninguna notificación registrada usa esta plantilla directamente.",
package/dist/web/i18n.js CHANGED
@@ -3,6 +3,7 @@ export const DOCUMENT_DESIGNER_I18N = {
3
3
  "screen:templates.title": { en: "Templates", de: "Vorlagen" },
4
4
  "screen:platform-templates.title": { en: "System templates", de: "System-Vorlagen" },
5
5
  "screen:layout-editor.title": { en: "Layout", de: "Layout" },
6
+ "screen:platform-layout-editor.title": { en: "System layout", de: "System-Layout" },
6
7
  "screen:pdf-template-editor.title": { en: "PDF template", de: "PDF-Vorlage" },
7
8
  "document-designer:nav.templates": { en: "Templates", de: "Vorlagen" },
8
9
  "document-designer:nav.platformTemplates": { en: "System templates", de: "System-Vorlagen" },
@@ -1,2 +1,4 @@
1
1
  import { type ReactNode } from "react";
2
- export declare function LayoutEditor(): ReactNode;
2
+ export declare function LayoutEditor({ scope, }: {
3
+ readonly scope?: "tenant" | "system";
4
+ }): ReactNode;
@@ -11,7 +11,11 @@ function parseLayoutKind(raw) {
11
11
  return raw === "pdf-layout" ? "pdf-layout" : "mail-layout";
12
12
  }
13
13
  function LayoutEditorLoader({ kind, slug, isSystemAdmin, onKindChange, }) {
14
- const source = useQuery(TemplateResolverQueries.layoutSource, { kind, slug });
14
+ const source = useQuery(TemplateResolverQueries.layoutSource, {
15
+ kind,
16
+ slug,
17
+ scope: isSystemAdmin ? "system" : "tenant",
18
+ });
15
19
  const overview = useQuery(TemplateResolverQueries.overview, {
16
20
  limit: 200,
17
21
  layoutSlug: slug,
@@ -38,10 +42,10 @@ function LayoutEditorLoader({ kind, slug, isSystemAdmin, onKindChange, }) {
38
42
  const bodyKey = [kind, slug, data.tenant?.id, data.tenant?.version, data.system?.version].join("|");
39
43
  return (_jsx(LayoutEditorBody, { kind: kind, slug: slug, isSystemAdmin: isSystemAdmin, source: data, overviewRows: rows, usedByCount: overview.data?.total ?? rows.length, overrides: isSystemAdmin ? overrides.data : null, onKindChange: onKindChange, onReload: reload }, bodyKey));
40
44
  }
41
- export function LayoutEditor() {
45
+ export function LayoutEditor({ scope = "tenant", }) {
42
46
  const nav = useNav();
43
47
  const roles = useUserRoles();
44
48
  const [kind, setKind] = useState(() => parseLayoutKind(nav.searchParams["kind"]));
45
49
  const slug = nav.searchParams["slug"] ?? DEFAULT_LAYOUT_SLUG;
46
- return (_jsx(LayoutEditorLoader, { kind: kind, slug: slug, isSystemAdmin: roles?.includes("SystemAdmin") === true, onKindChange: setKind }, kind));
50
+ return (_jsx(LayoutEditorLoader, { kind: kind, slug: slug, isSystemAdmin: scope === "system" && roles?.includes("SystemAdmin") === true, onKindChange: setKind }, kind));
47
51
  }
@@ -1,8 +1,8 @@
1
- // Mirrors the mail-layout meta of template-resolver (template-meta.ts); the server parses it again on every write and render.
2
- export const LAYOUT_WIDTHS = [560, 600, 640];
3
- export const LAYOUT_FONTS = ["sans", "serif"];
4
- export const LAYOUT_ACCENT_LINES = ["branding", "none"];
5
- export const LAYOUT_BUTTON_STYLES = ["filled-rounded", "filled-square", "outline"];
1
+ import { HEX_COLOR_PATTERN, MAIL_LAYOUT_ACCENT_LINES, MAIL_LAYOUT_BUTTON_STYLES, MAIL_LAYOUT_FONTS, MAIL_LAYOUT_WIDTHS, } from "@cosmicdrift/kumiko-bundled-features/template-resolver/web";
2
+ export const LAYOUT_WIDTHS = MAIL_LAYOUT_WIDTHS;
3
+ export const LAYOUT_FONTS = MAIL_LAYOUT_FONTS;
4
+ export const LAYOUT_ACCENT_LINES = MAIL_LAYOUT_ACCENT_LINES;
5
+ export const LAYOUT_BUTTON_STYLES = MAIL_LAYOUT_BUTTON_STYLES;
6
6
  export const DEFAULT_LAYOUT_APPEARANCE = {
7
7
  width: 600,
8
8
  font: "sans",
@@ -10,9 +10,8 @@ export const DEFAULT_LAYOUT_APPEARANCE = {
10
10
  accentLine: "none",
11
11
  buttonStyle: "filled-rounded",
12
12
  };
13
- const HEX_COLOR = /^#[0-9a-fA-F]{6}$/;
14
13
  export function isHexColor(value) {
15
- return HEX_COLOR.test(value);
14
+ return HEX_COLOR_PATTERN.test(value);
16
15
  }
17
16
  export function appearanceOf(meta) {
18
17
  return { ...DEFAULT_LAYOUT_APPEARANCE, ...meta };
@@ -1,4 +1,4 @@
1
- import type { LinkedResourceView, MailDraft, MailVariable, PlaceholderType, TemplateRowView } from "./mail-types.ts";
1
+ import type { LinkedResourceView, MailDraft, MailVariable, PickedValues, PlaceholderType, TemplateRowView } from "./mail-types.ts";
2
2
  export declare const EMPTY_DRAFT: MailDraft;
3
3
  export declare const PLACEHOLDER_SYMBOL: Readonly<Record<PlaceholderType, string>>;
4
4
  export declare const LAYOUT_OWNED_PLACEHOLDERS: ReadonlySet<string>;
@@ -8,6 +8,7 @@ export declare function systemPlaceholderLabel(t: Translate, name: string): stri
8
8
  export declare function labelPlaceholders(line: string, variableLabels: Readonly<Record<string, string>>): string;
9
9
  export declare function systemLabelMap(t: Translate, names: readonly string[]): Readonly<Record<string, string>>;
10
10
  export declare function parseJsonRecord(value: unknown): Readonly<Record<string, unknown>>;
11
+ export declare function pickDeclaredVariables(picked: PickedValues | null, declared: Readonly<Record<string, MailVariable>> | undefined): PickedValues | null;
11
12
  export declare function loadedMailOf(row: TemplateRowView | null): MailDraft;
12
13
  export declare function buildMeta(draft: MailDraft): Record<string, unknown>;
13
14
  export declare function isDraftDirty(current: MailDraft, saved: MailDraft): boolean;
@@ -62,6 +62,12 @@ export function parseJsonRecord(value) {
62
62
  return {};
63
63
  }
64
64
  }
65
+ // The server rejects any variable the template does not declare, so a stale pick must not travel along.
66
+ export function pickDeclaredVariables(picked, declared) {
67
+ if (picked === null || declared === undefined)
68
+ return null;
69
+ return Object.fromEntries(Object.entries(picked).filter(([name]) => Object.hasOwn(declared, name)));
70
+ }
65
71
  function categoryOf(value) {
66
72
  return value === "transactional" || value === "marketing" ? value : "";
67
73
  }
@@ -1,12 +1,12 @@
1
1
  import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { TemplateResolverHandlers, TemplateResolverQueries, } from "@cosmicdrift/kumiko-bundled-features/template-resolver/web";
3
3
  import { dispatcherErrorText, useMutation, usePrimitives, useQuery, useTranslation, useUnsavedInputSource, } from "@cosmicdrift/kumiko-renderer";
4
- import { useState } from "react";
4
+ import { useEffect, useRef, useState } from "react";
5
5
  import { EditorHeader, EditorLocaleSwitch, EditorModeSwitch, originOf, } from "./editor-header.js";
6
6
  import { EditorBody, EditorFooter, EditorFrame } from "./editor-shell.js";
7
7
  import { fileUrl } from "./file-name.js";
8
8
  import { MailConfirmDialogs } from "./mail-dialogs.js";
9
- import { buildMeta, isDraftDirty, loadedMailOf, parseJsonRecord, parseLinkedResources, systemLabelMap, variableLabelMap, } from "./mail-draft.js";
9
+ import { buildMeta, isDraftDirty, loadedMailOf, parseJsonRecord, parseLinkedResources, pickDeclaredVariables, systemLabelMap, variableLabelMap, } from "./mail-draft.js";
10
10
  import { MailEditCenter } from "./mail-edit-center.js";
11
11
  import { footerMessage, metaFieldMessages } from "./mail-footer.js";
12
12
  import { PlaceholderSection, ResourceSection, SystemPlaceholderSection, } from "./mail-left-rail.js";
@@ -83,6 +83,21 @@ export function MailEditorBody(props) {
83
83
  locale,
84
84
  scope: isSystemAdmin ? "system" : "tenant",
85
85
  }).data;
86
+ const pickedVariables = pickDeclaredVariables(picked, validation?.variables);
87
+ // A pick belongs to the variables it was made for: after the declared set changes it is dropped,
88
+ // so a re-added variable does not revive an old record value.
89
+ const declaredVariables = validation?.variables;
90
+ const declaredVariableNames = declaredVariables === undefined ? null : Object.keys(declaredVariables).sort().join("|");
91
+ const previousDeclaredNames = useRef(declaredVariableNames);
92
+ useEffect(() => {
93
+ if (declaredVariableNames === null)
94
+ return;
95
+ if (previousDeclaredNames.current !== null &&
96
+ previousDeclaredNames.current !== declaredVariableNames) {
97
+ setPicked(null);
98
+ }
99
+ previousDeclaredNames.current = declaredVariableNames;
100
+ }, [declaredVariableNames]);
86
101
  const update = (patch) => {
87
102
  const next = { ...draft, ...patch };
88
103
  setDraft(next);
@@ -153,7 +168,7 @@ export function MailEditorBody(props) {
153
168
  variableSchema,
154
169
  includeAttachments,
155
170
  prefixSubject,
156
- ...(picked !== null && { variables: picked }),
171
+ ...(pickedVariables !== null && { variables: pickedVariables }),
157
172
  });
158
173
  };
159
174
  // Nothing is written yet in a missing language, so no placeholder may read as missing.
@@ -185,7 +200,7 @@ export function MailEditorBody(props) {
185
200
  content: settled.body,
186
201
  meta: buildMeta(settled),
187
202
  variableSchema,
188
- ...(picked !== null && { variables: picked }),
203
+ ...(pickedVariables !== null && { variables: pickedVariables }),
189
204
  }, picked: picked, onPick: setPicked, validation: validation ?? null, slug: slug, locale: locale, fromName: settled.senderName, own: own, origin: origin, controls: controls, testMail: {
190
205
  availability: testMail.availability,
191
206
  pending: testMail.pending,
@@ -1,8 +1,6 @@
1
1
  import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { ConfigQueries } from "@cosmicdrift/kumiko-bundled-features/config/web";
3
- import { useNav, usePrimitives, useQuery, useTranslation } from "@cosmicdrift/kumiko-renderer";
2
+ import { useNav, usePrimitives, useTenantCurrency, useTranslation, } from "@cosmicdrift/kumiko-renderer";
4
3
  import { DOCUMENT_FALLBACK_CURRENCY } from "@cosmicdrift/kumiko-renderer/headless";
5
- import { TENANT_CURRENCY_CONFIG_KEY } from "@cosmicdrift/kumiko-types/fields";
6
4
  import { useState } from "react";
7
5
  import { displayValue } from "./mail-draft.js";
8
6
  import { usePreviewRecordPicker } from "./preview-record-picker.js";
@@ -22,22 +20,16 @@ function checkText(t, check, validation) {
22
20
  const translated = t(check.i18nKey, params);
23
21
  return translated === check.i18nKey ? check.code : translated;
24
22
  }
25
- // Same source and fallback as the renderer's readDocumentFormat, so a bare amount shows
26
- // the currency the rendered mail prints.
27
- function useTenantCurrency() {
28
- const { data, error } = useQuery(ConfigQueries.values, {
29
- keys: [TENANT_CURRENCY_CONFIG_KEY],
30
- });
31
- if (error)
32
- return undefined;
33
- if (data === null)
34
- return undefined;
35
- const value = data[TENANT_CURRENCY_CONFIG_KEY]?.value;
36
- return typeof value === "string" && value !== "" ? value : DOCUMENT_FALLBACK_CURRENCY;
23
+ // Same fallback as the renderer's readDocumentFormat, so a bare amount shows the currency the
24
+ // rendered mail prints, also when the config query fails.
25
+ function useSampleTenantCurrency(validation) {
26
+ const hasMoneyVariable = Object.values(validation?.variables ?? {}).some((variable) => variable.type === "money");
27
+ const resolution = useTenantCurrency(hasMoneyVariable, DOCUMENT_FALLBACK_CURRENCY);
28
+ return resolution.status === "ready" ? resolution.currency : undefined;
37
29
  }
38
30
  function SampleValues({ preview, validation, picked, locale, }) {
39
31
  const t = useTranslation();
40
- const tenantCurrency = useTenantCurrency();
32
+ const tenantCurrency = useSampleTenantCurrency(validation);
41
33
  const values = { ...(preview?.sampleValues ?? {}), ...(picked ?? {}) };
42
34
  const names = Object.keys(validation?.variables ?? values);
43
35
  return (_jsx(_Fragment, { children: names.map((name) => {
@@ -5,7 +5,7 @@ import { isOneOf } from "@cosmicdrift/kumiko-types/type-guards";
5
5
  import { DEFAULT_LAYOUT_SLUG, TENANT_MAIL_SENDING_SCREEN_ID } from "./constants.js";
6
6
  import { labelPlaceholders } from "./mail-draft.js";
7
7
  import { RailField, RailHint, RailRow, RailSection } from "./rail.js";
8
- import { usageParts } from "./used-by-label.js";
8
+ import { templateTitleText, usageParts } from "./used-by-label.js";
9
9
  function useTemplateUsage(slug) {
10
10
  const { data } = useQuery(TemplateResolverQueries.usage, {
11
11
  slug,
@@ -33,7 +33,7 @@ export function layoutOptionLabel(t, layout) {
33
33
  // A layout has no registry title; the slug would read like an id, so name it by kind.
34
34
  const title = layout.title === layout.slug
35
35
  ? t(`documentDesigner.editor.title.${layout.kind === "mail-layout" ? "mail" : "pdf"}`)
36
- : layout.title;
36
+ : templateTitleText(t, layout.title);
37
37
  return t(layout.origin === "tenant"
38
38
  ? "documentDesigner.mail.props.layoutTenant"
39
39
  : "documentDesigner.mail.props.layoutSystem", { title, version: layout.version });
@@ -92,7 +92,7 @@ export type MailDraft = {
92
92
  readonly category: MailCategory;
93
93
  readonly senderName: string;
94
94
  readonly replyTo: string;
95
- /** Meta lines without rail fields yet; carried so a tenant save keeps them. */
95
+ /** Placeholder lines; an empty tone stores the accent default. */
96
96
  readonly headerChip: string;
97
97
  readonly tone: string;
98
98
  readonly footerNote: string;
@@ -23,7 +23,7 @@ import { DEFAULT_LAYOUT_SLUG, DOCUMENT_PDF_KIND, PDF_LAYOUT_KIND, } from "./type
23
23
  import { useFilesCapability } from "./use-files-capability.js";
24
24
  import { useInsertTargets } from "./use-insert-targets.js";
25
25
  import { useOverviewRows } from "./use-overview-rows.js";
26
- import { templateTitle } from "./used-by-label.js";
26
+ import { templateTitle, templateTitleText } from "./used-by-label.js";
27
27
  const SETTLE_MS = 500;
28
28
  const NO_VARIABLES = {};
29
29
  // Only the label of an images variable is needed, for the Images block and its picker.
@@ -282,7 +282,8 @@ export function PdfTemplateEditor({ slug, locale: requestedLocale, onLocaleChang
282
282
  }
283
283
  const canPublish = validation?.canPublish === true;
284
284
  const layoutSlugShown = layout?.slug ?? DEFAULT_LAYOUT_SLUG;
285
- const layoutName = layouts.rows.find((row) => row.slug === layoutSlugShown)?.title ?? layoutSlugShown;
285
+ const layoutTitle = layouts.rows.find((row) => row.slug === layoutSlugShown)?.title;
286
+ const layoutName = layoutTitle === undefined ? layoutSlugShown : templateTitleText(t, layoutTitle);
286
287
  const layoutResourceSrc = (key) => key === "logo" && brandingLogoFile !== "" ? fileUrl(brandingLogoFile) : undefined;
287
288
  const placeholderSource = validation === undefined || validation === null
288
289
  ? null
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cosmicdrift/kumiko-document-designer",
3
- "version": "0.387.0",
3
+ "version": "0.389.0",
4
4
  "description": "Document designer for Kumiko: template overview, layout editor (mail layout, PDF letterhead) and the PDF template editor.",
5
5
  "license": "MIT",
6
6
  "author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
@@ -20,11 +20,11 @@
20
20
  "./web/styles.css": "./src/web/styles.css"
21
21
  },
22
22
  "dependencies": {
23
- "@cosmicdrift/kumiko-bundled-features": "0.387.0",
24
- "@cosmicdrift/kumiko-framework": "0.387.0",
25
- "@cosmicdrift/kumiko-renderer": "0.387.0",
26
- "@cosmicdrift/kumiko-renderer-puppeteer-client": "0.387.0",
27
- "@cosmicdrift/kumiko-types": "0.387.0",
23
+ "@cosmicdrift/kumiko-bundled-features": "0.389.0",
24
+ "@cosmicdrift/kumiko-framework": "0.389.0",
25
+ "@cosmicdrift/kumiko-renderer": "0.389.0",
26
+ "@cosmicdrift/kumiko-renderer-puppeteer-client": "0.389.0",
27
+ "@cosmicdrift/kumiko-types": "0.389.0",
28
28
  "react": "^19.2.6"
29
29
  },
30
30
  "devDependencies": {
package/src/changes.json CHANGED
@@ -1,4 +1,34 @@
1
1
  [
2
+ {
3
+ "version": "0.389.0",
4
+ "type": "fix",
5
+ "title": "layout editor scope follows the entry screen",
6
+ "detail": "The tenant overview opens layout rows in the tenant editor even for a SystemAdmin, so a system-origin row there loads and saves the tenant layout through layout-source with scope tenant and upsert-tenant. The platform overview opens the new platform-layout-editor screen (PLATFORM_LAYOUT_EDITOR_SCREEN_ID) for the system layout. layout-source takes an optional scope and answers 403 to scope system for non-SystemAdmins. templateRowActions now takes { platform }."
7
+ },
8
+ {
9
+ "version": "0.389.0",
10
+ "type": "fix",
11
+ "title": "mail editor resets the record pick when variables change",
12
+ "detail": "The picked record values are cleared as soon as the set of declared variables changes, so a removed or renamed variable no longer leaves a stale pick behind and a re-added variable does not revive an old value."
13
+ },
14
+ {
15
+ "version": "0.388.0",
16
+ "type": "fix",
17
+ "title": "Picked preview values are filtered to declared variables",
18
+ "detail": "A picked record value for a variable the template does not declare no longer travels into the preview or test mail, which the server rejected as an unknown variable."
19
+ },
20
+ {
21
+ "version": "0.388.0",
22
+ "type": "fix",
23
+ "title": "Singular tenant counts in the override texts",
24
+ "detail": "The override summary and the system origin bar use plural forms, so one customising tenant reads \"1 tenant has its own version\" in en, de and es."
25
+ },
26
+ {
27
+ "version": "0.388.0",
28
+ "type": "fix",
29
+ "title": "Seeded layout titles show translated in the PDF editor and mail rail",
30
+ "detail": "A layout seeded with a title i18n key now shows its translated name in the PDF editor header and the mail editor properties rail instead of the raw key."
31
+ },
2
32
  {
3
33
  "version": "0.384.0",
4
34
  "type": "improvement",