@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.
|
|
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.
|
|
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({
|
package/src/view-model/list.ts
CHANGED
|
@@ -3,7 +3,12 @@ import type {
|
|
|
3
3
|
EntityListScreenDefinition,
|
|
4
4
|
FieldDefinition,
|
|
5
5
|
} from "@cosmicdrift/kumiko-framework/ui-types";
|
|
6
|
-
import {
|
|
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
|