@cosmicdrift/kumiko-headless 0.21.1 → 0.23.1

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.21.1",
3
+ "version": "0.23.1",
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>",
package/src/index.ts CHANGED
@@ -64,7 +64,9 @@ export { createStore, shallowEqual } from "./store";
64
64
  export type {
65
65
  ComputeEditViewModelInput,
66
66
  ComputeListViewModelInput,
67
+ EditExtensionSectionViewModel,
67
68
  EditFieldSpec,
69
+ EditFieldsSectionViewModel,
68
70
  EditFieldViewModel,
69
71
  EditSectionSpec,
70
72
  EditSectionViewModel,
@@ -4,6 +4,14 @@ import type {
4
4
  EntityEditScreenDefinition,
5
5
  } from "@cosmicdrift/kumiko-framework/ui-types";
6
6
  import { computeEditViewModel } from "../edit";
7
+ import type { EditFieldsSectionViewModel, EditSectionViewModel } from "../types";
8
+
9
+ function asFields(s: EditSectionViewModel | undefined): EditFieldsSectionViewModel {
10
+ if (s === undefined || s.kind !== "fields") {
11
+ throw new Error(`expected fields-section, got ${s?.kind ?? "undefined"}`);
12
+ }
13
+ return s;
14
+ }
7
15
 
8
16
  const orderEntity = {
9
17
  fields: {
@@ -44,8 +52,8 @@ describe("computeEditViewModel", () => {
44
52
  expect(vm.sections).toHaveLength(1);
45
53
  const section = vm.sections[0];
46
54
  expect(section?.title).toBe("Hauptdaten");
47
- expect(section?.columns).toBe(1); // default
48
- expect(section?.fields).toEqual([
55
+ expect(asFields(section).columns).toBe(1); // default
56
+ expect(asFields(section).fields).toEqual([
49
57
  {
50
58
  field: "customerName",
51
59
  label: "orders:entity:order:field:customerName",
@@ -81,8 +89,8 @@ describe("computeEditViewModel", () => {
81
89
  featureName: "orders",
82
90
  });
83
91
 
84
- expect(vm.sections[0]?.columns).toBe(2);
85
- expect(vm.sections[1]?.columns).toBe(1);
92
+ expect(asFields(vm.sections[0]).columns).toBe(2);
93
+ expect(asFields(vm.sections[1]).columns).toBe(1);
86
94
  });
87
95
 
88
96
  test("visible predicate evaluated against current values (live-reactive)", () => {
@@ -110,7 +118,7 @@ describe("computeEditViewModel", () => {
110
118
  translate,
111
119
  featureName: "orders",
112
120
  });
113
- const reasonHidden = hidden.sections[0]?.fields[1];
121
+ const reasonHidden = asFields(hidden.sections[0]).fields[1];
114
122
  expect(reasonHidden?.visible).toBe(false);
115
123
  expect(reasonHidden?.required).toBe(false);
116
124
 
@@ -121,7 +129,7 @@ describe("computeEditViewModel", () => {
121
129
  translate,
122
130
  featureName: "orders",
123
131
  });
124
- const reasonShown = shown.sections[0]?.fields[1];
132
+ const reasonShown = asFields(shown.sections[0]).fields[1];
125
133
  expect(reasonShown?.visible).toBe(true);
126
134
  expect(reasonShown?.required).toBe(true);
127
135
  });
@@ -149,7 +157,7 @@ describe("computeEditViewModel", () => {
149
157
  translate,
150
158
  featureName: "orders",
151
159
  });
152
- expect(nonAdmin.sections[0]?.fields[0]?.readOnly).toBe(true);
160
+ expect(asFields(nonAdmin.sections[0]).fields[0]?.readOnly).toBe(true);
153
161
 
154
162
  const admin = computeEditViewModel({
155
163
  screen,
@@ -159,7 +167,7 @@ describe("computeEditViewModel", () => {
159
167
  translate,
160
168
  featureName: "orders",
161
169
  });
162
- expect(admin.sections[0]?.fields[0]?.readOnly).toBe(false);
170
+ expect(asFields(admin.sections[0]).fields[0]?.readOnly).toBe(false);
163
171
  });
164
172
 
165
173
  test("screen-level required override wins over entity-level required", () => {
@@ -180,7 +188,7 @@ describe("computeEditViewModel", () => {
180
188
  featureName: "orders",
181
189
  });
182
190
 
183
- expect(vm.sections[0]?.fields[0]?.required).toBe(false);
191
+ expect(asFields(vm.sections[0]).fields[0]?.required).toBe(false);
184
192
  });
185
193
 
186
194
  test("id is extracted from values or null on create (no existing row)", () => {
@@ -237,6 +245,6 @@ describe("computeEditViewModel", () => {
237
245
  translate,
238
246
  featureName: "orders",
239
247
  });
240
- expect(vm.sections[0]?.fields[0]?.span).toBe(2);
248
+ expect(asFields(vm.sections[0]).fields[0]?.span).toBe(2);
241
249
  });
242
250
  });
@@ -3,7 +3,11 @@ import type {
3
3
  EntityEditScreenDefinition,
4
4
  FieldCondition,
5
5
  } from "@cosmicdrift/kumiko-framework/ui-types";
6
- import { normalizeEditField, parseRefTarget } from "@cosmicdrift/kumiko-framework/ui-types";
6
+ import {
7
+ isExtensionEditSection,
8
+ normalizeEditField,
9
+ parseRefTarget,
10
+ } from "@cosmicdrift/kumiko-framework/ui-types";
7
11
  import { buildOptionLabels, fieldLabelKey } from "./list";
8
12
  import type { EditFieldViewModel, EditSectionViewModel, EditViewModel, Translate } from "./types";
9
13
 
@@ -36,6 +40,13 @@ export function computeEditViewModel<
36
40
  const { screen, entity, values, translate, featureName, ctx } = input;
37
41
 
38
42
  const sections: EditSectionViewModel[] = screen.layout.sections.map((sectionSpec) => {
43
+ if (isExtensionEditSection(sectionSpec)) {
44
+ return {
45
+ kind: "extension" as const,
46
+ title: translate(sectionSpec.title),
47
+ component: sectionSpec.component,
48
+ };
49
+ }
39
50
  const fields: EditFieldViewModel[] = sectionSpec.fields.map((fieldSpec) => {
40
51
  const normalized = normalizeEditField(fieldSpec);
41
52
  const fieldDef = entity.fields[normalized.field];
@@ -124,6 +135,7 @@ export function computeEditViewModel<
124
135
  return view;
125
136
  });
126
137
  return {
138
+ kind: "fields" as const,
127
139
  title: translate(sectionSpec.title),
128
140
  columns: sectionSpec.columns ?? 1,
129
141
  fields,
@@ -3,7 +3,9 @@ export { computeEditViewModel } from "./edit";
3
3
  export type { ComputeListViewModelInput } from "./list";
4
4
  export { computeListViewModel, fieldLabelKey } from "./list";
5
5
  export type {
6
+ EditExtensionSectionViewModel,
6
7
  EditFieldSpec,
8
+ EditFieldsSectionViewModel,
7
9
  EditFieldViewModel,
8
10
  EditSectionSpec,
9
11
  EditSectionViewModel,
@@ -3,6 +3,7 @@ import type {
3
3
  EditSectionSpec,
4
4
  FieldRenderer,
5
5
  ListColumnSpec,
6
+ PlatformComponent,
6
7
  ScreenSlots,
7
8
  } from "@cosmicdrift/kumiko-framework/ui-types";
8
9
 
@@ -117,12 +118,24 @@ export type EditFieldViewModel = {
117
118
  readonly refMultiple?: boolean;
118
119
  };
119
120
 
120
- export type EditSectionViewModel = {
121
+ // Discriminated by `kind` — mirrors EditSectionSpec on the engine side.
122
+ // The builder always emits `kind` explicitly (no defaulting), so the
123
+ // renderer narrows with a strict equality check.
124
+ export type EditSectionViewModel = EditFieldsSectionViewModel | EditExtensionSectionViewModel;
125
+
126
+ export type EditFieldsSectionViewModel = {
127
+ readonly kind: "fields";
121
128
  readonly title: string;
122
129
  readonly columns: number;
123
130
  readonly fields: readonly EditFieldViewModel[];
124
131
  };
125
132
 
133
+ export type EditExtensionSectionViewModel = {
134
+ readonly kind: "extension";
135
+ readonly title: string;
136
+ readonly component: PlatformComponent;
137
+ };
138
+
126
139
  export type EditViewModel = {
127
140
  readonly screenId: string;
128
141
  readonly entityName: string;