@cosmicdrift/kumiko-renderer 0.327.0 → 0.328.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/dist/app/action-form-shim.d.ts +14 -0
- package/{src/app/action-form-shim.ts → dist/app/action-form-shim.js} +26 -44
- package/dist/app/app-features-context.d.ts +8 -0
- package/dist/app/app-features-context.js +16 -0
- package/dist/app/column-renderers.d.ts +30 -0
- package/dist/app/column-renderers.js +34 -0
- package/dist/app/config-edit-shim.d.ts +11 -0
- package/{src/app/config-edit-shim.ts → dist/app/config-edit-shim.js} +12 -25
- package/dist/app/content-editors.d.ts +38 -0
- package/dist/app/content-editors.js +26 -0
- package/dist/app/content-preview.d.ts +11 -0
- package/dist/app/content-preview.js +30 -0
- package/dist/app/custom-screens.d.ts +11 -0
- package/dist/app/custom-screens.js +20 -0
- package/dist/app/dashboard-body.d.ts +16 -0
- package/dist/app/dashboard-body.js +11 -0
- package/dist/app/extension-form-submit.d.ts +32 -0
- package/dist/app/extension-form-submit.js +73 -0
- package/dist/app/extension-sections.d.ts +72 -0
- package/dist/app/extension-sections.js +34 -0
- package/dist/app/feature-schema.d.ts +4 -0
- package/{src/app/feature-schema.ts → dist/app/feature-schema.js} +12 -22
- package/dist/app/form-schema.d.ts +4 -0
- package/dist/app/form-schema.js +85 -0
- package/dist/app/kumiko-screen.d.ts +43 -0
- package/dist/app/kumiko-screen.js +2206 -0
- package/dist/app/layout-fields.d.ts +3 -0
- package/dist/app/layout-fields.js +20 -0
- package/dist/app/list-facets.d.ts +48 -0
- package/dist/app/list-facets.js +185 -0
- package/dist/app/nav.d.ts +61 -0
- package/dist/app/nav.js +139 -0
- package/dist/app/projection-detail-shim.d.ts +8 -0
- package/dist/app/projection-detail-shim.js +71 -0
- package/dist/app/projection-list-shim.d.ts +13 -0
- package/{src/app/projection-list-shim.ts → dist/app/projection-list-shim.js} +26 -38
- package/dist/app/qn.d.ts +7 -0
- package/{src/app/qn.ts → dist/app/qn.js} +9 -11
- package/dist/app/qualify-screen-id.d.ts +2 -0
- package/{src/app/qualify-screen-id.ts → dist/app/qualify-screen-id.js} +5 -6
- package/dist/app/reference-facet-bridge.d.ts +10 -0
- package/dist/app/reference-facet-bridge.js +22 -0
- package/dist/app/return-to.d.ts +31 -0
- package/dist/app/return-to.js +188 -0
- package/dist/app/row-actions.d.ts +77 -0
- package/dist/app/row-actions.js +416 -0
- package/dist/app/screen-access.d.ts +8 -0
- package/dist/app/screen-access.js +48 -0
- package/dist/app/secret-mint-body.d.ts +10 -0
- package/dist/app/secret-mint-body.js +140 -0
- package/dist/app/secrets-edit-body.d.ts +8 -0
- package/dist/app/secrets-edit-body.js +76 -0
- package/dist/app/use-embedded-screen.d.ts +6 -0
- package/dist/app/use-embedded-screen.js +19 -0
- package/dist/app/variable-chips.d.ts +7 -0
- package/dist/app/variable-chips.js +16 -0
- package/dist/app/write-failed-error.d.ts +6 -0
- package/dist/app/write-failed-error.js +21 -0
- package/dist/components/embedded-list-field.d.ts +10 -0
- package/dist/components/embedded-list-field.js +259 -0
- package/dist/components/grid-cell-for-field.d.ts +25 -0
- package/dist/components/grid-cell-for-field.js +31 -0
- package/dist/components/multi-select-checkboxes.d.ts +14 -0
- package/dist/components/multi-select-checkboxes.js +36 -0
- package/dist/components/reference-create-dialog.d.ts +13 -0
- package/dist/components/reference-create-dialog.js +46 -0
- package/dist/components/related-list-section.d.ts +25 -0
- package/dist/components/related-list-section.js +273 -0
- package/dist/components/render-edit-action-button.d.ts +22 -0
- package/dist/components/render-edit-action-button.js +43 -0
- package/dist/components/render-edit-logic.d.ts +6 -0
- package/dist/components/render-edit-logic.js +63 -0
- package/dist/components/render-edit-types.d.ts +209 -0
- package/dist/components/render-edit-types.js +1 -0
- package/dist/components/render-edit.d.ts +6 -0
- package/dist/components/render-edit.js +1169 -0
- package/dist/components/render-field.d.ts +38 -0
- package/dist/components/render-field.js +544 -0
- package/dist/components/render-list.d.ts +117 -0
- package/dist/components/render-list.js +232 -0
- package/dist/components/write-form-section.d.ts +17 -0
- package/dist/components/write-form-section.js +104 -0
- package/dist/context/dispatcher-context.d.ts +10 -0
- package/{src/context/dispatcher-context.tsx → dist/context/dispatcher-context.js} +16 -29
- package/dist/context/draft-storage-context.d.ts +12 -0
- package/dist/context/draft-storage-context.js +18 -0
- package/dist/context/user-roles-context.d.ts +7 -0
- package/{src/context/user-roles-context.tsx → dist/context/user-roles-context.js} +7 -15
- package/dist/format-when.d.ts +1 -0
- package/{src/format-when.ts → dist/format-when.js} +7 -7
- package/dist/hooks/reference-limits.d.ts +3 -0
- package/{src/hooks/reference-limits.ts → dist/hooks/reference-limits.js} +0 -1
- package/dist/hooks/use-ai-text.d.ts +52 -0
- package/dist/hooks/use-ai-text.js +90 -0
- package/dist/hooks/use-disclosure.d.ts +7 -0
- package/dist/hooks/use-disclosure.js +8 -0
- package/dist/hooks/use-form.d.ts +7 -0
- package/dist/hooks/use-form.js +36 -0
- package/dist/hooks/use-list-url-state.d.ts +43 -0
- package/dist/hooks/use-list-url-state.js +131 -0
- package/dist/hooks/use-mutation.d.ts +9 -0
- package/dist/hooks/use-mutation.js +37 -0
- package/dist/hooks/use-query.d.ts +14 -0
- package/dist/hooks/use-query.js +118 -0
- package/dist/hooks/use-reference-lookup.d.ts +14 -0
- package/dist/hooks/use-reference-lookup.js +63 -0
- package/dist/hooks/use-store.d.ts +3 -0
- package/dist/hooks/use-store.js +38 -0
- package/dist/hooks/use-stream-handler.d.ts +14 -0
- package/dist/hooks/use-stream-handler.js +93 -0
- package/dist/i18n-defaults.d.ts +2 -0
- package/dist/i18n-defaults.js +201 -0
- package/dist/i18n.d.ts +64 -0
- package/dist/i18n.js +166 -0
- package/dist/index.d.ts +77 -0
- package/dist/index.js +55 -0
- package/dist/inside-drawer.d.ts +2 -0
- package/dist/inside-drawer.js +9 -0
- package/dist/page-header-slot.d.ts +2 -0
- package/dist/page-header-slot.js +9 -0
- package/dist/primitives.d.ts +1351 -0
- package/dist/primitives.js +73 -0
- package/dist/screen-fills-height.d.ts +3 -0
- package/dist/screen-fills-height.js +3 -0
- package/dist/share-links.d.ts +4 -0
- package/dist/share-links.js +11 -0
- package/dist/sort-by-accessor.d.ts +5 -0
- package/dist/sort-by-accessor.js +16 -0
- package/dist/sse/live-events.d.ts +23 -0
- package/dist/sse/live-events.js +28 -0
- package/dist/tokens.d.ts +83 -0
- package/dist/tokens.js +80 -0
- package/package.json +16 -9
- package/src/changes.json +52 -0
- package/src/__tests__/error-i18n-defaults.test.ts +0 -82
- package/src/__tests__/format-when.test.ts +0 -12
- package/src/__tests__/i18n.test.tsx +0 -283
- package/src/__tests__/merge-search-params-into-initial.test.ts +0 -525
- package/src/__tests__/qn.test.ts +0 -83
- package/src/__tests__/share-links.test.ts +0 -26
- package/src/__tests__/sort-by-accessor.test.ts +0 -48
- package/src/__tests__/stringify-nav-params.test.ts +0 -33
- package/src/__tests__/use-list-url-state.test.tsx +0 -250
- package/src/app/__tests__/action-form-field-labels.test.tsx +0 -92
- package/src/app/__tests__/action-form-shim.test.ts +0 -137
- package/src/app/__tests__/build-list-query-payload.test.ts +0 -107
- package/src/app/__tests__/config-edit-shim.test.ts +0 -57
- package/src/app/__tests__/content-editors.test.tsx +0 -128
- package/src/app/__tests__/content-preview.test.tsx +0 -184
- package/src/app/__tests__/entity-edit-create-search-params.test.tsx +0 -236
- package/src/app/__tests__/entity-edit-jsonb-embedded-submit.test.tsx +0 -239
- package/src/app/__tests__/entity-list-create-singleton.test.tsx +0 -128
- package/src/app/__tests__/entity-list-row-action-entity-target.test.tsx +0 -424
- package/src/app/__tests__/entity-list-row-action-icon.test.tsx +0 -248
- package/src/app/__tests__/entity-list-row-action-kumiko-actions-view.test.tsx +0 -162
- package/src/app/__tests__/entity-list-row-action-refetch.test.tsx +0 -401
- package/src/app/__tests__/entity-list-screen-padding.test.tsx +0 -111
- package/src/app/__tests__/feature-schema.test.ts +0 -40
- package/src/app/__tests__/form-schema.test.ts +0 -247
- package/src/app/__tests__/list-daterange-facet.test.ts +0 -165
- package/src/app/__tests__/list-filter-facets.test.tsx +0 -521
- package/src/app/__tests__/nav.test.ts +0 -198
- package/src/app/__tests__/projection-detail-actions.test.tsx +0 -719
- package/src/app/__tests__/projection-detail-reference-field.test.tsx +0 -165
- package/src/app/__tests__/projection-detail-shim.test.ts +0 -188
- package/src/app/__tests__/projection-detail-singleton.test.tsx +0 -191
- package/src/app/__tests__/projection-list-bad-shape.test.tsx +0 -152
- package/src/app/__tests__/projection-list-reference-column.test.tsx +0 -219
- package/src/app/__tests__/projection-list-search-sort.test.tsx +0 -212
- package/src/app/__tests__/qualified-names.test.ts +0 -37
- package/src/app/__tests__/return-to.test.ts +0 -396
- package/src/app/__tests__/row-actions-navigate-tab.test.ts +0 -223
- package/src/app/__tests__/row-default-edit-action.test.tsx +0 -324
- package/src/app/__tests__/screen-access-allows.test.ts +0 -34
- package/src/app/__tests__/secret-mint-body.test.tsx +0 -404
- package/src/app/__tests__/variable-chips.test.tsx +0 -70
- package/src/app/app-features-context.tsx +0 -25
- package/src/app/column-renderers.tsx +0 -68
- package/src/app/content-editors.tsx +0 -85
- package/src/app/content-preview.tsx +0 -67
- package/src/app/custom-screens.tsx +0 -29
- package/src/app/dashboard-body.tsx +0 -33
- package/src/app/extension-form-submit.tsx +0 -123
- package/src/app/extension-sections.tsx +0 -111
- package/src/app/form-schema.ts +0 -91
- package/src/app/kumiko-screen.tsx +0 -3614
- package/src/app/layout-fields.ts +0 -31
- package/src/app/list-facets.ts +0 -259
- package/src/app/nav.tsx +0 -258
- package/src/app/projection-detail-shim.ts +0 -85
- package/src/app/reference-facet-bridge.tsx +0 -58
- package/src/app/return-to.tsx +0 -234
- package/src/app/row-actions.ts +0 -639
- package/src/app/screen-access.ts +0 -63
- package/src/app/secret-mint-body.tsx +0 -259
- package/src/app/secrets-edit-body.tsx +0 -194
- package/src/app/use-embedded-screen.ts +0 -27
- package/src/app/variable-chips.tsx +0 -39
- package/src/app/write-failed-error.ts +0 -27
- package/src/components/__tests__/embedded-list-field.test.tsx +0 -531
- package/src/components/__tests__/multi-select-checkboxes.test.tsx +0 -162
- package/src/components/__tests__/related-list-section-empty-state.test.tsx +0 -143
- package/src/components/__tests__/related-list-section.test.tsx +0 -1669
- package/src/components/__tests__/render-edit-action-button.test.tsx +0 -205
- package/src/components/__tests__/render-edit-hidden-section.test.tsx +0 -128
- package/src/components/__tests__/render-edit-logic.test.ts +0 -314
- package/src/components/__tests__/render-edit-screen-description.test.tsx +0 -152
- package/src/components/__tests__/render-edit-submit-actions.test.tsx +0 -460
- package/src/components/__tests__/render-field-app-locale.test.tsx +0 -142
- package/src/components/__tests__/render-field-enum-option.test.tsx +0 -99
- package/src/components/__tests__/render-field-icon-derivation.test.tsx +0 -119
- package/src/components/__tests__/render-field-json-format.test.tsx +0 -164
- package/src/components/__tests__/render-field-money-embedded-list-consistency.test.tsx +0 -212
- package/src/components/__tests__/render-field-money-roundtrip.test.tsx +0 -217
- package/src/components/__tests__/render-field-number-grouping.test.tsx +0 -76
- package/src/components/__tests__/render-field-number-unit.test.tsx +0 -227
- package/src/components/__tests__/render-field-password-format.test.tsx +0 -101
- package/src/components/__tests__/render-field-select-display.test.tsx +0 -134
- package/src/components/__tests__/render-field-unit-format.test.tsx +0 -107
- package/src/components/__tests__/render-field-unsupported-types.test.tsx +0 -195
- package/src/components/__tests__/render-list-toolbar-icon-only.test.tsx +0 -146
- package/src/components/__tests__/write-form-section.test.tsx +0 -375
- package/src/components/embedded-list-field.tsx +0 -354
- package/src/components/grid-cell-for-field.tsx +0 -60
- package/src/components/multi-select-checkboxes.tsx +0 -93
- package/src/components/reference-create-dialog.tsx +0 -91
- package/src/components/related-list-section.tsx +0 -467
- package/src/components/render-edit-action-button.tsx +0 -106
- package/src/components/render-edit-logic.ts +0 -85
- package/src/components/render-edit-types.ts +0 -216
- package/src/components/render-edit.tsx +0 -1700
- package/src/components/render-field.tsx +0 -942
- package/src/components/render-list.tsx +0 -561
- package/src/components/write-form-section.tsx +0 -199
- package/src/context/draft-storage-context.tsx +0 -39
- package/src/hooks/__tests__/use-ai-text.test.tsx +0 -177
- package/src/hooks/__tests__/use-disclosure.test.tsx +0 -25
- package/src/hooks/__tests__/use-form.test.tsx +0 -65
- package/src/hooks/__tests__/use-mutation.test.tsx +0 -77
- package/src/hooks/__tests__/use-stream-handler.test.tsx +0 -107
- package/src/hooks/use-ai-text.ts +0 -172
- package/src/hooks/use-disclosure.ts +0 -20
- package/src/hooks/use-form.ts +0 -78
- package/src/hooks/use-list-url-state.ts +0 -171
- package/src/hooks/use-mutation.ts +0 -61
- package/src/hooks/use-query.ts +0 -132
- package/src/hooks/use-reference-lookup.ts +0 -76
- package/src/hooks/use-store.ts +0 -48
- package/src/hooks/use-stream-handler.ts +0 -134
- package/src/i18n-defaults.ts +0 -220
- package/src/i18n.tsx +0 -286
- package/src/index.ts +0 -272
- package/src/primitives.tsx +0 -1446
- package/src/share-links.ts +0 -12
- package/src/sort-by-accessor.ts +0 -20
- package/src/sse/live-events.tsx +0 -56
- package/src/tokens.tsx +0 -170
|
@@ -1,1669 +0,0 @@
|
|
|
1
|
-
import { beforeEach, describe, expect, spyOn, test } from "bun:test";
|
|
2
|
-
import type { EntityEditScreenDefinition, RowAction } from "@cosmicdrift/kumiko-framework/ui-types";
|
|
3
|
-
import type { Dispatcher, EditRelatedListSectionViewModel } from "@cosmicdrift/kumiko-headless";
|
|
4
|
-
import { act, fireEvent, render, screen as rtlScreen, waitFor } from "@testing-library/react";
|
|
5
|
-
import type { ComponentType, ReactNode } from "react";
|
|
6
|
-
import { AppFeaturesProvider } from "../../app/app-features-context";
|
|
7
|
-
import type { FeatureSchema } from "../../app/feature-schema";
|
|
8
|
-
import { type NavApi, NavProvider } from "../../app/nav";
|
|
9
|
-
import { resetDrawerActionWarningsForTests } from "../../app/row-actions";
|
|
10
|
-
import { DispatcherProvider } from "../../context/dispatcher-context";
|
|
11
|
-
import { UserRolesProvider } from "../../context/user-roles-context";
|
|
12
|
-
import { createStaticLocaleResolver, LocaleProvider } from "../../i18n";
|
|
13
|
-
import { kumikoDefaultTranslations } from "../../i18n-defaults";
|
|
14
|
-
import {
|
|
15
|
-
type CorePrimitives,
|
|
16
|
-
type DataTableProps,
|
|
17
|
-
type InputProps,
|
|
18
|
-
PrimitivesProvider,
|
|
19
|
-
type SectionProps,
|
|
20
|
-
} from "../../primitives";
|
|
21
|
-
import { RelatedListSection } from "../related-list-section";
|
|
22
|
-
|
|
23
|
-
const passChildren = ({ children }: { readonly children?: ReactNode }): ReactNode => children;
|
|
24
|
-
const noop = () => {};
|
|
25
|
-
|
|
26
|
-
const testSection: ComponentType<SectionProps> = ({ testId, children }) => (
|
|
27
|
-
<div data-testid={testId}>{children}</div>
|
|
28
|
-
);
|
|
29
|
-
|
|
30
|
-
// A minimal DataTable stub: renders one button per row per row-action,
|
|
31
|
-
// wired straight to the action's onTrigger — enough to prove
|
|
32
|
-
// RelatedListSection wires rowActions through to a real dispatch, without
|
|
33
|
-
// needing the production DataTable's sorting/paging/kebab-menu chrome.
|
|
34
|
-
// The isVisible filter mirrors the production DataTable's own row-action
|
|
35
|
-
// filter (renderer-web primitives/index.tsx) so per-row gating is exercised
|
|
36
|
-
// here rather than assumed.
|
|
37
|
-
const testDataTable: ComponentType<DataTableProps> = ({ rows, rowActions }) => (
|
|
38
|
-
<table>
|
|
39
|
-
<tbody>
|
|
40
|
-
{rows.map((row) => (
|
|
41
|
-
<tr key={row.id} data-testid={`row-${row.id}`}>
|
|
42
|
-
<td>
|
|
43
|
-
{(rowActions ?? [])
|
|
44
|
-
.filter((action) => action.isVisible === undefined || action.isVisible(row))
|
|
45
|
-
.map((action) => (
|
|
46
|
-
<button
|
|
47
|
-
key={action.id}
|
|
48
|
-
type="button"
|
|
49
|
-
data-testid={`action-${action.id}-${row.id}`}
|
|
50
|
-
onClick={() => void action.onTrigger(row)}
|
|
51
|
-
>
|
|
52
|
-
{action.label}
|
|
53
|
-
</button>
|
|
54
|
-
))}
|
|
55
|
-
</td>
|
|
56
|
-
</tr>
|
|
57
|
-
))}
|
|
58
|
-
</tbody>
|
|
59
|
-
</table>
|
|
60
|
-
);
|
|
61
|
-
|
|
62
|
-
const testInput = (props: InputProps) =>
|
|
63
|
-
props.kind === "text" ? (
|
|
64
|
-
<input
|
|
65
|
-
data-testid={props.id}
|
|
66
|
-
value={props.value}
|
|
67
|
-
onChange={(e) => props.onChange(e.target.value)}
|
|
68
|
-
/>
|
|
69
|
-
) : null;
|
|
70
|
-
|
|
71
|
-
function testPrimitives(): CorePrimitives {
|
|
72
|
-
return {
|
|
73
|
-
Button: noop,
|
|
74
|
-
Banner: ({ children, testId }: { children?: ReactNode; testId?: string }) => (
|
|
75
|
-
<div data-testid={testId}>{children}</div>
|
|
76
|
-
),
|
|
77
|
-
Field: passChildren,
|
|
78
|
-
Input: testInput,
|
|
79
|
-
DataTable: testDataTable,
|
|
80
|
-
Form: noop,
|
|
81
|
-
Section: testSection,
|
|
82
|
-
Card: passChildren,
|
|
83
|
-
Grid: passChildren,
|
|
84
|
-
GridCell: passChildren,
|
|
85
|
-
Text: noop,
|
|
86
|
-
Heading: noop,
|
|
87
|
-
Dialog: noop,
|
|
88
|
-
Modal: noop,
|
|
89
|
-
Lightbox: noop,
|
|
90
|
-
ConfigSourceBadge: noop,
|
|
91
|
-
ConfigCascadeView: noop,
|
|
92
|
-
Link: noop,
|
|
93
|
-
} as unknown as CorePrimitives;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
function stubDispatcher(
|
|
97
|
-
rows: readonly Record<string, unknown>[] = [{ id: "r1", name: "Alice" }],
|
|
98
|
-
nextCursor: string | null = null,
|
|
99
|
-
): {
|
|
100
|
-
dispatcher: Dispatcher;
|
|
101
|
-
writes: Array<{ type: string; payload: unknown }>;
|
|
102
|
-
queryCount: () => number;
|
|
103
|
-
} {
|
|
104
|
-
const writes: Array<{ type: string; payload: unknown }> = [];
|
|
105
|
-
let queryCalls = 0;
|
|
106
|
-
const dispatcher: Dispatcher = {
|
|
107
|
-
write: (async (type, payload) => {
|
|
108
|
-
writes.push({ type, payload });
|
|
109
|
-
return { isSuccess: true, data: null };
|
|
110
|
-
}) as Dispatcher["write"],
|
|
111
|
-
query: (async () => {
|
|
112
|
-
queryCalls += 1;
|
|
113
|
-
return { isSuccess: true, data: { rows, nextCursor } };
|
|
114
|
-
}) as Dispatcher["query"],
|
|
115
|
-
batch: (async () => ({ isSuccess: true, results: [] })) as Dispatcher["batch"],
|
|
116
|
-
statusStore: {
|
|
117
|
-
getState: () => "online",
|
|
118
|
-
subscribe: () => () => {},
|
|
119
|
-
} as unknown as Dispatcher["statusStore"],
|
|
120
|
-
async *stream() {},
|
|
121
|
-
pendingWrites: () => [],
|
|
122
|
-
pendingFiles: () => [],
|
|
123
|
-
};
|
|
124
|
-
return { dispatcher, writes, queryCount: () => queryCalls };
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
function stubNav(): {
|
|
128
|
-
nav: NavApi;
|
|
129
|
-
navigations: unknown[];
|
|
130
|
-
searchParams: Array<Record<string, string | null>>;
|
|
131
|
-
} {
|
|
132
|
-
const navigations: unknown[] = [];
|
|
133
|
-
const searchParams: Array<Record<string, string | null>> = [];
|
|
134
|
-
const nav: NavApi = {
|
|
135
|
-
route: undefined,
|
|
136
|
-
navigate: (target: unknown) => {
|
|
137
|
-
navigations.push(target);
|
|
138
|
-
},
|
|
139
|
-
replace: noop,
|
|
140
|
-
hrefFor: () => "#",
|
|
141
|
-
searchParams: {},
|
|
142
|
-
setSearchParams: (params: Record<string, string | null>) => {
|
|
143
|
-
searchParams.push(params);
|
|
144
|
-
},
|
|
145
|
-
} as unknown as NavApi;
|
|
146
|
-
return { nav, navigations, searchParams };
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
const rowActions: readonly RowAction[] = [
|
|
150
|
-
{
|
|
151
|
-
id: "resend",
|
|
152
|
-
label: "actions.resend",
|
|
153
|
-
handler: "orders:write:resend",
|
|
154
|
-
payload: { pick: ["id"] },
|
|
155
|
-
},
|
|
156
|
-
];
|
|
157
|
-
|
|
158
|
-
const historySection: EditRelatedListSectionViewModel = {
|
|
159
|
-
kind: "relatedList",
|
|
160
|
-
title: "History",
|
|
161
|
-
query: "orders:query:notifications:list",
|
|
162
|
-
columns: [{ field: "name" }],
|
|
163
|
-
rowActions,
|
|
164
|
-
};
|
|
165
|
-
|
|
166
|
-
function renderRelatedList(
|
|
167
|
-
dispatcher: Dispatcher,
|
|
168
|
-
section: EditRelatedListSectionViewModel = historySection,
|
|
169
|
-
nav: NavApi = stubNav().nav,
|
|
170
|
-
) {
|
|
171
|
-
return render(
|
|
172
|
-
<LocaleProvider
|
|
173
|
-
resolver={createStaticLocaleResolver({ locale: "en-US" })}
|
|
174
|
-
fallbackBundles={[kumikoDefaultTranslations]}
|
|
175
|
-
>
|
|
176
|
-
<DispatcherProvider dispatcher={dispatcher}>
|
|
177
|
-
<PrimitivesProvider value={testPrimitives()}>
|
|
178
|
-
<NavProvider value={nav}>
|
|
179
|
-
<RelatedListSection
|
|
180
|
-
section={section}
|
|
181
|
-
parentId="order-1"
|
|
182
|
-
record={{ id: "order-1" }}
|
|
183
|
-
featureName="orders"
|
|
184
|
-
/>
|
|
185
|
-
</NavProvider>
|
|
186
|
-
</PrimitivesProvider>
|
|
187
|
-
</DispatcherProvider>
|
|
188
|
-
</LocaleProvider>,
|
|
189
|
-
);
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
describe("RelatedListSection — tabs-mode card chrome (fw#2722)", () => {
|
|
193
|
-
test("hideTitle (tabs mode) renders the list without a Section wrapper, drops the table frame (the tab card frames it), and marks scrollBody", async () => {
|
|
194
|
-
const { dispatcher } = stubDispatcher();
|
|
195
|
-
let capturedChromeless: boolean | undefined;
|
|
196
|
-
let capturedScrollBody: boolean | undefined;
|
|
197
|
-
const capturingDataTable: ComponentType<DataTableProps> = (props) => {
|
|
198
|
-
capturedChromeless = props.chromeless;
|
|
199
|
-
capturedScrollBody = props.scrollBody;
|
|
200
|
-
return testDataTable(props);
|
|
201
|
-
};
|
|
202
|
-
render(
|
|
203
|
-
<LocaleProvider
|
|
204
|
-
resolver={createStaticLocaleResolver({ locale: "en-US" })}
|
|
205
|
-
fallbackBundles={[kumikoDefaultTranslations]}
|
|
206
|
-
>
|
|
207
|
-
<DispatcherProvider dispatcher={dispatcher}>
|
|
208
|
-
<PrimitivesProvider value={{ ...testPrimitives(), DataTable: capturingDataTable }}>
|
|
209
|
-
<NavProvider value={stubNav().nav}>
|
|
210
|
-
<RelatedListSection
|
|
211
|
-
section={historySection}
|
|
212
|
-
parentId="order-1"
|
|
213
|
-
record={{ id: "order-1" }}
|
|
214
|
-
featureName="orders"
|
|
215
|
-
hideTitle
|
|
216
|
-
/>
|
|
217
|
-
</NavProvider>
|
|
218
|
-
</PrimitivesProvider>
|
|
219
|
-
</DispatcherProvider>
|
|
220
|
-
</LocaleProvider>,
|
|
221
|
-
);
|
|
222
|
-
|
|
223
|
-
await waitFor(() => expect(rtlScreen.getByTestId("row-r1")).toBeTruthy());
|
|
224
|
-
expect(rtlScreen.queryByTestId(`related-list-${historySection.title}`)).toBeNull();
|
|
225
|
-
expect(capturedChromeless).toBe(true);
|
|
226
|
-
expect(capturedScrollBody).toBe(true);
|
|
227
|
-
});
|
|
228
|
-
|
|
229
|
-
test("without hideTitle (stacked mode), the same section keeps its Section wrapper and an un-chromeless, unbounded-height table", async () => {
|
|
230
|
-
const { dispatcher } = stubDispatcher();
|
|
231
|
-
let capturedChromeless: boolean | undefined;
|
|
232
|
-
let capturedScrollBody: boolean | undefined;
|
|
233
|
-
const capturingDataTable: ComponentType<DataTableProps> = (props) => {
|
|
234
|
-
capturedChromeless = props.chromeless;
|
|
235
|
-
capturedScrollBody = props.scrollBody;
|
|
236
|
-
return testDataTable(props);
|
|
237
|
-
};
|
|
238
|
-
render(
|
|
239
|
-
<LocaleProvider
|
|
240
|
-
resolver={createStaticLocaleResolver({ locale: "en-US" })}
|
|
241
|
-
fallbackBundles={[kumikoDefaultTranslations]}
|
|
242
|
-
>
|
|
243
|
-
<DispatcherProvider dispatcher={dispatcher}>
|
|
244
|
-
<PrimitivesProvider value={{ ...testPrimitives(), DataTable: capturingDataTable }}>
|
|
245
|
-
<NavProvider value={stubNav().nav}>
|
|
246
|
-
<RelatedListSection
|
|
247
|
-
section={historySection}
|
|
248
|
-
parentId="order-1"
|
|
249
|
-
record={{ id: "order-1" }}
|
|
250
|
-
featureName="orders"
|
|
251
|
-
/>
|
|
252
|
-
</NavProvider>
|
|
253
|
-
</PrimitivesProvider>
|
|
254
|
-
</DispatcherProvider>
|
|
255
|
-
</LocaleProvider>,
|
|
256
|
-
);
|
|
257
|
-
|
|
258
|
-
await waitFor(() => expect(rtlScreen.getByTestId("row-r1")).toBeTruthy());
|
|
259
|
-
expect(rtlScreen.getByTestId(`related-list-${historySection.title}`)).toBeTruthy();
|
|
260
|
-
expect(capturedChromeless).toBeUndefined();
|
|
261
|
-
expect(capturedScrollBody).toBeUndefined();
|
|
262
|
-
});
|
|
263
|
-
});
|
|
264
|
-
|
|
265
|
-
describe("RelatedListSection — list-screen toolbar in tabs mode", () => {
|
|
266
|
-
test("hideTitle (tabs mode) still surfaces search, facets and a toolbarActions button", async () => {
|
|
267
|
-
const { dispatcher } = stubDispatcher();
|
|
268
|
-
let capturedFilterFacets: DataTableProps["filterFacets"];
|
|
269
|
-
const capturingDataTable: ComponentType<DataTableProps> = (props) => {
|
|
270
|
-
capturedFilterFacets = props.filterFacets;
|
|
271
|
-
return (
|
|
272
|
-
<>
|
|
273
|
-
{props.toolbarEnd}
|
|
274
|
-
{testDataTable(props)}
|
|
275
|
-
</>
|
|
276
|
-
);
|
|
277
|
-
};
|
|
278
|
-
const testButton = ({
|
|
279
|
-
children,
|
|
280
|
-
testId,
|
|
281
|
-
onClick,
|
|
282
|
-
}: {
|
|
283
|
-
readonly children?: ReactNode;
|
|
284
|
-
readonly testId?: string;
|
|
285
|
-
readonly onClick?: () => void;
|
|
286
|
-
}) => (
|
|
287
|
-
<button type="button" data-testid={testId} onClick={() => onClick?.()}>
|
|
288
|
-
{children}
|
|
289
|
-
</button>
|
|
290
|
-
);
|
|
291
|
-
const section: EditRelatedListSectionViewModel = {
|
|
292
|
-
...historySection,
|
|
293
|
-
searchable: true,
|
|
294
|
-
facets: [
|
|
295
|
-
{
|
|
296
|
-
field: "name",
|
|
297
|
-
type: "select",
|
|
298
|
-
label: "Name",
|
|
299
|
-
options: [{ value: "Alice", label: "Alice" }],
|
|
300
|
-
},
|
|
301
|
-
],
|
|
302
|
-
toolbarActions: [
|
|
303
|
-
{
|
|
304
|
-
kind: "navigate",
|
|
305
|
-
id: "create",
|
|
306
|
-
label: "New notification",
|
|
307
|
-
screen: "notification-create",
|
|
308
|
-
},
|
|
309
|
-
],
|
|
310
|
-
};
|
|
311
|
-
render(
|
|
312
|
-
<LocaleProvider
|
|
313
|
-
resolver={createStaticLocaleResolver({ locale: "en-US" })}
|
|
314
|
-
fallbackBundles={[kumikoDefaultTranslations]}
|
|
315
|
-
>
|
|
316
|
-
<DispatcherProvider dispatcher={dispatcher}>
|
|
317
|
-
<PrimitivesProvider
|
|
318
|
-
value={{ ...testPrimitives(), DataTable: capturingDataTable, Button: testButton }}
|
|
319
|
-
>
|
|
320
|
-
<NavProvider value={stubNav().nav}>
|
|
321
|
-
<RelatedListSection
|
|
322
|
-
section={section}
|
|
323
|
-
parentId="order-1"
|
|
324
|
-
record={{ id: "order-1" }}
|
|
325
|
-
featureName="orders"
|
|
326
|
-
hideTitle
|
|
327
|
-
/>
|
|
328
|
-
</NavProvider>
|
|
329
|
-
</PrimitivesProvider>
|
|
330
|
-
</DispatcherProvider>
|
|
331
|
-
</LocaleProvider>,
|
|
332
|
-
);
|
|
333
|
-
|
|
334
|
-
await waitFor(() => expect(rtlScreen.getByTestId("row-r1")).toBeTruthy());
|
|
335
|
-
expect(capturedFilterFacets?.length).toBe(1);
|
|
336
|
-
expect(rtlScreen.getByTestId("render-list-toolbar-action-create")).toBeTruthy();
|
|
337
|
-
});
|
|
338
|
-
|
|
339
|
-
test("clicking a toolbarAction navigates to its screen and prefills the parent id under parentParam", async () => {
|
|
340
|
-
const { dispatcher } = stubDispatcher();
|
|
341
|
-
const { nav, navigations, searchParams } = stubNav();
|
|
342
|
-
const capturingDataTable: ComponentType<DataTableProps> = (props) => (
|
|
343
|
-
<>
|
|
344
|
-
{props.toolbarEnd}
|
|
345
|
-
{testDataTable(props)}
|
|
346
|
-
</>
|
|
347
|
-
);
|
|
348
|
-
const testButton = ({
|
|
349
|
-
children,
|
|
350
|
-
testId,
|
|
351
|
-
onClick,
|
|
352
|
-
}: {
|
|
353
|
-
readonly children?: ReactNode;
|
|
354
|
-
readonly testId?: string;
|
|
355
|
-
readonly onClick?: () => void;
|
|
356
|
-
}) => (
|
|
357
|
-
<button type="button" data-testid={testId} onClick={() => onClick?.()}>
|
|
358
|
-
{children}
|
|
359
|
-
</button>
|
|
360
|
-
);
|
|
361
|
-
const section: EditRelatedListSectionViewModel = {
|
|
362
|
-
...historySection,
|
|
363
|
-
parentParam: "leaseId",
|
|
364
|
-
toolbarActions: [
|
|
365
|
-
{ kind: "navigate", id: "create", label: "Add position", screen: "position-create" },
|
|
366
|
-
],
|
|
367
|
-
};
|
|
368
|
-
render(
|
|
369
|
-
<LocaleProvider
|
|
370
|
-
resolver={createStaticLocaleResolver({ locale: "en-US" })}
|
|
371
|
-
fallbackBundles={[kumikoDefaultTranslations]}
|
|
372
|
-
>
|
|
373
|
-
<DispatcherProvider dispatcher={dispatcher}>
|
|
374
|
-
<PrimitivesProvider
|
|
375
|
-
value={{ ...testPrimitives(), DataTable: capturingDataTable, Button: testButton }}
|
|
376
|
-
>
|
|
377
|
-
<NavProvider value={nav}>
|
|
378
|
-
<RelatedListSection
|
|
379
|
-
section={section}
|
|
380
|
-
parentId="lease-9"
|
|
381
|
-
record={{ id: "lease-9" }}
|
|
382
|
-
featureName="leases"
|
|
383
|
-
/>
|
|
384
|
-
</NavProvider>
|
|
385
|
-
</PrimitivesProvider>
|
|
386
|
-
</DispatcherProvider>
|
|
387
|
-
</LocaleProvider>,
|
|
388
|
-
);
|
|
389
|
-
|
|
390
|
-
await waitFor(() => expect(rtlScreen.getByTestId("row-r1")).toBeTruthy());
|
|
391
|
-
rtlScreen.getByTestId("render-list-toolbar-action-create").click();
|
|
392
|
-
|
|
393
|
-
await waitFor(() => expect(navigations).toHaveLength(1));
|
|
394
|
-
expect(navigations[0]).toEqual({ screenId: "position-create" });
|
|
395
|
-
expect(searchParams).toEqual([{ leaseId: "lease-9" }]);
|
|
396
|
-
});
|
|
397
|
-
|
|
398
|
-
test("a toolbarAction with a visible condition that the parent record fails is not rendered", async () => {
|
|
399
|
-
const { dispatcher } = stubDispatcher();
|
|
400
|
-
const { nav } = stubNav();
|
|
401
|
-
const capturingDataTable: ComponentType<DataTableProps> = (props) => (
|
|
402
|
-
<>
|
|
403
|
-
{props.toolbarEnd}
|
|
404
|
-
{testDataTable(props)}
|
|
405
|
-
</>
|
|
406
|
-
);
|
|
407
|
-
const testButton = ({
|
|
408
|
-
children,
|
|
409
|
-
testId,
|
|
410
|
-
}: {
|
|
411
|
-
readonly children?: ReactNode;
|
|
412
|
-
readonly testId?: string;
|
|
413
|
-
}) => (
|
|
414
|
-
<button type="button" data-testid={testId}>
|
|
415
|
-
{children}
|
|
416
|
-
</button>
|
|
417
|
-
);
|
|
418
|
-
const section: EditRelatedListSectionViewModel = {
|
|
419
|
-
...historySection,
|
|
420
|
-
parentParam: "leaseId",
|
|
421
|
-
toolbarActions: [
|
|
422
|
-
{
|
|
423
|
-
kind: "navigate",
|
|
424
|
-
id: "create",
|
|
425
|
-
label: "Add position",
|
|
426
|
-
screen: "position-create",
|
|
427
|
-
visible: { field: "status", eq: "active" },
|
|
428
|
-
},
|
|
429
|
-
],
|
|
430
|
-
};
|
|
431
|
-
render(
|
|
432
|
-
<LocaleProvider
|
|
433
|
-
resolver={createStaticLocaleResolver({ locale: "en-US" })}
|
|
434
|
-
fallbackBundles={[kumikoDefaultTranslations]}
|
|
435
|
-
>
|
|
436
|
-
<DispatcherProvider dispatcher={dispatcher}>
|
|
437
|
-
<PrimitivesProvider
|
|
438
|
-
value={{ ...testPrimitives(), DataTable: capturingDataTable, Button: testButton }}
|
|
439
|
-
>
|
|
440
|
-
<NavProvider value={nav}>
|
|
441
|
-
<RelatedListSection
|
|
442
|
-
section={section}
|
|
443
|
-
parentId="lease-9"
|
|
444
|
-
record={{ id: "lease-9", status: "closed" }}
|
|
445
|
-
featureName="leases"
|
|
446
|
-
/>
|
|
447
|
-
</NavProvider>
|
|
448
|
-
</PrimitivesProvider>
|
|
449
|
-
</DispatcherProvider>
|
|
450
|
-
</LocaleProvider>,
|
|
451
|
-
);
|
|
452
|
-
|
|
453
|
-
await waitFor(() => expect(rtlScreen.getByTestId("row-r1")).toBeTruthy());
|
|
454
|
-
expect(rtlScreen.queryByTestId("render-list-toolbar-action-create")).toBeNull();
|
|
455
|
-
});
|
|
456
|
-
|
|
457
|
-
test("a toolbarAction with a visible condition the parent record satisfies is rendered", async () => {
|
|
458
|
-
const { dispatcher } = stubDispatcher();
|
|
459
|
-
const { nav } = stubNav();
|
|
460
|
-
const capturingDataTable: ComponentType<DataTableProps> = (props) => (
|
|
461
|
-
<>
|
|
462
|
-
{props.toolbarEnd}
|
|
463
|
-
{testDataTable(props)}
|
|
464
|
-
</>
|
|
465
|
-
);
|
|
466
|
-
const testButton = ({
|
|
467
|
-
children,
|
|
468
|
-
testId,
|
|
469
|
-
}: {
|
|
470
|
-
readonly children?: ReactNode;
|
|
471
|
-
readonly testId?: string;
|
|
472
|
-
}) => (
|
|
473
|
-
<button type="button" data-testid={testId}>
|
|
474
|
-
{children}
|
|
475
|
-
</button>
|
|
476
|
-
);
|
|
477
|
-
const section: EditRelatedListSectionViewModel = {
|
|
478
|
-
...historySection,
|
|
479
|
-
parentParam: "leaseId",
|
|
480
|
-
toolbarActions: [
|
|
481
|
-
{
|
|
482
|
-
kind: "navigate",
|
|
483
|
-
id: "create",
|
|
484
|
-
label: "Add position",
|
|
485
|
-
screen: "position-create",
|
|
486
|
-
visible: { field: "status", eq: "active" },
|
|
487
|
-
},
|
|
488
|
-
],
|
|
489
|
-
};
|
|
490
|
-
render(
|
|
491
|
-
<LocaleProvider
|
|
492
|
-
resolver={createStaticLocaleResolver({ locale: "en-US" })}
|
|
493
|
-
fallbackBundles={[kumikoDefaultTranslations]}
|
|
494
|
-
>
|
|
495
|
-
<DispatcherProvider dispatcher={dispatcher}>
|
|
496
|
-
<PrimitivesProvider
|
|
497
|
-
value={{ ...testPrimitives(), DataTable: capturingDataTable, Button: testButton }}
|
|
498
|
-
>
|
|
499
|
-
<NavProvider value={nav}>
|
|
500
|
-
<RelatedListSection
|
|
501
|
-
section={section}
|
|
502
|
-
parentId="lease-9"
|
|
503
|
-
record={{ id: "lease-9", status: "active" }}
|
|
504
|
-
featureName="leases"
|
|
505
|
-
/>
|
|
506
|
-
</NavProvider>
|
|
507
|
-
</PrimitivesProvider>
|
|
508
|
-
</DispatcherProvider>
|
|
509
|
-
</LocaleProvider>,
|
|
510
|
-
);
|
|
511
|
-
|
|
512
|
-
await waitFor(() =>
|
|
513
|
-
expect(rtlScreen.getByTestId("render-list-toolbar-action-create")).toBeTruthy(),
|
|
514
|
-
);
|
|
515
|
-
});
|
|
516
|
-
|
|
517
|
-
test("a toolbarAction's params extractor maps the parent record and replaces the implicit parentParam prefill", async () => {
|
|
518
|
-
const { dispatcher } = stubDispatcher();
|
|
519
|
-
const { nav, navigations, searchParams } = stubNav();
|
|
520
|
-
const capturingDataTable: ComponentType<DataTableProps> = (props) => (
|
|
521
|
-
<>
|
|
522
|
-
{props.toolbarEnd}
|
|
523
|
-
{testDataTable(props)}
|
|
524
|
-
</>
|
|
525
|
-
);
|
|
526
|
-
const testButton = ({
|
|
527
|
-
children,
|
|
528
|
-
testId,
|
|
529
|
-
onClick,
|
|
530
|
-
}: {
|
|
531
|
-
readonly children?: ReactNode;
|
|
532
|
-
readonly testId?: string;
|
|
533
|
-
readonly onClick?: () => void;
|
|
534
|
-
}) => (
|
|
535
|
-
<button type="button" data-testid={testId} onClick={() => onClick?.()}>
|
|
536
|
-
{children}
|
|
537
|
-
</button>
|
|
538
|
-
);
|
|
539
|
-
const section: EditRelatedListSectionViewModel = {
|
|
540
|
-
...historySection,
|
|
541
|
-
// Default parentParam ("id") would prefill `{ id: "lease-9" }` — the
|
|
542
|
-
// declared params extractor below must produce `{ leaseId: ... }`
|
|
543
|
-
// instead, proving params replaces rather than merges with it.
|
|
544
|
-
toolbarActions: [
|
|
545
|
-
{
|
|
546
|
-
kind: "navigate",
|
|
547
|
-
id: "create",
|
|
548
|
-
label: "Add position",
|
|
549
|
-
screen: "position-create",
|
|
550
|
-
params: { map: { leaseId: "id" } },
|
|
551
|
-
},
|
|
552
|
-
],
|
|
553
|
-
};
|
|
554
|
-
render(
|
|
555
|
-
<LocaleProvider
|
|
556
|
-
resolver={createStaticLocaleResolver({ locale: "en-US" })}
|
|
557
|
-
fallbackBundles={[kumikoDefaultTranslations]}
|
|
558
|
-
>
|
|
559
|
-
<DispatcherProvider dispatcher={dispatcher}>
|
|
560
|
-
<PrimitivesProvider
|
|
561
|
-
value={{ ...testPrimitives(), DataTable: capturingDataTable, Button: testButton }}
|
|
562
|
-
>
|
|
563
|
-
<NavProvider value={nav}>
|
|
564
|
-
<RelatedListSection
|
|
565
|
-
section={section}
|
|
566
|
-
parentId="lease-9"
|
|
567
|
-
record={{ id: "lease-9" }}
|
|
568
|
-
featureName="leases"
|
|
569
|
-
/>
|
|
570
|
-
</NavProvider>
|
|
571
|
-
</PrimitivesProvider>
|
|
572
|
-
</DispatcherProvider>
|
|
573
|
-
</LocaleProvider>,
|
|
574
|
-
);
|
|
575
|
-
|
|
576
|
-
await waitFor(() => expect(rtlScreen.getByTestId("row-r1")).toBeTruthy());
|
|
577
|
-
rtlScreen.getByTestId("render-list-toolbar-action-create").click();
|
|
578
|
-
|
|
579
|
-
await waitFor(() => expect(navigations).toHaveLength(1));
|
|
580
|
-
expect(searchParams).toEqual([{ leaseId: "lease-9" }]);
|
|
581
|
-
});
|
|
582
|
-
|
|
583
|
-
test("a reference facet loads its options from the referenced entity's list query, and selecting one sends an id filter", async () => {
|
|
584
|
-
const calls: Array<{ type: string; payload: unknown }> = [];
|
|
585
|
-
const dispatcher: Dispatcher = {
|
|
586
|
-
write: (async () => ({ isSuccess: true, data: null })) as Dispatcher["write"],
|
|
587
|
-
query: (async (type: string, payload: unknown) => {
|
|
588
|
-
calls.push({ type, payload });
|
|
589
|
-
if (type === "orders:query:tenant:list") {
|
|
590
|
-
return {
|
|
591
|
-
isSuccess: true,
|
|
592
|
-
data: {
|
|
593
|
-
rows: [{ id: "t1", name: "Acme" }],
|
|
594
|
-
nextCursor: null,
|
|
595
|
-
},
|
|
596
|
-
};
|
|
597
|
-
}
|
|
598
|
-
return { isSuccess: true, data: { rows: [{ id: "r1", name: "Alice" }], nextCursor: null } };
|
|
599
|
-
}) as Dispatcher["query"],
|
|
600
|
-
batch: (async () => ({ isSuccess: true, results: [] })) as Dispatcher["batch"],
|
|
601
|
-
statusStore: {
|
|
602
|
-
getState: () => "online",
|
|
603
|
-
subscribe: () => () => {},
|
|
604
|
-
} as unknown as Dispatcher["statusStore"],
|
|
605
|
-
async *stream() {},
|
|
606
|
-
pendingWrites: () => [],
|
|
607
|
-
pendingFiles: () => [],
|
|
608
|
-
};
|
|
609
|
-
let capturedFilterFacets: DataTableProps["filterFacets"];
|
|
610
|
-
let capturedOnFilterChange: DataTableProps["onFilterChange"];
|
|
611
|
-
const capturingDataTable: ComponentType<DataTableProps> = (props) => {
|
|
612
|
-
capturedFilterFacets = props.filterFacets;
|
|
613
|
-
capturedOnFilterChange = props.onFilterChange;
|
|
614
|
-
return testDataTable(props);
|
|
615
|
-
};
|
|
616
|
-
const section: EditRelatedListSectionViewModel = {
|
|
617
|
-
...historySection,
|
|
618
|
-
facets: [
|
|
619
|
-
{ field: "name", type: "reference", label: "Tenant", entity: "tenant", labelField: "name" },
|
|
620
|
-
],
|
|
621
|
-
};
|
|
622
|
-
render(
|
|
623
|
-
<LocaleProvider
|
|
624
|
-
resolver={createStaticLocaleResolver({ locale: "en-US" })}
|
|
625
|
-
fallbackBundles={[kumikoDefaultTranslations]}
|
|
626
|
-
>
|
|
627
|
-
<DispatcherProvider dispatcher={dispatcher}>
|
|
628
|
-
<PrimitivesProvider value={{ ...testPrimitives(), DataTable: capturingDataTable }}>
|
|
629
|
-
<NavProvider value={stubNav().nav}>
|
|
630
|
-
<RelatedListSection
|
|
631
|
-
section={section}
|
|
632
|
-
parentId="order-1"
|
|
633
|
-
record={{ id: "order-1" }}
|
|
634
|
-
featureName="orders"
|
|
635
|
-
hideTitle
|
|
636
|
-
/>
|
|
637
|
-
</NavProvider>
|
|
638
|
-
</PrimitivesProvider>
|
|
639
|
-
</DispatcherProvider>
|
|
640
|
-
</LocaleProvider>,
|
|
641
|
-
);
|
|
642
|
-
|
|
643
|
-
await waitFor(() => expect(rtlScreen.getByTestId("row-r1")).toBeTruthy());
|
|
644
|
-
await waitFor(() => expect(capturedFilterFacets?.[0]?.options).toHaveLength(1));
|
|
645
|
-
expect(capturedFilterFacets).toEqual([
|
|
646
|
-
{ field: "name", label: "Tenant", options: [{ value: "t1", label: "Acme" }] },
|
|
647
|
-
]);
|
|
648
|
-
|
|
649
|
-
const countBeforeSelect = calls.length;
|
|
650
|
-
await act(async () => {
|
|
651
|
-
capturedOnFilterChange?.("name", ["t1"]);
|
|
652
|
-
});
|
|
653
|
-
await waitFor(() => expect(calls.length).toBeGreaterThan(countBeforeSelect));
|
|
654
|
-
const lastListCall = [...calls]
|
|
655
|
-
.reverse()
|
|
656
|
-
.find((c) => c.type === "orders:query:notifications:list");
|
|
657
|
-
expect(lastListCall?.payload).toMatchObject({
|
|
658
|
-
filters: [{ field: "name", op: "in", value: ["t1"] }],
|
|
659
|
-
});
|
|
660
|
-
});
|
|
661
|
-
});
|
|
662
|
-
|
|
663
|
-
describe("RelatedListSection — rowActions", () => {
|
|
664
|
-
test("clicking a row action dispatches through the configured write-handler with the extracted payload, then refetches", async () => {
|
|
665
|
-
const { dispatcher, writes, queryCount } = stubDispatcher();
|
|
666
|
-
renderRelatedList(dispatcher);
|
|
667
|
-
|
|
668
|
-
await waitFor(() => expect(rtlScreen.getByTestId("row-r1")).toBeTruthy());
|
|
669
|
-
rtlScreen.getByTestId("action-resend-r1").click();
|
|
670
|
-
|
|
671
|
-
await waitFor(() => expect(writes).toHaveLength(1));
|
|
672
|
-
expect(writes[0]).toEqual({ type: "orders:write:resend", payload: { id: "r1" } });
|
|
673
|
-
await waitFor(() => expect(queryCount()).toBe(2));
|
|
674
|
-
});
|
|
675
|
-
|
|
676
|
-
test("a navigate row action targets its screen and carries the clicked row's own values as search params", async () => {
|
|
677
|
-
const { dispatcher } = stubDispatcher([{ id: "item-7", name: "Rent 2024", amount: 1200 }]);
|
|
678
|
-
const { nav, navigations, searchParams } = stubNav();
|
|
679
|
-
renderRelatedList(
|
|
680
|
-
dispatcher,
|
|
681
|
-
{
|
|
682
|
-
kind: "relatedList",
|
|
683
|
-
title: "Positions",
|
|
684
|
-
query: "lease:query:items:list",
|
|
685
|
-
columns: [{ field: "name" }],
|
|
686
|
-
rowActions: [
|
|
687
|
-
{
|
|
688
|
-
kind: "navigate",
|
|
689
|
-
id: "adjust-rent",
|
|
690
|
-
label: "actions.adjustRent",
|
|
691
|
-
screen: "adjust-rent-form",
|
|
692
|
-
params: { map: { itemId: "id", currentAmount: "amount" } },
|
|
693
|
-
},
|
|
694
|
-
],
|
|
695
|
-
},
|
|
696
|
-
nav,
|
|
697
|
-
);
|
|
698
|
-
|
|
699
|
-
await waitFor(() => expect(rtlScreen.getByTestId("row-item-7")).toBeTruthy());
|
|
700
|
-
rtlScreen.getByTestId("action-adjust-rent-item-7").click();
|
|
701
|
-
|
|
702
|
-
await waitFor(() => expect(navigations).toHaveLength(1));
|
|
703
|
-
expect(navigations[0]).toEqual({ screenId: "adjust-rent-form" });
|
|
704
|
-
expect(searchParams).toEqual([{ itemId: "item-7", currentAmount: "1200" }]);
|
|
705
|
-
});
|
|
706
|
-
|
|
707
|
-
test("a row action with a visible condition renders only on the rows that satisfy it", async () => {
|
|
708
|
-
const { dispatcher } = stubDispatcher([
|
|
709
|
-
{ id: "active-1", name: "Running", status: "active" },
|
|
710
|
-
{ id: "ended-1", name: "Closed", status: "ended" },
|
|
711
|
-
]);
|
|
712
|
-
renderRelatedList(dispatcher, {
|
|
713
|
-
kind: "relatedList",
|
|
714
|
-
title: "Positions",
|
|
715
|
-
query: "lease:query:items:list",
|
|
716
|
-
columns: [{ field: "name" }],
|
|
717
|
-
rowActions: [
|
|
718
|
-
{
|
|
719
|
-
id: "end-item",
|
|
720
|
-
label: "actions.endItem",
|
|
721
|
-
handler: "lease:write:end-item",
|
|
722
|
-
payload: { pick: ["id"] },
|
|
723
|
-
visible: { field: "status", eq: "active" },
|
|
724
|
-
},
|
|
725
|
-
],
|
|
726
|
-
});
|
|
727
|
-
|
|
728
|
-
await waitFor(() => expect(rtlScreen.getByTestId("row-ended-1")).toBeTruthy());
|
|
729
|
-
expect(rtlScreen.getByTestId("action-end-item-active-1")).toBeTruthy();
|
|
730
|
-
expect(rtlScreen.queryByTestId("action-end-item-ended-1")).toBeNull();
|
|
731
|
-
});
|
|
732
|
-
});
|
|
733
|
-
|
|
734
|
-
// RelatedListSection never renders the Drawer itself (require-cycle with
|
|
735
|
-
// kumiko-screen.tsx, which owns the shared Drawer state) — it only forwards
|
|
736
|
-
// a drawer-kind rowAction to the injected `onOpenDrawer` callback. This
|
|
737
|
-
// pins that hand-off, not the Drawer UI itself (covered by kumiko-screen's
|
|
738
|
-
// own tests).
|
|
739
|
-
describe("RelatedListSection — rowActions drawer-kind (fw#2710)", () => {
|
|
740
|
-
beforeEach(() => {
|
|
741
|
-
resetDrawerActionWarningsForTests();
|
|
742
|
-
});
|
|
743
|
-
|
|
744
|
-
test("clicking a drawer rowAction calls onOpenDrawer with the action and the row's extracted values", async () => {
|
|
745
|
-
const { dispatcher } = stubDispatcher([{ id: "item-7", name: "Rent 2024", amount: 1200 }]);
|
|
746
|
-
const openDrawerCalls: unknown[][] = [];
|
|
747
|
-
render(
|
|
748
|
-
<LocaleProvider
|
|
749
|
-
resolver={createStaticLocaleResolver({ locale: "en-US" })}
|
|
750
|
-
fallbackBundles={[kumikoDefaultTranslations]}
|
|
751
|
-
>
|
|
752
|
-
<DispatcherProvider dispatcher={dispatcher}>
|
|
753
|
-
<PrimitivesProvider value={testPrimitives()}>
|
|
754
|
-
<NavProvider value={stubNav().nav}>
|
|
755
|
-
<RelatedListSection
|
|
756
|
-
section={{
|
|
757
|
-
kind: "relatedList",
|
|
758
|
-
title: "Positions",
|
|
759
|
-
query: "lease:query:items:list",
|
|
760
|
-
columns: [{ field: "name" }],
|
|
761
|
-
rowActions: [
|
|
762
|
-
{
|
|
763
|
-
kind: "drawer",
|
|
764
|
-
id: "adjust-rent",
|
|
765
|
-
label: "actions.adjustRent",
|
|
766
|
-
screen: "adjust-rent-form",
|
|
767
|
-
params: { map: { itemId: "id", currentAmount: "amount" } },
|
|
768
|
-
},
|
|
769
|
-
],
|
|
770
|
-
}}
|
|
771
|
-
parentId="order-1"
|
|
772
|
-
record={{ id: "order-1" }}
|
|
773
|
-
featureName="orders"
|
|
774
|
-
onOpenDrawer={(action, initialValues) =>
|
|
775
|
-
openDrawerCalls.push([action, initialValues])
|
|
776
|
-
}
|
|
777
|
-
/>
|
|
778
|
-
</NavProvider>
|
|
779
|
-
</PrimitivesProvider>
|
|
780
|
-
</DispatcherProvider>
|
|
781
|
-
</LocaleProvider>,
|
|
782
|
-
);
|
|
783
|
-
|
|
784
|
-
await waitFor(() => expect(rtlScreen.getByTestId("row-item-7")).toBeTruthy());
|
|
785
|
-
rtlScreen.getByTestId("action-adjust-rent-item-7").click();
|
|
786
|
-
|
|
787
|
-
await waitFor(() => expect(openDrawerCalls).toHaveLength(1));
|
|
788
|
-
const call = openDrawerCalls[0];
|
|
789
|
-
if (call === undefined) throw new Error("expected onOpenDrawer to have been called");
|
|
790
|
-
expect((call[0] as { readonly id: string }).id).toBe("adjust-rent");
|
|
791
|
-
expect(call[1]).toEqual({ itemId: "item-7", currentAmount: 1200 });
|
|
792
|
-
});
|
|
793
|
-
|
|
794
|
-
test("a drawer rowAction is dropped (not rendered) when no onOpenDrawer is wired", async () => {
|
|
795
|
-
const { dispatcher } = stubDispatcher([{ id: "item-7", name: "Rent 2024", amount: 1200 }]);
|
|
796
|
-
renderRelatedList(dispatcher, {
|
|
797
|
-
kind: "relatedList",
|
|
798
|
-
title: "Positions",
|
|
799
|
-
query: "lease:query:items:list",
|
|
800
|
-
columns: [{ field: "name" }],
|
|
801
|
-
rowActions: [
|
|
802
|
-
{
|
|
803
|
-
kind: "drawer",
|
|
804
|
-
id: "adjust-rent",
|
|
805
|
-
label: "actions.adjustRent",
|
|
806
|
-
screen: "adjust-rent-form",
|
|
807
|
-
},
|
|
808
|
-
],
|
|
809
|
-
});
|
|
810
|
-
|
|
811
|
-
await waitFor(() => expect(rtlScreen.getByTestId("row-item-7")).toBeTruthy());
|
|
812
|
-
expect(rtlScreen.queryByTestId("action-adjust-rent-item-7")).toBeNull();
|
|
813
|
-
});
|
|
814
|
-
|
|
815
|
-
test("a dropped drawer rowAction without onOpenDrawer logs a dev warning naming the action id (fw#2733)", async () => {
|
|
816
|
-
const { dispatcher } = stubDispatcher([{ id: "item-7", name: "Rent 2024", amount: 1200 }]);
|
|
817
|
-
const warnSpy = spyOn(console, "warn").mockImplementation(() => {});
|
|
818
|
-
try {
|
|
819
|
-
renderRelatedList(dispatcher, {
|
|
820
|
-
kind: "relatedList",
|
|
821
|
-
title: "Positions",
|
|
822
|
-
query: "lease:query:items:list",
|
|
823
|
-
columns: [{ field: "name" }],
|
|
824
|
-
rowActions: [
|
|
825
|
-
{
|
|
826
|
-
kind: "drawer",
|
|
827
|
-
id: "adjust-rent-warn-only-2733",
|
|
828
|
-
label: "actions.adjustRent",
|
|
829
|
-
screen: "adjust-rent-form",
|
|
830
|
-
},
|
|
831
|
-
],
|
|
832
|
-
});
|
|
833
|
-
|
|
834
|
-
await waitFor(() => expect(rtlScreen.getByTestId("row-item-7")).toBeTruthy());
|
|
835
|
-
expect(warnSpy).toHaveBeenCalledTimes(1);
|
|
836
|
-
expect(warnSpy.mock.calls[0]?.[0]).toContain("adjust-rent-warn-only-2733");
|
|
837
|
-
} finally {
|
|
838
|
-
warnSpy.mockRestore();
|
|
839
|
-
}
|
|
840
|
-
});
|
|
841
|
-
|
|
842
|
-
test("a drawer rowAction with onOpenDrawer wired renders without a dev warning (fw#2733)", async () => {
|
|
843
|
-
const { dispatcher } = stubDispatcher([{ id: "item-7", name: "Rent 2024", amount: 1200 }]);
|
|
844
|
-
const warnSpy = spyOn(console, "warn").mockImplementation(() => {});
|
|
845
|
-
try {
|
|
846
|
-
render(
|
|
847
|
-
<LocaleProvider
|
|
848
|
-
resolver={createStaticLocaleResolver({ locale: "en-US" })}
|
|
849
|
-
fallbackBundles={[kumikoDefaultTranslations]}
|
|
850
|
-
>
|
|
851
|
-
<DispatcherProvider dispatcher={dispatcher}>
|
|
852
|
-
<PrimitivesProvider value={testPrimitives()}>
|
|
853
|
-
<NavProvider value={stubNav().nav}>
|
|
854
|
-
<RelatedListSection
|
|
855
|
-
section={{
|
|
856
|
-
kind: "relatedList",
|
|
857
|
-
title: "Positions",
|
|
858
|
-
query: "lease:query:items:list",
|
|
859
|
-
columns: [{ field: "name" }],
|
|
860
|
-
rowActions: [
|
|
861
|
-
{
|
|
862
|
-
kind: "drawer",
|
|
863
|
-
id: "adjust-rent-warn-wired-2733",
|
|
864
|
-
label: "actions.adjustRent",
|
|
865
|
-
screen: "adjust-rent-form",
|
|
866
|
-
},
|
|
867
|
-
],
|
|
868
|
-
}}
|
|
869
|
-
parentId="order-1"
|
|
870
|
-
record={{ id: "order-1" }}
|
|
871
|
-
featureName="orders"
|
|
872
|
-
onOpenDrawer={noop}
|
|
873
|
-
/>
|
|
874
|
-
</NavProvider>
|
|
875
|
-
</PrimitivesProvider>
|
|
876
|
-
</DispatcherProvider>
|
|
877
|
-
</LocaleProvider>,
|
|
878
|
-
);
|
|
879
|
-
|
|
880
|
-
await waitFor(() => expect(rtlScreen.getByTestId("row-item-7")).toBeTruthy());
|
|
881
|
-
expect(rtlScreen.getByTestId("action-adjust-rent-warn-wired-2733-item-7")).toBeTruthy();
|
|
882
|
-
expect(warnSpy).not.toHaveBeenCalled();
|
|
883
|
-
} finally {
|
|
884
|
-
warnSpy.mockRestore();
|
|
885
|
-
}
|
|
886
|
-
});
|
|
887
|
-
});
|
|
888
|
-
|
|
889
|
-
// A DataTable stub that renders one row per query row (DOM order == passed
|
|
890
|
-
// `rows` order, so a re-sort is observable as a re-ordered row list) plus a
|
|
891
|
-
// button that calls `onSortChange` the same way a real SortableHeader click
|
|
892
|
-
// would — enough to prove RelatedListSection actually re-orders rows, not
|
|
893
|
-
// just that a header is clickable.
|
|
894
|
-
const orderedDataTable: ComponentType<DataTableProps> = ({ rows, onSortChange }) => (
|
|
895
|
-
<div>
|
|
896
|
-
<table>
|
|
897
|
-
<tbody>
|
|
898
|
-
{rows.map((row) => (
|
|
899
|
-
<tr key={row.id} data-testid={`row-${row.id}`} />
|
|
900
|
-
))}
|
|
901
|
-
</tbody>
|
|
902
|
-
</table>
|
|
903
|
-
{onSortChange !== undefined && (
|
|
904
|
-
<button
|
|
905
|
-
type="button"
|
|
906
|
-
data-testid="sort-amount-desc"
|
|
907
|
-
onClick={() => onSortChange({ field: "amount", dir: "desc" })}
|
|
908
|
-
>
|
|
909
|
-
sort
|
|
910
|
-
</button>
|
|
911
|
-
)}
|
|
912
|
-
</div>
|
|
913
|
-
);
|
|
914
|
-
|
|
915
|
-
function renderedRowOrder(): string[] {
|
|
916
|
-
return rtlScreen
|
|
917
|
-
.getAllByTestId(/^row-/)
|
|
918
|
-
.map((el) => el.getAttribute("data-testid")?.replace("row-", "") ?? "");
|
|
919
|
-
}
|
|
920
|
-
|
|
921
|
-
describe("RelatedListSection — sorting (fw#2722)", () => {
|
|
922
|
-
const unsortedRows = [
|
|
923
|
-
{ id: "r1", name: "Charlie", amount: 300 },
|
|
924
|
-
{ id: "r2", name: "Alice", amount: 100 },
|
|
925
|
-
{ id: "r3", name: "Bob", amount: 200 },
|
|
926
|
-
];
|
|
927
|
-
|
|
928
|
-
test("defaultSort sorts the already-loaded rows client-side, independent of query response order", async () => {
|
|
929
|
-
const { dispatcher } = stubDispatcher(unsortedRows);
|
|
930
|
-
render(
|
|
931
|
-
<LocaleProvider
|
|
932
|
-
resolver={createStaticLocaleResolver({ locale: "en-US" })}
|
|
933
|
-
fallbackBundles={[kumikoDefaultTranslations]}
|
|
934
|
-
>
|
|
935
|
-
<DispatcherProvider dispatcher={dispatcher}>
|
|
936
|
-
<PrimitivesProvider value={{ ...testPrimitives(), DataTable: orderedDataTable }}>
|
|
937
|
-
<NavProvider value={stubNav().nav}>
|
|
938
|
-
<RelatedListSection
|
|
939
|
-
section={{
|
|
940
|
-
kind: "relatedList",
|
|
941
|
-
title: "Positions",
|
|
942
|
-
query: "lease:query:items:list",
|
|
943
|
-
columns: [{ field: "amount", sortable: true }],
|
|
944
|
-
defaultSort: { field: "amount", dir: "asc" },
|
|
945
|
-
}}
|
|
946
|
-
parentId="order-1"
|
|
947
|
-
record={{ id: "order-1" }}
|
|
948
|
-
featureName="orders"
|
|
949
|
-
/>
|
|
950
|
-
</NavProvider>
|
|
951
|
-
</PrimitivesProvider>
|
|
952
|
-
</DispatcherProvider>
|
|
953
|
-
</LocaleProvider>,
|
|
954
|
-
);
|
|
955
|
-
|
|
956
|
-
await waitFor(() => expect(rtlScreen.getByTestId("row-r1")).toBeTruthy());
|
|
957
|
-
expect(renderedRowOrder()).toEqual(["r2", "r3", "r1"]);
|
|
958
|
-
});
|
|
959
|
-
|
|
960
|
-
test("toggling sort via onSortChange re-orders rows without a refetch", async () => {
|
|
961
|
-
const { dispatcher, queryCount } = stubDispatcher(unsortedRows);
|
|
962
|
-
render(
|
|
963
|
-
<LocaleProvider
|
|
964
|
-
resolver={createStaticLocaleResolver({ locale: "en-US" })}
|
|
965
|
-
fallbackBundles={[kumikoDefaultTranslations]}
|
|
966
|
-
>
|
|
967
|
-
<DispatcherProvider dispatcher={dispatcher}>
|
|
968
|
-
<PrimitivesProvider value={{ ...testPrimitives(), DataTable: orderedDataTable }}>
|
|
969
|
-
<NavProvider value={stubNav().nav}>
|
|
970
|
-
<RelatedListSection
|
|
971
|
-
section={{
|
|
972
|
-
kind: "relatedList",
|
|
973
|
-
title: "Positions",
|
|
974
|
-
query: "lease:query:items:list",
|
|
975
|
-
columns: [{ field: "amount", sortable: true }],
|
|
976
|
-
}}
|
|
977
|
-
parentId="order-1"
|
|
978
|
-
record={{ id: "order-1" }}
|
|
979
|
-
featureName="orders"
|
|
980
|
-
/>
|
|
981
|
-
</NavProvider>
|
|
982
|
-
</PrimitivesProvider>
|
|
983
|
-
</DispatcherProvider>
|
|
984
|
-
</LocaleProvider>,
|
|
985
|
-
);
|
|
986
|
-
|
|
987
|
-
await waitFor(() => expect(rtlScreen.getByTestId("row-r1")).toBeTruthy());
|
|
988
|
-
expect(renderedRowOrder()).toEqual(["r1", "r2", "r3"]);
|
|
989
|
-
|
|
990
|
-
fireEvent.click(rtlScreen.getByTestId("sort-amount-desc"));
|
|
991
|
-
|
|
992
|
-
await waitFor(() => expect(renderedRowOrder()).toEqual(["r1", "r3", "r2"]));
|
|
993
|
-
expect(queryCount()).toBe(1);
|
|
994
|
-
});
|
|
995
|
-
|
|
996
|
-
test("a section without defaultSort renders rows in the original query order (unchanged behavior)", async () => {
|
|
997
|
-
const { dispatcher } = stubDispatcher(unsortedRows);
|
|
998
|
-
render(
|
|
999
|
-
<LocaleProvider
|
|
1000
|
-
resolver={createStaticLocaleResolver({ locale: "en-US" })}
|
|
1001
|
-
fallbackBundles={[kumikoDefaultTranslations]}
|
|
1002
|
-
>
|
|
1003
|
-
<DispatcherProvider dispatcher={dispatcher}>
|
|
1004
|
-
<PrimitivesProvider value={{ ...testPrimitives(), DataTable: orderedDataTable }}>
|
|
1005
|
-
<NavProvider value={stubNav().nav}>
|
|
1006
|
-
<RelatedListSection
|
|
1007
|
-
section={{
|
|
1008
|
-
kind: "relatedList",
|
|
1009
|
-
title: "Positions",
|
|
1010
|
-
query: "lease:query:items:list",
|
|
1011
|
-
columns: [{ field: "amount" }],
|
|
1012
|
-
}}
|
|
1013
|
-
parentId="order-1"
|
|
1014
|
-
record={{ id: "order-1" }}
|
|
1015
|
-
featureName="orders"
|
|
1016
|
-
/>
|
|
1017
|
-
</NavProvider>
|
|
1018
|
-
</PrimitivesProvider>
|
|
1019
|
-
</DispatcherProvider>
|
|
1020
|
-
</LocaleProvider>,
|
|
1021
|
-
);
|
|
1022
|
-
|
|
1023
|
-
await waitFor(() => expect(rtlScreen.getByTestId("row-r1")).toBeTruthy());
|
|
1024
|
-
expect(renderedRowOrder()).toEqual(["r1", "r2", "r3"]);
|
|
1025
|
-
});
|
|
1026
|
-
});
|
|
1027
|
-
|
|
1028
|
-
describe("RelatedListSection — truncation banner (fw#2722 review)", () => {
|
|
1029
|
-
test("nextCursor !== null renders a truncation banner", async () => {
|
|
1030
|
-
const { dispatcher } = stubDispatcher([{ id: "r1", name: "Alice" }], "cursor-abc");
|
|
1031
|
-
renderRelatedList(dispatcher);
|
|
1032
|
-
|
|
1033
|
-
await waitFor(() => expect(rtlScreen.getByTestId("row-r1")).toBeTruthy());
|
|
1034
|
-
const banner = rtlScreen.getByTestId("related-list-truncated");
|
|
1035
|
-
expect(banner).toBeTruthy();
|
|
1036
|
-
// Proves {count} actually interpolates (single-brace, the renderer's own
|
|
1037
|
-
// i18n.tsx interpolate() syntax) rather than rendering as literal braces.
|
|
1038
|
-
expect(banner.textContent).toContain("Showing the first 1 entries.");
|
|
1039
|
-
});
|
|
1040
|
-
|
|
1041
|
-
test("nextCursor === null renders no truncation banner", async () => {
|
|
1042
|
-
const { dispatcher } = stubDispatcher([{ id: "r1", name: "Alice" }], null);
|
|
1043
|
-
renderRelatedList(dispatcher);
|
|
1044
|
-
|
|
1045
|
-
await waitFor(() => expect(rtlScreen.getByTestId("row-r1")).toBeTruthy());
|
|
1046
|
-
expect(rtlScreen.queryByTestId("related-list-truncated")).toBeNull();
|
|
1047
|
-
});
|
|
1048
|
-
|
|
1049
|
-
test("the banner shows regardless of an active sort — an unsorted truncated list is equally misleading", async () => {
|
|
1050
|
-
const { dispatcher } = stubDispatcher(
|
|
1051
|
-
[
|
|
1052
|
-
{ id: "r1", name: "Charlie", amount: 300 },
|
|
1053
|
-
{ id: "r2", name: "Alice", amount: 100 },
|
|
1054
|
-
],
|
|
1055
|
-
"cursor-abc",
|
|
1056
|
-
);
|
|
1057
|
-
renderRelatedList(dispatcher, {
|
|
1058
|
-
kind: "relatedList",
|
|
1059
|
-
title: "Positions",
|
|
1060
|
-
query: "lease:query:items:list",
|
|
1061
|
-
columns: [{ field: "amount", sortable: true }],
|
|
1062
|
-
defaultSort: { field: "amount", dir: "asc" },
|
|
1063
|
-
});
|
|
1064
|
-
|
|
1065
|
-
await waitFor(() => expect(rtlScreen.getByTestId("row-r1")).toBeTruthy());
|
|
1066
|
-
expect(rtlScreen.getByTestId("related-list-truncated")).toBeTruthy();
|
|
1067
|
-
});
|
|
1068
|
-
});
|
|
1069
|
-
|
|
1070
|
-
// A DataTable stub that renders the toolbarStart slot (carries the search
|
|
1071
|
-
// Input) and filterFacets as one button per option, wired straight to
|
|
1072
|
-
// onFilterChange/onFilterReset — enough to prove RelatedListSection resolves
|
|
1073
|
-
// facets and wires search through RenderList, without the production
|
|
1074
|
-
// DataTable's own facet-dropdown chrome.
|
|
1075
|
-
const searchFacetDataTable: ComponentType<DataTableProps> = ({
|
|
1076
|
-
rows,
|
|
1077
|
-
toolbarStart,
|
|
1078
|
-
filterFacets,
|
|
1079
|
-
onFilterChange,
|
|
1080
|
-
onFilterReset,
|
|
1081
|
-
}) => (
|
|
1082
|
-
<div>
|
|
1083
|
-
{toolbarStart}
|
|
1084
|
-
{(filterFacets ?? []).map((facet) => (
|
|
1085
|
-
<div key={facet.field}>
|
|
1086
|
-
{facet.options.map((opt) => (
|
|
1087
|
-
<button
|
|
1088
|
-
key={opt.value}
|
|
1089
|
-
type="button"
|
|
1090
|
-
data-testid={`facet-${facet.field}-${opt.value}`}
|
|
1091
|
-
onClick={() => onFilterChange?.(facet.field, [opt.value])}
|
|
1092
|
-
>
|
|
1093
|
-
{opt.label}
|
|
1094
|
-
</button>
|
|
1095
|
-
))}
|
|
1096
|
-
</div>
|
|
1097
|
-
))}
|
|
1098
|
-
{onFilterReset !== undefined && (
|
|
1099
|
-
<button type="button" data-testid="filter-reset" onClick={onFilterReset}>
|
|
1100
|
-
Reset
|
|
1101
|
-
</button>
|
|
1102
|
-
)}
|
|
1103
|
-
<table>
|
|
1104
|
-
<tbody>
|
|
1105
|
-
{rows.map((row) => (
|
|
1106
|
-
<tr key={row.id} data-testid={`row-${row.id}`} />
|
|
1107
|
-
))}
|
|
1108
|
-
</tbody>
|
|
1109
|
-
</table>
|
|
1110
|
-
</div>
|
|
1111
|
-
);
|
|
1112
|
-
|
|
1113
|
-
// A dispatcher stub that genuinely filters a fixed row set by the received
|
|
1114
|
-
// payload — `search` as a case-insensitive substring on `name`, `filters`
|
|
1115
|
-
// entries (`{ field, op: "in", value }`) as membership on that field —
|
|
1116
|
-
// instead of returning canned rows, so a test can tell a real payload from a
|
|
1117
|
-
// stale one. Every payload it sees is recorded for the "last payload"
|
|
1118
|
-
// assertions below.
|
|
1119
|
-
function filteringDispatcher(rows: readonly Record<string, unknown>[]): {
|
|
1120
|
-
dispatcher: Dispatcher;
|
|
1121
|
-
payloads: Record<string, unknown>[];
|
|
1122
|
-
} {
|
|
1123
|
-
const payloads: Record<string, unknown>[] = [];
|
|
1124
|
-
const dispatcher: Dispatcher = {
|
|
1125
|
-
write: (async () => ({ isSuccess: true, data: null })) as Dispatcher["write"],
|
|
1126
|
-
query: (async (_type: string, payload: unknown) => {
|
|
1127
|
-
const p = payload as {
|
|
1128
|
-
search?: string;
|
|
1129
|
-
filters?: readonly { field: string; op: "in"; value: readonly unknown[] }[];
|
|
1130
|
-
};
|
|
1131
|
-
payloads.push(p);
|
|
1132
|
-
let result = rows;
|
|
1133
|
-
if (p.search !== undefined && p.search !== "") {
|
|
1134
|
-
const term = p.search.toLowerCase();
|
|
1135
|
-
result = result.filter((r) =>
|
|
1136
|
-
String(r["name"] ?? "")
|
|
1137
|
-
.toLowerCase()
|
|
1138
|
-
.includes(term),
|
|
1139
|
-
);
|
|
1140
|
-
}
|
|
1141
|
-
for (const f of p.filters ?? []) {
|
|
1142
|
-
result = result.filter((r) => f.value.includes(r[f.field]));
|
|
1143
|
-
}
|
|
1144
|
-
return { isSuccess: true, data: { rows: result, nextCursor: null } };
|
|
1145
|
-
}) as Dispatcher["query"],
|
|
1146
|
-
batch: (async () => ({ isSuccess: true, results: [] })) as Dispatcher["batch"],
|
|
1147
|
-
statusStore: {
|
|
1148
|
-
getState: () => "online",
|
|
1149
|
-
subscribe: () => () => {},
|
|
1150
|
-
} as unknown as Dispatcher["statusStore"],
|
|
1151
|
-
async *stream() {},
|
|
1152
|
-
pendingWrites: () => [],
|
|
1153
|
-
pendingFiles: () => [],
|
|
1154
|
-
};
|
|
1155
|
-
return { dispatcher, payloads };
|
|
1156
|
-
}
|
|
1157
|
-
|
|
1158
|
-
function renderWithDataTable(
|
|
1159
|
-
dispatcher: Dispatcher,
|
|
1160
|
-
section: EditRelatedListSectionViewModel,
|
|
1161
|
-
DataTable: ComponentType<DataTableProps>,
|
|
1162
|
-
hideTitle?: boolean,
|
|
1163
|
-
) {
|
|
1164
|
-
return render(
|
|
1165
|
-
<LocaleProvider
|
|
1166
|
-
resolver={createStaticLocaleResolver({ locale: "en-US" })}
|
|
1167
|
-
fallbackBundles={[kumikoDefaultTranslations]}
|
|
1168
|
-
>
|
|
1169
|
-
<DispatcherProvider dispatcher={dispatcher}>
|
|
1170
|
-
<PrimitivesProvider value={{ ...testPrimitives(), DataTable }}>
|
|
1171
|
-
<NavProvider value={stubNav().nav}>
|
|
1172
|
-
<RelatedListSection
|
|
1173
|
-
section={section}
|
|
1174
|
-
parentId="order-1"
|
|
1175
|
-
record={{ id: "order-1" }}
|
|
1176
|
-
featureName="orders"
|
|
1177
|
-
{...(hideTitle === true && { hideTitle: true })}
|
|
1178
|
-
/>
|
|
1179
|
-
</NavProvider>
|
|
1180
|
-
</PrimitivesProvider>
|
|
1181
|
-
</DispatcherProvider>
|
|
1182
|
-
</LocaleProvider>,
|
|
1183
|
-
);
|
|
1184
|
-
}
|
|
1185
|
-
|
|
1186
|
-
describe("RelatedListSection — search + facets (fw#2740)", () => {
|
|
1187
|
-
const nameRows = [
|
|
1188
|
-
{ id: "r1", name: "Alice" },
|
|
1189
|
-
{ id: "r2", name: "Bob" },
|
|
1190
|
-
];
|
|
1191
|
-
|
|
1192
|
-
test("searchable: true — typing (after the 300ms debounce) sends payload.search and narrows the rendered rows", async () => {
|
|
1193
|
-
const { dispatcher, payloads } = filteringDispatcher(nameRows);
|
|
1194
|
-
renderWithDataTable(
|
|
1195
|
-
dispatcher,
|
|
1196
|
-
{
|
|
1197
|
-
kind: "relatedList",
|
|
1198
|
-
title: "Contacts",
|
|
1199
|
-
query: "lease:query:contacts:list",
|
|
1200
|
-
columns: [{ field: "name" }],
|
|
1201
|
-
searchable: true,
|
|
1202
|
-
},
|
|
1203
|
-
searchFacetDataTable,
|
|
1204
|
-
);
|
|
1205
|
-
|
|
1206
|
-
await waitFor(() => expect(rtlScreen.getByTestId("row-r1")).toBeTruthy());
|
|
1207
|
-
expect(rtlScreen.getByTestId("row-r2")).toBeTruthy();
|
|
1208
|
-
|
|
1209
|
-
fireEvent.change(rtlScreen.getByTestId("render-list-search"), {
|
|
1210
|
-
target: { value: "ali" },
|
|
1211
|
-
});
|
|
1212
|
-
|
|
1213
|
-
await waitFor(
|
|
1214
|
-
() => {
|
|
1215
|
-
const last = payloads[payloads.length - 1];
|
|
1216
|
-
expect(last?.["search"]).toBe("ali");
|
|
1217
|
-
},
|
|
1218
|
-
{ timeout: 2000 },
|
|
1219
|
-
);
|
|
1220
|
-
await waitFor(() => expect(rtlScreen.queryByTestId("row-r2")).toBeNull());
|
|
1221
|
-
expect(rtlScreen.getByTestId("row-r1")).toBeTruthy();
|
|
1222
|
-
});
|
|
1223
|
-
|
|
1224
|
-
test("without searchable, RenderList shows no search input and no payload ever carries a search key", async () => {
|
|
1225
|
-
const { dispatcher, payloads } = filteringDispatcher(nameRows);
|
|
1226
|
-
renderWithDataTable(
|
|
1227
|
-
dispatcher,
|
|
1228
|
-
{
|
|
1229
|
-
kind: "relatedList",
|
|
1230
|
-
title: "Contacts",
|
|
1231
|
-
query: "lease:query:contacts:list",
|
|
1232
|
-
columns: [{ field: "name" }],
|
|
1233
|
-
},
|
|
1234
|
-
searchFacetDataTable,
|
|
1235
|
-
);
|
|
1236
|
-
|
|
1237
|
-
await waitFor(() => expect(rtlScreen.getByTestId("row-r1")).toBeTruthy());
|
|
1238
|
-
expect(rtlScreen.queryByTestId("render-list-search")).toBeNull();
|
|
1239
|
-
for (const p of payloads) {
|
|
1240
|
-
expect("search" in p).toBe(false);
|
|
1241
|
-
}
|
|
1242
|
-
});
|
|
1243
|
-
|
|
1244
|
-
test("tabs mode (hideTitle): the search box still renders and narrows the rows", async () => {
|
|
1245
|
-
const { dispatcher, payloads } = filteringDispatcher(nameRows);
|
|
1246
|
-
renderWithDataTable(
|
|
1247
|
-
dispatcher,
|
|
1248
|
-
{
|
|
1249
|
-
kind: "relatedList",
|
|
1250
|
-
title: "Contacts",
|
|
1251
|
-
query: "lease:query:contacts:list",
|
|
1252
|
-
columns: [{ field: "name" }],
|
|
1253
|
-
searchable: true,
|
|
1254
|
-
},
|
|
1255
|
-
searchFacetDataTable,
|
|
1256
|
-
true,
|
|
1257
|
-
);
|
|
1258
|
-
|
|
1259
|
-
await waitFor(() => expect(rtlScreen.getByTestId("row-r1")).toBeTruthy());
|
|
1260
|
-
expect(rtlScreen.getByTestId("row-r2")).toBeTruthy();
|
|
1261
|
-
expect(rtlScreen.getByTestId("render-list-search")).toBeTruthy();
|
|
1262
|
-
|
|
1263
|
-
fireEvent.change(rtlScreen.getByTestId("render-list-search"), {
|
|
1264
|
-
target: { value: "ali" },
|
|
1265
|
-
});
|
|
1266
|
-
|
|
1267
|
-
await waitFor(
|
|
1268
|
-
() => {
|
|
1269
|
-
const last = payloads[payloads.length - 1];
|
|
1270
|
-
expect(last?.["search"]).toBe("ali");
|
|
1271
|
-
},
|
|
1272
|
-
{ timeout: 2000 },
|
|
1273
|
-
);
|
|
1274
|
-
await waitFor(() => expect(rtlScreen.queryByTestId("row-r2")).toBeNull());
|
|
1275
|
-
expect(rtlScreen.getByTestId("row-r1")).toBeTruthy();
|
|
1276
|
-
});
|
|
1277
|
-
|
|
1278
|
-
const statusSection: EditRelatedListSectionViewModel = {
|
|
1279
|
-
kind: "relatedList",
|
|
1280
|
-
title: "Positions",
|
|
1281
|
-
query: "lease:query:items:list",
|
|
1282
|
-
columns: [{ field: "name" }, { field: "status" }],
|
|
1283
|
-
facets: [
|
|
1284
|
-
{
|
|
1285
|
-
field: "status",
|
|
1286
|
-
type: "select",
|
|
1287
|
-
label: "Status",
|
|
1288
|
-
options: [
|
|
1289
|
-
{ value: "active", label: "Active" },
|
|
1290
|
-
{ value: "ended", label: "Ended" },
|
|
1291
|
-
],
|
|
1292
|
-
},
|
|
1293
|
-
],
|
|
1294
|
-
};
|
|
1295
|
-
const statusRows = [
|
|
1296
|
-
{ id: "active-1", name: "Running", status: "active" },
|
|
1297
|
-
{ id: "ended-1", name: "Closed", status: "ended" },
|
|
1298
|
-
];
|
|
1299
|
-
|
|
1300
|
-
test("select facet: clicking an option filters payload.filters and rows, resetting clears both", async () => {
|
|
1301
|
-
const { dispatcher, payloads } = filteringDispatcher(statusRows);
|
|
1302
|
-
renderWithDataTable(dispatcher, statusSection, searchFacetDataTable);
|
|
1303
|
-
|
|
1304
|
-
await waitFor(() => expect(rtlScreen.getByTestId("row-active-1")).toBeTruthy());
|
|
1305
|
-
expect(rtlScreen.getByTestId("row-ended-1")).toBeTruthy();
|
|
1306
|
-
|
|
1307
|
-
fireEvent.click(rtlScreen.getByTestId("facet-status-active"));
|
|
1308
|
-
|
|
1309
|
-
await waitFor(() => expect(rtlScreen.queryByTestId("row-ended-1")).toBeNull());
|
|
1310
|
-
expect(rtlScreen.getByTestId("row-active-1")).toBeTruthy();
|
|
1311
|
-
expect(payloads[payloads.length - 1]?.["filters"]).toEqual([
|
|
1312
|
-
{ field: "status", op: "in", value: ["active"] },
|
|
1313
|
-
]);
|
|
1314
|
-
|
|
1315
|
-
fireEvent.click(rtlScreen.getByTestId("filter-reset"));
|
|
1316
|
-
|
|
1317
|
-
await waitFor(() => expect(rtlScreen.getByTestId("row-ended-1")).toBeTruthy());
|
|
1318
|
-
expect(rtlScreen.getByTestId("row-active-1")).toBeTruthy();
|
|
1319
|
-
expect(payloads[payloads.length - 1]?.["filters"]).toBeUndefined();
|
|
1320
|
-
});
|
|
1321
|
-
|
|
1322
|
-
test("boolean facet: selecting a value sends a real boolean in payload.filters and narrows rows accordingly", async () => {
|
|
1323
|
-
const paidRows = [
|
|
1324
|
-
{ id: "paid-1", name: "Invoice A", paid: true },
|
|
1325
|
-
{ id: "unpaid-1", name: "Invoice B", paid: false },
|
|
1326
|
-
];
|
|
1327
|
-
const { dispatcher, payloads } = filteringDispatcher(paidRows);
|
|
1328
|
-
renderWithDataTable(
|
|
1329
|
-
dispatcher,
|
|
1330
|
-
{
|
|
1331
|
-
kind: "relatedList",
|
|
1332
|
-
title: "Invoices",
|
|
1333
|
-
query: "lease:query:invoices:list",
|
|
1334
|
-
columns: [{ field: "name" }, { field: "paid" }],
|
|
1335
|
-
facets: [
|
|
1336
|
-
{
|
|
1337
|
-
field: "paid",
|
|
1338
|
-
type: "boolean",
|
|
1339
|
-
label: "Paid",
|
|
1340
|
-
trueLabel: "Yes",
|
|
1341
|
-
falseLabel: "No",
|
|
1342
|
-
},
|
|
1343
|
-
],
|
|
1344
|
-
},
|
|
1345
|
-
searchFacetDataTable,
|
|
1346
|
-
);
|
|
1347
|
-
|
|
1348
|
-
await waitFor(() => expect(rtlScreen.getByTestId("row-paid-1")).toBeTruthy());
|
|
1349
|
-
expect(rtlScreen.getByTestId("row-unpaid-1")).toBeTruthy();
|
|
1350
|
-
|
|
1351
|
-
fireEvent.click(rtlScreen.getByTestId("facet-paid-true"));
|
|
1352
|
-
|
|
1353
|
-
await waitFor(() => expect(rtlScreen.queryByTestId("row-unpaid-1")).toBeNull());
|
|
1354
|
-
expect(rtlScreen.getByTestId("row-paid-1")).toBeTruthy();
|
|
1355
|
-
expect(payloads[payloads.length - 1]?.["filters"]).toEqual([
|
|
1356
|
-
{ field: "paid", op: "in", value: [true] },
|
|
1357
|
-
]);
|
|
1358
|
-
});
|
|
1359
|
-
});
|
|
1360
|
-
|
|
1361
|
-
// A dispatcher stub that filters a fixed row set by `payload.filter` (single
|
|
1362
|
-
// eq clause), `payload.search` (substring on `name`) and `payload.filters`
|
|
1363
|
-
// (facet membership) together — proves parentFilter composes with search
|
|
1364
|
-
// and facets instead of being clobbered by either.
|
|
1365
|
-
function parentFilterDispatcher(rows: readonly Record<string, unknown>[]): {
|
|
1366
|
-
dispatcher: Dispatcher;
|
|
1367
|
-
payloads: Record<string, unknown>[];
|
|
1368
|
-
} {
|
|
1369
|
-
const payloads: Record<string, unknown>[] = [];
|
|
1370
|
-
const dispatcher: Dispatcher = {
|
|
1371
|
-
write: (async () => ({ isSuccess: true, data: null })) as Dispatcher["write"],
|
|
1372
|
-
query: (async (_type: string, payload: unknown) => {
|
|
1373
|
-
const p = payload as {
|
|
1374
|
-
filter?: { field: string; op: "eq"; value: unknown };
|
|
1375
|
-
search?: string;
|
|
1376
|
-
filters?: readonly { field: string; op: "in"; value: readonly unknown[] }[];
|
|
1377
|
-
};
|
|
1378
|
-
payloads.push(p);
|
|
1379
|
-
let result = rows;
|
|
1380
|
-
if (p.filter !== undefined) {
|
|
1381
|
-
const { field, value } = p.filter;
|
|
1382
|
-
result = result.filter((r) => r[field] === value);
|
|
1383
|
-
}
|
|
1384
|
-
if (p.search !== undefined && p.search !== "") {
|
|
1385
|
-
const term = p.search.toLowerCase();
|
|
1386
|
-
result = result.filter((r) =>
|
|
1387
|
-
String(r["name"] ?? "")
|
|
1388
|
-
.toLowerCase()
|
|
1389
|
-
.includes(term),
|
|
1390
|
-
);
|
|
1391
|
-
}
|
|
1392
|
-
for (const f of p.filters ?? []) {
|
|
1393
|
-
result = result.filter((r) => f.value.includes(r[f.field]));
|
|
1394
|
-
}
|
|
1395
|
-
return { isSuccess: true, data: { rows: result, nextCursor: null } };
|
|
1396
|
-
}) as Dispatcher["query"],
|
|
1397
|
-
batch: (async () => ({ isSuccess: true, results: [] })) as Dispatcher["batch"],
|
|
1398
|
-
statusStore: {
|
|
1399
|
-
getState: () => "online",
|
|
1400
|
-
subscribe: () => () => {},
|
|
1401
|
-
} as unknown as Dispatcher["statusStore"],
|
|
1402
|
-
async *stream() {},
|
|
1403
|
-
pendingWrites: () => [],
|
|
1404
|
-
pendingFiles: () => [],
|
|
1405
|
-
};
|
|
1406
|
-
return { dispatcher, payloads };
|
|
1407
|
-
}
|
|
1408
|
-
|
|
1409
|
-
describe("RelatedListSection — parentFilter", () => {
|
|
1410
|
-
const rows = [
|
|
1411
|
-
{ id: "r1", parentId: "order-1", name: "Alice", status: "active" },
|
|
1412
|
-
{ id: "r2", parentId: "order-1", name: "Bob", status: "ended" },
|
|
1413
|
-
{ id: "r3", parentId: "order-2", name: "Carol", status: "active" },
|
|
1414
|
-
];
|
|
1415
|
-
const section: EditRelatedListSectionViewModel = {
|
|
1416
|
-
kind: "relatedList",
|
|
1417
|
-
title: "Positions",
|
|
1418
|
-
query: "orders:query:items:list",
|
|
1419
|
-
columns: [{ field: "name" }, { field: "status" }],
|
|
1420
|
-
parentFilter: { field: "parentId" },
|
|
1421
|
-
pageSize: 25,
|
|
1422
|
-
searchable: true,
|
|
1423
|
-
facets: [
|
|
1424
|
-
{
|
|
1425
|
-
field: "status",
|
|
1426
|
-
type: "select",
|
|
1427
|
-
label: "Status",
|
|
1428
|
-
options: [
|
|
1429
|
-
{ value: "active", label: "Active" },
|
|
1430
|
-
{ value: "ended", label: "Ended" },
|
|
1431
|
-
],
|
|
1432
|
-
},
|
|
1433
|
-
],
|
|
1434
|
-
};
|
|
1435
|
-
|
|
1436
|
-
test("sends payload.filter eq parentId (no top-level parentParam key), combined with limit/search/facets, and only the parent's rows render", async () => {
|
|
1437
|
-
const { dispatcher, payloads } = parentFilterDispatcher(rows);
|
|
1438
|
-
renderWithDataTable(dispatcher, section, searchFacetDataTable);
|
|
1439
|
-
|
|
1440
|
-
await waitFor(() => expect(rtlScreen.getByTestId("row-r1")).toBeTruthy());
|
|
1441
|
-
expect(rtlScreen.getByTestId("row-r2")).toBeTruthy();
|
|
1442
|
-
expect(rtlScreen.queryByTestId("row-r3")).toBeNull();
|
|
1443
|
-
|
|
1444
|
-
const firstPayload = payloads[0];
|
|
1445
|
-
expect(firstPayload).toEqual({
|
|
1446
|
-
filter: { field: "parentId", op: "eq", value: "order-1" },
|
|
1447
|
-
limit: 25,
|
|
1448
|
-
});
|
|
1449
|
-
expect(firstPayload?.["id"]).toBeUndefined();
|
|
1450
|
-
expect(firstPayload?.["parentId"]).toBeUndefined();
|
|
1451
|
-
});
|
|
1452
|
-
|
|
1453
|
-
test("typing a search term keeps payload.filter and narrows via payload.search together", async () => {
|
|
1454
|
-
const { dispatcher, payloads } = parentFilterDispatcher(rows);
|
|
1455
|
-
renderWithDataTable(dispatcher, section, searchFacetDataTable);
|
|
1456
|
-
|
|
1457
|
-
await waitFor(() => expect(rtlScreen.getByTestId("row-r1")).toBeTruthy());
|
|
1458
|
-
fireEvent.change(rtlScreen.getByTestId("render-list-search"), {
|
|
1459
|
-
target: { value: "ali" },
|
|
1460
|
-
});
|
|
1461
|
-
|
|
1462
|
-
await waitFor(
|
|
1463
|
-
() => {
|
|
1464
|
-
const last = payloads[payloads.length - 1];
|
|
1465
|
-
expect(last?.["search"]).toBe("ali");
|
|
1466
|
-
},
|
|
1467
|
-
{ timeout: 2000 },
|
|
1468
|
-
);
|
|
1469
|
-
const last = payloads[payloads.length - 1];
|
|
1470
|
-
expect(last?.["filter"]).toEqual({ field: "parentId", op: "eq", value: "order-1" });
|
|
1471
|
-
await waitFor(() => expect(rtlScreen.queryByTestId("row-r2")).toBeNull());
|
|
1472
|
-
expect(rtlScreen.getByTestId("row-r1")).toBeTruthy();
|
|
1473
|
-
});
|
|
1474
|
-
|
|
1475
|
-
test("selecting a facet leaves payload.filter unchanged and adds payload.filters", async () => {
|
|
1476
|
-
const { dispatcher, payloads } = parentFilterDispatcher(rows);
|
|
1477
|
-
renderWithDataTable(dispatcher, section, searchFacetDataTable);
|
|
1478
|
-
|
|
1479
|
-
await waitFor(() => expect(rtlScreen.getByTestId("row-r1")).toBeTruthy());
|
|
1480
|
-
fireEvent.click(rtlScreen.getByTestId("facet-status-active"));
|
|
1481
|
-
|
|
1482
|
-
await waitFor(() => expect(rtlScreen.queryByTestId("row-r2")).toBeNull());
|
|
1483
|
-
expect(rtlScreen.getByTestId("row-r1")).toBeTruthy();
|
|
1484
|
-
const last = payloads[payloads.length - 1];
|
|
1485
|
-
expect(last?.["filter"]).toEqual({ field: "parentId", op: "eq", value: "order-1" });
|
|
1486
|
-
expect(last?.["filters"]).toEqual([{ field: "status", op: "in", value: ["active"] }]);
|
|
1487
|
-
});
|
|
1488
|
-
|
|
1489
|
-
test("a toolbarAction without params prefills the parent id under parentFilter.field, not parentParam/id", async () => {
|
|
1490
|
-
const { dispatcher } = parentFilterDispatcher(rows);
|
|
1491
|
-
const { nav, navigations, searchParams } = stubNav();
|
|
1492
|
-
const capturingDataTable: ComponentType<DataTableProps> = (props) => (
|
|
1493
|
-
<>
|
|
1494
|
-
{props.toolbarEnd}
|
|
1495
|
-
{testDataTable(props)}
|
|
1496
|
-
</>
|
|
1497
|
-
);
|
|
1498
|
-
const testButton = ({
|
|
1499
|
-
children,
|
|
1500
|
-
testId,
|
|
1501
|
-
onClick,
|
|
1502
|
-
}: {
|
|
1503
|
-
readonly children?: ReactNode;
|
|
1504
|
-
readonly testId?: string;
|
|
1505
|
-
readonly onClick?: () => void;
|
|
1506
|
-
}) => (
|
|
1507
|
-
<button type="button" data-testid={testId} onClick={() => onClick?.()}>
|
|
1508
|
-
{children}
|
|
1509
|
-
</button>
|
|
1510
|
-
);
|
|
1511
|
-
render(
|
|
1512
|
-
<LocaleProvider
|
|
1513
|
-
resolver={createStaticLocaleResolver({ locale: "en-US" })}
|
|
1514
|
-
fallbackBundles={[kumikoDefaultTranslations]}
|
|
1515
|
-
>
|
|
1516
|
-
<DispatcherProvider dispatcher={dispatcher}>
|
|
1517
|
-
<PrimitivesProvider
|
|
1518
|
-
value={{ ...testPrimitives(), DataTable: capturingDataTable, Button: testButton }}
|
|
1519
|
-
>
|
|
1520
|
-
<NavProvider value={nav}>
|
|
1521
|
-
<RelatedListSection
|
|
1522
|
-
section={{
|
|
1523
|
-
...section,
|
|
1524
|
-
toolbarActions: [
|
|
1525
|
-
{
|
|
1526
|
-
kind: "navigate",
|
|
1527
|
-
id: "create",
|
|
1528
|
-
label: "Add position",
|
|
1529
|
-
screen: "item-create",
|
|
1530
|
-
},
|
|
1531
|
-
],
|
|
1532
|
-
}}
|
|
1533
|
-
parentId="order-1"
|
|
1534
|
-
record={{ id: "order-1" }}
|
|
1535
|
-
featureName="orders"
|
|
1536
|
-
/>
|
|
1537
|
-
</NavProvider>
|
|
1538
|
-
</PrimitivesProvider>
|
|
1539
|
-
</DispatcherProvider>
|
|
1540
|
-
</LocaleProvider>,
|
|
1541
|
-
);
|
|
1542
|
-
|
|
1543
|
-
await waitFor(() => expect(rtlScreen.getByTestId("row-r1")).toBeTruthy());
|
|
1544
|
-
rtlScreen.getByTestId("render-list-toolbar-action-create").click();
|
|
1545
|
-
|
|
1546
|
-
await waitFor(() => expect(navigations).toHaveLength(1));
|
|
1547
|
-
expect(searchParams).toEqual([{ parentId: "order-1" }]);
|
|
1548
|
-
});
|
|
1549
|
-
});
|
|
1550
|
-
|
|
1551
|
-
// Same findEditScreenFor/buildDefaultEditRowAction resolution as
|
|
1552
|
-
// entityList/projectionList, driven by rowClick.entity and rowClick.idColumn.
|
|
1553
|
-
describe("RelatedListSection — default edit row action", () => {
|
|
1554
|
-
function editScreen(entity: string, roles?: readonly string[]): EntityEditScreenDefinition {
|
|
1555
|
-
return {
|
|
1556
|
-
id: "lease:screen:item-edit",
|
|
1557
|
-
type: "entityEdit",
|
|
1558
|
-
entity,
|
|
1559
|
-
layout: { sections: [{ columns: 1, fields: ["name"] }] },
|
|
1560
|
-
...(roles !== undefined && { access: { roles } }),
|
|
1561
|
-
};
|
|
1562
|
-
}
|
|
1563
|
-
|
|
1564
|
-
function renderWithFeatures(
|
|
1565
|
-
section: EditRelatedListSectionViewModel,
|
|
1566
|
-
dispatcher: Dispatcher,
|
|
1567
|
-
features: readonly FeatureSchema[],
|
|
1568
|
-
userRoles: readonly string[] = [],
|
|
1569
|
-
nav: NavApi = stubNav().nav,
|
|
1570
|
-
) {
|
|
1571
|
-
return render(
|
|
1572
|
-
<LocaleProvider
|
|
1573
|
-
resolver={createStaticLocaleResolver({ locale: "en-US" })}
|
|
1574
|
-
fallbackBundles={[kumikoDefaultTranslations]}
|
|
1575
|
-
>
|
|
1576
|
-
<DispatcherProvider dispatcher={dispatcher}>
|
|
1577
|
-
<AppFeaturesProvider features={features}>
|
|
1578
|
-
<UserRolesProvider roles={userRoles}>
|
|
1579
|
-
<PrimitivesProvider value={testPrimitives()}>
|
|
1580
|
-
<NavProvider value={nav}>
|
|
1581
|
-
<RelatedListSection
|
|
1582
|
-
section={section}
|
|
1583
|
-
parentId="order-1"
|
|
1584
|
-
record={{ id: "order-1" }}
|
|
1585
|
-
featureName="orders"
|
|
1586
|
-
/>
|
|
1587
|
-
</NavProvider>
|
|
1588
|
-
</PrimitivesProvider>
|
|
1589
|
-
</UserRolesProvider>
|
|
1590
|
-
</AppFeaturesProvider>
|
|
1591
|
-
</DispatcherProvider>
|
|
1592
|
-
</LocaleProvider>,
|
|
1593
|
-
);
|
|
1594
|
-
}
|
|
1595
|
-
|
|
1596
|
-
const sectionWithRowClick: EditRelatedListSectionViewModel = {
|
|
1597
|
-
kind: "relatedList",
|
|
1598
|
-
title: "Positions",
|
|
1599
|
-
query: "lease:query:items:list",
|
|
1600
|
-
columns: [{ field: "name" }],
|
|
1601
|
-
rowClick: { entity: "item" },
|
|
1602
|
-
};
|
|
1603
|
-
|
|
1604
|
-
test("adds a default edit action at the first position when rowClick.entity resolves an entityEdit screen", async () => {
|
|
1605
|
-
const { dispatcher } = stubDispatcher();
|
|
1606
|
-
const { nav, navigations } = stubNav();
|
|
1607
|
-
const schema: FeatureSchema = {
|
|
1608
|
-
featureName: "lease",
|
|
1609
|
-
entities: {},
|
|
1610
|
-
screens: [editScreen("item")],
|
|
1611
|
-
};
|
|
1612
|
-
renderWithFeatures(sectionWithRowClick, dispatcher, [schema], [], nav);
|
|
1613
|
-
|
|
1614
|
-
await waitFor(() => expect(rtlScreen.getByTestId("row-r1")).toBeTruthy());
|
|
1615
|
-
const editButton = rtlScreen.getByTestId("action-edit-r1");
|
|
1616
|
-
expect(editButton.textContent).toBe("Edit");
|
|
1617
|
-
|
|
1618
|
-
fireEvent.click(editButton);
|
|
1619
|
-
await waitFor(() => expect(navigations).toHaveLength(1));
|
|
1620
|
-
expect(navigations[0]).toEqual({ screenId: "item-edit", entityId: "r1" });
|
|
1621
|
-
});
|
|
1622
|
-
|
|
1623
|
-
test("a declared rowAction with id 'edit' wins — no doubling", async () => {
|
|
1624
|
-
const { dispatcher } = stubDispatcher();
|
|
1625
|
-
const schema: FeatureSchema = {
|
|
1626
|
-
featureName: "lease",
|
|
1627
|
-
entities: {},
|
|
1628
|
-
screens: [editScreen("item")],
|
|
1629
|
-
};
|
|
1630
|
-
renderWithFeatures(
|
|
1631
|
-
{
|
|
1632
|
-
...sectionWithRowClick,
|
|
1633
|
-
rowActions: [{ kind: "navigate", id: "edit", label: "custom-edit", screen: "item-edit" }],
|
|
1634
|
-
},
|
|
1635
|
-
dispatcher,
|
|
1636
|
-
[schema],
|
|
1637
|
-
);
|
|
1638
|
-
|
|
1639
|
-
await waitFor(() => expect(rtlScreen.getByTestId("row-r1")).toBeTruthy());
|
|
1640
|
-
expect(rtlScreen.queryAllByTestId("action-edit-r1")).toHaveLength(1);
|
|
1641
|
-
expect(rtlScreen.getByTestId("action-edit-r1").textContent).toBe("custom-edit");
|
|
1642
|
-
});
|
|
1643
|
-
|
|
1644
|
-
test("no rowClick.entity → no default edit action", async () => {
|
|
1645
|
-
const { dispatcher } = stubDispatcher();
|
|
1646
|
-
const schema: FeatureSchema = {
|
|
1647
|
-
featureName: "lease",
|
|
1648
|
-
entities: {},
|
|
1649
|
-
screens: [editScreen("item")],
|
|
1650
|
-
};
|
|
1651
|
-
renderWithFeatures({ ...sectionWithRowClick, rowClick: undefined }, dispatcher, [schema]);
|
|
1652
|
-
|
|
1653
|
-
await waitFor(() => expect(rtlScreen.getByTestId("row-r1")).toBeTruthy());
|
|
1654
|
-
expect(rtlScreen.queryByTestId("action-edit-r1")).toBeNull();
|
|
1655
|
-
});
|
|
1656
|
-
|
|
1657
|
-
test("access denied to the entityEdit screen → no default edit action", async () => {
|
|
1658
|
-
const { dispatcher } = stubDispatcher();
|
|
1659
|
-
const schema: FeatureSchema = {
|
|
1660
|
-
featureName: "lease",
|
|
1661
|
-
entities: {},
|
|
1662
|
-
screens: [editScreen("item", ["admin"])],
|
|
1663
|
-
};
|
|
1664
|
-
renderWithFeatures(sectionWithRowClick, dispatcher, [schema]);
|
|
1665
|
-
|
|
1666
|
-
await waitFor(() => expect(rtlScreen.getByTestId("row-r1")).toBeTruthy());
|
|
1667
|
-
expect(rtlScreen.queryByTestId("action-edit-r1")).toBeNull();
|
|
1668
|
-
});
|
|
1669
|
-
});
|