@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cosmicdrift/kumiko-headless",
3
- "version": "0.33.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
- // Only visible when VAT is exempt.
106
- visible: (data) => (data as { vatExempt?: boolean }).vatExempt === true,
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 predicate receives ctx and evaluates per snapshot", () => {
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 nonAdmin = computeEditViewModel({
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(nonAdmin.sections[0]).fields[0]?.readOnly).toBe(true);
153
+ expect(asFields(locked.sections[0]).fields[0]?.readOnly).toBe(true);
161
154
 
162
- const admin = computeEditViewModel({
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(admin.sections[0]).fields[0]?.readOnly).toBe(false);
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: () => false }],
173
+ fields: [{ field: "customerName", required: false }],
182
174
  },
183
175
  ],
184
176
  }),
@@ -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. Conditional predicates are
30
- // evaluated here so the renderer never re-runs them during React render.
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
- TCtx = unknown,
39
- >(input: ComputeEditViewModelInput<TValues, TCtx>): EditViewModel {
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<TValues, TCtx>(normalized.visible, true, values, ctx);
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<TValues, TCtx>(normalized.readOnly, false, values, ctx);
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<TValues, TCtx>(
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 (undefined | predicate) into its boolean
157
- // value against the current row values + ctx. `undefined` means "not
158
- // declared" — caller substitutes the entity-level default.
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
- return condition(values, ctx as TCtx);
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
  }