@izumisy-tailor/tailor-data-viewer 0.2.6 → 0.2.8
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/README.md +39 -36
- package/package.json +1 -1
- package/src/component/collection/collection-provider.tsx +90 -0
- package/src/component/{collection-params/use-collection-params.test.ts → collection/use-collection.test.ts} +97 -145
- package/src/component/{collection-params/use-collection-params.ts → collection/use-collection.ts} +66 -67
- package/src/component/data-table/index.tsx +1 -1
- package/src/component/data-table/use-data-table.ts +14 -14
- package/src/component/index.ts +9 -8
- package/src/component/search-filter-form.tsx +4 -2
- package/src/component/types.ts +121 -45
- package/src/component/collection-params/collection-params-provider.tsx +0 -83
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
import { renderHook, act } from "@testing-library/react";
|
|
2
2
|
import { describe, it, expect } from "vitest";
|
|
3
3
|
import type { TableMetadataMap } from "../../generator/metadata-generator";
|
|
4
|
-
import {
|
|
4
|
+
import { useCollection } from "./use-collection";
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
const FAKE_QUERY = { kind: "Document" } as const;
|
|
7
|
+
|
|
8
|
+
describe("useCollection", () => {
|
|
7
9
|
// ---------------------------------------------------------------------------
|
|
8
10
|
// Initial state
|
|
9
11
|
// ---------------------------------------------------------------------------
|
|
10
12
|
describe("initial state", () => {
|
|
11
13
|
it("returns default variables with pageSize 20", () => {
|
|
12
|
-
const { result } = renderHook(() =>
|
|
13
|
-
expect(result.current.variables).toEqual({ first: 20 });
|
|
14
|
+
const { result } = renderHook(() => useCollection({ query: FAKE_QUERY }));
|
|
15
|
+
expect(result.current.toQueryArgs().variables).toEqual({ first: 20 });
|
|
14
16
|
expect(result.current.filters).toEqual([]);
|
|
15
17
|
expect(result.current.sortStates).toEqual([]);
|
|
16
18
|
expect(result.current.cursor).toBeNull();
|
|
@@ -19,40 +21,45 @@ describe("useCollectionParams", () => {
|
|
|
19
21
|
|
|
20
22
|
it("uses custom pageSize", () => {
|
|
21
23
|
const { result } = renderHook(() =>
|
|
22
|
-
|
|
24
|
+
useCollection({ query: FAKE_QUERY, params: { pageSize: 50 } }),
|
|
23
25
|
);
|
|
24
|
-
expect(result.current.variables.first).toBe(50);
|
|
26
|
+
expect(result.current.toQueryArgs().variables.first).toBe(50);
|
|
25
27
|
});
|
|
26
28
|
|
|
27
29
|
it("applies initial sort", () => {
|
|
28
30
|
const { result } = renderHook(() =>
|
|
29
|
-
|
|
30
|
-
|
|
31
|
+
useCollection({
|
|
32
|
+
query: FAKE_QUERY,
|
|
33
|
+
params: {
|
|
34
|
+
initialSort: [{ field: "createdAt", direction: "Desc" }],
|
|
35
|
+
},
|
|
31
36
|
}),
|
|
32
37
|
);
|
|
33
38
|
expect(result.current.sortStates).toEqual([
|
|
34
39
|
{ field: "createdAt", direction: "Desc" },
|
|
35
40
|
]);
|
|
36
|
-
expect(result.current.variables.order).toEqual([
|
|
41
|
+
expect(result.current.toQueryArgs().variables.order).toEqual([
|
|
37
42
|
{ field: "createdAt", direction: "Desc" },
|
|
38
43
|
]);
|
|
39
44
|
});
|
|
40
45
|
|
|
41
46
|
it("applies initial filters", () => {
|
|
42
47
|
const { result } = renderHook(() =>
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
48
|
+
useCollection({
|
|
49
|
+
query: FAKE_QUERY,
|
|
50
|
+
params: {
|
|
51
|
+
initialFilters: [
|
|
52
|
+
{
|
|
53
|
+
field: "status",
|
|
54
|
+
operator: "eq",
|
|
55
|
+
value: "ACTIVE",
|
|
56
|
+
},
|
|
57
|
+
],
|
|
58
|
+
},
|
|
52
59
|
}),
|
|
53
60
|
);
|
|
54
61
|
expect(result.current.filters).toHaveLength(1);
|
|
55
|
-
expect(result.current.variables.query).toEqual({
|
|
62
|
+
expect(result.current.toQueryArgs().variables.query).toEqual({
|
|
56
63
|
status: { eq: "ACTIVE" },
|
|
57
64
|
});
|
|
58
65
|
});
|
|
@@ -63,7 +70,7 @@ describe("useCollectionParams", () => {
|
|
|
63
70
|
// ---------------------------------------------------------------------------
|
|
64
71
|
describe("filter operations", () => {
|
|
65
72
|
it("adds a filter", () => {
|
|
66
|
-
const { result } = renderHook(() =>
|
|
73
|
+
const { result } = renderHook(() => useCollection({ query: FAKE_QUERY }));
|
|
67
74
|
|
|
68
75
|
act(() => {
|
|
69
76
|
result.current.addFilter("status", "ACTIVE", "eq");
|
|
@@ -75,13 +82,13 @@ describe("useCollectionParams", () => {
|
|
|
75
82
|
operator: "eq",
|
|
76
83
|
value: "ACTIVE",
|
|
77
84
|
});
|
|
78
|
-
expect(result.current.variables.query).toEqual({
|
|
85
|
+
expect(result.current.toQueryArgs().variables.query).toEqual({
|
|
79
86
|
status: { eq: "ACTIVE" },
|
|
80
87
|
});
|
|
81
88
|
});
|
|
82
89
|
|
|
83
90
|
it("replaces filter for same field", () => {
|
|
84
|
-
const { result } = renderHook(() =>
|
|
91
|
+
const { result } = renderHook(() => useCollection({ query: FAKE_QUERY }));
|
|
85
92
|
|
|
86
93
|
act(() => {
|
|
87
94
|
result.current.addFilter("status", "ACTIVE", "eq");
|
|
@@ -95,19 +102,17 @@ describe("useCollectionParams", () => {
|
|
|
95
102
|
});
|
|
96
103
|
|
|
97
104
|
it("sets filters in bulk", () => {
|
|
98
|
-
const { result } = renderHook(() =>
|
|
105
|
+
const { result } = renderHook(() => useCollection({ query: FAKE_QUERY }));
|
|
99
106
|
|
|
100
107
|
act(() => {
|
|
101
108
|
result.current.setFilters([
|
|
102
109
|
{
|
|
103
110
|
field: "status",
|
|
104
|
-
fieldType: "enum",
|
|
105
111
|
operator: "eq",
|
|
106
112
|
value: "ACTIVE",
|
|
107
113
|
},
|
|
108
114
|
{
|
|
109
115
|
field: "amount",
|
|
110
|
-
fieldType: "number",
|
|
111
116
|
operator: "gte",
|
|
112
117
|
value: 1000,
|
|
113
118
|
},
|
|
@@ -115,14 +120,14 @@ describe("useCollectionParams", () => {
|
|
|
115
120
|
});
|
|
116
121
|
|
|
117
122
|
expect(result.current.filters).toHaveLength(2);
|
|
118
|
-
expect(result.current.variables.query).toEqual({
|
|
123
|
+
expect(result.current.toQueryArgs().variables.query).toEqual({
|
|
119
124
|
status: { eq: "ACTIVE" },
|
|
120
125
|
amount: { gte: 1000 },
|
|
121
126
|
});
|
|
122
127
|
});
|
|
123
128
|
|
|
124
129
|
it("removes a filter", () => {
|
|
125
|
-
const { result } = renderHook(() =>
|
|
130
|
+
const { result } = renderHook(() => useCollection({ query: FAKE_QUERY }));
|
|
126
131
|
|
|
127
132
|
act(() => {
|
|
128
133
|
result.current.addFilter("status", "ACTIVE", "eq");
|
|
@@ -137,7 +142,7 @@ describe("useCollectionParams", () => {
|
|
|
137
142
|
});
|
|
138
143
|
|
|
139
144
|
it("clears all filters", () => {
|
|
140
|
-
const { result } = renderHook(() =>
|
|
145
|
+
const { result } = renderHook(() => useCollection({ query: FAKE_QUERY }));
|
|
141
146
|
|
|
142
147
|
act(() => {
|
|
143
148
|
result.current.addFilter("status", "ACTIVE", "eq");
|
|
@@ -148,11 +153,11 @@ describe("useCollectionParams", () => {
|
|
|
148
153
|
});
|
|
149
154
|
|
|
150
155
|
expect(result.current.filters).toHaveLength(0);
|
|
151
|
-
expect(result.current.variables.query).toBeUndefined();
|
|
156
|
+
expect(result.current.toQueryArgs().variables.query).toBeUndefined();
|
|
152
157
|
});
|
|
153
158
|
|
|
154
159
|
it("resets pagination when filters change", () => {
|
|
155
|
-
const { result } = renderHook(() =>
|
|
160
|
+
const { result } = renderHook(() => useCollection({ query: FAKE_QUERY }));
|
|
156
161
|
|
|
157
162
|
// Navigate to next page
|
|
158
163
|
act(() => {
|
|
@@ -174,7 +179,7 @@ describe("useCollectionParams", () => {
|
|
|
174
179
|
// ---------------------------------------------------------------------------
|
|
175
180
|
describe("sort operations", () => {
|
|
176
181
|
it("sets sort", () => {
|
|
177
|
-
const { result } = renderHook(() =>
|
|
182
|
+
const { result } = renderHook(() => useCollection({ query: FAKE_QUERY }));
|
|
178
183
|
|
|
179
184
|
act(() => {
|
|
180
185
|
result.current.setSort("createdAt", "Desc");
|
|
@@ -183,13 +188,13 @@ describe("useCollectionParams", () => {
|
|
|
183
188
|
expect(result.current.sortStates).toEqual([
|
|
184
189
|
{ field: "createdAt", direction: "Desc" },
|
|
185
190
|
]);
|
|
186
|
-
expect(result.current.variables.order).toEqual([
|
|
191
|
+
expect(result.current.toQueryArgs().variables.order).toEqual([
|
|
187
192
|
{ field: "createdAt", direction: "Desc" },
|
|
188
193
|
]);
|
|
189
194
|
});
|
|
190
195
|
|
|
191
196
|
it("replaces sort by default", () => {
|
|
192
|
-
const { result } = renderHook(() =>
|
|
197
|
+
const { result } = renderHook(() => useCollection({ query: FAKE_QUERY }));
|
|
193
198
|
|
|
194
199
|
act(() => {
|
|
195
200
|
result.current.setSort("createdAt", "Desc");
|
|
@@ -204,7 +209,7 @@ describe("useCollectionParams", () => {
|
|
|
204
209
|
});
|
|
205
210
|
|
|
206
211
|
it("appends sort with append=true (multi-sort)", () => {
|
|
207
|
-
const { result } = renderHook(() =>
|
|
212
|
+
const { result } = renderHook(() => useCollection({ query: FAKE_QUERY }));
|
|
208
213
|
|
|
209
214
|
act(() => {
|
|
210
215
|
result.current.setSort("createdAt", "Desc");
|
|
@@ -220,7 +225,7 @@ describe("useCollectionParams", () => {
|
|
|
220
225
|
});
|
|
221
226
|
|
|
222
227
|
it("clears sort", () => {
|
|
223
|
-
const { result } = renderHook(() =>
|
|
228
|
+
const { result } = renderHook(() => useCollection({ query: FAKE_QUERY }));
|
|
224
229
|
|
|
225
230
|
act(() => {
|
|
226
231
|
result.current.setSort("createdAt", "Desc");
|
|
@@ -230,7 +235,7 @@ describe("useCollectionParams", () => {
|
|
|
230
235
|
});
|
|
231
236
|
|
|
232
237
|
expect(result.current.sortStates).toEqual([]);
|
|
233
|
-
expect(result.current.variables.order).toBeUndefined();
|
|
238
|
+
expect(result.current.toQueryArgs().variables.order).toBeUndefined();
|
|
234
239
|
});
|
|
235
240
|
});
|
|
236
241
|
|
|
@@ -239,7 +244,7 @@ describe("useCollectionParams", () => {
|
|
|
239
244
|
// ---------------------------------------------------------------------------
|
|
240
245
|
describe("pagination operations", () => {
|
|
241
246
|
it("navigates to next page", () => {
|
|
242
|
-
const { result } = renderHook(() =>
|
|
247
|
+
const { result } = renderHook(() => useCollection({ query: FAKE_QUERY }));
|
|
243
248
|
|
|
244
249
|
act(() => {
|
|
245
250
|
result.current.nextPage("cursor1");
|
|
@@ -247,11 +252,11 @@ describe("useCollectionParams", () => {
|
|
|
247
252
|
|
|
248
253
|
expect(result.current.cursor).toBe("cursor1");
|
|
249
254
|
expect(result.current.hasPrevPage).toBe(true);
|
|
250
|
-
expect(result.current.variables.after).toBe("cursor1");
|
|
255
|
+
expect(result.current.toQueryArgs().variables.after).toBe("cursor1");
|
|
251
256
|
});
|
|
252
257
|
|
|
253
258
|
it("navigates back to previous page", () => {
|
|
254
|
-
const { result } = renderHook(() =>
|
|
259
|
+
const { result } = renderHook(() => useCollection({ query: FAKE_QUERY }));
|
|
255
260
|
|
|
256
261
|
act(() => {
|
|
257
262
|
result.current.nextPage("cursor1");
|
|
@@ -268,7 +273,7 @@ describe("useCollectionParams", () => {
|
|
|
268
273
|
});
|
|
269
274
|
|
|
270
275
|
it("navigates back to first page", () => {
|
|
271
|
-
const { result } = renderHook(() =>
|
|
276
|
+
const { result } = renderHook(() => useCollection({ query: FAKE_QUERY }));
|
|
272
277
|
|
|
273
278
|
act(() => {
|
|
274
279
|
result.current.nextPage("cursor1");
|
|
@@ -282,7 +287,7 @@ describe("useCollectionParams", () => {
|
|
|
282
287
|
});
|
|
283
288
|
|
|
284
289
|
it("resets page", () => {
|
|
285
|
-
const { result } = renderHook(() =>
|
|
290
|
+
const { result } = renderHook(() => useCollection({ query: FAKE_QUERY }));
|
|
286
291
|
|
|
287
292
|
act(() => {
|
|
288
293
|
result.current.nextPage("cursor1");
|
|
@@ -298,22 +303,24 @@ describe("useCollectionParams", () => {
|
|
|
298
303
|
});
|
|
299
304
|
|
|
300
305
|
// ---------------------------------------------------------------------------
|
|
301
|
-
//
|
|
306
|
+
// toQueryArgs
|
|
302
307
|
// ---------------------------------------------------------------------------
|
|
303
|
-
describe("
|
|
308
|
+
describe("toQueryArgs", () => {
|
|
304
309
|
it("generates complete variables with filters, sort, and cursor", () => {
|
|
305
310
|
const { result } = renderHook(() =>
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
311
|
+
useCollection({
|
|
312
|
+
query: FAKE_QUERY,
|
|
313
|
+
params: {
|
|
314
|
+
pageSize: 10,
|
|
315
|
+
initialFilters: [
|
|
316
|
+
{
|
|
317
|
+
field: "status",
|
|
318
|
+
operator: "eq",
|
|
319
|
+
value: "ACTIVE",
|
|
320
|
+
},
|
|
321
|
+
],
|
|
322
|
+
initialSort: [{ field: "createdAt", direction: "Desc" }],
|
|
323
|
+
},
|
|
317
324
|
}),
|
|
318
325
|
);
|
|
319
326
|
|
|
@@ -321,21 +328,37 @@ describe("useCollectionParams", () => {
|
|
|
321
328
|
result.current.nextPage("abc123");
|
|
322
329
|
});
|
|
323
330
|
|
|
324
|
-
expect(result.current.
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
331
|
+
expect(result.current.toQueryArgs()).toEqual({
|
|
332
|
+
query: FAKE_QUERY,
|
|
333
|
+
variables: {
|
|
334
|
+
first: 10,
|
|
335
|
+
query: { status: { eq: "ACTIVE" } },
|
|
336
|
+
order: [{ field: "createdAt", direction: "Desc" }],
|
|
337
|
+
after: "abc123",
|
|
338
|
+
},
|
|
329
339
|
});
|
|
330
340
|
});
|
|
331
341
|
|
|
332
342
|
it("omits undefined fields from variables", () => {
|
|
333
|
-
const { result } = renderHook(() =>
|
|
334
|
-
const
|
|
335
|
-
expect(
|
|
336
|
-
expect("query" in
|
|
337
|
-
expect("order" in
|
|
338
|
-
expect("after" in
|
|
343
|
+
const { result } = renderHook(() => useCollection({ query: FAKE_QUERY }));
|
|
344
|
+
const { variables } = result.current.toQueryArgs();
|
|
345
|
+
expect(variables).toEqual({ first: 20 });
|
|
346
|
+
expect("query" in variables).toBe(false);
|
|
347
|
+
expect("order" in variables).toBe(false);
|
|
348
|
+
expect("after" in variables).toBe(false);
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
it("includes query in toQueryArgs result", () => {
|
|
352
|
+
const fakeQuery = { kind: "Document" } as const;
|
|
353
|
+
const { result } = renderHook(() =>
|
|
354
|
+
useCollection({ query: fakeQuery, params: { pageSize: 10 } }),
|
|
355
|
+
);
|
|
356
|
+
|
|
357
|
+
const args = result.current.toQueryArgs();
|
|
358
|
+
expect(args).toEqual({
|
|
359
|
+
query: fakeQuery,
|
|
360
|
+
variables: { first: 10 },
|
|
361
|
+
});
|
|
339
362
|
});
|
|
340
363
|
});
|
|
341
364
|
|
|
@@ -365,96 +388,25 @@ describe("useCollectionParams", () => {
|
|
|
365
388
|
|
|
366
389
|
it("works with metadata and tableName", () => {
|
|
367
390
|
const { result } = renderHook(() =>
|
|
368
|
-
|
|
369
|
-
metadata: testMetadata,
|
|
370
|
-
tableName: "task",
|
|
371
|
-
pageSize: 10,
|
|
372
|
-
}),
|
|
373
|
-
);
|
|
374
|
-
expect(result.current.variables).toEqual({ first: 10 });
|
|
375
|
-
});
|
|
376
|
-
|
|
377
|
-
it("auto-detects fieldType from metadata for string field", () => {
|
|
378
|
-
const { result } = renderHook(() =>
|
|
379
|
-
useCollectionParams({
|
|
380
|
-
metadata: testMetadata,
|
|
381
|
-
tableName: "task",
|
|
382
|
-
}),
|
|
383
|
-
);
|
|
384
|
-
|
|
385
|
-
act(() => {
|
|
386
|
-
result.current.addFilter("title", "foo");
|
|
387
|
-
});
|
|
388
|
-
|
|
389
|
-
expect(result.current.filters[0].fieldType).toBe("string");
|
|
390
|
-
});
|
|
391
|
-
|
|
392
|
-
it("auto-detects fieldType from metadata for enum field", () => {
|
|
393
|
-
const { result } = renderHook(() =>
|
|
394
|
-
useCollectionParams({
|
|
395
|
-
metadata: testMetadata,
|
|
396
|
-
tableName: "task",
|
|
397
|
-
}),
|
|
398
|
-
);
|
|
399
|
-
|
|
400
|
-
act(() => {
|
|
401
|
-
result.current.addFilter("status", "todo");
|
|
402
|
-
});
|
|
403
|
-
|
|
404
|
-
expect(result.current.filters[0].fieldType).toBe("enum");
|
|
405
|
-
});
|
|
406
|
-
|
|
407
|
-
it("auto-detects fieldType from metadata for date field", () => {
|
|
408
|
-
const { result } = renderHook(() =>
|
|
409
|
-
useCollectionParams({
|
|
391
|
+
useCollection({
|
|
410
392
|
metadata: testMetadata,
|
|
411
393
|
tableName: "task",
|
|
394
|
+
query: FAKE_QUERY,
|
|
395
|
+
params: { pageSize: 10 },
|
|
412
396
|
}),
|
|
413
397
|
);
|
|
414
|
-
|
|
415
|
-
act(() => {
|
|
416
|
-
result.current.addFilter("dueDate", "2026-01-01");
|
|
417
|
-
});
|
|
418
|
-
|
|
419
|
-
expect(result.current.filters[0].fieldType).toBe("date");
|
|
420
|
-
});
|
|
421
|
-
|
|
422
|
-
it("auto-detects fieldType from metadata for uuid field", () => {
|
|
423
|
-
const { result } = renderHook(() =>
|
|
424
|
-
useCollectionParams({
|
|
425
|
-
metadata: testMetadata,
|
|
426
|
-
tableName: "task",
|
|
427
|
-
}),
|
|
428
|
-
);
|
|
429
|
-
|
|
430
|
-
act(() => {
|
|
431
|
-
result.current.addFilter("id", "some-uuid");
|
|
432
|
-
});
|
|
433
|
-
|
|
434
|
-
expect(result.current.filters[0].fieldType).toBe("uuid");
|
|
435
|
-
});
|
|
436
|
-
|
|
437
|
-
it("auto-detects fieldType from metadata for number field", () => {
|
|
438
|
-
const { result } = renderHook(() =>
|
|
439
|
-
useCollectionParams({
|
|
440
|
-
metadata: testMetadata,
|
|
441
|
-
tableName: "task",
|
|
442
|
-
}),
|
|
443
|
-
);
|
|
444
|
-
|
|
445
|
-
act(() => {
|
|
446
|
-
result.current.addFilter("count", 42);
|
|
447
|
-
});
|
|
448
|
-
|
|
449
|
-
expect(result.current.filters[0].fieldType).toBe("number");
|
|
398
|
+
expect(result.current.toQueryArgs().variables).toEqual({ first: 10 });
|
|
450
399
|
});
|
|
451
400
|
|
|
452
401
|
it("applies typed initialSort", () => {
|
|
453
402
|
const { result } = renderHook(() =>
|
|
454
|
-
|
|
403
|
+
useCollection({
|
|
455
404
|
metadata: testMetadata,
|
|
456
405
|
tableName: "task",
|
|
457
|
-
|
|
406
|
+
query: FAKE_QUERY,
|
|
407
|
+
params: {
|
|
408
|
+
initialSort: [{ field: "dueDate", direction: "Desc" }],
|
|
409
|
+
},
|
|
458
410
|
}),
|
|
459
411
|
);
|
|
460
412
|
|
package/src/component/{collection-params/use-collection-params.ts → collection/use-collection.ts}
RENAMED
|
@@ -1,20 +1,27 @@
|
|
|
1
|
-
import { useCallback, useMemo,
|
|
2
|
-
import type {
|
|
3
|
-
FieldType,
|
|
4
|
-
TableMetadataMap,
|
|
5
|
-
} from "../../generator/metadata-generator";
|
|
1
|
+
import { useCallback, useMemo, useState } from "react";
|
|
2
|
+
import type { TableMetadataMap } from "../../generator/metadata-generator";
|
|
6
3
|
import type {
|
|
7
4
|
Filter,
|
|
8
5
|
FilterOperator,
|
|
6
|
+
MetadataFilter,
|
|
9
7
|
QueryVariables,
|
|
10
8
|
SortState,
|
|
11
|
-
|
|
12
|
-
|
|
9
|
+
UseCollectionOptions,
|
|
10
|
+
UseCollectionReturn,
|
|
13
11
|
ExtractQueryVariables,
|
|
14
12
|
} from "../types";
|
|
15
|
-
import { fieldTypeToFilterConfig } from "../types";
|
|
16
13
|
import type { FieldName } from "../types";
|
|
17
14
|
|
|
15
|
+
/**
|
|
16
|
+
* Resolves query variables type: uses `ExtractQueryVariables<TQuery>` when
|
|
17
|
+
* the query document carries a `__variablesType` brand (e.g. gql-tada),
|
|
18
|
+
* otherwise falls back to `QueryVariables<TFieldName>`.
|
|
19
|
+
*/
|
|
20
|
+
type ResolveVariables<TQuery, TFieldName extends string = string> =
|
|
21
|
+
ExtractQueryVariables<TQuery> extends never
|
|
22
|
+
? QueryVariables<TFieldName>
|
|
23
|
+
: ExtractQueryVariables<TQuery>;
|
|
24
|
+
|
|
18
25
|
// -----------------------------------------------------------------------------
|
|
19
26
|
// Overload signatures
|
|
20
27
|
// -----------------------------------------------------------------------------
|
|
@@ -23,97 +30,91 @@ import type { FieldName } from "../types";
|
|
|
23
30
|
* Hook for managing collection query parameters (filters, sort, pagination)
|
|
24
31
|
* with metadata-based field name typing and automatic `fieldType` detection.
|
|
25
32
|
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
* The `query` value is only used for type inference and ignored at runtime.
|
|
33
|
+
* `toQueryArgs()` returns `{ query, variables }` so the result can be
|
|
34
|
+
* spread directly into `useQuery()`.
|
|
29
35
|
*
|
|
30
36
|
* @example
|
|
31
37
|
* ```tsx
|
|
32
38
|
* import { tableMetadata } from "./generated/data-viewer-metadata.generated";
|
|
33
39
|
*
|
|
34
|
-
*
|
|
35
|
-
* const params = useCollectionParams({
|
|
36
|
-
* metadata: tableMetadata,
|
|
37
|
-
* tableName: "task",
|
|
38
|
-
* pageSize: 20,
|
|
39
|
-
* });
|
|
40
|
-
*
|
|
41
|
-
* // With query — variables typed as VariablesOf<typeof GET_TASKS>
|
|
42
|
-
* const params = useCollectionParams({
|
|
40
|
+
* const collection = useCollection({
|
|
43
41
|
* metadata: tableMetadata,
|
|
44
42
|
* tableName: "task",
|
|
45
43
|
* query: GET_TASKS,
|
|
46
|
-
* pageSize: 20,
|
|
44
|
+
* params: { pageSize: 20 },
|
|
47
45
|
* });
|
|
46
|
+
* const [result] = useQuery({ ...collection.toQueryArgs() });
|
|
48
47
|
* ```
|
|
49
48
|
*/
|
|
50
|
-
export function
|
|
49
|
+
export function useCollection<
|
|
51
50
|
const TMetadata extends TableMetadataMap,
|
|
52
51
|
TTableName extends string & keyof TMetadata,
|
|
53
|
-
TQuery
|
|
52
|
+
TQuery,
|
|
54
53
|
>(
|
|
55
|
-
options:
|
|
54
|
+
options: UseCollectionOptions<
|
|
55
|
+
FieldName<TMetadata, TTableName>,
|
|
56
|
+
MetadataFilter<TMetadata, TTableName>
|
|
57
|
+
> & {
|
|
56
58
|
metadata: TMetadata;
|
|
57
59
|
tableName: TTableName;
|
|
58
|
-
query
|
|
60
|
+
query: TQuery;
|
|
59
61
|
},
|
|
60
|
-
):
|
|
62
|
+
): UseCollectionReturn<
|
|
61
63
|
FieldName<TMetadata, TTableName>,
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
:
|
|
64
|
+
ResolveVariables<TQuery, FieldName<TMetadata, TTableName>>,
|
|
65
|
+
{
|
|
66
|
+
query: TQuery;
|
|
67
|
+
variables: ResolveVariables<TQuery, FieldName<TMetadata, TTableName>>;
|
|
68
|
+
}
|
|
65
69
|
>;
|
|
66
70
|
|
|
67
71
|
/**
|
|
68
72
|
* Hook for managing collection query parameters (filters, sort, pagination).
|
|
69
73
|
*
|
|
70
74
|
* Produces `variables` in Tailor Platform format that can be passed directly
|
|
71
|
-
* to a GraphQL query (e.g. urql's `useQuery`)
|
|
75
|
+
* to a GraphQL query (e.g. urql's `useQuery`) via `toQueryArgs()`.
|
|
72
76
|
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
77
|
+
* `toQueryArgs()` returns `{ query, variables }` so the result can be spread
|
|
78
|
+
* directly into `useQuery()`.
|
|
75
79
|
*
|
|
76
80
|
* @example
|
|
77
81
|
* ```tsx
|
|
78
|
-
* const
|
|
79
|
-
* const [result] = useQuery({
|
|
80
|
-
*
|
|
81
|
-
* // With query — variables auto-typed
|
|
82
|
-
* const params = useCollectionParams({ query: GET_ORDERS, pageSize: 20 });
|
|
82
|
+
* const collection = useCollection({ query: GET_ORDERS, params: { pageSize: 20 } });
|
|
83
|
+
* const [result] = useQuery({ ...collection.toQueryArgs() });
|
|
83
84
|
* ```
|
|
84
85
|
*/
|
|
85
|
-
export function
|
|
86
|
-
options
|
|
87
|
-
query
|
|
86
|
+
export function useCollection<TQuery>(
|
|
87
|
+
options: UseCollectionOptions & {
|
|
88
|
+
query: TQuery;
|
|
88
89
|
metadata?: never;
|
|
89
90
|
tableName?: never;
|
|
90
91
|
},
|
|
91
|
-
):
|
|
92
|
+
): UseCollectionReturn<
|
|
92
93
|
string,
|
|
93
|
-
|
|
94
|
+
ResolveVariables<TQuery>,
|
|
95
|
+
{ query: TQuery; variables: ResolveVariables<TQuery> }
|
|
94
96
|
>;
|
|
95
97
|
|
|
96
98
|
// -----------------------------------------------------------------------------
|
|
97
99
|
// Implementation
|
|
98
100
|
// -----------------------------------------------------------------------------
|
|
99
|
-
export function
|
|
100
|
-
options:
|
|
101
|
+
export function useCollection(
|
|
102
|
+
options: UseCollectionOptions & {
|
|
101
103
|
metadata?: TableMetadataMap;
|
|
102
104
|
tableName?: string;
|
|
103
|
-
|
|
104
|
-
|
|
105
|
+
query: unknown;
|
|
106
|
+
},
|
|
107
|
+
): UseCollectionReturn<
|
|
108
|
+
string,
|
|
109
|
+
QueryVariables,
|
|
110
|
+
{ query: unknown; variables: QueryVariables }
|
|
111
|
+
> {
|
|
112
|
+
const { params = {}, query: queryDocument } = options;
|
|
105
113
|
const {
|
|
106
114
|
initialFilters = [],
|
|
107
115
|
initialSort = [],
|
|
108
116
|
pageSize: initialPageSize = 20,
|
|
109
|
-
|
|
110
|
-
tableName,
|
|
111
|
-
} = options;
|
|
112
|
-
|
|
113
|
-
// Keep a ref to the resolved fields list so callbacks can look up fieldType.
|
|
114
|
-
const fieldsRef = useRef(
|
|
115
|
-
metadata && tableName ? metadata[tableName]?.fields : undefined,
|
|
116
|
-
);
|
|
117
|
+
} = params;
|
|
117
118
|
|
|
118
119
|
// ---------------------------------------------------------------------------
|
|
119
120
|
// State
|
|
@@ -131,17 +132,8 @@ export function useCollectionParams(
|
|
|
131
132
|
(field: string, value: unknown, operator: FilterOperator = "eq") => {
|
|
132
133
|
setFiltersState((prev) => {
|
|
133
134
|
const existing = prev.findIndex((f) => f.field === field);
|
|
134
|
-
// Auto-detect fieldType from metadata when available
|
|
135
|
-
const fieldMeta = fieldsRef.current?.find((f) => f.name === field);
|
|
136
|
-
const detectedType: Filter["fieldType"] = fieldMeta
|
|
137
|
-
? (fieldTypeToFilterConfig(
|
|
138
|
-
fieldMeta.type as FieldType,
|
|
139
|
-
fieldMeta.enumValues as readonly string[] | undefined,
|
|
140
|
-
)?.type ?? "string")
|
|
141
|
-
: "string";
|
|
142
135
|
const newFilter: Filter = {
|
|
143
136
|
field,
|
|
144
|
-
fieldType: detectedType,
|
|
145
137
|
operator,
|
|
146
138
|
value,
|
|
147
139
|
};
|
|
@@ -245,11 +237,11 @@ export function useCollectionParams(
|
|
|
245
237
|
|
|
246
238
|
// Build query (filters)
|
|
247
239
|
if (filters.length > 0) {
|
|
248
|
-
const
|
|
240
|
+
const filterQuery: Record<string, unknown> = {};
|
|
249
241
|
for (const filter of filters) {
|
|
250
|
-
|
|
242
|
+
filterQuery[filter.field] = { [filter.operator]: filter.value };
|
|
251
243
|
}
|
|
252
|
-
vars.query =
|
|
244
|
+
vars.query = filterQuery;
|
|
253
245
|
}
|
|
254
246
|
|
|
255
247
|
// Build order (sort)
|
|
@@ -268,11 +260,18 @@ export function useCollectionParams(
|
|
|
268
260
|
return vars;
|
|
269
261
|
}, [filters, sortStates, pageSize, cursor]);
|
|
270
262
|
|
|
263
|
+
// ---------------------------------------------------------------------------
|
|
264
|
+
// toQueryArgs
|
|
265
|
+
// ---------------------------------------------------------------------------
|
|
266
|
+
const toQueryArgs = useCallback(() => {
|
|
267
|
+
return { query: queryDocument, variables };
|
|
268
|
+
}, [queryDocument, variables]);
|
|
269
|
+
|
|
271
270
|
// ---------------------------------------------------------------------------
|
|
272
271
|
// Return
|
|
273
272
|
// ---------------------------------------------------------------------------
|
|
274
273
|
return {
|
|
275
|
-
|
|
274
|
+
toQueryArgs,
|
|
276
275
|
filters,
|
|
277
276
|
addFilter,
|
|
278
277
|
setFilters,
|
|
@@ -302,7 +302,7 @@ function formatValue(value: unknown): ReactNode {
|
|
|
302
302
|
*
|
|
303
303
|
* @example
|
|
304
304
|
* ```tsx
|
|
305
|
-
* const table = useDataTable({ columns, data, loading,
|
|
305
|
+
* const table = useDataTable({ columns, data, loading, collection });
|
|
306
306
|
*
|
|
307
307
|
* <DataTable.Root {...table.rootProps}>
|
|
308
308
|
* <DataTable.Headers />
|