@izumisy-tailor/tailor-data-viewer 0.2.7 → 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 -66
- package/src/component/{collection-params/use-collection-params.ts → collection/use-collection.ts} +60 -45
- 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 +8 -8
- package/src/component/search-filter-form.tsx +1 -1
- package/src/component/types.ts +35 -20
- package/src/component/collection-params/collection-params-provider.tsx +0 -83
package/README.md
CHANGED
|
@@ -5,8 +5,8 @@ A low-level React component library for building data table interfaces with Tail
|
|
|
5
5
|
## Features
|
|
6
6
|
|
|
7
7
|
- **Separation of Concerns**: Data fetching, query parameter management, and UI are fully decoupled
|
|
8
|
-
- **`
|
|
9
|
-
- **`
|
|
8
|
+
- **`useCollection` Hook**: Manages filter, sort, and pagination state; outputs Tailor Platform-compatible GraphQL variables
|
|
9
|
+
- **`Collection.Provider`**: Shares query parameters via React Context across sibling components
|
|
10
10
|
- **`Table.*` Compound Components**: Static, unstyled table primitives (`<table>`, `<thead>`, `<tbody>`, `<tr>`, `<th>`, `<td>`)
|
|
11
11
|
- **`DataTable.*` Compound Components**: Data-bound table with sort indicators, cell renderers, and `useDataTable` integration
|
|
12
12
|
- **`useDataTable` Hook**: Integrates data, column visibility, row operations (optimistic updates), and props generators
|
|
@@ -15,7 +15,7 @@ A low-level React component library for building data table interfaces with Tail
|
|
|
15
15
|
- **Utility Components**: `ColumnSelector`, `CsvButton`, `SearchFilterForm`, `Pagination` — all props-based, spreadable from hooks
|
|
16
16
|
- **Multi-sort Support**: Multiple simultaneous sort fields
|
|
17
17
|
- **Optimistic Updates**: `updateRow`, `deleteRow`, `insertRow` with rollback
|
|
18
|
-
- **Presentation Agnostic**: Same `
|
|
18
|
+
- **Presentation Agnostic**: Same `useCollection` can drive tables, kanbans, calendars, etc.
|
|
19
19
|
|
|
20
20
|
## Installation
|
|
21
21
|
|
|
@@ -47,9 +47,9 @@ npm install react react-dom
|
|
|
47
47
|
|
|
48
48
|
```tsx
|
|
49
49
|
import {
|
|
50
|
-
|
|
50
|
+
useCollection,
|
|
51
51
|
useDataTable,
|
|
52
|
-
|
|
52
|
+
Collection,
|
|
53
53
|
DataTable,
|
|
54
54
|
Pagination,
|
|
55
55
|
field,
|
|
@@ -87,73 +87,76 @@ const columns = [
|
|
|
87
87
|
|
|
88
88
|
// 2. Build a page
|
|
89
89
|
function OrdersPage() {
|
|
90
|
-
const
|
|
91
|
-
const [result] = useQuery({
|
|
90
|
+
const collection = useCollection({ query: GET_ORDERS, params: { pageSize: 20 } });
|
|
91
|
+
const [result] = useQuery({ ...collection.toQueryArgs() });
|
|
92
92
|
|
|
93
93
|
const table = useDataTable<Order>({
|
|
94
94
|
columns,
|
|
95
95
|
data: result.data?.orders,
|
|
96
96
|
loading: result.fetching,
|
|
97
|
-
|
|
97
|
+
collection,
|
|
98
98
|
});
|
|
99
99
|
|
|
100
100
|
return (
|
|
101
|
-
<
|
|
101
|
+
<Collection.Provider value={collection}>
|
|
102
102
|
<DataTable.Root {...table.rootProps}>
|
|
103
103
|
<DataTable.Headers />
|
|
104
104
|
<DataTable.Body />
|
|
105
105
|
</DataTable.Root>
|
|
106
106
|
<Pagination {...table} />
|
|
107
|
-
</
|
|
107
|
+
</Collection.Provider>
|
|
108
108
|
);
|
|
109
109
|
}
|
|
110
110
|
```
|
|
111
111
|
|
|
112
112
|
## API Overview
|
|
113
113
|
|
|
114
|
-
### `
|
|
114
|
+
### `useCollection(options)`
|
|
115
115
|
|
|
116
|
-
Manages filter, sort, and pagination state. Returns Tailor Platform-compatible
|
|
116
|
+
Manages filter, sort, and pagination state. Returns `toQueryArgs()` which produces Tailor Platform-compatible arguments for `useQuery()`.
|
|
117
117
|
|
|
118
118
|
```tsx
|
|
119
|
-
const
|
|
120
|
-
|
|
121
|
-
|
|
119
|
+
const collection = useCollection({
|
|
120
|
+
query: GET_ORDERS,
|
|
121
|
+
params: {
|
|
122
|
+
pageSize: 20,
|
|
123
|
+
initialSort: [{ field: "createdAt", direction: "Desc" }],
|
|
124
|
+
},
|
|
122
125
|
});
|
|
123
126
|
|
|
124
|
-
//
|
|
125
|
-
const [result] = useQuery({
|
|
127
|
+
// Spread toQueryArgs() into useQuery
|
|
128
|
+
const [result] = useQuery({ ...collection.toQueryArgs() });
|
|
126
129
|
|
|
127
130
|
// Filter operations
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
131
|
+
collection.addFilter("status", "ACTIVE", "eq");
|
|
132
|
+
collection.setFilters([{ field: "status", fieldType: "enum", operator: "eq", value: "ACTIVE" }]);
|
|
133
|
+
collection.removeFilter("status");
|
|
134
|
+
collection.clearFilters();
|
|
132
135
|
|
|
133
136
|
// Sort operations
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
+
collection.setSort("createdAt", "Desc");
|
|
138
|
+
collection.setSort("name", "Asc", true); // append for multi-sort
|
|
139
|
+
collection.clearSort();
|
|
137
140
|
|
|
138
141
|
// Pagination
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
+
collection.nextPage(endCursor);
|
|
143
|
+
collection.prevPage();
|
|
144
|
+
collection.resetPage();
|
|
142
145
|
```
|
|
143
146
|
|
|
144
|
-
### `
|
|
147
|
+
### `Collection.Provider` / `useCollectionContext()`
|
|
145
148
|
|
|
146
|
-
Shares `
|
|
149
|
+
Shares `useCollection` return value via Context. Child components access it with `useCollectionContext()`.
|
|
147
150
|
|
|
148
151
|
```tsx
|
|
149
|
-
<
|
|
150
|
-
<StatusFilter /> {/*
|
|
152
|
+
<Collection.Provider value={collection}>
|
|
153
|
+
<StatusFilter /> {/* useCollectionContext() inside */}
|
|
151
154
|
<DataTable.Root {...table.rootProps}>
|
|
152
155
|
<DataTable.Headers />
|
|
153
156
|
<DataTable.Body />
|
|
154
157
|
</DataTable.Root>
|
|
155
158
|
<Pagination {...table} />
|
|
156
|
-
</
|
|
159
|
+
</Collection.Provider>
|
|
157
160
|
```
|
|
158
161
|
|
|
159
162
|
Provider is optional — for simple cases, pass params directly via props.
|
|
@@ -246,7 +249,7 @@ const table = useDataTable<Order>({
|
|
|
246
249
|
data: result.data?.orders, // CollectionResult<Order>
|
|
247
250
|
loading: result.fetching,
|
|
248
251
|
error: result.error,
|
|
249
|
-
|
|
252
|
+
collection,
|
|
250
253
|
});
|
|
251
254
|
|
|
252
255
|
// Spread props to components
|
|
@@ -320,17 +323,17 @@ Pair with `useDataTable` for automatic header sorting, cell rendering, and row o
|
|
|
320
323
|
|
|
321
324
|
### Utility Components
|
|
322
325
|
|
|
323
|
-
All utility components are props-based and designed to be used with spread from `useDataTable` / `
|
|
326
|
+
All utility components are props-based and designed to be used with spread from `useDataTable` / `useCollection`.
|
|
324
327
|
|
|
325
328
|
| Component | Spread from | Description |
|
|
326
329
|
|-----------|------------|-------------|
|
|
327
330
|
| `ColumnSelector` | `{...table}` | Column visibility toggle UI |
|
|
328
331
|
| `CsvButton` | `{...table}` | Export visible data as CSV |
|
|
329
|
-
| `SearchFilterForm` | `{...table, ...
|
|
332
|
+
| `SearchFilterForm` | `{...table, ...collection}` | Multi-field filter form with operator selection |
|
|
330
333
|
| `Pagination` | `{...table}` | Previous/Next page controls |
|
|
331
334
|
|
|
332
335
|
```tsx
|
|
333
|
-
<SearchFilterForm {...table} {...
|
|
336
|
+
<SearchFilterForm {...table} {...collection} />
|
|
334
337
|
<ColumnSelector {...table} />
|
|
335
338
|
<CsvButton {...table} filename="orders-export" />
|
|
336
339
|
<Pagination {...table} />
|
package/package.json
CHANGED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { createContext, useContext, type ReactNode } from "react";
|
|
2
|
+
import type { UseCollectionReturn } from "../types";
|
|
3
|
+
|
|
4
|
+
const CollectionContext = createContext<UseCollectionReturn<
|
|
5
|
+
string,
|
|
6
|
+
unknown,
|
|
7
|
+
unknown
|
|
8
|
+
> | null>(null);
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Provider that shares collection query parameters via React Context.
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```tsx
|
|
15
|
+
* const collection = useCollection({ params: { pageSize: 20 } });
|
|
16
|
+
*
|
|
17
|
+
* <CollectionProvider value={collection}>
|
|
18
|
+
* <FilterPanel />
|
|
19
|
+
* <DataTable.Root {...table.rootProps}>...</DataTable.Root>
|
|
20
|
+
* <Pagination {...table} />
|
|
21
|
+
* </CollectionProvider>
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
export function CollectionProvider({
|
|
25
|
+
value,
|
|
26
|
+
children,
|
|
27
|
+
}: {
|
|
28
|
+
value: UseCollectionReturn<string, unknown, unknown>;
|
|
29
|
+
children: ReactNode;
|
|
30
|
+
}) {
|
|
31
|
+
return (
|
|
32
|
+
<CollectionContext.Provider value={value}>
|
|
33
|
+
{children}
|
|
34
|
+
</CollectionContext.Provider>
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Hook to access collection state from the nearest `Collection.Provider`.
|
|
42
|
+
*
|
|
43
|
+
* Returns the same interface as `useCollection()`. Pass a `TFieldName`
|
|
44
|
+
* type parameter to narrow method arguments like `addFilter` / `setSort`.
|
|
45
|
+
*
|
|
46
|
+
* @typeParam TFieldName - Union of allowed field name strings (default: `string`).
|
|
47
|
+
*
|
|
48
|
+
* @throws Error if used outside of `Collection.Provider`.
|
|
49
|
+
*
|
|
50
|
+
* @example
|
|
51
|
+
* ```tsx
|
|
52
|
+
* function StatusFilter() {
|
|
53
|
+
* const { filters, addFilter, removeFilter } = useCollectionContext();
|
|
54
|
+
* // ...
|
|
55
|
+
* }
|
|
56
|
+
*
|
|
57
|
+
* // With typed field names:
|
|
58
|
+
* type TaskField = FieldName<typeof tableMetadata, "task">;
|
|
59
|
+
* const { addFilter } = useCollectionContext<TaskField>();
|
|
60
|
+
* ```
|
|
61
|
+
*/
|
|
62
|
+
export function useCollectionContext<
|
|
63
|
+
TFieldName extends string = string,
|
|
64
|
+
>(): UseCollectionReturn<TFieldName> {
|
|
65
|
+
const ctx = useContext(CollectionContext);
|
|
66
|
+
if (!ctx) {
|
|
67
|
+
throw new Error(
|
|
68
|
+
"useCollectionContext must be used within <Collection.Provider>",
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
return ctx as UseCollectionReturn<TFieldName>;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* `Collection` namespace object providing the Provider component.
|
|
78
|
+
*
|
|
79
|
+
* @example
|
|
80
|
+
* ```tsx
|
|
81
|
+
* <Collection.Provider value={collection}>
|
|
82
|
+
* ...
|
|
83
|
+
* </Collection.Provider>
|
|
84
|
+
* ```
|
|
85
|
+
*/
|
|
86
|
+
export const Collection = {
|
|
87
|
+
Provider: CollectionProvider,
|
|
88
|
+
} as const;
|
|
89
|
+
|
|
90
|
+
|
|
@@ -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,39 +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
|
-
|
|
48
|
+
useCollection({
|
|
49
|
+
query: FAKE_QUERY,
|
|
50
|
+
params: {
|
|
51
|
+
initialFilters: [
|
|
52
|
+
{
|
|
53
|
+
field: "status",
|
|
54
|
+
operator: "eq",
|
|
55
|
+
value: "ACTIVE",
|
|
56
|
+
},
|
|
57
|
+
],
|
|
58
|
+
},
|
|
51
59
|
}),
|
|
52
60
|
);
|
|
53
61
|
expect(result.current.filters).toHaveLength(1);
|
|
54
|
-
expect(result.current.variables.query).toEqual({
|
|
62
|
+
expect(result.current.toQueryArgs().variables.query).toEqual({
|
|
55
63
|
status: { eq: "ACTIVE" },
|
|
56
64
|
});
|
|
57
65
|
});
|
|
@@ -62,7 +70,7 @@ describe("useCollectionParams", () => {
|
|
|
62
70
|
// ---------------------------------------------------------------------------
|
|
63
71
|
describe("filter operations", () => {
|
|
64
72
|
it("adds a filter", () => {
|
|
65
|
-
const { result } = renderHook(() =>
|
|
73
|
+
const { result } = renderHook(() => useCollection({ query: FAKE_QUERY }));
|
|
66
74
|
|
|
67
75
|
act(() => {
|
|
68
76
|
result.current.addFilter("status", "ACTIVE", "eq");
|
|
@@ -74,13 +82,13 @@ describe("useCollectionParams", () => {
|
|
|
74
82
|
operator: "eq",
|
|
75
83
|
value: "ACTIVE",
|
|
76
84
|
});
|
|
77
|
-
expect(result.current.variables.query).toEqual({
|
|
85
|
+
expect(result.current.toQueryArgs().variables.query).toEqual({
|
|
78
86
|
status: { eq: "ACTIVE" },
|
|
79
87
|
});
|
|
80
88
|
});
|
|
81
89
|
|
|
82
90
|
it("replaces filter for same field", () => {
|
|
83
|
-
const { result } = renderHook(() =>
|
|
91
|
+
const { result } = renderHook(() => useCollection({ query: FAKE_QUERY }));
|
|
84
92
|
|
|
85
93
|
act(() => {
|
|
86
94
|
result.current.addFilter("status", "ACTIVE", "eq");
|
|
@@ -94,7 +102,7 @@ describe("useCollectionParams", () => {
|
|
|
94
102
|
});
|
|
95
103
|
|
|
96
104
|
it("sets filters in bulk", () => {
|
|
97
|
-
const { result } = renderHook(() =>
|
|
105
|
+
const { result } = renderHook(() => useCollection({ query: FAKE_QUERY }));
|
|
98
106
|
|
|
99
107
|
act(() => {
|
|
100
108
|
result.current.setFilters([
|
|
@@ -112,14 +120,14 @@ describe("useCollectionParams", () => {
|
|
|
112
120
|
});
|
|
113
121
|
|
|
114
122
|
expect(result.current.filters).toHaveLength(2);
|
|
115
|
-
expect(result.current.variables.query).toEqual({
|
|
123
|
+
expect(result.current.toQueryArgs().variables.query).toEqual({
|
|
116
124
|
status: { eq: "ACTIVE" },
|
|
117
125
|
amount: { gte: 1000 },
|
|
118
126
|
});
|
|
119
127
|
});
|
|
120
128
|
|
|
121
129
|
it("removes a filter", () => {
|
|
122
|
-
const { result } = renderHook(() =>
|
|
130
|
+
const { result } = renderHook(() => useCollection({ query: FAKE_QUERY }));
|
|
123
131
|
|
|
124
132
|
act(() => {
|
|
125
133
|
result.current.addFilter("status", "ACTIVE", "eq");
|
|
@@ -134,7 +142,7 @@ describe("useCollectionParams", () => {
|
|
|
134
142
|
});
|
|
135
143
|
|
|
136
144
|
it("clears all filters", () => {
|
|
137
|
-
const { result } = renderHook(() =>
|
|
145
|
+
const { result } = renderHook(() => useCollection({ query: FAKE_QUERY }));
|
|
138
146
|
|
|
139
147
|
act(() => {
|
|
140
148
|
result.current.addFilter("status", "ACTIVE", "eq");
|
|
@@ -145,11 +153,11 @@ describe("useCollectionParams", () => {
|
|
|
145
153
|
});
|
|
146
154
|
|
|
147
155
|
expect(result.current.filters).toHaveLength(0);
|
|
148
|
-
expect(result.current.variables.query).toBeUndefined();
|
|
156
|
+
expect(result.current.toQueryArgs().variables.query).toBeUndefined();
|
|
149
157
|
});
|
|
150
158
|
|
|
151
159
|
it("resets pagination when filters change", () => {
|
|
152
|
-
const { result } = renderHook(() =>
|
|
160
|
+
const { result } = renderHook(() => useCollection({ query: FAKE_QUERY }));
|
|
153
161
|
|
|
154
162
|
// Navigate to next page
|
|
155
163
|
act(() => {
|
|
@@ -171,7 +179,7 @@ describe("useCollectionParams", () => {
|
|
|
171
179
|
// ---------------------------------------------------------------------------
|
|
172
180
|
describe("sort operations", () => {
|
|
173
181
|
it("sets sort", () => {
|
|
174
|
-
const { result } = renderHook(() =>
|
|
182
|
+
const { result } = renderHook(() => useCollection({ query: FAKE_QUERY }));
|
|
175
183
|
|
|
176
184
|
act(() => {
|
|
177
185
|
result.current.setSort("createdAt", "Desc");
|
|
@@ -180,13 +188,13 @@ describe("useCollectionParams", () => {
|
|
|
180
188
|
expect(result.current.sortStates).toEqual([
|
|
181
189
|
{ field: "createdAt", direction: "Desc" },
|
|
182
190
|
]);
|
|
183
|
-
expect(result.current.variables.order).toEqual([
|
|
191
|
+
expect(result.current.toQueryArgs().variables.order).toEqual([
|
|
184
192
|
{ field: "createdAt", direction: "Desc" },
|
|
185
193
|
]);
|
|
186
194
|
});
|
|
187
195
|
|
|
188
196
|
it("replaces sort by default", () => {
|
|
189
|
-
const { result } = renderHook(() =>
|
|
197
|
+
const { result } = renderHook(() => useCollection({ query: FAKE_QUERY }));
|
|
190
198
|
|
|
191
199
|
act(() => {
|
|
192
200
|
result.current.setSort("createdAt", "Desc");
|
|
@@ -201,7 +209,7 @@ describe("useCollectionParams", () => {
|
|
|
201
209
|
});
|
|
202
210
|
|
|
203
211
|
it("appends sort with append=true (multi-sort)", () => {
|
|
204
|
-
const { result } = renderHook(() =>
|
|
212
|
+
const { result } = renderHook(() => useCollection({ query: FAKE_QUERY }));
|
|
205
213
|
|
|
206
214
|
act(() => {
|
|
207
215
|
result.current.setSort("createdAt", "Desc");
|
|
@@ -217,7 +225,7 @@ describe("useCollectionParams", () => {
|
|
|
217
225
|
});
|
|
218
226
|
|
|
219
227
|
it("clears sort", () => {
|
|
220
|
-
const { result } = renderHook(() =>
|
|
228
|
+
const { result } = renderHook(() => useCollection({ query: FAKE_QUERY }));
|
|
221
229
|
|
|
222
230
|
act(() => {
|
|
223
231
|
result.current.setSort("createdAt", "Desc");
|
|
@@ -227,7 +235,7 @@ describe("useCollectionParams", () => {
|
|
|
227
235
|
});
|
|
228
236
|
|
|
229
237
|
expect(result.current.sortStates).toEqual([]);
|
|
230
|
-
expect(result.current.variables.order).toBeUndefined();
|
|
238
|
+
expect(result.current.toQueryArgs().variables.order).toBeUndefined();
|
|
231
239
|
});
|
|
232
240
|
});
|
|
233
241
|
|
|
@@ -236,7 +244,7 @@ describe("useCollectionParams", () => {
|
|
|
236
244
|
// ---------------------------------------------------------------------------
|
|
237
245
|
describe("pagination operations", () => {
|
|
238
246
|
it("navigates to next page", () => {
|
|
239
|
-
const { result } = renderHook(() =>
|
|
247
|
+
const { result } = renderHook(() => useCollection({ query: FAKE_QUERY }));
|
|
240
248
|
|
|
241
249
|
act(() => {
|
|
242
250
|
result.current.nextPage("cursor1");
|
|
@@ -244,11 +252,11 @@ describe("useCollectionParams", () => {
|
|
|
244
252
|
|
|
245
253
|
expect(result.current.cursor).toBe("cursor1");
|
|
246
254
|
expect(result.current.hasPrevPage).toBe(true);
|
|
247
|
-
expect(result.current.variables.after).toBe("cursor1");
|
|
255
|
+
expect(result.current.toQueryArgs().variables.after).toBe("cursor1");
|
|
248
256
|
});
|
|
249
257
|
|
|
250
258
|
it("navigates back to previous page", () => {
|
|
251
|
-
const { result } = renderHook(() =>
|
|
259
|
+
const { result } = renderHook(() => useCollection({ query: FAKE_QUERY }));
|
|
252
260
|
|
|
253
261
|
act(() => {
|
|
254
262
|
result.current.nextPage("cursor1");
|
|
@@ -265,7 +273,7 @@ describe("useCollectionParams", () => {
|
|
|
265
273
|
});
|
|
266
274
|
|
|
267
275
|
it("navigates back to first page", () => {
|
|
268
|
-
const { result } = renderHook(() =>
|
|
276
|
+
const { result } = renderHook(() => useCollection({ query: FAKE_QUERY }));
|
|
269
277
|
|
|
270
278
|
act(() => {
|
|
271
279
|
result.current.nextPage("cursor1");
|
|
@@ -279,7 +287,7 @@ describe("useCollectionParams", () => {
|
|
|
279
287
|
});
|
|
280
288
|
|
|
281
289
|
it("resets page", () => {
|
|
282
|
-
const { result } = renderHook(() =>
|
|
290
|
+
const { result } = renderHook(() => useCollection({ query: FAKE_QUERY }));
|
|
283
291
|
|
|
284
292
|
act(() => {
|
|
285
293
|
result.current.nextPage("cursor1");
|
|
@@ -295,21 +303,24 @@ describe("useCollectionParams", () => {
|
|
|
295
303
|
});
|
|
296
304
|
|
|
297
305
|
// ---------------------------------------------------------------------------
|
|
298
|
-
//
|
|
306
|
+
// toQueryArgs
|
|
299
307
|
// ---------------------------------------------------------------------------
|
|
300
|
-
describe("
|
|
308
|
+
describe("toQueryArgs", () => {
|
|
301
309
|
it("generates complete variables with filters, sort, and cursor", () => {
|
|
302
310
|
const { result } = renderHook(() =>
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
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
|
+
},
|
|
313
324
|
}),
|
|
314
325
|
);
|
|
315
326
|
|
|
@@ -317,21 +328,37 @@ describe("useCollectionParams", () => {
|
|
|
317
328
|
result.current.nextPage("abc123");
|
|
318
329
|
});
|
|
319
330
|
|
|
320
|
-
expect(result.current.
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
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
|
+
},
|
|
325
339
|
});
|
|
326
340
|
});
|
|
327
341
|
|
|
328
342
|
it("omits undefined fields from variables", () => {
|
|
329
|
-
const { result } = renderHook(() =>
|
|
330
|
-
const
|
|
331
|
-
expect(
|
|
332
|
-
expect("query" in
|
|
333
|
-
expect("order" in
|
|
334
|
-
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
|
+
});
|
|
335
362
|
});
|
|
336
363
|
});
|
|
337
364
|
|
|
@@ -361,21 +388,25 @@ describe("useCollectionParams", () => {
|
|
|
361
388
|
|
|
362
389
|
it("works with metadata and tableName", () => {
|
|
363
390
|
const { result } = renderHook(() =>
|
|
364
|
-
|
|
391
|
+
useCollection({
|
|
365
392
|
metadata: testMetadata,
|
|
366
393
|
tableName: "task",
|
|
367
|
-
|
|
394
|
+
query: FAKE_QUERY,
|
|
395
|
+
params: { pageSize: 10 },
|
|
368
396
|
}),
|
|
369
397
|
);
|
|
370
|
-
expect(result.current.variables).toEqual({ first: 10 });
|
|
398
|
+
expect(result.current.toQueryArgs().variables).toEqual({ first: 10 });
|
|
371
399
|
});
|
|
372
400
|
|
|
373
401
|
it("applies typed initialSort", () => {
|
|
374
402
|
const { result } = renderHook(() =>
|
|
375
|
-
|
|
403
|
+
useCollection({
|
|
376
404
|
metadata: testMetadata,
|
|
377
405
|
tableName: "task",
|
|
378
|
-
|
|
406
|
+
query: FAKE_QUERY,
|
|
407
|
+
params: {
|
|
408
|
+
initialSort: [{ field: "dueDate", direction: "Desc" }],
|
|
409
|
+
},
|
|
379
410
|
}),
|
|
380
411
|
);
|
|
381
412
|
|
package/src/component/{collection-params/use-collection-params.ts → collection/use-collection.ts}
RENAMED
|
@@ -6,12 +6,22 @@ import type {
|
|
|
6
6
|
MetadataFilter,
|
|
7
7
|
QueryVariables,
|
|
8
8
|
SortState,
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
UseCollectionOptions,
|
|
10
|
+
UseCollectionReturn,
|
|
11
11
|
ExtractQueryVariables,
|
|
12
12
|
} from "../types";
|
|
13
13
|
import type { FieldName } from "../types";
|
|
14
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
|
+
|
|
15
25
|
// -----------------------------------------------------------------------------
|
|
16
26
|
// Overload signatures
|
|
17
27
|
// -----------------------------------------------------------------------------
|
|
@@ -20,93 +30,91 @@ import type { FieldName } from "../types";
|
|
|
20
30
|
* Hook for managing collection query parameters (filters, sort, pagination)
|
|
21
31
|
* with metadata-based field name typing and automatic `fieldType` detection.
|
|
22
32
|
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
* 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()`.
|
|
26
35
|
*
|
|
27
36
|
* @example
|
|
28
37
|
* ```tsx
|
|
29
38
|
* import { tableMetadata } from "./generated/data-viewer-metadata.generated";
|
|
30
39
|
*
|
|
31
|
-
*
|
|
32
|
-
* const params = useCollectionParams({
|
|
33
|
-
* metadata: tableMetadata,
|
|
34
|
-
* tableName: "task",
|
|
35
|
-
* pageSize: 20,
|
|
36
|
-
* });
|
|
37
|
-
*
|
|
38
|
-
* // With query — variables typed as VariablesOf<typeof GET_TASKS>
|
|
39
|
-
* const params = useCollectionParams({
|
|
40
|
+
* const collection = useCollection({
|
|
40
41
|
* metadata: tableMetadata,
|
|
41
42
|
* tableName: "task",
|
|
42
43
|
* query: GET_TASKS,
|
|
43
|
-
* pageSize: 20,
|
|
44
|
+
* params: { pageSize: 20 },
|
|
44
45
|
* });
|
|
46
|
+
* const [result] = useQuery({ ...collection.toQueryArgs() });
|
|
45
47
|
* ```
|
|
46
48
|
*/
|
|
47
|
-
export function
|
|
49
|
+
export function useCollection<
|
|
48
50
|
const TMetadata extends TableMetadataMap,
|
|
49
51
|
TTableName extends string & keyof TMetadata,
|
|
50
|
-
TQuery
|
|
52
|
+
TQuery,
|
|
51
53
|
>(
|
|
52
|
-
options:
|
|
54
|
+
options: UseCollectionOptions<
|
|
53
55
|
FieldName<TMetadata, TTableName>,
|
|
54
56
|
MetadataFilter<TMetadata, TTableName>
|
|
55
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
|
-
} =
|
|
117
|
+
} = params;
|
|
110
118
|
|
|
111
119
|
// ---------------------------------------------------------------------------
|
|
112
120
|
// State
|
|
@@ -229,11 +237,11 @@ export function useCollectionParams(
|
|
|
229
237
|
|
|
230
238
|
// Build query (filters)
|
|
231
239
|
if (filters.length > 0) {
|
|
232
|
-
const
|
|
240
|
+
const filterQuery: Record<string, unknown> = {};
|
|
233
241
|
for (const filter of filters) {
|
|
234
|
-
|
|
242
|
+
filterQuery[filter.field] = { [filter.operator]: filter.value };
|
|
235
243
|
}
|
|
236
|
-
vars.query =
|
|
244
|
+
vars.query = filterQuery;
|
|
237
245
|
}
|
|
238
246
|
|
|
239
247
|
// Build order (sort)
|
|
@@ -252,11 +260,18 @@ export function useCollectionParams(
|
|
|
252
260
|
return vars;
|
|
253
261
|
}, [filters, sortStates, pageSize, cursor]);
|
|
254
262
|
|
|
263
|
+
// ---------------------------------------------------------------------------
|
|
264
|
+
// toQueryArgs
|
|
265
|
+
// ---------------------------------------------------------------------------
|
|
266
|
+
const toQueryArgs = useCallback(() => {
|
|
267
|
+
return { query: queryDocument, variables };
|
|
268
|
+
}, [queryDocument, variables]);
|
|
269
|
+
|
|
255
270
|
// ---------------------------------------------------------------------------
|
|
256
271
|
// Return
|
|
257
272
|
// ---------------------------------------------------------------------------
|
|
258
273
|
return {
|
|
259
|
-
|
|
274
|
+
toQueryArgs,
|
|
260
275
|
filters,
|
|
261
276
|
addFilter,
|
|
262
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 />
|
|
@@ -15,14 +15,14 @@ import type {
|
|
|
15
15
|
*
|
|
16
16
|
* @example
|
|
17
17
|
* ```tsx
|
|
18
|
-
* const
|
|
19
|
-
* const [result] = useQuery({
|
|
18
|
+
* const collection = useCollection({ query: GET_ORDERS, params: { pageSize: 20 } });
|
|
19
|
+
* const [result] = useQuery({ ...collection.toQueryArgs() });
|
|
20
20
|
*
|
|
21
21
|
* const table = useDataTable<Order>({
|
|
22
22
|
* columns,
|
|
23
23
|
* data: result.data?.orders,
|
|
24
24
|
* loading: result.fetching,
|
|
25
|
-
*
|
|
25
|
+
* collection,
|
|
26
26
|
* });
|
|
27
27
|
*
|
|
28
28
|
* <DataTable.Root {...table.rootProps}>
|
|
@@ -39,7 +39,7 @@ export function useDataTable<TRow extends Record<string, unknown>>(
|
|
|
39
39
|
data,
|
|
40
40
|
loading = false,
|
|
41
41
|
error = null,
|
|
42
|
-
|
|
42
|
+
collection,
|
|
43
43
|
} = options;
|
|
44
44
|
|
|
45
45
|
// ---------------------------------------------------------------------------
|
|
@@ -102,20 +102,20 @@ export function useDataTable<TRow extends Record<string, unknown>>(
|
|
|
102
102
|
);
|
|
103
103
|
|
|
104
104
|
// ---------------------------------------------------------------------------
|
|
105
|
-
// Pagination (delegated from
|
|
105
|
+
// Pagination (delegated from collection)
|
|
106
106
|
// ---------------------------------------------------------------------------
|
|
107
107
|
const nextPage = useCallback(
|
|
108
108
|
(endCursor: string) => {
|
|
109
|
-
|
|
109
|
+
collection?.nextPage(endCursor);
|
|
110
110
|
},
|
|
111
|
-
[
|
|
111
|
+
[collection],
|
|
112
112
|
);
|
|
113
113
|
|
|
114
114
|
const prevPage = useCallback(() => {
|
|
115
|
-
|
|
116
|
-
}, [
|
|
115
|
+
collection?.prevPage();
|
|
116
|
+
}, [collection]);
|
|
117
117
|
|
|
118
|
-
const hasPrevPage =
|
|
118
|
+
const hasPrevPage = collection?.hasPrevPage ?? false;
|
|
119
119
|
|
|
120
120
|
// ---------------------------------------------------------------------------
|
|
121
121
|
// Row Operations (Optimistic Updates)
|
|
@@ -184,11 +184,11 @@ export function useDataTable<TRow extends Record<string, unknown>>(
|
|
|
184
184
|
rows,
|
|
185
185
|
loading,
|
|
186
186
|
error,
|
|
187
|
-
onSort:
|
|
187
|
+
onSort: collection
|
|
188
188
|
? (field: string, direction?: "Asc" | "Desc") =>
|
|
189
|
-
|
|
189
|
+
collection.setSort(field, direction)
|
|
190
190
|
: undefined,
|
|
191
|
-
sortStates:
|
|
191
|
+
sortStates: collection?.sortStates,
|
|
192
192
|
rowOperations: { updateRow, deleteRow, insertRow },
|
|
193
193
|
children: null,
|
|
194
194
|
};
|
|
@@ -197,7 +197,7 @@ export function useDataTable<TRow extends Record<string, unknown>>(
|
|
|
197
197
|
rows,
|
|
198
198
|
loading,
|
|
199
199
|
error,
|
|
200
|
-
|
|
200
|
+
collection,
|
|
201
201
|
updateRow,
|
|
202
202
|
deleteRow,
|
|
203
203
|
insertRow,
|
package/src/component/index.ts
CHANGED
|
@@ -19,8 +19,8 @@ export type {
|
|
|
19
19
|
DisplayColumn,
|
|
20
20
|
Column,
|
|
21
21
|
ColumnDefinition,
|
|
22
|
-
|
|
23
|
-
|
|
22
|
+
UseCollectionOptions,
|
|
23
|
+
UseCollectionReturn,
|
|
24
24
|
UseDataTableOptions,
|
|
25
25
|
UseDataTableReturn,
|
|
26
26
|
RowOperations,
|
|
@@ -47,13 +47,13 @@ export {
|
|
|
47
47
|
fieldTypeToFilterConfig,
|
|
48
48
|
} from "./types";
|
|
49
49
|
|
|
50
|
-
// Collection
|
|
51
|
-
export {
|
|
50
|
+
// Collection
|
|
51
|
+
export { useCollection } from "./collection/use-collection";
|
|
52
52
|
export {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
} from "./collection
|
|
53
|
+
Collection,
|
|
54
|
+
CollectionProvider,
|
|
55
|
+
useCollectionContext,
|
|
56
|
+
} from "./collection/collection-provider";
|
|
57
57
|
|
|
58
58
|
// Table (static)
|
|
59
59
|
export { Table } from "./table";
|
|
@@ -17,7 +17,7 @@ import { OPERATORS_BY_FILTER_TYPE, DEFAULT_OPERATOR_LABELS } from "./types";
|
|
|
17
17
|
*
|
|
18
18
|
* Renders a dropdown panel with type-specific filter inputs, operator
|
|
19
19
|
* selectors, and active filter badges. Designed to receive props via
|
|
20
|
-
* spread from `useDataTable()` / `
|
|
20
|
+
* spread from `useDataTable()` / `useCollection()`.
|
|
21
21
|
*
|
|
22
22
|
* All text is customisable through the optional `labels` prop (defaults
|
|
23
23
|
* to English).
|
package/src/component/types.ts
CHANGED
|
@@ -172,7 +172,7 @@ export interface PageInfo {
|
|
|
172
172
|
* GraphQL query variables in Tailor Platform format.
|
|
173
173
|
*
|
|
174
174
|
* @typeParam TFieldName - Union of allowed field name strings (default: `string`).
|
|
175
|
-
* When metadata is provided to `
|
|
175
|
+
* When metadata is provided to `useCollection`, this
|
|
176
176
|
* narrows `order[].field` to match the table's field names,
|
|
177
177
|
* making the output directly compatible with gql-tada's
|
|
178
178
|
* `VariablesOf<>` types.
|
|
@@ -320,47 +320,62 @@ export type ColumnDefinition<TRow extends Record<string, unknown>> =
|
|
|
320
320
|
Column<TRow>;
|
|
321
321
|
|
|
322
322
|
// =============================================================================
|
|
323
|
-
//
|
|
323
|
+
// useCollection Types
|
|
324
324
|
// =============================================================================
|
|
325
325
|
|
|
326
326
|
/**
|
|
327
|
-
* Options for `
|
|
327
|
+
* Options for `useCollection` hook.
|
|
328
328
|
*
|
|
329
329
|
* @typeParam TFieldName - Union of allowed field name strings (default: `string`).
|
|
330
330
|
*/
|
|
331
|
-
export interface
|
|
331
|
+
export interface UseCollectionOptions<
|
|
332
332
|
TFieldName extends string = string,
|
|
333
333
|
TFilter extends Filter<TFieldName> = Filter<TFieldName>,
|
|
334
334
|
> {
|
|
335
|
-
/**
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
335
|
+
/** Collection parameters (pagination, sort, filters) */
|
|
336
|
+
params?: {
|
|
337
|
+
/** Initial filters to apply */
|
|
338
|
+
initialFilters?: TFilter[];
|
|
339
|
+
/** Initial sort states */
|
|
340
|
+
initialSort?: { field: TFieldName; direction: "Asc" | "Desc" }[];
|
|
341
|
+
/** Number of items per page (default: 20) */
|
|
342
|
+
pageSize?: number;
|
|
343
|
+
};
|
|
341
344
|
}
|
|
342
345
|
|
|
343
346
|
/**
|
|
344
|
-
* Return type of `
|
|
347
|
+
* Return type of `useCollection` hook.
|
|
345
348
|
*
|
|
346
349
|
* Methods that accept a field name are typed with `TFieldName` so that
|
|
347
350
|
* auto-completion works when a concrete union is supplied.
|
|
348
351
|
*
|
|
349
352
|
* **Note:** Methods use *method syntax* (not property syntax) intentionally
|
|
350
|
-
* so that `
|
|
351
|
-
* `
|
|
353
|
+
* so that `UseCollectionReturn<"a" | "b">` remains assignable to
|
|
354
|
+
* `UseCollectionReturn<string>` (bivariant method check).
|
|
352
355
|
*
|
|
353
356
|
* @typeParam TFieldName - Union of allowed field name strings (default: `string`).
|
|
354
357
|
* @typeParam TVariables - Type of the `variables` output (default: `QueryVariables<TFieldName>`).
|
|
355
358
|
* Pass `VariablesOf<typeof YOUR_QUERY>` (gql-tada) to get
|
|
356
359
|
* exact type compatibility with your GraphQL client.
|
|
360
|
+
* @typeParam TQueryArgs - Type returned by `toQueryArgs()`. Contains both
|
|
361
|
+
* `query` and `variables`.
|
|
357
362
|
*/
|
|
358
|
-
export interface
|
|
363
|
+
export interface UseCollectionReturn<
|
|
359
364
|
TFieldName extends string = string,
|
|
360
365
|
TVariables = QueryVariables<TFieldName>,
|
|
366
|
+
TQueryArgs = { query: unknown; variables: TVariables },
|
|
361
367
|
> {
|
|
362
|
-
/**
|
|
363
|
-
|
|
368
|
+
/**
|
|
369
|
+
* Returns query arguments (`{ query, variables }`) that can be spread
|
|
370
|
+
* directly into `useQuery()`.
|
|
371
|
+
*
|
|
372
|
+
* @example
|
|
373
|
+
* ```tsx
|
|
374
|
+
* const collection = useCollection({ query: GET_ORDERS, params: { pageSize: 20 } });
|
|
375
|
+
* const [result] = useQuery({ ...collection.toQueryArgs() });
|
|
376
|
+
* ```
|
|
377
|
+
*/
|
|
378
|
+
toQueryArgs(): TQueryArgs;
|
|
364
379
|
|
|
365
380
|
// Filter operations
|
|
366
381
|
/** Current active filters */
|
|
@@ -417,8 +432,8 @@ export interface UseDataTableOptions<TRow extends Record<string, unknown>> {
|
|
|
417
432
|
loading?: boolean;
|
|
418
433
|
/** Error */
|
|
419
434
|
error?: Error | null;
|
|
420
|
-
/** Collection
|
|
421
|
-
|
|
435
|
+
/** Collection state for sort/pagination integration */
|
|
436
|
+
collection?: UseCollectionReturn<string, unknown, unknown>;
|
|
422
437
|
}
|
|
423
438
|
|
|
424
439
|
/**
|
|
@@ -445,7 +460,7 @@ export interface DataTableRootProps<TRow extends Record<string, unknown>> {
|
|
|
445
460
|
loading?: boolean;
|
|
446
461
|
/** Error */
|
|
447
462
|
error?: Error | null;
|
|
448
|
-
/** Sort handler (connected to
|
|
463
|
+
/** Sort handler (connected to collection) */
|
|
449
464
|
onSort?: (field: string, direction?: "Asc" | "Desc") => void;
|
|
450
465
|
/** Current sort states */
|
|
451
466
|
sortStates?: SortState[];
|
|
@@ -498,7 +513,7 @@ export interface UseDataTableReturn<TRow extends Record<string, unknown>> {
|
|
|
498
513
|
/** Error */
|
|
499
514
|
error: Error | null;
|
|
500
515
|
|
|
501
|
-
// Pagination (delegated from
|
|
516
|
+
// Pagination (delegated from collection)
|
|
502
517
|
/** Page info from GraphQL response */
|
|
503
518
|
pageInfo: PageInfo;
|
|
504
519
|
/** Navigate to next page */
|
|
@@ -1,83 +0,0 @@
|
|
|
1
|
-
import { createContext, useContext, type ReactNode } from "react";
|
|
2
|
-
import type { UseCollectionParamsReturn } from "../types";
|
|
3
|
-
|
|
4
|
-
const CollectionParamsContext = createContext<UseCollectionParamsReturn<
|
|
5
|
-
string,
|
|
6
|
-
unknown
|
|
7
|
-
> | null>(null);
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Provider that shares collection query parameters via React Context.
|
|
11
|
-
*
|
|
12
|
-
* @example
|
|
13
|
-
* ```tsx
|
|
14
|
-
* const params = useCollectionParams({ pageSize: 20 });
|
|
15
|
-
*
|
|
16
|
-
* <CollectionParamsProvider value={params}>
|
|
17
|
-
* <FilterPanel />
|
|
18
|
-
* <DataTable.Root {...table.rootProps}>...</DataTable.Root>
|
|
19
|
-
* <Pagination {...table} />
|
|
20
|
-
* </CollectionParamsProvider>
|
|
21
|
-
* ```
|
|
22
|
-
*/
|
|
23
|
-
export function CollectionParamsProvider({
|
|
24
|
-
value,
|
|
25
|
-
children,
|
|
26
|
-
}: {
|
|
27
|
-
value: UseCollectionParamsReturn<string, unknown>;
|
|
28
|
-
children: ReactNode;
|
|
29
|
-
}) {
|
|
30
|
-
return (
|
|
31
|
-
<CollectionParamsContext.Provider value={value}>
|
|
32
|
-
{children}
|
|
33
|
-
</CollectionParamsContext.Provider>
|
|
34
|
-
);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* Hook to access collection params from the nearest `CollectionParams.Provider`.
|
|
39
|
-
*
|
|
40
|
-
* Returns the same interface as `useCollectionParams()`. Pass a `TFieldName`
|
|
41
|
-
* type parameter to narrow method arguments like `addFilter` / `setSort`.
|
|
42
|
-
*
|
|
43
|
-
* @typeParam TFieldName - Union of allowed field name strings (default: `string`).
|
|
44
|
-
*
|
|
45
|
-
* @throws Error if used outside of `CollectionParams.Provider`.
|
|
46
|
-
*
|
|
47
|
-
* @example
|
|
48
|
-
* ```tsx
|
|
49
|
-
* function StatusFilter() {
|
|
50
|
-
* const { filters, addFilter, removeFilter } = useCollectionParamsContext();
|
|
51
|
-
* // ...
|
|
52
|
-
* }
|
|
53
|
-
*
|
|
54
|
-
* // With typed field names:
|
|
55
|
-
* type TaskField = FieldName<typeof tableMetadata, "task">;
|
|
56
|
-
* const { addFilter } = useCollectionParamsContext<TaskField>();
|
|
57
|
-
* ```
|
|
58
|
-
*/
|
|
59
|
-
export function useCollectionParamsContext<
|
|
60
|
-
TFieldName extends string = string,
|
|
61
|
-
>(): UseCollectionParamsReturn<TFieldName> {
|
|
62
|
-
const ctx = useContext(CollectionParamsContext);
|
|
63
|
-
if (!ctx) {
|
|
64
|
-
throw new Error(
|
|
65
|
-
"useCollectionParamsContext must be used within <CollectionParams.Provider>",
|
|
66
|
-
);
|
|
67
|
-
}
|
|
68
|
-
return ctx as UseCollectionParamsReturn<TFieldName>;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
/**
|
|
72
|
-
* `CollectionParams` namespace object providing the Provider component.
|
|
73
|
-
*
|
|
74
|
-
* @example
|
|
75
|
-
* ```tsx
|
|
76
|
-
* <CollectionParams.Provider value={params}>
|
|
77
|
-
* ...
|
|
78
|
-
* </CollectionParams.Provider>
|
|
79
|
-
* ```
|
|
80
|
-
*/
|
|
81
|
-
export const CollectionParams = {
|
|
82
|
-
Provider: CollectionParamsProvider,
|
|
83
|
-
} as const;
|