@cosmicdrift/kumiko-bundled-features 0.377.0 → 0.378.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/cap-counter/stock-cap-guard.js +8 -0
- package/dist/config/web/i18n.js +1 -0
- package/dist/inbound-mail-foundation/handlers/list-accounts.query.d.ts +1 -1
- package/dist/inbound-mail-foundation/handlers/list-accounts.query.js +11 -3
- package/dist/mail-foundation/i18n.d.ts +5 -2
- package/dist/mail-foundation/i18n.js +3 -2
- package/dist/mail-transport-smtp/i18n.d.ts +1 -1
- package/dist/mail-transport-smtp/i18n.js +1 -1
- package/dist/template-resolver/compile-document-body.d.ts +3 -0
- package/dist/template-resolver/compile-document-body.js +50 -3
- package/dist/template-resolver/constants.d.ts +3 -0
- package/dist/template-resolver/constants.js +2 -0
- package/dist/template-resolver/document-blocks.d.ts +2 -2
- package/dist/template-resolver/document-blocks.js +1 -0
- package/dist/template-resolver/document-images.d.ts +2 -0
- package/dist/template-resolver/document-images.js +28 -0
- package/dist/template-resolver/document-sanitizer.d.ts +1 -1
- package/dist/template-resolver/document-sanitizer.js +20 -0
- package/dist/template-resolver/document-schema.d.ts +2 -1
- package/dist/template-resolver/document-schema.js +4 -0
- package/dist/template-resolver/feature.d.ts +9 -0
- package/dist/template-resolver/feature.js +58 -33
- package/dist/template-resolver/handlers/collection-item.query.js +2 -1
- package/dist/template-resolver/handlers/collection-list.query.js +2 -1
- package/dist/template-resolver/handlers/collection-set.write.js +2 -2
- package/dist/template-resolver/handlers/collection-shared.d.ts +1 -2
- package/dist/template-resolver/handlers/collection-shared.js +0 -4
- package/dist/template-resolver/handlers/copy-locale.write.d.ts +2 -1
- package/dist/template-resolver/handlers/copy-locale.write.js +67 -63
- package/dist/template-resolver/handlers/discard-draft.write.d.ts +3 -2
- package/dist/template-resolver/handlers/discard-draft.write.js +18 -6
- package/dist/template-resolver/handlers/import-bundle.write.d.ts +2 -1
- package/dist/template-resolver/handlers/import-bundle.write.js +30 -25
- package/dist/template-resolver/handlers/overview.query.d.ts +2 -1
- package/dist/template-resolver/handlers/overview.query.js +46 -42
- package/dist/template-resolver/handlers/patch-draft.write.d.ts +2 -1
- package/dist/template-resolver/handlers/patch-draft.write.js +92 -86
- package/dist/template-resolver/handlers/reset-to-system.write.d.ts +2 -1
- package/dist/template-resolver/handlers/reset-to-system.write.js +51 -44
- package/dist/template-resolver/handlers/resource-link-pdf.write.d.ts +2 -1
- package/dist/template-resolver/handlers/resource-link-pdf.write.js +34 -28
- package/dist/template-resolver/handlers/resource-remove.write.d.ts +2 -1
- package/dist/template-resolver/handlers/resource-remove.write.js +39 -33
- package/dist/template-resolver/handlers/resource-upload.write.d.ts +2 -1
- package/dist/template-resolver/handlers/resource-upload.write.js +104 -96
- package/dist/template-resolver/handlers/send-test-shared.d.ts +5 -0
- package/dist/template-resolver/handlers/send-test-shared.js +19 -0
- package/dist/template-resolver/handlers/send-test-status.query.d.ts +9 -0
- package/dist/template-resolver/handlers/send-test-status.query.js +18 -0
- package/dist/template-resolver/handlers/send-test.write.d.ts +2 -1
- package/dist/template-resolver/handlers/send-test.write.js +87 -89
- package/dist/template-resolver/handlers/set.write.d.ts +2 -1
- package/dist/template-resolver/handlers/set.write.js +94 -90
- package/dist/template-resolver/handlers/toggle-status.write.d.ts +6 -7
- package/dist/template-resolver/handlers/toggle-status.write.js +28 -10
- package/dist/template-resolver/handlers/upsert-tenant.write.d.ts +2 -1
- package/dist/template-resolver/handlers/upsert-tenant.write.js +30 -24
- package/dist/template-resolver/index.d.ts +4 -2
- package/dist/template-resolver/index.js +3 -1
- package/dist/template-resolver/qualified-names.d.ts +2 -0
- package/dist/template-resolver/qualified-names.js +2 -0
- package/dist/template-resolver/template-edit-policy.d.ts +22 -0
- package/dist/template-resolver/template-edit-policy.js +45 -0
- package/dist/template-resolver/template-preview.js +7 -2
- package/dist/template-resolver/template-validation.js +33 -13
- package/dist/template-resolver/web/document-editor-extensions.d.ts +1 -0
- package/dist/template-resolver/web/document-editor-extensions.js +21 -1
- package/dist/template-resolver/web/document-tiptap-editor.d.ts +1 -1
- package/dist/template-resolver/web/document-tiptap-editor.js +22 -6
- package/dist/template-resolver/web/document-toolbar-blocks.d.ts +2 -0
- package/dist/template-resolver/web/i18n.js +4 -0
- package/package.json +8 -8
- package/src/cap-counter/changes.json +12 -0
- package/src/inbound-mail-foundation/changes.json +6 -0
- package/src/mail-foundation/changes.json +6 -0
- package/src/template-resolver/changes.json +20 -0
|
@@ -38,6 +38,7 @@ const scalarVariableSchema = z.discriminatedUnion("type", [
|
|
|
38
38
|
]);
|
|
39
39
|
const variableDefSchema = z.union([
|
|
40
40
|
scalarVariableSchema,
|
|
41
|
+
z.object({ ...variableCommon, type: z.literal("images") }),
|
|
41
42
|
z.object({
|
|
42
43
|
...variableCommon,
|
|
43
44
|
type: z.literal("list"),
|
|
@@ -74,7 +75,9 @@ function listItemFields(variables, listName) {
|
|
|
74
75
|
const def = listName !== null && Object.hasOwn(variables, listName) ? variables[listName] : undefined;
|
|
75
76
|
return def?.type === "list" ? def.item : {};
|
|
76
77
|
}
|
|
77
|
-
|
|
78
|
+
// An images variable holds file ids: as text it would leak them, so only an images block
|
|
79
|
+
// (and a condition on its presence) may name it.
|
|
80
|
+
function isKnownName(name, scope, itemFields, allowImages = false) {
|
|
78
81
|
const { variables } = scope;
|
|
79
82
|
const root = rootName(name);
|
|
80
83
|
// Declared variables and item fields are scalars: a dotted path under one never renders a value.
|
|
@@ -82,12 +85,15 @@ function isKnownName(name, scope, itemFields) {
|
|
|
82
85
|
return { known: true, used: "" };
|
|
83
86
|
if (scope.systemPlaceholders.has(root))
|
|
84
87
|
return { known: true, used: "" };
|
|
85
|
-
if (Object.hasOwn(variables, name))
|
|
86
|
-
return
|
|
88
|
+
if (Object.hasOwn(variables, name)) {
|
|
89
|
+
return variables[name]?.type === "images" && !allowImages
|
|
90
|
+
? { known: false, used: "" }
|
|
91
|
+
: { known: true, used: name };
|
|
92
|
+
}
|
|
87
93
|
return { known: false, used: "" };
|
|
88
94
|
}
|
|
89
|
-
function recordName(name, itemFields, scope) {
|
|
90
|
-
const resolution = isKnownName(name, scope, itemFields);
|
|
95
|
+
function recordName(name, itemFields, scope, allowImages = false) {
|
|
96
|
+
const resolution = isKnownName(name, scope, itemFields, allowImages);
|
|
91
97
|
if (!resolution.known)
|
|
92
98
|
scope.unknown.add(name);
|
|
93
99
|
else if (resolution.used !== "")
|
|
@@ -96,10 +102,21 @@ function recordName(name, itemFields, scope) {
|
|
|
96
102
|
function enclosingEachName(element) {
|
|
97
103
|
return element.closest("tr[data-each]")?.getAttribute("data-each") ?? null;
|
|
98
104
|
}
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
105
|
+
function nameAttributeOf(element) {
|
|
106
|
+
const tag = element.tagName.toLowerCase();
|
|
107
|
+
if (tag === "span")
|
|
108
|
+
return "data-variable";
|
|
109
|
+
if (tag !== "div")
|
|
110
|
+
return undefined;
|
|
111
|
+
return element.hasAttribute("data-images") ? "data-images" : "data-steps";
|
|
112
|
+
}
|
|
113
|
+
function recordImagesName(name, scope) {
|
|
114
|
+
const def = Object.hasOwn(scope.variables, name) ? scope.variables[name] : undefined;
|
|
115
|
+
if (def?.type === "images")
|
|
116
|
+
scope.used.add(name);
|
|
117
|
+
else
|
|
118
|
+
scope.unknown.add(name);
|
|
119
|
+
}
|
|
103
120
|
function scanRepeatRows(root, scope) {
|
|
104
121
|
for (const element of Array.from(root.querySelectorAll("tr[data-each]"))) {
|
|
105
122
|
const listName = element.getAttribute("data-each") ?? "";
|
|
@@ -113,15 +130,18 @@ function scanRepeatRows(root, scope) {
|
|
|
113
130
|
function scanConditions(root, scope) {
|
|
114
131
|
for (const element of Array.from(root.querySelectorAll(`[${DOCUMENT_CONDITION_ATTRIBUTE}]`))) {
|
|
115
132
|
const itemFields = listItemFields(scope.variables, enclosingEachName(conditionScopeElement(element)));
|
|
116
|
-
recordName(element.getAttribute(DOCUMENT_CONDITION_ATTRIBUTE) ?? "", itemFields, scope);
|
|
133
|
+
recordName(element.getAttribute(DOCUMENT_CONDITION_ATTRIBUTE) ?? "", itemFields, scope, true);
|
|
117
134
|
}
|
|
118
135
|
}
|
|
119
136
|
function scanReferences(root, scope) {
|
|
120
|
-
for (const element of Array.from(root.querySelectorAll("span[data-variable], div[data-steps], a[href], img[data-resource]"))) {
|
|
137
|
+
for (const element of Array.from(root.querySelectorAll("span[data-variable], div[data-steps], div[data-images], a[href], img[data-resource]"))) {
|
|
121
138
|
const itemFields = listItemFields(scope.variables, enclosingEachName(element));
|
|
122
139
|
const tag = element.tagName.toLowerCase();
|
|
123
|
-
const nameAttribute =
|
|
124
|
-
if (nameAttribute
|
|
140
|
+
const nameAttribute = nameAttributeOf(element);
|
|
141
|
+
if (nameAttribute === "data-images") {
|
|
142
|
+
recordImagesName(element.getAttribute(nameAttribute) ?? "", scope);
|
|
143
|
+
}
|
|
144
|
+
else if (nameAttribute !== undefined) {
|
|
125
145
|
recordName(element.getAttribute(nameAttribute) ?? "", itemFields, scope);
|
|
126
146
|
}
|
|
127
147
|
else if (tag === "a") {
|
|
@@ -7,6 +7,7 @@ export type DocumentEditorLabels = {
|
|
|
7
7
|
readonly pageBreak: string;
|
|
8
8
|
readonly button?: string;
|
|
9
9
|
readonly steps?: string;
|
|
10
|
+
readonly images?: string;
|
|
10
11
|
/** Canvas block label of an image, e.g. "Image · inline". */
|
|
11
12
|
readonly imageBlock?: string;
|
|
12
13
|
readonly variableLabels?: Readonly<Record<string, string>>;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { ListKeymap } from "@tiptap/extension-list";
|
|
2
2
|
import { Dropcursor, Gapcursor, Placeholder, UndoRedo } from "@tiptap/extensions";
|
|
3
|
-
import { DocumentButton, DocumentPageBreak, DocumentResource, DocumentSteps, DocumentVariable, documentExtensions, } from "../document-schema.js";
|
|
3
|
+
import { DocumentButton, DocumentImages, DocumentPageBreak, DocumentResource, DocumentSteps, DocumentVariable, documentExtensions, } from "../document-schema.js";
|
|
4
4
|
import { readDocumentStepsAttribute } from "../document-steps.js";
|
|
5
5
|
const DOCUMENT_RESOURCE_SIZE_TOKEN = "{size}";
|
|
6
6
|
export const DOCUMENT_BLOCK_ATTRIBUTE = "data-document-block";
|
|
@@ -134,6 +134,24 @@ function createStepsView(labels) {
|
|
|
134
134
|
return { dom };
|
|
135
135
|
};
|
|
136
136
|
}
|
|
137
|
+
// A sketch only: the photos are record data, known at render time.
|
|
138
|
+
function createImagesView(labels) {
|
|
139
|
+
return ({ node }) => {
|
|
140
|
+
const dom = createElementWithClass("div", "relative my-3 rounded border border-dashed border-input px-3 pb-3 pt-4");
|
|
141
|
+
dom.setAttribute(DOCUMENT_BLOCK_ATTRIBUTE, "");
|
|
142
|
+
dom.contentEditable = "false";
|
|
143
|
+
const variable = String(node.attrs["variable"]);
|
|
144
|
+
const variableLabels = labels.variableLabels ?? {};
|
|
145
|
+
const variableLabel = Object.hasOwn(variableLabels, variable)
|
|
146
|
+
? variableLabels[variable]
|
|
147
|
+
: undefined;
|
|
148
|
+
dom.append(createBlockLabel(`${labels.images ?? "Photos"} · ${variableLabel ?? variable}`));
|
|
149
|
+
const grid = createElementWithClass("div", "grid grid-cols-2 gap-1.5");
|
|
150
|
+
grid.append(createElementWithClass("div", "h-12 rounded bg-muted"), createElementWithClass("div", "h-12 rounded bg-muted"));
|
|
151
|
+
dom.append(grid);
|
|
152
|
+
return { dom };
|
|
153
|
+
};
|
|
154
|
+
}
|
|
137
155
|
// Replaced by name, not appended: the schema must keep exactly one node per
|
|
138
156
|
// name, and only the editor view differs from the shared document schema.
|
|
139
157
|
export function createDocumentEditorExtensions(labels) {
|
|
@@ -151,6 +169,8 @@ export function createDocumentEditorExtensions(labels) {
|
|
|
151
169
|
: extension;
|
|
152
170
|
case DocumentSteps.name:
|
|
153
171
|
return DocumentSteps.extend({ addNodeView: () => createStepsView(labels) });
|
|
172
|
+
case DocumentImages.name:
|
|
173
|
+
return DocumentImages.extend({ addNodeView: () => createImagesView(labels) });
|
|
154
174
|
case DocumentPageBreak.name:
|
|
155
175
|
return DocumentPageBreak.extend({
|
|
156
176
|
addNodeView: () => createPageBreakView(labels.pageBreak),
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import { type ReactNode } from "react";
|
|
2
2
|
import { type DocumentEditorProps } from "./document-toolbar-blocks.ts";
|
|
3
|
-
export default function DocumentTiptapEditor({ id, value, onChange, variables, readOnly, blocks, variableLabels, systemVariables, resourcePreviews, chrome, toolbar, renderPaper, toolbarHost, onEditorReady, }: DocumentEditorProps): ReactNode;
|
|
3
|
+
export default function DocumentTiptapEditor({ id, value, onChange, variables, readOnly, blocks: configuredBlocks, imageVariables, variableLabels, systemVariables, resourcePreviews, chrome, toolbar, renderPaper, toolbarHost, onEditorReady, }: DocumentEditorProps): ReactNode;
|
|
@@ -10,7 +10,7 @@ import { cn } from "@cosmicdrift/kumiko-renderer/web";
|
|
|
10
10
|
import { isOneOf } from "@cosmicdrift/kumiko-types/type-guards";
|
|
11
11
|
import { EditorContent, useEditor, useEditorState } from "@tiptap/react";
|
|
12
12
|
import { BubbleMenu } from "@tiptap/react/menus";
|
|
13
|
-
import { AlignCenter, AlignJustify, AlignLeft, AlignRight, Bold as BoldIcon, Braces, ChevronDown, Image as ImageIcon, Italic as ItalicIcon, Link as LinkIcon, List, ListChecks, ListOrdered, Minus, MousePointerClick, Repeat, Scissors, Table as TableIcon, Underline as UnderlineIcon, } from "lucide-react";
|
|
13
|
+
import { AlignCenter, AlignJustify, AlignLeft, AlignRight, Bold as BoldIcon, Braces, ChevronDown, GalleryThumbnails, Image as ImageIcon, Italic as ItalicIcon, Link as LinkIcon, List, ListChecks, ListOrdered, Minus, MousePointerClick, Repeat, Scissors, Table as TableIcon, Underline as UnderlineIcon, } from "lucide-react";
|
|
14
14
|
import { Fragment, useEffect, useMemo, useState } from "react";
|
|
15
15
|
import { createPortal } from "react-dom";
|
|
16
16
|
import { DOCUMENT_TEXT_ALIGNMENTS, isAllowedDocumentLinkHref, isDocumentPlaceholderHref, isDocumentReferenceName, isDocumentResourceKey, } from "../document-schema.js";
|
|
@@ -35,6 +35,7 @@ const BLOCK_PANEL = {
|
|
|
35
35
|
repeat: "repeat",
|
|
36
36
|
align: "align",
|
|
37
37
|
steps: "steps",
|
|
38
|
+
images: "images",
|
|
38
39
|
};
|
|
39
40
|
const ALIGNMENT_ICONS = {
|
|
40
41
|
left: _jsx(AlignLeft, { size: 16 }),
|
|
@@ -243,7 +244,11 @@ function InsertPanel({ editor, editorId, kind, variables, variableLabels, onClos
|
|
|
243
244
|
}
|
|
244
245
|
insertAndClose({ type: "steps", attrs: { variable: selectedName, steps } });
|
|
245
246
|
};
|
|
246
|
-
const needsVariables = kind === "button" ||
|
|
247
|
+
const needsVariables = kind === "button" ||
|
|
248
|
+
kind === "repeat" ||
|
|
249
|
+
kind === "placeholder" ||
|
|
250
|
+
kind === "steps" ||
|
|
251
|
+
kind === "images";
|
|
247
252
|
const noVariables = needsVariables && selectable.length === 0;
|
|
248
253
|
const idPrefix = `${editorId}-panel`;
|
|
249
254
|
return (_jsxs("div", { className: "flex flex-col gap-2 border-b border-input bg-muted/40 p-2", children: [noVariables && (_jsx("p", { className: "text-sm text-muted-foreground", children: t("template-resolver.documentEditor.noVariables") })), kind === "link" && (_jsx(Field, { id: `${idPrefix}-href`, label: t("template-resolver.documentEditor.linkHref"), children: _jsx(Input, { kind: "text", id: `${idPrefix}-href`, name: "href", value: href, onChange: setHref, placeholder: t("template-resolver.documentEditor.linkHrefPlaceholder") }) })), kind === "placeholder" && !noVariables && (_jsx("div", { className: "flex flex-wrap gap-1", children: _jsx(VariableChips, { variables: selectable, labels: variableLabels, onInsert: (name) => insertAndClose({ type: "variable", attrs: { name } }) }) })), kind === "align" && (_jsx("div", { className: "flex flex-wrap gap-1", children: DOCUMENT_TEXT_ALIGNMENTS.map((alignment) => (_jsxs(Button, { type: "button", variant: "secondary", size: "sm", onClick: () => {
|
|
@@ -254,7 +259,7 @@ function InsertPanel({ editor, editorId, kind, variables, variableLabels, onClos
|
|
|
254
259
|
.updateAttributes("heading", { textAlign: alignment })
|
|
255
260
|
.run();
|
|
256
261
|
onClose();
|
|
257
|
-
}, children: [ALIGNMENT_ICONS[alignment], t(ALIGNMENT_LABEL_KEYS[alignment])] }, alignment))) })), kind === "image" && (_jsxs(_Fragment, { children: [_jsx(Field, { id: `${idPrefix}-resource`, label: t("template-resolver.documentEditor.resourceKey"), children: _jsx(Input, { kind: "text", id: `${idPrefix}-resource`, name: "resource", value: resource, onChange: setResource }) }), _jsx(Field, { id: `${idPrefix}-alt`, label: t("template-resolver.documentEditor.imageAlt"), children: _jsx(Input, { kind: "text", id: `${idPrefix}-alt`, name: "alt", value: alt, onChange: setAlt }) })] })), kind === "button" && !noVariables && (_jsxs(_Fragment, { children: [_jsx(NameSelect, { id: `${idPrefix}-target`, label: t("template-resolver.documentEditor.buttonTarget"), names: selectable, labels: variableLabels, value: selectedName, onChange: setChosenName }), _jsx(Field, { id: `${idPrefix}-text`, label: t("template-resolver.documentEditor.buttonText"), children: _jsx(Input, { kind: "text", id: `${idPrefix}-text`, name: "text", value: buttonText, onChange: setButtonText }) })] })), kind === "repeat" && !noVariables && (_jsx(NameSelect, { id: `${idPrefix}-list`, label: t("template-resolver.documentEditor.repeatList"), names: selectable, labels: variableLabels, value: selectedName, onChange: setChosenName })), kind === "steps" && !noVariables && (_jsxs(_Fragment, { children: [_jsx(NameSelect, { id: `${idPrefix}-steps-variable`, label: t("template-resolver.documentEditor.stepsVariable"), names: selectable, labels: variableLabels, value: selectedName, onChange: setChosenName }), _jsx(Field, { id: `${idPrefix}-steps-list`, label: t("template-resolver.documentEditor.stepsList"), children: _jsx(Input, { kind: "text", id: `${idPrefix}-steps-list`, name: "steps", value: stepsText, onChange: setStepsText }) })] })), error !== null && (_jsx("p", { role: "alert", className: "text-sm text-destructive", children: error })), _jsx("div", { className: "flex flex-wrap gap-2", children: _jsx(PanelActions, { onCancel: onClose, ...(kind === "link" && { onInsert: submitLink }), ...(kind === "link" &&
|
|
262
|
+
}, children: [ALIGNMENT_ICONS[alignment], t(ALIGNMENT_LABEL_KEYS[alignment])] }, alignment))) })), kind === "image" && (_jsxs(_Fragment, { children: [_jsx(Field, { id: `${idPrefix}-resource`, label: t("template-resolver.documentEditor.resourceKey"), children: _jsx(Input, { kind: "text", id: `${idPrefix}-resource`, name: "resource", value: resource, onChange: setResource }) }), _jsx(Field, { id: `${idPrefix}-alt`, label: t("template-resolver.documentEditor.imageAlt"), children: _jsx(Input, { kind: "text", id: `${idPrefix}-alt`, name: "alt", value: alt, onChange: setAlt }) })] })), kind === "button" && !noVariables && (_jsxs(_Fragment, { children: [_jsx(NameSelect, { id: `${idPrefix}-target`, label: t("template-resolver.documentEditor.buttonTarget"), names: selectable, labels: variableLabels, value: selectedName, onChange: setChosenName }), _jsx(Field, { id: `${idPrefix}-text`, label: t("template-resolver.documentEditor.buttonText"), children: _jsx(Input, { kind: "text", id: `${idPrefix}-text`, name: "text", value: buttonText, onChange: setButtonText }) })] })), kind === "repeat" && !noVariables && (_jsx(NameSelect, { id: `${idPrefix}-list`, label: t("template-resolver.documentEditor.repeatList"), names: selectable, labels: variableLabels, value: selectedName, onChange: setChosenName })), kind === "images" && !noVariables && (_jsx(NameSelect, { id: `${idPrefix}-images-variable`, label: t("template-resolver.documentEditor.imagesVariable"), names: selectable, labels: variableLabels, value: selectedName, onChange: setChosenName })), kind === "steps" && !noVariables && (_jsxs(_Fragment, { children: [_jsx(NameSelect, { id: `${idPrefix}-steps-variable`, label: t("template-resolver.documentEditor.stepsVariable"), names: selectable, labels: variableLabels, value: selectedName, onChange: setChosenName }), _jsx(Field, { id: `${idPrefix}-steps-list`, label: t("template-resolver.documentEditor.stepsList"), children: _jsx(Input, { kind: "text", id: `${idPrefix}-steps-list`, name: "steps", value: stepsText, onChange: setStepsText }) })] })), error !== null && (_jsx("p", { role: "alert", className: "text-sm text-destructive", children: error })), _jsx("div", { className: "flex flex-wrap gap-2", children: _jsx(PanelActions, { onCancel: onClose, ...(kind === "link" && { onInsert: submitLink }), ...(kind === "link" &&
|
|
258
263
|
editor.isActive("link") && {
|
|
259
264
|
onRemoveLink: () => {
|
|
260
265
|
editor.chain().focus().extendMarkRange("link").unsetLink().run();
|
|
@@ -268,9 +273,12 @@ function InsertPanel({ editor, editorId, kind, variables, variableLabels, onClos
|
|
|
268
273
|
content: buttonText ? [{ type: "text", text: buttonText }] : [],
|
|
269
274
|
}),
|
|
270
275
|
}), ...(kind === "repeat" &&
|
|
271
|
-
!noVariables && { onInsert: () => insertAndClose(repeatTableContent(selectedName)) }), ...(kind === "steps" && !noVariables && { onInsert: submitSteps })
|
|
276
|
+
!noVariables && { onInsert: () => insertAndClose(repeatTableContent(selectedName)) }), ...(kind === "steps" && !noVariables && { onInsert: submitSteps }), ...(kind === "images" &&
|
|
277
|
+
!noVariables && {
|
|
278
|
+
onInsert: () => insertAndClose({ type: "images", attrs: { variable: selectedName } }),
|
|
279
|
+
}) }) })] }));
|
|
272
280
|
}
|
|
273
|
-
export default function DocumentTiptapEditor({ id, value, onChange, variables, readOnly, blocks = MAIL_DOCUMENT_BLOCKS, variableLabels = NO_LABELS, systemVariables = NO_NAMES, resourcePreviews = NO_PREVIEWS, chrome = "default", toolbar = "full", renderPaper, toolbarHost, onEditorReady, }) {
|
|
281
|
+
export default function DocumentTiptapEditor({ id, value, onChange, variables, readOnly, blocks: configuredBlocks = MAIL_DOCUMENT_BLOCKS, imageVariables = NO_NAMES, variableLabels = NO_LABELS, systemVariables = NO_NAMES, resourcePreviews = NO_PREVIEWS, chrome = "default", toolbar = "full", renderPaper, toolbarHost, onEditorReady, }) {
|
|
274
282
|
const t = useTranslation();
|
|
275
283
|
const { ActionOverflowMenu, Button, Field, Input } = usePrimitives();
|
|
276
284
|
const [openPanel, setOpenPanel] = useState(null);
|
|
@@ -279,12 +287,17 @@ export default function DocumentTiptapEditor({ id, value, onChange, variables, r
|
|
|
279
287
|
const canvas = chrome === "canvas";
|
|
280
288
|
const compact = toolbar === "compact" && !canvas;
|
|
281
289
|
const hosted = toolbarHost !== undefined;
|
|
290
|
+
// A photo block needs an images variable to bind to; without one the button would open an empty panel.
|
|
291
|
+
const blocks = imageVariables.length === 0
|
|
292
|
+
? configuredBlocks.filter((block) => block !== "images")
|
|
293
|
+
: configuredBlocks;
|
|
282
294
|
const placeholderLabel = t("template-resolver.documentEditor.placeholder");
|
|
283
295
|
const imageLabel = t("template-resolver.documentEditor.image");
|
|
284
296
|
const imageBlockLabel = t("template-resolver.documentEditor.imageBlock");
|
|
285
297
|
const pageBreakLabel = t("template-resolver.documentEditor.pageBreak");
|
|
286
298
|
const buttonLabel = t("template-resolver.documentEditor.button");
|
|
287
299
|
const stepsLabel = t("template-resolver.documentEditor.steps");
|
|
300
|
+
const imagesLabel = t("template-resolver.documentEditor.images");
|
|
288
301
|
const stableLabels = useStableByKey(variableLabels, JSON.stringify(variableLabels));
|
|
289
302
|
const stableSystem = useStableByKey(systemVariables, systemVariables.join("\n"));
|
|
290
303
|
const stablePreviews = useStableByKey(resourcePreviews, previewsKey(resourcePreviews));
|
|
@@ -294,6 +307,7 @@ export default function DocumentTiptapEditor({ id, value, onChange, variables, r
|
|
|
294
307
|
pageBreak: pageBreakLabel,
|
|
295
308
|
button: buttonLabel,
|
|
296
309
|
steps: stepsLabel,
|
|
310
|
+
images: imagesLabel,
|
|
297
311
|
imageBlock: imageBlockLabel,
|
|
298
312
|
variableLabels: stableLabels,
|
|
299
313
|
systemVariables: stableSystem,
|
|
@@ -306,6 +320,7 @@ export default function DocumentTiptapEditor({ id, value, onChange, variables, r
|
|
|
306
320
|
pageBreakLabel,
|
|
307
321
|
buttonLabel,
|
|
308
322
|
stepsLabel,
|
|
323
|
+
imagesLabel,
|
|
309
324
|
stableLabels,
|
|
310
325
|
stableSystem,
|
|
311
326
|
stablePreviews,
|
|
@@ -415,6 +430,7 @@ export default function DocumentTiptapEditor({ id, value, onChange, variables, r
|
|
|
415
430
|
pageBreak: { label: pageBreakLabel, icon: _jsx(Scissors, { size: 16 }) },
|
|
416
431
|
align: { label: t("template-resolver.documentEditor.align"), icon: _jsx(AlignLeft, { size: 16 }) },
|
|
417
432
|
steps: { label: stepsLabel, icon: _jsx(ListChecks, { size: 16 }) },
|
|
433
|
+
images: { label: imagesLabel, icon: _jsx(GalleryThumbnails, { size: 16 }) },
|
|
418
434
|
};
|
|
419
435
|
const repeatTable = activeState?.repeatTable ?? null;
|
|
420
436
|
const repeatListLabel = repeatTable !== null && repeatTable.lists.length === 1
|
|
@@ -507,7 +523,7 @@ export default function DocumentTiptapEditor({ id, value, onChange, variables, r
|
|
|
507
523
|
: t("template-resolver.documentEditor.buttonTarget"), ": ", selectedBlock.type === "button" ? (_jsx("strong", { children: buttonTargetLabel(selectedBlock.detail, variableLabels) })) : (_jsx("code", { children: selectedBlock.detail }))] }), _jsx(Button, { type: "button", size: "sm", variant: "danger-ghost", onClick: () => {
|
|
508
524
|
editor.chain().focus().deleteNode(selectedBlock.type).run();
|
|
509
525
|
}, children: t("template-resolver.documentEditor.removeBlock") })] })) : (repeatTable !== null && (_jsxs("div", { "data-testid": "document-repeat-bar", className: BUBBLE_BAR_CLASS, children: [_jsx("span", { className: "px-1.5 text-[10px] uppercase tracking-wide text-muted-foreground", children: repeatBarLabel }), _jsx(Button, { type: "button", variant: "secondary", size: "sm", pressed: repeatTable.headerRepeats, onClick: () => toggleRepeatHeader(editor), children: t("template-resolver.documentEditor.repeatHeader") })] }))) }, mountKeyOf(editor)));
|
|
510
|
-
const insertPanel = openPanel !== null && !readOnly && (_jsx(InsertPanel, { editor: editor, editorId: id, kind: openPanel, variables: variables, variableLabels: stableLabels, onClose: () => setOpenPanel(null) }, openPanel));
|
|
526
|
+
const insertPanel = openPanel !== null && !readOnly && (_jsx(InsertPanel, { editor: editor, editorId: id, kind: openPanel, variables: openPanel === "images" ? imageVariables : variables, variableLabels: stableLabels, onClose: () => setOpenPanel(null) }, openPanel));
|
|
511
527
|
if (hosted) {
|
|
512
528
|
return (_jsxs(_Fragment, { children: [toolbarHost !== null &&
|
|
513
529
|
createPortal(_jsxs(_Fragment, { children: [toolbarBar, insertPanel] }), toolbarHost), !readOnly && bubbleMenu, editorContent] }));
|
|
@@ -17,6 +17,8 @@ export type DocumentResourcePreview = {
|
|
|
17
17
|
export type DocumentEditorChrome = "default" | "canvas";
|
|
18
18
|
export type DocumentEditorProps = ContentEditorProps & {
|
|
19
19
|
readonly blocks?: readonly DocumentToolbarBlock[];
|
|
20
|
+
/** Names of the template's images variables; the photo block offers exactly these and is hidden without any. */
|
|
21
|
+
readonly imageVariables?: readonly string[];
|
|
20
22
|
/** Display label per variable name, as inserted (list item fields use the
|
|
21
23
|
* dotted form, e.g. "lines.description"). A chip shows label and name. */
|
|
22
24
|
readonly variableLabels?: Readonly<Record<string, string>>;
|
|
@@ -39,6 +39,8 @@ export const defaultTranslations = {
|
|
|
39
39
|
"template-resolver.documentEditor.removeLink": "Remove link",
|
|
40
40
|
"template-resolver.documentEditor.repeat": "Repeat",
|
|
41
41
|
"template-resolver.documentEditor.repeatList": "List to repeat",
|
|
42
|
+
"template-resolver.documentEditor.images": "Photos",
|
|
43
|
+
"template-resolver.documentEditor.imagesVariable": "Photos from",
|
|
42
44
|
"template-resolver.documentEditor.steps": "Steps",
|
|
43
45
|
"template-resolver.documentEditor.stepsVariable": "Current step from",
|
|
44
46
|
"template-resolver.documentEditor.stepsList": "Steps, separated by commas (value=label)",
|
|
@@ -52,6 +54,7 @@ export const defaultTranslations = {
|
|
|
52
54
|
"template-resolver.editor.saving": "Saving…",
|
|
53
55
|
"template-resolver.editor.created": "Created.",
|
|
54
56
|
"template-resolver.editor.saved": "Saved.",
|
|
57
|
+
"templateResolver.errors.document.imagesBlockPdfOnly": "Photo blocks are only allowed in PDF documents.",
|
|
55
58
|
"templateResolver.errors.document.buttonHrefNotPlaceholder": "The button target “{detail}” must be a placeholder.",
|
|
56
59
|
"templateResolver.errors.document.disallowedLinkHref": "Link target “{detail}” is not allowed; use https, http, mailto, tel or a placeholder.",
|
|
57
60
|
"templateResolver.errors.document.documentRequiresHtmlFormat": "This template kind requires the html format, got “{detail}”.",
|
|
@@ -91,6 +94,7 @@ export const defaultTranslations = {
|
|
|
91
94
|
"templateResolver.errors.noSystemTemplate": "There is no active system template to reset to.",
|
|
92
95
|
"templateResolver.errors.noDraft": "There is no draft to discard.",
|
|
93
96
|
"templateResolver.errors.systemOnlyTemplate": "Only the platform maintains this template; it cannot be overridden per tenant.",
|
|
97
|
+
"templateResolver.errors.templateNotTenantEditable": "Only the platform may edit this template.",
|
|
94
98
|
"templateResolver.errors.template.invalidMeta": "The template settings are invalid.",
|
|
95
99
|
"templateResolver.errors.template.meta.subject.required": "The subject is missing.",
|
|
96
100
|
"templateResolver.errors.template.meta.subject.invalid": "The subject is invalid (at most 200 characters, no line breaks).",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmicdrift/kumiko-bundled-features",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.378.0",
|
|
4
4
|
"description": "Built-in features — tenant, user, auth, delivery. The stuff you'd rewrite anyway, already typed.",
|
|
5
5
|
"license": "BUSL-1.1",
|
|
6
6
|
"author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
|
|
@@ -540,9 +540,9 @@
|
|
|
540
540
|
}
|
|
541
541
|
},
|
|
542
542
|
"dependencies": {
|
|
543
|
-
"@cosmicdrift/kumiko-framework": "0.
|
|
544
|
-
"@cosmicdrift/kumiko-renderer": "0.
|
|
545
|
-
"@cosmicdrift/kumiko-types": "0.
|
|
543
|
+
"@cosmicdrift/kumiko-framework": "0.378.0",
|
|
544
|
+
"@cosmicdrift/kumiko-renderer": "0.378.0",
|
|
545
|
+
"@cosmicdrift/kumiko-types": "0.378.0",
|
|
546
546
|
"@mollie/api-client": "^4.5.0",
|
|
547
547
|
"@node-rs/argon2": "^2.0.2",
|
|
548
548
|
"@tiptap/core": "3.31.3",
|
|
@@ -1107,12 +1107,12 @@
|
|
|
1107
1107
|
],
|
|
1108
1108
|
"devDependencies": {
|
|
1109
1109
|
"@testing-library/user-event": "^14.6.1",
|
|
1110
|
-
"@cosmicdrift/kumiko-locale-de": "0.
|
|
1111
|
-
"@cosmicdrift/kumiko-locale-es": "0.
|
|
1112
|
-
"@cosmicdrift/kumiko-renderer-mail-html": "0.
|
|
1110
|
+
"@cosmicdrift/kumiko-locale-de": "0.378.0",
|
|
1111
|
+
"@cosmicdrift/kumiko-locale-es": "0.378.0",
|
|
1112
|
+
"@cosmicdrift/kumiko-renderer-mail-html": "0.378.0",
|
|
1113
1113
|
"jsqr": "^1.4.0",
|
|
1114
1114
|
"unpdf": "^1.8.1",
|
|
1115
|
-
"@cosmicdrift/kumiko-testing": "0.
|
|
1115
|
+
"@cosmicdrift/kumiko-testing": "0.378.0"
|
|
1116
1116
|
},
|
|
1117
1117
|
"peerDependencies": {
|
|
1118
1118
|
"@cosmicdrift/kumiko-testing": ">=0.366.0 <1.0.0"
|
|
@@ -1,4 +1,16 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "0.378.0",
|
|
4
|
+
"type": "fix",
|
|
5
|
+
"title": "Stock caps are atomic under parallel writes",
|
|
6
|
+
"detail": "withStockCap now takes a transaction-scoped advisory lock per tenant and cap, so concurrent creates cannot both pass the check and exceed the cap."
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
"version": "0.378.0",
|
|
10
|
+
"type": "improvement",
|
|
11
|
+
"title": "withStockCap no longer widens the handler's grants",
|
|
12
|
+
"detail": "withStockCap serialises through TenantDb.advisoryXactLock instead of ctx.db.unsafeRaw(), so wrapped handlers no longer receive an implicit unsafeRaw grant."
|
|
13
|
+
},
|
|
2
14
|
{
|
|
3
15
|
"version": "0.370.0",
|
|
4
16
|
"type": "breaking",
|
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "0.378.0",
|
|
4
|
+
"type": "fix",
|
|
5
|
+
"title": "Mailbox list accepts the screen's limit",
|
|
6
|
+
"detail": "account:list accepted an empty payload only, so the projectionList screen's limit was rejected. It now takes limit up to MAX_LIST_LIMIT and returns the visible mailboxes ordered by id."
|
|
7
|
+
},
|
|
2
8
|
{
|
|
3
9
|
"version": "0.377.0",
|
|
4
10
|
"type": "improvement",
|
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "0.378.0",
|
|
4
|
+
"type": "improvement",
|
|
5
|
+
"title": "Email sending offers a platform default and an own SMTP server",
|
|
6
|
+
"detail": "The provider field is labelled \"Send via\" with the options \"Platform sending (default)\" (empty, saved as \"\") and \"Own SMTP server\"; the SMTP fields show only while the own server is picked."
|
|
7
|
+
},
|
|
2
8
|
{
|
|
3
9
|
"version": "0.370.0",
|
|
4
10
|
"type": "breaking",
|
|
@@ -1,4 +1,24 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "0.378.0",
|
|
4
|
+
"type": "improvement",
|
|
5
|
+
"title": "Document PDFs embed the images of a record",
|
|
6
|
+
"detail": "A template variable of type images (templateVariable.images()) holds FileRef ids of the record. The new Images block (div[data-images], PDF templates only, rejected for mail kinds) renders them as data-URI images in the real PDF render. Only files of the rendering tenant are read, only png, jpeg, gif and webp, at most 24 images and 20 MB per document; missing, foreign, oversized or non-image ids are dropped silently. Previews and draft PDFs never read files and show one placeholder box per block. An images variable never renders as inline text. i18n keys template-resolver.documentEditor.images, template-resolver.documentEditor.imagesVariable and templateResolver.errors.document.imagesBlockPdfOnly ship in de, es, fr and it.",
|
|
7
|
+
"migration": "Declare a template variable with type `images`, place the Images block, and pass FileRef ids of the record in `variables`. Images resolve only in the real PDF render, previews show placeholders. The ids must come from the record on the server, never from client input: a resolved id is read for the whole tenant without an entity access check."
|
|
8
|
+
},
|
|
9
|
+
{
|
|
10
|
+
"version": "0.378.0",
|
|
11
|
+
"type": "breaking",
|
|
12
|
+
"title": "Template editing is SystemAdmin-only unless the app unlocks it",
|
|
13
|
+
"detail": "Every tenant write handler, the overview and resource upload accept SystemAdmin only, plus the roles of the tenantEditable entries. A write on a template that is not unlocked fails with templateNotTenantEditable. The test send follows the same rule. For a non-SystemAdmin an unknown template id answers 403 like a locked one, so the response does not reveal whether it exists. Collections without an access rule default to SystemAdmin.",
|
|
14
|
+
"migration": "Where TenantAdmins must keep editing a template, pass tenantEditable to createTemplateResolverFeature, for example tenantEditable: [{ kind: \"mail\", slug: \"billing:notify:invoice-issued\", access: { roles: [\"TenantAdmin\"] } }]. Give template collections an explicit access rule if tenants should manage them."
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"version": "0.378.0",
|
|
18
|
+
"type": "improvement",
|
|
19
|
+
"title": "send-test-status query for the template test mail",
|
|
20
|
+
"detail": "template-resolver:query:send-test-status answers whether a test mail can be sent (canSend) and, if not, why (no_channel or no_address), so clients can disable the action instead of failing on send."
|
|
21
|
+
},
|
|
2
22
|
{
|
|
3
23
|
"version": "0.377.0",
|
|
4
24
|
"type": "improvement",
|