@cosmicdrift/kumiko-renderer 0.33.0 → 0.34.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 +39 -11
- package/src/components/render-edit.tsx +26 -6
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmicdrift/kumiko-renderer",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.34.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>",
|
|
@@ -5,8 +5,10 @@ import type {
|
|
|
5
5
|
EntityDefinition,
|
|
6
6
|
EntityEditScreenDefinition,
|
|
7
7
|
EntityListScreenDefinition,
|
|
8
|
+
FieldCondition,
|
|
8
9
|
RowAction,
|
|
9
10
|
RowActionWriteHandler,
|
|
11
|
+
RowFieldExtractor,
|
|
10
12
|
ScreenDefinition,
|
|
11
13
|
ToolbarAction,
|
|
12
14
|
} from "@cosmicdrift/kumiko-framework/ui-types";
|
|
@@ -34,6 +36,23 @@ import { useNav } from "./nav";
|
|
|
34
36
|
import { lastSegment } from "./qn";
|
|
35
37
|
import { dispatcherErrorText, WriteFailedError } from "./write-failed-error";
|
|
36
38
|
|
|
39
|
+
function evalRowExtractor(
|
|
40
|
+
extractor: RowFieldExtractor,
|
|
41
|
+
row: Record<string, unknown>,
|
|
42
|
+
): Record<string, unknown> {
|
|
43
|
+
if ("pick" in extractor) {
|
|
44
|
+
return Object.fromEntries(extractor.pick.map((f) => [f, row[f]]));
|
|
45
|
+
}
|
|
46
|
+
return Object.fromEntries(Object.entries(extractor.map).map(([to, from]) => [to, row[from]]));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function evalFieldCondition(cond: FieldCondition, values: Record<string, unknown>): boolean {
|
|
50
|
+
if (typeof cond === "boolean") return cond;
|
|
51
|
+
const val = values[cond.field];
|
|
52
|
+
if ("eq" in cond) return val === cond.eq;
|
|
53
|
+
return val !== cond.ne;
|
|
54
|
+
}
|
|
55
|
+
|
|
37
56
|
// KumikoScreen picks up a ScreenDefinition from the schema by qn and
|
|
38
57
|
// routes it to the right renderer based on `screen.type`. Command
|
|
39
58
|
// qualification (`<feature>:write:<entity>:create` etc.) happens here
|
|
@@ -433,6 +452,10 @@ function EntityEditUpdateForm({
|
|
|
433
452
|
entity={entity}
|
|
434
453
|
featureName={schema.featureName}
|
|
435
454
|
initial={initial}
|
|
455
|
+
// Echte route-id an die extension-section (Set-Value-UI): das
|
|
456
|
+
// Update-Form lässt `id` bewusst aus den Form-values, daher braucht
|
|
457
|
+
// die Section die id explizit — sonst create-mode trotz Edit.
|
|
458
|
+
entityId={entityId}
|
|
436
459
|
writeCommand={writeCommand}
|
|
437
460
|
payloadMode="changes"
|
|
438
461
|
buildPayload={buildPayload}
|
|
@@ -659,11 +682,8 @@ function EntityListBody({
|
|
|
659
682
|
// navigate-Variante braucht keinen Dispatcher; nav ist
|
|
660
683
|
// immer da (Provider von createKumikoApp).
|
|
661
684
|
if (action.kind === "navigate") {
|
|
662
|
-
//
|
|
663
|
-
//
|
|
664
|
-
// Function-Form (action.entityId) JSON-injizierte Schemas
|
|
665
|
-
// (window.__KUMIKO_SCHEMA__) nicht überlebt — silent gedroppt,
|
|
666
|
-
// siehe RowAction-Type-Header.
|
|
685
|
+
// Default entityId für entityEdit-Targets: row["id"] wenn
|
|
686
|
+
// kein expliziter entityId-Feldname gesetzt ist.
|
|
667
687
|
const targetIsEntityEdit = schema.screens.some(
|
|
668
688
|
(s) => s.type === "entityEdit" && lastSegment(s.id) === action.screen,
|
|
669
689
|
);
|
|
@@ -672,14 +692,20 @@ function EntityListBody({
|
|
|
672
692
|
label: effectiveTranslate(action.label),
|
|
673
693
|
...(action.style !== undefined && { style: action.style }),
|
|
674
694
|
onTrigger: (row: ListRowViewModel) => {
|
|
675
|
-
const explicit =
|
|
695
|
+
const explicit =
|
|
696
|
+
action.entityId !== undefined
|
|
697
|
+
? String(row.values[action.entityId] ?? "")
|
|
698
|
+
: undefined;
|
|
676
699
|
const fallback = targetIsEntityEdit ? String(row.values["id"] ?? "") : undefined;
|
|
677
700
|
const entityId = explicit ?? fallback;
|
|
678
701
|
nav.navigate({
|
|
679
702
|
screenId: action.screen,
|
|
680
703
|
...(entityId !== undefined && entityId !== "" && { entityId }),
|
|
681
704
|
});
|
|
682
|
-
const params =
|
|
705
|
+
const params =
|
|
706
|
+
action.params !== undefined
|
|
707
|
+
? evalRowExtractor(action.params, row.values)
|
|
708
|
+
: undefined;
|
|
683
709
|
if (params !== undefined) {
|
|
684
710
|
// setSearchParams nimmt string|null. Komplexe Werte
|
|
685
711
|
// (number/boolean) wandeln wir zu String — der Reader
|
|
@@ -696,7 +722,7 @@ function EntityListBody({
|
|
|
696
722
|
}
|
|
697
723
|
},
|
|
698
724
|
...(action.visible !== undefined && {
|
|
699
|
-
isVisible: (row: ListRowViewModel) => action.visible
|
|
725
|
+
isVisible: (row: ListRowViewModel) => evalFieldCondition(action.visible!, row.values),
|
|
700
726
|
}),
|
|
701
727
|
};
|
|
702
728
|
}
|
|
@@ -716,7 +742,9 @@ function EntityListBody({
|
|
|
716
742
|
onTrigger: async (row: ListRowViewModel) => {
|
|
717
743
|
const buildPayload = writeAction.payload;
|
|
718
744
|
const payload =
|
|
719
|
-
buildPayload !== undefined
|
|
745
|
+
buildPayload !== undefined
|
|
746
|
+
? evalRowExtractor(buildPayload, row.values)
|
|
747
|
+
: { id: row.values["id"] };
|
|
720
748
|
const result = await dispatcher.write(writeAction.handler, payload);
|
|
721
749
|
// write() wirft nicht — Failure-Result MUSS hier zum Error
|
|
722
750
|
// werden, sonst schließt der Confirm-Dialog kommentarlos und
|
|
@@ -730,7 +758,7 @@ function EntityListBody({
|
|
|
730
758
|
},
|
|
731
759
|
isVisible:
|
|
732
760
|
writeAction.visible !== undefined
|
|
733
|
-
? (row: ListRowViewModel) => writeAction.visible
|
|
761
|
+
? (row: ListRowViewModel) => evalFieldCondition(writeAction.visible!, row.values)
|
|
734
762
|
: undefined,
|
|
735
763
|
};
|
|
736
764
|
})
|
|
@@ -766,7 +794,7 @@ function EntityListBody({
|
|
|
766
794
|
confirmLabel: effectiveTranslate(action.confirmLabel),
|
|
767
795
|
}),
|
|
768
796
|
onTrigger: async () => {
|
|
769
|
-
const payload = action.payload
|
|
797
|
+
const payload = action.payload ?? {};
|
|
770
798
|
await dispatcher.write(action.handler, payload);
|
|
771
799
|
},
|
|
772
800
|
};
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
EntityDefinition,
|
|
3
3
|
EntityEditScreenDefinition,
|
|
4
|
+
FieldCondition,
|
|
4
5
|
} from "@cosmicdrift/kumiko-framework/ui-types";
|
|
5
6
|
import { isExtensionEditSection, normalizeEditField } from "@cosmicdrift/kumiko-framework/ui-types";
|
|
6
7
|
import type {
|
|
@@ -34,6 +35,13 @@ export type RenderEditProps<TValues extends FormValues, TCtx = unknown> = {
|
|
|
34
35
|
readonly entity: EntityDefinition;
|
|
35
36
|
readonly featureName: string;
|
|
36
37
|
readonly initial: TValues;
|
|
38
|
+
/** Echte entity-id für extension-section-Mounts (Set-Value-UI). Der
|
|
39
|
+
* Update-Body kennt sie aus der Route; ohne sie fiele die Section auf
|
|
40
|
+
* `vm.id` (= values["id"]) zurück, das im Update-Form immer fehlt
|
|
41
|
+
* (id ist keine deklarierte Form-Field, siehe EntityEditUpdateForm) —
|
|
42
|
+
* die Section bliebe dann fälschlich im create-mode. Weglassen
|
|
43
|
+
* (undefined) = create-mode / kein extension-Kontext (vm.id-Fallback). */
|
|
44
|
+
readonly entityId?: string | null;
|
|
37
45
|
/** Standard single-write Submit-Pfad. Ignoriert wenn `customSubmit`
|
|
38
46
|
* gesetzt ist (configEdit-Screens dispatchen mehrere Writes pro
|
|
39
47
|
* Submit, da macht writeCommand keinen Sinn). */
|
|
@@ -64,6 +72,18 @@ export type RenderEditProps<TValues extends FormValues, TCtx = unknown> = {
|
|
|
64
72
|
readonly fieldAppendix?: (fieldName: string) => ReactNode | undefined;
|
|
65
73
|
};
|
|
66
74
|
|
|
75
|
+
function toConditionValue<TValues extends FormValues, TCtx>(
|
|
76
|
+
cond: FieldCondition,
|
|
77
|
+
): NonNullable<FieldConditions<TValues, TCtx>["visible"]> {
|
|
78
|
+
if (typeof cond === "boolean") return cond;
|
|
79
|
+
if ("eq" in cond) {
|
|
80
|
+
const { field, eq } = cond;
|
|
81
|
+
return (values: TValues) => (values as Record<string, unknown>)[field] === eq;
|
|
82
|
+
}
|
|
83
|
+
const { field, ne } = cond;
|
|
84
|
+
return (values: TValues) => (values as Record<string, unknown>)[field] !== ne;
|
|
85
|
+
}
|
|
86
|
+
|
|
67
87
|
function deriveFormFields<TValues extends FormValues, TCtx>(
|
|
68
88
|
screen: EntityEditScreenDefinition,
|
|
69
89
|
): Record<string, FieldConditions<TValues, TCtx>> {
|
|
@@ -74,13 +94,13 @@ function deriveFormFields<TValues extends FormValues, TCtx>(
|
|
|
74
94
|
const normalized = normalizeEditField(spec);
|
|
75
95
|
out[normalized.field] = {
|
|
76
96
|
...(normalized.visible !== undefined && {
|
|
77
|
-
visible:
|
|
97
|
+
visible: toConditionValue<TValues, TCtx>(normalized.visible),
|
|
78
98
|
}),
|
|
79
99
|
...(normalized.readOnly !== undefined && {
|
|
80
|
-
readonly:
|
|
100
|
+
readonly: toConditionValue<TValues, TCtx>(normalized.readOnly),
|
|
81
101
|
}),
|
|
82
102
|
...(normalized.required !== undefined && {
|
|
83
|
-
required:
|
|
103
|
+
required: toConditionValue<TValues, TCtx>(normalized.required),
|
|
84
104
|
}),
|
|
85
105
|
};
|
|
86
106
|
}
|
|
@@ -150,6 +170,7 @@ export function RenderEdit<TValues extends FormValues, TCtx = unknown>(
|
|
|
150
170
|
onReload,
|
|
151
171
|
submitLabel,
|
|
152
172
|
fieldAppendix,
|
|
173
|
+
entityId: entityIdProp,
|
|
153
174
|
} = props;
|
|
154
175
|
const { customSubmit } = props;
|
|
155
176
|
// Translate-Fallback: wenn der Caller keine Translate-Fn übergibt,
|
|
@@ -195,9 +216,8 @@ export function RenderEdit<TValues extends FormValues, TCtx = unknown>(
|
|
|
195
216
|
values: snapshot.values,
|
|
196
217
|
translate,
|
|
197
218
|
featureName,
|
|
198
|
-
ctx,
|
|
199
219
|
}),
|
|
200
|
-
[screen, entity, snapshot.values, translate, featureName
|
|
220
|
+
[screen, entity, snapshot.values, translate, featureName],
|
|
201
221
|
);
|
|
202
222
|
|
|
203
223
|
async function handleSubmit(): Promise<void> {
|
|
@@ -310,7 +330,7 @@ export function RenderEdit<TValues extends FormValues, TCtx = unknown>(
|
|
|
310
330
|
key={section.title}
|
|
311
331
|
section={section}
|
|
312
332
|
entityName={vm.entityName}
|
|
313
|
-
entityId={vm.id}
|
|
333
|
+
entityId={entityIdProp !== undefined ? entityIdProp : vm.id}
|
|
314
334
|
/>
|
|
315
335
|
);
|
|
316
336
|
}
|