@cosmicdrift/kumiko-renderer-web 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
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmicdrift/kumiko-renderer-web",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.34.1",
|
|
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>",
|
|
@@ -7,7 +7,11 @@ import type {
|
|
|
7
7
|
} from "@cosmicdrift/kumiko-framework/ui-types";
|
|
8
8
|
import type { Dispatcher } from "@cosmicdrift/kumiko-headless";
|
|
9
9
|
import type { FeatureSchema } from "@cosmicdrift/kumiko-renderer";
|
|
10
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
DispatcherProvider,
|
|
12
|
+
ExtensionSectionsProvider,
|
|
13
|
+
KumikoScreen,
|
|
14
|
+
} from "@cosmicdrift/kumiko-renderer";
|
|
11
15
|
import userEvent from "@testing-library/user-event";
|
|
12
16
|
import { createMockDispatcher, fireEvent, render, screen, waitFor } from "./test-utils";
|
|
13
17
|
|
|
@@ -160,6 +164,70 @@ describe("KumikoScreen", () => {
|
|
|
160
164
|
});
|
|
161
165
|
});
|
|
162
166
|
|
|
167
|
+
// Regression-Anker für den Set-Value-UI-Bug: die extension-section muss im
|
|
168
|
+
// Update-Mode die ECHTE entity-id bekommen — durch den vollen Flow
|
|
169
|
+
// (KumikoScreen → detail-load → EntityEditUpdateForm → RenderEdit → Mount).
|
|
170
|
+
// EntityEditUpdateForm lässt `id` bewusst aus den Form-values (id ist keine
|
|
171
|
+
// deklarierte Field), also reicht NUR der route-entityId-Durchgriff. Ohne
|
|
172
|
+
// ihn fiele die Section auf vm.id (=values["id"]=undefined) zurück und zeigte
|
|
173
|
+
// create-mode trotz Edit. Der alte render-edit-Test mockte `initial.id`
|
|
174
|
+
// manuell und war für genau diesen Flow blind — dieser Test rendert den
|
|
175
|
+
// realen detail-Pfad, der das in CI gefangen hätte.
|
|
176
|
+
test("entityEdit mit entityId → extension-section bekommt die echte entity-id (nicht create-mode)", async () => {
|
|
177
|
+
const editScreenWithExtension: EntityEditScreenDefinition = {
|
|
178
|
+
id: "task-edit-ext",
|
|
179
|
+
type: "entityEdit",
|
|
180
|
+
entity: "task",
|
|
181
|
+
layout: {
|
|
182
|
+
sections: [
|
|
183
|
+
{ title: "Basics", fields: ["title"] },
|
|
184
|
+
{
|
|
185
|
+
kind: "extension",
|
|
186
|
+
title: "Custom Fields",
|
|
187
|
+
component: { react: { __component: "TaskCustomFields" } },
|
|
188
|
+
},
|
|
189
|
+
],
|
|
190
|
+
},
|
|
191
|
+
};
|
|
192
|
+
const extSchema: FeatureSchema = {
|
|
193
|
+
featureName: "tasks",
|
|
194
|
+
entities: { task: taskEntity },
|
|
195
|
+
screens: [editScreenWithExtension],
|
|
196
|
+
};
|
|
197
|
+
const TaskCustomFields = ({
|
|
198
|
+
entityName,
|
|
199
|
+
entityId,
|
|
200
|
+
}: {
|
|
201
|
+
entityName: string;
|
|
202
|
+
entityId: string | null;
|
|
203
|
+
}) => (
|
|
204
|
+
<div data-testid="task-custom-fields">
|
|
205
|
+
{entityName}:{entityId ?? "(create)"}
|
|
206
|
+
</div>
|
|
207
|
+
);
|
|
208
|
+
const dispatcher = makeDispatcher({
|
|
209
|
+
// detail liefert die row MIT id — aber der Update-Form filtert id aus
|
|
210
|
+
// den Form-values; die Section darf trotzdem nicht in create-mode fallen.
|
|
211
|
+
query: (async () => ({
|
|
212
|
+
isSuccess: true,
|
|
213
|
+
data: { id: "task-1", version: 7, title: "loaded", count: 0, done: false },
|
|
214
|
+
})) as unknown as Dispatcher["query"],
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
render(
|
|
218
|
+
<DispatcherProvider dispatcher={dispatcher}>
|
|
219
|
+
<ExtensionSectionsProvider value={{ TaskCustomFields }}>
|
|
220
|
+
<KumikoScreen schema={extSchema} qn="tasks:screen:task-edit-ext" entityId="task-1" />
|
|
221
|
+
</ExtensionSectionsProvider>
|
|
222
|
+
</DispatcherProvider>,
|
|
223
|
+
);
|
|
224
|
+
|
|
225
|
+
await waitFor(() => expect(screen.queryByTestId("kumiko-screen-loading")).toBeNull());
|
|
226
|
+
const section = screen.getByTestId("task-custom-fields");
|
|
227
|
+
// Der Anker: echte route-id, NICHT "(create)". Vor dem Fix: "task:(create)".
|
|
228
|
+
expect(section.textContent).toBe("task:task-1");
|
|
229
|
+
});
|
|
230
|
+
|
|
163
231
|
test("entityList onRowClick → Callback feuert mit Row-Viewmodel", async () => {
|
|
164
232
|
const clicks: { id: string }[] = [];
|
|
165
233
|
const dispatcher = makeDispatcher({
|
|
@@ -316,7 +384,7 @@ describe("KumikoScreen", () => {
|
|
|
316
384
|
id: "archive",
|
|
317
385
|
label: "actions.archive",
|
|
318
386
|
handler: "tasks:write:task:archive",
|
|
319
|
-
payload:
|
|
387
|
+
payload: { pick: ["id"] },
|
|
320
388
|
},
|
|
321
389
|
],
|
|
322
390
|
};
|
|
@@ -344,7 +412,7 @@ describe("KumikoScreen", () => {
|
|
|
344
412
|
await waitFor(() => expect(writeCalls.length).toBe(1));
|
|
345
413
|
expect(writeCalls[0]).toEqual({
|
|
346
414
|
type: "tasks:write:task:archive",
|
|
347
|
-
payload: { id: "r1"
|
|
415
|
+
payload: { id: "r1" },
|
|
348
416
|
});
|
|
349
417
|
});
|
|
350
418
|
|
|
@@ -459,7 +527,7 @@ describe("KumikoScreen", () => {
|
|
|
459
527
|
id: "edit",
|
|
460
528
|
label: "actions.edit",
|
|
461
529
|
screen: "task-edit",
|
|
462
|
-
params:
|
|
530
|
+
params: { map: { taskId: "id" } },
|
|
463
531
|
},
|
|
464
532
|
],
|
|
465
533
|
};
|
|
@@ -482,7 +550,7 @@ describe("KumikoScreen", () => {
|
|
|
482
550
|
await waitFor(() => expect(navigateCalls.length).toBe(1));
|
|
483
551
|
expect(navigateCalls[0]).toEqual({ screenId: "task-edit" });
|
|
484
552
|
// params werden zu Strings serialisiert (URL-Layer kennt nur Strings).
|
|
485
|
-
expect(searchParamUpdates).toEqual([{ taskId: "r1"
|
|
553
|
+
expect(searchParamUpdates).toEqual([{ taskId: "r1" }]);
|
|
486
554
|
// Reihenfolge-Pin: erst navigate, dann setSearchParams.
|
|
487
555
|
expect(calls.map((c) => c.kind)).toEqual(["navigate", "setSearchParams"]);
|
|
488
556
|
});
|
|
@@ -522,7 +590,7 @@ describe("KumikoScreen", () => {
|
|
|
522
590
|
id: "edit",
|
|
523
591
|
label: "actions.edit",
|
|
524
592
|
screen: "task-edit",
|
|
525
|
-
entityId:
|
|
593
|
+
entityId: "id",
|
|
526
594
|
},
|
|
527
595
|
],
|
|
528
596
|
};
|
|
@@ -547,12 +615,10 @@ describe("KumikoScreen", () => {
|
|
|
547
615
|
expect(searchParamUpdates).toEqual([]);
|
|
548
616
|
});
|
|
549
617
|
|
|
550
|
-
// JSON-Schema-Fall (window.__KUMIKO_SCHEMA__):
|
|
551
|
-
//
|
|
552
|
-
//
|
|
553
|
-
|
|
554
|
-
// (Prod-e2e-Befund 2026-06-07 nach F1).
|
|
555
|
-
test("entityList rowActions kind=navigate auf entityEdit-Ziel: row.id ist der entityId-Default (JSON-Schema-sicher)", async () => {
|
|
618
|
+
// JSON-Schema-Fall (window.__KUMIKO_SCHEMA__): Declarative entityId: "id"
|
|
619
|
+
// überlebt JSON.stringify (String, kein Function-Drop). Das Schema
|
|
620
|
+
// funktioniert identisch ob direkt oder nach JSON-Roundtrip geladen.
|
|
621
|
+
test("entityList rowActions kind=navigate auf entityEdit-Ziel: entityId-String überlebt JSON-Roundtrip (JSON-Schema-sicher)", async () => {
|
|
556
622
|
const navigateCalls: { screenId: string; entityId?: string }[] = [];
|
|
557
623
|
const memoryNav = {
|
|
558
624
|
route: { screenId: "task-list" },
|
|
@@ -583,10 +649,7 @@ describe("KumikoScreen", () => {
|
|
|
583
649
|
id: "edit",
|
|
584
650
|
label: "actions.edit",
|
|
585
651
|
screen: "task-edit",
|
|
586
|
-
|
|
587
|
-
// JSON-Roundtrip gedroppt — exakt was buildAppSchema +
|
|
588
|
-
// JSON.stringify mit dem Schema im Browser machen.
|
|
589
|
-
entityId: (row) => String(row["id"] ?? ""),
|
|
652
|
+
entityId: "id",
|
|
590
653
|
},
|
|
591
654
|
],
|
|
592
655
|
};
|
|
@@ -786,7 +849,7 @@ describe("KumikoScreen", () => {
|
|
|
786
849
|
id: "sync",
|
|
787
850
|
label: "actions.sync",
|
|
788
851
|
handler: "tasks:write:task:sync",
|
|
789
|
-
payload:
|
|
852
|
+
payload: { all: true },
|
|
790
853
|
},
|
|
791
854
|
],
|
|
792
855
|
};
|
|
@@ -898,8 +961,7 @@ describe("KumikoScreen", () => {
|
|
|
898
961
|
id: "start",
|
|
899
962
|
label: "actions.start",
|
|
900
963
|
handler: "tasks:write:task:start",
|
|
901
|
-
|
|
902
|
-
visible: (row: unknown) => (row as { status?: string }).status === "scheduled",
|
|
964
|
+
visible: { field: "status", eq: "scheduled" },
|
|
903
965
|
},
|
|
904
966
|
],
|
|
905
967
|
};
|
|
@@ -36,8 +36,8 @@ function makeScreen(): EntityEditScreenDefinition {
|
|
|
36
36
|
"isUrgent",
|
|
37
37
|
{
|
|
38
38
|
field: "notes",
|
|
39
|
-
visible:
|
|
40
|
-
required:
|
|
39
|
+
visible: { field: "isUrgent", eq: true },
|
|
40
|
+
required: { field: "isUrgent", eq: true },
|
|
41
41
|
},
|
|
42
42
|
],
|
|
43
43
|
},
|
|
@@ -229,6 +229,59 @@ describe("RenderEdit", () => {
|
|
|
229
229
|
expect(mounted.textContent).toBe("order:row-42");
|
|
230
230
|
});
|
|
231
231
|
|
|
232
|
+
// Realer Update-Flow: EntityEditUpdateForm lässt `id` BEWUSST aus den
|
|
233
|
+
// Form-values (id ist keine deklarierte Field) und reicht die route-id
|
|
234
|
+
// stattdessen über die entityId-prop durch. Ohne den entityId-prop-Pfad
|
|
235
|
+
// fiele die Section auf vm.id (=values["id"]=undefined) zurück → create-
|
|
236
|
+
// mode trotz Edit (der Set-Value-UI-Bug, #187..fix).
|
|
237
|
+
test("extension section uses the entityId prop when id is absent from form values", () => {
|
|
238
|
+
const screenDef: EntityEditScreenDefinition = {
|
|
239
|
+
id: "orders:screen:order-edit",
|
|
240
|
+
type: "entityEdit",
|
|
241
|
+
entity: "order",
|
|
242
|
+
layout: {
|
|
243
|
+
sections: [
|
|
244
|
+
{ title: "Basics", columns: 2, fields: [{ field: "title", span: 2 }] },
|
|
245
|
+
{
|
|
246
|
+
kind: "extension",
|
|
247
|
+
title: "Custom Fields",
|
|
248
|
+
component: { react: { __component: "MyCustomFieldsForm" } },
|
|
249
|
+
},
|
|
250
|
+
],
|
|
251
|
+
},
|
|
252
|
+
};
|
|
253
|
+
const MyCustomFieldsForm = ({
|
|
254
|
+
entityName,
|
|
255
|
+
entityId,
|
|
256
|
+
}: {
|
|
257
|
+
entityName: string;
|
|
258
|
+
entityId: string | null;
|
|
259
|
+
}) => (
|
|
260
|
+
<div data-testid="my-custom-fields-form">
|
|
261
|
+
{entityName}:{entityId ?? "(create)"}
|
|
262
|
+
</div>
|
|
263
|
+
);
|
|
264
|
+
render(
|
|
265
|
+
<DispatcherProvider dispatcher={makeDispatcher()}>
|
|
266
|
+
<ExtensionSectionsProvider value={{ MyCustomFieldsForm }}>
|
|
267
|
+
<RenderEdit<TestValues>
|
|
268
|
+
screen={screenDef}
|
|
269
|
+
entity={orderEntity}
|
|
270
|
+
featureName="orders"
|
|
271
|
+
// KEIN id in den values (wie der echte Update-Form), aber
|
|
272
|
+
// entityId-prop trägt die route-id.
|
|
273
|
+
initial={{ title: "Existing", count: 0, isUrgent: false } as TestValues}
|
|
274
|
+
entityId="order-99"
|
|
275
|
+
writeCommand="order:update"
|
|
276
|
+
/>
|
|
277
|
+
</ExtensionSectionsProvider>
|
|
278
|
+
</DispatcherProvider>,
|
|
279
|
+
);
|
|
280
|
+
|
|
281
|
+
const mounted = screen.getByTestId("my-custom-fields-form");
|
|
282
|
+
expect(mounted.textContent).toBe("order:order-99");
|
|
283
|
+
});
|
|
284
|
+
|
|
232
285
|
test("extension section without registered component shows the placeholder banner", () => {
|
|
233
286
|
const screenDef: EntityEditScreenDefinition = {
|
|
234
287
|
id: "orders:screen:order-edit",
|