@cosmicdrift/kumiko-renderer 0.104.0 → 0.105.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.104.0",
3
+ "version": "0.105.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>",
@@ -15,8 +15,8 @@
15
15
  }
16
16
  },
17
17
  "dependencies": {
18
- "@cosmicdrift/kumiko-framework": "0.104.0",
19
- "@cosmicdrift/kumiko-headless": "0.104.0",
18
+ "@cosmicdrift/kumiko-framework": "0.105.0",
19
+ "@cosmicdrift/kumiko-headless": "0.105.0",
20
20
  "react": "^19.2.6"
21
21
  },
22
22
  "devDependencies": {
@@ -6,6 +6,7 @@ import type {
6
6
  EntityEditScreenDefinition,
7
7
  EntityListScreenDefinition,
8
8
  RowAction,
9
+ RowActionNavigate,
9
10
  RowActionWriteHandler,
10
11
  RowFieldExtractor,
11
12
  ScreenDefinition,
@@ -770,6 +771,45 @@ function EntityListBody({
770
771
  // Discriminated Union: writeHandler (default) dispatched einen Server-
771
772
  // Handler, navigate ruft nav.navigate() ggf. mit URL-Search-Params aus
772
773
  // params(row). nav lebt schon weiter unten in EntityListBody-Scope.
774
+ // Gemeinsame navigate-Ausführung: sowohl das "…"-Aktionsmenü (rowActions) als
775
+ // auch der Row-Body-Klick (rowClick) laufen hierdurch — ein Pfad, damit beide
776
+ // nicht auseinanderdriften.
777
+ const runNavigate = useCallback(
778
+ (action: RowActionNavigate, row: ListRowViewModel) => {
779
+ // Default entityId für entityEdit-Targets: row["id"] wenn kein expliziter
780
+ // entityId-Feldname gesetzt ist. Nur für Targets DERSELBEN Entity — sonst
781
+ // bekäme ein Cross-Entity-Edit-Screen die falsche row.id injiziert.
782
+ const targetIsEntityEdit = schema.screens.some(
783
+ (s) =>
784
+ s.type === "entityEdit" &&
785
+ s.entity === screen.entity &&
786
+ lastSegment(s.id) === action.screen,
787
+ );
788
+ const explicit =
789
+ action.entityId !== undefined ? String(row.values[action.entityId] ?? "") : undefined;
790
+ const fallback = targetIsEntityEdit ? String(row.values["id"] ?? "") : undefined;
791
+ const entityId = explicit ?? fallback;
792
+ nav.navigate({
793
+ screenId: action.screen,
794
+ ...(entityId !== undefined && entityId !== "" && { entityId }),
795
+ });
796
+ const params =
797
+ action.params !== undefined ? evalRowExtractor(action.params, row.values) : undefined;
798
+ if (params !== undefined) {
799
+ // setSearchParams nimmt string|null — komplexe Werte zu String (der Reader
800
+ // kennt via URL nur Strings). Known-edge: zielt die Action auf den
801
+ // AKTUELLEN pathname, mergen die Params auf den alten ?-String (für
802
+ // Row-Actions praktisch nicht erreichbar, Pfad differiert).
803
+ const stringified: Record<string, string | null> = {};
804
+ for (const [k, v] of Object.entries(params)) {
805
+ stringified[k] = v === null || v === undefined ? null : String(v);
806
+ }
807
+ nav.setSearchParams(stringified);
808
+ }
809
+ },
810
+ [nav, schema.screens, screen.entity],
811
+ );
812
+
773
813
  const rowActions = useMemo(() => {
774
814
  if (screen.rowActions === undefined) return undefined;
775
815
  return screen.rowActions
@@ -777,58 +817,13 @@ function EntityListBody({
777
817
  // navigate-Variante braucht keinen Dispatcher; nav ist
778
818
  // immer da (Provider von createKumikoApp).
779
819
  if (action.kind === "navigate") {
780
- // Default entityId für entityEdit-Targets: row["id"] wenn
781
- // kein expliziter entityId-Feldname gesetzt ist. Nur für Targets
782
- // DERSELBEN Entity — ein Cross-Entity-Edit-Screen bekäme sonst die
783
- // falsche row.id injiziert, und "Duplicate → Create"-Patterns
784
- // würden in den Update-Mode gezwungen. Cross-Entity-Navigation
785
- // setzt action.entityId explizit.
786
- const targetIsEntityEdit = schema.screens.some(
787
- (s) =>
788
- s.type === "entityEdit" &&
789
- s.entity === screen.entity &&
790
- lastSegment(s.id) === action.screen,
791
- );
820
+ const navigateAction = action;
792
821
  const actionVisible = action.visible;
793
822
  return {
794
823
  id: action.id,
795
824
  label: effectiveTranslate(action.label),
796
825
  ...(action.style !== undefined && { style: action.style }),
797
- onTrigger: (row: ListRowViewModel) => {
798
- const explicit =
799
- action.entityId !== undefined
800
- ? String(row.values[action.entityId] ?? "")
801
- : undefined;
802
- const fallback = targetIsEntityEdit ? String(row.values["id"] ?? "") : undefined;
803
- const entityId = explicit ?? fallback;
804
- nav.navigate({
805
- screenId: action.screen,
806
- ...(entityId !== undefined && entityId !== "" && { entityId }),
807
- });
808
- const params =
809
- action.params !== undefined
810
- ? evalRowExtractor(action.params, row.values)
811
- : undefined;
812
- if (params !== undefined) {
813
- // setSearchParams nimmt string|null. Komplexe Werte
814
- // (number/boolean) wandeln wir zu String — der Reader
815
- // (use-list-url-state / actionForm-init) kennt nur
816
- // Strings via URL.
817
- const stringified: Record<string, string | null> = {};
818
- for (const [k, v] of Object.entries(params)) {
819
- stringified[k] = v === null || v === undefined ? null : String(v);
820
- }
821
- // NACH navigate: pushState trägt keine Query — Params die
822
- // vor dem Push gesetzt werden, kleben an der ALTEN URL und
823
- // sind auf dem Ziel-Screen weg (actionForm-Prefill leer).
824
- // Bekannte Kante (bewusst offen): zielt die Action auf den
825
- // AKTUELLEN pathname, short-circuit't pushPath ohne die Query
826
- // zu leeren — die neuen Params mergen dann auf den alten
827
- // ?-String. Für Row-Actions praktisch nicht erreichbar
828
- // (Pfad differiert über entityId/screen).
829
- nav.setSearchParams(stringified);
830
- }
831
- },
826
+ onTrigger: (row: ListRowViewModel) => runNavigate(navigateAction, row),
832
827
  ...(actionVisible !== undefined && {
833
828
  isVisible: (row: ListRowViewModel) => evalFieldCondition(actionVisible, row.values),
834
829
  }),
@@ -872,7 +867,7 @@ function EntityListBody({
872
867
  };
873
868
  })
874
869
  .filter((a: DataTableRowAction | null): a is DataTableRowAction => a !== null);
875
- }, [screen.rowActions, screen.entity, effectiveTranslate, dispatcher, nav, schema.screens]);
870
+ }, [screen.rowActions, effectiveTranslate, dispatcher, runNavigate]);
876
871
 
877
872
  // ToolbarActions: Schema → Resolved-Form (analog rowActions).
878
873
  // navigate-kind → useNav().navigate({ screenId }), writeHandler-kind
@@ -930,8 +925,16 @@ function EntityListBody({
930
925
  // 2-arg shape (row, entityName) is the public surface — thread the
931
926
  // screen's entity through here so callers don't have to re-derive
932
927
  // it.
933
- const wrappedOnRowClick =
934
- onRowClick !== undefined
928
+ // Row-Body-Klick: eine explizit als rowClick markierte navigate-rowAction
929
+ // gewinnt (deklarierte Author-Absicht "Klick auf die Zeile tut X"). Ohne Flag
930
+ // bleibt der Pfad unverändert — create-app's entityEdit-Default bzw. ein vom
931
+ // App-Author gesetztes onRowClick.
932
+ const rowClickAction = screen.rowActions?.find(
933
+ (a): a is RowActionNavigate => a.kind === "navigate" && a.rowClick === true,
934
+ );
935
+ const wrappedOnRowClick = rowClickAction
936
+ ? (row: ListRowViewModel) => runNavigate(rowClickAction, row)
937
+ : onRowClick !== undefined
935
938
  ? (row: ListRowViewModel) => onRowClick(row, screen.entity)
936
939
  : undefined;
937
940