@izumisy-tailor/tailor-data-viewer 0.3.1 → 0.3.2
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 +20 -23
- package/src/component/collection/use-collection.test.ts +67 -67
- package/src/component/collection/use-collection.ts +28 -25
- package/src/component/data-table/data-table.tsx +6 -6
- package/src/component/data-table/pagination.tsx +2 -2
- package/src/component/data-table/search-filter-form.tsx +2 -2
- package/src/component/data-table/use-data-table.test.ts +2 -2
- package/src/component/data-table/use-data-table.ts +22 -27
- package/src/component/index.ts +3 -2
- package/src/component/types.ts +54 -26
- package/src/tests/helpers.tsx +8 -13
package/package.json
CHANGED
|
@@ -1,69 +1,66 @@
|
|
|
1
1
|
import { createContext, useContext, type ReactNode } from "react";
|
|
2
|
-
import type {
|
|
2
|
+
import type { CollectionControl } from "../types";
|
|
3
3
|
|
|
4
|
-
const
|
|
5
|
-
string,
|
|
6
|
-
unknown
|
|
7
|
-
> | null>(null);
|
|
4
|
+
const CollectionControlContext = createContext<CollectionControl | null>(null);
|
|
8
5
|
|
|
9
6
|
/**
|
|
10
|
-
* Provider that shares collection
|
|
7
|
+
* Provider that shares collection control state via React Context.
|
|
11
8
|
*
|
|
12
9
|
* @example
|
|
13
10
|
* ```tsx
|
|
14
|
-
* const { variables,
|
|
11
|
+
* const { variables, control } = useCollectionVariables({ params: { pageSize: 20 } });
|
|
15
12
|
*
|
|
16
|
-
* <
|
|
13
|
+
* <CollectionControlProvider value={control}>
|
|
17
14
|
* <FilterPanel />
|
|
18
15
|
* <DataTable.Root>...</DataTable.Root>
|
|
19
16
|
* <Pagination />
|
|
20
|
-
* </
|
|
17
|
+
* </CollectionControlProvider>
|
|
21
18
|
* ```
|
|
22
19
|
*/
|
|
23
|
-
export function
|
|
20
|
+
export function CollectionControlProvider({
|
|
24
21
|
value,
|
|
25
22
|
children,
|
|
26
23
|
}: {
|
|
27
|
-
value:
|
|
24
|
+
value: CollectionControl;
|
|
28
25
|
children: ReactNode;
|
|
29
26
|
}) {
|
|
30
27
|
return (
|
|
31
|
-
<
|
|
28
|
+
<CollectionControlContext.Provider value={value}>
|
|
32
29
|
{children}
|
|
33
|
-
</
|
|
30
|
+
</CollectionControlContext.Provider>
|
|
34
31
|
);
|
|
35
32
|
}
|
|
36
33
|
|
|
37
34
|
/**
|
|
38
|
-
* Hook to access collection
|
|
35
|
+
* Hook to access collection control from the nearest `CollectionControlProvider`.
|
|
39
36
|
*
|
|
40
|
-
* Returns the
|
|
37
|
+
* Returns the `CollectionControl` interface. Pass a `TFieldName`
|
|
41
38
|
* type parameter to narrow method arguments like `addFilter` / `setSort`.
|
|
42
39
|
*
|
|
43
40
|
* @typeParam TFieldName - Union of allowed field name strings (default: `string`).
|
|
44
41
|
*
|
|
45
|
-
* @throws Error if used outside of `
|
|
42
|
+
* @throws Error if used outside of `CollectionControlProvider`.
|
|
46
43
|
*
|
|
47
44
|
* @example
|
|
48
45
|
* ```tsx
|
|
49
46
|
* function StatusFilter() {
|
|
50
|
-
* const { filters, addFilter, removeFilter } =
|
|
47
|
+
* const { filters, addFilter, removeFilter } = useCollectionControl();
|
|
51
48
|
* // ...
|
|
52
49
|
* }
|
|
53
50
|
*
|
|
54
51
|
* // With typed field names:
|
|
55
52
|
* type TaskField = FieldName<typeof tableMetadata, "task">;
|
|
56
|
-
* const { addFilter } =
|
|
53
|
+
* const { addFilter } = useCollectionControl<TaskField>();
|
|
57
54
|
* ```
|
|
58
55
|
*/
|
|
59
|
-
export function
|
|
56
|
+
export function useCollectionControl<
|
|
60
57
|
TFieldName extends string = string,
|
|
61
|
-
>():
|
|
62
|
-
const ctx = useContext(
|
|
58
|
+
>(): CollectionControl<TFieldName> {
|
|
59
|
+
const ctx = useContext(CollectionControlContext);
|
|
63
60
|
if (!ctx) {
|
|
64
61
|
throw new Error(
|
|
65
|
-
"
|
|
62
|
+
"useCollectionControl must be used within <CollectionControlProvider>",
|
|
66
63
|
);
|
|
67
64
|
}
|
|
68
|
-
return ctx as
|
|
65
|
+
return ctx as CollectionControl<TFieldName>;
|
|
69
66
|
}
|
|
@@ -13,10 +13,10 @@ describe("useCollectionVariables", () => {
|
|
|
13
13
|
expect(result.current.variables.pagination).toEqual({ first: 20 });
|
|
14
14
|
expect(result.current.variables.query).toBeUndefined();
|
|
15
15
|
expect(result.current.variables.order).toBeUndefined();
|
|
16
|
-
expect(result.current.filters).toEqual([]);
|
|
17
|
-
expect(result.current.sortStates).toEqual([]);
|
|
18
|
-
expect(result.current.cursor).toBeNull();
|
|
19
|
-
expect(result.current.hasPrevPage).toBe(false);
|
|
16
|
+
expect(result.current.control.filters).toEqual([]);
|
|
17
|
+
expect(result.current.control.sortStates).toEqual([]);
|
|
18
|
+
expect(result.current.control.cursor).toBeNull();
|
|
19
|
+
expect(result.current.control.hasPrevPage).toBe(false);
|
|
20
20
|
});
|
|
21
21
|
|
|
22
22
|
it("uses custom pageSize", () => {
|
|
@@ -34,7 +34,7 @@ describe("useCollectionVariables", () => {
|
|
|
34
34
|
},
|
|
35
35
|
}),
|
|
36
36
|
);
|
|
37
|
-
expect(result.current.sortStates).toEqual([
|
|
37
|
+
expect(result.current.control.sortStates).toEqual([
|
|
38
38
|
{ field: "createdAt", direction: "Desc" },
|
|
39
39
|
]);
|
|
40
40
|
expect(result.current.variables.order).toEqual([
|
|
@@ -56,7 +56,7 @@ describe("useCollectionVariables", () => {
|
|
|
56
56
|
},
|
|
57
57
|
}),
|
|
58
58
|
);
|
|
59
|
-
expect(result.current.filters).toHaveLength(1);
|
|
59
|
+
expect(result.current.control.filters).toHaveLength(1);
|
|
60
60
|
expect(result.current.variables.query).toEqual({
|
|
61
61
|
status: { eq: "ACTIVE" },
|
|
62
62
|
});
|
|
@@ -71,11 +71,11 @@ describe("useCollectionVariables", () => {
|
|
|
71
71
|
const { result } = renderHook(() => useCollectionVariables({}));
|
|
72
72
|
|
|
73
73
|
act(() => {
|
|
74
|
-
result.current.addFilter("status", "eq", "ACTIVE");
|
|
74
|
+
result.current.control.addFilter("status", "eq", "ACTIVE");
|
|
75
75
|
});
|
|
76
76
|
|
|
77
|
-
expect(result.current.filters).toHaveLength(1);
|
|
78
|
-
expect(result.current.filters[0]).toMatchObject({
|
|
77
|
+
expect(result.current.control.filters).toHaveLength(1);
|
|
78
|
+
expect(result.current.control.filters[0]).toMatchObject({
|
|
79
79
|
field: "status",
|
|
80
80
|
operator: "eq",
|
|
81
81
|
value: "ACTIVE",
|
|
@@ -89,21 +89,21 @@ describe("useCollectionVariables", () => {
|
|
|
89
89
|
const { result } = renderHook(() => useCollectionVariables({}));
|
|
90
90
|
|
|
91
91
|
act(() => {
|
|
92
|
-
result.current.addFilter("status", "eq", "ACTIVE");
|
|
92
|
+
result.current.control.addFilter("status", "eq", "ACTIVE");
|
|
93
93
|
});
|
|
94
94
|
act(() => {
|
|
95
|
-
result.current.addFilter("status", "eq", "INACTIVE");
|
|
95
|
+
result.current.control.addFilter("status", "eq", "INACTIVE");
|
|
96
96
|
});
|
|
97
97
|
|
|
98
|
-
expect(result.current.filters).toHaveLength(1);
|
|
99
|
-
expect(result.current.filters[0].value).toBe("INACTIVE");
|
|
98
|
+
expect(result.current.control.filters).toHaveLength(1);
|
|
99
|
+
expect(result.current.control.filters[0].value).toBe("INACTIVE");
|
|
100
100
|
});
|
|
101
101
|
|
|
102
102
|
it("sets filters in bulk", () => {
|
|
103
103
|
const { result } = renderHook(() => useCollectionVariables({}));
|
|
104
104
|
|
|
105
105
|
act(() => {
|
|
106
|
-
result.current.setFilters([
|
|
106
|
+
result.current.control.setFilters([
|
|
107
107
|
{
|
|
108
108
|
field: "status",
|
|
109
109
|
operator: "eq",
|
|
@@ -117,7 +117,7 @@ describe("useCollectionVariables", () => {
|
|
|
117
117
|
]);
|
|
118
118
|
});
|
|
119
119
|
|
|
120
|
-
expect(result.current.filters).toHaveLength(2);
|
|
120
|
+
expect(result.current.control.filters).toHaveLength(2);
|
|
121
121
|
expect(result.current.variables.query).toEqual({
|
|
122
122
|
status: { eq: "ACTIVE" },
|
|
123
123
|
amount: { gte: 1000 },
|
|
@@ -128,29 +128,29 @@ describe("useCollectionVariables", () => {
|
|
|
128
128
|
const { result } = renderHook(() => useCollectionVariables({}));
|
|
129
129
|
|
|
130
130
|
act(() => {
|
|
131
|
-
result.current.addFilter("status", "eq", "ACTIVE");
|
|
132
|
-
result.current.addFilter("amount", "gte", 1000);
|
|
131
|
+
result.current.control.addFilter("status", "eq", "ACTIVE");
|
|
132
|
+
result.current.control.addFilter("amount", "gte", 1000);
|
|
133
133
|
});
|
|
134
134
|
act(() => {
|
|
135
|
-
result.current.removeFilter("status");
|
|
135
|
+
result.current.control.removeFilter("status");
|
|
136
136
|
});
|
|
137
137
|
|
|
138
|
-
expect(result.current.filters).toHaveLength(1);
|
|
139
|
-
expect(result.current.filters[0].field).toBe("amount");
|
|
138
|
+
expect(result.current.control.filters).toHaveLength(1);
|
|
139
|
+
expect(result.current.control.filters[0].field).toBe("amount");
|
|
140
140
|
});
|
|
141
141
|
|
|
142
142
|
it("clears all filters", () => {
|
|
143
143
|
const { result } = renderHook(() => useCollectionVariables({}));
|
|
144
144
|
|
|
145
145
|
act(() => {
|
|
146
|
-
result.current.addFilter("status", "eq", "ACTIVE");
|
|
147
|
-
result.current.addFilter("amount", "gte", 1000);
|
|
146
|
+
result.current.control.addFilter("status", "eq", "ACTIVE");
|
|
147
|
+
result.current.control.addFilter("amount", "gte", 1000);
|
|
148
148
|
});
|
|
149
149
|
act(() => {
|
|
150
|
-
result.current.clearFilters();
|
|
150
|
+
result.current.control.clearFilters();
|
|
151
151
|
});
|
|
152
152
|
|
|
153
|
-
expect(result.current.filters).toHaveLength(0);
|
|
153
|
+
expect(result.current.control.filters).toHaveLength(0);
|
|
154
154
|
expect(result.current.variables.query).toBeUndefined();
|
|
155
155
|
});
|
|
156
156
|
|
|
@@ -159,16 +159,16 @@ describe("useCollectionVariables", () => {
|
|
|
159
159
|
|
|
160
160
|
// Navigate to next page
|
|
161
161
|
act(() => {
|
|
162
|
-
result.current.nextPage("cursor1");
|
|
162
|
+
result.current.control.nextPage("cursor1");
|
|
163
163
|
});
|
|
164
|
-
expect(result.current.cursor).toBe("cursor1");
|
|
164
|
+
expect(result.current.control.cursor).toBe("cursor1");
|
|
165
165
|
|
|
166
166
|
// Adding filter should reset pagination
|
|
167
167
|
act(() => {
|
|
168
|
-
result.current.addFilter("status", "eq", "ACTIVE");
|
|
168
|
+
result.current.control.addFilter("status", "eq", "ACTIVE");
|
|
169
169
|
});
|
|
170
|
-
expect(result.current.cursor).toBeNull();
|
|
171
|
-
expect(result.current.hasPrevPage).toBe(false);
|
|
170
|
+
expect(result.current.control.cursor).toBeNull();
|
|
171
|
+
expect(result.current.control.hasPrevPage).toBe(false);
|
|
172
172
|
});
|
|
173
173
|
});
|
|
174
174
|
|
|
@@ -180,10 +180,10 @@ describe("useCollectionVariables", () => {
|
|
|
180
180
|
const { result } = renderHook(() => useCollectionVariables({}));
|
|
181
181
|
|
|
182
182
|
act(() => {
|
|
183
|
-
result.current.setSort("createdAt", "Desc");
|
|
183
|
+
result.current.control.setSort("createdAt", "Desc");
|
|
184
184
|
});
|
|
185
185
|
|
|
186
|
-
expect(result.current.sortStates).toEqual([
|
|
186
|
+
expect(result.current.control.sortStates).toEqual([
|
|
187
187
|
{ field: "createdAt", direction: "Desc" },
|
|
188
188
|
]);
|
|
189
189
|
expect(result.current.variables.order).toEqual([
|
|
@@ -195,13 +195,13 @@ describe("useCollectionVariables", () => {
|
|
|
195
195
|
const { result } = renderHook(() => useCollectionVariables({}));
|
|
196
196
|
|
|
197
197
|
act(() => {
|
|
198
|
-
result.current.setSort("createdAt", "Desc");
|
|
198
|
+
result.current.control.setSort("createdAt", "Desc");
|
|
199
199
|
});
|
|
200
200
|
act(() => {
|
|
201
|
-
result.current.setSort("name", "Asc");
|
|
201
|
+
result.current.control.setSort("name", "Asc");
|
|
202
202
|
});
|
|
203
203
|
|
|
204
|
-
expect(result.current.sortStates).toEqual([
|
|
204
|
+
expect(result.current.control.sortStates).toEqual([
|
|
205
205
|
{ field: "createdAt", direction: "Desc" },
|
|
206
206
|
{ field: "name", direction: "Asc" },
|
|
207
207
|
]);
|
|
@@ -211,16 +211,16 @@ describe("useCollectionVariables", () => {
|
|
|
211
211
|
const { result } = renderHook(() => useCollectionVariables({}));
|
|
212
212
|
|
|
213
213
|
act(() => {
|
|
214
|
-
result.current.setSort("createdAt", "Desc");
|
|
214
|
+
result.current.control.setSort("createdAt", "Desc");
|
|
215
215
|
});
|
|
216
216
|
act(() => {
|
|
217
|
-
result.current.setSort("name", "Asc");
|
|
217
|
+
result.current.control.setSort("name", "Asc");
|
|
218
218
|
});
|
|
219
219
|
act(() => {
|
|
220
|
-
result.current.setSort("createdAt", "Asc");
|
|
220
|
+
result.current.control.setSort("createdAt", "Asc");
|
|
221
221
|
});
|
|
222
222
|
|
|
223
|
-
expect(result.current.sortStates).toEqual([
|
|
223
|
+
expect(result.current.control.sortStates).toEqual([
|
|
224
224
|
{ field: "name", direction: "Asc" },
|
|
225
225
|
{ field: "createdAt", direction: "Asc" },
|
|
226
226
|
]);
|
|
@@ -230,16 +230,16 @@ describe("useCollectionVariables", () => {
|
|
|
230
230
|
const { result } = renderHook(() => useCollectionVariables({}));
|
|
231
231
|
|
|
232
232
|
act(() => {
|
|
233
|
-
result.current.setSort("createdAt", "Desc");
|
|
233
|
+
result.current.control.setSort("createdAt", "Desc");
|
|
234
234
|
});
|
|
235
235
|
act(() => {
|
|
236
|
-
result.current.setSort("name", "Asc");
|
|
236
|
+
result.current.control.setSort("name", "Asc");
|
|
237
237
|
});
|
|
238
238
|
act(() => {
|
|
239
|
-
result.current.setSort("createdAt");
|
|
239
|
+
result.current.control.setSort("createdAt");
|
|
240
240
|
});
|
|
241
241
|
|
|
242
|
-
expect(result.current.sortStates).toEqual([
|
|
242
|
+
expect(result.current.control.sortStates).toEqual([
|
|
243
243
|
{ field: "name", direction: "Asc" },
|
|
244
244
|
]);
|
|
245
245
|
});
|
|
@@ -248,13 +248,13 @@ describe("useCollectionVariables", () => {
|
|
|
248
248
|
const { result } = renderHook(() => useCollectionVariables({}));
|
|
249
249
|
|
|
250
250
|
act(() => {
|
|
251
|
-
result.current.setSort("createdAt", "Desc");
|
|
251
|
+
result.current.control.setSort("createdAt", "Desc");
|
|
252
252
|
});
|
|
253
253
|
act(() => {
|
|
254
|
-
result.current.clearSort();
|
|
254
|
+
result.current.control.clearSort();
|
|
255
255
|
});
|
|
256
256
|
|
|
257
|
-
expect(result.current.sortStates).toEqual([]);
|
|
257
|
+
expect(result.current.control.sortStates).toEqual([]);
|
|
258
258
|
expect(result.current.variables.order).toBeUndefined();
|
|
259
259
|
});
|
|
260
260
|
});
|
|
@@ -267,11 +267,11 @@ describe("useCollectionVariables", () => {
|
|
|
267
267
|
const { result } = renderHook(() => useCollectionVariables({}));
|
|
268
268
|
|
|
269
269
|
act(() => {
|
|
270
|
-
result.current.nextPage("cursor1");
|
|
270
|
+
result.current.control.nextPage("cursor1");
|
|
271
271
|
});
|
|
272
272
|
|
|
273
|
-
expect(result.current.cursor).toBe("cursor1");
|
|
274
|
-
expect(result.current.paginationDirection).toBe("forward");
|
|
273
|
+
expect(result.current.control.cursor).toBe("cursor1");
|
|
274
|
+
expect(result.current.control.paginationDirection).toBe("forward");
|
|
275
275
|
expect(result.current.variables.pagination.after).toBe("cursor1");
|
|
276
276
|
expect(result.current.variables.pagination.first).toBe(20);
|
|
277
277
|
expect(result.current.variables.pagination.last).toBeUndefined();
|
|
@@ -282,11 +282,11 @@ describe("useCollectionVariables", () => {
|
|
|
282
282
|
const { result } = renderHook(() => useCollectionVariables({}));
|
|
283
283
|
|
|
284
284
|
act(() => {
|
|
285
|
-
result.current.prevPage("cursor1");
|
|
285
|
+
result.current.control.prevPage("cursor1");
|
|
286
286
|
});
|
|
287
287
|
|
|
288
|
-
expect(result.current.cursor).toBe("cursor1");
|
|
289
|
-
expect(result.current.paginationDirection).toBe("backward");
|
|
288
|
+
expect(result.current.control.cursor).toBe("cursor1");
|
|
289
|
+
expect(result.current.control.paginationDirection).toBe("backward");
|
|
290
290
|
expect(result.current.variables.pagination.before).toBe("cursor1");
|
|
291
291
|
expect(result.current.variables.pagination.last).toBe(20);
|
|
292
292
|
expect(result.current.variables.pagination.first).toBeUndefined();
|
|
@@ -297,14 +297,14 @@ describe("useCollectionVariables", () => {
|
|
|
297
297
|
const { result } = renderHook(() => useCollectionVariables({}));
|
|
298
298
|
|
|
299
299
|
act(() => {
|
|
300
|
-
result.current.prevPage("cursorB");
|
|
300
|
+
result.current.control.prevPage("cursorB");
|
|
301
301
|
});
|
|
302
|
-
expect(result.current.paginationDirection).toBe("backward");
|
|
302
|
+
expect(result.current.control.paginationDirection).toBe("backward");
|
|
303
303
|
|
|
304
304
|
act(() => {
|
|
305
|
-
result.current.nextPage("cursorA");
|
|
305
|
+
result.current.control.nextPage("cursorA");
|
|
306
306
|
});
|
|
307
|
-
expect(result.current.paginationDirection).toBe("forward");
|
|
307
|
+
expect(result.current.control.paginationDirection).toBe("forward");
|
|
308
308
|
expect(result.current.variables.pagination.after).toBe("cursorA");
|
|
309
309
|
expect(result.current.variables.pagination.first).toBe(20);
|
|
310
310
|
});
|
|
@@ -313,33 +313,33 @@ describe("useCollectionVariables", () => {
|
|
|
313
313
|
const { result } = renderHook(() => useCollectionVariables({}));
|
|
314
314
|
|
|
315
315
|
act(() => {
|
|
316
|
-
result.current.prevPage("cursor1");
|
|
316
|
+
result.current.control.prevPage("cursor1");
|
|
317
317
|
});
|
|
318
318
|
act(() => {
|
|
319
|
-
result.current.resetPage();
|
|
319
|
+
result.current.control.resetPage();
|
|
320
320
|
});
|
|
321
321
|
|
|
322
|
-
expect(result.current.cursor).toBeNull();
|
|
323
|
-
expect(result.current.paginationDirection).toBe("forward");
|
|
322
|
+
expect(result.current.control.cursor).toBeNull();
|
|
323
|
+
expect(result.current.control.paginationDirection).toBe("forward");
|
|
324
324
|
});
|
|
325
325
|
|
|
326
326
|
it("tracks hasPrevPage from currentPage and hasNextPage from setPageInfo", () => {
|
|
327
327
|
const { result } = renderHook(() => useCollectionVariables({}));
|
|
328
328
|
|
|
329
329
|
// Initially on page 1: no prev, no next
|
|
330
|
-
expect(result.current.hasPrevPage).toBe(false);
|
|
331
|
-
expect(result.current.hasNextPage).toBe(false);
|
|
330
|
+
expect(result.current.control.hasPrevPage).toBe(false);
|
|
331
|
+
expect(result.current.control.hasNextPage).toBe(false);
|
|
332
332
|
|
|
333
333
|
// After navigating to next page, hasPrevPage becomes true
|
|
334
334
|
act(() => {
|
|
335
|
-
result.current.nextPage("cursor-end");
|
|
335
|
+
result.current.control.nextPage("cursor-end");
|
|
336
336
|
});
|
|
337
337
|
|
|
338
|
-
expect(result.current.hasPrevPage).toBe(true);
|
|
338
|
+
expect(result.current.control.hasPrevPage).toBe(true);
|
|
339
339
|
|
|
340
340
|
// hasNextPage comes from setPageInfo when totalPages is null
|
|
341
341
|
act(() => {
|
|
342
|
-
result.current.setPageInfo({
|
|
342
|
+
result.current.control.setPageInfo({
|
|
343
343
|
hasNextPage: true,
|
|
344
344
|
endCursor: "end",
|
|
345
345
|
hasPreviousPage: false,
|
|
@@ -347,7 +347,7 @@ describe("useCollectionVariables", () => {
|
|
|
347
347
|
});
|
|
348
348
|
});
|
|
349
349
|
|
|
350
|
-
expect(result.current.hasNextPage).toBe(true);
|
|
350
|
+
expect(result.current.control.hasNextPage).toBe(true);
|
|
351
351
|
});
|
|
352
352
|
});
|
|
353
353
|
|
|
@@ -373,7 +373,7 @@ describe("useCollectionVariables", () => {
|
|
|
373
373
|
);
|
|
374
374
|
|
|
375
375
|
act(() => {
|
|
376
|
-
result.current.nextPage("abc123");
|
|
376
|
+
result.current.control.nextPage("abc123");
|
|
377
377
|
});
|
|
378
378
|
|
|
379
379
|
expect(result.current.variables).toEqual({
|
|
@@ -449,7 +449,7 @@ describe("useCollectionVariables", () => {
|
|
|
449
449
|
}),
|
|
450
450
|
);
|
|
451
451
|
|
|
452
|
-
expect(result.current.sortStates).toEqual([
|
|
452
|
+
expect(result.current.control.sortStates).toEqual([
|
|
453
453
|
{ field: "dueDate", direction: "Desc" },
|
|
454
454
|
]);
|
|
455
455
|
});
|
|
@@ -2,6 +2,7 @@ import { useCallback, useMemo, useState } from "react";
|
|
|
2
2
|
import type { TableMetadata } from "../../generator/metadata-generator";
|
|
3
3
|
import type {
|
|
4
4
|
BuildQueryVariables,
|
|
5
|
+
CollectionControl,
|
|
5
6
|
CollectionVariables,
|
|
6
7
|
Filter,
|
|
7
8
|
FilterOperator,
|
|
@@ -298,30 +299,32 @@ export function useCollectionVariables(
|
|
|
298
299
|
// ---------------------------------------------------------------------------
|
|
299
300
|
return {
|
|
300
301
|
variables,
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
302
|
+
control: {
|
|
303
|
+
filters,
|
|
304
|
+
// Cast needed: implementation accepts FilterOperator (widest),
|
|
305
|
+
// but overload signatures narrow via OperatorForField<TFilter, F>.
|
|
306
|
+
addFilter: addFilter as CollectionControl["addFilter"],
|
|
307
|
+
setFilters,
|
|
308
|
+
removeFilter,
|
|
309
|
+
clearFilters,
|
|
310
|
+
sortStates,
|
|
311
|
+
setSort,
|
|
312
|
+
clearSort,
|
|
313
|
+
pageSize,
|
|
314
|
+
setPageSize,
|
|
315
|
+
cursor,
|
|
316
|
+
paginationDirection,
|
|
317
|
+
nextPage,
|
|
318
|
+
prevPage,
|
|
319
|
+
resetPage,
|
|
320
|
+
hasPrevPage,
|
|
321
|
+
hasNextPage,
|
|
322
|
+
setPageInfo,
|
|
323
|
+
currentPage,
|
|
324
|
+
totalPages,
|
|
325
|
+
goToFirstPage,
|
|
326
|
+
goToLastPage,
|
|
327
|
+
setTotal,
|
|
328
|
+
},
|
|
326
329
|
};
|
|
327
330
|
}
|
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
type ReactNode,
|
|
9
9
|
} from "react";
|
|
10
10
|
import { cn } from "../lib/utils";
|
|
11
|
-
import {
|
|
11
|
+
import { CollectionControlProvider } from "../collection/collection-provider";
|
|
12
12
|
import { Table } from "../table";
|
|
13
13
|
import type { RowAction, SortConfig, UseDataTableReturn } from "../types";
|
|
14
14
|
import {
|
|
@@ -102,7 +102,7 @@ function DataTableProviderComponent<TRow extends Record<string, unknown>>({
|
|
|
102
102
|
locale: value.locale,
|
|
103
103
|
};
|
|
104
104
|
|
|
105
|
-
const
|
|
105
|
+
const controlValue = value.control ?? null;
|
|
106
106
|
|
|
107
107
|
const inner = (
|
|
108
108
|
<DataTableContext.Provider value={dataTableValue}>
|
|
@@ -110,12 +110,12 @@ function DataTableProviderComponent<TRow extends Record<string, unknown>>({
|
|
|
110
110
|
</DataTableContext.Provider>
|
|
111
111
|
);
|
|
112
112
|
|
|
113
|
-
// Wrap with
|
|
114
|
-
if (
|
|
113
|
+
// Wrap with CollectionControlProvider when control is available
|
|
114
|
+
if (controlValue) {
|
|
115
115
|
return (
|
|
116
|
-
<
|
|
116
|
+
<CollectionControlProvider value={controlValue}>
|
|
117
117
|
{inner}
|
|
118
|
-
</
|
|
118
|
+
</CollectionControlProvider>
|
|
119
119
|
);
|
|
120
120
|
}
|
|
121
121
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useDataTableContext } from "./data-table-context";
|
|
2
|
-
import {
|
|
2
|
+
import { useCollectionControl } from "../collection/collection-provider";
|
|
3
3
|
import { getLabels } from "./i18n";
|
|
4
4
|
|
|
5
5
|
// =============================================================================
|
|
@@ -139,7 +139,7 @@ export function Pagination({ labels, pageSizeOptions }: PaginationProps = {}) {
|
|
|
139
139
|
goToLastPage,
|
|
140
140
|
pageSize,
|
|
141
141
|
setPageSize,
|
|
142
|
-
} =
|
|
142
|
+
} = useCollectionControl();
|
|
143
143
|
|
|
144
144
|
const pl = getLabels(locale).pagination;
|
|
145
145
|
const firstLabel = labels?.first ?? pl.first;
|
|
@@ -14,7 +14,7 @@ import type {
|
|
|
14
14
|
} from "../types";
|
|
15
15
|
import { OPERATORS_BY_FILTER_TYPE } from "../types";
|
|
16
16
|
import { useDataTableContext } from "./data-table-context";
|
|
17
|
-
import {
|
|
17
|
+
import { useCollectionControl } from "../collection/collection-provider";
|
|
18
18
|
import { getLabels } from "./i18n";
|
|
19
19
|
|
|
20
20
|
/**
|
|
@@ -46,7 +46,7 @@ export function SearchFilterForm({
|
|
|
46
46
|
} = {}) {
|
|
47
47
|
const { columns, locale } = useDataTableContext();
|
|
48
48
|
const { filters, addFilter, removeFilter, clearFilters } =
|
|
49
|
-
|
|
49
|
+
useCollectionControl();
|
|
50
50
|
const sf = getLabels(locale).searchFilter;
|
|
51
51
|
const filterableColumns = columns.filter((col) => !!col.filter);
|
|
52
52
|
|
|
@@ -234,7 +234,7 @@ describe("useDataTable", () => {
|
|
|
234
234
|
// Sort state
|
|
235
235
|
// ---------------------------------------------------------------------------
|
|
236
236
|
describe("sort state", () => {
|
|
237
|
-
it("sortStates is empty when no
|
|
237
|
+
it("sortStates is empty when no control is provided", () => {
|
|
238
238
|
const { result } = renderHook(() =>
|
|
239
239
|
useDataTable<TestRow>({ columns: testColumns, data: testData }),
|
|
240
240
|
);
|
|
@@ -242,7 +242,7 @@ describe("useDataTable", () => {
|
|
|
242
242
|
expect(result.current.sortStates).toEqual([]);
|
|
243
243
|
});
|
|
244
244
|
|
|
245
|
-
it("onSort is undefined when no
|
|
245
|
+
it("onSort is undefined when no control is provided", () => {
|
|
246
246
|
const { result } = renderHook(() =>
|
|
247
247
|
useDataTable<TestRow>({ columns: testColumns, data: testData }),
|
|
248
248
|
);
|
|
@@ -13,7 +13,7 @@ import type {
|
|
|
13
13
|
*
|
|
14
14
|
* @example
|
|
15
15
|
* ```tsx
|
|
16
|
-
* const { variables,
|
|
16
|
+
* const { variables, control } = useCollectionVariables({ params: { pageSize: 20 } });
|
|
17
17
|
* const { query, order, pagination } = variables;
|
|
18
18
|
* const [result] = useQuery({
|
|
19
19
|
* query: GET_ORDERS,
|
|
@@ -24,7 +24,7 @@ import type {
|
|
|
24
24
|
* columns,
|
|
25
25
|
* data: result.data?.orders,
|
|
26
26
|
* loading: result.fetching,
|
|
27
|
-
*
|
|
27
|
+
* control,
|
|
28
28
|
* });
|
|
29
29
|
*
|
|
30
30
|
* <DataTable.Provider value={table}>
|
|
@@ -43,7 +43,7 @@ export function useDataTable<TRow extends Record<string, unknown>>(
|
|
|
43
43
|
data,
|
|
44
44
|
loading = false,
|
|
45
45
|
error = null,
|
|
46
|
-
|
|
46
|
+
control,
|
|
47
47
|
onClickRow,
|
|
48
48
|
rowActions,
|
|
49
49
|
locale = "en",
|
|
@@ -76,20 +76,15 @@ export function useDataTable<TRow extends Record<string, unknown>>(
|
|
|
76
76
|
);
|
|
77
77
|
}, [data]);
|
|
78
78
|
|
|
79
|
-
// Sync pageInfo to
|
|
79
|
+
// Sync pageInfo to control so hasPrevPage/hasNextPage are up-to-date
|
|
80
80
|
useEffect(() => {
|
|
81
81
|
if (data?.pageInfo) {
|
|
82
|
-
|
|
82
|
+
control?.setPageInfo(data.pageInfo);
|
|
83
83
|
}
|
|
84
84
|
if (data?.total != null) {
|
|
85
|
-
|
|
85
|
+
control?.setTotal(data.total);
|
|
86
86
|
}
|
|
87
|
-
}, [
|
|
88
|
-
data?.pageInfo,
|
|
89
|
-
data?.total,
|
|
90
|
-
collection?.setPageInfo,
|
|
91
|
-
collection?.setTotal,
|
|
92
|
-
]);
|
|
87
|
+
}, [data?.pageInfo, data?.total, control?.setPageInfo, control?.setTotal]);
|
|
93
88
|
|
|
94
89
|
// ---------------------------------------------------------------------------
|
|
95
90
|
// Column visibility management
|
|
@@ -131,24 +126,24 @@ export function useDataTable<TRow extends Record<string, unknown>>(
|
|
|
131
126
|
);
|
|
132
127
|
|
|
133
128
|
// ---------------------------------------------------------------------------
|
|
134
|
-
// Pagination (delegated from
|
|
129
|
+
// Pagination (delegated from control)
|
|
135
130
|
// ---------------------------------------------------------------------------
|
|
136
131
|
const nextPage = useCallback(
|
|
137
132
|
(endCursor: string) => {
|
|
138
|
-
|
|
133
|
+
control?.nextPage(endCursor);
|
|
139
134
|
},
|
|
140
|
-
[
|
|
135
|
+
[control],
|
|
141
136
|
);
|
|
142
137
|
|
|
143
138
|
const prevPage = useCallback(
|
|
144
139
|
(startCursor: string) => {
|
|
145
|
-
|
|
140
|
+
control?.prevPage(startCursor);
|
|
146
141
|
},
|
|
147
|
-
[
|
|
142
|
+
[control],
|
|
148
143
|
);
|
|
149
144
|
|
|
150
|
-
const hasPrevPage =
|
|
151
|
-
const hasNextPage =
|
|
145
|
+
const hasPrevPage = control?.hasPrevPage ?? false;
|
|
146
|
+
const hasNextPage = control?.hasNextPage ?? false;
|
|
152
147
|
|
|
153
148
|
// ---------------------------------------------------------------------------
|
|
154
149
|
// Row Operations (Optimistic Updates)
|
|
@@ -209,19 +204,19 @@ export function useDataTable<TRow extends Record<string, unknown>>(
|
|
|
209
204
|
);
|
|
210
205
|
|
|
211
206
|
// ---------------------------------------------------------------------------
|
|
212
|
-
// Sort (delegated from
|
|
207
|
+
// Sort (delegated from control)
|
|
213
208
|
// ---------------------------------------------------------------------------
|
|
214
209
|
const sortStates = useMemo<SortState[]>(() => {
|
|
215
|
-
return
|
|
216
|
-
}, [
|
|
210
|
+
return control?.sortStates ?? [];
|
|
211
|
+
}, [control?.sortStates]);
|
|
217
212
|
|
|
218
213
|
const onSort = useMemo<
|
|
219
214
|
((field: string, direction?: "Asc" | "Desc") => void) | undefined
|
|
220
215
|
>(() => {
|
|
221
|
-
if (!
|
|
216
|
+
if (!control) return undefined;
|
|
222
217
|
return (field: string, direction?: "Asc" | "Desc") =>
|
|
223
|
-
|
|
224
|
-
}, [
|
|
218
|
+
control.setSort(field, direction);
|
|
219
|
+
}, [control]);
|
|
225
220
|
|
|
226
221
|
// ---------------------------------------------------------------------------
|
|
227
222
|
// Return
|
|
@@ -254,8 +249,8 @@ export function useDataTable<TRow extends Record<string, unknown>>(
|
|
|
254
249
|
deleteRow,
|
|
255
250
|
insertRow,
|
|
256
251
|
|
|
257
|
-
//
|
|
258
|
-
|
|
252
|
+
// Control (passthrough for DataTable.Provider)
|
|
253
|
+
control,
|
|
259
254
|
|
|
260
255
|
// Row interaction (passthrough for DataTable.Provider)
|
|
261
256
|
onClickRow,
|
package/src/component/index.ts
CHANGED
|
@@ -18,6 +18,7 @@ export type {
|
|
|
18
18
|
ColumnDefinition,
|
|
19
19
|
UseCollectionOptions,
|
|
20
20
|
UseCollectionReturn,
|
|
21
|
+
CollectionControl,
|
|
21
22
|
UseDataTableOptions,
|
|
22
23
|
UseDataTableReturn,
|
|
23
24
|
RowAction,
|
|
@@ -44,8 +45,8 @@ export {
|
|
|
44
45
|
// Collection
|
|
45
46
|
export { useCollectionVariables } from "./collection/use-collection";
|
|
46
47
|
export {
|
|
47
|
-
|
|
48
|
-
|
|
48
|
+
CollectionControlProvider,
|
|
49
|
+
useCollectionControl,
|
|
49
50
|
} from "./collection/collection-provider";
|
|
50
51
|
|
|
51
52
|
// Table (static)
|
package/src/component/types.ts
CHANGED
|
@@ -466,40 +466,25 @@ export interface UseCollectionOptions<
|
|
|
466
466
|
}
|
|
467
467
|
|
|
468
468
|
/**
|
|
469
|
-
*
|
|
469
|
+
* Collection control interface for UI components to interact with
|
|
470
|
+
* filter, sort, and pagination state.
|
|
470
471
|
*
|
|
471
472
|
* Methods that accept a field name are typed with `TFieldName` so that
|
|
472
473
|
* auto-completion works when a concrete union is supplied.
|
|
473
474
|
*
|
|
474
475
|
* **Note:** Methods that accept `TFieldName` use *method syntax* intentionally
|
|
475
|
-
* so that `
|
|
476
|
-
* `
|
|
476
|
+
* so that `CollectionControl<"a" | "b">` remains assignable to
|
|
477
|
+
* `CollectionControl<string>` (bivariant method check).
|
|
477
478
|
* Methods that don't depend on `TFieldName` use property syntax so they
|
|
478
479
|
* can be safely destructured without triggering `unbound-method` lint rules.
|
|
479
480
|
*
|
|
480
481
|
* @typeParam TFieldName - Union of allowed field name strings (default: `string`).
|
|
481
|
-
* @typeParam
|
|
482
|
+
* @typeParam TFilter - Filter type (default: `Filter<TFieldName>`).
|
|
482
483
|
*/
|
|
483
|
-
export interface
|
|
484
|
+
export interface CollectionControl<
|
|
484
485
|
TFieldName extends string = string,
|
|
485
|
-
TVariables = CollectionVariables,
|
|
486
486
|
TFilter = Filter<TFieldName>,
|
|
487
487
|
> {
|
|
488
|
-
/**
|
|
489
|
-
* Collection variables split into explicit sub-properties
|
|
490
|
-
* for direct mapping to GraphQL query variables.
|
|
491
|
-
*
|
|
492
|
-
* @example
|
|
493
|
-
* ```tsx
|
|
494
|
-
* const { query, order, pagination } = collection.variables;
|
|
495
|
-
* const [result] = useQuery({
|
|
496
|
-
* query: GET_TASKS,
|
|
497
|
-
* variables: { ...pagination, query, order },
|
|
498
|
-
* });
|
|
499
|
-
* ```
|
|
500
|
-
*/
|
|
501
|
-
variables: TVariables;
|
|
502
|
-
|
|
503
488
|
// Filter operations
|
|
504
489
|
/** Current active filters */
|
|
505
490
|
filters: Filter[];
|
|
@@ -557,6 +542,49 @@ export interface UseCollectionReturn<
|
|
|
557
542
|
setTotal: (total: number) => void;
|
|
558
543
|
}
|
|
559
544
|
|
|
545
|
+
/**
|
|
546
|
+
* Return type of `useCollectionVariables` hook.
|
|
547
|
+
*
|
|
548
|
+
* @typeParam TFieldName - Union of allowed field name strings (default: `string`).
|
|
549
|
+
* @typeParam TVariables - Type of `variables` property.
|
|
550
|
+
* @typeParam TFilter - Filter type (default: `Filter<TFieldName>`).
|
|
551
|
+
*/
|
|
552
|
+
export interface UseCollectionReturn<
|
|
553
|
+
TFieldName extends string = string,
|
|
554
|
+
TVariables = CollectionVariables,
|
|
555
|
+
TFilter = Filter<TFieldName>,
|
|
556
|
+
> {
|
|
557
|
+
/**
|
|
558
|
+
* Collection variables split into explicit sub-properties
|
|
559
|
+
* for direct mapping to GraphQL query variables.
|
|
560
|
+
*
|
|
561
|
+
* @example
|
|
562
|
+
* ```tsx
|
|
563
|
+
* const { variables, control } = useCollectionVariables({ params: { pageSize: 20 } });
|
|
564
|
+
* const { query, order, pagination } = variables;
|
|
565
|
+
* const [result] = useQuery({
|
|
566
|
+
* query: GET_TASKS,
|
|
567
|
+
* variables: { ...pagination, query, order },
|
|
568
|
+
* });
|
|
569
|
+
* ```
|
|
570
|
+
*/
|
|
571
|
+
variables: TVariables;
|
|
572
|
+
|
|
573
|
+
/**
|
|
574
|
+
* Collection control interface for UI components.
|
|
575
|
+
*
|
|
576
|
+
* Pass this to `useDataTable({ control })` to connect filter, sort,
|
|
577
|
+
* and pagination state to the data table.
|
|
578
|
+
*
|
|
579
|
+
* @example
|
|
580
|
+
* ```tsx
|
|
581
|
+
* const { variables, control } = useCollectionVariables({ params: { pageSize: 20 } });
|
|
582
|
+
* const table = useDataTable({ columns, data: result.data?.orders, control });
|
|
583
|
+
* ```
|
|
584
|
+
*/
|
|
585
|
+
control: CollectionControl<TFieldName, TFilter>;
|
|
586
|
+
}
|
|
587
|
+
|
|
560
588
|
// =============================================================================
|
|
561
589
|
// useDataTable Types
|
|
562
590
|
// =============================================================================
|
|
@@ -573,8 +601,8 @@ export interface UseDataTableOptions<TRow extends Record<string, unknown>> {
|
|
|
573
601
|
loading?: boolean;
|
|
574
602
|
/** Error */
|
|
575
603
|
error?: Error | null;
|
|
576
|
-
/** Collection
|
|
577
|
-
|
|
604
|
+
/** Collection control for sort/filter/pagination integration */
|
|
605
|
+
control?: CollectionControl;
|
|
578
606
|
/** Handler called when a row is clicked */
|
|
579
607
|
onClickRow?: (row: TRow) => void;
|
|
580
608
|
/** Row action definitions for the actions column */
|
|
@@ -680,9 +708,9 @@ export interface UseDataTableReturn<TRow extends Record<string, unknown>> {
|
|
|
680
708
|
/** Optimistically insert a row */
|
|
681
709
|
insertRow: (row: TRow) => { rollback: () => void };
|
|
682
710
|
|
|
683
|
-
//
|
|
684
|
-
/** Collection
|
|
685
|
-
|
|
711
|
+
// Control (passthrough for DataTable.Provider)
|
|
712
|
+
/** Collection control passed through from options */
|
|
713
|
+
control: CollectionControl | undefined;
|
|
686
714
|
|
|
687
715
|
// Row interaction (passthrough for DataTable.Provider)
|
|
688
716
|
/** Handler called when a row is clicked */
|
package/src/tests/helpers.tsx
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import type { ReactNode } from "react";
|
|
2
2
|
import { vi } from "vitest";
|
|
3
|
-
import type { Column,
|
|
3
|
+
import type { Column, CollectionControl } from "../component/types";
|
|
4
4
|
import { DataTableContext } from "../component/data-table/data-table-context";
|
|
5
5
|
import type { DataTableContextValue } from "../component/data-table/data-table-context";
|
|
6
|
-
import {
|
|
6
|
+
import { CollectionControlProvider } from "../component/collection/collection-provider";
|
|
7
7
|
|
|
8
8
|
// =============================================================================
|
|
9
9
|
// Mock factory: DataTableContext
|
|
@@ -47,14 +47,9 @@ export function createMockDataTableContext<T extends Record<string, unknown>>(
|
|
|
47
47
|
// =============================================================================
|
|
48
48
|
|
|
49
49
|
export function createMockCollectionContext(
|
|
50
|
-
overrides?: Partial<
|
|
51
|
-
):
|
|
50
|
+
overrides?: Partial<CollectionControl>,
|
|
51
|
+
): CollectionControl {
|
|
52
52
|
return {
|
|
53
|
-
variables: {
|
|
54
|
-
query: undefined,
|
|
55
|
-
order: undefined,
|
|
56
|
-
pagination: { first: 20 },
|
|
57
|
-
},
|
|
58
53
|
filters: [],
|
|
59
54
|
addFilter: vi.fn(),
|
|
60
55
|
setFilters: vi.fn(),
|
|
@@ -110,7 +105,7 @@ export function createTestProviders<
|
|
|
110
105
|
columns: Column<T>[];
|
|
111
106
|
rows: T[];
|
|
112
107
|
dataTableDefaults?: Partial<DataTableContextValue<T>>;
|
|
113
|
-
collectionDefaults?: Partial<
|
|
108
|
+
collectionDefaults?: Partial<CollectionControl>;
|
|
114
109
|
}) {
|
|
115
110
|
return function TestProviders({
|
|
116
111
|
children,
|
|
@@ -119,10 +114,10 @@ export function createTestProviders<
|
|
|
119
114
|
}: {
|
|
120
115
|
children: ReactNode;
|
|
121
116
|
dataTable?: Partial<DataTableContextValue<T>>;
|
|
122
|
-
collection?: Partial<
|
|
117
|
+
collection?: Partial<CollectionControl>;
|
|
123
118
|
}) {
|
|
124
119
|
return (
|
|
125
|
-
<
|
|
120
|
+
<CollectionControlProvider
|
|
126
121
|
value={createMockCollectionContext({
|
|
127
122
|
...defaults.collectionDefaults,
|
|
128
123
|
...collection,
|
|
@@ -136,7 +131,7 @@ export function createTestProviders<
|
|
|
136
131
|
>
|
|
137
132
|
{children}
|
|
138
133
|
</DataTableContext.Provider>
|
|
139
|
-
</
|
|
134
|
+
</CollectionControlProvider>
|
|
140
135
|
);
|
|
141
136
|
};
|
|
142
137
|
}
|