@izumisy-tailor/tailor-data-viewer 0.2.25 → 0.2.27
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/component/collection/use-collection.test.ts +11 -3
- package/src/component/collection/use-collection.ts +40 -2
- package/src/component/data-table/use-data-table.ts +9 -1
- package/src/component/index.ts +1 -0
- package/src/component/pagination.test.tsx +97 -13
- package/src/component/pagination.tsx +148 -11
- package/src/component/table.tsx +1 -1
- package/src/component/types.ts +13 -2
- package/src/tests/helpers.tsx +5 -0
package/package.json
CHANGED
|
@@ -325,22 +325,30 @@ describe("useCollection", () => {
|
|
|
325
325
|
expect(result.current.paginationDirection).toBe("forward");
|
|
326
326
|
});
|
|
327
327
|
|
|
328
|
-
it("tracks hasPrevPage and hasNextPage from setPageInfo", () => {
|
|
328
|
+
it("tracks hasPrevPage from currentPage and hasNextPage from setPageInfo", () => {
|
|
329
329
|
const { result } = renderHook(() => useCollection({ query: FAKE_QUERY }));
|
|
330
330
|
|
|
331
|
+
// Initially on page 1: no prev, no next
|
|
331
332
|
expect(result.current.hasPrevPage).toBe(false);
|
|
332
333
|
expect(result.current.hasNextPage).toBe(false);
|
|
333
334
|
|
|
335
|
+
// After navigating to next page, hasPrevPage becomes true
|
|
336
|
+
act(() => {
|
|
337
|
+
result.current.nextPage("cursor-end");
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
expect(result.current.hasPrevPage).toBe(true);
|
|
341
|
+
|
|
342
|
+
// hasNextPage comes from setPageInfo when totalPages is null
|
|
334
343
|
act(() => {
|
|
335
344
|
result.current.setPageInfo({
|
|
336
345
|
hasNextPage: true,
|
|
337
346
|
endCursor: "end",
|
|
338
|
-
hasPreviousPage:
|
|
347
|
+
hasPreviousPage: false,
|
|
339
348
|
startCursor: "start",
|
|
340
349
|
});
|
|
341
350
|
});
|
|
342
351
|
|
|
343
|
-
expect(result.current.hasPrevPage).toBe(true);
|
|
344
352
|
expect(result.current.hasNextPage).toBe(true);
|
|
345
353
|
});
|
|
346
354
|
});
|
|
@@ -132,6 +132,8 @@ export function useCollection(
|
|
|
132
132
|
hasPreviousPage: false,
|
|
133
133
|
startCursor: null,
|
|
134
134
|
});
|
|
135
|
+
const [currentPage, setCurrentPage] = useState(1);
|
|
136
|
+
const [total, setTotalState] = useState<number | null>(null);
|
|
135
137
|
|
|
136
138
|
// ---------------------------------------------------------------------------
|
|
137
139
|
// Filter operations
|
|
@@ -155,6 +157,7 @@ export function useCollection(
|
|
|
155
157
|
// Reset pagination when filters change
|
|
156
158
|
setCursor(null);
|
|
157
159
|
setPaginationDirection("forward");
|
|
160
|
+
setCurrentPage(1);
|
|
158
161
|
},
|
|
159
162
|
[],
|
|
160
163
|
);
|
|
@@ -164,6 +167,7 @@ export function useCollection(
|
|
|
164
167
|
// Reset pagination when filters change
|
|
165
168
|
setCursor(null);
|
|
166
169
|
setPaginationDirection("forward");
|
|
170
|
+
setCurrentPage(1);
|
|
167
171
|
}, []);
|
|
168
172
|
|
|
169
173
|
const removeFilter = useCallback((field: string) => {
|
|
@@ -171,12 +175,14 @@ export function useCollection(
|
|
|
171
175
|
// Reset pagination when filters change
|
|
172
176
|
setCursor(null);
|
|
173
177
|
setPaginationDirection("forward");
|
|
178
|
+
setCurrentPage(1);
|
|
174
179
|
}, []);
|
|
175
180
|
|
|
176
181
|
const clearFilters = useCallback(() => {
|
|
177
182
|
setFiltersState([]);
|
|
178
183
|
setCursor(null);
|
|
179
184
|
setPaginationDirection("forward");
|
|
185
|
+
setCurrentPage(1);
|
|
180
186
|
}, []);
|
|
181
187
|
|
|
182
188
|
// ---------------------------------------------------------------------------
|
|
@@ -196,12 +202,14 @@ export function useCollection(
|
|
|
196
202
|
// Reset pagination when sort changes
|
|
197
203
|
setCursor(null);
|
|
198
204
|
setPaginationDirection("forward");
|
|
205
|
+
setCurrentPage(1);
|
|
199
206
|
}, []);
|
|
200
207
|
|
|
201
208
|
const clearSort = useCallback(() => {
|
|
202
209
|
setSortStates([]);
|
|
203
210
|
setCursor(null);
|
|
204
211
|
setPaginationDirection("forward");
|
|
212
|
+
setCurrentPage(1);
|
|
205
213
|
}, []);
|
|
206
214
|
|
|
207
215
|
// ---------------------------------------------------------------------------
|
|
@@ -210,24 +218,49 @@ export function useCollection(
|
|
|
210
218
|
const nextPage = useCallback((endCursor: string) => {
|
|
211
219
|
setCursor(endCursor);
|
|
212
220
|
setPaginationDirection("forward");
|
|
221
|
+
setCurrentPage((p) => p + 1);
|
|
213
222
|
}, []);
|
|
214
223
|
|
|
215
224
|
const prevPage = useCallback((startCursor: string) => {
|
|
216
225
|
setCursor(startCursor);
|
|
217
226
|
setPaginationDirection("backward");
|
|
227
|
+
setCurrentPage((p) => Math.max(1, p - 1));
|
|
218
228
|
}, []);
|
|
219
229
|
|
|
220
230
|
const resetPage = useCallback(() => {
|
|
221
231
|
setCursor(null);
|
|
222
232
|
setPaginationDirection("forward");
|
|
233
|
+
setCurrentPage(1);
|
|
223
234
|
}, []);
|
|
224
235
|
|
|
225
236
|
const setPageInfo = useCallback((pageInfo: PageInfo) => {
|
|
226
237
|
setCurrentPageInfo(pageInfo);
|
|
227
238
|
}, []);
|
|
228
239
|
|
|
229
|
-
const
|
|
230
|
-
|
|
240
|
+
const setTotal = useCallback((value: number) => {
|
|
241
|
+
setTotalState(value);
|
|
242
|
+
}, []);
|
|
243
|
+
|
|
244
|
+
const totalPages =
|
|
245
|
+
total !== null ? Math.max(1, Math.ceil(total / pageSize)) : null;
|
|
246
|
+
|
|
247
|
+
const goToFirstPage = useCallback(() => {
|
|
248
|
+
setCursor(null);
|
|
249
|
+
setPaginationDirection("forward");
|
|
250
|
+
setCurrentPage(1);
|
|
251
|
+
}, []);
|
|
252
|
+
|
|
253
|
+
const goToLastPage = useCallback(() => {
|
|
254
|
+
setCursor(null);
|
|
255
|
+
setPaginationDirection("backward");
|
|
256
|
+
setCurrentPage(totalPages ?? 1);
|
|
257
|
+
}, [totalPages]);
|
|
258
|
+
|
|
259
|
+
const hasPrevPage = currentPage > 1;
|
|
260
|
+
const hasNextPage =
|
|
261
|
+
totalPages !== null
|
|
262
|
+
? currentPage < totalPages
|
|
263
|
+
: currentPageInfo.hasNextPage;
|
|
231
264
|
|
|
232
265
|
// ---------------------------------------------------------------------------
|
|
233
266
|
// Build query variables (Tailor Platform format)
|
|
@@ -299,5 +332,10 @@ export function useCollection(
|
|
|
299
332
|
hasPrevPage,
|
|
300
333
|
hasNextPage,
|
|
301
334
|
setPageInfo,
|
|
335
|
+
currentPage,
|
|
336
|
+
totalPages,
|
|
337
|
+
goToFirstPage,
|
|
338
|
+
goToLastPage,
|
|
339
|
+
setTotal,
|
|
302
340
|
};
|
|
303
341
|
}
|
|
@@ -76,7 +76,15 @@ export function useDataTable<TRow extends Record<string, unknown>>(
|
|
|
76
76
|
if (data?.pageInfo) {
|
|
77
77
|
collection?.setPageInfo(data.pageInfo);
|
|
78
78
|
}
|
|
79
|
-
|
|
79
|
+
if (data?.total != null) {
|
|
80
|
+
collection?.setTotal(data.total);
|
|
81
|
+
}
|
|
82
|
+
}, [
|
|
83
|
+
data?.pageInfo,
|
|
84
|
+
data?.total,
|
|
85
|
+
collection?.setPageInfo,
|
|
86
|
+
collection?.setTotal,
|
|
87
|
+
]);
|
|
80
88
|
|
|
81
89
|
// ---------------------------------------------------------------------------
|
|
82
90
|
// Column visibility management
|
package/src/component/index.ts
CHANGED
|
@@ -69,6 +69,7 @@ export { createColumnHelper } from "./field-helpers";
|
|
|
69
69
|
|
|
70
70
|
// Utility components
|
|
71
71
|
export { Pagination } from "./pagination";
|
|
72
|
+
export type { PaginationProps, PaginationLabels } from "./pagination";
|
|
72
73
|
export { ColumnSelector } from "./column-selector";
|
|
73
74
|
export { CsvButton } from "./csv-button";
|
|
74
75
|
export { SearchFilterForm } from "./search-filter-form";
|
|
@@ -34,6 +34,8 @@ const TestProviders = createTestProviders({
|
|
|
34
34
|
collectionDefaults: {
|
|
35
35
|
hasPrevPage: true,
|
|
36
36
|
hasNextPage: true,
|
|
37
|
+
currentPage: 2,
|
|
38
|
+
totalPages: 5,
|
|
37
39
|
},
|
|
38
40
|
});
|
|
39
41
|
|
|
@@ -42,37 +44,75 @@ const TestProviders = createTestProviders({
|
|
|
42
44
|
// =============================================================================
|
|
43
45
|
|
|
44
46
|
describe("Pagination", () => {
|
|
45
|
-
it("renders
|
|
47
|
+
it("renders four navigation buttons", () => {
|
|
46
48
|
render(
|
|
47
49
|
<TestProviders>
|
|
48
50
|
<Pagination />
|
|
49
51
|
</TestProviders>,
|
|
50
52
|
);
|
|
51
53
|
|
|
52
|
-
expect(screen.
|
|
53
|
-
expect(screen.
|
|
54
|
+
expect(screen.getByLabelText("First page")).toBeInTheDocument();
|
|
55
|
+
expect(screen.getByLabelText("Previous page")).toBeInTheDocument();
|
|
56
|
+
expect(screen.getByLabelText("Next page")).toBeInTheDocument();
|
|
57
|
+
expect(screen.getByLabelText("Last page")).toBeInTheDocument();
|
|
54
58
|
});
|
|
55
59
|
|
|
56
|
-
it("
|
|
60
|
+
it("displays current page and total pages as N / M", () => {
|
|
57
61
|
render(
|
|
58
|
-
<TestProviders
|
|
62
|
+
<TestProviders>
|
|
63
|
+
<Pagination />
|
|
64
|
+
</TestProviders>,
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
expect(screen.getByText("2 / 5")).toBeInTheDocument();
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("hides page indicator when totalPages is null", () => {
|
|
71
|
+
render(
|
|
72
|
+
<TestProviders collection={{ totalPages: null, currentPage: 3 }}>
|
|
73
|
+
<Pagination />
|
|
74
|
+
</TestProviders>,
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
expect(screen.queryByText("3")).not.toBeInTheDocument();
|
|
78
|
+
expect(screen.queryByText("/")).not.toBeInTheDocument();
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("disables First and Previous when hasPrevPage is false", () => {
|
|
82
|
+
render(
|
|
83
|
+
<TestProviders collection={{ hasPrevPage: false, currentPage: 1 }}>
|
|
59
84
|
<Pagination />
|
|
60
85
|
</TestProviders>,
|
|
61
86
|
);
|
|
62
87
|
|
|
63
|
-
expect(screen.
|
|
64
|
-
expect(screen.
|
|
88
|
+
expect(screen.getByLabelText("First page")).toBeDisabled();
|
|
89
|
+
expect(screen.getByLabelText("Previous page")).toBeDisabled();
|
|
90
|
+
expect(screen.getByLabelText("Next page")).toBeEnabled();
|
|
91
|
+
expect(screen.getByLabelText("Last page")).toBeEnabled();
|
|
65
92
|
});
|
|
66
93
|
|
|
67
|
-
it("disables Next when hasNextPage is false", () => {
|
|
94
|
+
it("disables Next and Last when hasNextPage is false", () => {
|
|
68
95
|
render(
|
|
69
96
|
<TestProviders collection={{ hasNextPage: false }}>
|
|
70
97
|
<Pagination />
|
|
71
98
|
</TestProviders>,
|
|
72
99
|
);
|
|
73
100
|
|
|
74
|
-
expect(screen.
|
|
75
|
-
expect(screen.
|
|
101
|
+
expect(screen.getByLabelText("First page")).toBeEnabled();
|
|
102
|
+
expect(screen.getByLabelText("Previous page")).toBeEnabled();
|
|
103
|
+
expect(screen.getByLabelText("Next page")).toBeDisabled();
|
|
104
|
+
expect(screen.getByLabelText("Last page")).toBeDisabled();
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("disables Last when totalPages is null even if hasNextPage", () => {
|
|
108
|
+
render(
|
|
109
|
+
<TestProviders collection={{ hasNextPage: true, totalPages: null }}>
|
|
110
|
+
<Pagination />
|
|
111
|
+
</TestProviders>,
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
expect(screen.getByLabelText("Next page")).toBeEnabled();
|
|
115
|
+
expect(screen.getByLabelText("Last page")).toBeDisabled();
|
|
76
116
|
});
|
|
77
117
|
|
|
78
118
|
it("calls nextPage with endCursor when Next is clicked", () => {
|
|
@@ -83,7 +123,7 @@ describe("Pagination", () => {
|
|
|
83
123
|
</TestProviders>,
|
|
84
124
|
);
|
|
85
125
|
|
|
86
|
-
fireEvent.click(screen.
|
|
126
|
+
fireEvent.click(screen.getByLabelText("Next page"));
|
|
87
127
|
expect(nextPage).toHaveBeenCalledWith("cursor-end");
|
|
88
128
|
});
|
|
89
129
|
|
|
@@ -95,10 +135,34 @@ describe("Pagination", () => {
|
|
|
95
135
|
</TestProviders>,
|
|
96
136
|
);
|
|
97
137
|
|
|
98
|
-
fireEvent.click(screen.
|
|
138
|
+
fireEvent.click(screen.getByLabelText("Previous page"));
|
|
99
139
|
expect(prevPage).toHaveBeenCalledWith("cursor-start");
|
|
100
140
|
});
|
|
101
141
|
|
|
142
|
+
it("calls goToFirstPage when First is clicked", () => {
|
|
143
|
+
const goToFirstPage = vi.fn();
|
|
144
|
+
render(
|
|
145
|
+
<TestProviders collection={{ goToFirstPage }}>
|
|
146
|
+
<Pagination />
|
|
147
|
+
</TestProviders>,
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
fireEvent.click(screen.getByLabelText("First page"));
|
|
151
|
+
expect(goToFirstPage).toHaveBeenCalledOnce();
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it("calls goToLastPage when Last is clicked", () => {
|
|
155
|
+
const goToLastPage = vi.fn();
|
|
156
|
+
render(
|
|
157
|
+
<TestProviders collection={{ goToLastPage }}>
|
|
158
|
+
<Pagination />
|
|
159
|
+
</TestProviders>,
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
fireEvent.click(screen.getByLabelText("Last page"));
|
|
163
|
+
expect(goToLastPage).toHaveBeenCalledOnce();
|
|
164
|
+
});
|
|
165
|
+
|
|
102
166
|
it("does not call nextPage when endCursor is null", () => {
|
|
103
167
|
const nextPage = vi.fn();
|
|
104
168
|
render(
|
|
@@ -117,10 +181,30 @@ describe("Pagination", () => {
|
|
|
117
181
|
</TestProviders>,
|
|
118
182
|
);
|
|
119
183
|
|
|
120
|
-
fireEvent.click(screen.
|
|
184
|
+
fireEvent.click(screen.getByLabelText("Next page"));
|
|
121
185
|
expect(nextPage).not.toHaveBeenCalled();
|
|
122
186
|
});
|
|
123
187
|
|
|
188
|
+
it("supports custom aria-labels via labels prop", () => {
|
|
189
|
+
render(
|
|
190
|
+
<TestProviders>
|
|
191
|
+
<Pagination
|
|
192
|
+
labels={{
|
|
193
|
+
first: "最初のページ",
|
|
194
|
+
previous: "前のページ",
|
|
195
|
+
next: "次のページ",
|
|
196
|
+
last: "最後のページ",
|
|
197
|
+
}}
|
|
198
|
+
/>
|
|
199
|
+
</TestProviders>,
|
|
200
|
+
);
|
|
201
|
+
|
|
202
|
+
expect(screen.getByLabelText("最初のページ")).toBeInTheDocument();
|
|
203
|
+
expect(screen.getByLabelText("前のページ")).toBeInTheDocument();
|
|
204
|
+
expect(screen.getByLabelText("次のページ")).toBeInTheDocument();
|
|
205
|
+
expect(screen.getByLabelText("最後のページ")).toBeInTheDocument();
|
|
206
|
+
});
|
|
207
|
+
|
|
124
208
|
it("throws when rendered outside provider", () => {
|
|
125
209
|
console.error = vi.fn();
|
|
126
210
|
expect(() => render(<Pagination />)).toThrow();
|
|
@@ -1,49 +1,186 @@
|
|
|
1
1
|
import { useDataTableContext } from "./data-table/data-table-context";
|
|
2
2
|
import { useCollectionContext } from "./collection/collection-provider";
|
|
3
3
|
|
|
4
|
+
// =============================================================================
|
|
5
|
+
// Inline SVG Icons (lucide-style, no external dependency)
|
|
6
|
+
// =============================================================================
|
|
7
|
+
|
|
8
|
+
const iconClass = "size-4";
|
|
9
|
+
|
|
10
|
+
function ChevronsLeftIcon() {
|
|
11
|
+
return (
|
|
12
|
+
<svg
|
|
13
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
14
|
+
viewBox="0 0 24 24"
|
|
15
|
+
fill="none"
|
|
16
|
+
stroke="currentColor"
|
|
17
|
+
strokeWidth={2}
|
|
18
|
+
strokeLinecap="round"
|
|
19
|
+
strokeLinejoin="round"
|
|
20
|
+
className={iconClass}
|
|
21
|
+
>
|
|
22
|
+
<path d="m11 17-5-5 5-5" />
|
|
23
|
+
<path d="m18 17-5-5 5-5" />
|
|
24
|
+
</svg>
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function ChevronLeftIcon() {
|
|
29
|
+
return (
|
|
30
|
+
<svg
|
|
31
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
32
|
+
viewBox="0 0 24 24"
|
|
33
|
+
fill="none"
|
|
34
|
+
stroke="currentColor"
|
|
35
|
+
strokeWidth={2}
|
|
36
|
+
strokeLinecap="round"
|
|
37
|
+
strokeLinejoin="round"
|
|
38
|
+
className={iconClass}
|
|
39
|
+
>
|
|
40
|
+
<path d="m15 18-6-6 6-6" />
|
|
41
|
+
</svg>
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function ChevronRightIcon() {
|
|
46
|
+
return (
|
|
47
|
+
<svg
|
|
48
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
49
|
+
viewBox="0 0 24 24"
|
|
50
|
+
fill="none"
|
|
51
|
+
stroke="currentColor"
|
|
52
|
+
strokeWidth={2}
|
|
53
|
+
strokeLinecap="round"
|
|
54
|
+
strokeLinejoin="round"
|
|
55
|
+
className={iconClass}
|
|
56
|
+
>
|
|
57
|
+
<path d="m9 18 6-6-6-6" />
|
|
58
|
+
</svg>
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function ChevronsRightIcon() {
|
|
63
|
+
return (
|
|
64
|
+
<svg
|
|
65
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
66
|
+
viewBox="0 0 24 24"
|
|
67
|
+
fill="none"
|
|
68
|
+
stroke="currentColor"
|
|
69
|
+
strokeWidth={2}
|
|
70
|
+
strokeLinecap="round"
|
|
71
|
+
strokeLinejoin="round"
|
|
72
|
+
className={iconClass}
|
|
73
|
+
>
|
|
74
|
+
<path d="m13 17 5-5-5-5" />
|
|
75
|
+
<path d="m6 17 5-5-5-5" />
|
|
76
|
+
</svg>
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// =============================================================================
|
|
81
|
+
// Pagination Component
|
|
82
|
+
// =============================================================================
|
|
83
|
+
|
|
84
|
+
export interface PaginationLabels {
|
|
85
|
+
first?: string;
|
|
86
|
+
previous?: string;
|
|
87
|
+
next?: string;
|
|
88
|
+
last?: string;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export interface PaginationProps {
|
|
92
|
+
/** Aria-labels for pagination buttons (defaults to English). */
|
|
93
|
+
labels?: PaginationLabels;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const btnClass =
|
|
97
|
+
"inline-flex items-center justify-center rounded-md border p-1.5 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground disabled:pointer-events-none disabled:opacity-50";
|
|
98
|
+
|
|
4
99
|
/**
|
|
5
|
-
* Pagination controls
|
|
100
|
+
* Pagination controls with first/prev/next/last navigation and page indicator.
|
|
6
101
|
*
|
|
7
|
-
* Reads
|
|
8
|
-
*
|
|
102
|
+
* Reads pagination state from `DataTableContext` and `CollectionContext`.
|
|
103
|
+
* Must be rendered inside `DataTable.Provider` but **outside** `DataTable.Root`.
|
|
9
104
|
*
|
|
10
105
|
* @example
|
|
11
106
|
* ```tsx
|
|
12
107
|
* <DataTable.Provider value={table}>
|
|
13
|
-
* <
|
|
108
|
+
* <DataTable.Root>
|
|
109
|
+
* <DataTable.Headers />
|
|
110
|
+
* <DataTable.Body />
|
|
111
|
+
* </DataTable.Root>
|
|
112
|
+
* <Pagination labels={{ first: "最初のページ", previous: "前のページ", next: "次のページ", last: "最後のページ" }} />
|
|
14
113
|
* </DataTable.Provider>
|
|
15
114
|
* ```
|
|
16
115
|
*/
|
|
17
|
-
export function Pagination() {
|
|
116
|
+
export function Pagination({ labels }: PaginationProps = {}) {
|
|
18
117
|
const { pageInfo } = useDataTableContext();
|
|
19
|
-
const {
|
|
20
|
-
|
|
118
|
+
const {
|
|
119
|
+
nextPage,
|
|
120
|
+
prevPage,
|
|
121
|
+
hasPrevPage,
|
|
122
|
+
hasNextPage,
|
|
123
|
+
currentPage,
|
|
124
|
+
totalPages,
|
|
125
|
+
goToFirstPage,
|
|
126
|
+
goToLastPage,
|
|
127
|
+
} = useCollectionContext();
|
|
128
|
+
|
|
129
|
+
const firstLabel = labels?.first ?? "First page";
|
|
130
|
+
const previousLabel = labels?.previous ?? "Previous page";
|
|
131
|
+
const nextLabel = labels?.next ?? "Next page";
|
|
132
|
+
const lastLabel = labels?.last ?? "Last page";
|
|
21
133
|
|
|
22
134
|
return (
|
|
23
135
|
<div className="flex items-center justify-end gap-2 py-2">
|
|
136
|
+
{totalPages !== null && (
|
|
137
|
+
<span className="text-sm text-muted-foreground tabular-nums">
|
|
138
|
+
{currentPage} / {totalPages}
|
|
139
|
+
</span>
|
|
140
|
+
)}
|
|
24
141
|
<button
|
|
25
142
|
type="button"
|
|
26
|
-
className=
|
|
143
|
+
className={btnClass}
|
|
144
|
+
onClick={goToFirstPage}
|
|
145
|
+
disabled={!hasPrevPage}
|
|
146
|
+
aria-label={firstLabel}
|
|
147
|
+
>
|
|
148
|
+
<ChevronsLeftIcon />
|
|
149
|
+
</button>
|
|
150
|
+
<button
|
|
151
|
+
type="button"
|
|
152
|
+
className={btnClass}
|
|
27
153
|
onClick={() => {
|
|
28
154
|
if (pageInfo.startCursor) {
|
|
29
155
|
prevPage(pageInfo.startCursor);
|
|
30
156
|
}
|
|
31
157
|
}}
|
|
32
158
|
disabled={!hasPrevPage}
|
|
159
|
+
aria-label={previousLabel}
|
|
33
160
|
>
|
|
34
|
-
|
|
161
|
+
<ChevronLeftIcon />
|
|
35
162
|
</button>
|
|
36
163
|
<button
|
|
37
164
|
type="button"
|
|
38
|
-
className=
|
|
165
|
+
className={btnClass}
|
|
39
166
|
onClick={() => {
|
|
40
167
|
if (pageInfo.endCursor) {
|
|
41
168
|
nextPage(pageInfo.endCursor);
|
|
42
169
|
}
|
|
43
170
|
}}
|
|
44
171
|
disabled={!hasNextPage}
|
|
172
|
+
aria-label={nextLabel}
|
|
173
|
+
>
|
|
174
|
+
<ChevronRightIcon />
|
|
175
|
+
</button>
|
|
176
|
+
<button
|
|
177
|
+
type="button"
|
|
178
|
+
className={btnClass}
|
|
179
|
+
onClick={goToLastPage}
|
|
180
|
+
disabled={!hasNextPage || totalPages === null}
|
|
181
|
+
aria-label={lastLabel}
|
|
45
182
|
>
|
|
46
|
-
|
|
183
|
+
<ChevronsRightIcon />
|
|
47
184
|
</button>
|
|
48
185
|
</div>
|
|
49
186
|
);
|
package/src/component/table.tsx
CHANGED
|
@@ -12,7 +12,7 @@ interface TableRootProps extends ComponentProps<"table"> {
|
|
|
12
12
|
|
|
13
13
|
function Root({ className, tableLayout = "fixed", ...props }: TableRootProps) {
|
|
14
14
|
return (
|
|
15
|
-
<div className="relative w-full overflow-x-auto">
|
|
15
|
+
<div className="relative w-full overflow-x-auto rounded-md border bg-background">
|
|
16
16
|
<table
|
|
17
17
|
className={cn(
|
|
18
18
|
"w-full caption-bottom text-sm",
|
package/src/component/types.ts
CHANGED
|
@@ -250,6 +250,7 @@ export interface QueryVariables {
|
|
|
250
250
|
export interface CollectionResult<T> {
|
|
251
251
|
edges: { node: T }[];
|
|
252
252
|
pageInfo: PageInfo;
|
|
253
|
+
total?: number | null;
|
|
253
254
|
}
|
|
254
255
|
|
|
255
256
|
/**
|
|
@@ -472,12 +473,22 @@ export interface UseCollectionReturn<
|
|
|
472
473
|
prevPage: (startCursor: string) => void;
|
|
473
474
|
/** Reset to first page */
|
|
474
475
|
resetPage: () => void;
|
|
475
|
-
/** Whether there is a previous page
|
|
476
|
+
/** Whether there is a previous page */
|
|
476
477
|
hasPrevPage: boolean;
|
|
477
|
-
/** Whether there is a next page
|
|
478
|
+
/** Whether there is a next page */
|
|
478
479
|
hasNextPage: boolean;
|
|
479
480
|
/** Set pageInfo from graphql result to track hasPrevPage/hasNextPage */
|
|
480
481
|
setPageInfo: (pageInfo: PageInfo) => void;
|
|
482
|
+
/** Current page number (1-based) */
|
|
483
|
+
currentPage: number;
|
|
484
|
+
/** Total number of pages, or null if total count is unknown */
|
|
485
|
+
totalPages: number | null;
|
|
486
|
+
/** Navigate to the first page */
|
|
487
|
+
goToFirstPage: () => void;
|
|
488
|
+
/** Navigate to the last page (requires totalPages) */
|
|
489
|
+
goToLastPage: () => void;
|
|
490
|
+
/** Set total item count from GraphQL result */
|
|
491
|
+
setTotal: (total: number) => void;
|
|
481
492
|
}
|
|
482
493
|
|
|
483
494
|
// =============================================================================
|
package/src/tests/helpers.tsx
CHANGED
|
@@ -67,6 +67,11 @@ export function createMockCollectionContext(
|
|
|
67
67
|
hasPrevPage: false,
|
|
68
68
|
hasNextPage: false,
|
|
69
69
|
setPageInfo: vi.fn(),
|
|
70
|
+
currentPage: 1,
|
|
71
|
+
totalPages: null,
|
|
72
|
+
goToFirstPage: vi.fn(),
|
|
73
|
+
goToLastPage: vi.fn(),
|
|
74
|
+
setTotal: vi.fn(),
|
|
70
75
|
...overrides,
|
|
71
76
|
};
|
|
72
77
|
}
|