@cosmicdrift/kumiko-headless 0.92.0 → 0.94.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.94.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>",
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
}
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
|
-
"@cosmicdrift/kumiko-framework": "0.
|
|
35
|
+
"@cosmicdrift/kumiko-framework": "0.94.0",
|
|
36
36
|
"zod": "^4.4.3"
|
|
37
37
|
},
|
|
38
38
|
"publishConfig": {
|
|
@@ -15,6 +15,13 @@ const taskEntity = {
|
|
|
15
15
|
done: { type: "boolean" },
|
|
16
16
|
priority: { type: "number", sortable: true },
|
|
17
17
|
},
|
|
18
|
+
derivedFields: {
|
|
19
|
+
// Read-time computed (value appended by the list-query handler). The
|
|
20
|
+
// view-model only reads valueType; the derive body never runs here, so a
|
|
21
|
+
// no-op stand-in is enough.
|
|
22
|
+
statusLabel: { valueType: "text", derive: () => "" },
|
|
23
|
+
ageDays: { valueType: "number", derive: () => 0 },
|
|
24
|
+
},
|
|
18
25
|
} as unknown as EntityDefinition;
|
|
19
26
|
|
|
20
27
|
function listScreen(columns: EntityListScreenDefinition["columns"]): EntityListScreenDefinition {
|
|
@@ -125,6 +132,56 @@ describe("computeListViewModel", () => {
|
|
|
125
132
|
expect(spy).toHaveBeenCalledWith("tasks:entity:task:field:priority");
|
|
126
133
|
});
|
|
127
134
|
|
|
135
|
+
test("derived-field column carries its valueType, is display-only (never sortable), no stored-field metadata", () => {
|
|
136
|
+
const vm = computeListViewModel({
|
|
137
|
+
screen: listScreen(["statusLabel", "ageDays"]),
|
|
138
|
+
entity: taskEntity,
|
|
139
|
+
rows: [],
|
|
140
|
+
translate,
|
|
141
|
+
featureName: "tasks",
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
expect(vm.columns).toEqual([
|
|
145
|
+
{
|
|
146
|
+
field: "statusLabel",
|
|
147
|
+
label: "tasks:entity:task:field:statusLabel",
|
|
148
|
+
type: "text",
|
|
149
|
+
sortable: false,
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
field: "ageDays",
|
|
153
|
+
label: "tasks:entity:task:field:ageDays",
|
|
154
|
+
type: "number",
|
|
155
|
+
sortable: false,
|
|
156
|
+
},
|
|
157
|
+
]);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
test("derived column value passes through from the row (handler already appended it)", () => {
|
|
161
|
+
const vm = computeListViewModel({
|
|
162
|
+
screen: listScreen(["title", "statusLabel"]),
|
|
163
|
+
entity: taskEntity,
|
|
164
|
+
rows: [{ id: "t-1", title: "first", statusLabel: "overdue" }],
|
|
165
|
+
translate,
|
|
166
|
+
featureName: "tasks",
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
expect(vm.rows[0]?.values).toEqual({ id: "t-1", title: "first", statusLabel: "overdue" });
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
test("derived columns accept an object-form renderer like stored columns", () => {
|
|
173
|
+
const fmt = { format: "currency" as const, symbol: "€" };
|
|
174
|
+
const vm = computeListViewModel({
|
|
175
|
+
screen: listScreen([{ field: "ageDays", renderer: fmt }]),
|
|
176
|
+
entity: taskEntity,
|
|
177
|
+
rows: [],
|
|
178
|
+
translate,
|
|
179
|
+
featureName: "tasks",
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
expect(vm.columns[0]?.renderer).toEqual(fmt);
|
|
183
|
+
});
|
|
184
|
+
|
|
128
185
|
test("slots pass through unchanged for the renderer to mount", () => {
|
|
129
186
|
const slots = { header: { react: { component: "HeaderRef" } } };
|
|
130
187
|
const vm = computeListViewModel({
|
package/src/view-model/list.ts
CHANGED
|
@@ -37,14 +37,30 @@ export function computeListViewModel(input: ComputeListViewModelInput): ListView
|
|
|
37
37
|
const normalized = normalizeListColumn(spec);
|
|
38
38
|
const fieldDef = entity.fields[normalized.field];
|
|
39
39
|
if (!fieldDef) {
|
|
40
|
-
//
|
|
41
|
-
// the
|
|
42
|
-
//
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
40
|
+
// Not a stored field — may be a read-time derived field (value appended
|
|
41
|
+
// by the list-query handler). Render it as its display valueType; derived
|
|
42
|
+
// columns carry no reference/select metadata and never server-sort.
|
|
43
|
+
const derivedDef = entity.derivedFields?.[normalized.field];
|
|
44
|
+
if (!derivedDef) {
|
|
45
|
+
// Screen references a field that's neither stored nor derived. Fail
|
|
46
|
+
// loud — the boot-validator (r.screen) should catch this, but a stale
|
|
47
|
+
// field-rename would leave the screen referring to a ghost column
|
|
48
|
+
// until ops re-runs boot. We throw so the renderer sees the error
|
|
49
|
+
// instead of drawing an empty column.
|
|
50
|
+
throw new Error(
|
|
51
|
+
`computeListViewModel: screen "${screen.id}" references unknown field "${normalized.field}" on entity "${screen.entity}"`,
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
columns.push({
|
|
55
|
+
field: normalized.field,
|
|
56
|
+
label: translate(fieldLabelKey(featureName, screen.entity, normalized.field)),
|
|
57
|
+
type: derivedDef.valueType,
|
|
58
|
+
// Display-only: a header sort would round-trip to the server, which has
|
|
59
|
+
// no column to sort by (see DerivedFieldDef). Never offer the affordance.
|
|
60
|
+
sortable: false,
|
|
61
|
+
...(normalized.renderer !== undefined && { renderer: normalized.renderer }),
|
|
62
|
+
});
|
|
63
|
+
continue;
|
|
48
64
|
}
|
|
49
65
|
const label = translate(fieldLabelKey(featureName, screen.entity, normalized.field));
|
|
50
66
|
// Tier 2.7e-3 + Cross-Feature: Reference-Field — entity-String
|