@izumisy-tailor/tailor-data-viewer 0.2.18 → 0.2.20
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 +1 -1
- package/src/component/collection/collection-provider.tsx +1 -1
- package/src/component/column-selector.test.tsx +120 -0
- package/src/component/column-selector.tsx +30 -32
- package/src/component/components.test.tsx +88 -36
- package/src/component/csv-button.test.tsx +122 -0
- package/src/component/csv-button.tsx +8 -7
- package/src/component/data-table/data-table-context.tsx +22 -9
- package/src/component/data-table/{index.tsx → data-table.tsx} +141 -117
- package/src/component/data-table/use-data-table.test.ts +6 -29
- package/src/component/data-table/use-data-table.ts +25 -56
- package/src/component/index.ts +1 -8
- package/src/component/pagination.test.tsx +129 -0
- package/src/component/pagination.tsx +12 -10
- package/src/component/search-filter-form.test.tsx +222 -0
- package/src/component/search-filter-form.tsx +20 -15
- package/src/component/types.ts +8 -53
- package/src/tests/helpers.tsx +131 -0
|
@@ -5,8 +5,9 @@ import {
|
|
|
5
5
|
type ReactNode,
|
|
6
6
|
} from "react";
|
|
7
7
|
import { cn } from "../lib/utils";
|
|
8
|
+
import { CollectionProvider } from "../collection/collection-provider";
|
|
8
9
|
import { Table } from "../table";
|
|
9
|
-
import type { Column,
|
|
10
|
+
import type { Column, UseDataTableReturn } from "../types";
|
|
10
11
|
import {
|
|
11
12
|
DataTableContext,
|
|
12
13
|
type DataTableContextValue,
|
|
@@ -17,61 +18,102 @@ import {
|
|
|
17
18
|
// =============================================================================
|
|
18
19
|
|
|
19
20
|
/**
|
|
20
|
-
*
|
|
21
|
+
* Root container for the DataTable compound component.
|
|
22
|
+
*
|
|
23
|
+
* Must be used within `DataTable.Provider`.
|
|
24
|
+
* Renders a `<Table.Root>` wrapper; all data is read from context.
|
|
21
25
|
*/
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
}
|
|
26
|
+
function DataTableRoot({
|
|
27
|
+
children,
|
|
28
|
+
className,
|
|
29
|
+
}: {
|
|
30
|
+
children: ReactNode;
|
|
31
|
+
className?: string;
|
|
32
|
+
}) {
|
|
33
|
+
return <Table.Root className={className}>{children}</Table.Root>;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// =============================================================================
|
|
37
|
+
// DataTable.Provider
|
|
38
|
+
// =============================================================================
|
|
30
39
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
40
|
+
/**
|
|
41
|
+
* Provider that shares `useDataTable()` state via React Context.
|
|
42
|
+
*
|
|
43
|
+
* Internally provides both `DataTableContext` and `CollectionContext`
|
|
44
|
+
* so that utility components (`Pagination`, `ColumnSelector`,
|
|
45
|
+
* `SearchFilterForm`, `CsvButton`) can consume data without explicit props.
|
|
46
|
+
*
|
|
47
|
+
* @example
|
|
48
|
+
* ```tsx
|
|
49
|
+
* const collection = useCollection({ query: GET_ORDERS, params: { pageSize: 20 } });
|
|
50
|
+
* const [result] = useQuery({ ...collection.toQueryArgs() });
|
|
51
|
+
* const table = useDataTable({ columns, data: result.data?.orders, collection });
|
|
52
|
+
*
|
|
53
|
+
* <DataTable.Provider value={table}>
|
|
54
|
+
* <SearchFilterForm />
|
|
55
|
+
* <ColumnSelector />
|
|
56
|
+
* <DataTable.Root>
|
|
57
|
+
* <DataTable.Headers />
|
|
58
|
+
* <DataTable.Body />
|
|
59
|
+
* </DataTable.Root>
|
|
60
|
+
* <Pagination />
|
|
61
|
+
* </DataTable.Provider>
|
|
62
|
+
* ```
|
|
63
|
+
*/
|
|
64
|
+
function DataTableProviderComponent<TRow extends Record<string, unknown>>({
|
|
65
|
+
value,
|
|
39
66
|
children,
|
|
40
|
-
}:
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
67
|
+
}: {
|
|
68
|
+
value: UseDataTableReturn<TRow>;
|
|
69
|
+
children: ReactNode;
|
|
70
|
+
}) {
|
|
71
|
+
const dataTableValue: DataTableContextValue<TRow> = {
|
|
72
|
+
columns: value.columns,
|
|
73
|
+
rows: value.rows,
|
|
74
|
+
loading: value.loading,
|
|
75
|
+
error: value.error,
|
|
76
|
+
sortStates: value.sortStates ?? [],
|
|
77
|
+
onSort: value.onSort,
|
|
78
|
+
updateRow: value.updateRow,
|
|
79
|
+
deleteRow: value.deleteRow,
|
|
80
|
+
insertRow: value.insertRow,
|
|
81
|
+
visibleColumns: value.visibleColumns,
|
|
82
|
+
isColumnVisible: value.isColumnVisible,
|
|
83
|
+
toggleColumn: value.toggleColumn,
|
|
84
|
+
showAllColumns: value.showAllColumns,
|
|
85
|
+
hideAllColumns: value.hideAllColumns,
|
|
86
|
+
pageInfo: value.pageInfo,
|
|
54
87
|
};
|
|
55
88
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
89
|
+
const collectionValue = value.collection ?? null;
|
|
90
|
+
|
|
91
|
+
const inner = (
|
|
92
|
+
<DataTableContext.Provider value={dataTableValue}>
|
|
93
|
+
{children}
|
|
59
94
|
</DataTableContext.Provider>
|
|
60
95
|
);
|
|
96
|
+
|
|
97
|
+
// Wrap with CollectionContext when collection is available
|
|
98
|
+
if (collectionValue) {
|
|
99
|
+
return (
|
|
100
|
+
<CollectionProvider value={collectionValue}>{inner}</CollectionProvider>
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return inner;
|
|
61
105
|
}
|
|
62
106
|
|
|
63
107
|
// =============================================================================
|
|
64
108
|
// DataTable.Headers
|
|
65
109
|
// =============================================================================
|
|
66
110
|
|
|
67
|
-
|
|
68
|
-
className?: string;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
function DataTableHeaders({ className }: DataTableHeadersProps) {
|
|
111
|
+
function DataTableHeaders({ className }: { className?: string }) {
|
|
72
112
|
const ctx = useContext(DataTableContext);
|
|
73
113
|
if (!ctx) {
|
|
74
|
-
throw new Error(
|
|
114
|
+
throw new Error(
|
|
115
|
+
"<DataTable.Headers> must be used within <DataTable.Provider>",
|
|
116
|
+
);
|
|
75
117
|
}
|
|
76
118
|
const { columns, sortStates, onSort } = ctx;
|
|
77
119
|
|
|
@@ -129,15 +171,18 @@ function SortIndicator({ direction }: { direction: "Asc" | "Desc" }) {
|
|
|
129
171
|
// DataTable.Body
|
|
130
172
|
// =============================================================================
|
|
131
173
|
|
|
132
|
-
|
|
174
|
+
function DataTableBody({
|
|
175
|
+
children,
|
|
176
|
+
className,
|
|
177
|
+
}: {
|
|
133
178
|
children?: ReactNode;
|
|
134
179
|
className?: string;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
function DataTableBody({ children, className }: DataTableBodyProps) {
|
|
180
|
+
}) {
|
|
138
181
|
const ctx = useContext(DataTableContext);
|
|
139
182
|
if (!ctx) {
|
|
140
|
-
throw new Error(
|
|
183
|
+
throw new Error(
|
|
184
|
+
"<DataTable.Body> must be used within <DataTable.Provider>",
|
|
185
|
+
);
|
|
141
186
|
}
|
|
142
187
|
const { columns, rows, loading, error } = ctx;
|
|
143
188
|
|
|
@@ -180,19 +225,19 @@ function DataTableBody({ children, className }: DataTableBodyProps) {
|
|
|
180
225
|
</Table.Row>
|
|
181
226
|
)}
|
|
182
227
|
{rows?.map((row, rowIndex) => (
|
|
183
|
-
<
|
|
228
|
+
<Table.Row key={rowIndex}>
|
|
184
229
|
{columns?.map((col) => {
|
|
185
230
|
const key = col.kind === "field" ? col.dataKey : col.id;
|
|
186
231
|
return (
|
|
187
|
-
<
|
|
232
|
+
<Table.Cell
|
|
188
233
|
key={key}
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
234
|
+
style={col.width ? { width: col.width } : undefined}
|
|
235
|
+
>
|
|
236
|
+
{resolveContent(row, col, rowIndex)}
|
|
237
|
+
</Table.Cell>
|
|
193
238
|
);
|
|
194
239
|
})}
|
|
195
|
-
</
|
|
240
|
+
</Table.Row>
|
|
196
241
|
))}
|
|
197
242
|
</Table.Body>
|
|
198
243
|
);
|
|
@@ -202,82 +247,54 @@ function DataTableBody({ children, className }: DataTableBodyProps) {
|
|
|
202
247
|
// DataTable.Row
|
|
203
248
|
// =============================================================================
|
|
204
249
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
function DataTableRow<TRow extends Record<string, unknown>>({
|
|
213
|
-
children,
|
|
214
|
-
...restProps
|
|
215
|
-
}: DataTableRowComponentProps<TRow>) {
|
|
216
|
-
return <Table.Row {...restProps}>{children}</Table.Row>;
|
|
250
|
+
/**
|
|
251
|
+
* Thin wrapper around `Table.Row` for use in custom rendering within
|
|
252
|
+
* `DataTable.Body`.
|
|
253
|
+
*/
|
|
254
|
+
function DataTableRow(props: ComponentProps<"tr">) {
|
|
255
|
+
return <Table.Row {...props} />;
|
|
217
256
|
}
|
|
218
257
|
|
|
219
258
|
// =============================================================================
|
|
220
259
|
// DataTable.Cell
|
|
221
260
|
// =============================================================================
|
|
222
261
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
262
|
+
/**
|
|
263
|
+
* Thin wrapper around `Table.Cell` for use in custom rendering within
|
|
264
|
+
* `DataTable.Body`.
|
|
265
|
+
*/
|
|
266
|
+
function DataTableCell(props: ComponentProps<"td">) {
|
|
267
|
+
return <Table.Cell {...props} />;
|
|
229
268
|
}
|
|
230
269
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
rowIndex = 0,
|
|
235
|
-
children,
|
|
236
|
-
className,
|
|
237
|
-
...restProps
|
|
238
|
-
}: DataTableCellComponentProps<TRow>) {
|
|
239
|
-
const content = resolveContent(children, row, column, rowIndex);
|
|
240
|
-
|
|
241
|
-
return (
|
|
242
|
-
<Table.Cell
|
|
243
|
-
className={className}
|
|
244
|
-
style={column?.width ? { width: column.width } : undefined}
|
|
245
|
-
{...restProps}
|
|
246
|
-
>
|
|
247
|
-
{content}
|
|
248
|
-
</Table.Cell>
|
|
249
|
-
);
|
|
250
|
-
}
|
|
270
|
+
// =============================================================================
|
|
271
|
+
// Helpers
|
|
272
|
+
// =============================================================================
|
|
251
273
|
|
|
252
274
|
/**
|
|
253
|
-
* Resolve cell content from
|
|
275
|
+
* Resolve cell content from row data and column definition.
|
|
254
276
|
*/
|
|
255
277
|
function resolveContent<TRow extends Record<string, unknown>>(
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
column: Column<TRow> | undefined,
|
|
278
|
+
row: TRow,
|
|
279
|
+
column: Column<TRow>,
|
|
259
280
|
rowIndex: number,
|
|
260
281
|
): ReactNode {
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
});
|
|
272
|
-
}
|
|
273
|
-
return formatValue(value);
|
|
282
|
+
switch (column.kind) {
|
|
283
|
+
case "field": {
|
|
284
|
+
const value = row[column.dataKey];
|
|
285
|
+
if (column.renderer) {
|
|
286
|
+
return createElement(column.renderer, {
|
|
287
|
+
value,
|
|
288
|
+
row,
|
|
289
|
+
rowIndex,
|
|
290
|
+
column,
|
|
291
|
+
});
|
|
274
292
|
}
|
|
275
|
-
|
|
276
|
-
return column.render(row);
|
|
293
|
+
return formatValue(value);
|
|
277
294
|
}
|
|
295
|
+
case "display":
|
|
296
|
+
return column.render(row);
|
|
278
297
|
}
|
|
279
|
-
|
|
280
|
-
return children;
|
|
281
298
|
}
|
|
282
299
|
|
|
283
300
|
/**
|
|
@@ -298,19 +315,26 @@ function formatValue(value: unknown): ReactNode {
|
|
|
298
315
|
/**
|
|
299
316
|
* Data-bound table compound component.
|
|
300
317
|
*
|
|
301
|
-
* Use with `useDataTable()` hook
|
|
318
|
+
* Use with `useDataTable()` hook and wrap with `DataTable.Provider`
|
|
319
|
+
* for context-based data flow.
|
|
302
320
|
*
|
|
303
321
|
* @example
|
|
304
322
|
* ```tsx
|
|
305
323
|
* const table = useDataTable({ columns, data, loading, collection });
|
|
306
324
|
*
|
|
307
|
-
* <DataTable.
|
|
308
|
-
* <
|
|
309
|
-
* <
|
|
310
|
-
*
|
|
325
|
+
* <DataTable.Provider value={table}>
|
|
326
|
+
* <SearchFilterForm />
|
|
327
|
+
* <ColumnSelector />
|
|
328
|
+
* <DataTable.Root>
|
|
329
|
+
* <DataTable.Headers />
|
|
330
|
+
* <DataTable.Body />
|
|
331
|
+
* </DataTable.Root>
|
|
332
|
+
* <Pagination />
|
|
333
|
+
* </DataTable.Provider>
|
|
311
334
|
* ```
|
|
312
335
|
*/
|
|
313
336
|
export const DataTable = {
|
|
337
|
+
Provider: DataTableProviderComponent,
|
|
314
338
|
Root: DataTableRoot,
|
|
315
339
|
Headers: DataTableHeaders,
|
|
316
340
|
Body: DataTableBody,
|
|
@@ -234,46 +234,23 @@ describe("useDataTable", () => {
|
|
|
234
234
|
});
|
|
235
235
|
|
|
236
236
|
// ---------------------------------------------------------------------------
|
|
237
|
-
//
|
|
237
|
+
// Sort state
|
|
238
238
|
// ---------------------------------------------------------------------------
|
|
239
|
-
describe("
|
|
240
|
-
it("
|
|
241
|
-
const { result } = renderHook(() =>
|
|
242
|
-
useDataTable<TestRow>({
|
|
243
|
-
columns: testColumns,
|
|
244
|
-
data: testData,
|
|
245
|
-
loading: true,
|
|
246
|
-
}),
|
|
247
|
-
);
|
|
248
|
-
|
|
249
|
-
const { rootProps } = result.current;
|
|
250
|
-
expect(rootProps.columns).toHaveLength(4);
|
|
251
|
-
expect(rootProps.rows).toHaveLength(3);
|
|
252
|
-
expect(rootProps.loading).toBe(true);
|
|
253
|
-
});
|
|
254
|
-
|
|
255
|
-
it("getRowProps returns row", () => {
|
|
239
|
+
describe("sort state", () => {
|
|
240
|
+
it("sortStates is empty when no collection is provided", () => {
|
|
256
241
|
const { result } = renderHook(() =>
|
|
257
242
|
useDataTable<TestRow>({ columns: testColumns, data: testData }),
|
|
258
243
|
);
|
|
259
244
|
|
|
260
|
-
|
|
261
|
-
expect(rowProps.row.name).toBe("Alice");
|
|
245
|
+
expect(result.current.sortStates).toEqual([]);
|
|
262
246
|
});
|
|
263
247
|
|
|
264
|
-
it("
|
|
248
|
+
it("onSort is undefined when no collection is provided", () => {
|
|
265
249
|
const { result } = renderHook(() =>
|
|
266
250
|
useDataTable<TestRow>({ columns: testColumns, data: testData }),
|
|
267
251
|
);
|
|
268
252
|
|
|
269
|
-
|
|
270
|
-
result.current.rows[0],
|
|
271
|
-
testColumns[0],
|
|
272
|
-
0,
|
|
273
|
-
);
|
|
274
|
-
expect(cellProps.row.name).toBe("Alice");
|
|
275
|
-
expect(cellProps.column).toBe(testColumns[0]);
|
|
276
|
-
expect(cellProps.rowIndex).toBe(0);
|
|
253
|
+
expect(result.current.onSort).toBeUndefined();
|
|
277
254
|
});
|
|
278
255
|
});
|
|
279
256
|
});
|
|
@@ -1,17 +1,15 @@
|
|
|
1
1
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
2
2
|
import type {
|
|
3
3
|
Column,
|
|
4
|
-
DataTableCellProps,
|
|
5
|
-
DataTableRootProps,
|
|
6
|
-
DataTableRowProps,
|
|
7
4
|
PageInfo,
|
|
5
|
+
SortState,
|
|
8
6
|
UseDataTableOptions,
|
|
9
7
|
UseDataTableReturn,
|
|
10
8
|
} from "../types";
|
|
11
9
|
|
|
12
10
|
/**
|
|
13
11
|
* Hook that integrates data management, column visibility, row operations, and
|
|
14
|
-
*
|
|
12
|
+
* sort/pagination state for the `DataTable.*` compound component.
|
|
15
13
|
*
|
|
16
14
|
* @example
|
|
17
15
|
* ```tsx
|
|
@@ -25,10 +23,12 @@ import type {
|
|
|
25
23
|
* collection,
|
|
26
24
|
* });
|
|
27
25
|
*
|
|
28
|
-
* <DataTable.
|
|
29
|
-
* <DataTable.
|
|
30
|
-
*
|
|
31
|
-
*
|
|
26
|
+
* <DataTable.Provider value={table}>
|
|
27
|
+
* <DataTable.Root>
|
|
28
|
+
* <DataTable.Headers />
|
|
29
|
+
* <DataTable.Body />
|
|
30
|
+
* </DataTable.Root>
|
|
31
|
+
* </DataTable.Provider>
|
|
32
32
|
* ```
|
|
33
33
|
*/
|
|
34
34
|
export function useDataTable<TRow extends Record<string, unknown>>(
|
|
@@ -194,64 +194,30 @@ export function useDataTable<TRow extends Record<string, unknown>>(
|
|
|
194
194
|
);
|
|
195
195
|
|
|
196
196
|
// ---------------------------------------------------------------------------
|
|
197
|
-
//
|
|
197
|
+
// Sort (delegated from collection)
|
|
198
198
|
// ---------------------------------------------------------------------------
|
|
199
|
-
const
|
|
200
|
-
return
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
rowOperations: { updateRow, deleteRow, insertRow },
|
|
211
|
-
children: null,
|
|
212
|
-
};
|
|
213
|
-
}, [
|
|
214
|
-
visibleColumns,
|
|
215
|
-
rows,
|
|
216
|
-
loading,
|
|
217
|
-
error,
|
|
218
|
-
collection,
|
|
219
|
-
updateRow,
|
|
220
|
-
deleteRow,
|
|
221
|
-
insertRow,
|
|
222
|
-
]);
|
|
223
|
-
|
|
224
|
-
const getRowProps = useCallback(
|
|
225
|
-
(row: TRow): DataTableRowProps<TRow> => ({ row }),
|
|
226
|
-
[],
|
|
227
|
-
);
|
|
228
|
-
|
|
229
|
-
const getCellProps = useCallback(
|
|
230
|
-
(
|
|
231
|
-
row: TRow,
|
|
232
|
-
column: Column<TRow>,
|
|
233
|
-
rowIndex: number,
|
|
234
|
-
): DataTableCellProps<TRow> => ({
|
|
235
|
-
row,
|
|
236
|
-
column,
|
|
237
|
-
rowIndex,
|
|
238
|
-
}),
|
|
239
|
-
[],
|
|
240
|
-
);
|
|
199
|
+
const sortStates = useMemo<SortState[]>(() => {
|
|
200
|
+
return collection?.sortStates ?? [];
|
|
201
|
+
}, [collection?.sortStates]);
|
|
202
|
+
|
|
203
|
+
const onSort = useMemo<
|
|
204
|
+
((field: string, direction?: "Asc" | "Desc") => void) | undefined
|
|
205
|
+
>(() => {
|
|
206
|
+
if (!collection) return undefined;
|
|
207
|
+
return (field: string, direction?: "Asc" | "Desc") =>
|
|
208
|
+
collection.setSort(field, direction);
|
|
209
|
+
}, [collection]);
|
|
241
210
|
|
|
242
211
|
// ---------------------------------------------------------------------------
|
|
243
212
|
// Return
|
|
244
213
|
// ---------------------------------------------------------------------------
|
|
245
214
|
return {
|
|
246
|
-
// Props generators
|
|
247
|
-
rootProps,
|
|
248
|
-
getRowProps,
|
|
249
|
-
getCellProps,
|
|
250
|
-
|
|
251
215
|
// Data
|
|
252
216
|
rows,
|
|
253
217
|
loading,
|
|
254
218
|
error,
|
|
219
|
+
sortStates,
|
|
220
|
+
onSort,
|
|
255
221
|
|
|
256
222
|
// Pagination
|
|
257
223
|
pageInfo,
|
|
@@ -272,5 +238,8 @@ export function useDataTable<TRow extends Record<string, unknown>>(
|
|
|
272
238
|
updateRow,
|
|
273
239
|
deleteRow,
|
|
274
240
|
insertRow,
|
|
241
|
+
|
|
242
|
+
// Collection (passthrough for DataTable.Provider)
|
|
243
|
+
collection,
|
|
275
244
|
};
|
|
276
245
|
}
|
package/src/component/index.ts
CHANGED
|
@@ -23,9 +23,6 @@ export type {
|
|
|
23
23
|
UseDataTableOptions,
|
|
24
24
|
UseDataTableReturn,
|
|
25
25
|
RowOperations,
|
|
26
|
-
DataTableRootProps,
|
|
27
|
-
DataTableRowProps,
|
|
28
|
-
DataTableCellProps,
|
|
29
26
|
FieldName,
|
|
30
27
|
TableFieldName,
|
|
31
28
|
OrderableFieldName,
|
|
@@ -40,11 +37,7 @@ export type {
|
|
|
40
37
|
MetadataFieldsOptions,
|
|
41
38
|
MetadataFilter,
|
|
42
39
|
TableMetadataFilter,
|
|
43
|
-
ColumnSelectorProps,
|
|
44
|
-
CsvButtonProps,
|
|
45
|
-
SearchFilterFormProps,
|
|
46
40
|
SearchFilterLabels,
|
|
47
|
-
PaginationProps,
|
|
48
41
|
} from "./types";
|
|
49
42
|
|
|
50
43
|
export {
|
|
@@ -66,7 +59,7 @@ export {
|
|
|
66
59
|
export { Table } from "./table";
|
|
67
60
|
|
|
68
61
|
// DataTable (data-bound)
|
|
69
|
-
export { DataTable } from "./data-table/
|
|
62
|
+
export { DataTable } from "./data-table/data-table";
|
|
70
63
|
export { useDataTable } from "./data-table/use-data-table";
|
|
71
64
|
export { useDataTableContext } from "./data-table/data-table-context";
|
|
72
65
|
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { render, screen, fireEvent } from "@testing-library/react";
|
|
2
|
+
import { describe, it, expect, vi } from "vitest";
|
|
3
|
+
import type { Column } from "./types";
|
|
4
|
+
import { createTestProviders } from "../tests/helpers";
|
|
5
|
+
import { Pagination } from "./pagination";
|
|
6
|
+
|
|
7
|
+
// =============================================================================
|
|
8
|
+
// Fixtures
|
|
9
|
+
// =============================================================================
|
|
10
|
+
|
|
11
|
+
type TestRow = { id: string; name: string; status: string };
|
|
12
|
+
|
|
13
|
+
const testColumns: Column<TestRow>[] = [
|
|
14
|
+
{ kind: "field", dataKey: "name", label: "Name" },
|
|
15
|
+
{ kind: "field", dataKey: "status", label: "Status" },
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
const testRows: TestRow[] = [
|
|
19
|
+
{ id: "1", name: "Alice", status: "active" },
|
|
20
|
+
{ id: "2", name: "Bob", status: "inactive" },
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
const TestProviders = createTestProviders({
|
|
24
|
+
columns: testColumns,
|
|
25
|
+
rows: testRows,
|
|
26
|
+
dataTableDefaults: {
|
|
27
|
+
pageInfo: {
|
|
28
|
+
hasNextPage: true,
|
|
29
|
+
endCursor: "cursor-end",
|
|
30
|
+
hasPreviousPage: true,
|
|
31
|
+
startCursor: "cursor-start",
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
collectionDefaults: {
|
|
35
|
+
hasPrevPage: true,
|
|
36
|
+
hasNextPage: true,
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
// =============================================================================
|
|
41
|
+
// Tests
|
|
42
|
+
// =============================================================================
|
|
43
|
+
|
|
44
|
+
describe("Pagination", () => {
|
|
45
|
+
it("renders Previous and Next buttons", () => {
|
|
46
|
+
render(
|
|
47
|
+
<TestProviders>
|
|
48
|
+
<Pagination />
|
|
49
|
+
</TestProviders>,
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
expect(screen.getByText("Previous")).toBeInTheDocument();
|
|
53
|
+
expect(screen.getByText("Next")).toBeInTheDocument();
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("disables Previous when hasPrevPage is false", () => {
|
|
57
|
+
render(
|
|
58
|
+
<TestProviders collection={{ hasPrevPage: false }}>
|
|
59
|
+
<Pagination />
|
|
60
|
+
</TestProviders>,
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
expect(screen.getByText("Previous")).toBeDisabled();
|
|
64
|
+
expect(screen.getByText("Next")).toBeEnabled();
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("disables Next when hasNextPage is false", () => {
|
|
68
|
+
render(
|
|
69
|
+
<TestProviders collection={{ hasNextPage: false }}>
|
|
70
|
+
<Pagination />
|
|
71
|
+
</TestProviders>,
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
expect(screen.getByText("Previous")).toBeEnabled();
|
|
75
|
+
expect(screen.getByText("Next")).toBeDisabled();
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("calls nextPage with endCursor when Next is clicked", () => {
|
|
79
|
+
const nextPage = vi.fn();
|
|
80
|
+
render(
|
|
81
|
+
<TestProviders collection={{ nextPage }}>
|
|
82
|
+
<Pagination />
|
|
83
|
+
</TestProviders>,
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
fireEvent.click(screen.getByText("Next"));
|
|
87
|
+
expect(nextPage).toHaveBeenCalledWith("cursor-end");
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("calls prevPage with startCursor when Previous is clicked", () => {
|
|
91
|
+
const prevPage = vi.fn();
|
|
92
|
+
render(
|
|
93
|
+
<TestProviders collection={{ prevPage }}>
|
|
94
|
+
<Pagination />
|
|
95
|
+
</TestProviders>,
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
fireEvent.click(screen.getByText("Previous"));
|
|
99
|
+
expect(prevPage).toHaveBeenCalledWith("cursor-start");
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("does not call nextPage when endCursor is null", () => {
|
|
103
|
+
const nextPage = vi.fn();
|
|
104
|
+
render(
|
|
105
|
+
<TestProviders
|
|
106
|
+
dataTable={{
|
|
107
|
+
pageInfo: {
|
|
108
|
+
hasNextPage: true,
|
|
109
|
+
endCursor: null,
|
|
110
|
+
hasPreviousPage: false,
|
|
111
|
+
startCursor: null,
|
|
112
|
+
},
|
|
113
|
+
}}
|
|
114
|
+
collection={{ nextPage, hasNextPage: true }}
|
|
115
|
+
>
|
|
116
|
+
<Pagination />
|
|
117
|
+
</TestProviders>,
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
fireEvent.click(screen.getByText("Next"));
|
|
121
|
+
expect(nextPage).not.toHaveBeenCalled();
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it("throws when rendered outside provider", () => {
|
|
125
|
+
console.error = vi.fn();
|
|
126
|
+
expect(() => render(<Pagination />)).toThrow();
|
|
127
|
+
console.error = globalThis.console.error;
|
|
128
|
+
});
|
|
129
|
+
});
|