@cosmicdrift/kumiko-headless 0.168.0 → 0.171.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.171.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>",
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
}
|
|
37
37
|
},
|
|
38
38
|
"dependencies": {
|
|
39
|
-
"@cosmicdrift/kumiko-framework": "0.
|
|
39
|
+
"@cosmicdrift/kumiko-framework": "0.171.0",
|
|
40
40
|
"temporal-polyfill": "^0.3.2",
|
|
41
41
|
"zod": "^4.4.3"
|
|
42
42
|
},
|
|
@@ -19,6 +19,7 @@ const orderEntity = {
|
|
|
19
19
|
notes: { type: "text" },
|
|
20
20
|
vatExempt: { type: "boolean" },
|
|
21
21
|
vatReason: { type: "text" },
|
|
22
|
+
kind: { type: "text" },
|
|
22
23
|
},
|
|
23
24
|
} as unknown as EntityDefinition;
|
|
24
25
|
|
|
@@ -152,6 +153,34 @@ describe("computeEditViewModel", () => {
|
|
|
152
153
|
expect(reasonShown?.required).toBe(true);
|
|
153
154
|
});
|
|
154
155
|
|
|
156
|
+
test("section with all fields hidden by visible condition → section.visible is false but section stays in the array (key stability)", () => {
|
|
157
|
+
const screen = editScreen({
|
|
158
|
+
sections: [
|
|
159
|
+
{
|
|
160
|
+
title: "Person",
|
|
161
|
+
fields: [{ field: "customerName", visible: { field: "kind", eq: "person" } }],
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
title: "Firma",
|
|
165
|
+
fields: [{ field: "notes", visible: { field: "kind", eq: "company" } }],
|
|
166
|
+
},
|
|
167
|
+
],
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
const vm = computeEditViewModel({
|
|
171
|
+
screen,
|
|
172
|
+
entity: orderEntity,
|
|
173
|
+
values: { kind: "person", customerName: "Acme" },
|
|
174
|
+
translate,
|
|
175
|
+
featureName: "orders",
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
expect(vm.sections).toHaveLength(2);
|
|
179
|
+
expect(asFields(vm.sections[0]).visible).toBe(true);
|
|
180
|
+
expect(asFields(vm.sections[1]).visible).toBe(false);
|
|
181
|
+
expect(vm.sections[1]?.title).toBe("Firma");
|
|
182
|
+
});
|
|
183
|
+
|
|
155
184
|
test("readonly condition { field, ne } evaluates against form values", () => {
|
|
156
185
|
const screen = editScreen({
|
|
157
186
|
sections: [
|
|
@@ -258,6 +287,42 @@ describe("computeEditViewModel", () => {
|
|
|
258
287
|
});
|
|
259
288
|
expect(asFields(vm.sections[0]).fields[0]?.span).toBe(2);
|
|
260
289
|
});
|
|
290
|
+
|
|
291
|
+
test("section.description is translated and passed through; absent stays undefined", () => {
|
|
292
|
+
const vm = computeEditViewModel({
|
|
293
|
+
screen: editScreen({
|
|
294
|
+
sections: [
|
|
295
|
+
{
|
|
296
|
+
title: "Basics",
|
|
297
|
+
description: "orders:screen:order-edit.basics.help",
|
|
298
|
+
fields: ["notes"],
|
|
299
|
+
},
|
|
300
|
+
{ title: "Extra", fields: ["kind"] },
|
|
301
|
+
],
|
|
302
|
+
}),
|
|
303
|
+
entity: orderEntity,
|
|
304
|
+
values: {},
|
|
305
|
+
translate,
|
|
306
|
+
featureName: "orders",
|
|
307
|
+
});
|
|
308
|
+
expect(asFields(vm.sections[0]).description).toBe("orders:screen:order-edit.basics.help");
|
|
309
|
+
expect(asFields(vm.sections[1]).description).toBeUndefined();
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
test("field.icon propagates to the view model; absent stays undefined", () => {
|
|
313
|
+
const vm = computeEditViewModel({
|
|
314
|
+
screen: editScreen({
|
|
315
|
+
sections: [{ title: "x", fields: [{ field: "customerName", icon: "user" }, "notes"] }],
|
|
316
|
+
}),
|
|
317
|
+
entity: orderEntity,
|
|
318
|
+
values: {},
|
|
319
|
+
translate,
|
|
320
|
+
featureName: "orders",
|
|
321
|
+
});
|
|
322
|
+
const fields = asFields(vm.sections[0]).fields;
|
|
323
|
+
expect(fields[0]?.icon).toBe("user");
|
|
324
|
+
expect(fields[1]?.icon).toBeUndefined();
|
|
325
|
+
});
|
|
261
326
|
});
|
|
262
327
|
|
|
263
328
|
describe("computeEditViewModel — date/timestamp min/max/locale (#369)", () => {
|
package/src/view-model/edit.ts
CHANGED
|
@@ -151,13 +151,21 @@ export function computeEditViewModel<
|
|
|
151
151
|
...(fileDef?.accept !== undefined && { accept: fileDef.accept }),
|
|
152
152
|
...(fileDef?.maxSize !== undefined && { maxSize: fileDef.maxSize }),
|
|
153
153
|
...(isFileType && { entityType: screen.entity, fieldName: normalized.field }),
|
|
154
|
+
...(normalized.icon !== undefined && { icon: normalized.icon }),
|
|
154
155
|
};
|
|
155
156
|
return view;
|
|
156
157
|
});
|
|
158
|
+
// Boot-validator rejects fields.length === 0 (screens.ts), so an empty
|
|
159
|
+
// section never reaches this code.
|
|
160
|
+
const visible = fields.some((field) => field.visible);
|
|
157
161
|
return {
|
|
158
162
|
kind: "fields" as const,
|
|
163
|
+
visible,
|
|
159
164
|
// Titellose Section (flache Form) → kein h3; nur übersetzen wenn gesetzt.
|
|
160
165
|
...(sectionSpec.title !== undefined && { title: translate(sectionSpec.title) }),
|
|
166
|
+
...(sectionSpec.description !== undefined && {
|
|
167
|
+
description: translate(sectionSpec.description),
|
|
168
|
+
}),
|
|
161
169
|
columns: sectionSpec.columns ?? 1,
|
|
162
170
|
fields,
|
|
163
171
|
};
|
package/src/view-model/types.ts
CHANGED
|
@@ -143,6 +143,11 @@ export type EditFieldViewModel = {
|
|
|
143
143
|
* die richtige Field-Def prüfen kann. */
|
|
144
144
|
readonly entityType?: string;
|
|
145
145
|
readonly fieldName?: string;
|
|
146
|
+
/** Prefix-icon key from `EditFieldSpec.icon`, passed through unchanged
|
|
147
|
+
* (no i18n — it's a symbolic key, not a display string). The renderer
|
|
148
|
+
* resolves it against the FIELD_ICONS registry; unknown keys silently
|
|
149
|
+
* fall back to "no icon". */
|
|
150
|
+
readonly icon?: string;
|
|
146
151
|
};
|
|
147
152
|
|
|
148
153
|
// Discriminated by `kind` — mirrors EditSectionSpec on the engine side.
|
|
@@ -152,8 +157,15 @@ export type EditSectionViewModel = EditFieldsSectionViewModel | EditExtensionSec
|
|
|
152
157
|
|
|
153
158
|
export type EditFieldsSectionViewModel = {
|
|
154
159
|
readonly kind: "fields";
|
|
160
|
+
/** False when the section has fields but none of them is visible.
|
|
161
|
+
* The renderer then draws nothing, but the section stays in the array
|
|
162
|
+
* (key stability). */
|
|
163
|
+
readonly visible: boolean;
|
|
155
164
|
/** Optional — eine titellose Section rendert nur ihre Felder (flache Form). */
|
|
156
165
|
readonly title?: string;
|
|
166
|
+
/** Translated help text under the block heading, from
|
|
167
|
+
* `EditFieldsSection.description`. Renders even without `title`. */
|
|
168
|
+
readonly description?: string;
|
|
157
169
|
readonly columns: number;
|
|
158
170
|
readonly fields: readonly EditFieldViewModel[];
|
|
159
171
|
};
|