@leancodepl/antd-table-hooks 10.1.3

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 ADDED
@@ -0,0 +1,470 @@
1
+ # @leancodepl/antd-table-hooks
2
+
3
+ React hooks for managing Ant Design table state — sorting, pagination, and filters — with optional URL query parameter
4
+ persistence via Zod schemas.
5
+
6
+ ## Installation
7
+
8
+ ```bash
9
+ npm install @leancodepl/antd-table-hooks
10
+ ```
11
+
12
+ ```bash
13
+ yarn add @leancodepl/antd-table-hooks
14
+ ```
15
+
16
+ ## API
17
+
18
+ ### `useTable(props)`
19
+
20
+ Orchestrates table sorting, pagination, and filter state through URL query parameters. Reads current state from
21
+ `queryParams` and writes updates via `setQueryParams`, enabling shareable links, browser navigation, and state
22
+ restoration on page reload.
23
+
24
+ **Parameters:**
25
+
26
+ | Name | Type | Description |
27
+ | ---------------------------- | ------------------------------------------- | --------------------------------------------------------------------------------- |
28
+ | `props.queryParams` | `Record<string, unknown>` | Current URL search params object (e.g. from TanStack Router's `useSearch`) |
29
+ | `props.setQueryParams` | `(params: Record<string, unknown>) => void` | Callback to update URL search params |
30
+ | `props.definedFilters` | `FilterDefinition[]` | Filter definitions created by `defineFilters` |
31
+ | `props.tableId` | `string` | Unique prefix for URL params to avoid collisions between tables on the same route |
32
+ | `props.defaultSortKey` | `Key` | Initial sort column key |
33
+ | `props.defaultSortDirection` | `SortOrder` | Initial sort direction (`"ascend"` or `"descend"`) |
34
+ | `props.defaultPageSize` | `number` (optional) | Initial page size (defaults to `100`) |
35
+
36
+ **Returns:** `{ filters, pagination, sorting }` — state objects to pass to `useFilters`, `usePagination`, and
37
+ `useSorting`.
38
+
39
+ ### `useSorting(props)`
40
+
41
+ Manages table column sorting state. Supports two modes: URL-driven (via `useTable().sorting`) or standalone with local
42
+ state.
43
+
44
+ **Parameters (URL-driven mode):**
45
+
46
+ | Name | Type | Description |
47
+ | --------------------- | ---------------------------------------------------- | -------------------------- |
48
+ | `props.sortKey` | `Key` | Current sort column key |
49
+ | `props.sortDirection` | `SortOrder` | Current sort direction |
50
+ | `props.onSortUpdate` | `(sortKey?: Key, sortDirection?: SortOrder) => void` | Callback when sort changes |
51
+
52
+ **Parameters (standalone mode):**
53
+
54
+ | Name | Type | Description |
55
+ | ---------------------------- | --------------------------------------------------------------- | -------------------------- |
56
+ | `props.defaultSortKey` | `Key` (optional) | Initial sort column key |
57
+ | `props.defaultSortDirection` | `SortOrder` (optional) | Initial sort direction |
58
+ | `props.onSortUpdate` | `(sortKey?: Key, sortDirection?: SortOrder) => void` (optional) | Callback when sort changes |
59
+
60
+ **Returns:** `{ sortData, sortKey, sortDirection, isDescending }`
61
+
62
+ - `sortData` — pass to an Ant Design table's `sort` prop
63
+ - `sortKey` — current sort column key
64
+ - `sortDirection` — current sort direction (`"ascend"` | `"descend"`)
65
+ - `isDescending` — `true` when `sortDirection` is `"descend"`
66
+
67
+ ### `usePagination(props)`
68
+
69
+ Manages table pagination state with zero-indexed page output for API calls. Returns a `getTablePagination` helper that
70
+ produces an Ant Design `TablePaginationConfig`.
71
+
72
+ **Parameters (URL-driven mode):**
73
+
74
+ | Name | Type | Description |
75
+ | -------------------------- | ------------------------------------------------------------ | -------------------------------- |
76
+ | `props.displayPage` | `number` | Current display page (1-indexed) |
77
+ | `props.pageSize` | `number` | Current page size |
78
+ | `props.onPaginationChange` | `(props: { displayPage: number; pageSize: number }) => void` | Callback when pagination changes |
79
+
80
+ **Parameters (standalone mode, all optional):**
81
+
82
+ | Name | Type | Description |
83
+ | -------------------------- | ----------------------------------------------------------------------- | -------------------------------------- |
84
+ | `props.initialDisplayPage` | `number` (optional) | Initial display page (defaults to `1`) |
85
+ | `props.initialPageSize` | `number` (optional) | Initial page size (defaults to `100`) |
86
+ | `props.onPaginationChange` | `(props: { displayPage: number; pageSize: number }) => void` (optional) | Callback when pagination changes |
87
+
88
+ **Returns:** `{ page, pageSize, getTablePagination, resetPage }`
89
+
90
+ - `page` — zero-indexed page number for API calls
91
+ - `pageSize` — current page size
92
+ - `getTablePagination(total?)` — returns an Ant Design `TablePaginationConfig`
93
+ - `resetPage()` — resets to page 1
94
+
95
+ ### `defineFilters()`
96
+
97
+ Creates a typed filter definition factory along with a combined Zod search schema for URL param validation. Call with a
98
+ query type generic, then pass an array of filter definitions (or a factory function for context-dependent filters).
99
+
100
+ **Returns:** A function that accepts filter definitions and returns `{ searchSchema, filters }`.
101
+
102
+ - `searchSchema` — Zod schema for filter-related URL params, used with `tableSearchSchema`
103
+ - `filters` — the filter definitions array (or factory function) to pass to `useFilters`
104
+
105
+ ### `useFilters(props)`
106
+
107
+ Manages filter state and produces an `applyFilters` function that applies all active filters to a query object.
108
+
109
+ **Parameters:**
110
+
111
+ | Name | Type | Description |
112
+ | ----------------------- | -------------------------------------- | ------------------------------------------------------------- |
113
+ | `props.filters` | `FilterDefinition[]` | Array of filter definitions (from `defineFilters`) |
114
+ | `props.onFiltersChange` | `(filters, values) => void` (optional) | Callback invoked when any filter value changes |
115
+ | `props.initialValues` | `Partial<TValues>` (optional) | Initial filter values (e.g. from `useTable().filters.values`) |
116
+
117
+ **Returns:** `{ filters, applyFilters, filterComponents, resetFilters, anyFilterSet }`
118
+
119
+ - `filters` — convenience wrapper object containing `applyFilters`, `filterComponents`, `resetFilters`, and `anyFilterSet`
120
+ - `applyFilters(query)` — applies all active filter transforms to the query object
121
+ - `filterComponents` — array of React nodes to render filter UI
122
+ - `resetFilters()` — clears all filters
123
+ - `anyFilterSet` — `true` when at least one filter is active
124
+
125
+ ### `tableSearchSchema(filtersSearchSchema, tableId, sortKeySchema)`
126
+
127
+ Combines filter, pagination, and sorting schemas into a single Zod schema for route validation.
128
+
129
+ **Parameters:**
130
+
131
+ | Name | Type | Description |
132
+ | --------------------- | --------------------- | ----------------------------------------------------------- |
133
+ | `filtersSearchSchema` | `DefinedSearchSchema` | Search schema returned by `defineFilters` |
134
+ | `tableId` | `string` | Unique table identifier used to prefix parameter names |
135
+ | `sortKeySchema` | `ZodType` | Zod schema for the sort key type (e.g. `z.coerce.number()`) |
136
+
137
+ **Returns:** Combined Zod object schema covering all table URL parameters.
138
+
139
+ ### `buildPaginationSearchSchema(tableId)`
140
+
141
+ Creates a Zod schema for pagination URL parameters (`{tableId}-displayPage`, `{tableId}-pageSize`).
142
+
143
+ **Parameters:**
144
+
145
+ | Name | Type | Description |
146
+ | --------- | -------- | ------------------------------------------------------ |
147
+ | `tableId` | `string` | Unique table identifier used to prefix parameter names |
148
+
149
+ **Returns:** Zod object schema for pagination params.
150
+
151
+ ### `buildSortingSearchSchema(tableId, sortKeySchema)`
152
+
153
+ Creates a Zod schema for sorting URL parameters (`{tableId}-sortKey`, `{tableId}-sortDescending`).
154
+
155
+ **Parameters:**
156
+
157
+ | Name | Type | Description |
158
+ | --------------- | --------- | ----------------------------------------------------------- |
159
+ | `tableId` | `string` | Unique table identifier used to prefix parameter names |
160
+ | `sortKeySchema` | `ZodType` | Zod schema for the sort key type (e.g. `z.coerce.number()`) |
161
+
162
+ **Returns:** Zod object schema for sorting params.
163
+
164
+ ## Usage Examples
165
+
166
+ ### Full Table with URL Query State
167
+
168
+ The primary pattern — persists sorting, pagination, and filters in URL search params for shareable links and state
169
+ restoration:
170
+
171
+ ```tsx
172
+ import { useSearch, useNavigate } from "@tanstack/react-router"
173
+ import { keepPreviousData } from "@tanstack/react-query"
174
+ import {
175
+ useTable,
176
+ useSorting,
177
+ usePagination,
178
+ useFilters,
179
+ defineFilters,
180
+ tableSearchSchema,
181
+ InferFiltersSchema,
182
+ } from "@leancodepl/antd-table-hooks"
183
+ import z from "zod"
184
+
185
+ const tableId = "reviews"
186
+
187
+ const { searchSchema: filtersSearchSchema, filters: reviewFilters } = defineFilters<SearchQuery>()([
188
+ emailFilter,
189
+ statusFilter,
190
+ ])
191
+
192
+ export const searchSchema = tableSearchSchema(filtersSearchSchema, tableId, z.coerce.number())
193
+
194
+ export function ReviewsTable() {
195
+ const search = useSearch({ from: "/reviews" })
196
+ const navigate = useNavigate({ from: "/reviews" })
197
+
198
+ const queryState = useTable({
199
+ queryParams: search,
200
+ setQueryParams: params => navigate({ search: params }),
201
+ definedFilters: reviewFilters,
202
+ tableId,
203
+ defaultSortKey: SortKey.DateCreated,
204
+ defaultSortDirection: "descend",
205
+ })
206
+
207
+ const { sortData, sortKey, isDescending } = useSorting(queryState.sorting)
208
+ const { page, pageSize, getTablePagination, resetPage } = usePagination(queryState.pagination)
209
+
210
+ const { applyFilters, filters } = useFilters<SearchQuery, InferFiltersSchema<typeof reviewFilters>>({
211
+ filters: reviewFilters,
212
+ initialValues: queryState.filters.values,
213
+ onFiltersChange: (_, values) => {
214
+ queryState.filters.onFiltersChange(values)
215
+ resetPage()
216
+ },
217
+ })
218
+
219
+ const { data, isPending, isPlaceholderData } = api.useSearchReviews(
220
+ applyFilters({ PageNumber: page, PageSize: pageSize, SortBy: sortKey, SortByDescending: isDescending }),
221
+ { placeholderData: keepPreviousData },
222
+ )
223
+
224
+ return (
225
+ <Table
226
+ columns={columns}
227
+ dataSource={data?.items}
228
+ loading={isPending || isPlaceholderData}
229
+ pagination={getTablePagination(data?.totalCount)}
230
+ />
231
+ )
232
+ }
233
+ ```
234
+
235
+ ### Route Configuration
236
+
237
+ Use the combined search schema as `validateSearch` in a TanStack Router route:
238
+
239
+ ```tsx
240
+ import { createFileRoute } from "@tanstack/react-router"
241
+ import { searchSchema } from "./ReviewsTable"
242
+
243
+ export const Route = createFileRoute("/reviews")({
244
+ component: ReviewsPage,
245
+ validateSearch: searchSchema,
246
+ })
247
+ ```
248
+
249
+ ### Standalone Table (Without URL State)
250
+
251
+ Use `useSorting`, `usePagination`, and `useFilters` directly when URL persistence is not needed:
252
+
253
+ ```tsx
254
+ import { useSorting, usePagination, useFilters } from "@leancodepl/antd-table-hooks"
255
+ import { keepPreviousData } from "@tanstack/react-query"
256
+
257
+ export function SimpleTable() {
258
+ const { sortData, sortKey, isDescending } = useSorting({
259
+ defaultSortDirection: "descend",
260
+ defaultSortKey: SortKey.CreatedAt,
261
+ })
262
+
263
+ const { page, pageSize, getTablePagination, resetPage } = usePagination()
264
+
265
+ const { applyFilters, filters } = useFilters({
266
+ filters: simpleFilters,
267
+ onFiltersChange: resetPage,
268
+ })
269
+
270
+ const { data, isPending } = api.useSearch(
271
+ applyFilters({ PageNumber: page, PageSize: pageSize, SortBy: sortKey, SortByDescending: isDescending }),
272
+ { placeholderData: keepPreviousData },
273
+ )
274
+
275
+ return (
276
+ <Table
277
+ columns={columns}
278
+ dataSource={data?.items}
279
+ loading={isPending}
280
+ pagination={getTablePagination(data?.totalCount)}
281
+ />
282
+ )
283
+ }
284
+ ```
285
+
286
+ ### Defining Filters
287
+
288
+ Filters are defined outside the component using `defineFilters`. Each filter is a `FilterDefinition` object describing
289
+ its ID, component, search schema entries, and how it transforms the query:
290
+
291
+ ```tsx
292
+ import { defineFilters, FilterDefinition } from "@leancodepl/antd-table-hooks"
293
+
294
+ const tableId = "users"
295
+
296
+ const { searchSchema: usersFiltersSchema, filters: userFilters } = defineFilters<SearchQuery>()([
297
+ emailTextFilter,
298
+ stateSelectFilter,
299
+ createdAtDateRangeFilter,
300
+ ])
301
+ ```
302
+
303
+ For context-dependent filters (e.g. internationalization):
304
+
305
+ ```tsx
306
+ type FiltersContext = { intl: IntlShape }
307
+
308
+ const { searchSchema, filters: filtersFn } = defineFilters<SearchQuery, FiltersContext>()(context => [
309
+ nameFilter(context?.intl),
310
+ ])
311
+
312
+ // In the component:
313
+ const intl = useIntl()
314
+ const filtersDefinition = useMemo(() => filtersFn({ intl }), [intl])
315
+ ```
316
+
317
+ ### Implementing Custom Filter Definitions
318
+
319
+ This library does not ship filter UI components — you implement them in your application to match your design system. A
320
+ filter definition is an object satisfying the `FilterDefinition<TQuery, TValue, TId>` type:
321
+
322
+ | Property | Type | Description |
323
+ | ------------------ | -------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- |
324
+ | `id` | `string` | Unique identifier for the filter within the table |
325
+ | `component` | `ComponentType<FilterProps>` | React component receiving `{ applyFilter, reset$, initialValue }` |
326
+ | `searchSchema` | `[key, ZodType][]` (optional) | URL param key/schema pairs for serialization |
327
+ | `buildApplyFilter` | `(value) => ((query) => query) \| undefined` (optional) | Reconstructs the filter function from a stored value (needed for initial/restored values) |
328
+ | `toSearchParams` | `(value) => Record<string, unknown>` (required when `searchSchema` is set) | Serializes the filter value to URL params |
329
+ | `fromSearchParams` | `(params) => value \| undefined` (required when `searchSchema` is set) | Deserializes URL params back to a filter value |
330
+
331
+ Each filter component receives three props from `useFilters`:
332
+
333
+ - `applyFilter(filterFn, value)` — call with a query transform function when the value changes, or `undefined` to clear
334
+ - `reset$` — an RxJS Observable that emits when filters are reset; subscribe to clear local state
335
+ - `initialValue` — restored value when hydrating from URL params
336
+
337
+ #### Minimal Example
338
+
339
+ A simple text input filter showing the essential structure:
340
+
341
+ ```tsx
342
+ import { useState, useEffect, useCallback } from "react"
343
+ import z from "zod"
344
+ import { FilterDefinition } from "@leancodepl/antd-table-hooks"
345
+
346
+ const schema = z.string().optional().catch(undefined)
347
+
348
+ export function textFilter<TQuery, const TId extends string, const TTableId extends string>({
349
+ id,
350
+ tableId,
351
+ label,
352
+ filter,
353
+ }: {
354
+ id: TId
355
+ tableId: TTableId
356
+ label: string
357
+ filter: (value: string, query: TQuery) => TQuery
358
+ }) {
359
+ const paramKey = `${tableId}-${id}` as const
360
+
361
+ return {
362
+ id,
363
+
364
+ component: ({ applyFilter, reset$, initialValue }) => {
365
+ const [value, setValue] = useState(initialValue)
366
+
367
+ const clear = useCallback(() => {
368
+ setValue(undefined)
369
+ applyFilter(undefined)
370
+ }, [applyFilter])
371
+
372
+ useEffect(() => {
373
+ const sub = reset$.subscribe(clear)
374
+ return () => sub.unsubscribe()
375
+ }, [clear, reset$])
376
+
377
+ return (
378
+ <input
379
+ placeholder={label}
380
+ value={value}
381
+ onChange={e => {
382
+ const v = e.target.value
383
+ setValue(v)
384
+ applyFilter(v ? query => filter(v, query) : undefined, v)
385
+ }}
386
+ />
387
+ )
388
+ },
389
+
390
+ buildApplyFilter: value => (value ? query => filter(value, query) : undefined),
391
+
392
+ searchSchema: [[paramKey, schema]] as const,
393
+ toSearchParams: value => ({ [paramKey]: value }),
394
+ fromSearchParams: params => params[paramKey] as string | undefined,
395
+ } satisfies FilterDefinition<TQuery, string, TId>
396
+ }
397
+ ```
398
+
399
+ Usage:
400
+
401
+ ```tsx
402
+ textFilter({
403
+ id: "email",
404
+ tableId: "users",
405
+ label: "Email",
406
+ filter: (email, query) => ({ ...query, EmailFilter: email }),
407
+ })
408
+ ```
409
+
410
+ #### Key Implementation Patterns
411
+
412
+ **Single URL param** (text, select): use one `searchSchema` entry keyed as `{tableId}-{id}`.
413
+
414
+ ```tsx
415
+ searchSchema: [[`${tableId}-${id}`, z.string().optional()]] as const
416
+ ```
417
+
418
+ **Multiple URL params** (date ranges): use multiple `searchSchema` entries with distinct keys.
419
+
420
+ ```tsx
421
+ searchSchema: [
422
+ [`${tableId}-${id}-from`, z.string().optional()],
423
+ [`${tableId}-${id}-to`, z.string().optional()],
424
+ ] as const
425
+ ```
426
+
427
+ **Clearing**: call `applyFilter(undefined)` when the filter value is empty or cleared. The `reset$` observable fires
428
+ when the user resets all filters — subscribe to it and clear local state.
429
+
430
+ **Restoring from URL**: `fromSearchParams` deserializes raw URL values back into the filter's value type.
431
+ `buildApplyFilter` reconstructs the query transform function from a deserialized value, enabling filter restoration on
432
+ page load.
433
+
434
+ ### Reusable Table with Query Params from Parent
435
+
436
+ Pass query params as props to share the same table component across multiple pages:
437
+
438
+ ```tsx
439
+ import { useSearch, useNavigate } from "@tanstack/react-router"
440
+ import z from "zod"
441
+
442
+ type UsersTableQueryParams = z.infer<typeof usersSearchSchema>
443
+
444
+ type UsersTableProps = {
445
+ name: string
446
+ queryParams: UsersTableQueryParams
447
+ setQueryParams: (params: UsersTableQueryParams) => void
448
+ }
449
+
450
+ export function UsersTable({ name, queryParams, setQueryParams }: UsersTableProps) {
451
+ const queryState = useTable({
452
+ queryParams,
453
+ setQueryParams,
454
+ definedFilters: usersFilters,
455
+ tableId: "users",
456
+ defaultSortKey: SortKey.CreatedAt,
457
+ defaultSortDirection: "descend",
458
+ })
459
+
460
+ // ... same pattern as full table example
461
+ }
462
+
463
+ // Parent page
464
+ export function UsersPage() {
465
+ const search = useSearch({ from: "/users" })
466
+ const navigate = useNavigate({ from: "/users" })
467
+
468
+ return <UsersTable name="users" queryParams={search} setQueryParams={params => navigate({ search: params })} />
469
+ }
470
+ ```
@@ -0,0 +1,2 @@
1
+ export * from './lib';
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,OAAO,CAAA"}