@cosmicdrift/kumiko-document-designer 0.371.0 → 0.372.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/web/cells.js +6 -7
- package/dist/web/i18n.js +75 -0
- package/dist/web/mail-draft.d.ts +6 -1
- package/dist/web/mail-draft.js +20 -6
- package/dist/web/mail-origin-bar.js +2 -1
- package/dist/web/mail-preview-side.js +31 -7
- package/dist/web/mail-right-rail.js +6 -3
- package/dist/web/mail-versions.js +2 -1
- package/dist/web/origin-bar.js +2 -1
- package/dist/web/rail.js +6 -1
- package/dist/web/settings-panel.js +1 -1
- package/dist/web/used-by-label.js +3 -3
- package/package.json +8 -8
- package/src/changes.json +18 -0
package/dist/web/cells.js
CHANGED
|
@@ -1,16 +1,15 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
// @runtime client
|
|
3
3
|
import { TemplateResolverQueries } from "@cosmicdrift/kumiko-bundled-features/template-resolver/web";
|
|
4
|
-
import { toInstant } from "@cosmicdrift/kumiko-headless";
|
|
4
|
+
import { EMPTY_VALUE_PLACEHOLDER, toInstant } from "@cosmicdrift/kumiko-headless";
|
|
5
5
|
import { formatWhen, useQuery, useTranslation, } from "@cosmicdrift/kumiko-renderer";
|
|
6
6
|
import { StatusBadge } from "@cosmicdrift/kumiko-renderer-web";
|
|
7
7
|
import { parseOverviewRow } from "./overview-row.js";
|
|
8
8
|
import { statusLabel, statusTone } from "./status-label.js";
|
|
9
9
|
import { templateTitle, usedByLabel } from "./used-by-label.js";
|
|
10
|
-
const EMPTY_MARK = "–";
|
|
11
10
|
function withRow(row, render) {
|
|
12
11
|
const parsed = parseOverviewRow(row);
|
|
13
|
-
return parsed === null ?
|
|
12
|
+
return parsed === null ? EMPTY_VALUE_PLACEHOLDER : render(parsed);
|
|
14
13
|
}
|
|
15
14
|
export function TitleCell({ row }) {
|
|
16
15
|
const t = useTranslation();
|
|
@@ -34,7 +33,7 @@ export function createLocalesCell(appLocales) {
|
|
|
34
33
|
return function LocalesCell({ row }) {
|
|
35
34
|
return withRow(row, (r) => {
|
|
36
35
|
if (r.kind === "mail-layout" || r.kind === "pdf-layout")
|
|
37
|
-
return
|
|
36
|
+
return EMPTY_VALUE_PLACEHOLDER;
|
|
38
37
|
const present = new Set(r.locales);
|
|
39
38
|
const shown = [...appLocales, ...r.locales.filter((l) => !appLocales.includes(l))];
|
|
40
39
|
return (_jsx("span", { className: "inline-flex gap-1", children: shown.map((locale) => (_jsx("span", { "data-testid": `document-designer-locale-${locale}`, "data-present": present.has(locale), className: present.has(locale)
|
|
@@ -65,7 +64,7 @@ function formatModifiedDate(value) {
|
|
|
65
64
|
}
|
|
66
65
|
}
|
|
67
66
|
export function ModifiedCell({ row }) {
|
|
68
|
-
return withRow(row, (r) => r.modifiedAt === "" ? (
|
|
67
|
+
return withRow(row, (r) => r.modifiedAt === "" ? (EMPTY_VALUE_PLACEHOLDER) : (_jsx("span", { className: "whitespace-nowrap", title: formatWhen(r.modifiedAt), children: formatModifiedDate(r.modifiedAt) })));
|
|
69
68
|
}
|
|
70
69
|
function OverriddenByCount({ slug, kind, }) {
|
|
71
70
|
const t = useTranslation();
|
|
@@ -74,7 +73,7 @@ function OverriddenByCount({ slug, kind, }) {
|
|
|
74
73
|
kind,
|
|
75
74
|
});
|
|
76
75
|
if (data === null)
|
|
77
|
-
return
|
|
76
|
+
return EMPTY_VALUE_PLACEHOLDER;
|
|
78
77
|
const label = data.totalTenants === null
|
|
79
78
|
? t("documentDesigner.overriddenBy.tenants", { active: data.activeOverrides })
|
|
80
79
|
: t("documentDesigner.overriddenBy.ofTotal", {
|
|
@@ -84,5 +83,5 @@ function OverriddenByCount({ slug, kind, }) {
|
|
|
84
83
|
return _jsx("span", { className: "whitespace-nowrap", children: label });
|
|
85
84
|
}
|
|
86
85
|
export function OverriddenByCell({ row }) {
|
|
87
|
-
return withRow(row, (r) => r.origin === "system" ? _jsx(OverriddenByCount, { slug: r.slug, kind: r.kind }) :
|
|
86
|
+
return withRow(row, (r) => r.origin === "system" ? (_jsx(OverriddenByCount, { slug: r.slug, kind: r.kind })) : (EMPTY_VALUE_PLACEHOLDER));
|
|
88
87
|
}
|
package/dist/web/i18n.js
CHANGED
|
@@ -43,6 +43,7 @@ export const documentDesignerTranslations = {
|
|
|
43
43
|
"documentDesigner.resources.title": "Resources",
|
|
44
44
|
"documentDesigner.resources.upload": "+ Upload",
|
|
45
45
|
"documentDesigner.settings.fileName": "File name",
|
|
46
|
+
"documentDesigner.settings.fileNamePlaceholder": "invoice-{{number}}.pdf",
|
|
46
47
|
"documentDesigner.settings.landscape": "Landscape",
|
|
47
48
|
"documentDesigner.settings.layout": "Layout",
|
|
48
49
|
"documentDesigner.settings.layoutHeaderFooter": "Header and footer from layout",
|
|
@@ -115,6 +116,7 @@ export const documentDesignerTranslations = {
|
|
|
115
116
|
"documentDesigner.resources.title": "Ressourcen",
|
|
116
117
|
"documentDesigner.resources.upload": "+ Hochladen",
|
|
117
118
|
"documentDesigner.settings.fileName": "Dateiname",
|
|
119
|
+
"documentDesigner.settings.fileNamePlaceholder": "rechnung-{{number}}.pdf",
|
|
118
120
|
"documentDesigner.settings.landscape": "Querformat",
|
|
119
121
|
"documentDesigner.settings.layout": "Layout",
|
|
120
122
|
"documentDesigner.settings.layoutHeaderFooter": "Kopf- und Fußzeile aus Layout",
|
|
@@ -143,4 +145,77 @@ export const documentDesignerTranslations = {
|
|
|
143
145
|
"documentDesigner.validation.missingRequired": 'Pflicht-Platzhalter "{name}" fehlt. Veröffentlichen ist gesperrt.',
|
|
144
146
|
"documentDesigner.validation.ok": "Alle Pflicht-Platzhalter vorhanden. Nach dem Veröffentlichen nutzen neue PDFs diese Fassung.",
|
|
145
147
|
},
|
|
148
|
+
es: {
|
|
149
|
+
"documentDesigner.actions.generatePdf": "Generar PDF",
|
|
150
|
+
"documentDesigner.canvas.caption": "Página 1 · {format} · La vista previa solo muestra los saltos de página de forma aproximada; lo que vale es «Generar PDF».",
|
|
151
|
+
"documentDesigner.canvas.fromLayout": "del diseño «{layout}»",
|
|
152
|
+
"documentDesigner.error.resourceTooLarge": "La imagen es demasiado grande. Elige un archivo más pequeño.",
|
|
153
|
+
"documentDesigner.error.resourceUnsupportedImage": "Este formato de imagen no es compatible. Usa PNG, JPEG, GIF o WebP.",
|
|
154
|
+
"documentDesigner.locale": "Idioma",
|
|
155
|
+
"documentDesigner.localeDialog.cancel": "Cancelar",
|
|
156
|
+
"documentDesigner.localeDialog.description": "Los cambios en este idioma aún no se han guardado.",
|
|
157
|
+
"documentDesigner.localeDialog.discard": "Descartar cambios",
|
|
158
|
+
"documentDesigner.localeDialog.save": "Guardar y cambiar",
|
|
159
|
+
"documentDesigner.localeDialog.title": "Cambios sin guardar",
|
|
160
|
+
"documentDesigner.notFound": "No se encontró la plantilla «{slug}».",
|
|
161
|
+
"documentDesigner.originBar.export": "Exportar como paquete",
|
|
162
|
+
"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í.",
|
|
163
|
+
"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í.",
|
|
164
|
+
"documentDesigner.originBar.showOverrides": "Ver personalizaciones de las organizaciones",
|
|
165
|
+
"documentDesigner.overrides.action.diff": "Ver diferencias",
|
|
166
|
+
"documentDesigner.overrides.action.open": "Abrir",
|
|
167
|
+
"documentDesigner.overrides.back": "Volver a la lista",
|
|
168
|
+
"documentDesigner.overrides.column.changed": "Modificada",
|
|
169
|
+
"documentDesigner.overrides.column.status": "Estado",
|
|
170
|
+
"documentDesigner.overrides.column.tenant": "Organización",
|
|
171
|
+
"documentDesigner.overrides.column.version": "Versión",
|
|
172
|
+
"documentDesigner.overrides.detail.version": "{tenant}, versión {version}",
|
|
173
|
+
"documentDesigner.overrides.empty": "Ninguna organización ha personalizado esta plantilla. Los cambios que hagas aquí se aplican a todas.",
|
|
174
|
+
"documentDesigner.overrides.status.active": "Personalizada",
|
|
175
|
+
"documentDesigner.overrides.status.draft": "Borrador",
|
|
176
|
+
"documentDesigner.overrides.summary": "{count} organizaciones tienen su propia versión.",
|
|
177
|
+
"documentDesigner.overrides.title": "Personalizaciones de las organizaciones",
|
|
178
|
+
"documentDesigner.overrides.usedBy": "Se usa en",
|
|
179
|
+
"documentDesigner.overrides.usedByEmpty": "Ninguna notificación registrada usa esta plantilla directamente.",
|
|
180
|
+
"documentDesigner.pdfUnavailable": "La generación de PDF no está disponible: {message}",
|
|
181
|
+
"documentDesigner.pdfUnavailableNoReason": "La generación de PDF no está disponible.",
|
|
182
|
+
"documentDesigner.placeholders.layoutHint": "Las imágenes van incluidas en el PDF como URI de datos. El encabezado y el pie de página vienen del diseño «{layout}» y son iguales en todas las páginas.",
|
|
183
|
+
"documentDesigner.preview.loading": "Generando PDF...",
|
|
184
|
+
"documentDesigner.preview.pageOf": "Página {page} de {pages}",
|
|
185
|
+
"documentDesigner.preview.renderFailed": "No se pudo mostrar el PDF.",
|
|
186
|
+
"documentDesigner.preview.stale": "La vista previa está desactualizada. Vuelve a generar el PDF.",
|
|
187
|
+
"documentDesigner.resources.empty": "Todavía no hay imágenes.",
|
|
188
|
+
"documentDesigner.resources.saveFirst": "Guarda primero la plantilla; después podrás subir imágenes.",
|
|
189
|
+
"documentDesigner.resources.title": "Recursos",
|
|
190
|
+
"documentDesigner.resources.upload": "+ Subir",
|
|
191
|
+
"documentDesigner.settings.fileName": "Nombre del archivo",
|
|
192
|
+
"documentDesigner.settings.fileNamePlaceholder": "factura-{{number}}.pdf",
|
|
193
|
+
"documentDesigner.settings.landscape": "Horizontal",
|
|
194
|
+
"documentDesigner.settings.layout": "Diseño",
|
|
195
|
+
"documentDesigner.settings.layoutHeaderFooter": "Encabezado y pie de página del diseño",
|
|
196
|
+
"documentDesigner.settings.marginBottom": "Inferior (mm)",
|
|
197
|
+
"documentDesigner.settings.marginLeft": "Izquierdo (mm)",
|
|
198
|
+
"documentDesigner.settings.marginRight": "Derecho (mm)",
|
|
199
|
+
"documentDesigner.settings.marginTop": "Superior (mm)",
|
|
200
|
+
"documentDesigner.settings.metaAuthor": "Autor",
|
|
201
|
+
"documentDesigner.settings.metadata": "Metadatos del PDF",
|
|
202
|
+
"documentDesigner.settings.metaLanguage": "Idioma",
|
|
203
|
+
"documentDesigner.settings.metaLanguageHint": "Sigue el idioma de la plantilla. Cámbialo con el selector de idioma del encabezado.",
|
|
204
|
+
"documentDesigner.settings.margins": "Márgenes en mm (superior, derecho, inferior, izquierdo)",
|
|
205
|
+
"documentDesigner.settings.metaTitle": "Título",
|
|
206
|
+
"documentDesigner.settings.orientation": "Orientación",
|
|
207
|
+
"documentDesigner.settings.page": "Documento",
|
|
208
|
+
"documentDesigner.settings.pageFormat": "Formato de página",
|
|
209
|
+
"documentDesigner.settings.portrait": "Vertical",
|
|
210
|
+
"documentDesigner.settings.printBackground": "Imprimir fondos y colores",
|
|
211
|
+
"documentDesigner.settings.storage": "Almacenamiento",
|
|
212
|
+
"documentDesigner.settings.storageHint": "Cada PDF generado lleva el id y la versión de la plantilla. Así las facturas siguen siendo verificables aunque la plantilla cambie más adelante.",
|
|
213
|
+
"documentDesigner.status.active": "Activa",
|
|
214
|
+
"documentDesigner.status.activeVersion": "Activa v{version}",
|
|
215
|
+
"documentDesigner.status.draft": "Borrador",
|
|
216
|
+
"documentDesigner.status.draftVersion": "Borrador v{version}",
|
|
217
|
+
"documentDesigner.validation.lastProbe": "Última generación de prueba: {pages} páginas, {size}, {seconds} s",
|
|
218
|
+
"documentDesigner.validation.missingRequired": "Falta el marcador obligatorio «{name}». No se puede publicar.",
|
|
219
|
+
"documentDesigner.validation.ok": "Están todos los marcadores obligatorios. Tras publicar, los PDF nuevos usan esta versión.",
|
|
220
|
+
},
|
|
146
221
|
};
|
package/dist/web/mail-draft.d.ts
CHANGED
|
@@ -5,6 +5,7 @@ export declare const LAYOUT_OWNED_PLACEHOLDERS: ReadonlySet<string>;
|
|
|
5
5
|
export declare const SYSTEM_PLACEHOLDER_TYPE: Readonly<Record<string, PlaceholderType>>;
|
|
6
6
|
type Translate = (key: string, params?: Readonly<Record<string, unknown>>) => string;
|
|
7
7
|
export declare function systemPlaceholderLabel(t: Translate, name: string): string;
|
|
8
|
+
export declare function labelPlaceholders(line: string, variableLabels: Readonly<Record<string, string>>): string;
|
|
8
9
|
export declare function systemLabelMap(t: Translate, names: readonly string[]): Readonly<Record<string, string>>;
|
|
9
10
|
export declare function parseJsonRecord(value: unknown): Readonly<Record<string, unknown>>;
|
|
10
11
|
export declare function loadedMailOf(row: TemplateRowView | null): MailDraft;
|
|
@@ -15,5 +16,9 @@ export declare function uniqueResourceKey(source: string, taken: readonly string
|
|
|
15
16
|
export declare function lastSlugSegment(slug: string): string;
|
|
16
17
|
export declare function formatKilobytes(bytes: number): string;
|
|
17
18
|
export declare function variableLabelMap(t: Translate, variables: Readonly<Record<string, MailVariable>>): Record<string, string>;
|
|
18
|
-
export
|
|
19
|
+
export type SampleValueFormat = {
|
|
20
|
+
readonly locale: string;
|
|
21
|
+
readonly tenantCurrency: string | undefined;
|
|
22
|
+
};
|
|
23
|
+
export declare function displayValue(t: (key: string, params?: Readonly<Record<string, unknown>>) => string, value: unknown, type: PlaceholderType | undefined, format: SampleValueFormat): string;
|
|
19
24
|
export {};
|
package/dist/web/mail-draft.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
// @runtime client
|
|
2
|
+
import { DOCUMENT_FALLBACK_CURRENCY, formatDocumentDate, formatDocumentMoney, formatDocumentPercent, } from "@cosmicdrift/kumiko-headless";
|
|
1
3
|
import { variableLabelText } from "./used-by-label.js";
|
|
2
4
|
export const EMPTY_DRAFT = {
|
|
3
5
|
subject: "",
|
|
@@ -33,6 +35,13 @@ export function systemPlaceholderLabel(t, name) {
|
|
|
33
35
|
const translated = t(key);
|
|
34
36
|
return translated === key ? name : translated;
|
|
35
37
|
}
|
|
38
|
+
// For places that show a stored {{name}} line as plain text instead of chips.
|
|
39
|
+
export function labelPlaceholders(line, variableLabels) {
|
|
40
|
+
return line.replace(/\{\{([^{}]*)\}\}/g, (_token, rawName) => {
|
|
41
|
+
const name = rawName.trim();
|
|
42
|
+
return Object.hasOwn(variableLabels, name) ? (variableLabels[name] ?? name) : name;
|
|
43
|
+
});
|
|
44
|
+
}
|
|
36
45
|
// Chips in the editors show these instead of the raw system names.
|
|
37
46
|
export function systemLabelMap(t, names) {
|
|
38
47
|
return Object.fromEntries(names.map((name) => [name, systemPlaceholderLabel(t, name)]));
|
|
@@ -156,14 +165,19 @@ export function variableLabelMap(t, variables) {
|
|
|
156
165
|
}
|
|
157
166
|
return labels;
|
|
158
167
|
}
|
|
159
|
-
export function displayValue(t, value) {
|
|
168
|
+
export function displayValue(t, value, type, format) {
|
|
160
169
|
if (Array.isArray(value))
|
|
161
170
|
return t("documentDesigner.mail.preview.entries", { count: value.length });
|
|
162
|
-
if (
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
return JSON.stringify(value);
|
|
171
|
+
if (type === "money") {
|
|
172
|
+
if (typeof value === "number" && format.tenantCurrency === undefined)
|
|
173
|
+
return String(value);
|
|
174
|
+
return formatDocumentMoney(value, format.tenantCurrency ?? DOCUMENT_FALLBACK_CURRENCY, format.locale);
|
|
167
175
|
}
|
|
176
|
+
if (type === "date")
|
|
177
|
+
return formatDocumentDate(value, format.locale);
|
|
178
|
+
if (type === "percent")
|
|
179
|
+
return formatDocumentPercent(value, format.locale);
|
|
180
|
+
if (isRecord(value))
|
|
181
|
+
return JSON.stringify(value);
|
|
168
182
|
return String(value ?? "");
|
|
169
183
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
// @runtime client
|
|
3
3
|
import { TemplateResolverHandlers } from "@cosmicdrift/kumiko-bundled-features/template-resolver/web";
|
|
4
|
+
import { EMPTY_VALUE_PLACEHOLDER } from "@cosmicdrift/kumiko-headless";
|
|
4
5
|
import { dispatcherErrorText, formatWhen, useMutation, usePrimitives, useTranslation, } from "@cosmicdrift/kumiko-renderer";
|
|
5
6
|
import { useState } from "react";
|
|
6
7
|
import { originOf } from "./editor-header.js";
|
|
@@ -18,7 +19,7 @@ function statusSentence(t, row) {
|
|
|
18
19
|
return besideActive;
|
|
19
20
|
if (row.status === "active") {
|
|
20
21
|
return t("documentDesigner.mail.origin.activeSince", {
|
|
21
|
-
when: row.modifiedAt === null ?
|
|
22
|
+
when: row.modifiedAt === null ? EMPTY_VALUE_PLACEHOLDER : formatWhen(row.modifiedAt),
|
|
22
23
|
});
|
|
23
24
|
}
|
|
24
25
|
return t(row.status === "archived"
|
|
@@ -1,30 +1,54 @@
|
|
|
1
1
|
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
// @runtime client
|
|
3
|
-
import {
|
|
3
|
+
import { ConfigQueries } from "@cosmicdrift/kumiko-bundled-features/config/web";
|
|
4
|
+
import { DOCUMENT_FALLBACK_CURRENCY } from "@cosmicdrift/kumiko-headless";
|
|
5
|
+
import { useNav, usePrimitives, useQuery, useTranslation } from "@cosmicdrift/kumiko-renderer";
|
|
6
|
+
import { TENANT_CURRENCY_CONFIG_KEY } from "@cosmicdrift/kumiko-types/fields";
|
|
4
7
|
import { useState } from "react";
|
|
5
8
|
import { displayValue } from "./mail-draft.js";
|
|
6
9
|
import { usePreviewRecordPicker } from "./preview-record-picker.js";
|
|
7
10
|
import { RailField, RailHint, RailRow, RailSection } from "./rail.js";
|
|
8
11
|
import { variableLabelText } from "./used-by-label.js";
|
|
9
12
|
const noop = () => undefined;
|
|
10
|
-
// Check params list names as arrays; the message templates expect one string
|
|
11
|
-
|
|
13
|
+
// Check params list placeholder names as arrays; the message templates expect one string, and
|
|
14
|
+
// the editor shows each placeholder by its label.
|
|
15
|
+
function checkText(t, check, validation) {
|
|
16
|
+
const labelOf = (name) => typeof name === "string"
|
|
17
|
+
? variableLabelText(t, name, validation?.variables[name]?.label)
|
|
18
|
+
: String(name);
|
|
12
19
|
const params = Object.fromEntries(Object.entries(check.params ?? {}).map(([name, value]) => [
|
|
13
20
|
name,
|
|
14
|
-
Array.isArray(value) ? value.join(", ") : value,
|
|
21
|
+
Array.isArray(value) ? value.map(labelOf).join(", ") : value,
|
|
15
22
|
]));
|
|
16
23
|
const translated = t(check.i18nKey, params);
|
|
17
24
|
return translated === check.i18nKey ? check.code : translated;
|
|
18
25
|
}
|
|
19
|
-
|
|
26
|
+
// Same source and fallback as the renderer's readDocumentFormat, so a bare amount shows
|
|
27
|
+
// the currency the rendered mail prints.
|
|
28
|
+
function useTenantCurrency() {
|
|
29
|
+
const { data, error } = useQuery(ConfigQueries.values, {
|
|
30
|
+
keys: [TENANT_CURRENCY_CONFIG_KEY],
|
|
31
|
+
});
|
|
32
|
+
if (error)
|
|
33
|
+
return undefined;
|
|
34
|
+
if (data === null)
|
|
35
|
+
return undefined;
|
|
36
|
+
const value = data[TENANT_CURRENCY_CONFIG_KEY]?.value;
|
|
37
|
+
return typeof value === "string" && value !== "" ? value : DOCUMENT_FALLBACK_CURRENCY;
|
|
38
|
+
}
|
|
39
|
+
function SampleValues({ preview, validation, picked, locale, }) {
|
|
20
40
|
const t = useTranslation();
|
|
41
|
+
const tenantCurrency = useTenantCurrency();
|
|
21
42
|
const values = { ...(preview?.sampleValues ?? {}), ...(picked ?? {}) };
|
|
22
43
|
const names = Object.keys(validation?.variables ?? values);
|
|
23
44
|
return (_jsx(_Fragment, { children: names.map((name) => {
|
|
24
45
|
const missing = validation?.missingRequired.includes(name) === true;
|
|
25
46
|
return (_jsx(RailRow, { testId: `mail-sample-${name}`, label: variableLabelText(t, name, validation?.variables[name]?.label), nameHint: name, bad: missing, trailing: missing
|
|
26
47
|
? t("documentDesigner.mail.preview.missingInContent")
|
|
27
|
-
: displayValue(t, values[name]
|
|
48
|
+
: displayValue(t, values[name], validation?.variables[name]?.type, {
|
|
49
|
+
locale,
|
|
50
|
+
tenantCurrency,
|
|
51
|
+
}) }, name));
|
|
28
52
|
}) }));
|
|
29
53
|
}
|
|
30
54
|
function RecordPickerLink({ slug, locale, onPick, }) {
|
|
@@ -54,5 +78,5 @@ export function PreviewSide({ preview, validation, slug, locale, own, testMail,
|
|
|
54
78
|
const { Button, Field, Input, Text } = usePrimitives();
|
|
55
79
|
const hasAttachments = preview?.attachments.some((a) => a.kind !== "inline") === true;
|
|
56
80
|
const [picked, setPicked] = useState(null);
|
|
57
|
-
return (_jsxs("div", { "data-testid": "mail-preview-side", children: [_jsxs(RailSection, { title: t("documentDesigner.mail.preview.samples"), action: _jsx(RecordPickerLink, { slug: slug, locale: locale, onPick: setPicked }), children: [_jsx(SampleValues, { preview: preview, validation: validation, picked: picked }), _jsx(RailHint, { children: t("documentDesigner.mail.preview.samplesHint") })] }), _jsxs(RailSection, { title: t("documentDesigner.mail.testMail.title"), children: [_jsx(TestMailRecipient, { own: own }), _jsxs(RailField, { children: [_jsx(Field, { id: "mail-test-prefix", label: t("documentDesigner.mail.testMail.prefixSubject"), layout: "inline", children: _jsx(Input, { kind: "boolean", id: "mail-test-prefix", name: "mail-test-prefix", value: testMail.prefixSubject, onChange: testMail.onPrefixSubject }) }), hasAttachments ? (_jsx(Field, { id: "mail-test-attachments", label: t("documentDesigner.mail.testMail.attachments"), layout: "inline", children: _jsx(Input, { kind: "boolean", id: "mail-test-attachments", name: "mail-test-attachments", value: testMail.includeAttachments, onChange: testMail.onIncludeAttachments }) })) : (_jsx(Text, { variant: "muted", testId: "mail-test-no-attachments", children: t("documentDesigner.mail.testMail.noAttachments") })), _jsx(Button, { variant: "secondary", width: "full", disabled: testMail.pending, loading: testMail.pending, onClick: testMail.onSend, testId: "mail-test-send", children: t("documentDesigner.mail.testMail.send") })] }), _jsx(RailHint, { children: t("documentDesigner.mail.testMail.hint") })] }), _jsxs(RailSection, { title: t("documentDesigner.mail.preview.checks"), testId: "mail-preview-checks", children: [(preview?.checks ?? []).map((check) => (_jsx(RailRow, { testId: `mail-check-${check.code}`, symbol: check.ok ? "✓" : "!", label: checkText(t, check), bad: !check.ok }, check.code))), _jsx(RailHint, { children: t("documentDesigner.mail.preview.checksHint") })] })] }));
|
|
81
|
+
return (_jsxs("div", { "data-testid": "mail-preview-side", children: [_jsxs(RailSection, { title: t("documentDesigner.mail.preview.samples"), action: _jsx(RecordPickerLink, { slug: slug, locale: locale, onPick: setPicked }), children: [_jsx(SampleValues, { preview: preview, validation: validation, picked: picked, locale: locale }), _jsx(RailHint, { children: t("documentDesigner.mail.preview.samplesHint") })] }), _jsxs(RailSection, { title: t("documentDesigner.mail.testMail.title"), children: [_jsx(TestMailRecipient, { own: own }), _jsxs(RailField, { children: [_jsx(Field, { id: "mail-test-prefix", label: t("documentDesigner.mail.testMail.prefixSubject"), layout: "inline", children: _jsx(Input, { kind: "boolean", id: "mail-test-prefix", name: "mail-test-prefix", value: testMail.prefixSubject, onChange: testMail.onPrefixSubject }) }), hasAttachments ? (_jsx(Field, { id: "mail-test-attachments", label: t("documentDesigner.mail.testMail.attachments"), layout: "inline", children: _jsx(Input, { kind: "boolean", id: "mail-test-attachments", name: "mail-test-attachments", value: testMail.includeAttachments, onChange: testMail.onIncludeAttachments }) })) : (_jsx(Text, { variant: "muted", testId: "mail-test-no-attachments", children: t("documentDesigner.mail.testMail.noAttachments") })), _jsx(Button, { variant: "secondary", width: "full", disabled: testMail.pending, loading: testMail.pending, onClick: testMail.onSend, testId: "mail-test-send", children: t("documentDesigner.mail.testMail.send") })] }), _jsx(RailHint, { children: t("documentDesigner.mail.testMail.hint") })] }), _jsxs(RailSection, { title: t("documentDesigner.mail.preview.checks"), testId: "mail-preview-checks", children: [(preview?.checks ?? []).map((check) => (_jsx(RailRow, { testId: `mail-check-${check.code}`, symbol: check.ok ? "✓" : "!", label: checkText(t, check, validation), bad: !check.ok }, check.code))), _jsx(RailHint, { children: t("documentDesigner.mail.preview.checksHint") })] })] }));
|
|
58
82
|
}
|
|
@@ -4,6 +4,7 @@ import { MAIL_TONES, PlaceholderLineEditor, TemplateResolverQueries, } from "@co
|
|
|
4
4
|
import { useAppFeatures, useNav, usePrimitives, useQuery, useTranslation, } from "@cosmicdrift/kumiko-renderer";
|
|
5
5
|
import { isOneOf } from "@cosmicdrift/kumiko-types/type-guards";
|
|
6
6
|
import { DEFAULT_LAYOUT_SLUG, TENANT_MAIL_SENDING_SCREEN_ID } from "../constants.js";
|
|
7
|
+
import { labelPlaceholders } from "./mail-draft.js";
|
|
7
8
|
import { RailField, RailHint, RailRow, RailSection } from "./rail.js";
|
|
8
9
|
import { usageParts } from "./used-by-label.js";
|
|
9
10
|
function useTemplateUsage(slug) {
|
|
@@ -93,7 +94,7 @@ const TONE_LABEL_KEYS = {
|
|
|
93
94
|
};
|
|
94
95
|
// The stored tone may be a placeholder that resolves per mail; the select keeps it as its own
|
|
95
96
|
// option so opening the template never silently drops it.
|
|
96
|
-
function toneOptions(t, current) {
|
|
97
|
+
function toneOptions(t, current, variableLabels) {
|
|
97
98
|
const fixed = MAIL_TONES.map((tone) => ({ value: tone, label: t(TONE_LABEL_KEYS[tone]) }));
|
|
98
99
|
return current === "" || isOneOf(MAIL_TONES, current)
|
|
99
100
|
? fixed
|
|
@@ -101,7 +102,9 @@ function toneOptions(t, current) {
|
|
|
101
102
|
...fixed,
|
|
102
103
|
{
|
|
103
104
|
value: current,
|
|
104
|
-
label: t("documentDesigner.mail.chrome.toneFromPlaceholder", {
|
|
105
|
+
label: t("documentDesigner.mail.chrome.toneFromPlaceholder", {
|
|
106
|
+
value: labelPlaceholders(current, variableLabels),
|
|
107
|
+
}),
|
|
105
108
|
},
|
|
106
109
|
];
|
|
107
110
|
}
|
|
@@ -110,7 +113,7 @@ export function ChromeSection({ draft, vocabulary, insert, onChange, }) {
|
|
|
110
113
|
const { Field, Input } = usePrimitives();
|
|
111
114
|
// "accent" is the renderer default, so it is stored as an empty value.
|
|
112
115
|
const toneValue = draft.tone === "" ? "accent" : draft.tone;
|
|
113
|
-
return (_jsxs(RailSection, { title: t("documentDesigner.mail.chrome.title"), testId: "mail-editor-chrome", children: [_jsx(RailField, { children: _jsx(Field, { id: "mail-header-chip", label: t("documentDesigner.mail.chrome.headerChip"), children: _jsx("div", { onFocusCapture: insert.markFocused("headerChip"), "data-testid": "mail-header-chip", children: _jsx(PlaceholderLineEditor, { id: "mail-header-chip", value: draft.headerChip, onChange: (headerChip) => onChange({ headerChip }), variableLabels: vocabulary.labels, systemVariables: vocabulary.system, placeholder: t("documentDesigner.mail.chrome.headerChipPlaceholder"), onEditorReady: insert.register("headerChip"), "aria-label": t("documentDesigner.mail.chrome.headerChip") }) }) }) }), _jsx(RailField, { children: _jsx(Field, { id: "mail-tone", label: t("documentDesigner.mail.chrome.tone"), children: _jsx(Input, { kind: "select", id: "mail-tone", name: "mail-tone", value: toneValue, display: "dropdown", onChange: (next) => onChange({ tone: next === "accent" ? "" : next }), options: toneOptions(t, draft.tone) }) }) }), _jsx(RailField, { children: _jsx(Field, { id: "mail-footer-note", label: t("documentDesigner.mail.chrome.footerNote"), children: _jsx("div", { onFocusCapture: insert.markFocused("footerNote"), "data-testid": "mail-footer-note", children: _jsx(PlaceholderLineEditor, { id: "mail-footer-note", value: draft.footerNote, onChange: (footerNote) => onChange({ footerNote }), variableLabels: vocabulary.labels, systemVariables: vocabulary.system, placeholder: t("documentDesigner.mail.chrome.footerNotePlaceholder"), onEditorReady: insert.register("footerNote"), "aria-label": t("documentDesigner.mail.chrome.footerNote") }) }) }) }), _jsx(RailHint, { children: t("documentDesigner.mail.chrome.hint") })] }));
|
|
116
|
+
return (_jsxs(RailSection, { title: t("documentDesigner.mail.chrome.title"), testId: "mail-editor-chrome", children: [_jsx(RailField, { children: _jsx(Field, { id: "mail-header-chip", label: t("documentDesigner.mail.chrome.headerChip"), children: _jsx("div", { onFocusCapture: insert.markFocused("headerChip"), "data-testid": "mail-header-chip", children: _jsx(PlaceholderLineEditor, { id: "mail-header-chip", value: draft.headerChip, onChange: (headerChip) => onChange({ headerChip }), variableLabels: vocabulary.labels, systemVariables: vocabulary.system, placeholder: t("documentDesigner.mail.chrome.headerChipPlaceholder"), onEditorReady: insert.register("headerChip"), "aria-label": t("documentDesigner.mail.chrome.headerChip") }) }) }) }), _jsx(RailField, { children: _jsx(Field, { id: "mail-tone", label: t("documentDesigner.mail.chrome.tone"), children: _jsx(Input, { kind: "select", id: "mail-tone", name: "mail-tone", value: toneValue, display: "dropdown", onChange: (next) => onChange({ tone: next === "accent" ? "" : next }), options: toneOptions(t, draft.tone, vocabulary.labels) }) }) }), _jsx(RailField, { children: _jsx(Field, { id: "mail-footer-note", label: t("documentDesigner.mail.chrome.footerNote"), children: _jsx("div", { onFocusCapture: insert.markFocused("footerNote"), "data-testid": "mail-footer-note", children: _jsx(PlaceholderLineEditor, { id: "mail-footer-note", value: draft.footerNote, onChange: (footerNote) => onChange({ footerNote }), variableLabels: vocabulary.labels, systemVariables: vocabulary.system, placeholder: t("documentDesigner.mail.chrome.footerNotePlaceholder"), onEditorReady: insert.register("footerNote"), "aria-label": t("documentDesigner.mail.chrome.footerNote") }) }) }) }), _jsx(RailHint, { children: t("documentDesigner.mail.chrome.hint") })] }));
|
|
114
117
|
}
|
|
115
118
|
export function UsageSection({ slug }) {
|
|
116
119
|
const t = useTranslation();
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { EMPTY_VALUE_PLACEHOLDER } from "@cosmicdrift/kumiko-headless";
|
|
2
3
|
// @runtime client
|
|
3
4
|
import { formatWhen, useAppFeatures, useQuery, useTranslation } from "@cosmicdrift/kumiko-renderer";
|
|
4
5
|
import { StatusBadge } from "@cosmicdrift/kumiko-renderer-web";
|
|
@@ -29,7 +30,7 @@ function VersionRows({ rowId, currentVersion, currentStatus, }) {
|
|
|
29
30
|
return (_jsx(_Fragment, { children: versions.map((row) => (_jsx(RailRow, { testId: `mail-version-${row.version}`, label: t("documentDesigner.mail.versions.line", {
|
|
30
31
|
version: row.version,
|
|
31
32
|
when: formatWhen(row.createdAt),
|
|
32
|
-
name: names.get(row.createdBy) ??
|
|
33
|
+
name: names.get(row.createdBy) ?? EMPTY_VALUE_PLACEHOLDER,
|
|
33
34
|
}), trailing: row.version === currentVersion ? (_jsx(StatusBadge, { tone: statusTone(status), children: statusLabel(t, status, row.version) })) : undefined }, row.version))) }));
|
|
34
35
|
}
|
|
35
36
|
// Hidden when the audit feature is not mounted, so no failing query shows up as an error.
|
package/dist/web/origin-bar.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
// @runtime client
|
|
3
3
|
import { TemplateResolverHandlers, TemplateResolverQueries, } from "@cosmicdrift/kumiko-bundled-features/template-resolver/web";
|
|
4
|
+
import { EMPTY_VALUE_PLACEHOLDER } from "@cosmicdrift/kumiko-headless";
|
|
4
5
|
import { dispatcherErrorText, useMutation, usePrimitives, useQuery, useTranslation, } from "@cosmicdrift/kumiko-renderer";
|
|
5
6
|
import { EmptyState, ErrorState, LoadingState } from "@cosmicdrift/kumiko-renderer-web";
|
|
6
7
|
import { useState } from "react";
|
|
@@ -19,7 +20,7 @@ const DIFF_TONE = {
|
|
|
19
20
|
};
|
|
20
21
|
function metaValueText(value) {
|
|
21
22
|
if (value === undefined || value === null)
|
|
22
|
-
return
|
|
23
|
+
return EMPTY_VALUE_PLACEHOLDER;
|
|
23
24
|
return typeof value === "string" ? value : JSON.stringify(value);
|
|
24
25
|
}
|
|
25
26
|
// "full" shows the tenant's version as it reads (everything but the removed system blocks).
|
package/dist/web/rail.js
CHANGED
|
@@ -12,8 +12,13 @@ const TRAILING_TONE = {
|
|
|
12
12
|
bad: "text-status-bad",
|
|
13
13
|
muted: "text-muted-foreground",
|
|
14
14
|
};
|
|
15
|
+
// The star rides on the last word so a wrapping label never leaves it alone on a line.
|
|
16
|
+
function RequiredLabel({ label }) {
|
|
17
|
+
const lastSpace = label.lastIndexOf(" ");
|
|
18
|
+
return (_jsxs(_Fragment, { children: [label.slice(0, lastSpace + 1), _jsxs("span", { className: "whitespace-nowrap", children: [label.slice(lastSpace + 1), _jsx("span", { className: "font-semibold text-status-bad", children: " *" })] })] }));
|
|
19
|
+
}
|
|
15
20
|
export function RailRow({ symbol, label, nameHint, required, trailing, trailingTone = "muted", action, bad, testId, onPress, }) {
|
|
16
|
-
const content = (_jsxs(_Fragment, { children: [symbol !== undefined && (_jsx("span", { className: "inline-flex h-[18px] w-[22px] shrink-0 items-center justify-center rounded-sm bg-muted font-mono text-xs text-muted-foreground", children: symbol })),
|
|
21
|
+
const content = (_jsxs(_Fragment, { children: [symbol !== undefined && (_jsx("span", { className: "inline-flex h-[18px] w-[22px] shrink-0 items-center justify-center rounded-sm bg-muted font-mono text-xs text-muted-foreground", children: symbol })), _jsx("span", { className: cn("min-w-0 grow break-words text-sm", trailing !== undefined && "min-w-min max-w-[65%] shrink-0", bad === true && "text-status-bad"), title: nameHint !== undefined && nameHint !== label ? nameHint : undefined, children: required === true ? _jsx(RequiredLabel, { label: label }) : label }), trailing !== undefined && (_jsx("span", { className: cn("min-w-0 truncate text-right text-xs", bad === true ? "text-status-bad" : TRAILING_TONE[trailingTone]), title: typeof trailing === "string" ? trailing : undefined, children: trailing }))] }));
|
|
17
22
|
const rowClass = "flex min-h-10 min-w-0 grow items-center gap-2 px-3.5 text-left";
|
|
18
23
|
return (_jsxs("div", { className: cn("flex items-center border-b", bad === true && "bg-status-bad-surface"), "data-testid": testId, "data-bad": bad === true ? "true" : undefined, children: [onPress === undefined ? (_jsx("div", { className: rowClass, children: content })) : (_jsx("button", { type: "button", onClick: onPress, className: cn(rowClass, "hover:bg-muted"), ...(testId !== undefined && { "data-testid": `${testId}-insert` }), children: content })), action !== undefined && _jsx("div", { className: "shrink-0 pr-2", children: action })] }));
|
|
19
24
|
}
|
|
@@ -21,7 +21,7 @@ export function SettingsPanel({ meta, onMetaChange, layouts, locale, version, vo
|
|
|
21
21
|
return (_jsxs("div", { "data-testid": "document-designer-settings", children: [_jsx(RailSection, { title: t("documentDesigner.settings.page"), testId: "pdf-editor-document", children: _jsxs(RailField, { children: [_jsx(Field, { id: "dd-layout", label: t("documentDesigner.settings.layout"), children: _jsx(Input, { kind: "select", id: "dd-layout", name: "layoutSlug", value: meta.layoutSlug === "" ? DEFAULT_LAYOUT_SLUG : meta.layoutSlug, onChange: (layoutSlug) => patch({ layoutSlug }), display: "dropdown", options: layouts.map((layout) => ({
|
|
22
22
|
value: layout.slug,
|
|
23
23
|
label: layoutOptionLabel(t, layout),
|
|
24
|
-
})) }) }), _jsx(Field, { id: "dd-file-name", label: t("documentDesigner.settings.fileName"), children: _jsx(Input, { kind: "text", id: "dd-file-name", name: "fileName", value: meta.fileName, onChange: (fileName) => patch({ fileName }), placeholder: "
|
|
24
|
+
})) }) }), _jsx(Field, { id: "dd-file-name", label: t("documentDesigner.settings.fileName"), children: _jsx(Input, { kind: "text", id: "dd-file-name", name: "fileName", value: meta.fileName, onChange: (fileName) => patch({ fileName }), placeholder: t("documentDesigner.settings.fileNamePlaceholder") }) }), _jsxs("div", { className: "grid grid-cols-[minmax(0,1fr)_minmax(0,1.5fr)] gap-2", children: [_jsx(Field, { id: "dd-page-format", label: t("documentDesigner.settings.pageFormat"), children: _jsx(Input, { kind: "select", id: "dd-page-format", name: "pageFormat", value: meta.pageFormat, onChange: (pageFormat) => {
|
|
25
25
|
const next = PDF_PAGE_FORMATS.find((format) => format === pageFormat);
|
|
26
26
|
if (next !== undefined)
|
|
27
27
|
patch({ pageFormat: next });
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { EMPTY_VALUE_PLACEHOLDER } from "@cosmicdrift/kumiko-headless";
|
|
1
2
|
// @runtime client
|
|
2
3
|
import { DEFAULT_LAYOUT_SLUG } from "../constants.js";
|
|
3
4
|
// t() returns the key itself when no translation exists.
|
|
@@ -31,7 +32,6 @@ export function usageLabel(t, usage) {
|
|
|
31
32
|
return usage.feature === "" ? notification : `${feature} · ${notification}`;
|
|
32
33
|
}
|
|
33
34
|
export function usedByLabel(t, row) {
|
|
34
|
-
const emptyMark = "–";
|
|
35
35
|
if (row.kind === "mail-layout" || row.kind === "pdf-layout") {
|
|
36
36
|
if (row.slug === DEFAULT_LAYOUT_SLUG) {
|
|
37
37
|
return t(row.kind === "mail-layout"
|
|
@@ -39,9 +39,9 @@ export function usedByLabel(t, row) {
|
|
|
39
39
|
: "documentDesigner.usedBy.allPdfs");
|
|
40
40
|
}
|
|
41
41
|
return row.usedByCount === 0
|
|
42
|
-
?
|
|
42
|
+
? EMPTY_VALUE_PLACEHOLDER
|
|
43
43
|
: t("documentDesigner.usedBy.templates", { count: row.usedByCount });
|
|
44
44
|
}
|
|
45
45
|
const first = row.usedBy[0];
|
|
46
|
-
return first === undefined ?
|
|
46
|
+
return first === undefined ? EMPTY_VALUE_PLACEHOLDER : usageLabel(t, first);
|
|
47
47
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmicdrift/kumiko-document-designer",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.372.0",
|
|
4
4
|
"description": "Document designer for Kumiko: template overview, layout editor (mail layout, PDF letterhead) and the PDF template editor.",
|
|
5
5
|
"license": "BUSL-1.1",
|
|
6
6
|
"author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
|
|
@@ -19,13 +19,13 @@
|
|
|
19
19
|
}
|
|
20
20
|
},
|
|
21
21
|
"dependencies": {
|
|
22
|
-
"@cosmicdrift/kumiko-bundled-features": "0.
|
|
23
|
-
"@cosmicdrift/kumiko-framework": "0.
|
|
24
|
-
"@cosmicdrift/kumiko-headless": "0.
|
|
25
|
-
"@cosmicdrift/kumiko-renderer": "0.
|
|
26
|
-
"@cosmicdrift/kumiko-renderer-puppeteer-client": "0.
|
|
27
|
-
"@cosmicdrift/kumiko-renderer-web": "0.
|
|
28
|
-
"@cosmicdrift/kumiko-types": "0.
|
|
22
|
+
"@cosmicdrift/kumiko-bundled-features": "0.372.0",
|
|
23
|
+
"@cosmicdrift/kumiko-framework": "0.372.0",
|
|
24
|
+
"@cosmicdrift/kumiko-headless": "0.372.0",
|
|
25
|
+
"@cosmicdrift/kumiko-renderer": "0.372.0",
|
|
26
|
+
"@cosmicdrift/kumiko-renderer-puppeteer-client": "0.372.0",
|
|
27
|
+
"@cosmicdrift/kumiko-renderer-web": "0.372.0",
|
|
28
|
+
"@cosmicdrift/kumiko-types": "0.372.0",
|
|
29
29
|
"pdfjs-dist": "6.3.289",
|
|
30
30
|
"react": "^19.2.6"
|
|
31
31
|
},
|
package/src/changes.json
CHANGED
|
@@ -1,4 +1,22 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "0.372.0",
|
|
4
|
+
"type": "fix",
|
|
5
|
+
"title": "The designer names placeholders by their label everywhere",
|
|
6
|
+
"detail": "Placeholder chips in the mail subject, the header label, the footer note and the paper editor now show only the variable's label; the raw name stays available as a tooltip. The tone select reads \"From placeholder: <label>\" instead of \"{{tone}}\", and the validation notes in the preview list labels instead of names. The file-name hint in the settings is translated (de, en, es). Unknown placeholders and variables without a label still show their name."
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
"version": "0.372.0",
|
|
10
|
+
"type": "fix",
|
|
11
|
+
"title": "Sample values show formatted money and dates",
|
|
12
|
+
"detail": "The sample-values bar in the mail preview formats money, dates and percentages in the language of the selected tab and with the tenant currency, through the same helpers the mail and PDF renderers use (formatDocumentMoney, formatDocumentDate and formatDocumentPercent, now exported from kumiko-headless). An amount reads \"1.284,50 €\" instead of \"128450 EUR\". The required star in the placeholder list stays on the label's last word instead of wrapping onto its own line. config/web now exports ConfigQueries."
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"version": "0.371.1",
|
|
16
|
+
"type": "fix",
|
|
17
|
+
"title": "The Document Designer editor shows its own texts in Spanish",
|
|
18
|
+
"detail": "The client translation bundle of the Document Designer (canvas caption, origin bar, tenant customisations, resources, PDF settings, preview and validation messages) had only en and de, so Spanish users saw these texts in English. It now ships an es block that uses tú and the same terms as locale-es (organización, diseño, marcador). A parity test keeps the keys and placeholders of en, de and es aligned."
|
|
19
|
+
},
|
|
2
20
|
{
|
|
3
21
|
"version": "0.370.0",
|
|
4
22
|
"type": "fix",
|