@cosmicdrift/kumiko-headless 0.235.0 → 0.235.2

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.235.0",
3
+ "version": "0.235.2",
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.235.0",
39
+ "@cosmicdrift/kumiko-framework": "0.235.2",
40
40
  "temporal-polyfill": "^0.3.2",
41
41
  "zod": "^4.4.3"
42
42
  },
@@ -3,6 +3,10 @@ import type {
3
3
  EntityDefinition,
4
4
  EntityListScreenDefinition,
5
5
  } from "@cosmicdrift/kumiko-framework/ui-types";
6
+ import {
7
+ LIST_ROW_META_COLUMNS,
8
+ LIST_ROW_META_REFERENCES,
9
+ } from "@cosmicdrift/kumiko-framework/ui-types";
6
10
  import { computeListViewModel } from "../list";
7
11
 
8
12
  // Minimal EntityDefinition-shape. ui-core's view-model only reads
@@ -264,6 +268,96 @@ describe("computeListViewModel", () => {
264
268
  });
265
269
  });
266
270
 
271
+ // Table-driven over LIST_ROW_META_COLUMNS itself (not one-off cases) — a
272
+ // row-meta column (id/tenantId/version/insertedAt/...) is a base-table
273
+ // column, never a declared entity field (a SystemAdmin cross-tenant list
274
+ // picks tenantId as a column this way). Together with boot-validator.test.ts
275
+ // (both boot validators accept the same set) and list-row-meta-drift.test.ts
276
+ // (LIST_ROW_META_COLUMNS key-set drift guard), this pins "whatever boot
277
+ // accepts as a row-meta column, the renderer can actually draw" — a #2601
278
+ // review found the two sides had drifted apart.
279
+ test("every row-meta column resolves to its declared type, sortable, no throw", () => {
280
+ for (const [field, expectedType] of Object.entries(LIST_ROW_META_COLUMNS)) {
281
+ const vm = computeListViewModel({
282
+ screen: listScreen(["title", field]),
283
+ entity: taskEntity,
284
+ rows: [],
285
+ translate,
286
+ featureName: "tasks",
287
+ });
288
+
289
+ const ref = LIST_ROW_META_REFERENCES[field];
290
+ expect(vm.columns[1]).toEqual({
291
+ field,
292
+ label: `tasks:entity:task:field:${field}`,
293
+ ...(ref !== undefined
294
+ ? {
295
+ type: "reference",
296
+ sortable: false,
297
+ refFeature: ref.refFeature,
298
+ refEntity: ref.refEntity,
299
+ refLabelField: ref.refLabelField,
300
+ }
301
+ : { type: expectedType, sortable: true }),
302
+ });
303
+ }
304
+ });
305
+
306
+ // tenantId carries a GUID in the DB but should render the referenced
307
+ // tenant's display name, like a declared reference field does — not the
308
+ // raw GUID. insertedById stays unchanged: this also pins that the map
309
+ // switches exactly one column, not every row-meta ID column.
310
+ test("tenantId row-meta column resolves as a reference column, insertedById unchanged", () => {
311
+ const vm = computeListViewModel({
312
+ screen: listScreen(["tenantId", "insertedById", "id"]),
313
+ entity: taskEntity,
314
+ rows: [],
315
+ translate,
316
+ featureName: "tasks",
317
+ });
318
+
319
+ expect(vm.columns[0]).toEqual({
320
+ field: "tenantId",
321
+ label: "tasks:entity:task:field:tenantId",
322
+ type: "reference",
323
+ sortable: false,
324
+ refFeature: "tenant",
325
+ refEntity: "tenant",
326
+ refLabelField: "name",
327
+ });
328
+ expect(vm.columns[1]).toEqual({
329
+ field: "insertedById",
330
+ label: "tasks:entity:task:field:insertedById",
331
+ type: "text",
332
+ sortable: true,
333
+ });
334
+ expect(vm.columns[2]).toEqual({
335
+ field: "id",
336
+ label: "tasks:entity:task:field:id",
337
+ type: "text",
338
+ sortable: true,
339
+ });
340
+ });
341
+
342
+ test("row-meta column: explicit label + renderer are passed through unchanged", () => {
343
+ const fmt = { format: "currency" as const, symbol: "€" };
344
+ const vm = computeListViewModel({
345
+ screen: listScreen([{ field: "insertedAt", label: "custom.created", renderer: fmt }]),
346
+ entity: taskEntity,
347
+ rows: [],
348
+ translate,
349
+ featureName: "tasks",
350
+ });
351
+
352
+ expect(vm.columns[0]).toEqual({
353
+ field: "insertedAt",
354
+ label: "custom.created",
355
+ type: "timestamp",
356
+ sortable: true,
357
+ renderer: fmt,
358
+ });
359
+ });
360
+
267
361
  test("slots pass through unchanged for the renderer to mount", () => {
268
362
  const slots = { header: { react: { component: "HeaderRef" } } };
269
363
  const vm = computeListViewModel({
@@ -3,7 +3,12 @@ import type {
3
3
  EntityListScreenDefinition,
4
4
  FieldDefinition,
5
5
  } from "@cosmicdrift/kumiko-framework/ui-types";
6
- import { normalizeListColumn, parseRefTarget } from "@cosmicdrift/kumiko-framework/ui-types";
6
+ import {
7
+ LIST_ROW_META_COLUMNS,
8
+ LIST_ROW_META_REFERENCES,
9
+ normalizeListColumn,
10
+ parseRefTarget,
11
+ } from "@cosmicdrift/kumiko-framework/ui-types";
7
12
  import type { ListColumnViewModel, ListRowViewModel, ListViewModel, Translate } from "./types";
8
13
 
9
14
  export type ComputeListViewModelInput = {
@@ -42,6 +47,33 @@ export function computeListViewModel(input: ComputeListViewModelInput): ListView
42
47
  // columns carry no reference/select metadata and never server-sort.
43
48
  const derivedDef = entity.derivedFields?.[normalized.field];
44
49
  if (!derivedDef) {
50
+ // Row-meta column (id/tenantId/version/insertedAt/modifiedAt/...) —
51
+ // a real base-table column that's never a declared entity field, e.g.
52
+ // a SystemAdmin cross-tenant list exposing tenantId. Unlike the
53
+ // derivedFields branch above, these are actual DB columns, so
54
+ // server-side ORDER BY works — sortable stays true, except for the
55
+ // ID columns in LIST_ROW_META_REFERENCES (see their reference branch).
56
+ const rowMetaType = LIST_ROW_META_COLUMNS[normalized.field];
57
+ if (rowMetaType !== undefined) {
58
+ const rowMetaRef = LIST_ROW_META_REFERENCES[normalized.field];
59
+ columns.push({
60
+ field: normalized.field,
61
+ label: translate(
62
+ normalized.label ?? fieldLabelKey(featureName, screen.entity, normalized.field),
63
+ ),
64
+ type: rowMetaRef !== undefined ? "reference" : rowMetaType,
65
+ // The DB column holds the GUID, not the resolved label — server-side
66
+ // ORDER BY would sort by GUID while the cell shows the name.
67
+ sortable: rowMetaRef === undefined,
68
+ ...(normalized.renderer !== undefined && { renderer: normalized.renderer }),
69
+ ...(rowMetaRef !== undefined && {
70
+ refEntity: rowMetaRef.refEntity,
71
+ refFeature: rowMetaRef.refFeature,
72
+ refLabelField: rowMetaRef.refLabelField,
73
+ }),
74
+ });
75
+ continue;
76
+ }
45
77
  // A virtual presentational column — drawn entirely by a columnRenderer
46
78
  // component from the row (e.g. tag chips); value is undefined and it
47
79
  // never server-sorts. `field` is just the column key. It needs BOTH a