@cosmicdrift/kumiko-renderer 0.32.1 → 0.34.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-renderer",
3
- "version": "0.32.1",
3
+ "version": "0.34.0",
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
@@ -659,11 +678,8 @@ function EntityListBody({
659
678
  // navigate-Variante braucht keinen Dispatcher; nav ist
660
679
  // immer da (Provider von createKumikoApp).
661
680
  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.
681
+ // Default entityId für entityEdit-Targets: row["id"] wenn
682
+ // kein expliziter entityId-Feldname gesetzt ist.
667
683
  const targetIsEntityEdit = schema.screens.some(
668
684
  (s) => s.type === "entityEdit" && lastSegment(s.id) === action.screen,
669
685
  );
@@ -672,14 +688,20 @@ function EntityListBody({
672
688
  label: effectiveTranslate(action.label),
673
689
  ...(action.style !== undefined && { style: action.style }),
674
690
  onTrigger: (row: ListRowViewModel) => {
675
- const explicit = action.entityId?.(row.values);
691
+ const explicit =
692
+ action.entityId !== undefined
693
+ ? String(row.values[action.entityId] ?? "")
694
+ : undefined;
676
695
  const fallback = targetIsEntityEdit ? String(row.values["id"] ?? "") : undefined;
677
696
  const entityId = explicit ?? fallback;
678
697
  nav.navigate({
679
698
  screenId: action.screen,
680
699
  ...(entityId !== undefined && entityId !== "" && { entityId }),
681
700
  });
682
- const params = action.params?.(row.values);
701
+ const params =
702
+ action.params !== undefined
703
+ ? evalRowExtractor(action.params, row.values)
704
+ : undefined;
683
705
  if (params !== undefined) {
684
706
  // setSearchParams nimmt string|null. Komplexe Werte
685
707
  // (number/boolean) wandeln wir zu String — der Reader
@@ -696,7 +718,7 @@ function EntityListBody({
696
718
  }
697
719
  },
698
720
  ...(action.visible !== undefined && {
699
- isVisible: (row: ListRowViewModel) => action.visible?.(row.values, undefined) ?? true,
721
+ isVisible: (row: ListRowViewModel) => evalFieldCondition(action.visible!, row.values),
700
722
  }),
701
723
  };
702
724
  }
@@ -716,7 +738,9 @@ function EntityListBody({
716
738
  onTrigger: async (row: ListRowViewModel) => {
717
739
  const buildPayload = writeAction.payload;
718
740
  const payload =
719
- buildPayload !== undefined ? buildPayload(row.values) : { id: row.values["id"] };
741
+ buildPayload !== undefined
742
+ ? evalRowExtractor(buildPayload, row.values)
743
+ : { id: row.values["id"] };
720
744
  const result = await dispatcher.write(writeAction.handler, payload);
721
745
  // write() wirft nicht — Failure-Result MUSS hier zum Error
722
746
  // werden, sonst schließt der Confirm-Dialog kommentarlos und
@@ -730,7 +754,7 @@ function EntityListBody({
730
754
  },
731
755
  isVisible:
732
756
  writeAction.visible !== undefined
733
- ? (row: ListRowViewModel) => writeAction.visible?.(row.values, undefined) ?? true
757
+ ? (row: ListRowViewModel) => evalFieldCondition(writeAction.visible!, row.values)
734
758
  : undefined,
735
759
  };
736
760
  })
@@ -766,7 +790,7 @@ function EntityListBody({
766
790
  confirmLabel: effectiveTranslate(action.confirmLabel),
767
791
  }),
768
792
  onTrigger: async () => {
769
- const payload = action.payload?.() ?? {};
793
+ const payload = action.payload ?? {};
770
794
  await dispatcher.write(action.handler, payload);
771
795
  },
772
796
  };
@@ -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 {
@@ -64,6 +65,18 @@ export type RenderEditProps<TValues extends FormValues, TCtx = unknown> = {
64
65
  readonly fieldAppendix?: (fieldName: string) => ReactNode | undefined;
65
66
  };
66
67
 
68
+ function toConditionValue<TValues extends FormValues, TCtx>(
69
+ cond: FieldCondition,
70
+ ): NonNullable<FieldConditions<TValues, TCtx>["visible"]> {
71
+ if (typeof cond === "boolean") return cond;
72
+ if ("eq" in cond) {
73
+ const { field, eq } = cond;
74
+ return (values: TValues) => (values as Record<string, unknown>)[field] === eq;
75
+ }
76
+ const { field, ne } = cond;
77
+ return (values: TValues) => (values as Record<string, unknown>)[field] !== ne;
78
+ }
79
+
67
80
  function deriveFormFields<TValues extends FormValues, TCtx>(
68
81
  screen: EntityEditScreenDefinition,
69
82
  ): Record<string, FieldConditions<TValues, TCtx>> {
@@ -74,13 +87,13 @@ function deriveFormFields<TValues extends FormValues, TCtx>(
74
87
  const normalized = normalizeEditField(spec);
75
88
  out[normalized.field] = {
76
89
  ...(normalized.visible !== undefined && {
77
- visible: normalized.visible as FieldConditions<TValues, TCtx>["visible"],
90
+ visible: toConditionValue<TValues, TCtx>(normalized.visible),
78
91
  }),
79
92
  ...(normalized.readOnly !== undefined && {
80
- readonly: normalized.readOnly as FieldConditions<TValues, TCtx>["readonly"],
93
+ readonly: toConditionValue<TValues, TCtx>(normalized.readOnly),
81
94
  }),
82
95
  ...(normalized.required !== undefined && {
83
- required: normalized.required as FieldConditions<TValues, TCtx>["required"],
96
+ required: toConditionValue<TValues, TCtx>(normalized.required),
84
97
  }),
85
98
  };
86
99
  }
@@ -195,9 +208,8 @@ export function RenderEdit<TValues extends FormValues, TCtx = unknown>(
195
208
  values: snapshot.values,
196
209
  translate,
197
210
  featureName,
198
- ctx,
199
211
  }),
200
- [screen, entity, snapshot.values, translate, featureName, ctx],
212
+ [screen, entity, snapshot.values, translate, featureName],
201
213
  );
202
214
 
203
215
  async function handleSubmit(): Promise<void> {