@cosmicdrift/kumiko-renderer 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-renderer",
3
- "version": "0.33.0",
3
+ "version": "0.34.1",
4
4
  "description": "Platform-agnostic React renderer for Kumiko screens. Contains the shared logic — primitives-contract, hooks, KumikoScreen, navigation & SSE abstractions — that any platform-specific renderer (web, native) composes. No DOM, no EventSource, no react-dom.",
5
5
  "license": "BUSL-1.1",
6
6
  "author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
@@ -5,8 +5,10 @@ import type {
5
5
  EntityDefinition,
6
6
  EntityEditScreenDefinition,
7
7
  EntityListScreenDefinition,
8
+ FieldCondition,
8
9
  RowAction,
9
10
  RowActionWriteHandler,
11
+ RowFieldExtractor,
10
12
  ScreenDefinition,
11
13
  ToolbarAction,
12
14
  } from "@cosmicdrift/kumiko-framework/ui-types";
@@ -34,6 +36,23 @@ import { useNav } from "./nav";
34
36
  import { lastSegment } from "./qn";
35
37
  import { dispatcherErrorText, WriteFailedError } from "./write-failed-error";
36
38
 
39
+ function evalRowExtractor(
40
+ extractor: RowFieldExtractor,
41
+ row: Record<string, unknown>,
42
+ ): Record<string, unknown> {
43
+ if ("pick" in extractor) {
44
+ return Object.fromEntries(extractor.pick.map((f) => [f, row[f]]));
45
+ }
46
+ return Object.fromEntries(Object.entries(extractor.map).map(([to, from]) => [to, row[from]]));
47
+ }
48
+
49
+ function evalFieldCondition(cond: FieldCondition, values: Record<string, unknown>): boolean {
50
+ if (typeof cond === "boolean") return cond;
51
+ const val = values[cond.field];
52
+ if ("eq" in cond) return val === cond.eq;
53
+ return val !== cond.ne;
54
+ }
55
+
37
56
  // KumikoScreen picks up a ScreenDefinition from the schema by qn and
38
57
  // routes it to the right renderer based on `screen.type`. Command
39
58
  // qualification (`<feature>:write:<entity>:create` etc.) happens here
