@cosmicdrift/kumiko-renderer 0.359.0 → 0.360.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/components/embedded-list-field.js +1 -1
- package/dist/components/file-list-field.d.ts +10 -0
- package/dist/components/file-list-field.js +49 -0
- package/dist/components/render-edit.js +11 -1
- package/dist/components/render-field.js +17 -14
- package/dist/i18n-defaults.js +11 -0
- package/dist/primitives.d.ts +8 -0
- package/package.json +5 -5
- package/src/changes.json +6 -0
|
@@ -263,5 +263,5 @@ function EmbeddedListFieldInner({ field, id, onChange, allIssues, featureName, }
|
|
|
263
263
|
const recomputed = nextRows.map((row, i) => touchedIndices.has(i) ? withRecomputedDerived(row, derived, cells) : row);
|
|
264
264
|
onChange(recomputed);
|
|
265
265
|
}
|
|
266
|
-
return (_jsx(EmbeddedListInput, { id: id, columns: columns, rows: rows, totals: totals, currency: field.embeddedListCurrency, disabled: field.readOnly, minItems: field.embeddedListMinItems, maxItems: field.embeddedListMaxItems, listIssues: combinedListIssues, rowIssues: rowIssues, cellIssues: cellIssues, onCellChange: handleCellChange, onAddRow: handleAddRow, onRemoveRow: handleRemoveRow, onDuplicateRow: handleDuplicateRow, onMoveRow: handleMoveRow, onPasteCells: handlePasteCells, addLabel: t("kumiko.field.embedded-list.add-row"), removeLabel: t("kumiko.field.embedded-list.remove-row"), duplicateLabel: t("kumiko.field.embedded-list.duplicate-row"), moveUpLabel: t("kumiko.field.embedded-list.move-up"), moveDownLabel: t("kumiko.field.embedded-list.move-down"), emptyLabel: t("kumiko.field.embedded-list.empty"), emptyCtaLabel: t("kumiko.field.embedded-list.empty-cta"), testId: id }));
|
|
266
|
+
return (_jsx(EmbeddedListInput, { id: id, columns: columns, rows: rows, totals: totals, currency: field.embeddedListCurrency, disabled: field.readOnly, minItems: field.embeddedListMinItems, maxItems: field.embeddedListMaxItems, listIssues: combinedListIssues, rowIssues: rowIssues, cellIssues: cellIssues, onCellChange: handleCellChange, onAddRow: handleAddRow, onRemoveRow: handleRemoveRow, onDuplicateRow: handleDuplicateRow, onMoveRow: handleMoveRow, onPasteCells: handlePasteCells, addLabel: t("kumiko.field.embedded-list.add-row"), actionsLabel: t("kumiko.field.embedded-list.actions"), removeLabel: t("kumiko.field.embedded-list.remove-row"), duplicateLabel: t("kumiko.field.embedded-list.duplicate-row"), moveUpLabel: t("kumiko.field.embedded-list.move-up"), moveDownLabel: t("kumiko.field.embedded-list.move-down"), emptyLabel: t("kumiko.field.embedded-list.empty"), emptyCtaLabel: t("kumiko.field.embedded-list.empty-cta"), testId: id }));
|
|
267
267
|
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { EditFieldViewModel } from "@cosmicdrift/kumiko-headless";
|
|
2
|
+
import type { ReactNode } from "react";
|
|
3
|
+
export type FileListFieldProps = {
|
|
4
|
+
readonly field: EditFieldViewModel;
|
|
5
|
+
readonly id: string;
|
|
6
|
+
readonly kind: "file" | "image";
|
|
7
|
+
readonly onChange: (value: readonly string[]) => void;
|
|
8
|
+
readonly hasError: boolean;
|
|
9
|
+
};
|
|
10
|
+
export declare function FileListField({ field, id, kind, onChange, hasError, }: FileListFieldProps): ReactNode;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useTranslation } from "../i18n.js";
|
|
3
|
+
import { usePrimitives } from "../primitives.js";
|
|
4
|
+
function isFileIdList(value) {
|
|
5
|
+
return Array.isArray(value);
|
|
6
|
+
}
|
|
7
|
+
function readFileIds(value) {
|
|
8
|
+
if (!isFileIdList(value))
|
|
9
|
+
return [];
|
|
10
|
+
return value.filter((entry) => typeof entry === "string" && entry !== "");
|
|
11
|
+
}
|
|
12
|
+
export function FileListField({ field, id, kind, onChange, hasError, }) {
|
|
13
|
+
const { Input, Button, Grid, GridCell, Text } = usePrimitives();
|
|
14
|
+
const t = useTranslation();
|
|
15
|
+
const fileIds = readFileIds(field.value);
|
|
16
|
+
const disabled = field.readOnly;
|
|
17
|
+
const canAdd = !disabled && (field.maxCount === undefined || fileIds.length < field.maxCount);
|
|
18
|
+
const inputProps = {
|
|
19
|
+
kind,
|
|
20
|
+
name: field.field,
|
|
21
|
+
disabled,
|
|
22
|
+
hasError,
|
|
23
|
+
...(field.accept !== undefined && { accept: field.accept }),
|
|
24
|
+
...(field.maxSize !== undefined && { maxSize: field.maxSize }),
|
|
25
|
+
...(field.entityType !== undefined && { entityType: field.entityType }),
|
|
26
|
+
...(field.entityId !== undefined && { entityId: field.entityId }),
|
|
27
|
+
...(field.fieldName !== undefined && { fieldName: field.fieldName }),
|
|
28
|
+
...(kind === "image" &&
|
|
29
|
+
field.imageVariant !== undefined && { imageVariant: field.imageVariant }),
|
|
30
|
+
...(kind === "image" && field.capture !== undefined && { capture: field.capture }),
|
|
31
|
+
};
|
|
32
|
+
function move(fromIndex, toIndex) {
|
|
33
|
+
const next = [...fileIds];
|
|
34
|
+
const [moved] = next.splice(fromIndex, 1);
|
|
35
|
+
if (moved === undefined)
|
|
36
|
+
return;
|
|
37
|
+
next.splice(toIndex, 0, moved);
|
|
38
|
+
onChange(next);
|
|
39
|
+
}
|
|
40
|
+
return (_jsxs(Grid, { columns: 1, testId: `${id}-file-list`, children: [fileIds.length === 0 && _jsx(Text, { variant: "muted", children: t("kumiko.field.file-list.empty") }), fileIds.map((fileId, index) => (_jsxs(GridCell, { children: [_jsx(Input, { ...inputProps, id: `${id}-${index}`, value: fileId, onChange: (next) => {
|
|
41
|
+
if (next === null)
|
|
42
|
+
onChange(fileIds.filter((_, i) => i !== index));
|
|
43
|
+
else
|
|
44
|
+
onChange(fileIds.map((existing, i) => (i === index ? next : existing)));
|
|
45
|
+
} }), !disabled && (_jsxs(Grid, { columns: "auto", children: [index > 0 && (_jsx(Button, { variant: "ghost", size: "icon", icon: "chevron-up", ariaLabel: t("kumiko.field.file-list.move-up"), testId: `${id}-${index}-move-up`, onClick: () => move(index, index - 1) })), index < fileIds.length - 1 && (_jsx(Button, { variant: "ghost", size: "icon", icon: "chevron-down", ariaLabel: t("kumiko.field.file-list.move-down"), testId: `${id}-${index}-move-down`, onClick: () => move(index, index + 1) }))] }))] }, fileId))), canAdd && (_jsxs(GridCell, { children: [_jsx(Text, { variant: "muted", children: t("kumiko.field.file-list.add") }), _jsx(Input, { ...inputProps, id: `${id}-add`, value: null, onChange: (next) => {
|
|
46
|
+
if (next !== null)
|
|
47
|
+
onChange([...fileIds, next]);
|
|
48
|
+
} })] }, "add"))] }));
|
|
49
|
+
}
|
|
@@ -631,7 +631,17 @@ export function RenderEdit(props) {
|
|
|
631
631
|
translate,
|
|
632
632
|
featureName,
|
|
633
633
|
mode: writeVerb,
|
|
634
|
-
|
|
634
|
+
...(!isCreateMode && entityIdProp != null && { entityId: entityIdProp }),
|
|
635
|
+
}), [
|
|
636
|
+
screen,
|
|
637
|
+
entity,
|
|
638
|
+
snapshot.values,
|
|
639
|
+
translate,
|
|
640
|
+
featureName,
|
|
641
|
+
writeVerb,
|
|
642
|
+
isCreateMode,
|
|
643
|
+
entityIdProp,
|
|
644
|
+
]);
|
|
635
645
|
const filteredSections = useMemo(
|
|
636
646
|
// A fully-hidden "fields" section (every field in it currently
|
|
637
647
|
// condition-hidden) must not occupy a wizard step; it would render
|
|
@@ -15,6 +15,7 @@ import { useLocale, useTranslation } from "../i18n.js";
|
|
|
15
15
|
import { usePrimitives } from "../primitives.js";
|
|
16
16
|
import { AvailabilityOptionsMultiSelect, AvailabilityOptionsSelect, staticSelectOptions, } from "./availability-options-select.js";
|
|
17
17
|
import { EmbeddedListField } from "./embedded-list-field.js";
|
|
18
|
+
import { FileListField } from "./file-list-field.js";
|
|
18
19
|
import { MultiSelectCheckboxes } from "./multi-select-checkboxes.js";
|
|
19
20
|
import { QueryOptionsSelect } from "./query-options-select.js";
|
|
20
21
|
import { ReferenceCreateDialog } from "./reference-create-dialog.js";
|
|
@@ -276,11 +277,11 @@ function ReadOnlyReferenceValue({ field, featureName, }) {
|
|
|
276
277
|
return _jsx(Text, { testId: `field-value-${field.field}`, children: labels.join(", ") });
|
|
277
278
|
}
|
|
278
279
|
// Field types whose read display already isn't a boxed "disabled input" look
|
|
279
|
-
// (embedded/jsonb render as an info Banner
|
|
280
|
-
//
|
|
281
|
-
//
|
|
280
|
+
// (embedded/jsonb render as an info Banner) — `readOnlyDisplayText` doesn't
|
|
281
|
+
// cover these, they keep going through `renderInput`'s existing Banner branch
|
|
282
|
+
// in text-display mode too.
|
|
282
283
|
function isComplexFieldType(type) {
|
|
283
|
-
return type === "embedded" || type === "jsonb"
|
|
284
|
+
return type === "embedded" || type === "jsonb";
|
|
284
285
|
}
|
|
285
286
|
// Mirrors ACTION_ICON_BY_ID (kumiko-screen.tsx) for fields: only names with
|
|
286
287
|
// an unambiguous matching FieldIconKey are listed here, everything else
|
|
@@ -383,22 +384,21 @@ function renderInput({ field, id, hasError, onChange, Input, appLocale, Banner,
|
|
|
383
384
|
case "image": {
|
|
384
385
|
const kind = field.type === "image" ? "image" : "file";
|
|
385
386
|
const fileId = typeof field.value === "string" && field.value !== "" ? field.value : null;
|
|
386
|
-
return (_jsx(Input, { kind: kind, ...common, value: fileId, onChange: (v) => onChange(v), ...(field.accept !== undefined && { accept: field.accept }), ...(field.maxSize !== undefined && { maxSize: field.maxSize }), ...(field.entityType !== undefined && { entityType: field.entityType }), ...(field.fieldName !== undefined && { fieldName: field.fieldName }), ...(field.imageVariant !== undefined && { imageVariant: field.imageVariant }), ...(field.capture !== undefined && { capture: field.capture }) }));
|
|
387
|
+
return (_jsx(Input, { kind: kind, ...common, value: fileId, onChange: (v) => onChange(v), ...(field.accept !== undefined && { accept: field.accept }), ...(field.maxSize !== undefined && { maxSize: field.maxSize }), ...(field.entityType !== undefined && { entityType: field.entityType }), ...(field.entityId !== undefined && { entityId: field.entityId }), ...(field.fieldName !== undefined && { fieldName: field.fieldName }), ...(field.imageVariant !== undefined && { imageVariant: field.imageVariant }), ...(field.capture !== undefined && { capture: field.capture }) }));
|
|
387
388
|
}
|
|
389
|
+
case "files":
|
|
390
|
+
case "images":
|
|
391
|
+
return (_jsx(FileListField, { field: field, id: id, kind: field.type === "images" ? "image" : "file", hasError: hasError, onChange: (ids) => onChange(ids) }));
|
|
388
392
|
// embedded (without embeddedListCells — that's embeddedList, which has
|
|
389
393
|
// had its own EmbeddedListField widget since #1838) and jsonb carry
|
|
390
|
-
// arbitrary objects
|
|
391
|
-
//
|
|
392
|
-
//
|
|
393
|
-
//
|
|
394
|
-
// string, and saving that overwrites the real data with the mangled
|
|
395
|
-
// string (#1834). A `required: true` on any of these is caught loudly
|
|
394
|
+
// arbitrary objects. Without a dedicated widget these must NOT fall
|
|
395
|
+
// through to a text input: stringValue() turns them into
|
|
396
|
+
// "[object Object]" and saving that overwrites the real data with the
|
|
397
|
+
// mangled string (#1834). A `required: true` on either is caught loudly
|
|
396
398
|
// at boot (validateNoWidgetRequiredField in the framework package)
|
|
397
399
|
// instead of silently failing here.
|
|
398
400
|
case "embedded":
|
|
399
|
-
case "jsonb":
|
|
400
|
-
case "files":
|
|
401
|
-
case "images": {
|
|
401
|
+
case "jsonb": {
|
|
402
402
|
const hasValue = field.value !== undefined && field.value !== null && field.value !== "";
|
|
403
403
|
return (_jsxs(Banner, { id: id, variant: "info", children: [t("kumiko.field.unsupported"), hasValue &&
|
|
404
404
|
(JsonView !== undefined ? (_jsx(JsonView, { value: field.value, testId: `field-value-${field.field}` })) : (_jsx(Text, { variant: "code", children: JSON.stringify(field.value) })))] }));
|
|
@@ -559,6 +559,9 @@ function readOnlyDisplayText(field, appLocale, t) {
|
|
|
559
559
|
case "file":
|
|
560
560
|
case "image":
|
|
561
561
|
return typeof value === "string" ? value : "—";
|
|
562
|
+
case "files":
|
|
563
|
+
case "images":
|
|
564
|
+
return Array.isArray(value) && value.length > 0 ? value.map(stringValue).join(", ") : "—";
|
|
562
565
|
default:
|
|
563
566
|
return stringValue(value);
|
|
564
567
|
}
|
package/dist/i18n-defaults.js
CHANGED
|
@@ -45,6 +45,17 @@ export const kumikoDefaultTranslations = {
|
|
|
45
45
|
"kumiko.field.writeOnly.setPlaceholder": "Set — leave empty to keep it",
|
|
46
46
|
"kumiko.field.writeOnly.undo": "Undo",
|
|
47
47
|
"kumiko.field.writeOnly.willRemove": "Removed on save",
|
|
48
|
+
"kumiko.field.embedded-list.actions": "Actions",
|
|
49
|
+
"kumiko.field.embedded-list.cell-label": "{column}, row {row}",
|
|
50
|
+
"kumiko.field.file-list.add": "Add file",
|
|
51
|
+
"kumiko.field.file-list.move-up": "Move file up",
|
|
52
|
+
"kumiko.field.file-list.move-down": "Move file down",
|
|
53
|
+
"kumiko.field.file-list.empty": "No files yet.",
|
|
54
|
+
"kumiko.field.file-upload.attached": "File attached",
|
|
55
|
+
"kumiko.field.file-upload.change": "Change",
|
|
56
|
+
"kumiko.field.file-upload.failed": "Upload failed",
|
|
57
|
+
"kumiko.field.file-upload.remove": "Remove",
|
|
58
|
+
"kumiko.field.file-upload.upload": "Upload",
|
|
48
59
|
"kumiko.field.embedded-list.add-row": "Add row",
|
|
49
60
|
"kumiko.field.embedded-list.remove-row": "Remove row",
|
|
50
61
|
"kumiko.field.embedded-list.duplicate-row": "Duplicate row",
|
package/dist/primitives.d.ts
CHANGED
|
@@ -295,6 +295,9 @@ export type InputProps = {
|
|
|
295
295
|
readonly name: string;
|
|
296
296
|
/** FileRef-UUID (gespeicherte Datei) oder null (kein Upload). */
|
|
297
297
|
readonly value: string | null;
|
|
298
|
+
/** Implementations must offer their own remove control that calls
|
|
299
|
+
* `onChange(null)`: FileListField relies on it to drop a file from an
|
|
300
|
+
* `images`/`files` field. */
|
|
298
301
|
readonly onChange: (fileId: string | null) => void;
|
|
299
302
|
readonly disabled?: boolean;
|
|
300
303
|
readonly required?: boolean;
|
|
@@ -305,6 +308,9 @@ export type InputProps = {
|
|
|
305
308
|
/** Für den Upload-POST `/api/files` — der Endpoint prüft maxSize/accept
|
|
306
309
|
* gegen die Field-Def von (entityType, fieldName). */
|
|
307
310
|
readonly entityType?: string;
|
|
311
|
+
/** Must be sent as the `entityId` part of the upload so the FileRef is
|
|
312
|
+
* attached to the record (qr overlays and access guards need it). */
|
|
313
|
+
readonly entityId?: string;
|
|
308
314
|
readonly fieldName?: string;
|
|
309
315
|
/** Only for kind:"image" — variant name for the preview
|
|
310
316
|
* (`/api/files/:id/variant/:name`). Absent → the original. */
|
|
@@ -810,6 +816,8 @@ export type EmbeddedListInputProps = {
|
|
|
810
816
|
* handling wired up. */
|
|
811
817
|
readonly onPasteCells?: (rowIndex: number, columnIndex: number, grid: readonly (readonly string[])[]) => void;
|
|
812
818
|
readonly addLabel: string;
|
|
819
|
+
/** Accessible name of the (otherwise headerless) row-actions column. */
|
|
820
|
+
readonly actionsLabel: string;
|
|
813
821
|
readonly removeLabel: string;
|
|
814
822
|
readonly duplicateLabel: string;
|
|
815
823
|
readonly moveUpLabel: string;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmicdrift/kumiko-renderer",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.360.0",
|
|
4
4
|
"description": "Platform-agnostic React renderer for Kumiko screens. Contains the shared logic — primitives-contract, hooks, KumikoScreen, navigation & SSE abstractions — that any platform-specific renderer (web, native) composes. No DOM, no EventSource, no react-dom.",
|
|
5
5
|
"license": "BUSL-1.1",
|
|
6
6
|
"author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
|
|
@@ -15,9 +15,9 @@
|
|
|
15
15
|
}
|
|
16
16
|
},
|
|
17
17
|
"dependencies": {
|
|
18
|
-
"@cosmicdrift/kumiko-framework": "0.
|
|
19
|
-
"@cosmicdrift/kumiko-headless": "0.
|
|
20
|
-
"@cosmicdrift/kumiko-types": "0.
|
|
18
|
+
"@cosmicdrift/kumiko-framework": "0.360.0",
|
|
19
|
+
"@cosmicdrift/kumiko-headless": "0.360.0",
|
|
20
|
+
"@cosmicdrift/kumiko-types": "0.360.0",
|
|
21
21
|
"react": "^19.2.6",
|
|
22
22
|
"temporal-polyfill": "^0.3.2",
|
|
23
23
|
"zod": "^4.4.3"
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"@types/react-dom": "^19.2.3",
|
|
29
29
|
"jsdom": "^29.1.1",
|
|
30
30
|
"react-dom": "^19.2.6",
|
|
31
|
-
"@cosmicdrift/kumiko-locale-de": "0.
|
|
31
|
+
"@cosmicdrift/kumiko-locale-de": "0.360.0"
|
|
32
32
|
},
|
|
33
33
|
"repository": {
|
|
34
34
|
"type": "git",
|
package/src/changes.json
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "0.360.0",
|
|
4
|
+
"type": "improvement",
|
|
5
|
+
"title": "FileListField edits images and files fields",
|
|
6
|
+
"detail": "RenderField renders type images and files as FileListField instead of the unsupported banner. One tile per stored FileRef id with move up, move down and remove buttons, one trailing empty tile to add a file (hidden at maxCount or when read-only). It is built from Input kind file or image, Button and the layout primitives, passes accept, maxSize, entityType, entityId, fieldName, imageVariant and capture to every tile, and reads only string ids from the stored value. The read-only text display of these fields lists the ids. New i18n keys kumiko.field.file-list.add, move-up, move-down and empty; kumiko.field.file-upload.upload, change, remove, attached and failed; kumiko.field.embedded-list.actions and cell-label. The file and image Input kinds accept entityId. EmbeddedListInput has a new required actionsLabel prop."
|
|
7
|
+
},
|
|
2
8
|
{
|
|
3
9
|
"version": "0.357.0",
|
|
4
10
|
"type": "improvement",
|