@cosmicdrift/kumiko-renderer 0.31.0 → 0.32.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.31.0",
3
+ "version": "0.32.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>",
@@ -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
@@ -663,6 +664,11 @@ function EntityListBody({
663
664
  label: effectiveTranslate(action.label),
664
665
  ...(action.style !== undefined && { style: action.style }),
665
666
  onTrigger: (row: ListRowViewModel) => {
667
+ const entityId = action.entityId?.(row.values);
668
+ nav.navigate({
669
+ screenId: action.screen,
670
+ ...(entityId !== undefined && entityId !== "" && { entityId }),
671
+ });
666
672
  const params = action.params?.(row.values);
667
673
  if (params !== undefined) {
668
674
  // setSearchParams nimmt string|null. Komplexe Werte
@@ -673,9 +679,11 @@ function EntityListBody({
673
679
  for (const [k, v] of Object.entries(params)) {
674
680
  stringified[k] = v === null || v === undefined ? null : String(v);
675
681
  }
682
+ // NACH navigate: pushState trägt keine Query — Params die
683
+ // vor dem Push gesetzt werden, kleben an der ALTEN URL und
684
+ // sind auf dem Ziel-Screen weg (actionForm-Prefill leer).
676
685
  nav.setSearchParams(stringified);
677
686
  }
678
- nav.navigate({ screenId: action.screen });
679
687
  },
680
688
  ...(action.visible !== undefined && {
681
689
  isVisible: (row: ListRowViewModel) => action.visible?.(row.values, undefined) ?? true,
@@ -699,7 +707,16 @@ function EntityListBody({
699
707
  const buildPayload = writeAction.payload;
700
708
  const payload =
701
709
  buildPayload !== undefined ? buildPayload(row.values) : { id: row.values["id"] };
702
- await dispatcher.write(writeAction.handler, payload);
710
+ const result = await dispatcher.write(writeAction.handler, payload);
711
+ // write() wirft nicht — Failure-Result MUSS hier zum Error
712
+ // werden, sonst schließt der Confirm-Dialog kommentarlos und
713
+ // der User sieht "nichts passiert" (Prod-Bug 2026-06-07).
714
+ if (!result.isSuccess) {
715
+ throw new WriteFailedError(
716
+ result.error,
717
+ dispatcherErrorText(result.error, effectiveTranslate),
718
+ );
719
+ }
703
720
  },
704
721
  isVisible:
705
722
  writeAction.visible !== undefined
@@ -880,15 +897,15 @@ function ActionFormBody({
880
897
  },
881
898
  [nav, screen.redirect],
882
899
  );
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.
900
+ // Cancel ist nur sinnvoll wenn ein Navigations-Ziel existiert
901
+ // sonst hätte der Button nirgendwo hin zu navigieren. cancelTarget
902
+ // gewinnt über redirect; `false` schaltet den Button explizit ab
903
+ // (Single-Action-Screens, wo Cancel nur Submit-ohne-Senden wäre).
887
904
  const handleCancel = useMemo<(() => void) | undefined>(() => {
888
- if (screen.redirect === undefined) return undefined;
889
- const target = screen.redirect;
905
+ const target = screen.cancelTarget ?? screen.redirect;
906
+ if (target === undefined || target === false) return undefined;
890
907
  return () => nav.navigate({ screenId: target });
891
- }, [nav, screen.redirect]);
908
+ }, [nav, screen.redirect, screen.cancelTarget]);
892
909
  return (
893
910
  <RenderEdit
894
911
  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";