@cosmicdrift/kumiko-headless 0.359.0 → 0.361.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/format/index.js +5 -0
- package/dist/index.d.ts +1 -1
- package/dist/nav/resolve.js +14 -1
- package/dist/view-model/edit.d.ts +2 -0
- package/dist/view-model/edit.js +65 -45
- package/dist/view-model/index.d.ts +1 -1
- package/dist/view-model/list-file-display.d.ts +3 -0
- package/dist/view-model/list-file-display.js +28 -0
- package/dist/view-model/list.js +3 -0
- package/dist/view-model/types.d.ts +24 -1
- package/package.json +3 -3
- package/src/changes.json +24 -0
package/dist/format/index.js
CHANGED
|
@@ -198,6 +198,11 @@ export function applyFormatSpec(spec, value, translate) {
|
|
|
198
198
|
// multiSelect-style columns hold an array of option values
|
|
199
199
|
return Array.isArray(value) ? value.map(labelFor).join(", ") : labelFor(value);
|
|
200
200
|
}
|
|
201
|
+
// Thumbnails and file links are drawn by the web list cell; this is the
|
|
202
|
+
// text fallback for every other surface.
|
|
203
|
+
case "image":
|
|
204
|
+
case "file":
|
|
205
|
+
return Array.isArray(value) ? value.join(", ") : String(value);
|
|
201
206
|
case "json":
|
|
202
207
|
return stringifyJsonOrString(value, typeof spec["indent"] === "number" ? spec["indent"] : 2);
|
|
203
208
|
default:
|
package/dist/index.d.ts
CHANGED
|
@@ -10,5 +10,5 @@ export type { NavDefinition, NavNode, NavRegistrySlice, NavTree, ResolveNavigati
|
|
|
10
10
|
export { resolveNavigation } from "./nav/index.js";
|
|
11
11
|
export type { Store, WritableStore } from "./store/index.js";
|
|
12
12
|
export { createStore, shallowEqual } from "./store/index.js";
|
|
13
|
-
export type { ComputeEditViewModelInput, ComputeListViewModelInput, DerivedCellRoundingTarget, EditExtensionSectionViewModel, EditFieldSpec, EditFieldsSectionViewModel, EditFieldViewModel, EditFormMode, EditRelatedListSectionViewModel, EditSectionSpec, EditSectionViewModel, EditViewModel, EditWriteFormSectionViewModel, EmbeddedDerivedOp, EmbeddedListCellViewModel, EmbeddedListIssueGroups, FieldConditionCtx, FieldRenderer, ListColumnSpec, ListColumnViewModel, ListRowViewModel, ListViewModel, RuntimeRenderer, ScreenSlots, Translate, } from "./view-model/index.js";
|
|
13
|
+
export type { ComputeEditViewModelInput, ComputeListViewModelInput, DerivedCellRoundingTarget, EditExtensionSectionViewModel, EditFieldSpec, EditFieldsSectionViewModel, EditFieldViewModel, EditFormMode, EditRelatedListSectionViewModel, EditSectionSpec, EditSectionViewModel, EditViewModel, EditWriteFormSectionViewModel, EmbeddedDerivedOp, EmbeddedListCellViewModel, EmbeddedListIssueGroups, FieldConditionCtx, FieldRenderer, ListColumnFileDisplay, ListColumnSpec, ListColumnViewModel, ListRowViewModel, ListViewModel, RuntimeRenderer, ScreenSlots, Translate, } from "./view-model/index.js";
|
|
14
14
|
export { buildOptionLabels, computeDerivedCellValue, computeEditViewModel, computeListViewModel, computeRelatedListSectionViewModel, embeddedCellLabelKey, embeddedCellOptionLabelKey, fieldLabelKey, fieldOptionLabelKey, fieldOptionLabelKeyPrefix, groupEmbeddedListIssues, roundDerivedCellValue, sumEmbeddedListColumn, } from "./view-model/index.js";
|
package/dist/nav/resolve.js
CHANGED
|
@@ -4,7 +4,9 @@ import { isUiAccessGranted } from "@cosmicdrift/kumiko-framework/ui-types";
|
|
|
4
4
|
// access-checked; a hidden parent drops its entire subtree implicitly
|
|
5
5
|
// because we never recurse into it. Siblings sort by `order` (ascending,
|
|
6
6
|
// default 0), tie-broken by qualified name so renders stay deterministic
|
|
7
|
-
// across registry iteration orders.
|
|
7
|
+
// across registry iteration orders. A pure group (no screen, target,
|
|
8
|
+
// provider or createAction) whose children are all hidden or absent is
|
|
9
|
+
// dropped too.
|
|
8
10
|
//
|
|
9
11
|
// Pure — same inputs produce the same tree. The renderer memoizes the
|
|
10
12
|
// result and only recomputes when the registry changes (rare — boot
|
|
@@ -24,6 +26,10 @@ export function resolveNavigation(options) {
|
|
|
24
26
|
if (node !== null)
|
|
25
27
|
children.push(node);
|
|
26
28
|
}
|
|
29
|
+
// A pure group (nothing to open itself) with nothing visible below it
|
|
30
|
+
// would render as an empty, unclickable heading.
|
|
31
|
+
if (children.length === 0 && isPureGroup(entry))
|
|
32
|
+
return null;
|
|
27
33
|
children.sort(bySortKey);
|
|
28
34
|
return {
|
|
29
35
|
qualifiedName: entry.id,
|
|
@@ -47,6 +53,13 @@ export function resolveNavigation(options) {
|
|
|
47
53
|
roots.sort(bySortKey);
|
|
48
54
|
return roots;
|
|
49
55
|
}
|
|
56
|
+
function isPureGroup(entry) {
|
|
57
|
+
return (entry.screen === undefined &&
|
|
58
|
+
entry.target === undefined &&
|
|
59
|
+
entry.provider === undefined &&
|
|
60
|
+
entry.actions === undefined &&
|
|
61
|
+
entry.createAction === undefined);
|
|
62
|
+
}
|
|
50
63
|
function bySortKey(a, b) {
|
|
51
64
|
// Primary key: `order` ascending. Secondary: qualifiedName alphabetic,
|
|
52
65
|
// which is a stable fallback — the registry-iteration order isn't
|
|
@@ -10,6 +10,8 @@ export type ComputeEditViewModelInput<TValues extends Readonly<Record<string, un
|
|
|
10
10
|
* for that verb (create hides them, update shows them read-only); omitted
|
|
11
11
|
* means no exclusion applies. */
|
|
12
12
|
readonly mode?: EditFormMode;
|
|
13
|
+
/** The record being edited; omitted on create. */
|
|
14
|
+
readonly entityId?: string;
|
|
13
15
|
};
|
|
14
16
|
export type EditFormMode = "create" | "update";
|
|
15
17
|
export declare function computeRelatedListSectionViewModel(sectionSpec: EditRelatedListSection, translate: Translate): EditRelatedListSectionViewModel;
|
package/dist/view-model/edit.js
CHANGED
|
@@ -220,23 +220,66 @@ function deriveReferenceMultiHints(fieldDef, declaredRefTarget) {
|
|
|
220
220
|
...(refMultiple !== undefined && { refMultiple }),
|
|
221
221
|
};
|
|
222
222
|
}
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
const capture = fieldDef.type === "image" ? fileDef?.capture : undefined;
|
|
223
|
+
function isFileFieldType(fieldDef) {
|
|
224
|
+
return (fieldDef.type === "file" ||
|
|
225
|
+
fieldDef.type === "image" ||
|
|
226
|
+
fieldDef.type === "files" ||
|
|
227
|
+
fieldDef.type === "images");
|
|
228
|
+
}
|
|
229
|
+
function deriveImageHints(fieldDef, fileDef) {
|
|
230
|
+
if (fieldDef.type !== "image" && fieldDef.type !== "images")
|
|
231
|
+
return {};
|
|
232
|
+
const imageVariant = Object.keys(fileDef.variants ?? {})[0];
|
|
234
233
|
return {
|
|
235
|
-
...(fileDef?.accept !== undefined && { accept: fileDef.accept }),
|
|
236
|
-
...(fileDef?.maxSize !== undefined && { maxSize: fileDef.maxSize }),
|
|
237
|
-
...(isFileType && { entityType: entityName, fieldName }),
|
|
238
234
|
...(imageVariant !== undefined && { imageVariant }),
|
|
239
|
-
...(capture !== undefined && { capture }),
|
|
235
|
+
...(fileDef.capture !== undefined && { capture: fileDef.capture }),
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
function deriveFileFieldHints(fieldDef, entityName, fieldName, entityId) {
|
|
239
|
+
if (!isFileFieldType(fieldDef))
|
|
240
|
+
return {};
|
|
241
|
+
const fileDef = fieldDef;
|
|
242
|
+
return {
|
|
243
|
+
...(fileDef.accept !== undefined && { accept: fileDef.accept }),
|
|
244
|
+
...(fileDef.maxSize !== undefined && { maxSize: fileDef.maxSize }),
|
|
245
|
+
...(fileDef.maxCount !== undefined && { maxCount: fileDef.maxCount }),
|
|
246
|
+
entityType: entityName,
|
|
247
|
+
fieldName,
|
|
248
|
+
...(entityId !== undefined && { entityId }),
|
|
249
|
+
...deriveImageHints(fieldDef, fileDef),
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
function deriveEmbeddedCellSelectHints(subField, translate, featureName, entityName, fieldName, subFieldName) {
|
|
253
|
+
if (subField.type !== "select")
|
|
254
|
+
return {};
|
|
255
|
+
const options = subField.options ?? [];
|
|
256
|
+
return {
|
|
257
|
+
options,
|
|
258
|
+
optionLabels: buildOptionLabels(translate, (value) => embeddedCellOptionLabelKey(featureName, entityName, fieldName, subFieldName, value), options),
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
function deriveEmbeddedCellReferenceHints(subField, featureName) {
|
|
262
|
+
if (subField.type !== "reference")
|
|
263
|
+
return {};
|
|
264
|
+
const target = subField.entity !== undefined ? parseRefTarget(subField.entity, featureName) : undefined;
|
|
265
|
+
return {
|
|
266
|
+
...(target !== undefined && { refEntity: target.entityName, refFeature: target.featureName }),
|
|
267
|
+
...(subField.labelField !== undefined && { refLabelField: subField.labelField }),
|
|
268
|
+
...(subField.optionsQuery !== undefined && { refOptionsQuery: subField.optionsQuery }),
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
function buildEmbeddedListCell(subFieldName, subField, translate, featureName, entityName, fieldName, listSpec) {
|
|
272
|
+
return {
|
|
273
|
+
field: subFieldName,
|
|
274
|
+
label: translate(embeddedCellLabelKey(featureName, entityName, fieldName, subFieldName)),
|
|
275
|
+
type: subField.type,
|
|
276
|
+
required: subField.required === true,
|
|
277
|
+
...deriveEmbeddedCellSelectHints(subField, translate, featureName, entityName, fieldName, subFieldName),
|
|
278
|
+
...deriveEmbeddedCellReferenceHints(subField, featureName),
|
|
279
|
+
...(subField.type === "decimal" && subField.scale !== undefined && { scale: subField.scale }),
|
|
280
|
+
...(subField.type === "select" &&
|
|
281
|
+
subField.display !== undefined && { display: subField.display }),
|
|
282
|
+
...(listSpec?.readOnlyCells?.includes(subFieldName) === true && { readOnly: true }),
|
|
240
283
|
};
|
|
241
284
|
}
|
|
242
285
|
// Embedded-LIST field (`multiple: true`) — per-cell metadata for a renderer
|
|
@@ -244,39 +287,14 @@ function deriveFileFieldHints(fieldDef, entityName, fieldName) {
|
|
|
244
287
|
// (non-list) embedded field emits none of this; the renderer tells the two
|
|
245
288
|
// apart by whether embeddedListCells is set, not by `type` (which stays
|
|
246
289
|
// "embedded" either way).
|
|
247
|
-
function deriveEmbeddedListHints(fieldDef, translate, featureName, entityName, fieldName, resolvedCurrency) {
|
|
290
|
+
function deriveEmbeddedListHints(fieldDef, translate, featureName, entityName, fieldName, resolvedCurrency, listSpec) {
|
|
248
291
|
const isEmbeddedList = fieldDef.type === "embedded" &&
|
|
249
292
|
fieldDef.multiple === true;
|
|
250
293
|
const embeddedListDef = isEmbeddedList
|
|
251
294
|
? fieldDef
|
|
252
295
|
: undefined;
|
|
253
296
|
const embeddedListCells = embeddedListDef !== undefined
|
|
254
|
-
? Object.entries(embeddedListDef.schema).map(([subFieldName, subField]) =>
|
|
255
|
-
const cellLabel = translate(embeddedCellLabelKey(featureName, entityName, fieldName, subFieldName));
|
|
256
|
-
const cellOptions = subField.type === "select" ? (subField.options ?? []) : undefined;
|
|
257
|
-
const cellOptionLabels = cellOptions !== undefined
|
|
258
|
-
? buildOptionLabels(translate, (value) => embeddedCellOptionLabelKey(featureName, entityName, fieldName, subFieldName, value), cellOptions)
|
|
259
|
-
: undefined;
|
|
260
|
-
const cellRef = subField.type === "reference" ? subField : undefined;
|
|
261
|
-
const cellRefTarget = cellRef?.entity !== undefined ? parseRefTarget(cellRef.entity, featureName) : undefined;
|
|
262
|
-
const cell = {
|
|
263
|
-
field: subFieldName,
|
|
264
|
-
label: cellLabel,
|
|
265
|
-
type: subField.type,
|
|
266
|
-
required: subField.required === true,
|
|
267
|
-
...(cellOptions !== undefined && { options: cellOptions }),
|
|
268
|
-
...(cellOptionLabels !== undefined && { optionLabels: cellOptionLabels }),
|
|
269
|
-
...(cellRefTarget !== undefined && { refEntity: cellRefTarget.entityName }),
|
|
270
|
-
...(cellRefTarget !== undefined && { refFeature: cellRefTarget.featureName }),
|
|
271
|
-
...(cellRef?.labelField !== undefined && { refLabelField: cellRef.labelField }),
|
|
272
|
-
...(cellRef?.optionsQuery !== undefined && {
|
|
273
|
-
refOptionsQuery: cellRef.optionsQuery,
|
|
274
|
-
}),
|
|
275
|
-
...(subField.type === "decimal" &&
|
|
276
|
-
subField.scale !== undefined && { scale: subField.scale }),
|
|
277
|
-
};
|
|
278
|
-
return cell;
|
|
279
|
-
})
|
|
297
|
+
? Object.entries(embeddedListDef.schema).map(([subFieldName, subField]) => buildEmbeddedListCell(subFieldName, subField, translate, featureName, entityName, fieldName, listSpec))
|
|
280
298
|
: undefined;
|
|
281
299
|
return {
|
|
282
300
|
...(embeddedListCells !== undefined && { embeddedListCells }),
|
|
@@ -295,6 +313,8 @@ function deriveEmbeddedListHints(fieldDef, translate, featureName, entityName, f
|
|
|
295
313
|
// Currency lives on the head aggregate (entity.defaultCurrency), not per
|
|
296
314
|
// row — one value for the whole embedded list.
|
|
297
315
|
...(embeddedListDef !== undefined && { embeddedListCurrency: resolvedCurrency }),
|
|
316
|
+
...(embeddedListDef !== undefined &&
|
|
317
|
+
listSpec?.fixedRows === true && { embeddedListFixedRows: true }),
|
|
298
318
|
};
|
|
299
319
|
}
|
|
300
320
|
function buildExtensionSectionViewModel(sectionSpec, translate) {
|
|
@@ -316,7 +336,7 @@ function buildExtensionSectionViewModel(sectionSpec, translate) {
|
|
|
316
336
|
// section/field tree the renderer draws. FieldConditions are evaluated here
|
|
317
337
|
// so the renderer never re-runs them during React render.
|
|
318
338
|
export function computeEditViewModel(input) {
|
|
319
|
-
const { screen, entity, values, translate, featureName, mode } = input;
|
|
339
|
+
const { screen, entity, values, translate, featureName, mode, entityId } = input;
|
|
320
340
|
const excludedFields = new Set(mode === undefined ? [] : screen.writeExcludedFields?.[mode]);
|
|
321
341
|
const isHiddenByExclusion = (fieldSpec) => mode === "create" && excludedFields.has(normalizeEditField(fieldSpec).field);
|
|
322
342
|
const sections = screen.layout.sections.map((sectionSpec) => {
|
|
@@ -378,8 +398,8 @@ export function computeEditViewModel(input) {
|
|
|
378
398
|
const dateHints = deriveDateFieldHints(fieldDef);
|
|
379
399
|
const numericHints = deriveNumericFieldHints(fieldDef, resolvedCurrency);
|
|
380
400
|
const referenceHints = deriveReferenceFieldHints(fieldDef, normalized, declaredRefTarget, featureName);
|
|
381
|
-
const fileHints = deriveFileFieldHints(fieldDef, screen.entity, normalized.field);
|
|
382
|
-
const embeddedListHints = deriveEmbeddedListHints(fieldDef, translate, featureName, screen.entity, normalized.field, resolvedCurrency);
|
|
401
|
+
const fileHints = deriveFileFieldHints(fieldDef, screen.entity, normalized.field, entityId);
|
|
402
|
+
const embeddedListHints = deriveEmbeddedListHints(fieldDef, translate, featureName, screen.entity, normalized.field, resolvedCurrency, normalized.embeddedList);
|
|
383
403
|
const view = {
|
|
384
404
|
field: normalized.field,
|
|
385
405
|
label,
|
|
@@ -4,4 +4,4 @@ export type { DerivedCellRoundingTarget, EmbeddedDerivedOp, EmbeddedListIssueGro
|
|
|
4
4
|
export { computeDerivedCellValue, groupEmbeddedListIssues, roundDerivedCellValue, sumEmbeddedListColumn, } from "./embedded-list.js";
|
|
5
5
|
export type { ComputeListViewModelInput } from "./list.js";
|
|
6
6
|
export { buildOptionLabels, computeListViewModel, embeddedCellLabelKey, embeddedCellOptionLabelKey, fieldLabelKey, fieldOptionLabelKey, fieldOptionLabelKeyPrefix, } from "./list.js";
|
|
7
|
-
export type { EditExtensionSectionViewModel, EditFieldSpec, EditFieldsSectionViewModel, EditFieldViewModel, EditRelatedListSectionViewModel, EditSectionSpec, EditSectionViewModel, EditViewModel, EditWriteFormSectionViewModel, EmbeddedListCellViewModel, FieldConditionCtx, FieldRenderer, ListColumnSpec, ListColumnViewModel, ListRowViewModel, ListViewModel, RuntimeRenderer, ScreenSlots, Translate, } from "./types.js";
|
|
7
|
+
export type { EditExtensionSectionViewModel, EditFieldSpec, EditFieldsSectionViewModel, EditFieldViewModel, EditRelatedListSectionViewModel, EditSectionSpec, EditSectionViewModel, EditViewModel, EditWriteFormSectionViewModel, EmbeddedListCellViewModel, FieldConditionCtx, FieldRenderer, ListColumnFileDisplay, ListColumnSpec, ListColumnViewModel, ListRowViewModel, ListViewModel, RuntimeRenderer, ScreenSlots, Translate, } from "./types.js";
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { FieldDefinition } from "@cosmicdrift/kumiko-framework/ui-types";
|
|
2
|
+
import type { ListColumnFileDisplay, ListColumnViewModel } from "./types.js";
|
|
3
|
+
export declare function deriveListFileDisplay(fieldDef: FieldDefinition, renderer: ListColumnViewModel["renderer"]): ListColumnFileDisplay | undefined;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
function firstVariantKey(fieldDef) {
|
|
2
|
+
const variants = fieldDef.variants;
|
|
3
|
+
return variants === undefined ? undefined : Object.keys(variants)[0];
|
|
4
|
+
}
|
|
5
|
+
function fromFormatSpec(renderer) {
|
|
6
|
+
if (typeof renderer !== "object" || renderer === null || !("format" in renderer)) {
|
|
7
|
+
return undefined;
|
|
8
|
+
}
|
|
9
|
+
if (renderer.format === "file")
|
|
10
|
+
return { kind: "file" };
|
|
11
|
+
if (renderer.format !== "image")
|
|
12
|
+
return undefined;
|
|
13
|
+
return renderer.variant === undefined
|
|
14
|
+
? { kind: "image" }
|
|
15
|
+
: { kind: "image", variant: renderer.variant };
|
|
16
|
+
}
|
|
17
|
+
function fromFieldType(fieldDef) {
|
|
18
|
+
if (fieldDef.type === "file" || fieldDef.type === "files")
|
|
19
|
+
return { kind: "file" };
|
|
20
|
+
if (fieldDef.type !== "image" && fieldDef.type !== "images")
|
|
21
|
+
return undefined;
|
|
22
|
+
const variant = firstVariantKey(fieldDef);
|
|
23
|
+
return variant === undefined ? { kind: "image" } : { kind: "image", variant };
|
|
24
|
+
}
|
|
25
|
+
// An explicit `{ format: "image" | "file" }` renderer wins over the field type.
|
|
26
|
+
export function deriveListFileDisplay(fieldDef, renderer) {
|
|
27
|
+
return fromFormatSpec(renderer) ?? fromFieldType(fieldDef);
|
|
28
|
+
}
|
package/dist/view-model/list.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { LIST_ROW_META_COLUMNS, LIST_ROW_META_REFERENCES, normalizeListColumn, parseRefTarget, } from "@cosmicdrift/kumiko-framework/ui-types";
|
|
2
|
+
import { deriveListFileDisplay } from "./list-file-display.js";
|
|
2
3
|
const WRITE_ONLY_SET_LABEL_KEY = "kumiko.field.writeOnly.set";
|
|
3
4
|
// Pure transform: takes the declared screen + entity + incoming rows, spits
|
|
4
5
|
// out the flat shape the renderer draws. No conditions, no access-checks —
|
|
@@ -130,6 +131,7 @@ export function computeListViewModel(input) {
|
|
|
130
131
|
: undefined;
|
|
131
132
|
const optionTones = fieldDef.type === "select" ? fieldDef.optionTones : undefined;
|
|
132
133
|
const grouping = fieldDef.type === "number" ? fieldDef.grouping : undefined;
|
|
134
|
+
const fileDisplay = deriveListFileDisplay(fieldDef, normalized.renderer);
|
|
133
135
|
const column = {
|
|
134
136
|
field: normalized.field,
|
|
135
137
|
label,
|
|
@@ -142,6 +144,7 @@ export function computeListViewModel(input) {
|
|
|
142
144
|
...(refFeature !== undefined && { refFeature }),
|
|
143
145
|
...(refLabelField !== undefined && { refLabelField }),
|
|
144
146
|
...(grouping !== undefined && { grouping }),
|
|
147
|
+
...(fileDisplay !== undefined && { fileDisplay }),
|
|
145
148
|
...(normalized.hideOnNarrow === true && { hideOnNarrow: true }),
|
|
146
149
|
};
|
|
147
150
|
columns.push(column);
|
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
import type { EditFieldSpec, EditSectionSpec, FieldIconKey, FieldRenderer, IconKey, ListColumnSpec, ListFacetSpec, ListSortSpec, OptionsQueryPayload, PlatformComponent, RelatedListGroupBy, RelatedListRowTone, RelatedListToolbarAction, RowAction, RowActionMode, ScreenSlots, SelectOptionTone } from "@cosmicdrift/kumiko-framework/ui-types";
|
|
2
2
|
export type RuntimeRenderer = (value: unknown, row?: Readonly<Record<string, unknown>>) => string;
|
|
3
|
+
/** How a list cell shows a FileRef value (id or id array). */
|
|
4
|
+
export type ListColumnFileDisplay = {
|
|
5
|
+
readonly kind: "image";
|
|
6
|
+
readonly variant?: string;
|
|
7
|
+
} | {
|
|
8
|
+
readonly kind: "file";
|
|
9
|
+
};
|
|
3
10
|
export type ListColumnViewModel = {
|
|
4
11
|
readonly field: string;
|
|
5
12
|
readonly label: string;
|
|
@@ -34,6 +41,10 @@ export type ListColumnViewModel = {
|
|
|
34
41
|
* `true`). `false` renders without thousands-separators (e.g. a model
|
|
35
42
|
* year). */
|
|
36
43
|
readonly grouping?: boolean;
|
|
44
|
+
/** Set for file/image columns: from the entity field type, or from a
|
|
45
|
+
* `{ format: "image" | "file" }` renderer. The cell shows a thumbnail or the
|
|
46
|
+
* file name instead of the FileRef UUID. */
|
|
47
|
+
readonly fileDisplay?: ListColumnFileDisplay;
|
|
37
48
|
};
|
|
38
49
|
export type ListRowViewModel = {
|
|
39
50
|
readonly id: string;
|
|
@@ -55,6 +66,10 @@ export type EmbeddedListCellViewModel = {
|
|
|
55
66
|
/** Only for `type: "select"`. */
|
|
56
67
|
readonly options?: readonly string[];
|
|
57
68
|
readonly optionLabels?: Readonly<Record<string, string>>;
|
|
69
|
+
/** Only for `type: "select"`; `"radio"` renders segmented buttons instead of a dropdown. */
|
|
70
|
+
readonly display?: "radio" | "dropdown";
|
|
71
|
+
/** Cell is shown but not editable (`EditFieldSpec.embeddedList.readOnlyCells`). */
|
|
72
|
+
readonly readOnly?: boolean;
|
|
58
73
|
/** Only for `type: "reference"`. */
|
|
59
74
|
readonly refEntity?: string;
|
|
60
75
|
readonly refFeature?: string;
|
|
@@ -171,12 +186,17 @@ export type EditFieldViewModel = {
|
|
|
171
186
|
* Upload-Endpoint gegen die Field-Def. */
|
|
172
187
|
readonly accept?: readonly string[];
|
|
173
188
|
readonly maxSize?: string;
|
|
189
|
+
/** Only for `type: "files" | "images"` — upper bound on list length. */
|
|
190
|
+
readonly maxCount?: number;
|
|
174
191
|
/** Nur bei `type: "file" | "image"` — Entity-Name + Field-Name für den
|
|
175
192
|
* Upload-POST (`/api/files`), damit der Endpoint maxSize/accept gegen
|
|
176
193
|
* die richtige Field-Def prüfen kann. */
|
|
177
194
|
readonly entityType?: string;
|
|
178
195
|
readonly fieldName?: string;
|
|
179
|
-
/**
|
|
196
|
+
/** Record being edited (update mode only). Sent with the upload so the
|
|
197
|
+
* FileRef is attached to the record. */
|
|
198
|
+
readonly entityId?: string;
|
|
199
|
+
/** Only for `type: "image"` and `"images"` — the variant name the preview requests
|
|
180
200
|
* (`/api/files/:id/variant/:name`) instead of the original. The FIRST
|
|
181
201
|
* variant declared on the field def wins; a spec may legitimately carry
|
|
182
202
|
* neither `size` nor `maxEdge` (format-/blur-only), so "the smallest one"
|
|
@@ -204,6 +224,9 @@ export type EditFieldViewModel = {
|
|
|
204
224
|
}>>;
|
|
205
225
|
/** Numeric sub-field names to sum in a totals row. From EmbeddedFieldDef.totals. */
|
|
206
226
|
readonly embeddedListTotals?: readonly string[];
|
|
227
|
+
/** No add, remove, duplicate or move; cells stay editable. From
|
|
228
|
+
* EditFieldSpec.embeddedList.fixedRows. */
|
|
229
|
+
readonly embeddedListFixedRows?: boolean;
|
|
207
230
|
/** Currency for the whole embedded list's money cells and totals row —
|
|
208
231
|
* currency lives on the head aggregate (EntityDefinition.defaultCurrency),
|
|
209
232
|
* not per row. Falls back to "EUR" when the entity has no defaultCurrency. */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmicdrift/kumiko-headless",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.361.0",
|
|
4
4
|
"description": "Headless UI logic for Kumiko — Dispatcher contract, Form-Controller, View-Model, Nav-Resolver. Plattform- und React-frei; jeder Renderer (renderer, renderer-web, renderer-native, …) komponiert darauf.",
|
|
5
5
|
"license": "BUSL-1.1",
|
|
6
6
|
"author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
|
|
@@ -36,8 +36,8 @@
|
|
|
36
36
|
}
|
|
37
37
|
},
|
|
38
38
|
"dependencies": {
|
|
39
|
-
"@cosmicdrift/kumiko-framework": "0.
|
|
40
|
-
"@cosmicdrift/kumiko-types": "0.
|
|
39
|
+
"@cosmicdrift/kumiko-framework": "0.361.0",
|
|
40
|
+
"@cosmicdrift/kumiko-types": "0.361.0",
|
|
41
41
|
"temporal-polyfill": "^0.3.2",
|
|
42
42
|
"zod": "^4.4.3"
|
|
43
43
|
},
|
package/src/changes.json
CHANGED
|
@@ -1,4 +1,28 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "0.361.0",
|
|
4
|
+
"type": "improvement",
|
|
5
|
+
"title": "Edit view model carries read-only cells and fixed rows",
|
|
6
|
+
"detail": "EmbeddedListCellViewModel.readOnly and EditFieldViewModel.embeddedListFixedRows are derived from EditFieldSpec.embeddedList."
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
"version": "0.361.0",
|
|
10
|
+
"type": "improvement",
|
|
11
|
+
"title": "Embedded list cells carry the select display",
|
|
12
|
+
"detail": "EmbeddedListCellViewModel.display is taken from the sub-field."
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"version": "0.361.0",
|
|
16
|
+
"type": "improvement",
|
|
17
|
+
"title": "resolveNavigation no longer renders empty groups or entries that open nothing",
|
|
18
|
+
"detail": "A nav node with no screen, target, provider, actions and createAction is dropped when none of its children is visible, also when it never had children. Allowlisted sidebars lose groups whose children are all filtered out. Give the entry a screen or keep a child visible to retain it."
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"version": "0.360.0",
|
|
22
|
+
"type": "improvement",
|
|
23
|
+
"title": "Edit view model carries entityId and maxCount for file fields",
|
|
24
|
+
"detail": "ComputeEditViewModelInput has an optional entityId (the record being edited, omitted on create). EditFieldViewModel gains entityId for file, image, files and images fields and maxCount for files and images. The file-field hints now also cover type files, which was missing."
|
|
25
|
+
},
|
|
2
26
|
{
|
|
3
27
|
"version": "0.357.0",
|
|
4
28
|
"type": "improvement",
|