@@ -433,6 +452,10 @@ function EntityEditUpdateForm({
433
452
  entity={entity}
434
453
  featureName={schema.featureName}
435
454
  initial={initial}
455
+ // Echte route-id an die extension-section (Set-Value-UI): das
456
+ // Update-Form lässt `id` bewusst aus den Form-values, daher braucht
457
+ // die Section die id explizit — sonst create-mode trotz Edit.
458
+ entityId={entityId}
436
459
  writeCommand={writeCommand}
437
460
  payloadMode="changes"
438
461
  buildPayload={buildPayload}
@@ -659,11 +682,8 @@ function EntityListBody({
659
682
  // navigate-Variante braucht keinen Dispatcher; nav ist
660
683
  // immer da (Provider von createKumikoApp).
661
684
  if (action.kind === "navigate") {
662
- // Deklarativer entityId-Default: zielt die Action auf einen
663
- // entityEdit-Screen, ist row.id die entityId. Nötig weil die
664
- // Function-Form (action.entityId) JSON-injizierte Schemas
665
- // (window.__KUMIKO_SCHEMA__) nicht überlebt — silent gedroppt,
666
- // siehe RowAction-Type-Header.
685
+ // Default entityId für entityEdit-Targets: row["id"] wenn
686
+ // kein expliziter entityId-Feldname gesetzt ist.
667
687
  const targetIsEntityEdit = schema.screens.some(
668
688
  (s) => s.type === "entityEdit" && lastSegment(s.id) === action.screen,
669
689
  );
@@ -672,14 +692,20 @@ function EntityListBody({
672
692
  label: effectiveTranslate(action.label),
673
693
  ...(action.style !== undefined && { style: action.style }),
674
694
  onTrigger: (row: ListRowViewModel) => {
675
- const explicit = action.entityId?.(row.values);
695
+ const explicit =
696
+ action.entityId !== undefined
697
+ ? String(row.values[action.entityId] ?? "")
698
+ : undefined;
676
699
  const fallback = targetIsEntityEdit ? String(row.values["id"] ?? "") : undefined;
677
700
  const entityId = explicit ?? fallback;
678
701
  nav.navigate({
679
702
  screenId: action.screen,
680
703
  ...(entityId !== undefined && entityId !== "" && { entityId }),
681
704
  });
682
- const params = action.params?.(row.values);
705
+ const params =
706
+ action.params !== undefined
707
+ ? evalRowExtractor(action.params, row.values)
708
+ : undefined;
683
709
  if (params !== undefined) {
684
710
  // setSearchParams nimmt string|null. Komplexe Werte
685
711
  // (number/boolean) wandeln wir zu String — der Reader
@@ -696,7 +722,7 @@ function EntityListBody({
696
722
  }
697
723
  },
698
724
  ...(action.visible !== undefined && {
699
- isVisible: (row: ListRowViewModel) => action.visible?.(row.values, undefined) ?? true,
725
+ isVisible: (row: ListRowViewModel) => evalFieldCondition(action.visible!, row.values),
700
726
  }),
701
727
  };
702
728
  }
@@ -716,7 +742,9 @@ function EntityListBody({
716
742
  onTrigger: async (row: ListRowViewModel) => {
717
743
  const buildPayload = writeAction.payload;
718
744
  const payload =
719
- buildPayload !== undefined ? buildPayload(row.values) : { id: row.values["id"] };
745
+ buildPayload !== undefined
746
+ ? evalRowExtractor(buildPayload, row.values)
747
+ : { id: row.values["id"] };
720
748
  const result = await dispatcher.write(writeAction.handler, payload);
721
749
  // write() wirft nicht — Failure-Result MUSS hier zum Error
722
750
  // werden, sonst schließt der Confirm-Dialog kommentarlos und
@@ -730,7 +758,7 @@ function EntityListBody({
730
758
  },
731
759
  isVisible:
732
760
  writeAction.visible !== undefined
733
- ? (row: ListRowViewModel) => writeAction.visible?.(row.values, undefined) ?? true
761
+ ? (row: ListRowViewModel) => evalFieldCondition(writeAction.visible!, row.values)
734
762
  : undefined,
735
763
  };
736
764
  })
@@ -766,7 +794,7 @@ function EntityListBody({
766
794
  confirmLabel: effectiveTranslate(action.confirmLabel),
767
795
  }),
768
796
  onTrigger: async () => {
769
- const payload = action.payload?.() ?? {};
797
+ const payload = action.payload ?? {};
770
798
  await dispatcher.write(action.handler, payload);
771
799
  },
772
800
  };
@@ -1,6 +1,7 @@
1
1
  import type {
2
2
  EntityDefinition,
3
3
  EntityEditScreenDefinition,
4
+ FieldCondition,
4
5
  } from "@cosmicdrift/kumiko-framework/ui-types";
5
6
  import { isExtensionEditSection, normalizeEditField } from "@cosmicdrift/kumiko-framework/ui-types";
6
7
  import type {
@@ -34,6 +35,13 @@ export type RenderEditProps<TValues extends FormValues, TCtx = unknown> = {
34
35
  readonly entity: EntityDefinition;
35
36
  readonly featureName: string;
36
37
  readonly initial: TValues;
38
+ /** Echte entity-id für extension-section-Mounts (Set-Value-UI). Der
39
+ * Update-Body kennt sie aus der Route; ohne sie fiele die Section auf
40
+ * `vm.id` (= values["id"]) zurück, das im Update-Form immer fehlt
41
+ * (id ist keine deklarierte Form-Field, siehe EntityEditUpdateForm) —
42
+ * die Section bliebe dann fälschlich im create-mode. Weglassen
43
+ * (undefined) = create-mode / kein extension-Kontext (vm.id-Fallback). */
44
+ readonly entityId?: string | null;
37
45
  /** Standard single-write Submit-Pfad. Ignoriert wenn `customSubmit`
38
46
  * gesetzt ist (configEdit-Screens dispatchen mehrere Writes pro
39
47
  * Submit, da macht writeCommand keinen Sinn). */
@@ -64,6 +72,18 @@ export type RenderEditProps<TValues extends FormValues, TCtx = unknown> = {
64
72
  readonly fieldAppendix?: (fieldName: string) => ReactNode | undefined;
65
73
  };
66
74
 
75
+ function toConditionValue<TValues extends FormValues, TCtx>(
76
+ cond: FieldCondition,
77
+ ): NonNullable<FieldConditions<TValues, TCtx>["visible"]> {
78
+ if (typeof cond === "boolean") return cond;
79
+ if ("eq" in cond) {
80
+ const { field, eq } = cond;
81
+ return (values: TValues) => (values as Record<string, unknown>)[field] === eq;
82
+ }
83
+ const { field, ne } = cond;
84
+ return (values: TValues) => (values as Record<string, unknown>)[field] !== ne;
85
+ }
86
+
67
87
  function deriveFormFields<TValues extends FormValues, TCtx>(
68
88
  screen: EntityEditScreenDefinition,
69
89
  ): Record<string, FieldConditions<TValues, TCtx>> {
@@ -74,13 +94,13 @@ function deriveFormFields<TValues extends FormValues, TCtx>(
74
94
  const normalized = normalizeEditField(spec);
75
95
  out[normalized.field] = {
76
96
  ...(normalized.visible !== undefined && {
77
- visible: normalized.visible as FieldConditions<TValues, TCtx>["visible"],
97
+ visible: toConditionValue<TValues, TCtx>(normalized.visible),
78
98
  }),
79
99
  ...(normalized.readOnly !== undefined && {
80
- readonly: normalized.readOnly as FieldConditions<TValues, TCtx>["readonly"],
100
+ readonly: toConditionValue<TValues, TCtx>(normalized.readOnly),
81
101
  }),
82
102
  ...(normalized.required !== undefined && {
83
- required: normalized.required as FieldConditions<TValues, TCtx>["required"],
103
+ required: toConditionValue<TValues, TCtx>(normalized.required),
84
104
  }),
85
105
  };
86
106
  }
@@ -150,6 +170,7 @@ export function RenderEdit<TValues extends FormValues, TCtx = unknown>(
150
170
  onReload,
151
171
  submitLabel,
152
172
  fieldAppendix,
173
+ entityId: entityIdProp,
153
174
  } = props;
154
175
  const { customSubmit } = props;
155
176
  // Translate-Fallback: wenn der Caller keine Translate-Fn übergibt,
@@ -195,9 +216,8 @@ export function RenderEdit<TValues extends FormValues, TCtx = unknown>(
195
216
  values: snapshot.values,
196
217
  translate,
197
218
  featureName,
198
- ctx,
199
219
  }),
200
- [screen, entity, snapshot.values, translate, featureName, ctx],
220
+ [screen, entity, snapshot.values, translate, featureName],
201
221
  );
202
222
 
203
223
  async function handleSubmit(): Promise<void> {
@@ -310,7 +330,7 @@ export function RenderEdit<TValues extends FormValues, TCtx = unknown>(
310
330
  key={section.title}
311
331
  section={section}
312
332
  entityName={vm.entityName}
313
- entityId={vm.id}
333
+ entityId={entityIdProp !== undefined ? entityIdProp : vm.id}
314
334
  />
315
335
  );
316
336
  }