@cosmicdrift/kumiko-document-designer 0.371.1 → 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 +3 -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 +12 -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",
|
|
@@ -187,6 +189,7 @@ export const documentDesignerTranslations = {
|
|
|
187
189
|
"documentDesigner.resources.title": "Recursos",
|
|
188
190
|
"documentDesigner.resources.upload": "+ Subir",
|
|
189
191
|
"documentDesigner.settings.fileName": "Nombre del archivo",
|
|
192
|
+
"documentDesigner.settings.fileNamePlaceholder": "factura-{{number}}.pdf",
|
|
190
193
|
"documentDesigner.settings.landscape": "Horizontal",
|
|
191
194
|
"documentDesigner.settings.layout": "Diseño",
|
|
192
195
|
"documentDesigner.settings.layoutHeaderFooter": "Encabezado y pie de página del diseño",
|
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,16 @@
|
|
|
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
|
+
},
|
|
2
14
|
{
|
|
3
15
|
"version": "0.371.1",
|
|
4
16
|
"type": "fix",
|