@cosmicdrift/kumiko-renderer-web 0.203.0 → 0.204.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.204.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>",
|
|
@@ -16,9 +16,9 @@
|
|
|
16
16
|
"./styles.css": "./src/styles.css"
|
|
17
17
|
},
|
|
18
18
|
"dependencies": {
|
|
19
|
-
"@cosmicdrift/kumiko-dispatcher-live": "0.
|
|
20
|
-
"@cosmicdrift/kumiko-headless": "0.
|
|
21
|
-
"@cosmicdrift/kumiko-renderer": "0.
|
|
19
|
+
"@cosmicdrift/kumiko-dispatcher-live": "0.204.0",
|
|
20
|
+
"@cosmicdrift/kumiko-headless": "0.204.0",
|
|
21
|
+
"@cosmicdrift/kumiko-renderer": "0.204.0",
|
|
22
22
|
"@radix-ui/react-dialog": "^1.1.15",
|
|
23
23
|
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
|
24
24
|
"@radix-ui/react-label": "^2.1.8",
|
|
@@ -8,9 +8,9 @@
|
|
|
8
8
|
import { describe, expect, test } from "bun:test";
|
|
9
9
|
import type { ProjectionDetailScreenDefinition } from "@cosmicdrift/kumiko-framework/ui-types";
|
|
10
10
|
import type { Dispatcher } from "@cosmicdrift/kumiko-headless";
|
|
11
|
-
import type { FeatureSchema } from "@cosmicdrift/kumiko-renderer";
|
|
12
|
-
import { DispatcherProvider, KumikoScreen } from "@cosmicdrift/kumiko-renderer";
|
|
13
|
-
import { act, createMockDispatcher, render, screen, waitFor } from "./test-utils";
|
|
11
|
+
import type { FeatureSchema, NavApi, NavTarget } from "@cosmicdrift/kumiko-renderer";
|
|
12
|
+
import { DispatcherProvider, KumikoScreen, NavProvider } from "@cosmicdrift/kumiko-renderer";
|
|
13
|
+
import { act, createMockDispatcher, fireEvent, render, screen, waitFor } from "./test-utils";
|
|
14
14
|
|
|
15
15
|
const detailScreen: ProjectionDetailScreenDefinition = {
|
|
16
16
|
id: "session-detail",
|
|
@@ -129,3 +129,187 @@ describe("KumikoScreen / projectionDetail", () => {
|
|
|
129
129
|
await waitFor(() => screen.getByTestId("kumiko-screen-record-missing"));
|
|
130
130
|
});
|
|
131
131
|
});
|
|
132
|
+
|
|
133
|
+
// fw#2166: `relatedList` sections run their own query against the shown
|
|
134
|
+
// record's id, independent of the detail query above.
|
|
135
|
+
describe("KumikoScreen / projectionDetail relatedList section (fw#2166)", () => {
|
|
136
|
+
const relatedListSection = {
|
|
137
|
+
kind: "relatedList" as const,
|
|
138
|
+
title: "Payments",
|
|
139
|
+
query: "sessions:query:user-session:payments",
|
|
140
|
+
columns: [{ field: "amount", label: "Amount" }],
|
|
141
|
+
rowClick: { entity: "payment" },
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
const detailScreenWithRelatedList: ProjectionDetailScreenDefinition = {
|
|
145
|
+
...detailScreen,
|
|
146
|
+
layout: { sections: [...detailScreen.layout.sections, relatedListSection] },
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
const relatedListSchema: FeatureSchema = {
|
|
150
|
+
featureName: "sessions",
|
|
151
|
+
entities: {},
|
|
152
|
+
screens: [detailScreenWithRelatedList],
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
function makeRelatedListDispatcher(paymentsRows: readonly Readonly<Record<string, unknown>>[]): {
|
|
156
|
+
readonly dispatcher: Dispatcher;
|
|
157
|
+
readonly calls: { readonly type: string; readonly payload: unknown }[];
|
|
158
|
+
} {
|
|
159
|
+
const calls: { type: string; payload: unknown }[] = [];
|
|
160
|
+
const query = (async (type: string, payload: unknown) => {
|
|
161
|
+
calls.push({ type, payload });
|
|
162
|
+
if (type === "sessions:query:user-session:detail") {
|
|
163
|
+
return {
|
|
164
|
+
isSuccess: true,
|
|
165
|
+
data: { userId: "user-42", createdAt: "2026-07-01T00:00:00Z" },
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
return { isSuccess: true, data: { rows: paymentsRows, nextCursor: null } };
|
|
169
|
+
}) as unknown as Dispatcher["query"];
|
|
170
|
+
return { dispatcher: createMockDispatcher({ query }), calls };
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
test("runs its own query with the route entityId under the default `id` parentParam, renders its rows, and leaves the parent's own fields read-only", async () => {
|
|
174
|
+
const { dispatcher, calls } = makeRelatedListDispatcher([{ id: "pay-1", amount: "42" }]);
|
|
175
|
+
|
|
176
|
+
render(
|
|
177
|
+
<DispatcherProvider dispatcher={dispatcher}>
|
|
178
|
+
<KumikoScreen
|
|
179
|
+
schema={relatedListSchema}
|
|
180
|
+
qn="sessions:screen:session-detail"
|
|
181
|
+
entityId="sess-1"
|
|
182
|
+
/>
|
|
183
|
+
</DispatcherProvider>,
|
|
184
|
+
);
|
|
185
|
+
|
|
186
|
+
await waitFor(() => screen.getByTestId("row-pay-1"));
|
|
187
|
+
|
|
188
|
+
// The shim's isFieldsEditSection flip must not regress the structural
|
|
189
|
+
// readOnly proof from the first test above.
|
|
190
|
+
const userIdInput = screen.getByTestId("field-userId").querySelector("input");
|
|
191
|
+
expect(userIdInput?.disabled).toBe(true);
|
|
192
|
+
expect(screen.queryByTestId("render-edit-submit")).toBeNull();
|
|
193
|
+
|
|
194
|
+
const relatedCall = calls.find((c) => c.type === "sessions:query:user-session:payments");
|
|
195
|
+
expect(relatedCall?.payload).toEqual({ id: "sess-1" });
|
|
196
|
+
expect(screen.getByTestId("cell-pay-1-amount").textContent).toBe("42");
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
test("a custom parentParam sends the route entityId under that key instead of `id`", async () => {
|
|
200
|
+
const customParamScreen: ProjectionDetailScreenDefinition = {
|
|
201
|
+
...detailScreen,
|
|
202
|
+
layout: {
|
|
203
|
+
sections: [
|
|
204
|
+
...detailScreen.layout.sections,
|
|
205
|
+
{ ...relatedListSection, parentParam: "sessionId" },
|
|
206
|
+
],
|
|
207
|
+
},
|
|
208
|
+
};
|
|
209
|
+
const customParamSchema: FeatureSchema = {
|
|
210
|
+
featureName: "sessions",
|
|
211
|
+
entities: {},
|
|
212
|
+
screens: [customParamScreen],
|
|
213
|
+
};
|
|
214
|
+
const { dispatcher, calls } = makeRelatedListDispatcher([]);
|
|
215
|
+
|
|
216
|
+
render(
|
|
217
|
+
<DispatcherProvider dispatcher={dispatcher}>
|
|
218
|
+
<KumikoScreen
|
|
219
|
+
schema={customParamSchema}
|
|
220
|
+
qn="sessions:screen:session-detail"
|
|
221
|
+
entityId="sess-1"
|
|
222
|
+
/>
|
|
223
|
+
</DispatcherProvider>,
|
|
224
|
+
);
|
|
225
|
+
|
|
226
|
+
await waitFor(() =>
|
|
227
|
+
expect(calls.some((c) => c.type === "sessions:query:user-session:payments")).toBe(true),
|
|
228
|
+
);
|
|
229
|
+
const relatedCall = calls.find((c) => c.type === "sessions:query:user-session:payments");
|
|
230
|
+
expect(relatedCall?.payload).toEqual({ sessionId: "sess-1" });
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
test("clicking a row with rowClick set navigates with the exact ObjectTarget", async () => {
|
|
234
|
+
const { dispatcher } = makeRelatedListDispatcher([{ id: "pay-1", amount: "42" }]);
|
|
235
|
+
let navigated: NavTarget | undefined;
|
|
236
|
+
const navApi: NavApi = {
|
|
237
|
+
route: undefined,
|
|
238
|
+
navigate: (target) => {
|
|
239
|
+
navigated = target;
|
|
240
|
+
},
|
|
241
|
+
replace: () => {},
|
|
242
|
+
hrefFor: () => "",
|
|
243
|
+
searchParams: {},
|
|
244
|
+
setSearchParams: () => {},
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
render(
|
|
248
|
+
<NavProvider value={navApi}>
|
|
249
|
+
<DispatcherProvider dispatcher={dispatcher}>
|
|
250
|
+
<KumikoScreen
|
|
251
|
+
schema={relatedListSchema}
|
|
252
|
+
qn="sessions:screen:session-detail"
|
|
253
|
+
entityId="sess-1"
|
|
254
|
+
/>
|
|
255
|
+
</DispatcherProvider>
|
|
256
|
+
</NavProvider>,
|
|
257
|
+
);
|
|
258
|
+
|
|
259
|
+
const row = await waitFor(() => screen.getByTestId("row-pay-1"));
|
|
260
|
+
fireEvent.click(row);
|
|
261
|
+
|
|
262
|
+
expect(navigated).toEqual({ entity: "payment", id: "pay-1" });
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
test("without rowClick, clicking a row does not navigate", async () => {
|
|
266
|
+
const noRowClickScreen: ProjectionDetailScreenDefinition = {
|
|
267
|
+
...detailScreen,
|
|
268
|
+
layout: {
|
|
269
|
+
sections: [
|
|
270
|
+
...detailScreen.layout.sections,
|
|
271
|
+
{
|
|
272
|
+
kind: "relatedList",
|
|
273
|
+
title: "Payments",
|
|
274
|
+
query: "sessions:query:user-session:payments",
|
|
275
|
+
columns: [{ field: "amount", label: "Amount" }],
|
|
276
|
+
},
|
|
277
|
+
],
|
|
278
|
+
},
|
|
279
|
+
};
|
|
280
|
+
const noRowClickSchema: FeatureSchema = {
|
|
281
|
+
featureName: "sessions",
|
|
282
|
+
entities: {},
|
|
283
|
+
screens: [noRowClickScreen],
|
|
284
|
+
};
|
|
285
|
+
const { dispatcher } = makeRelatedListDispatcher([{ id: "pay-1", amount: "42" }]);
|
|
286
|
+
let navigated: NavTarget | undefined;
|
|
287
|
+
const navApi: NavApi = {
|
|
288
|
+
route: undefined,
|
|
289
|
+
navigate: (target) => {
|
|
290
|
+
navigated = target;
|
|
291
|
+
},
|
|
292
|
+
replace: () => {},
|
|
293
|
+
hrefFor: () => "",
|
|
294
|
+
searchParams: {},
|
|
295
|
+
setSearchParams: () => {},
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
render(
|
|
299
|
+
<NavProvider value={navApi}>
|
|
300
|
+
<DispatcherProvider dispatcher={dispatcher}>
|
|
301
|
+
<KumikoScreen
|
|
302
|
+
schema={noRowClickSchema}
|
|
303
|
+
qn="sessions:screen:session-detail"
|
|
304
|
+
entityId="sess-1"
|
|
305
|
+
/>
|
|
306
|
+
</DispatcherProvider>
|
|
307
|
+
</NavProvider>,
|
|
308
|
+
);
|
|
309
|
+
|
|
310
|
+
const row = await waitFor(() => screen.getByTestId("row-pay-1"));
|
|
311
|
+
fireEvent.click(row);
|
|
312
|
+
|
|
313
|
+
expect(navigated).toBeUndefined();
|
|
314
|
+
});
|
|
315
|
+
});
|