@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 +1 -1
- package/src/app/kumiko-screen.tsx +37 -10
- 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.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
|
|
884
|
-
// sonst hätte der Button nirgendwo hin zu navigieren.
|
|
885
|
-
//
|
|
886
|
-
//
|
|
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
|
-
|
|
889
|
-
|
|
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
|
+
}
|
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";
|