@cosmicdrift/kumiko-renderer 0.269.2 → 0.273.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 +4 -4
- package/src/app/__tests__/action-form-field-labels.test.tsx +92 -0
- package/src/app/__tests__/action-form-shim.test.ts +21 -0
- package/src/app/__tests__/entity-list-row-action-entity-target.test.tsx +12 -1
- package/src/app/__tests__/list-filter-facets.test.tsx +90 -2
- package/src/app/__tests__/projection-detail-actions.test.tsx +13 -1
- package/src/app/__tests__/return-to.test.ts +255 -0
- package/src/app/__tests__/row-default-edit-action.test.tsx +1 -1
- package/src/app/__tests__/screen-access-allows.test.ts +12 -2
- package/src/app/action-form-shim.ts +4 -2
- package/src/app/kumiko-screen.tsx +402 -276
- package/src/app/list-facets.ts +70 -27
- package/src/app/reference-facet-bridge.tsx +58 -0
- package/src/app/return-to.tsx +128 -0
- package/src/app/row-actions.ts +184 -25
- package/src/app/secret-mint-body.tsx +6 -4
- package/src/components/__tests__/related-list-section.test.tsx +611 -5
- package/src/components/related-list-section.tsx +100 -18
- package/src/components/render-edit.tsx +76 -33
- package/src/index.ts +9 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { describe, expect, spyOn, test } from "bun:test";
|
|
2
2
|
import type { EntityEditScreenDefinition, RowAction } from "@cosmicdrift/kumiko-framework/ui-types";
|
|
3
3
|
import type { Dispatcher, EditRelatedListSectionViewModel } from "@cosmicdrift/kumiko-headless";
|
|
4
|
-
import { fireEvent, render, screen as rtlScreen, waitFor } from "@testing-library/react";
|
|
4
|
+
import { act, fireEvent, render, screen as rtlScreen, waitFor } from "@testing-library/react";
|
|
5
5
|
import type { ComponentType, ReactNode } from "react";
|
|
6
6
|
import { AppFeaturesProvider } from "../../app/app-features-context";
|
|
7
7
|
import type { FeatureSchema } from "../../app/feature-schema";
|
|
@@ -175,7 +175,12 @@ function renderRelatedList(
|
|
|
175
175
|
<DispatcherProvider dispatcher={dispatcher}>
|
|
176
176
|
<PrimitivesProvider value={testPrimitives()}>
|
|
177
177
|
<NavProvider value={nav}>
|
|
178
|
-
<RelatedListSection
|
|
178
|
+
<RelatedListSection
|
|
179
|
+
section={section}
|
|
180
|
+
parentId="order-1"
|
|
181
|
+
record={{ id: "order-1" }}
|
|
182
|
+
featureName="orders"
|
|
183
|
+
/>
|
|
179
184
|
</NavProvider>
|
|
180
185
|
</PrimitivesProvider>
|
|
181
186
|
</DispatcherProvider>
|
|
@@ -184,7 +189,7 @@ function renderRelatedList(
|
|
|
184
189
|
}
|
|
185
190
|
|
|
186
191
|
describe("RelatedListSection — tabs-mode card chrome (fw#2722)", () => {
|
|
187
|
-
test("hideTitle (tabs mode) renders the list without a Section wrapper
|
|
192
|
+
test("hideTitle (tabs mode) renders the list without a Section wrapper, keeps the table frame, and marks scrollBody", async () => {
|
|
188
193
|
const { dispatcher } = stubDispatcher();
|
|
189
194
|
let capturedChromeless: boolean | undefined;
|
|
190
195
|
let capturedScrollBody: boolean | undefined;
|
|
@@ -204,6 +209,7 @@ describe("RelatedListSection — tabs-mode card chrome (fw#2722)", () => {
|
|
|
204
209
|
<RelatedListSection
|
|
205
210
|
section={historySection}
|
|
206
211
|
parentId="order-1"
|
|
212
|
+
record={{ id: "order-1" }}
|
|
207
213
|
featureName="orders"
|
|
208
214
|
hideTitle
|
|
209
215
|
/>
|
|
@@ -215,7 +221,7 @@ describe("RelatedListSection — tabs-mode card chrome (fw#2722)", () => {
|
|
|
215
221
|
|
|
216
222
|
await waitFor(() => expect(rtlScreen.getByTestId("row-r1")).toBeTruthy());
|
|
217
223
|
expect(rtlScreen.queryByTestId(`related-list-${historySection.title}`)).toBeNull();
|
|
218
|
-
expect(capturedChromeless).
|
|
224
|
+
expect(capturedChromeless).toBeUndefined();
|
|
219
225
|
expect(capturedScrollBody).toBe(true);
|
|
220
226
|
});
|
|
221
227
|
|
|
@@ -239,6 +245,7 @@ describe("RelatedListSection — tabs-mode card chrome (fw#2722)", () => {
|
|
|
239
245
|
<RelatedListSection
|
|
240
246
|
section={historySection}
|
|
241
247
|
parentId="order-1"
|
|
248
|
+
record={{ id: "order-1" }}
|
|
242
249
|
featureName="orders"
|
|
243
250
|
/>
|
|
244
251
|
</NavProvider>
|
|
@@ -254,6 +261,404 @@ describe("RelatedListSection — tabs-mode card chrome (fw#2722)", () => {
|
|
|
254
261
|
});
|
|
255
262
|
});
|
|
256
263
|
|
|
264
|
+
describe("RelatedListSection — list-screen toolbar in tabs mode", () => {
|
|
265
|
+
test("hideTitle (tabs mode) still surfaces search, facets and a toolbarActions button", async () => {
|
|
266
|
+
const { dispatcher } = stubDispatcher();
|
|
267
|
+
let capturedFilterFacets: DataTableProps["filterFacets"];
|
|
268
|
+
const capturingDataTable: ComponentType<DataTableProps> = (props) => {
|
|
269
|
+
capturedFilterFacets = props.filterFacets;
|
|
270
|
+
return (
|
|
271
|
+
<>
|
|
272
|
+
{props.toolbarEnd}
|
|
273
|
+
{testDataTable(props)}
|
|
274
|
+
</>
|
|
275
|
+
);
|
|
276
|
+
};
|
|
277
|
+
const testButton = ({
|
|
278
|
+
children,
|
|
279
|
+
testId,
|
|
280
|
+
onClick,
|
|
281
|
+
}: {
|
|
282
|
+
readonly children?: ReactNode;
|
|
283
|
+
readonly testId?: string;
|
|
284
|
+
readonly onClick?: () => void;
|
|
285
|
+
}) => (
|
|
286
|
+
<button type="button" data-testid={testId} onClick={() => onClick?.()}>
|
|
287
|
+
{children}
|
|
288
|
+
</button>
|
|
289
|
+
);
|
|
290
|
+
const section: EditRelatedListSectionViewModel = {
|
|
291
|
+
...historySection,
|
|
292
|
+
searchable: true,
|
|
293
|
+
facets: [
|
|
294
|
+
{
|
|
295
|
+
field: "name",
|
|
296
|
+
type: "select",
|
|
297
|
+
label: "Name",
|
|
298
|
+
options: [{ value: "Alice", label: "Alice" }],
|
|
299
|
+
},
|
|
300
|
+
],
|
|
301
|
+
toolbarActions: [
|
|
302
|
+
{
|
|
303
|
+
kind: "navigate",
|
|
304
|
+
id: "create",
|
|
305
|
+
label: "New notification",
|
|
306
|
+
screen: "notification-create",
|
|
307
|
+
},
|
|
308
|
+
],
|
|
309
|
+
};
|
|
310
|
+
render(
|
|
311
|
+
<LocaleProvider
|
|
312
|
+
resolver={createStaticLocaleResolver({ locale: "en-US" })}
|
|
313
|
+
fallbackBundles={[kumikoDefaultTranslations]}
|
|
314
|
+
>
|
|
315
|
+
<DispatcherProvider dispatcher={dispatcher}>
|
|
316
|
+
<PrimitivesProvider
|
|
317
|
+
value={{ ...testPrimitives(), DataTable: capturingDataTable, Button: testButton }}
|
|
318
|
+
>
|
|
319
|
+
<NavProvider value={stubNav().nav}>
|
|
320
|
+
<RelatedListSection
|
|
321
|
+
section={section}
|
|
322
|
+
parentId="order-1"
|
|
323
|
+
record={{ id: "order-1" }}
|
|
324
|
+
featureName="orders"
|
|
325
|
+
hideTitle
|
|
326
|
+
/>
|
|
327
|
+
</NavProvider>
|
|
328
|
+
</PrimitivesProvider>
|
|
329
|
+
</DispatcherProvider>
|
|
330
|
+
</LocaleProvider>,
|
|
331
|
+
);
|
|
332
|
+
|
|
333
|
+
await waitFor(() => expect(rtlScreen.getByTestId("row-r1")).toBeTruthy());
|
|
334
|
+
expect(capturedFilterFacets?.length).toBe(1);
|
|
335
|
+
expect(rtlScreen.getByTestId("render-list-toolbar-action-create")).toBeTruthy();
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
test("clicking a toolbarAction navigates to its screen and prefills the parent id under parentParam", async () => {
|
|
339
|
+
const { dispatcher } = stubDispatcher();
|
|
340
|
+
const { nav, navigations, searchParams } = stubNav();
|
|
341
|
+
const capturingDataTable: ComponentType<DataTableProps> = (props) => (
|
|
342
|
+
<>
|
|
343
|
+
{props.toolbarEnd}
|
|
344
|
+
{testDataTable(props)}
|
|
345
|
+
</>
|
|
346
|
+
);
|
|
347
|
+
const testButton = ({
|
|
348
|
+
children,
|
|
349
|
+
testId,
|
|
350
|
+
onClick,
|
|
351
|
+
}: {
|
|
352
|
+
readonly children?: ReactNode;
|
|
353
|
+
readonly testId?: string;
|
|
354
|
+
readonly onClick?: () => void;
|
|
355
|
+
}) => (
|
|
356
|
+
<button type="button" data-testid={testId} onClick={() => onClick?.()}>
|
|
357
|
+
{children}
|
|
358
|
+
</button>
|
|
359
|
+
);
|
|
360
|
+
const section: EditRelatedListSectionViewModel = {
|
|
361
|
+
...historySection,
|
|
362
|
+
parentParam: "leaseId",
|
|
363
|
+
toolbarActions: [
|
|
364
|
+
{ kind: "navigate", id: "create", label: "Add position", screen: "position-create" },
|
|
365
|
+
],
|
|
366
|
+
};
|
|
367
|
+
render(
|
|
368
|
+
<LocaleProvider
|
|
369
|
+
resolver={createStaticLocaleResolver({ locale: "en-US" })}
|
|
370
|
+
fallbackBundles={[kumikoDefaultTranslations]}
|
|
371
|
+
>
|
|
372
|
+
<DispatcherProvider dispatcher={dispatcher}>
|
|
373
|
+
<PrimitivesProvider
|
|
374
|
+
value={{ ...testPrimitives(), DataTable: capturingDataTable, Button: testButton }}
|
|
375
|
+
>
|
|
376
|
+
<NavProvider value={nav}>
|
|
377
|
+
<RelatedListSection
|
|
378
|
+
section={section}
|
|
379
|
+
parentId="lease-9"
|
|
380
|
+
record={{ id: "lease-9" }}
|
|
381
|
+
featureName="leases"
|
|
382
|
+
/>
|
|
383
|
+
</NavProvider>
|
|
384
|
+
</PrimitivesProvider>
|
|
385
|
+
</DispatcherProvider>
|
|
386
|
+
</LocaleProvider>,
|
|
387
|
+
);
|
|
388
|
+
|
|
389
|
+
await waitFor(() => expect(rtlScreen.getByTestId("row-r1")).toBeTruthy());
|
|
390
|
+
rtlScreen.getByTestId("render-list-toolbar-action-create").click();
|
|
391
|
+
|
|
392
|
+
await waitFor(() => expect(navigations).toHaveLength(1));
|
|
393
|
+
expect(navigations[0]).toEqual({ screenId: "position-create" });
|
|
394
|
+
expect(searchParams).toEqual([{ leaseId: "lease-9" }]);
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
test("a toolbarAction with a visible condition that the parent record fails is not rendered", async () => {
|
|
398
|
+
const { dispatcher } = stubDispatcher();
|
|
399
|
+
const { nav } = stubNav();
|
|
400
|
+
const capturingDataTable: ComponentType<DataTableProps> = (props) => (
|
|
401
|
+
<>
|
|
402
|
+
{props.toolbarEnd}
|
|
403
|
+
{testDataTable(props)}
|
|
404
|
+
</>
|
|
405
|
+
);
|
|
406
|
+
const testButton = ({
|
|
407
|
+
children,
|
|
408
|
+
testId,
|
|
409
|
+
}: {
|
|
410
|
+
readonly children?: ReactNode;
|
|
411
|
+
readonly testId?: string;
|
|
412
|
+
}) => (
|
|
413
|
+
<button type="button" data-testid={testId}>
|
|
414
|
+
{children}
|
|
415
|
+
</button>
|
|
416
|
+
);
|
|
417
|
+
const section: EditRelatedListSectionViewModel = {
|
|
418
|
+
...historySection,
|
|
419
|
+
parentParam: "leaseId",
|
|
420
|
+
toolbarActions: [
|
|
421
|
+
{
|
|
422
|
+
kind: "navigate",
|
|
423
|
+
id: "create",
|
|
424
|
+
label: "Add position",
|
|
425
|
+
screen: "position-create",
|
|
426
|
+
visible: { field: "status", eq: "active" },
|
|
427
|
+
},
|
|
428
|
+
],
|
|
429
|
+
};
|
|
430
|
+
render(
|
|
431
|
+
<LocaleProvider
|
|
432
|
+
resolver={createStaticLocaleResolver({ locale: "en-US" })}
|
|
433
|
+
fallbackBundles={[kumikoDefaultTranslations]}
|
|
434
|
+
>
|
|
435
|
+
<DispatcherProvider dispatcher={dispatcher}>
|
|
436
|
+
<PrimitivesProvider
|
|
437
|
+
value={{ ...testPrimitives(), DataTable: capturingDataTable, Button: testButton }}
|
|
438
|
+
>
|
|
439
|
+
<NavProvider value={nav}>
|
|
440
|
+
<RelatedListSection
|
|
441
|
+
section={section}
|
|
442
|
+
parentId="lease-9"
|
|
443
|
+
record={{ id: "lease-9", status: "closed" }}
|
|
444
|
+
featureName="leases"
|
|
445
|
+
/>
|
|
446
|
+
</NavProvider>
|
|
447
|
+
</PrimitivesProvider>
|
|
448
|
+
</DispatcherProvider>
|
|
449
|
+
</LocaleProvider>,
|
|
450
|
+
);
|
|
451
|
+
|
|
452
|
+
await waitFor(() => expect(rtlScreen.getByTestId("row-r1")).toBeTruthy());
|
|
453
|
+
expect(rtlScreen.queryByTestId("render-list-toolbar-action-create")).toBeNull();
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
test("a toolbarAction with a visible condition the parent record satisfies is rendered", async () => {
|
|
457
|
+
const { dispatcher } = stubDispatcher();
|
|
458
|
+
const { nav } = stubNav();
|
|
459
|
+
const capturingDataTable: ComponentType<DataTableProps> = (props) => (
|
|
460
|
+
<>
|
|
461
|
+
{props.toolbarEnd}
|
|
462
|
+
{testDataTable(props)}
|
|
463
|
+
</>
|
|
464
|
+
);
|
|
465
|
+
const testButton = ({
|
|
466
|
+
children,
|
|
467
|
+
testId,
|
|
468
|
+
}: {
|
|
469
|
+
readonly children?: ReactNode;
|
|
470
|
+
readonly testId?: string;
|
|
471
|
+
}) => (
|
|
472
|
+
<button type="button" data-testid={testId}>
|
|
473
|
+
{children}
|
|
474
|
+
</button>
|
|
475
|
+
);
|
|
476
|
+
const section: EditRelatedListSectionViewModel = {
|
|
477
|
+
...historySection,
|
|
478
|
+
parentParam: "leaseId",
|
|
479
|
+
toolbarActions: [
|
|
480
|
+
{
|
|
481
|
+
kind: "navigate",
|
|
482
|
+
id: "create",
|
|
483
|
+
label: "Add position",
|
|
484
|
+
screen: "position-create",
|
|
485
|
+
visible: { field: "status", eq: "active" },
|
|
486
|
+
},
|
|
487
|
+
],
|
|
488
|
+
};
|
|
489
|
+
render(
|
|
490
|
+
<LocaleProvider
|
|
491
|
+
resolver={createStaticLocaleResolver({ locale: "en-US" })}
|
|
492
|
+
fallbackBundles={[kumikoDefaultTranslations]}
|
|
493
|
+
>
|
|
494
|
+
<DispatcherProvider dispatcher={dispatcher}>
|
|
495
|
+
<PrimitivesProvider
|
|
496
|
+
value={{ ...testPrimitives(), DataTable: capturingDataTable, Button: testButton }}
|
|
497
|
+
>
|
|
498
|
+
<NavProvider value={nav}>
|
|
499
|
+
<RelatedListSection
|
|
500
|
+
section={section}
|
|
501
|
+
parentId="lease-9"
|
|
502
|
+
record={{ id: "lease-9", status: "active" }}
|
|
503
|
+
featureName="leases"
|
|
504
|
+
/>
|
|
505
|
+
</NavProvider>
|
|
506
|
+
</PrimitivesProvider>
|
|
507
|
+
</DispatcherProvider>
|
|
508
|
+
</LocaleProvider>,
|
|
509
|
+
);
|
|
510
|
+
|
|
511
|
+
await waitFor(() =>
|
|
512
|
+
expect(rtlScreen.getByTestId("render-list-toolbar-action-create")).toBeTruthy(),
|
|
513
|
+
);
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
test("a toolbarAction's params extractor maps the parent record and replaces the implicit parentParam prefill", async () => {
|
|
517
|
+
const { dispatcher } = stubDispatcher();
|
|
518
|
+
const { nav, navigations, searchParams } = stubNav();
|
|
519
|
+
const capturingDataTable: ComponentType<DataTableProps> = (props) => (
|
|
520
|
+
<>
|
|
521
|
+
{props.toolbarEnd}
|
|
522
|
+
{testDataTable(props)}
|
|
523
|
+
</>
|
|
524
|
+
);
|
|
525
|
+
const testButton = ({
|
|
526
|
+
children,
|
|
527
|
+
testId,
|
|
528
|
+
onClick,
|
|
529
|
+
}: {
|
|
530
|
+
readonly children?: ReactNode;
|
|
531
|
+
readonly testId?: string;
|
|
532
|
+
readonly onClick?: () => void;
|
|
533
|
+
}) => (
|
|
534
|
+
<button type="button" data-testid={testId} onClick={() => onClick?.()}>
|
|
535
|
+
{children}
|
|
536
|
+
</button>
|
|
537
|
+
);
|
|
538
|
+
const section: EditRelatedListSectionViewModel = {
|
|
539
|
+
...historySection,
|
|
540
|
+
// Default parentParam ("id") would prefill `{ id: "lease-9" }` — the
|
|
541
|
+
// declared params extractor below must produce `{ leaseId: ... }`
|
|
542
|
+
// instead, proving params replaces rather than merges with it.
|
|
543
|
+
toolbarActions: [
|
|
544
|
+
{
|
|
545
|
+
kind: "navigate",
|
|
546
|
+
id: "create",
|
|
547
|
+
label: "Add position",
|
|
548
|
+
screen: "position-create",
|
|
549
|
+
params: { map: { leaseId: "id" } },
|
|
550
|
+
},
|
|
551
|
+
],
|
|
552
|
+
};
|
|
553
|
+
render(
|
|
554
|
+
<LocaleProvider
|
|
555
|
+
resolver={createStaticLocaleResolver({ locale: "en-US" })}
|
|
556
|
+
fallbackBundles={[kumikoDefaultTranslations]}
|
|
557
|
+
>
|
|
558
|
+
<DispatcherProvider dispatcher={dispatcher}>
|
|
559
|
+
<PrimitivesProvider
|
|
560
|
+
value={{ ...testPrimitives(), DataTable: capturingDataTable, Button: testButton }}
|
|
561
|
+
>
|
|
562
|
+
<NavProvider value={nav}>
|
|
563
|
+
<RelatedListSection
|
|
564
|
+
section={section}
|
|
565
|
+
parentId="lease-9"
|
|
566
|
+
record={{ id: "lease-9" }}
|
|
567
|
+
featureName="leases"
|
|
568
|
+
/>
|
|
569
|
+
</NavProvider>
|
|
570
|
+
</PrimitivesProvider>
|
|
571
|
+
</DispatcherProvider>
|
|
572
|
+
</LocaleProvider>,
|
|
573
|
+
);
|
|
574
|
+
|
|
575
|
+
await waitFor(() => expect(rtlScreen.getByTestId("row-r1")).toBeTruthy());
|
|
576
|
+
rtlScreen.getByTestId("render-list-toolbar-action-create").click();
|
|
577
|
+
|
|
578
|
+
await waitFor(() => expect(navigations).toHaveLength(1));
|
|
579
|
+
expect(searchParams).toEqual([{ leaseId: "lease-9" }]);
|
|
580
|
+
});
|
|
581
|
+
|
|
582
|
+
test("a reference facet loads its options from the referenced entity's list query, and selecting one sends an id filter", async () => {
|
|
583
|
+
const calls: Array<{ type: string; payload: unknown }> = [];
|
|
584
|
+
const dispatcher: Dispatcher = {
|
|
585
|
+
write: (async () => ({ isSuccess: true, data: null })) as Dispatcher["write"],
|
|
586
|
+
query: (async (type: string, payload: unknown) => {
|
|
587
|
+
calls.push({ type, payload });
|
|
588
|
+
if (type === "orders:query:tenant:list") {
|
|
589
|
+
return {
|
|
590
|
+
isSuccess: true,
|
|
591
|
+
data: {
|
|
592
|
+
rows: [{ id: "t1", name: "Acme" }],
|
|
593
|
+
nextCursor: null,
|
|
594
|
+
},
|
|
595
|
+
};
|
|
596
|
+
}
|
|
597
|
+
return { isSuccess: true, data: { rows: [{ id: "r1", name: "Alice" }], nextCursor: null } };
|
|
598
|
+
}) as Dispatcher["query"],
|
|
599
|
+
batch: (async () => ({ isSuccess: true, results: [] })) as Dispatcher["batch"],
|
|
600
|
+
statusStore: {
|
|
601
|
+
getState: () => "online",
|
|
602
|
+
subscribe: () => () => {},
|
|
603
|
+
} as unknown as Dispatcher["statusStore"],
|
|
604
|
+
async *stream() {},
|
|
605
|
+
pendingWrites: () => [],
|
|
606
|
+
pendingFiles: () => [],
|
|
607
|
+
};
|
|
608
|
+
let capturedFilterFacets: DataTableProps["filterFacets"];
|
|
609
|
+
let capturedOnFilterChange: DataTableProps["onFilterChange"];
|
|
610
|
+
const capturingDataTable: ComponentType<DataTableProps> = (props) => {
|
|
611
|
+
capturedFilterFacets = props.filterFacets;
|
|
612
|
+
capturedOnFilterChange = props.onFilterChange;
|
|
613
|
+
return testDataTable(props);
|
|
614
|
+
};
|
|
615
|
+
const section: EditRelatedListSectionViewModel = {
|
|
616
|
+
...historySection,
|
|
617
|
+
facets: [
|
|
618
|
+
{ field: "name", type: "reference", label: "Tenant", entity: "tenant", labelField: "name" },
|
|
619
|
+
],
|
|
620
|
+
};
|
|
621
|
+
render(
|
|
622
|
+
<LocaleProvider
|
|
623
|
+
resolver={createStaticLocaleResolver({ locale: "en-US" })}
|
|
624
|
+
fallbackBundles={[kumikoDefaultTranslations]}
|
|
625
|
+
>
|
|
626
|
+
<DispatcherProvider dispatcher={dispatcher}>
|
|
627
|
+
<PrimitivesProvider value={{ ...testPrimitives(), DataTable: capturingDataTable }}>
|
|
628
|
+
<NavProvider value={stubNav().nav}>
|
|
629
|
+
<RelatedListSection
|
|
630
|
+
section={section}
|
|
631
|
+
parentId="order-1"
|
|
632
|
+
record={{ id: "order-1" }}
|
|
633
|
+
featureName="orders"
|
|
634
|
+
hideTitle
|
|
635
|
+
/>
|
|
636
|
+
</NavProvider>
|
|
637
|
+
</PrimitivesProvider>
|
|
638
|
+
</DispatcherProvider>
|
|
639
|
+
</LocaleProvider>,
|
|
640
|
+
);
|
|
641
|
+
|
|
642
|
+
await waitFor(() => expect(rtlScreen.getByTestId("row-r1")).toBeTruthy());
|
|
643
|
+
await waitFor(() => expect(capturedFilterFacets?.[0]?.options).toHaveLength(1));
|
|
644
|
+
expect(capturedFilterFacets).toEqual([
|
|
645
|
+
{ field: "name", label: "Tenant", options: [{ value: "t1", label: "Acme" }] },
|
|
646
|
+
]);
|
|
647
|
+
|
|
648
|
+
const countBeforeSelect = calls.length;
|
|
649
|
+
await act(async () => {
|
|
650
|
+
capturedOnFilterChange?.("name", ["t1"]);
|
|
651
|
+
});
|
|
652
|
+
await waitFor(() => expect(calls.length).toBeGreaterThan(countBeforeSelect));
|
|
653
|
+
const lastListCall = [...calls]
|
|
654
|
+
.reverse()
|
|
655
|
+
.find((c) => c.type === "orders:query:notifications:list");
|
|
656
|
+
expect(lastListCall?.payload).toMatchObject({
|
|
657
|
+
filters: [{ field: "name", op: "in", value: ["t1"] }],
|
|
658
|
+
});
|
|
659
|
+
});
|
|
660
|
+
});
|
|
661
|
+
|
|
257
662
|
describe("RelatedListSection — rowActions", () => {
|
|
258
663
|
test("clicking a row action dispatches through the configured write-handler with the extracted payload, then refetches", async () => {
|
|
259
664
|
const { dispatcher, writes, queryCount } = stubDispatcher();
|
|
@@ -359,6 +764,7 @@ describe("RelatedListSection — rowActions drawer-kind (fw#2710)", () => {
|
|
|
359
764
|
],
|
|
360
765
|
}}
|
|
361
766
|
parentId="order-1"
|
|
767
|
+
record={{ id: "order-1" }}
|
|
362
768
|
featureName="orders"
|
|
363
769
|
onOpenDrawer={(action, initialValues) =>
|
|
364
770
|
openDrawerCalls.push([action, initialValues])
|
|
@@ -456,6 +862,7 @@ describe("RelatedListSection — rowActions drawer-kind (fw#2710)", () => {
|
|
|
456
862
|
],
|
|
457
863
|
}}
|
|
458
864
|
parentId="order-1"
|
|
865
|
+
record={{ id: "order-1" }}
|
|
459
866
|
featureName="orders"
|
|
460
867
|
onOpenDrawer={noop}
|
|
461
868
|
/>
|
|
@@ -532,6 +939,7 @@ describe("RelatedListSection — sorting (fw#2722)", () => {
|
|
|
532
939
|
defaultSort: { field: "amount", dir: "asc" },
|
|
533
940
|
}}
|
|
534
941
|
parentId="order-1"
|
|
942
|
+
record={{ id: "order-1" }}
|
|
535
943
|
featureName="orders"
|
|
536
944
|
/>
|
|
537
945
|
</NavProvider>
|
|
@@ -562,6 +970,7 @@ describe("RelatedListSection — sorting (fw#2722)", () => {
|
|
|
562
970
|
columns: [{ field: "amount", sortable: true }],
|
|
563
971
|
}}
|
|
564
972
|
parentId="order-1"
|
|
973
|
+
record={{ id: "order-1" }}
|
|
565
974
|
featureName="orders"
|
|
566
975
|
/>
|
|
567
976
|
</NavProvider>
|
|
@@ -597,6 +1006,7 @@ describe("RelatedListSection — sorting (fw#2722)", () => {
|
|
|
597
1006
|
columns: [{ field: "amount" }],
|
|
598
1007
|
}}
|
|
599
1008
|
parentId="order-1"
|
|
1009
|
+
record={{ id: "order-1" }}
|
|
600
1010
|
featureName="orders"
|
|
601
1011
|
/>
|
|
602
1012
|
</NavProvider>
|
|
@@ -757,6 +1167,7 @@ function renderWithDataTable(
|
|
|
757
1167
|
<RelatedListSection
|
|
758
1168
|
section={section}
|
|
759
1169
|
parentId="order-1"
|
|
1170
|
+
record={{ id: "order-1" }}
|
|
760
1171
|
featureName="orders"
|
|
761
1172
|
{...(hideTitle === true && { hideTitle: true })}
|
|
762
1173
|
/>
|
|
@@ -942,6 +1353,196 @@ describe("RelatedListSection — search + facets (fw#2740)", () => {
|
|
|
942
1353
|
});
|
|
943
1354
|
});
|
|
944
1355
|
|
|
1356
|
+
// A dispatcher stub that filters a fixed row set by `payload.filter` (single
|
|
1357
|
+
// eq clause), `payload.search` (substring on `name`) and `payload.filters`
|
|
1358
|
+
// (facet membership) together — proves parentFilter composes with search
|
|
1359
|
+
// and facets instead of being clobbered by either.
|
|
1360
|
+
function parentFilterDispatcher(rows: readonly Record<string, unknown>[]): {
|
|
1361
|
+
dispatcher: Dispatcher;
|
|
1362
|
+
payloads: Record<string, unknown>[];
|
|
1363
|
+
} {
|
|
1364
|
+
const payloads: Record<string, unknown>[] = [];
|
|
1365
|
+
const dispatcher: Dispatcher = {
|
|
1366
|
+
write: (async () => ({ isSuccess: true, data: null })) as Dispatcher["write"],
|
|
1367
|
+
query: (async (_type: string, payload: unknown) => {
|
|
1368
|
+
const p = payload as {
|
|
1369
|
+
filter?: { field: string; op: "eq"; value: unknown };
|
|
1370
|
+
search?: string;
|
|
1371
|
+
filters?: readonly { field: string; op: "in"; value: readonly unknown[] }[];
|
|
1372
|
+
};
|
|
1373
|
+
payloads.push(p);
|
|
1374
|
+
let result = rows;
|
|
1375
|
+
if (p.filter !== undefined) {
|
|
1376
|
+
const { field, value } = p.filter;
|
|
1377
|
+
result = result.filter((r) => r[field] === value);
|
|
1378
|
+
}
|
|
1379
|
+
if (p.search !== undefined && p.search !== "") {
|
|
1380
|
+
const term = p.search.toLowerCase();
|
|
1381
|
+
result = result.filter((r) =>
|
|
1382
|
+
String(r["name"] ?? "")
|
|
1383
|
+
.toLowerCase()
|
|
1384
|
+
.includes(term),
|
|
1385
|
+
);
|
|
1386
|
+
}
|
|
1387
|
+
for (const f of p.filters ?? []) {
|
|
1388
|
+
result = result.filter((r) => f.value.includes(r[f.field]));
|
|
1389
|
+
}
|
|
1390
|
+
return { isSuccess: true, data: { rows: result, nextCursor: null } };
|
|
1391
|
+
}) as Dispatcher["query"],
|
|
1392
|
+
batch: (async () => ({ isSuccess: true, results: [] })) as Dispatcher["batch"],
|
|
1393
|
+
statusStore: {
|
|
1394
|
+
getState: () => "online",
|
|
1395
|
+
subscribe: () => () => {},
|
|
1396
|
+
} as unknown as Dispatcher["statusStore"],
|
|
1397
|
+
async *stream() {},
|
|
1398
|
+
pendingWrites: () => [],
|
|
1399
|
+
pendingFiles: () => [],
|
|
1400
|
+
};
|
|
1401
|
+
return { dispatcher, payloads };
|
|
1402
|
+
}
|
|
1403
|
+
|
|
1404
|
+
describe("RelatedListSection — parentFilter", () => {
|
|
1405
|
+
const rows = [
|
|
1406
|
+
{ id: "r1", parentId: "order-1", name: "Alice", status: "active" },
|
|
1407
|
+
{ id: "r2", parentId: "order-1", name: "Bob", status: "ended" },
|
|
1408
|
+
{ id: "r3", parentId: "order-2", name: "Carol", status: "active" },
|
|
1409
|
+
];
|
|
1410
|
+
const section: EditRelatedListSectionViewModel = {
|
|
1411
|
+
kind: "relatedList",
|
|
1412
|
+
title: "Positions",
|
|
1413
|
+
query: "orders:query:items:list",
|
|
1414
|
+
columns: [{ field: "name" }, { field: "status" }],
|
|
1415
|
+
parentFilter: { field: "parentId" },
|
|
1416
|
+
pageSize: 25,
|
|
1417
|
+
searchable: true,
|
|
1418
|
+
facets: [
|
|
1419
|
+
{
|
|
1420
|
+
field: "status",
|
|
1421
|
+
type: "select",
|
|
1422
|
+
label: "Status",
|
|
1423
|
+
options: [
|
|
1424
|
+
{ value: "active", label: "Active" },
|
|
1425
|
+
{ value: "ended", label: "Ended" },
|
|
1426
|
+
],
|
|
1427
|
+
},
|
|
1428
|
+
],
|
|
1429
|
+
};
|
|
1430
|
+
|
|
1431
|
+
test("sends payload.filter eq parentId (no top-level parentParam key), combined with limit/search/facets, and only the parent's rows render", async () => {
|
|
1432
|
+
const { dispatcher, payloads } = parentFilterDispatcher(rows);
|
|
1433
|
+
renderWithDataTable(dispatcher, section, searchFacetDataTable);
|
|
1434
|
+
|
|
1435
|
+
await waitFor(() => expect(rtlScreen.getByTestId("row-r1")).toBeTruthy());
|
|
1436
|
+
expect(rtlScreen.getByTestId("row-r2")).toBeTruthy();
|
|
1437
|
+
expect(rtlScreen.queryByTestId("row-r3")).toBeNull();
|
|
1438
|
+
|
|
1439
|
+
const firstPayload = payloads[0];
|
|
1440
|
+
expect(firstPayload).toEqual({
|
|
1441
|
+
filter: { field: "parentId", op: "eq", value: "order-1" },
|
|
1442
|
+
limit: 25,
|
|
1443
|
+
});
|
|
1444
|
+
expect(firstPayload?.["id"]).toBeUndefined();
|
|
1445
|
+
expect(firstPayload?.["parentId"]).toBeUndefined();
|
|
1446
|
+
});
|
|
1447
|
+
|
|
1448
|
+
test("typing a search term keeps payload.filter and narrows via payload.search together", async () => {
|
|
1449
|
+
const { dispatcher, payloads } = parentFilterDispatcher(rows);
|
|
1450
|
+
renderWithDataTable(dispatcher, section, searchFacetDataTable);
|
|
1451
|
+
|
|
1452
|
+
await waitFor(() => expect(rtlScreen.getByTestId("row-r1")).toBeTruthy());
|
|
1453
|
+
fireEvent.change(rtlScreen.getByTestId("render-list-search"), {
|
|
1454
|
+
target: { value: "ali" },
|
|
1455
|
+
});
|
|
1456
|
+
|
|
1457
|
+
await waitFor(
|
|
1458
|
+
() => {
|
|
1459
|
+
const last = payloads[payloads.length - 1];
|
|
1460
|
+
expect(last?.["search"]).toBe("ali");
|
|
1461
|
+
},
|
|
1462
|
+
{ timeout: 2000 },
|
|
1463
|
+
);
|
|
1464
|
+
const last = payloads[payloads.length - 1];
|
|
1465
|
+
expect(last?.["filter"]).toEqual({ field: "parentId", op: "eq", value: "order-1" });
|
|
1466
|
+
await waitFor(() => expect(rtlScreen.getByTestId("row-r1")).toBeTruthy());
|
|
1467
|
+
expect(rtlScreen.queryByTestId("row-r2")).toBeNull();
|
|
1468
|
+
});
|
|
1469
|
+
|
|
1470
|
+
test("selecting a facet leaves payload.filter unchanged and adds payload.filters", async () => {
|
|
1471
|
+
const { dispatcher, payloads } = parentFilterDispatcher(rows);
|
|
1472
|
+
renderWithDataTable(dispatcher, section, searchFacetDataTable);
|
|
1473
|
+
|
|
1474
|
+
await waitFor(() => expect(rtlScreen.getByTestId("row-r1")).toBeTruthy());
|
|
1475
|
+
fireEvent.click(rtlScreen.getByTestId("facet-status-active"));
|
|
1476
|
+
|
|
1477
|
+
await waitFor(() => expect(rtlScreen.queryByTestId("row-r2")).toBeNull());
|
|
1478
|
+
expect(rtlScreen.getByTestId("row-r1")).toBeTruthy();
|
|
1479
|
+
const last = payloads[payloads.length - 1];
|
|
1480
|
+
expect(last?.["filter"]).toEqual({ field: "parentId", op: "eq", value: "order-1" });
|
|
1481
|
+
expect(last?.["filters"]).toEqual([{ field: "status", op: "in", value: ["active"] }]);
|
|
1482
|
+
});
|
|
1483
|
+
|
|
1484
|
+
test("a toolbarAction without params prefills the parent id under parentFilter.field, not parentParam/id", async () => {
|
|
1485
|
+
const { dispatcher } = parentFilterDispatcher(rows);
|
|
1486
|
+
const { nav, navigations, searchParams } = stubNav();
|
|
1487
|
+
const capturingDataTable: ComponentType<DataTableProps> = (props) => (
|
|
1488
|
+
<>
|
|
1489
|
+
{props.toolbarEnd}
|
|
1490
|
+
{testDataTable(props)}
|
|
1491
|
+
</>
|
|
1492
|
+
);
|
|
1493
|
+
const testButton = ({
|
|
1494
|
+
children,
|
|
1495
|
+
testId,
|
|
1496
|
+
onClick,
|
|
1497
|
+
}: {
|
|
1498
|
+
readonly children?: ReactNode;
|
|
1499
|
+
readonly testId?: string;
|
|
1500
|
+
readonly onClick?: () => void;
|
|
1501
|
+
}) => (
|
|
1502
|
+
<button type="button" data-testid={testId} onClick={() => onClick?.()}>
|
|
1503
|
+
{children}
|
|
1504
|
+
</button>
|
|
1505
|
+
);
|
|
1506
|
+
render(
|
|
1507
|
+
<LocaleProvider
|
|
1508
|
+
resolver={createStaticLocaleResolver({ locale: "en-US" })}
|
|
1509
|
+
fallbackBundles={[kumikoDefaultTranslations]}
|
|
1510
|
+
>
|
|
1511
|
+
<DispatcherProvider dispatcher={dispatcher}>
|
|
1512
|
+
<PrimitivesProvider
|
|
1513
|
+
value={{ ...testPrimitives(), DataTable: capturingDataTable, Button: testButton }}
|
|
1514
|
+
>
|
|
1515
|
+
<NavProvider value={nav}>
|
|
1516
|
+
<RelatedListSection
|
|
1517
|
+
section={{
|
|
1518
|
+
...section,
|
|
1519
|
+
toolbarActions: [
|
|
1520
|
+
{
|
|
1521
|
+
kind: "navigate",
|
|
1522
|
+
id: "create",
|
|
1523
|
+
label: "Add position",
|
|
1524
|
+
screen: "item-create",
|
|
1525
|
+
},
|
|
1526
|
+
],
|
|
1527
|
+
}}
|
|
1528
|
+
parentId="order-1"
|
|
1529
|
+
record={{ id: "order-1" }}
|
|
1530
|
+
featureName="orders"
|
|
1531
|
+
/>
|
|
1532
|
+
</NavProvider>
|
|
1533
|
+
</PrimitivesProvider>
|
|
1534
|
+
</DispatcherProvider>
|
|
1535
|
+
</LocaleProvider>,
|
|
1536
|
+
);
|
|
1537
|
+
|
|
1538
|
+
await waitFor(() => expect(rtlScreen.getByTestId("row-r1")).toBeTruthy());
|
|
1539
|
+
rtlScreen.getByTestId("render-list-toolbar-action-create").click();
|
|
1540
|
+
|
|
1541
|
+
await waitFor(() => expect(navigations).toHaveLength(1));
|
|
1542
|
+
expect(searchParams).toEqual([{ parentId: "order-1" }]);
|
|
1543
|
+
});
|
|
1544
|
+
});
|
|
1545
|
+
|
|
945
1546
|
// Same findEditScreenFor/buildDefaultEditRowAction resolution as
|
|
946
1547
|
// entityList/projectionList, driven by rowClick.entity and rowClick.idColumn.
|
|
947
1548
|
describe("RelatedListSection — default edit row action", () => {
|
|
@@ -972,7 +1573,12 @@ describe("RelatedListSection — default edit row action", () => {
|
|
|
972
1573
|
<UserRolesProvider roles={userRoles}>
|
|
973
1574
|
<PrimitivesProvider value={testPrimitives()}>
|
|
974
1575
|
<NavProvider value={nav}>
|
|
975
|
-
<RelatedListSection
|
|
1576
|
+
<RelatedListSection
|
|
1577
|
+
section={section}
|
|
1578
|
+
parentId="order-1"
|
|
1579
|
+
record={{ id: "order-1" }}
|
|
1580
|
+
featureName="orders"
|
|
1581
|
+
/>
|
|
976
1582
|
</NavProvider>
|
|
977
1583
|
</PrimitivesProvider>
|
|
978
1584
|
</UserRolesProvider>
|