@cosmicdrift/kumiko-renderer-web 0.202.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.202.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.202.0",
20
- "@cosmicdrift/kumiko-headless": "0.202.0",
21
- "@cosmicdrift/kumiko-renderer": "0.202.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
+ });