@cosmicdrift/kumiko-headless 0.98.0 → 0.100.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.100.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.100.0",
|
|
36
36
|
"zod": "^4.4.3"
|
|
37
37
|
},
|
|
38
38
|
"publishConfig": {
|
|
@@ -118,6 +118,41 @@ describe("computeListViewModel", () => {
|
|
|
118
118
|
).toThrow(/unknown field "doesNotExist"/);
|
|
119
119
|
});
|
|
120
120
|
|
|
121
|
+
test("labeled column with no matching field → virtual presentational column (no throw)", () => {
|
|
122
|
+
const vm = computeListViewModel({
|
|
123
|
+
screen: listScreen([
|
|
124
|
+
"title",
|
|
125
|
+
{ field: "tags", label: "Tags", renderer: { react: { __component: "TagsCell" } } },
|
|
126
|
+
]),
|
|
127
|
+
entity: taskEntity,
|
|
128
|
+
rows: [],
|
|
129
|
+
translate,
|
|
130
|
+
featureName: "tasks",
|
|
131
|
+
});
|
|
132
|
+
// `field` becomes the column key; label is taken verbatim (translate is
|
|
133
|
+
// identity here), type defaults to text, never server-sortable.
|
|
134
|
+
expect(vm.columns[1]).toEqual({
|
|
135
|
+
field: "tags",
|
|
136
|
+
label: "Tags",
|
|
137
|
+
type: "text",
|
|
138
|
+
sortable: false,
|
|
139
|
+
renderer: { react: { __component: "TagsCell" } },
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
test("label overrides the field-convention header on a real field", () => {
|
|
144
|
+
const vm = computeListViewModel({
|
|
145
|
+
screen: listScreen([{ field: "title", label: "custom.header" }]),
|
|
146
|
+
entity: taskEntity,
|
|
147
|
+
rows: [],
|
|
148
|
+
translate,
|
|
149
|
+
featureName: "tasks",
|
|
150
|
+
});
|
|
151
|
+
// label goes through translate (identity here) instead of the
|
|
152
|
+
// tasks:entity:task:field:title convention key.
|
|
153
|
+
expect(vm.columns[0]?.label).toBe("custom.header");
|
|
154
|
+
});
|
|
155
|
+
|
|
121
156
|
test("translate is called with the expected i18n-key per field", () => {
|
|
122
157
|
const spy = mock((key: string) => `T:${key}`);
|
|
123
158
|
computeListViewModel({
|
package/src/view-model/list.ts
CHANGED
|
@@ -42,18 +42,31 @@ export function computeListViewModel(input: ComputeListViewModelInput): ListView
|
|
|
42
42
|
// columns carry no reference/select metadata and never server-sort.
|
|
43
43
|
const derivedDef = entity.derivedFields?.[normalized.field];
|
|
44
44
|
if (!derivedDef) {
|
|
45
|
-
//
|
|
46
|
-
//
|
|
47
|
-
//
|
|
48
|
-
//
|
|
49
|
-
//
|
|
45
|
+
// A labeled column with no matching stored/derived field is a *virtual*
|
|
46
|
+
// presentational column — drawn entirely by a columnRenderer component
|
|
47
|
+
// from the row (e.g. tag chips); value is undefined and it never
|
|
48
|
+
// server-sorts. `field` is just the column key. Without a label it's an
|
|
49
|
+
// author typo (or a stale field-rename) → fail loud so the renderer
|
|
50
|
+
// doesn't silently draw an empty column.
|
|
51
|
+
if (normalized.label !== undefined) {
|
|
52
|
+
columns.push({
|
|
53
|
+
field: normalized.field,
|
|
54
|
+
label: translate(normalized.label),
|
|
55
|
+
type: "text",
|
|
56
|
+
sortable: false,
|
|
57
|
+
...(normalized.renderer !== undefined && { renderer: normalized.renderer }),
|
|
58
|
+
});
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
50
61
|
throw new Error(
|
|
51
62
|
`computeListViewModel: screen "${screen.id}" references unknown field "${normalized.field}" on entity "${screen.entity}"`,
|
|
52
63
|
);
|
|
53
64
|
}
|
|
54
65
|
columns.push({
|
|
55
66
|
field: normalized.field,
|
|
56
|
-
label: translate(
|
|
67
|
+
label: translate(
|
|
68
|
+
normalized.label ?? fieldLabelKey(featureName, screen.entity, normalized.field),
|
|
69
|
+
),
|
|
57
70
|
type: derivedDef.valueType,
|
|
58
71
|
// Display-only: a header sort would round-trip to the server, which has
|
|
59
72
|
// no column to sort by (see DerivedFieldDef). Never offer the affordance.
|
|
@@ -62,7 +75,9 @@ export function computeListViewModel(input: ComputeListViewModelInput): ListView
|
|
|
62
75
|
});
|
|
63
76
|
continue;
|
|
64
77
|
}
|
|
65
|
-
const label = translate(
|
|
78
|
+
const label = translate(
|
|
79
|
+
normalized.label ?? fieldLabelKey(featureName, screen.entity, normalized.field),
|
|
80
|
+
);
|
|
66
81
|
// Tier 2.7e-3 + Cross-Feature: Reference-Field — entity-String
|
|
67
82
|
// kann same-feature ("user") oder cross-feature ("users:user")
|
|
68
83
|
// sein. parseRefTarget gibt (featureName, entityName), der
|