@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 +1 -1
- package/src/app/kumiko-screen.tsx +26 -9
- package/src/app/write-failed-error.ts +27 -0
- package/src/i18n-defaults.ts +30 -0
- package/src/index.ts +1 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmicdrift/kumiko-renderer",
|
|
3
|
-
"version": "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
|
|
884
|
-
// sonst hätte der Button nirgendwo hin zu navigieren.
|
|
885
|
-
//
|
|
886
|
-
//
|
|
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
|
-
|
|
889
|
-
|
|
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
|
+
}
|
package/src/i18n-defaults.ts
CHANGED
|
@@ -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";
|