@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.98.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.98.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({
@@ -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
- // Screen references a field that's neither stored nor derived. Fail
46
- // loudthe 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.
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(fieldLabelKey(featureName, screen.entity, normalized.field)),
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(fieldLabelKey(featureName, screen.entity, normalized.field));
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