@cosmicdrift/kumiko-headless 0.237.1 → 0.238.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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmicdrift/kumiko-headless",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.238.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,7 +36,7 @@
|
|
|
36
36
|
}
|
|
37
37
|
},
|
|
38
38
|
"dependencies": {
|
|
39
|
-
"@cosmicdrift/kumiko-framework": "0.
|
|
39
|
+
"@cosmicdrift/kumiko-framework": "0.238.0",
|
|
40
40
|
"temporal-polyfill": "^0.3.2",
|
|
41
41
|
"zod": "^4.4.3"
|
|
42
42
|
},
|
|
@@ -676,3 +676,57 @@ describe("computeEditViewModel — multiSelect display/columns/maxRows passthrou
|
|
|
676
676
|
expect(field?.maxRows).toBeUndefined();
|
|
677
677
|
});
|
|
678
678
|
});
|
|
679
|
+
|
|
680
|
+
describe("computeEditViewModel — declared reference metadata (fw#2662)", () => {
|
|
681
|
+
// projectionDetail fields have no real EntityDefinition — the shim that
|
|
682
|
+
// adapts them synthesizes a pseudo-entity where every field is hardcoded
|
|
683
|
+
// to "text" (see projection-detail-shim.ts). EditFieldSpec.refEntity must
|
|
684
|
+
// be evaluated BEFORE the entity.fields lookup, or it never fires against
|
|
685
|
+
// that shape.
|
|
686
|
+
const projectionPseudoEntity = {
|
|
687
|
+
fields: {
|
|
688
|
+
userId: { type: "text" },
|
|
689
|
+
ip: { type: "text" },
|
|
690
|
+
},
|
|
691
|
+
} as unknown as EntityDefinition;
|
|
692
|
+
|
|
693
|
+
test("EditFieldSpec.refEntity marks the field as a reference, even against a pseudo-entity whose fields are all 'text' (fw#2662)", () => {
|
|
694
|
+
const vm = computeEditViewModel({
|
|
695
|
+
screen: editScreen({
|
|
696
|
+
sections: [
|
|
697
|
+
{ fields: [{ field: "userId", refEntity: "user:user", refLabelField: "displayName" }] },
|
|
698
|
+
],
|
|
699
|
+
}),
|
|
700
|
+
entity: projectionPseudoEntity,
|
|
701
|
+
values: { userId: "u-1" },
|
|
702
|
+
translate,
|
|
703
|
+
featureName: "sessions",
|
|
704
|
+
});
|
|
705
|
+
|
|
706
|
+
const field = asFields(vm.sections[0]).fields[0];
|
|
707
|
+
expect(field).toMatchObject({
|
|
708
|
+
field: "userId",
|
|
709
|
+
type: "reference",
|
|
710
|
+
refEntity: "user",
|
|
711
|
+
refFeature: "user",
|
|
712
|
+
refLabelField: "displayName",
|
|
713
|
+
});
|
|
714
|
+
expect(field?.refMultiple).toBeUndefined();
|
|
715
|
+
});
|
|
716
|
+
|
|
717
|
+
test("non-regression: a projectionDetail field without refEntity metadata stays 'text', unchanged (fw#2662)", () => {
|
|
718
|
+
const vm = computeEditViewModel({
|
|
719
|
+
screen: editScreen({ sections: [{ fields: ["ip"] }] }),
|
|
720
|
+
entity: projectionPseudoEntity,
|
|
721
|
+
values: { ip: "10.0.0.1" },
|
|
722
|
+
translate,
|
|
723
|
+
featureName: "sessions",
|
|
724
|
+
});
|
|
725
|
+
|
|
726
|
+
const field = asFields(vm.sections[0]).fields[0];
|
|
727
|
+
expect(field?.type).toBe("text");
|
|
728
|
+
expect(field?.refEntity).toBeUndefined();
|
|
729
|
+
expect(field?.refFeature).toBeUndefined();
|
|
730
|
+
expect(field?.refLabelField).toBeUndefined();
|
|
731
|
+
});
|
|
732
|
+
});
|
|
@@ -339,6 +339,62 @@ describe("computeListViewModel", () => {
|
|
|
339
339
|
});
|
|
340
340
|
});
|
|
341
341
|
|
|
342
|
+
// projectionList/relatedList columns have no real EntityDefinition — the
|
|
343
|
+
// shim that adapts them synthesizes a pseudo-entity where every field is
|
|
344
|
+
// hardcoded to "text" (see projection-list-shim.ts /
|
|
345
|
+
// related-list-section.tsx). ListColumnSpec.refEntity must be evaluated
|
|
346
|
+
// BEFORE the entity.fields lookup, or it never fires against that shape.
|
|
347
|
+
const projectionPseudoEntity = {
|
|
348
|
+
fields: {
|
|
349
|
+
tenantId: { type: "text" },
|
|
350
|
+
type: { type: "text" },
|
|
351
|
+
},
|
|
352
|
+
} as unknown as EntityDefinition;
|
|
353
|
+
|
|
354
|
+
test("ListColumnSpec.refEntity marks the column as a reference, even against a pseudo-entity whose fields are all 'text' (fw#2662)", () => {
|
|
355
|
+
const vm = computeListViewModel({
|
|
356
|
+
screen: listScreen([
|
|
357
|
+
{
|
|
358
|
+
field: "tenantId",
|
|
359
|
+
label: "delivery.log.col.tenantId",
|
|
360
|
+
refEntity: "tenant:tenant",
|
|
361
|
+
refLabelField: "name",
|
|
362
|
+
},
|
|
363
|
+
]),
|
|
364
|
+
entity: projectionPseudoEntity,
|
|
365
|
+
rows: [],
|
|
366
|
+
translate,
|
|
367
|
+
featureName: "delivery",
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
expect(vm.columns[0]).toEqual({
|
|
371
|
+
field: "tenantId",
|
|
372
|
+
label: "delivery.log.col.tenantId",
|
|
373
|
+
type: "reference",
|
|
374
|
+
sortable: false,
|
|
375
|
+
refFeature: "tenant",
|
|
376
|
+
refEntity: "tenant",
|
|
377
|
+
refLabelField: "name",
|
|
378
|
+
});
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
test("non-regression: a projectionList column without refEntity metadata stays 'text', unchanged (fw#2662)", () => {
|
|
382
|
+
const vm = computeListViewModel({
|
|
383
|
+
screen: listScreen([{ field: "type", label: "delivery.log.col.type" }]),
|
|
384
|
+
entity: projectionPseudoEntity,
|
|
385
|
+
rows: [],
|
|
386
|
+
translate,
|
|
387
|
+
featureName: "delivery",
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
expect(vm.columns[0]).toEqual({
|
|
391
|
+
field: "type",
|
|
392
|
+
label: "delivery.log.col.type",
|
|
393
|
+
type: "text",
|
|
394
|
+
sortable: false,
|
|
395
|
+
});
|
|
396
|
+
});
|
|
397
|
+
|
|
342
398
|
test("row-meta column: explicit label + renderer are passed through unchanged", () => {
|
|
343
399
|
const fmt = { format: "currency" as const, symbol: "€" };
|
|
344
400
|
const vm = computeListViewModel({
|
package/src/view-model/edit.ts
CHANGED
|
@@ -97,6 +97,18 @@ export function computeEditViewModel<
|
|
|
97
97
|
`computeEditViewModel: screen "${screen.id}" references unknown field "${normalized.field}" on entity "${screen.entity}"`,
|
|
98
98
|
);
|
|
99
99
|
}
|
|
100
|
+
// Declared reference metadata (EditFieldSpec.refEntity) — for
|
|
101
|
+
// projectionDetail fields, which have no EntityDefinition field to
|
|
102
|
+
// carry a real "reference" type. Takes priority over fieldDef.type
|
|
103
|
+
// below so it also fires against the pseudo-entity that hardcodes
|
|
104
|
+
// every field as "text" (projection-detail-shim) — real entityEdit
|
|
105
|
+
// screens never set this, so their fieldDef.type === "reference"
|
|
106
|
+
// branch is unaffected.
|
|
107
|
+
const declaredRefTarget =
|
|
108
|
+
normalized.refEntity !== undefined
|
|
109
|
+
? parseRefTarget(normalized.refEntity, featureName)
|
|
110
|
+
: undefined;
|
|
111
|
+
const effectiveType = declaredRefTarget !== undefined ? "reference" : fieldDef.type;
|
|
100
112
|
const label = translate(
|
|
101
113
|
screen.fieldLabels?.[normalized.field] ??
|
|
102
114
|
fieldLabelKey(featureName, screen.entity, normalized.field),
|
|
@@ -160,26 +172,33 @@ export function computeEditViewModel<
|
|
|
160
172
|
const min = dateBounds?.min;
|
|
161
173
|
const max = dateBounds?.max;
|
|
162
174
|
const dateLocale = dateBounds?.locale;
|
|
163
|
-
// Tier 2.7e-3: Reference-Field — refEntity + refLabelField
|
|
164
|
-
//
|
|
165
|
-
//
|
|
166
|
-
//
|
|
167
|
-
//
|
|
168
|
-
//
|
|
169
|
-
//
|
|
175
|
+
// Tier 2.7e-3: Reference-Field — refEntity + refLabelField travel into
|
|
176
|
+
// the view model so the renderer can build the lookup query without
|
|
177
|
+
// touching the EntityDefinition again. The entity-string can be
|
|
178
|
+
// same-feature ("user") or cross-feature ("users:user"); parseRefTarget
|
|
179
|
+
// splits that, the renderer builds the lookup QN from
|
|
180
|
+
// (refFeature, refEntity). Declared metadata (declaredRefTarget) takes
|
|
181
|
+
// priority, same as effectiveType above.
|
|
170
182
|
const refRaw =
|
|
171
|
-
fieldDef.type === "reference"
|
|
183
|
+
declaredRefTarget === undefined && fieldDef.type === "reference"
|
|
172
184
|
? (fieldDef as unknown as { entity?: string }).entity
|
|
173
185
|
: undefined;
|
|
174
|
-
const refTarget =
|
|
186
|
+
const refTarget =
|
|
187
|
+
declaredRefTarget ??
|
|
188
|
+
(refRaw !== undefined ? parseRefTarget(refRaw, featureName) : undefined);
|
|
175
189
|
const refEntity = refTarget?.entityName;
|
|
176
190
|
const refFeature = refTarget?.featureName;
|
|
177
191
|
const refLabelField =
|
|
178
|
-
|
|
179
|
-
? (
|
|
180
|
-
:
|
|
192
|
+
declaredRefTarget !== undefined
|
|
193
|
+
? (normalized.refLabelField ?? "id")
|
|
194
|
+
: fieldDef.type === "reference"
|
|
195
|
+
? ((fieldDef as unknown as { labelField?: string }).labelField ?? "id")
|
|
196
|
+
: undefined;
|
|
197
|
+
// Declared reference metadata has no `multiple` concept (it targets
|
|
198
|
+
// row-meta/derived fields, always single-valued) — only a real
|
|
199
|
+
// ReferenceFieldDef carries it.
|
|
181
200
|
const refMultiple =
|
|
182
|
-
fieldDef.type === "reference"
|
|
201
|
+
declaredRefTarget === undefined && fieldDef.type === "reference"
|
|
183
202
|
? ((fieldDef as unknown as { multiple?: boolean }).multiple ?? false)
|
|
184
203
|
: undefined;
|
|
185
204
|
// file/image: accept/maxSize ins ViewModel + entityType/fieldName für
|
|
@@ -272,7 +291,7 @@ export function computeEditViewModel<
|
|
|
272
291
|
const view: EditFieldViewModel = {
|
|
273
292
|
field: normalized.field,
|
|
274
293
|
label,
|
|
275
|
-
type:
|
|
294
|
+
type: effectiveType,
|
|
276
295
|
value: values[normalized.field],
|
|
277
296
|
visible,
|
|
278
297
|
readOnly,
|
package/src/view-model/list.ts
CHANGED
|
@@ -40,6 +40,31 @@ export function computeListViewModel(input: ComputeListViewModelInput): ListView
|
|
|
40
40
|
const columns: ListColumnViewModel[] = [];
|
|
41
41
|
for (const spec of screen.columns) {
|
|
42
42
|
const normalized = normalizeListColumn(spec);
|
|
43
|
+
if (normalized.refEntity !== undefined) {
|
|
44
|
+
// Declared reference metadata (ListColumnSpec.refEntity) — for
|
|
45
|
+
// projectionList/relatedList columns, which have no EntityDefinition
|
|
46
|
+
// field to carry a real "reference" type. Checked before the
|
|
47
|
+
// entity.fields lookup below so it also fires against the pseudo-
|
|
48
|
+
// entities that hardcode every field as "text" (projection-list-shim,
|
|
49
|
+
// related-list-section) — those never reach the reference branch
|
|
50
|
+
// further down since fieldDef.type is always "text" there.
|
|
51
|
+
const refTarget = parseRefTarget(normalized.refEntity, featureName);
|
|
52
|
+
columns.push({
|
|
53
|
+
field: normalized.field,
|
|
54
|
+
label: translate(
|
|
55
|
+
normalized.label ?? fieldLabelKey(featureName, screen.entity, normalized.field),
|
|
56
|
+
),
|
|
57
|
+
type: "reference",
|
|
58
|
+
// The declared value is the referenced id, not the resolved label —
|
|
59
|
+
// there is no column to server-sort by, same as row-meta references.
|
|
60
|
+
sortable: false,
|
|
61
|
+
refEntity: refTarget.entityName,
|
|
62
|
+
refFeature: refTarget.featureName,
|
|
63
|
+
refLabelField: normalized.refLabelField ?? "id",
|
|
64
|
+
...(normalized.renderer !== undefined && { renderer: normalized.renderer }),
|
|
65
|
+
});
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
43
68
|
const fieldDef = entity.fields[normalized.field];
|
|
44
69
|
if (!fieldDef) {
|
|
45
70
|
// Not a stored field — may be a read-time derived field (value appended
|