@izumisy-tailor/tailor-data-viewer 0.2.33 → 0.3.0
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 +31 -18
- package/package.json +1 -1
- package/src/component/collection/collection-provider.tsx +16 -30
- package/src/component/collection/use-collection.test.ts +65 -76
- package/src/component/collection/use-collection.ts +67 -89
- package/src/component/collection/use-collection.typetest.ts +157 -293
- package/src/component/data-table/data-table.tsx +8 -6
- package/src/component/data-table/i18n.ts +12 -6
- package/src/component/data-table/pagination.tsx +3 -3
- package/src/component/data-table/search-filter-form.tsx +3 -3
- package/src/component/data-table/use-data-table.ts +6 -2
- package/src/component/index.ts +6 -9
- package/src/component/types.ts +145 -276
- package/src/tests/helpers.tsx +9 -5
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
|
+
- **`useCollectionVariables` Hook**: Manages filter, sort, and pagination state; outputs Tailor Platform-compatible GraphQL variables
|
|
9
|
+
- **`CollectionVariablesProvider`**: 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 `useCollectionVariables` can drive tables, kanbans, calendars, etc.
|
|
19
19
|
|
|
20
20
|
## Installation
|
|
21
21
|
|
|
@@ -47,7 +47,7 @@ npm install react react-dom
|
|
|
47
47
|
|
|
48
48
|
```tsx
|
|
49
49
|
import {
|
|
50
|
-
|
|
50
|
+
useCollectionVariables,
|
|
51
51
|
useDataTable,
|
|
52
52
|
DataTable,
|
|
53
53
|
Pagination,
|
|
@@ -95,8 +95,15 @@ const columns = [
|
|
|
95
95
|
|
|
96
96
|
// 2. Build a page
|
|
97
97
|
function OrdersPage() {
|
|
98
|
-
const
|
|
99
|
-
const [result] = useQuery({
|
|
98
|
+
const { variables } = useCollectionVariables({ params: { pageSize: 20 } });
|
|
99
|
+
const [result] = useQuery({
|
|
100
|
+
query: GET_ORDERS,
|
|
101
|
+
variables: {
|
|
102
|
+
...variables.pagination,
|
|
103
|
+
query: variables.query,
|
|
104
|
+
order: variables.order
|
|
105
|
+
}
|
|
106
|
+
});
|
|
100
107
|
|
|
101
108
|
const table = useDataTable<Order>({
|
|
102
109
|
columns,
|
|
@@ -119,21 +126,27 @@ function OrdersPage() {
|
|
|
119
126
|
|
|
120
127
|
## API Overview
|
|
121
128
|
|
|
122
|
-
### `
|
|
129
|
+
### `useCollectionVariables(options)`
|
|
123
130
|
|
|
124
|
-
Manages filter, sort, and pagination state. Returns `
|
|
131
|
+
Manages filter, sort, and pagination state. Returns `variables` containing `query`, `order`, and `pagination` sub-properties in Tailor Platform-compatible format.
|
|
125
132
|
|
|
126
133
|
```tsx
|
|
127
|
-
const collection =
|
|
128
|
-
query: GET_ORDERS,
|
|
134
|
+
const { variables, ...collection } = useCollectionVariables({
|
|
129
135
|
params: {
|
|
130
136
|
pageSize: 20,
|
|
131
137
|
initialSort: [{ field: "createdAt", direction: "Desc" }],
|
|
132
138
|
},
|
|
133
139
|
});
|
|
134
140
|
|
|
135
|
-
//
|
|
136
|
-
const [result] = useQuery({
|
|
141
|
+
// Use variables in your query
|
|
142
|
+
const [result] = useQuery({
|
|
143
|
+
query: GET_ORDERS,
|
|
144
|
+
variables: {
|
|
145
|
+
...variables.pagination,
|
|
146
|
+
query: variables.query,
|
|
147
|
+
order: variables.order,
|
|
148
|
+
},
|
|
149
|
+
});
|
|
137
150
|
|
|
138
151
|
// Filter operations
|
|
139
152
|
collection.addFilter("status", "eq", "ACTIVE");
|
|
@@ -157,15 +170,15 @@ collection.hasPrevPage; // boolean
|
|
|
157
170
|
collection.hasNextPage; // boolean
|
|
158
171
|
```
|
|
159
172
|
|
|
160
|
-
### `DataTable.Provider` / `useDataTableContext()` / `
|
|
173
|
+
### `DataTable.Provider` / `useDataTableContext()` / `useCollectionVariablesContext()`
|
|
161
174
|
|
|
162
175
|
`DataTable.Provider` wraps the table UI and provides both data table and collection context. All utility components (`Pagination`, `ColumnSelector`, `CsvButton`, `SearchFilterForm`) read from this context — no prop spreading needed.
|
|
163
176
|
|
|
164
|
-
When `collection` is passed to `useDataTable`, `DataTable.Provider` automatically wraps a `
|
|
177
|
+
When `collection` is passed to `useDataTable`, `DataTable.Provider` automatically wraps a `CollectionVariablesProvider` so child components can use `useCollectionVariablesContext()`.
|
|
165
178
|
|
|
166
179
|
```tsx
|
|
167
180
|
<DataTable.Provider value={table}>
|
|
168
|
-
<StatusFilter /> {/*
|
|
181
|
+
<StatusFilter /> {/* useCollectionVariablesContext() inside */}
|
|
169
182
|
<DataTable.Root>
|
|
170
183
|
<DataTable.Headers />
|
|
171
184
|
<DataTable.Body />
|
|
@@ -174,13 +187,13 @@ When `collection` is passed to `useDataTable`, `DataTable.Provider` automaticall
|
|
|
174
187
|
</DataTable.Provider>
|
|
175
188
|
```
|
|
176
189
|
|
|
177
|
-
For cases where you need `
|
|
190
|
+
For cases where you need `CollectionVariablesProvider` without `DataTable.Provider` (e.g., non-table UIs), you can use it standalone:
|
|
178
191
|
|
|
179
192
|
```tsx
|
|
180
|
-
<
|
|
193
|
+
<CollectionVariablesProvider value={collection}>
|
|
181
194
|
<StatusFilter />
|
|
182
195
|
<CustomKanbanBoard />
|
|
183
|
-
</
|
|
196
|
+
</CollectionVariablesProvider>
|
|
184
197
|
```
|
|
185
198
|
|
|
186
199
|
### Column Definition Helper
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createContext, useContext, type ReactNode } from "react";
|
|
2
2
|
import type { UseCollectionReturn } from "../types";
|
|
3
3
|
|
|
4
|
-
const
|
|
4
|
+
const CollectionVariablesContext = createContext<UseCollectionReturn<
|
|
5
5
|
string,
|
|
6
6
|
unknown
|
|
7
7
|
> | null>(null);
|
|
@@ -11,16 +11,16 @@ const CollectionContext = createContext<UseCollectionReturn<
|
|
|
11
11
|
*
|
|
12
12
|
* @example
|
|
13
13
|
* ```tsx
|
|
14
|
-
* const collection =
|
|
14
|
+
* const { variables, ...collection } = useCollectionVariables({ params: { pageSize: 20 } });
|
|
15
15
|
*
|
|
16
|
-
* <
|
|
16
|
+
* <CollectionVariablesProvider value={collection}>
|
|
17
17
|
* <FilterPanel />
|
|
18
18
|
* <DataTable.Root>...</DataTable.Root>
|
|
19
|
-
* <Pagination
|
|
20
|
-
* </
|
|
19
|
+
* <Pagination />
|
|
20
|
+
* </CollectionVariablesProvider>
|
|
21
21
|
* ```
|
|
22
22
|
*/
|
|
23
|
-
export function
|
|
23
|
+
export function CollectionVariablesProvider({
|
|
24
24
|
value,
|
|
25
25
|
children,
|
|
26
26
|
}: {
|
|
@@ -28,56 +28,42 @@ export function CollectionProvider({
|
|
|
28
28
|
children: ReactNode;
|
|
29
29
|
}) {
|
|
30
30
|
return (
|
|
31
|
-
<
|
|
31
|
+
<CollectionVariablesContext.Provider value={value}>
|
|
32
32
|
{children}
|
|
33
|
-
</
|
|
33
|
+
</CollectionVariablesContext.Provider>
|
|
34
34
|
);
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
/**
|
|
38
|
-
* Hook to access collection state from the nearest `
|
|
38
|
+
* Hook to access collection state from the nearest `CollectionVariablesProvider`.
|
|
39
39
|
*
|
|
40
|
-
* Returns the same interface as `
|
|
40
|
+
* Returns the same interface as `useCollectionVariables()`. Pass a `TFieldName`
|
|
41
41
|
* type parameter to narrow method arguments like `addFilter` / `setSort`.
|
|
42
42
|
*
|
|
43
43
|
* @typeParam TFieldName - Union of allowed field name strings (default: `string`).
|
|
44
44
|
*
|
|
45
|
-
* @throws Error if used outside of `
|
|
45
|
+
* @throws Error if used outside of `CollectionVariablesProvider`.
|
|
46
46
|
*
|
|
47
47
|
* @example
|
|
48
48
|
* ```tsx
|
|
49
49
|
* function StatusFilter() {
|
|
50
|
-
* const { filters, addFilter, removeFilter } =
|
|
50
|
+
* const { filters, addFilter, removeFilter } = useCollectionVariablesContext();
|
|
51
51
|
* // ...
|
|
52
52
|
* }
|
|
53
53
|
*
|
|
54
54
|
* // With typed field names:
|
|
55
55
|
* type TaskField = FieldName<typeof tableMetadata, "task">;
|
|
56
|
-
* const { addFilter } =
|
|
56
|
+
* const { addFilter } = useCollectionVariablesContext<TaskField>();
|
|
57
57
|
* ```
|
|
58
58
|
*/
|
|
59
|
-
export function
|
|
59
|
+
export function useCollectionVariablesContext<
|
|
60
60
|
TFieldName extends string = string,
|
|
61
61
|
>(): UseCollectionReturn<TFieldName> {
|
|
62
|
-
const ctx = useContext(
|
|
62
|
+
const ctx = useContext(CollectionVariablesContext);
|
|
63
63
|
if (!ctx) {
|
|
64
64
|
throw new Error(
|
|
65
|
-
"
|
|
65
|
+
"useCollectionVariablesContext must be used within <CollectionVariablesProvider>",
|
|
66
66
|
);
|
|
67
67
|
}
|
|
68
68
|
return ctx as UseCollectionReturn<TFieldName>;
|
|
69
69
|
}
|
|
70
|
-
|
|
71
|
-
/**
|
|
72
|
-
* `Collection` namespace object providing the Provider component.
|
|
73
|
-
*
|
|
74
|
-
* @example
|
|
75
|
-
* ```tsx
|
|
76
|
-
* <Collection.Provider value={collection}>
|
|
77
|
-
* ...
|
|
78
|
-
* </Collection.Provider>
|
|
79
|
-
* ```
|
|
80
|
-
*/
|
|
81
|
-
export const Collection = {
|
|
82
|
-
Provider: CollectionProvider,
|
|
83
|
-
} as const;
|
|
@@ -1,18 +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 { useCollectionVariables } from "./use-collection";
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
describe("useCollection", () => {
|
|
6
|
+
describe("useCollectionVariables", () => {
|
|
9
7
|
// ---------------------------------------------------------------------------
|
|
10
8
|
// Initial state
|
|
11
9
|
// ---------------------------------------------------------------------------
|
|
12
10
|
describe("initial state", () => {
|
|
13
11
|
it("returns default variables with pageSize 20", () => {
|
|
14
|
-
const { result } = renderHook(() =>
|
|
15
|
-
expect(result.current.
|
|
12
|
+
const { result } = renderHook(() => useCollectionVariables({}));
|
|
13
|
+
expect(result.current.variables.pagination).toEqual({ first: 20 });
|
|
14
|
+
expect(result.current.variables.query).toBeUndefined();
|
|
15
|
+
expect(result.current.variables.order).toBeUndefined();
|
|
16
16
|
expect(result.current.filters).toEqual([]);
|
|
17
17
|
expect(result.current.sortStates).toEqual([]);
|
|
18
18
|
expect(result.current.cursor).toBeNull();
|
|
@@ -21,15 +21,14 @@ describe("useCollection", () => {
|
|
|
21
21
|
|
|
22
22
|
it("uses custom pageSize", () => {
|
|
23
23
|
const { result } = renderHook(() =>
|
|
24
|
-
|
|
24
|
+
useCollectionVariables({ params: { pageSize: 50 } }),
|
|
25
25
|
);
|
|
26
|
-
expect(result.current.
|
|
26
|
+
expect(result.current.variables.pagination.first).toBe(50);
|
|
27
27
|
});
|
|
28
28
|
|
|
29
29
|
it("applies initial sort", () => {
|
|
30
30
|
const { result } = renderHook(() =>
|
|
31
|
-
|
|
32
|
-
query: FAKE_QUERY,
|
|
31
|
+
useCollectionVariables({
|
|
33
32
|
params: {
|
|
34
33
|
initialSort: [{ field: "createdAt", direction: "Desc" }],
|
|
35
34
|
},
|
|
@@ -38,15 +37,14 @@ describe("useCollection", () => {
|
|
|
38
37
|
expect(result.current.sortStates).toEqual([
|
|
39
38
|
{ field: "createdAt", direction: "Desc" },
|
|
40
39
|
]);
|
|
41
|
-
expect(result.current.
|
|
40
|
+
expect(result.current.variables.order).toEqual([
|
|
42
41
|
{ field: "createdAt", direction: "Desc" },
|
|
43
42
|
]);
|
|
44
43
|
});
|
|
45
44
|
|
|
46
45
|
it("applies initial filters", () => {
|
|
47
46
|
const { result } = renderHook(() =>
|
|
48
|
-
|
|
49
|
-
query: FAKE_QUERY,
|
|
47
|
+
useCollectionVariables({
|
|
50
48
|
params: {
|
|
51
49
|
initialFilters: [
|
|
52
50
|
{
|
|
@@ -59,7 +57,7 @@ describe("useCollection", () => {
|
|
|
59
57
|
}),
|
|
60
58
|
);
|
|
61
59
|
expect(result.current.filters).toHaveLength(1);
|
|
62
|
-
expect(result.current.
|
|
60
|
+
expect(result.current.variables.query).toEqual({
|
|
63
61
|
status: { eq: "ACTIVE" },
|
|
64
62
|
});
|
|
65
63
|
});
|
|
@@ -70,7 +68,7 @@ describe("useCollection", () => {
|
|
|
70
68
|
// ---------------------------------------------------------------------------
|
|
71
69
|
describe("filter operations", () => {
|
|
72
70
|
it("adds a filter", () => {
|
|
73
|
-
const { result } = renderHook(() =>
|
|
71
|
+
const { result } = renderHook(() => useCollectionVariables({}));
|
|
74
72
|
|
|
75
73
|
act(() => {
|
|
76
74
|
result.current.addFilter("status", "eq", "ACTIVE");
|
|
@@ -82,13 +80,13 @@ describe("useCollection", () => {
|
|
|
82
80
|
operator: "eq",
|
|
83
81
|
value: "ACTIVE",
|
|
84
82
|
});
|
|
85
|
-
expect(result.current.
|
|
83
|
+
expect(result.current.variables.query).toEqual({
|
|
86
84
|
status: { eq: "ACTIVE" },
|
|
87
85
|
});
|
|
88
86
|
});
|
|
89
87
|
|
|
90
88
|
it("replaces filter for same field", () => {
|
|
91
|
-
const { result } = renderHook(() =>
|
|
89
|
+
const { result } = renderHook(() => useCollectionVariables({}));
|
|
92
90
|
|
|
93
91
|
act(() => {
|
|
94
92
|
result.current.addFilter("status", "eq", "ACTIVE");
|
|
@@ -102,7 +100,7 @@ describe("useCollection", () => {
|
|
|
102
100
|
});
|
|
103
101
|
|
|
104
102
|
it("sets filters in bulk", () => {
|
|
105
|
-
const { result } = renderHook(() =>
|
|
103
|
+
const { result } = renderHook(() => useCollectionVariables({}));
|
|
106
104
|
|
|
107
105
|
act(() => {
|
|
108
106
|
result.current.setFilters([
|
|
@@ -120,14 +118,14 @@ describe("useCollection", () => {
|
|
|
120
118
|
});
|
|
121
119
|
|
|
122
120
|
expect(result.current.filters).toHaveLength(2);
|
|
123
|
-
expect(result.current.
|
|
121
|
+
expect(result.current.variables.query).toEqual({
|
|
124
122
|
status: { eq: "ACTIVE" },
|
|
125
123
|
amount: { gte: 1000 },
|
|
126
124
|
});
|
|
127
125
|
});
|
|
128
126
|
|
|
129
127
|
it("removes a filter", () => {
|
|
130
|
-
const { result } = renderHook(() =>
|
|
128
|
+
const { result } = renderHook(() => useCollectionVariables({}));
|
|
131
129
|
|
|
132
130
|
act(() => {
|
|
133
131
|
result.current.addFilter("status", "eq", "ACTIVE");
|
|
@@ -142,7 +140,7 @@ describe("useCollection", () => {
|
|
|
142
140
|
});
|
|
143
141
|
|
|
144
142
|
it("clears all filters", () => {
|
|
145
|
-
const { result } = renderHook(() =>
|
|
143
|
+
const { result } = renderHook(() => useCollectionVariables({}));
|
|
146
144
|
|
|
147
145
|
act(() => {
|
|
148
146
|
result.current.addFilter("status", "eq", "ACTIVE");
|
|
@@ -153,11 +151,11 @@ describe("useCollection", () => {
|
|
|
153
151
|
});
|
|
154
152
|
|
|
155
153
|
expect(result.current.filters).toHaveLength(0);
|
|
156
|
-
expect(result.current.
|
|
154
|
+
expect(result.current.variables.query).toBeUndefined();
|
|
157
155
|
});
|
|
158
156
|
|
|
159
157
|
it("resets pagination when filters change", () => {
|
|
160
|
-
const { result } = renderHook(() =>
|
|
158
|
+
const { result } = renderHook(() => useCollectionVariables({}));
|
|
161
159
|
|
|
162
160
|
// Navigate to next page
|
|
163
161
|
act(() => {
|
|
@@ -179,7 +177,7 @@ describe("useCollection", () => {
|
|
|
179
177
|
// ---------------------------------------------------------------------------
|
|
180
178
|
describe("sort operations", () => {
|
|
181
179
|
it("sets sort", () => {
|
|
182
|
-
const { result } = renderHook(() =>
|
|
180
|
+
const { result } = renderHook(() => useCollectionVariables({}));
|
|
183
181
|
|
|
184
182
|
act(() => {
|
|
185
183
|
result.current.setSort("createdAt", "Desc");
|
|
@@ -188,13 +186,13 @@ describe("useCollection", () => {
|
|
|
188
186
|
expect(result.current.sortStates).toEqual([
|
|
189
187
|
{ field: "createdAt", direction: "Desc" },
|
|
190
188
|
]);
|
|
191
|
-
expect(result.current.
|
|
189
|
+
expect(result.current.variables.order).toEqual([
|
|
192
190
|
{ field: "createdAt", direction: "Desc" },
|
|
193
191
|
]);
|
|
194
192
|
});
|
|
195
193
|
|
|
196
194
|
it("appends sort for different fields", () => {
|
|
197
|
-
const { result } = renderHook(() =>
|
|
195
|
+
const { result } = renderHook(() => useCollectionVariables({}));
|
|
198
196
|
|
|
199
197
|
act(() => {
|
|
200
198
|
result.current.setSort("createdAt", "Desc");
|
|
@@ -210,7 +208,7 @@ describe("useCollection", () => {
|
|
|
210
208
|
});
|
|
211
209
|
|
|
212
210
|
it("replaces direction for existing field", () => {
|
|
213
|
-
const { result } = renderHook(() =>
|
|
211
|
+
const { result } = renderHook(() => useCollectionVariables({}));
|
|
214
212
|
|
|
215
213
|
act(() => {
|
|
216
214
|
result.current.setSort("createdAt", "Desc");
|
|
@@ -229,7 +227,7 @@ describe("useCollection", () => {
|
|
|
229
227
|
});
|
|
230
228
|
|
|
231
229
|
it("removes sort when direction is undefined", () => {
|
|
232
|
-
const { result } = renderHook(() =>
|
|
230
|
+
const { result } = renderHook(() => useCollectionVariables({}));
|
|
233
231
|
|
|
234
232
|
act(() => {
|
|
235
233
|
result.current.setSort("createdAt", "Desc");
|
|
@@ -247,7 +245,7 @@ describe("useCollection", () => {
|
|
|
247
245
|
});
|
|
248
246
|
|
|
249
247
|
it("clears sort", () => {
|
|
250
|
-
const { result } = renderHook(() =>
|
|
248
|
+
const { result } = renderHook(() => useCollectionVariables({}));
|
|
251
249
|
|
|
252
250
|
act(() => {
|
|
253
251
|
result.current.setSort("createdAt", "Desc");
|
|
@@ -257,7 +255,7 @@ describe("useCollection", () => {
|
|
|
257
255
|
});
|
|
258
256
|
|
|
259
257
|
expect(result.current.sortStates).toEqual([]);
|
|
260
|
-
expect(result.current.
|
|
258
|
+
expect(result.current.variables.order).toBeUndefined();
|
|
261
259
|
});
|
|
262
260
|
});
|
|
263
261
|
|
|
@@ -266,7 +264,7 @@ describe("useCollection", () => {
|
|
|
266
264
|
// ---------------------------------------------------------------------------
|
|
267
265
|
describe("pagination operations", () => {
|
|
268
266
|
it("navigates to next page (forward)", () => {
|
|
269
|
-
const { result } = renderHook(() =>
|
|
267
|
+
const { result } = renderHook(() => useCollectionVariables({}));
|
|
270
268
|
|
|
271
269
|
act(() => {
|
|
272
270
|
result.current.nextPage("cursor1");
|
|
@@ -274,14 +272,14 @@ describe("useCollection", () => {
|
|
|
274
272
|
|
|
275
273
|
expect(result.current.cursor).toBe("cursor1");
|
|
276
274
|
expect(result.current.paginationDirection).toBe("forward");
|
|
277
|
-
expect(result.current.
|
|
278
|
-
expect(result.current.
|
|
279
|
-
expect(result.current.
|
|
280
|
-
expect(result.current.
|
|
275
|
+
expect(result.current.variables.pagination.after).toBe("cursor1");
|
|
276
|
+
expect(result.current.variables.pagination.first).toBe(20);
|
|
277
|
+
expect(result.current.variables.pagination.last).toBeUndefined();
|
|
278
|
+
expect(result.current.variables.pagination.before).toBeUndefined();
|
|
281
279
|
});
|
|
282
280
|
|
|
283
281
|
it("navigates to previous page (backward)", () => {
|
|
284
|
-
const { result } = renderHook(() =>
|
|
282
|
+
const { result } = renderHook(() => useCollectionVariables({}));
|
|
285
283
|
|
|
286
284
|
act(() => {
|
|
287
285
|
result.current.prevPage("cursor1");
|
|
@@ -289,14 +287,14 @@ describe("useCollection", () => {
|
|
|
289
287
|
|
|
290
288
|
expect(result.current.cursor).toBe("cursor1");
|
|
291
289
|
expect(result.current.paginationDirection).toBe("backward");
|
|
292
|
-
expect(result.current.
|
|
293
|
-
expect(result.current.
|
|
294
|
-
expect(result.current.
|
|
295
|
-
expect(result.current.
|
|
290
|
+
expect(result.current.variables.pagination.before).toBe("cursor1");
|
|
291
|
+
expect(result.current.variables.pagination.last).toBe(20);
|
|
292
|
+
expect(result.current.variables.pagination.first).toBeUndefined();
|
|
293
|
+
expect(result.current.variables.pagination.after).toBeUndefined();
|
|
296
294
|
});
|
|
297
295
|
|
|
298
296
|
it("switches direction on nextPage after prevPage", () => {
|
|
299
|
-
const { result } = renderHook(() =>
|
|
297
|
+
const { result } = renderHook(() => useCollectionVariables({}));
|
|
300
298
|
|
|
301
299
|
act(() => {
|
|
302
300
|
result.current.prevPage("cursorB");
|
|
@@ -307,12 +305,12 @@ describe("useCollection", () => {
|
|
|
307
305
|
result.current.nextPage("cursorA");
|
|
308
306
|
});
|
|
309
307
|
expect(result.current.paginationDirection).toBe("forward");
|
|
310
|
-
expect(result.current.
|
|
311
|
-
expect(result.current.
|
|
308
|
+
expect(result.current.variables.pagination.after).toBe("cursorA");
|
|
309
|
+
expect(result.current.variables.pagination.first).toBe(20);
|
|
312
310
|
});
|
|
313
311
|
|
|
314
312
|
it("resets page to forward direction", () => {
|
|
315
|
-
const { result } = renderHook(() =>
|
|
313
|
+
const { result } = renderHook(() => useCollectionVariables({}));
|
|
316
314
|
|
|
317
315
|
act(() => {
|
|
318
316
|
result.current.prevPage("cursor1");
|
|
@@ -326,7 +324,7 @@ describe("useCollection", () => {
|
|
|
326
324
|
});
|
|
327
325
|
|
|
328
326
|
it("tracks hasPrevPage from currentPage and hasNextPage from setPageInfo", () => {
|
|
329
|
-
const { result } = renderHook(() =>
|
|
327
|
+
const { result } = renderHook(() => useCollectionVariables({}));
|
|
330
328
|
|
|
331
329
|
// Initially on page 1: no prev, no next
|
|
332
330
|
expect(result.current.hasPrevPage).toBe(false);
|
|
@@ -354,13 +352,12 @@ describe("useCollection", () => {
|
|
|
354
352
|
});
|
|
355
353
|
|
|
356
354
|
// ---------------------------------------------------------------------------
|
|
357
|
-
//
|
|
355
|
+
// variables
|
|
358
356
|
// ---------------------------------------------------------------------------
|
|
359
|
-
describe("
|
|
357
|
+
describe("variables", () => {
|
|
360
358
|
it("generates complete variables with filters, sort, and cursor", () => {
|
|
361
359
|
const { result } = renderHook(() =>
|
|
362
|
-
|
|
363
|
-
query: FAKE_QUERY,
|
|
360
|
+
useCollectionVariables({
|
|
364
361
|
params: {
|
|
365
362
|
pageSize: 10,
|
|
366
363
|
initialFilters: [
|
|
@@ -379,39 +376,33 @@ describe("useCollection", () => {
|
|
|
379
376
|
result.current.nextPage("abc123");
|
|
380
377
|
});
|
|
381
378
|
|
|
382
|
-
expect(result.current.
|
|
383
|
-
query:
|
|
384
|
-
|
|
379
|
+
expect(result.current.variables).toEqual({
|
|
380
|
+
query: { status: { eq: "ACTIVE" } },
|
|
381
|
+
order: [{ field: "createdAt", direction: "Desc" }],
|
|
382
|
+
pagination: {
|
|
385
383
|
first: 10,
|
|
386
|
-
query: { status: { eq: "ACTIVE" } },
|
|
387
|
-
order: [{ field: "createdAt", direction: "Desc" }],
|
|
388
384
|
after: "abc123",
|
|
389
385
|
},
|
|
390
386
|
});
|
|
391
387
|
});
|
|
392
388
|
|
|
393
|
-
it("omits undefined fields from
|
|
394
|
-
const { result } = renderHook(() =>
|
|
395
|
-
const {
|
|
396
|
-
expect(
|
|
397
|
-
expect("
|
|
398
|
-
expect("
|
|
399
|
-
expect("
|
|
400
|
-
expect("last" in variables).toBe(false);
|
|
401
|
-
expect("before" in variables).toBe(false);
|
|
389
|
+
it("omits undefined fields from pagination", () => {
|
|
390
|
+
const { result } = renderHook(() => useCollectionVariables({}));
|
|
391
|
+
const { pagination } = result.current.variables;
|
|
392
|
+
expect(pagination).toEqual({ first: 20 });
|
|
393
|
+
expect("after" in pagination).toBe(false);
|
|
394
|
+
expect("last" in pagination).toBe(false);
|
|
395
|
+
expect("before" in pagination).toBe(false);
|
|
402
396
|
});
|
|
403
397
|
|
|
404
|
-
it("
|
|
405
|
-
const fakeQuery = { kind: "Document" } as const;
|
|
398
|
+
it("returns undefined for query and order when empty", () => {
|
|
406
399
|
const { result } = renderHook(() =>
|
|
407
|
-
|
|
400
|
+
useCollectionVariables({ params: { pageSize: 10 } }),
|
|
408
401
|
);
|
|
409
402
|
|
|
410
|
-
|
|
411
|
-
expect(
|
|
412
|
-
|
|
413
|
-
variables: { first: 10 },
|
|
414
|
-
});
|
|
403
|
+
expect(result.current.variables.query).toBeUndefined();
|
|
404
|
+
expect(result.current.variables.order).toBeUndefined();
|
|
405
|
+
expect(result.current.variables.pagination).toEqual({ first: 10 });
|
|
415
406
|
});
|
|
416
407
|
});
|
|
417
408
|
|
|
@@ -441,20 +432,18 @@ describe("useCollection", () => {
|
|
|
441
432
|
|
|
442
433
|
it("works with tableMetadata", () => {
|
|
443
434
|
const { result } = renderHook(() =>
|
|
444
|
-
|
|
435
|
+
useCollectionVariables({
|
|
445
436
|
tableMetadata: testMetadata.task,
|
|
446
|
-
query: FAKE_QUERY,
|
|
447
437
|
params: { pageSize: 10 },
|
|
448
438
|
}),
|
|
449
439
|
);
|
|
450
|
-
expect(result.current.
|
|
440
|
+
expect(result.current.variables.pagination).toEqual({ first: 10 });
|
|
451
441
|
});
|
|
452
442
|
|
|
453
443
|
it("applies typed initialSort", () => {
|
|
454
444
|
const { result } = renderHook(() =>
|
|
455
|
-
|
|
445
|
+
useCollectionVariables({
|
|
456
446
|
tableMetadata: testMetadata.task,
|
|
457
|
-
query: FAKE_QUERY,
|
|
458
447
|
params: {
|
|
459
448
|
initialSort: [{ field: "dueDate", direction: "Desc" }],
|
|
460
449
|
},
|