@cosmicdrift/kumiko-headless 0.33.0 → 0.34.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 +1 -1
- package/src/view-model/__tests__/edit.test.ts +11 -19
- package/src/view-model/edit.ts +15 -38
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmicdrift/kumiko-headless",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.34.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>",
|
|
@@ -102,9 +102,8 @@ describe("computeEditViewModel", () => {
|
|
|
102
102
|
"vatExempt",
|
|
103
103
|
{
|
|
104
104
|
field: "vatReason",
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
required: (data) => (data as { vatExempt?: boolean }).vatExempt === true,
|
|
105
|
+
visible: { field: "vatExempt", eq: true },
|
|
106
|
+
required: { field: "vatExempt", eq: true },
|
|
108
107
|
},
|
|
109
108
|
],
|
|
110
109
|
},
|
|
@@ -134,40 +133,33 @@ describe("computeEditViewModel", () => {
|
|
|
134
133
|
expect(reasonShown?.required).toBe(true);
|
|
135
134
|
});
|
|
136
135
|
|
|
137
|
-
test("readonly
|
|
136
|
+
test("readonly condition { field, ne } evaluates against form values", () => {
|
|
138
137
|
const screen = editScreen({
|
|
139
138
|
sections: [
|
|
140
139
|
{
|
|
141
140
|
title: "x",
|
|
142
|
-
fields: [
|
|
143
|
-
{
|
|
144
|
-
field: "customerName",
|
|
145
|
-
readOnly: (_d, ctx) => (ctx as { isAdmin: boolean }).isAdmin === false,
|
|
146
|
-
},
|
|
147
|
-
],
|
|
141
|
+
fields: [{ field: "customerName", readOnly: { field: "isEditable", ne: true } }],
|
|
148
142
|
},
|
|
149
143
|
],
|
|
150
144
|
});
|
|
151
145
|
|
|
152
|
-
const
|
|
146
|
+
const locked = computeEditViewModel({
|
|
153
147
|
screen,
|
|
154
148
|
entity: orderEntity,
|
|
155
|
-
values: { customerName: "A" },
|
|
156
|
-
ctx: { isAdmin: false },
|
|
149
|
+
values: { customerName: "A", isEditable: false },
|
|
157
150
|
translate,
|
|
158
151
|
featureName: "orders",
|
|
159
152
|
});
|
|
160
|
-
expect(asFields(
|
|
153
|
+
expect(asFields(locked.sections[0]).fields[0]?.readOnly).toBe(true);
|
|
161
154
|
|
|
162
|
-
const
|
|
155
|
+
const editable = computeEditViewModel({
|
|
163
156
|
screen,
|
|
164
157
|
entity: orderEntity,
|
|
165
|
-
values: { customerName: "A" },
|
|
166
|
-
ctx: { isAdmin: true },
|
|
158
|
+
values: { customerName: "A", isEditable: true },
|
|
167
159
|
translate,
|
|
168
160
|
featureName: "orders",
|
|
169
161
|
});
|
|
170
|
-
expect(asFields(
|
|
162
|
+
expect(asFields(editable.sections[0]).fields[0]?.readOnly).toBe(false);
|
|
171
163
|
});
|
|
172
164
|
|
|
173
165
|
test("screen-level required override wins over entity-level required", () => {
|
|
@@ -178,7 +170,7 @@ describe("computeEditViewModel", () => {
|
|
|
178
170
|
sections: [
|
|
179
171
|
{
|
|
180
172
|
title: "x",
|
|
181
|
-
fields: [{ field: "customerName", required:
|
|
173
|
+
fields: [{ field: "customerName", required: false }],
|
|
182
174
|
},
|
|
183
175
|
],
|
|
184
176
|
}),
|
package/src/view-model/edit.ts
CHANGED
|
@@ -13,31 +13,21 @@ import type { EditFieldViewModel, EditSectionViewModel, EditViewModel, Translate
|
|
|
13
13
|
|
|
14
14
|
export type ComputeEditViewModelInput<
|
|
15
15
|
TValues extends Readonly<Record<string, unknown>> = Readonly<Record<string, unknown>>,
|
|
16
|
-
TCtx = unknown,
|
|
17
16
|
> = {
|
|
18
17
|
readonly screen: EntityEditScreenDefinition;
|
|
19
18
|
readonly entity: EntityDefinition;
|
|
20
19
|
readonly values: TValues;
|
|
21
20
|
readonly translate: Translate;
|
|
22
21
|
readonly featureName: string;
|
|
23
|
-
// Optional condition context — forwarded to field visible/readonly/required
|
|
24
|
-
// predicates. Normally the host app passes `{ user, config, ... }`.
|
|
25
|
-
readonly ctx?: TCtx;
|
|
26
22
|
};
|
|
27
23
|
|
|
28
24
|
// Pure transform from screen-def + entity-def + row-values to the flat
|
|
29
|
-
// section/field tree the renderer draws.
|
|
30
|
-
//
|
|
31
|
-
//
|
|
32
|
-
// TValues / TCtx are propagated through evalCondition so call-sites can
|
|
33
|
-
// `computeEditViewModel<OrderRow, AdminCtx>(...)` and get typed data/ctx
|
|
34
|
-
// inside their predicates. Default `unknown` keeps unannotated call-sites
|
|
35
|
-
// working unchanged.
|
|
25
|
+
// section/field tree the renderer draws. FieldConditions are evaluated here
|
|
26
|
+
// so the renderer never re-runs them during React render.
|
|
36
27
|
export function computeEditViewModel<
|
|
37
28
|
TValues extends Readonly<Record<string, unknown>> = Readonly<Record<string, unknown>>,
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
const { screen, entity, values, translate, featureName, ctx } = input;
|
|
29
|
+
>(input: ComputeEditViewModelInput<TValues>): EditViewModel {
|
|
30
|
+
const { screen, entity, values, translate, featureName } = input;
|
|
41
31
|
|
|
42
32
|
const sections: EditSectionViewModel[] = screen.layout.sections.map((sectionSpec) => {
|
|
43
33
|
if (isExtensionEditSection(sectionSpec)) {
|
|
@@ -56,22 +46,17 @@ export function computeEditViewModel<
|
|
|
56
46
|
);
|
|
57
47
|
}
|
|
58
48
|
const label = translate(fieldLabelKey(featureName, screen.entity, normalized.field));
|
|
59
|
-
const visible = evalCondition
|
|
49
|
+
const visible = evalCondition(normalized.visible, true, values);
|
|
60
50
|
// `readOnly` (camelCase) is the name on both sides: EditFieldSpec
|
|
61
51
|
// in the engine, and the view-model emitted here. One convention
|
|
62
52
|
// through the stack beats translating at the boundary.
|
|
63
|
-
const readOnly = evalCondition
|
|
53
|
+
const readOnly = evalCondition(normalized.readOnly, false, values);
|
|
64
54
|
// `required` on the field-spec overrides the entity-default. A
|
|
65
55
|
// field that's required at the entity-level but marked required:
|
|
66
56
|
// false on the screen (e.g. a soft-onboarding wizard that
|
|
67
57
|
// collects less up-front) respects the screen override.
|
|
68
58
|
const entityRequired = (fieldDef as unknown as { required?: boolean }).required === true;
|
|
69
|
-
const required = evalCondition
|
|
70
|
-
normalized.required,
|
|
71
|
-
entityRequired,
|
|
72
|
-
values,
|
|
73
|
-
ctx,
|
|
74
|
-
);
|
|
59
|
+
const required = evalCondition(normalized.required, entityRequired, values);
|
|
75
60
|
// Select-Optionen bei `type: "select"` mitnehmen — der Renderer
|
|
76
61
|
// braucht sie für das Dropdown ohne nochmal die EntityDefinition
|
|
77
62
|
// zu reichen. Plus translated Labels (gleiche Convention wie der
|
|
@@ -153,24 +138,16 @@ export function computeEditViewModel<
|
|
|
153
138
|
};
|
|
154
139
|
}
|
|
155
140
|
|
|
156
|
-
// Resolves a FieldCondition
|
|
157
|
-
//
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
// TValues / TCtx mirror the generics on FieldCondition. EditFieldSpec
|
|
161
|
-
// stores predicates as FieldCondition<unknown, unknown>, which is
|
|
162
|
-
// assignable to FieldCondition<TValues, TCtx> for any TValues/TCtx by
|
|
163
|
-
// parameter contravariance (a predicate that accepts `unknown` accepts
|
|
164
|
-
// anything). The cast on ctx is the one rough edge: input.ctx is
|
|
165
|
-
// optional, so at runtime we may pass undefined to a predicate that
|
|
166
|
-
// declared a concrete TCtx — the caller is responsible for not doing
|
|
167
|
-
// that when they narrow TCtx away from `unknown`.
|
|
168
|
-
function evalCondition<TValues, TCtx>(
|
|
169
|
-
condition: FieldCondition<TValues, TCtx> | undefined,
|
|
141
|
+
// Resolves a FieldCondition against the current row values.
|
|
142
|
+
// `undefined` means "not declared" — caller substitutes the default.
|
|
143
|
+
function evalCondition<TValues>(
|
|
144
|
+
condition: FieldCondition | undefined,
|
|
170
145
|
fallback: boolean,
|
|
171
146
|
values: TValues,
|
|
172
|
-
ctx: TCtx | undefined,
|
|
173
147
|
): boolean {
|
|
174
148
|
if (condition === undefined) return fallback;
|
|
175
|
-
|
|
149
|
+
if (typeof condition === "boolean") return condition;
|
|
150
|
+
const val = (values as Record<string, unknown>)[condition.field];
|
|
151
|
+
if ("eq" in condition) return val === condition.eq;
|
|
152
|
+
return val !== condition.ne;
|
|
176
153
|
}
|