@cosmicdrift/kumiko-renderer-web 0.21.1 → 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
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmicdrift/kumiko-renderer-web",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.22.0",
|
|
4
4
|
"description": "Web-platform bindings for @cosmicdrift/kumiko-renderer. HTML default-primitives, browser history-based navigation, EventSource-backed live events, and a one-call createKumikoApp that mounts the whole stack via react-dom.",
|
|
5
5
|
"license": "BUSL-1.1",
|
|
6
6
|
"author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
|
|
@@ -4,7 +4,11 @@ import type {
|
|
|
4
4
|
EntityEditScreenDefinition,
|
|
5
5
|
} from "@cosmicdrift/kumiko-framework/ui-types";
|
|
6
6
|
import type { Dispatcher, SubmitResult } from "@cosmicdrift/kumiko-headless";
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
DispatcherProvider,
|
|
9
|
+
ExtensionSectionsProvider,
|
|
10
|
+
RenderEdit,
|
|
11
|
+
} from "@cosmicdrift/kumiko-renderer";
|
|
8
12
|
import { act, createMockDispatcher, fireEvent, render, screen } from "./test-utils";
|
|
9
13
|
|
|
10
14
|
const orderEntity = {
|
|
@@ -174,4 +178,92 @@ describe("RenderEdit", () => {
|
|
|
174
178
|
const actionsBar = screen.getByTestId("render-edit-form-actions");
|
|
175
179
|
expect(actionsBar.textContent).toContain("orders:screen:order-edit");
|
|
176
180
|
});
|
|
181
|
+
|
|
182
|
+
test("extension section renders the registered Component with entityName + entityId", () => {
|
|
183
|
+
const screenDef: EntityEditScreenDefinition = {
|
|
184
|
+
id: "orders:screen:order-edit",
|
|
185
|
+
type: "entityEdit",
|
|
186
|
+
entity: "order",
|
|
187
|
+
layout: {
|
|
188
|
+
sections: [
|
|
189
|
+
{
|
|
190
|
+
title: "Basics",
|
|
191
|
+
columns: 2,
|
|
192
|
+
fields: [{ field: "title", span: 2 }],
|
|
193
|
+
},
|
|
194
|
+
{
|
|
195
|
+
kind: "extension",
|
|
196
|
+
title: "Custom Fields",
|
|
197
|
+
component: { react: { __component: "MyCustomFieldsForm" } },
|
|
198
|
+
},
|
|
199
|
+
],
|
|
200
|
+
},
|
|
201
|
+
};
|
|
202
|
+
const MyCustomFieldsForm = ({
|
|
203
|
+
entityName,
|
|
204
|
+
entityId,
|
|
205
|
+
}: {
|
|
206
|
+
entityName: string;
|
|
207
|
+
entityId: string | null;
|
|
208
|
+
}) => (
|
|
209
|
+
<div data-testid="my-custom-fields-form">
|
|
210
|
+
{entityName}:{entityId ?? "(create)"}
|
|
211
|
+
</div>
|
|
212
|
+
);
|
|
213
|
+
render(
|
|
214
|
+
<DispatcherProvider dispatcher={makeDispatcher()}>
|
|
215
|
+
<ExtensionSectionsProvider value={{ MyCustomFieldsForm }}>
|
|
216
|
+
<RenderEdit<TestValues>
|
|
217
|
+
screen={screenDef}
|
|
218
|
+
entity={orderEntity}
|
|
219
|
+
featureName="orders"
|
|
220
|
+
initial={{ title: "", count: 0, isUrgent: false, id: "row-42" } as TestValues}
|
|
221
|
+
writeCommand="order:update"
|
|
222
|
+
/>
|
|
223
|
+
</ExtensionSectionsProvider>
|
|
224
|
+
</DispatcherProvider>,
|
|
225
|
+
);
|
|
226
|
+
|
|
227
|
+
expect(screen.getByTestId("section-extension-Custom Fields")).toBeTruthy();
|
|
228
|
+
const mounted = screen.getByTestId("my-custom-fields-form");
|
|
229
|
+
expect(mounted.textContent).toBe("order:row-42");
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
test("extension section without registered component shows the placeholder banner", () => {
|
|
233
|
+
const screenDef: EntityEditScreenDefinition = {
|
|
234
|
+
id: "orders:screen:order-edit",
|
|
235
|
+
type: "entityEdit",
|
|
236
|
+
entity: "order",
|
|
237
|
+
layout: {
|
|
238
|
+
sections: [
|
|
239
|
+
{
|
|
240
|
+
title: "Basics",
|
|
241
|
+
columns: 1,
|
|
242
|
+
fields: [{ field: "title", span: 1 }],
|
|
243
|
+
},
|
|
244
|
+
{
|
|
245
|
+
kind: "extension",
|
|
246
|
+
title: "Custom Fields",
|
|
247
|
+
component: { react: { __component: "UnregisteredComp" } },
|
|
248
|
+
},
|
|
249
|
+
],
|
|
250
|
+
},
|
|
251
|
+
};
|
|
252
|
+
render(
|
|
253
|
+
<DispatcherProvider dispatcher={makeDispatcher()}>
|
|
254
|
+
<ExtensionSectionsProvider value={{}}>
|
|
255
|
+
<RenderEdit<TestValues>
|
|
256
|
+
screen={screenDef}
|
|
257
|
+
entity={orderEntity}
|
|
258
|
+
featureName="orders"
|
|
259
|
+
initial={{ title: "", count: 0, isUrgent: false }}
|
|
260
|
+
writeCommand="order:create"
|
|
261
|
+
/>
|
|
262
|
+
</ExtensionSectionsProvider>
|
|
263
|
+
</DispatcherProvider>,
|
|
264
|
+
);
|
|
265
|
+
|
|
266
|
+
const placeholder = screen.getByTestId("section-extension-placeholder-Custom Fields");
|
|
267
|
+
expect(placeholder.textContent).toContain("UnregisteredComp");
|
|
268
|
+
});
|
|
177
269
|
});
|
|
@@ -14,7 +14,11 @@ import type {
|
|
|
14
14
|
TreeActionDef,
|
|
15
15
|
TreeChildrenSubscribe,
|
|
16
16
|
} from "@cosmicdrift/kumiko-framework/engine";
|
|
17
|
-
import type {
|
|
17
|
+
import type {
|
|
18
|
+
ColumnRendererComponent,
|
|
19
|
+
ExtensionSectionComponent,
|
|
20
|
+
TranslationsByLocale,
|
|
21
|
+
} from "@cosmicdrift/kumiko-renderer";
|
|
18
22
|
import type { ComponentType, ReactNode } from "react";
|
|
19
23
|
|
|
20
24
|
export type ClientFeatureDefinition = {
|
|
@@ -50,6 +54,13 @@ export type ClientFeatureDefinition = {
|
|
|
50
54
|
* echte JSX-Renderer leben im Client-Bundle. Last-Wins bei Key-
|
|
51
55
|
* Kollision über mehrere Features. */
|
|
52
56
|
readonly columnRenderers?: Readonly<Record<string, ColumnRendererComponent>>;
|
|
57
|
+
/** Extension-Section-Components — Map `__component`-name → React-
|
|
58
|
+
* Component. Schema deklariert eine entityEdit-Section mit
|
|
59
|
+
* `kind: "extension"` + `component: { react: { __component: "X" } }`;
|
|
60
|
+
* RenderEdit zieht den Component hier raus und mountet ihn mit
|
|
61
|
+
* `{ entityName, entityId }`. Pattern wie columnRenderers — Last-Wins
|
|
62
|
+
* bei Key-Kollision über mehrere Features. */
|
|
63
|
+
readonly extensionSectionComponents?: Readonly<Record<string, ExtensionSectionComponent>>;
|
|
53
64
|
/** Tree-Provider für `r.workspace({ navigation: "tree" })`-Workspaces
|
|
54
65
|
* (Visual-Tree). Wird beim Mount des Tree-Workspaces mit ctx aufgerufen,
|
|
55
66
|
* emittiert TreeNode[] die in der Sidebar gerendert werden. Closure-
|
package/src/app/create-app.tsx
CHANGED
|
@@ -12,6 +12,8 @@ import {
|
|
|
12
12
|
ColumnRenderersProvider,
|
|
13
13
|
CustomScreensProvider,
|
|
14
14
|
DispatcherProvider,
|
|
15
|
+
type ExtensionSectionComponent,
|
|
16
|
+
ExtensionSectionsProvider,
|
|
15
17
|
type FeatureSchema,
|
|
16
18
|
KumikoScreen,
|
|
17
19
|
kumikoDefaultTranslations,
|
|
@@ -194,6 +196,22 @@ export function createKumikoApp(options: CreateKumikoAppOptions = {}): { readonl
|
|
|
194
196
|
columnRenderers[key] = value;
|
|
195
197
|
}
|
|
196
198
|
}
|
|
199
|
+
// Extension-Section-Components — same Last-Wins + Warn-Semantik wie
|
|
200
|
+
// columnRenderers. Mountet sich am ExtensionSectionsProvider; RenderEdit
|
|
201
|
+
// löst die Component aus dem `__component`-Marker der section.
|
|
202
|
+
const extensionSectionComponents: Record<string, ExtensionSectionComponent> = {};
|
|
203
|
+
for (const f of clientFeatures) {
|
|
204
|
+
if (f.extensionSectionComponents === undefined) continue;
|
|
205
|
+
for (const [key, value] of Object.entries(f.extensionSectionComponents)) {
|
|
206
|
+
if (extensionSectionComponents[key] !== undefined) {
|
|
207
|
+
// biome-ignore lint/suspicious/noConsole: dev-warning für Schema-Konflikte
|
|
208
|
+
console.warn(
|
|
209
|
+
`[kumiko] extensionSectionComponent "${key}" defined by multiple clientFeatures — last definition (from "${f.name}") wins.`,
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
extensionSectionComponents[key] = value;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
197
215
|
|
|
198
216
|
// Tree-Provider-Map aggregieren — keyed by clientFeature.name (matches
|
|
199
217
|
// server-side FeatureDefinition.name). Mehrere clientFeatures mit
|
|
@@ -256,13 +274,15 @@ export function createKumikoApp(options: CreateKumikoAppOptions = {}): { readonl
|
|
|
256
274
|
<LiveEventsProvider value={liveEvents}>
|
|
257
275
|
<CustomScreensProvider value={customScreens}>
|
|
258
276
|
<ColumnRenderersProvider value={columnRenderers}>
|
|
259
|
-
<
|
|
260
|
-
<
|
|
261
|
-
<
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
277
|
+
<ExtensionSectionsProvider value={extensionSectionComponents}>
|
|
278
|
+
<TreeProvidersProvider value={treeProviders} entities={treeEntities}>
|
|
279
|
+
<ResolversProvider resolvers={resolvers}>
|
|
280
|
+
<ToastProvider>
|
|
281
|
+
{stackWrappers(providers, stackWrappers(gates, screenNode))}
|
|
282
|
+
</ToastProvider>
|
|
283
|
+
</ResolversProvider>
|
|
284
|
+
</TreeProvidersProvider>
|
|
285
|
+
</ExtensionSectionsProvider>
|
|
266
286
|
</ColumnRenderersProvider>
|
|
267
287
|
</CustomScreensProvider>
|
|
268
288
|
</LiveEventsProvider>
|