@cosmicdrift/kumiko-document-designer 0.387.0 → 0.388.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.
@@ -1,3 +1,4 @@
1
+ import { TemplateResolverHandlers } from "@cosmicdrift/kumiko-bundled-features/template-resolver";
1
2
  import { i18nKey, } from "@cosmicdrift/kumiko-framework/engine";
2
3
  import { DocumentDesignerCells, TEMPLATE_EDITOR_SCREENS, } from "./web/constants.js";
3
4
  function cell(component) {
@@ -90,11 +91,10 @@ const EDITABLE_KINDS = [
90
91
  "mail-layout",
91
92
  "pdf-layout",
92
93
  ];
93
- const RESET_TO_SYSTEM_HANDLER = "template-resolver:write:reset-to-system";
94
94
  const RESET_LOCKED_OVERRIDE_ACTION = {
95
95
  id: "reset-locked-override",
96
96
  label: i18nKey("documentDesigner.action.resetLockedOverride"),
97
- handler: RESET_TO_SYSTEM_HANDLER,
97
+ handler: TemplateResolverHandlers.resetToSystem,
98
98
  payload: { map: { id: "lockedOverrideId" } },
99
99
  confirm: i18nKey("documentDesigner.action.resetLockedOverride.confirm"),
100
100
  confirmLabel: i18nKey("documentDesigner.action.resetLockedOverride.confirmLabel"),
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",
@@ -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.",
@@ -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
  }
@@ -6,7 +6,7 @@ import { EditorHeader, EditorLocaleSwitch, EditorModeSwitch, originOf, } from ".
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,7 @@ export function MailEditorBody(props) {
83
83
  locale,
84
84
  scope: isSystemAdmin ? "system" : "tenant",
85
85
  }).data;
86
+ const pickedVariables = pickDeclaredVariables(picked, validation?.variables);
86
87
  const update = (patch) => {
87
88
  const next = { ...draft, ...patch };
88
89
  setDraft(next);
@@ -153,7 +154,7 @@ export function MailEditorBody(props) {
153
154
  variableSchema,
154
155
  includeAttachments,
155
156
  prefixSubject,
156
- ...(picked !== null && { variables: picked }),
157
+ ...(pickedVariables !== null && { variables: pickedVariables }),
157
158
  });
158
159
  };
159
160
  // Nothing is written yet in a missing language, so no placeholder may read as missing.
@@ -185,7 +186,7 @@ export function MailEditorBody(props) {
185
186
  content: settled.body,
186
187
  meta: buildMeta(settled),
187
188
  variableSchema,
188
- ...(picked !== null && { variables: picked }),
189
+ ...(pickedVariables !== null && { variables: pickedVariables }),
189
190
  }, picked: picked, onPick: setPicked, validation: validation ?? null, slug: slug, locale: locale, fromName: settled.senderName, own: own, origin: origin, controls: controls, testMail: {
190
191
  availability: testMail.availability,
191
192
  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.388.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.388.0",
24
+ "@cosmicdrift/kumiko-framework": "0.388.0",
25
+ "@cosmicdrift/kumiko-renderer": "0.388.0",
26
+ "@cosmicdrift/kumiko-renderer-puppeteer-client": "0.388.0",
27
+ "@cosmicdrift/kumiko-types": "0.388.0",
28
28
  "react": "^19.2.6"
29
29
  },
30
30
  "devDependencies": {
package/src/changes.json CHANGED
@@ -1,4 +1,22 @@
1
1
  [
2
+ {
3
+ "version": "0.388.0",
4
+ "type": "fix",
5
+ "title": "Picked preview values are filtered to declared variables",
6
+ "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."
7
+ },
8
+ {
9
+ "version": "0.388.0",
10
+ "type": "fix",
11
+ "title": "Singular tenant counts in the override texts",
12
+ "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."
13
+ },
14
+ {
15
+ "version": "0.388.0",
16
+ "type": "fix",
17
+ "title": "Seeded layout titles show translated in the PDF editor and mail rail",
18
+ "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."
19
+ },
2
20
  {
3
21
  "version": "0.384.0",
4
22
  "type": "improvement",