@cosmicdrift/kumiko-renderer 0.34.1 → 0.35.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.
|
|
3
|
+
"version": "0.35.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>",
|
|
@@ -29,6 +29,11 @@ export function extensionSectionName(component: PlatformComponent): string | und
|
|
|
29
29
|
export type ExtensionSectionProps = {
|
|
30
30
|
readonly entityName: string;
|
|
31
31
|
readonly entityId: string | null;
|
|
32
|
+
/** Bereits gespeicherte Extension-Werte der Entity (aus der geladenen
|
|
33
|
+
* detail-row durchgereicht). `undefined` im Create-Mode oder wenn der
|
|
34
|
+
* Host-Screen keine Werte liefert. Erlaubt der Section, den Bestand
|
|
35
|
+
* beim Edit anzuzeigen statt write-only zu sein. */
|
|
36
|
+
readonly initialValues?: Readonly<Record<string, unknown>>;
|
|
32
37
|
};
|
|
33
38
|
|
|
34
39
|
export type ExtensionSectionComponent = ComponentType<ExtensionSectionProps>;
|
|
@@ -421,6 +421,15 @@ function EntityEditUpdateForm({
|
|
|
421
421
|
return out as FormValues;
|
|
422
422
|
}, [entity.fields, record]);
|
|
423
423
|
|
|
424
|
+
// Extension-Werte (z.B. customFields-jsonb) an extension-sections geben,
|
|
425
|
+
// damit sie beim Edit den Bestand zeigen statt write-only zu sein.
|
|
426
|
+
const extensionInitialValues = useMemo((): Readonly<Record<string, unknown>> | undefined => {
|
|
427
|
+
const cf = record["customFields"];
|
|
428
|
+
return cf !== null && typeof cf === "object" && !Array.isArray(cf)
|
|
429
|
+
? (cf as Record<string, unknown>)
|
|
430
|
+
: undefined;
|
|
431
|
+
}, [record]);
|
|
432
|
+
|
|
424
433
|
const writeCommand = entityWriteCommand(schema.featureName, screen.entity, "update");
|
|
425
434
|
const deleteCommand = entityWriteCommand(schema.featureName, screen.entity, "delete");
|
|
426
435
|
const buildPayload = useMemo(
|
|
@@ -456,6 +465,9 @@ function EntityEditUpdateForm({
|
|
|
456
465
|
// Update-Form lässt `id` bewusst aus den Form-values, daher braucht
|
|
457
466
|
// die Section die id explizit — sonst create-mode trotz Edit.
|
|
458
467
|
entityId={entityId}
|
|
468
|
+
// customFields-Bestand an die extension-section, damit sie beim Edit
|
|
469
|
+
// die gespeicherten Werte zeigt (nicht write-only).
|
|
470
|
+
extensionInitialValues={extensionInitialValues}
|
|
459
471
|
writeCommand={writeCommand}
|
|
460
472
|
payloadMode="changes"
|
|
461
473
|
buildPayload={buildPayload}
|
|
@@ -687,6 +699,7 @@ function EntityListBody({
|
|
|
687
699
|
const targetIsEntityEdit = schema.screens.some(
|
|
688
700
|
(s) => s.type === "entityEdit" && lastSegment(s.id) === action.screen,
|
|
689
701
|
);
|
|
702
|
+
const actionVisible = action.visible;
|
|
690
703
|
return {
|
|
691
704
|
id: action.id,
|
|
692
705
|
label: effectiveTranslate(action.label),
|
|
@@ -721,14 +734,15 @@ function EntityListBody({
|
|
|
721
734
|
nav.setSearchParams(stringified);
|
|
722
735
|
}
|
|
723
736
|
},
|
|
724
|
-
...(
|
|
725
|
-
isVisible: (row: ListRowViewModel) => evalFieldCondition(
|
|
737
|
+
...(actionVisible !== undefined && {
|
|
738
|
+
isVisible: (row: ListRowViewModel) => evalFieldCondition(actionVisible, row.values),
|
|
726
739
|
}),
|
|
727
740
|
};
|
|
728
741
|
}
|
|
729
742
|
if (dispatcher === undefined) return null;
|
|
730
743
|
if (action.kind !== "writeHandler" && action.kind !== undefined) return null;
|
|
731
744
|
const writeAction = action as RowActionWriteHandler;
|
|
745
|
+
const writeActionVisible = writeAction.visible;
|
|
732
746
|
return {
|
|
733
747
|
id: writeAction.id,
|
|
734
748
|
label: effectiveTranslate(writeAction.label),
|
|
@@ -757,8 +771,8 @@ function EntityListBody({
|
|
|
757
771
|
}
|
|
758
772
|
},
|
|
759
773
|
isVisible:
|
|
760
|
-
|
|
761
|
-
? (row: ListRowViewModel) => evalFieldCondition(
|
|
774
|
+
writeActionVisible !== undefined
|
|
775
|
+
? (row: ListRowViewModel) => evalFieldCondition(writeActionVisible, row.values)
|
|
762
776
|
: undefined,
|
|
763
777
|
};
|
|
764
778
|
})
|
|
@@ -42,6 +42,10 @@ export type RenderEditProps<TValues extends FormValues, TCtx = unknown> = {
|
|
|
42
42
|
* die Section bliebe dann fälschlich im create-mode. Weglassen
|
|
43
43
|
* (undefined) = create-mode / kein extension-Kontext (vm.id-Fallback). */
|
|
44
44
|
readonly entityId?: string | null;
|
|
45
|
+
/** Bereits gespeicherte Extension-Werte (z.B. `record.customFields`) für
|
|
46
|
+
* extension-section-Mounts. Erlaubt der Section, den Bestand beim Edit
|
|
47
|
+
* anzuzeigen. Nur der Update-Body liefert das. */
|
|
48
|
+
readonly extensionInitialValues?: Readonly<Record<string, unknown>>;
|
|
45
49
|
/** Standard single-write Submit-Pfad. Ignoriert wenn `customSubmit`
|
|
46
50
|
* gesetzt ist (configEdit-Screens dispatchen mehrere Writes pro
|
|
47
51
|
* Submit, da macht writeCommand keinen Sinn). */
|
|
@@ -118,10 +122,12 @@ function ExtensionSectionMount({
|
|
|
118
122
|
section,
|
|
119
123
|
entityName,
|
|
120
124
|
entityId,
|
|
125
|
+
initialValues,
|
|
121
126
|
}: {
|
|
122
127
|
readonly section: EditExtensionSectionViewModel;
|
|
123
128
|
readonly entityName: string;
|
|
124
129
|
readonly entityId: string | null;
|
|
130
|
+
readonly initialValues?: Readonly<Record<string, unknown>>;
|
|
125
131
|
}): ReactNode {
|
|
126
132
|
const { Banner, Section, Text } = usePrimitives();
|
|
127
133
|
const name = extensionSectionName(section.component);
|
|
@@ -145,7 +151,7 @@ function ExtensionSectionMount({
|
|
|
145
151
|
}
|
|
146
152
|
return (
|
|
147
153
|
<Section title={section.title} testId={`section-extension-${section.title}`}>
|
|
148
|
-
<Component entityName={entityName} entityId={entityId} />
|
|
154
|
+
<Component entityName={entityName} entityId={entityId} initialValues={initialValues} />
|
|
149
155
|
</Section>
|
|
150
156
|
);
|
|
151
157
|
}
|
|
@@ -171,6 +177,7 @@ export function RenderEdit<TValues extends FormValues, TCtx = unknown>(
|
|
|
171
177
|
submitLabel,
|
|
172
178
|
fieldAppendix,
|
|
173
179
|
entityId: entityIdProp,
|
|
180
|
+
extensionInitialValues,
|
|
174
181
|
} = props;
|
|
175
182
|
const { customSubmit } = props;
|
|
176
183
|
// Translate-Fallback: wenn der Caller keine Translate-Fn übergibt,
|
|
@@ -331,6 +338,7 @@ export function RenderEdit<TValues extends FormValues, TCtx = unknown>(
|
|
|
331
338
|
section={section}
|
|
332
339
|
entityName={vm.entityName}
|
|
333
340
|
entityId={entityIdProp !== undefined ? entityIdProp : vm.id}
|
|
341
|
+
initialValues={extensionInitialValues}
|
|
334
342
|
/>
|
|
335
343
|
);
|
|
336
344
|
}
|
|
@@ -6,6 +6,8 @@ import type {
|
|
|
6
6
|
import type {
|
|
7
7
|
ListColumnViewModel,
|
|
8
8
|
ListRowViewModel,
|
|
9
|
+
ListViewModel,
|
|
10
|
+
RuntimeRenderer,
|
|
9
11
|
Translate,
|
|
10
12
|
} from "@cosmicdrift/kumiko-headless";
|
|
11
13
|
import { computeListViewModel } from "@cosmicdrift/kumiko-headless";
|
|
@@ -185,7 +187,7 @@ export function RenderList(props: RenderListProps): ReactNode {
|
|
|
185
187
|
return { ...prev, [field]: map };
|
|
186
188
|
});
|
|
187
189
|
}, []);
|
|
188
|
-
const enrichedColumns = useMemo(() => {
|
|
190
|
+
const enrichedColumns = useMemo((): readonly ListColumnViewModel[] => {
|
|
189
191
|
if (referenceColumns.length === 0) return vm.columns;
|
|
190
192
|
return vm.columns.map((col: ListColumnViewModel) => {
|
|
191
193
|
if (col.type !== "reference") return col;
|
|
@@ -194,7 +196,10 @@ export function RenderList(props: RenderListProps): ReactNode {
|
|
|
194
196
|
if (col.renderer !== undefined) return col;
|
|
195
197
|
const map = referenceLookups[col.field];
|
|
196
198
|
const labelField = col.refLabelField ?? "id";
|
|
197
|
-
const renderer = (
|
|
199
|
+
const renderer: RuntimeRenderer = (
|
|
200
|
+
value: unknown,
|
|
201
|
+
row?: Readonly<Record<string, unknown>>,
|
|
202
|
+
): string => {
|
|
198
203
|
// Tier 2.7e Server-Eagerload: wenn der Server _refs mit-
|
|
199
204
|
// schickt, lesen wir den Display-Wert direkt aus der
|
|
200
205
|
// resolved Row — kein Roundtrip durch die Bridge-Map nötig
|
|
@@ -221,7 +226,13 @@ export function RenderList(props: RenderListProps): ReactNode {
|
|
|
221
226
|
return { ...col, renderer };
|
|
222
227
|
});
|
|
223
228
|
}, [vm.columns, referenceColumns, referenceLookups]);
|
|
224
|
-
const enrichedVm = useMemo(
|
|
229
|
+
const enrichedVm = useMemo(
|
|
230
|
+
(): Omit<ListViewModel, "columns"> & { columns: readonly ListColumnViewModel[] } => ({
|
|
231
|
+
...vm,
|
|
232
|
+
columns: enrichedColumns,
|
|
233
|
+
}),
|
|
234
|
+
[vm, enrichedColumns],
|
|
235
|
+
);
|
|
225
236
|
|
|
226
237
|
// i18n-Defaults für Toolbar/Empty-State Strings — Caller kann jeden
|
|
227
238
|
// einzeln per Prop überschreiben, sonst kommen die Framework-Bundles
|
package/src/index.ts
CHANGED
package/src/primitives.tsx
CHANGED
|
@@ -38,6 +38,7 @@ import type {
|
|
|
38
38
|
FieldIssue,
|
|
39
39
|
ListColumnViewModel,
|
|
40
40
|
ListRowViewModel,
|
|
41
|
+
RuntimeRenderer,
|
|
41
42
|
} from "@cosmicdrift/kumiko-headless";
|
|
42
43
|
import {
|
|
43
44
|
type ComponentType,
|
|
@@ -47,6 +48,8 @@ import {
|
|
|
47
48
|
useContext,
|
|
48
49
|
} from "react";
|
|
49
50
|
|
|
51
|
+
export type { RuntimeRenderer };
|
|
52
|
+
|
|
50
53
|
// ---- Prop-Types (die Primitive-Contract-Oberfläche) ----
|
|
51
54
|
|
|
52
55
|
/** Standard-Button. `loading` zeigt einen Spinner statt der Children
|