@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.21.1",
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 { DispatcherProvider, RenderEdit } from "@cosmicdrift/kumiko-renderer";
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 { ColumnRendererComponent, TranslationsByLocale } from "@cosmicdrift/kumiko-renderer";
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-
@@ -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
- <TreeProvidersProvider value={treeProviders} entities={treeEntities}>
260
- <ResolversProvider resolvers={resolvers}>
261
- <ToastProvider>
262
- {stackWrappers(providers, stackWrappers(gates, screenNode))}
263
- </ToastProvider>
264
- </ResolversProvider>
265
- </TreeProvidersProvider>
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>