@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.92.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.92.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({
@@ -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
- // Screen references a field that's not on the entity. Fail loud
41
- // the boot-validator (r.screen) should catch this, but a stale
42
- // field-rename would leave the screen referring to a ghost column
43
- // until ops re-runs boot. We throw so the renderer sees the error
44
- // instead of drawing an empty column.
45
- throw new Error(
46
- `computeListViewModel: screen "${screen.id}" references unknown field "${normalized.field}" on entity "${screen.entity}"`,
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