@cosmicdrift/kumiko-renderer 0.21.0 → 0.22.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 +3 -3
- package/src/app/extension-sections.tsx +62 -0
- package/src/components/render-edit.tsx +81 -24
- package/src/index.ts +11 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmicdrift/kumiko-renderer",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.22.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>",
|
|
@@ -15,8 +15,8 @@
|
|
|
15
15
|
}
|
|
16
16
|
},
|
|
17
17
|
"dependencies": {
|
|
18
|
-
"@cosmicdrift/kumiko-framework": "0.
|
|
19
|
-
"@cosmicdrift/kumiko-headless": "0.
|
|
18
|
+
"@cosmicdrift/kumiko-framework": "0.21.0",
|
|
19
|
+
"@cosmicdrift/kumiko-headless": "0.21.0",
|
|
20
20
|
"react": "^19.2.6"
|
|
21
21
|
},
|
|
22
22
|
"devDependencies": {
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// Extension-Section-Components-Map: client-side Lookup für entityEdit
|
|
2
|
+
// sections vom Type `extension`. RenderEdit schaut hier nach dem
|
|
3
|
+
// `__component`-Namen aus section.component und mountet die passende
|
|
4
|
+
// Component mit { entityName, entityId } — die Bundled-Feature-Component
|
|
5
|
+
// lädt + persistiert dann ihre eigenen Daten (z.B. custom-fields).
|
|
6
|
+
//
|
|
7
|
+
// Mounting analog zu CustomScreensProvider — createKumikoApp im
|
|
8
|
+
// renderer-web sammelt alle clientFeatures.extensionSectionComponents und
|
|
9
|
+
// mountet den Provider; Tests die einzelne Sections prüfen wollen
|
|
10
|
+
// mounten den Provider direkt.
|
|
11
|
+
|
|
12
|
+
import type { PlatformComponent } from "@cosmicdrift/kumiko-framework/ui-types";
|
|
13
|
+
import { type ComponentType, createContext, type ReactNode, useContext } from "react";
|
|
14
|
+
|
|
15
|
+
/** Extrahiert den `__component`-Namen aus einer PlatformComponent. Liest
|
|
16
|
+
* react- und native-Branch (die App-Author registriert beide unter dem
|
|
17
|
+
* gleichen Namen); returnt den ersten gefundenen string. Pure narrowing
|
|
18
|
+
* via `in` — kein cast, kein assertion. */
|
|
19
|
+
export function extensionSectionName(component: PlatformComponent): string | undefined {
|
|
20
|
+
for (const branch of [component.react, component.native]) {
|
|
21
|
+
if (branch !== null && typeof branch === "object" && "__component" in branch) {
|
|
22
|
+
const candidate = branch.__component;
|
|
23
|
+
if (typeof candidate === "string") return candidate;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return undefined;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export type ExtensionSectionProps = {
|
|
30
|
+
readonly entityName: string;
|
|
31
|
+
readonly entityId: string | null;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export type ExtensionSectionComponent = ComponentType<ExtensionSectionProps>;
|
|
35
|
+
|
|
36
|
+
export type ExtensionSectionsMap = Readonly<Record<string, ExtensionSectionComponent>>;
|
|
37
|
+
|
|
38
|
+
const ExtensionSectionsContext = createContext<ExtensionSectionsMap | undefined>(undefined);
|
|
39
|
+
|
|
40
|
+
export type ExtensionSectionsProviderProps = {
|
|
41
|
+
readonly children: ReactNode;
|
|
42
|
+
readonly value: ExtensionSectionsMap;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export function ExtensionSectionsProvider({
|
|
46
|
+
children,
|
|
47
|
+
value,
|
|
48
|
+
}: ExtensionSectionsProviderProps): ReactNode {
|
|
49
|
+
return (
|
|
50
|
+
<ExtensionSectionsContext.Provider value={value}>{children}</ExtensionSectionsContext.Provider>
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Schaut die Component für einen extension-section-Namen nach. Returnt
|
|
55
|
+
* undefined wenn weder Provider gemounted noch der Name in der Map
|
|
56
|
+
* registriert ist — der Caller (RenderEdit) zeigt dann seinen
|
|
57
|
+
* Placeholder-Banner. */
|
|
58
|
+
export function useExtensionSectionComponent(name: string): ExtensionSectionComponent | undefined {
|
|
59
|
+
const map = useContext(ExtensionSectionsContext);
|
|
60
|
+
if (map === undefined) return undefined;
|
|
61
|
+
return map[name];
|
|
62
|
+
}
|
|
@@ -2,9 +2,10 @@ import type {
|
|
|
2
2
|
EntityDefinition,
|
|
3
3
|
EntityEditScreenDefinition,
|
|
4
4
|
} from "@cosmicdrift/kumiko-framework/ui-types";
|
|
5
|
-
import { normalizeEditField } from "@cosmicdrift/kumiko-framework/ui-types";
|
|
5
|
+
import { isExtensionEditSection, normalizeEditField } from "@cosmicdrift/kumiko-framework/ui-types";
|
|
6
6
|
import type {
|
|
7
7
|
DispatcherError,
|
|
8
|
+
EditExtensionSectionViewModel,
|
|
8
9
|
EditFieldViewModel,
|
|
9
10
|
EditSectionViewModel,
|
|
10
11
|
FieldConditions,
|
|
@@ -17,6 +18,7 @@ import type {
|
|
|
17
18
|
import { computeEditViewModel } from "@cosmicdrift/kumiko-headless";
|
|
18
19
|
import { type ReactNode, useMemo, useState } from "react";
|
|
19
20
|
import type { z } from "zod";
|
|
21
|
+
import { extensionSectionName, useExtensionSectionComponent } from "../app/extension-sections";
|
|
20
22
|
import { useForm } from "../hooks/use-form";
|
|
21
23
|
import { useTranslation } from "../i18n";
|
|
22
24
|
import { usePrimitives } from "../primitives";
|
|
@@ -67,6 +69,7 @@ function deriveFormFields<TValues extends FormValues, TCtx>(
|
|
|
67
69
|
): Record<string, FieldConditions<TValues, TCtx>> {
|
|
68
70
|
const out: Record<string, FieldConditions<TValues, TCtx>> = {};
|
|
69
71
|
for (const section of screen.layout.sections) {
|
|
72
|
+
if (isExtensionEditSection(section)) continue;
|
|
70
73
|
for (const spec of section.fields) {
|
|
71
74
|
const normalized = normalizeEditField(spec);
|
|
72
75
|
out[normalized.field] = {
|
|
@@ -85,6 +88,48 @@ function deriveFormFields<TValues extends FormValues, TCtx>(
|
|
|
85
88
|
return out;
|
|
86
89
|
}
|
|
87
90
|
|
|
91
|
+
// Resolves an extension-section's `{ react: { __component: "X" } }` marker
|
|
92
|
+
// to a registered React component via ExtensionSectionsProvider (filled in
|
|
93
|
+
// createKumikoApp from clientFeatures.extensionSectionComponents) and
|
|
94
|
+
// mounts it with the host entity name + id. Hook lives in its own
|
|
95
|
+
// component so we don't call `use*` inside vm.sections.map (rules-of-
|
|
96
|
+
// hooks would punish reordering sections between renders).
|
|
97
|
+
function ExtensionSectionMount({
|
|
98
|
+
section,
|
|
99
|
+
entityName,
|
|
100
|
+
entityId,
|
|
101
|
+
}: {
|
|
102
|
+
readonly section: EditExtensionSectionViewModel;
|
|
103
|
+
readonly entityName: string;
|
|
104
|
+
readonly entityId: string | null;
|
|
105
|
+
}): ReactNode {
|
|
106
|
+
const { Banner, Section, Text } = usePrimitives();
|
|
107
|
+
const name = extensionSectionName(section.component);
|
|
108
|
+
const Component = useExtensionSectionComponent(name ?? "");
|
|
109
|
+
if (Component === undefined) {
|
|
110
|
+
return (
|
|
111
|
+
<Section
|
|
112
|
+
key={section.title}
|
|
113
|
+
title={section.title}
|
|
114
|
+
testId={`section-extension-${section.title}`}
|
|
115
|
+
>
|
|
116
|
+
<Banner variant="info" testId={`section-extension-placeholder-${section.title}`}>
|
|
117
|
+
<Text>
|
|
118
|
+
Extension section component{" "}
|
|
119
|
+
<Text variant="code">{name ?? "(no __component name)"}</Text> not registered in
|
|
120
|
+
clientFeatures.extensionSectionComponents.
|
|
121
|
+
</Text>
|
|
122
|
+
</Banner>
|
|
123
|
+
</Section>
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
return (
|
|
127
|
+
<Section title={section.title} testId={`section-extension-${section.title}`}>
|
|
128
|
+
<Component entityName={entityName} entityId={entityId} />
|
|
129
|
+
</Section>
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
88
133
|
export function RenderEdit<TValues extends FormValues, TCtx = unknown>(
|
|
89
134
|
props: RenderEditProps<TValues, TCtx>,
|
|
90
135
|
): ReactNode {
|
|
@@ -258,29 +303,41 @@ export function RenderEdit<TValues extends FormValues, TCtx = unknown>(
|
|
|
258
303
|
actions={formActions}
|
|
259
304
|
testId="render-edit-form"
|
|
260
305
|
>
|
|
261
|
-
{vm.sections.map((section: EditSectionViewModel) =>
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
306
|
+
{vm.sections.map((section: EditSectionViewModel) => {
|
|
307
|
+
if (section.kind === "extension") {
|
|
308
|
+
return (
|
|
309
|
+
<ExtensionSectionMount
|
|
310
|
+
key={section.title}
|
|
311
|
+
section={section}
|
|
312
|
+
entityName={vm.entityName}
|
|
313
|
+
entityId={vm.id}
|
|
314
|
+
/>
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
return (
|
|
318
|
+
<Section key={section.title} title={section.title} testId={`section-${section.title}`}>
|
|
319
|
+
<Grid columns={section.columns}>
|
|
320
|
+
{section.fields.map((field: EditFieldViewModel) => (
|
|
321
|
+
<GridCellForField
|
|
322
|
+
key={field.field}
|
|
323
|
+
field={field}
|
|
324
|
+
columns={section.columns}
|
|
325
|
+
issues={snapshot.errors[field.field]}
|
|
326
|
+
onChange={(v) => {
|
|
327
|
+
(controller.setField as (k: string, v: unknown) => void)(field.field, v);
|
|
328
|
+
}}
|
|
329
|
+
GridCell={GridCell}
|
|
330
|
+
featureName={featureName}
|
|
331
|
+
{...(fieldAppendix !== undefined && {
|
|
332
|
+
labelAppendix: fieldAppendix(field.field),
|
|
333
|
+
fieldAppendix: fieldAppendix(field.field),
|
|
334
|
+
})}
|
|
335
|
+
/>
|
|
336
|
+
))}
|
|
337
|
+
</Grid>
|
|
338
|
+
</Section>
|
|
339
|
+
);
|
|
340
|
+
})}
|
|
284
341
|
{formError !== null && (
|
|
285
342
|
<Banner
|
|
286
343
|
variant="error"
|
package/src/index.ts
CHANGED
|
@@ -18,6 +18,17 @@ export type {
|
|
|
18
18
|
export { ColumnRenderersProvider, useColumnRenderer } from "./app/column-renderers";
|
|
19
19
|
export type { CustomScreensMap, CustomScreensProviderProps } from "./app/custom-screens";
|
|
20
20
|
export { CustomScreensProvider, useCustomScreenComponent } from "./app/custom-screens";
|
|
21
|
+
export type {
|
|
22
|
+
ExtensionSectionComponent,
|
|
23
|
+
ExtensionSectionProps,
|
|
24
|
+
ExtensionSectionsMap,
|
|
25
|
+
ExtensionSectionsProviderProps,
|
|
26
|
+
} from "./app/extension-sections";
|
|
27
|
+
export {
|
|
28
|
+
ExtensionSectionsProvider,
|
|
29
|
+
extensionSectionName,
|
|
30
|
+
useExtensionSectionComponent,
|
|
31
|
+
} from "./app/extension-sections";
|
|
21
32
|
export type { AppSchema, FeatureSchema, WorkspaceSchema } from "./app/feature-schema";
|
|
22
33
|
export { isAppSchema, toAppSchema } from "./app/feature-schema";
|
|
23
34
|
export type { KumikoScreenProps } from "./app/kumiko-screen";
|