@cosmicdrift/kumiko-renderer 0.31.1 → 0.32.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.31.1",
3
+ "version": "0.32.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>",
@@ -32,6 +32,7 @@ import { useCustomScreenComponent } from "./custom-screens";
32
32
  import type { FeatureSchema } from "./feature-schema";
33
33
  import { useNav } from "./nav";
34
34
  import { lastSegment } from "./qn";
35
+ import { dispatcherErrorText, WriteFailedError } from "./write-failed-error";
35
36
 
36
37
  // KumikoScreen picks up a ScreenDefinition from the schema by qn and
37
38
  // routes it to the right renderer based on `screen.type`. Command
@@ -658,11 +659,26 @@ function EntityListBody({
658
659
  // navigate-Variante braucht keinen Dispatcher; nav ist
659
660
  // immer da (Provider von createKumikoApp).
660
661
  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.
667
+ const targetIsEntityEdit = schema.screens.some(
668
+ (s) => s.type === "entityEdit" && lastSegment(s.id) === action.screen,
669
+ );
661
670
  return {
662
671
  id: action.id,
663
672
  label: effectiveTranslate(action.label),
664
673
  ...(action.style !== undefined && { style: action.style }),
665
674
  onTrigger: (row: ListRowViewModel) => {
675
+ const explicit = action.entityId?.(row.values);
676
+ const fallback = targetIsEntityEdit ? String(row.values["id"] ?? "") : undefined;
677
+ const entityId = explicit ?? fallback;
678
+ nav.navigate({
679
+ screenId: action.screen,
680
+ ...(entityId !== undefined && entityId !== "" && { entityId }),
681
+ });
666
682
  const params = action.params?.(row.values);
667
683
  if (params !== undefined) {
668
684
  // setSearchParams nimmt string|null. Komplexe Werte
@@ -673,9 +689,11 @@ function EntityListBody({
673
689
  for (const [k, v] of Object.entries(params)) {
674
690
  stringified[k] = v === null || v === undefined ? null : String(v);
675
691
  }
692
+ // NACH navigate: pushState trägt keine Query — Params die
693
+ // vor dem Push gesetzt werden, kleben an der ALTEN URL und
694
+ // sind auf dem Ziel-Screen weg (actionForm-Prefill leer).
676
695
  nav.setSearchParams(stringified);
677
696
  }
678
- nav.navigate({ screenId: action.screen });
679
697
  },
680
698
  ...(action.visible !== undefined && {
681
699
  isVisible: (row: ListRowViewModel) => action.visible?.(row.values, undefined) ?? true,
@@ -699,7 +717,16 @@ function EntityListBody({
699
717
  const buildPayload = writeAction.payload;
700
718
  const payload =
701
719
  buildPayload !== undefined ? buildPayload(row.values) : { id: row.values["id"] };
702
- await dispatcher.write(writeAction.handler, payload);
720
+ const result = await dispatcher.write(writeAction.handler, payload);
721
+ // write() wirft nicht — Failure-Result MUSS hier zum Error
722
+ // werden, sonst schließt der Confirm-Dialog kommentarlos und
723
+ // der User sieht "nichts passiert" (Prod-Bug 2026-06-07).
724
+ if (!result.isSuccess) {
725
+ throw new WriteFailedError(
726
+ result.error,
727
+ dispatcherErrorText(result.error, effectiveTranslate),
728
+ );
729
+ }
703
730
  },
704
731
  isVisible:
705
732
  writeAction.visible !== undefined
@@ -708,7 +735,7 @@ function EntityListBody({
708
735
  };
709
736
  })
710
737
  .filter((a: DataTableRowAction | null): a is DataTableRowAction => a !== null);
711
- }, [screen.rowActions, effectiveTranslate, dispatcher, nav]);
738
+ }, [screen.rowActions, effectiveTranslate, dispatcher, nav, schema.screens]);
712
739
 
713
740
  // ToolbarActions: Schema → Resolved-Form (analog rowActions).
714
741
  // navigate-kind → useNav().navigate({ screenId }), writeHandler-kind
@@ -880,15 +907,15 @@ function ActionFormBody({
880
907
  },
881
908
  [nav, screen.redirect],
882
909
  );
883
- // Cancel ist nur sinnvoll wenn ein Redirect-Target gesetzt ist
884
- // sonst hätte der Button nirgendwo hin zu navigieren. Bei Forms
885
- // ohne redirect bleibt der User per Sidebar/Browser-Back im Flow,
886
- // analog zu Settings-Pages.
910
+ // Cancel ist nur sinnvoll wenn ein Navigations-Ziel existiert
911
+ // sonst hätte der Button nirgendwo hin zu navigieren. cancelTarget
912
+ // gewinnt über redirect; `false` schaltet den Button explizit ab
913
+ // (Single-Action-Screens, wo Cancel nur Submit-ohne-Senden wäre).
887
914
  const handleCancel = useMemo<(() => void) | undefined>(() => {
888
- if (screen.redirect === undefined) return undefined;
889
- const target = screen.redirect;
915
+ const target = screen.cancelTarget ?? screen.redirect;
916
+ if (target === undefined || target === false) return undefined;
890
917
  return () => nav.navigate({ screenId: target });
891
- }, [nav, screen.redirect]);
918
+ }, [nav, screen.redirect, screen.cancelTarget]);
892
919
  return (
893
920
  <RenderEdit
894
921
  screen={synthScreen}
@@ -0,0 +1,27 @@
1
+ import type { DispatcherError } from "@cosmicdrift/kumiko-headless";
2
+
3
+ // dispatcher.write wirft bei Server-Fehlern NICHT — es returnt
4
+ // { isSuccess: false, error }. Action-Wiring das das Result verwirft,
5
+ // macht Fehler unsichtbar ("Klick tut nichts"-Prod-Bug 2026-06-07).
6
+ // Diese Klasse trägt den strukturierten DispatcherError dahin, wo eine
7
+ // UI ihn anzeigen kann (Toast mit docsUrl).
8
+ export class WriteFailedError extends Error {
9
+ readonly dispatcherError: DispatcherError;
10
+
11
+ constructor(error: DispatcherError, message: string) {
12
+ super(message);
13
+ this.name = "WriteFailedError";
14
+ this.dispatcherError = error;
15
+ }
16
+ }
17
+
18
+ // i18nKey gewinnt wenn das Bundle ihn kennt (translate returnt den Key
19
+ // unverändert wenn nicht — Renderer-Convention), sonst message, zuletzt code.
20
+ export function dispatcherErrorText(
21
+ error: DispatcherError,
22
+ translate: (key: string) => string,
23
+ ): string {
24
+ const translated = translate(error.i18nKey);
25
+ if (translated !== error.i18nKey) return translated;
26
+ return error.message !== "" ? error.message : error.code;
27
+ }
@@ -41,6 +41,22 @@ export const kumikoDefaultTranslations: TranslationsByLocale = {
41
41
  "kumiko.dialog.cancel": "Abbrechen",
42
42
  "kumiko.dialog.close": "Schließen",
43
43
 
44
+ // Row-Actions — Fehler-Toast wenn ein Action-Write fehlschlägt.
45
+ "kumiko.rowAction.failed": "Aktion fehlgeschlagen",
46
+
47
+ // Config-Cascade — Source-Badges + Cascade-Panel (ConfigCascadeView).
48
+ "kumiko.config.source.user": "Mein Wert",
49
+ "kumiko.config.source.tenant": "Tenant",
50
+ "kumiko.config.source.system": "System",
51
+ "kumiko.config.source.appOverride": "App-Override",
52
+ "kumiko.config.source.computed": "Berechnet",
53
+ "kumiko.config.source.default": "Standard",
54
+ "kumiko.config.source.missing": "Fehlt",
55
+ "kumiko.config.cascade.preset": "Vorgabe",
56
+ "kumiko.config.cascade.noValue": "Kein Wert gesetzt",
57
+ "kumiko.config.cascade.activeMarker": "aktiv",
58
+ "kumiko.config.cascade.resetTo": "Überschreibung zurücksetzen ({scope})",
59
+
44
60
  // Form — Standard-Errors (App-Code kann eigene zod-Reasons nutzen,
45
61
  // diese sind die letzte Sicherheitsschicht).
46
62
  "kumiko.form.error.generic": "Etwas ist schiefgegangen.",
@@ -81,6 +97,20 @@ export const kumikoDefaultTranslations: TranslationsByLocale = {
81
97
  "kumiko.dialog.cancel": "Cancel",
82
98
  "kumiko.dialog.close": "Close",
83
99
 
100
+ "kumiko.rowAction.failed": "Action failed",
101
+
102
+ "kumiko.config.source.user": "My value",
103
+ "kumiko.config.source.tenant": "Tenant",
104
+ "kumiko.config.source.system": "System",
105
+ "kumiko.config.source.appOverride": "App override",
106
+ "kumiko.config.source.computed": "Computed",
107
+ "kumiko.config.source.default": "Default",
108
+ "kumiko.config.source.missing": "Missing",
109
+ "kumiko.config.cascade.preset": "Preset",
110
+ "kumiko.config.cascade.noValue": "No value set",
111
+ "kumiko.config.cascade.activeMarker": "active",
112
+ "kumiko.config.cascade.resetTo": "Reset override ({scope})",
113
+
84
114
  "kumiko.form.error.generic": "Something went wrong.",
85
115
  "kumiko.form.error.version-conflict":
86
116
  "Record was modified in the meantime. Reload and try again.",
package/src/index.ts CHANGED
@@ -36,6 +36,7 @@ export { KumikoScreen, qualifyNavId, qualifyScreenId } from "./app/kumiko-screen
36
36
  export type { NavApi, NavProviderProps, NavRoute, NavTarget } from "./app/nav";
37
37
  export { formatPath, NavProvider, parsePath, useNav } from "./app/nav";
38
38
  export { lastSegment } from "./app/qn";
39
+ export { dispatcherErrorText, WriteFailedError } from "./app/write-failed-error";
39
40
  export type { RenderEditProps } from "./components/render-edit";
40
41
  export { RenderEdit } from "./components/render-edit";
41
42
  export type { RenderFieldProps } from "./components/render-field";