@rowakit/table 0.5.0 → 1.0.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 +149 -142
- package/dist/index.cjs +113 -36
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +11 -11
- package/dist/index.d.ts +11 -11
- package/dist/index.js +113 -36
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,56 +1,32 @@
|
|
|
1
1
|
# @rowakit/table
|
|
2
2
|
|
|
3
|
-
**Server-side-first React table for internal & business applications
|
|
3
|
+
**Server-side-first React table for internal & business applications.**
|
|
4
|
+
Predictable API. Thin client. No data-grid bloat.
|
|
4
5
|
|
|
5
|
-
|
|
6
|
+
## Stability
|
|
6
7
|
|
|
7
|
-
|
|
8
|
+
`@rowakit/table` is **stable as of v1.0.0**.
|
|
8
9
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
Most React table libraries are **client-first** and optimized for maximum flexibility. RowaKit takes the opposite approach:
|
|
14
|
-
|
|
15
|
-
* ✅ Server-side pagination, sorting, filtering by default
|
|
16
|
-
* ✅ Minimal, convention-driven API (less boilerplate)
|
|
17
|
-
* ✅ Strong TypeScript contracts between UI and backend
|
|
18
|
-
* ✅ Built for long-lived internal tools, not demo-heavy grids
|
|
10
|
+
See:
|
|
11
|
+
- `docs/API_STABILITY.md`
|
|
12
|
+
- `docs/API_FREEZE_SUMMARY.md`
|
|
19
13
|
|
|
20
|
-
|
|
14
|
+
---
|
|
21
15
|
|
|
22
|
-
|
|
23
|
-
* Back-office / internal tools
|
|
24
|
-
* B2B SaaS management screens
|
|
25
|
-
* Enterprise CRUD applications
|
|
16
|
+
## Why @rowakit/table?
|
|
26
17
|
|
|
27
|
-
|
|
18
|
+
Most React table libraries grow into complex data grids.
|
|
19
|
+
RowaKit Table is intentionally different:
|
|
28
20
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
* 🧠 **Minimal API** – convention over configuration
|
|
34
|
-
* 🪝 **Escape hatch** – `col.custom()` for full rendering control
|
|
35
|
-
* 🎛️ **7 column types** – text, number, date, boolean, badge, actions, custom
|
|
36
|
-
* 🖱️ **Column resizing** – drag handles, min/max width, double-click auto-fit (v0.4.0+)
|
|
37
|
-
* 📌 **Saved views** – persist table state to localStorage (v0.4.0+)
|
|
38
|
-
* 🔗 **URL sync** – share exact table state via query string (v0.4.0+)
|
|
39
|
-
* 🧮 **Number range filters** – min/max with optional value transforms
|
|
40
|
-
* ✅ **Row selection** – select/deselect rows with bulk header checkbox (v0.5.0+)
|
|
41
|
-
* 🎬 **Bulk actions** – execute operations on multiple selected rows (v0.5.0+)
|
|
42
|
-
* 💾 **CSV export** – server-triggered export with customizable formatter (v0.5.0+)
|
|
43
|
-
* 🔄 **Multi-column sorting** – Ctrl+Click to sort by multiple columns with priority (v0.5.0+)
|
|
44
|
-
* ♿ **Accessibility** – ARIA labels, keyboard navigation, focus management (v0.5.0+)
|
|
45
|
-
* 🔄 **Smart fetching** – retry on error, stale request protection
|
|
46
|
-
* ✅ **Built-in states** – loading, error, empty handled automatically
|
|
21
|
+
* Backend owns data logic (pagination, sorting, filtering)
|
|
22
|
+
* Frontend stays thin and predictable
|
|
23
|
+
* API is opinionated and stable
|
|
24
|
+
* Workflow features are built-in, not bolted on
|
|
47
25
|
|
|
48
26
|
---
|
|
49
27
|
|
|
50
28
|
## Installation
|
|
51
29
|
|
|
52
|
-
RowaKit Table is published on npm and works with **npm**, **pnpm**, or **yarn**.
|
|
53
|
-
|
|
54
30
|
```bash
|
|
55
31
|
npm install @rowakit/table
|
|
56
32
|
# or
|
|
@@ -59,50 +35,41 @@ pnpm add @rowakit/table
|
|
|
59
35
|
yarn add @rowakit/table
|
|
60
36
|
```
|
|
61
37
|
|
|
62
|
-
|
|
38
|
+
Import base styles:
|
|
63
39
|
|
|
64
|
-
|
|
40
|
+
```ts
|
|
41
|
+
import '@rowakit/table/styles';
|
|
42
|
+
```
|
|
65
43
|
|
|
66
|
-
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## Quick Start
|
|
67
47
|
|
|
68
48
|
```tsx
|
|
69
49
|
import { RowaKitTable, col } from '@rowakit/table';
|
|
70
50
|
import type { Fetcher } from '@rowakit/table';
|
|
71
51
|
import '@rowakit/table/styles';
|
|
72
|
-
```
|
|
73
52
|
|
|
74
|
-
|
|
53
|
+
type User = { id: string; name: string; email: string; active: boolean };
|
|
75
54
|
|
|
76
|
-
|
|
77
|
-
interface User {
|
|
78
|
-
id: string;
|
|
79
|
-
name: string;
|
|
80
|
-
email: string;
|
|
81
|
-
active: boolean;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
const fetchUsers: Fetcher<User> = async ({ page, pageSize, sort, filters }) => {
|
|
55
|
+
const fetchUsers: Fetcher<User> = async ({ page, pageSize, sort }) => {
|
|
85
56
|
const params = new URLSearchParams({
|
|
86
57
|
page: String(page),
|
|
87
58
|
pageSize: String(pageSize),
|
|
88
59
|
});
|
|
89
60
|
|
|
90
61
|
if (sort) {
|
|
91
|
-
params.set('
|
|
62
|
+
params.set('sortField', sort.field);
|
|
92
63
|
params.set('sortDir', sort.direction);
|
|
93
64
|
}
|
|
94
65
|
|
|
95
66
|
const res = await fetch(`/api/users?${params}`);
|
|
96
67
|
if (!res.ok) throw new Error('Failed to fetch users');
|
|
97
68
|
|
|
98
|
-
return res.json();
|
|
69
|
+
return res.json();
|
|
99
70
|
};
|
|
100
|
-
```
|
|
101
71
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
```tsx
|
|
105
|
-
function UsersTable() {
|
|
72
|
+
export function UsersTable() {
|
|
106
73
|
return (
|
|
107
74
|
<RowaKitTable
|
|
108
75
|
fetcher={fetchUsers}
|
|
@@ -112,7 +79,7 @@ function UsersTable() {
|
|
|
112
79
|
col.text('email', { header: 'Email' }),
|
|
113
80
|
col.boolean('active', { header: 'Active' }),
|
|
114
81
|
col.actions([
|
|
115
|
-
{ id: 'edit', label: 'Edit'
|
|
82
|
+
{ id: 'edit', label: 'Edit' },
|
|
116
83
|
{ id: 'delete', label: 'Delete', confirm: true },
|
|
117
84
|
]),
|
|
118
85
|
]}
|
|
@@ -121,139 +88,179 @@ function UsersTable() {
|
|
|
121
88
|
}
|
|
122
89
|
```
|
|
123
90
|
|
|
124
|
-
That’s it — loading, error, pagination, sorting, and retry are handled automatically.
|
|
125
|
-
|
|
126
91
|
---
|
|
127
92
|
|
|
128
|
-
##
|
|
93
|
+
## Features (v1.0.0)
|
|
129
94
|
|
|
130
|
-
###
|
|
95
|
+
### Core table
|
|
96
|
+
|
|
97
|
+
* Server-side pagination, sorting, filtering
|
|
98
|
+
* Typed `Fetcher<T>` contract
|
|
99
|
+
* Built-in loading / error / empty states
|
|
100
|
+
* Stale request protection
|
|
101
|
+
|
|
102
|
+
### Columns
|
|
103
|
+
|
|
104
|
+
* `col.text`
|
|
105
|
+
* `col.number`
|
|
106
|
+
* `col.date`
|
|
107
|
+
* `col.boolean`
|
|
108
|
+
* `col.badge`
|
|
109
|
+
* `col.actions`
|
|
110
|
+
* `col.custom`
|
|
111
|
+
|
|
112
|
+
### UX & workflows
|
|
113
|
+
|
|
114
|
+
* Column resizing (pointer events)
|
|
115
|
+
* Double-click auto-fit
|
|
116
|
+
* URL sync
|
|
117
|
+
* Saved views
|
|
118
|
+
* Row selection (page-scoped)
|
|
119
|
+
* Bulk actions
|
|
120
|
+
* Export via `exporter` callback
|
|
121
|
+
|
|
122
|
+
---
|
|
131
123
|
|
|
132
|
-
|
|
124
|
+
## Fetcher Contract
|
|
133
125
|
|
|
134
126
|
```ts
|
|
135
127
|
type Fetcher<T> = (query: {
|
|
136
128
|
page: number;
|
|
137
129
|
pageSize: number;
|
|
130
|
+
/** Deprecated (kept for backward compatibility; planned removal in v2.0.0). */
|
|
138
131
|
sort?: { field: string; direction: 'asc' | 'desc' };
|
|
132
|
+
/** Multi-column sorting (preferred). */
|
|
133
|
+
sorts?: Array<{ field: string; direction: 'asc' | 'desc'; priority: number }>;
|
|
139
134
|
filters?: Record<string, unknown>;
|
|
140
135
|
}) => Promise<{ items: T[]; total: number }>;
|
|
141
136
|
```
|
|
142
137
|
|
|
143
|
-
|
|
144
|
-
* Throw an error to trigger the built-in error + retry UI
|
|
145
|
-
* Stale requests are ignored automatically
|
|
138
|
+
Guidelines:
|
|
146
139
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
140
|
+
* Backend is the source of truth
|
|
141
|
+
* Throw errors to trigger built-in error UI
|
|
142
|
+
* Ignore stale requests (handled internally)
|
|
150
143
|
|
|
151
|
-
|
|
144
|
+
---
|
|
152
145
|
|
|
153
|
-
|
|
146
|
+
## Row Selection
|
|
154
147
|
|
|
155
|
-
```
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
148
|
+
```tsx
|
|
149
|
+
<RowaKitTable
|
|
150
|
+
enableRowSelection
|
|
151
|
+
onSelectionChange={(keys) => console.log(keys)}
|
|
152
|
+
fetcher={fetchUsers}
|
|
153
|
+
columns={[/* ... */]}
|
|
154
|
+
/>
|
|
160
155
|
```
|
|
161
156
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
```ts
|
|
165
|
-
col.badge('status', {
|
|
166
|
-
header: 'Status',
|
|
167
|
-
sortable: true,
|
|
168
|
-
map: {
|
|
169
|
-
active: { label: 'Active', tone: 'success' },
|
|
170
|
-
pending: { label: 'Pending', tone: 'warning' },
|
|
171
|
-
error: { label: 'Error', tone: 'danger' },
|
|
172
|
-
},
|
|
173
|
-
});
|
|
174
|
-
```
|
|
157
|
+
* Selection is page-scoped
|
|
158
|
+
* Resets on page change
|
|
175
159
|
|
|
176
|
-
|
|
160
|
+
---
|
|
177
161
|
|
|
178
|
-
|
|
179
|
-
col.actions([
|
|
180
|
-
{ id: 'edit', label: 'Edit', onClick: (row) => edit(row) },
|
|
181
|
-
{ id: 'delete', label: 'Delete', confirm: true, onClick: (row) => remove(row) },
|
|
182
|
-
]);
|
|
183
|
-
```
|
|
162
|
+
## Multi-Column Sorting
|
|
184
163
|
|
|
185
|
-
|
|
164
|
+
Sort by multiple columns simultaneously using **Ctrl+Click** (Windows/Linux) or **Cmd+Click** (Mac) on column headers:
|
|
186
165
|
|
|
187
166
|
```tsx
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
167
|
+
// Hold Ctrl/Cmd and click column headers in order
|
|
168
|
+
// Priority is determined by click order (first click = priority 1)
|
|
169
|
+
|
|
170
|
+
// The fetcher receives sorts array:
|
|
171
|
+
const fetcher = async (query: FetcherQuery) => {
|
|
172
|
+
// query.sorts = [
|
|
173
|
+
// { field: 'lastName', direction: 'asc', priority: 1 },
|
|
174
|
+
// { field: 'firstName', direction: 'asc', priority: 2 },
|
|
175
|
+
// { field: 'salary', direction: 'desc', priority: 3 }
|
|
176
|
+
// ]
|
|
177
|
+
const res = await fetch('/api/users', {
|
|
178
|
+
method: 'POST',
|
|
179
|
+
body: JSON.stringify(query),
|
|
180
|
+
});
|
|
181
|
+
return res.json();
|
|
182
|
+
};
|
|
195
183
|
|
|
196
|
-
|
|
184
|
+
<RowaKitTable fetcher={fetcher} columns={[/* ... */]} />
|
|
185
|
+
```
|
|
197
186
|
|
|
198
|
-
|
|
187
|
+
**Migration from deprecated `sort` field:**
|
|
188
|
+
- Old format: `query.sort = { field: 'name', direction: 'asc' }`
|
|
189
|
+
- New format: `query.sorts = [{ field: 'name', direction: 'asc', priority: 1 }]`
|
|
190
|
+
- Both fields coexist during transition; `sort` will be removed in v2.0.0
|
|
199
191
|
|
|
200
|
-
|
|
192
|
+
**UI Indicators:**
|
|
193
|
+
- Single column: Standard sort arrow indicator
|
|
194
|
+
- Multiple columns: Priority number displayed on sorted column headers
|
|
201
195
|
|
|
202
|
-
|
|
203
|
-
* Min/max width constraints
|
|
204
|
-
* Double-click to auto-fit content
|
|
205
|
-
* Pointer Events (mouse / touch / pen)
|
|
206
|
-
* No accidental sort while resizing
|
|
196
|
+
---
|
|
207
197
|
|
|
208
|
-
|
|
198
|
+
## Bulk Actions
|
|
209
199
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
200
|
+
```tsx
|
|
201
|
+
<RowaKitTable
|
|
202
|
+
enableRowSelection
|
|
203
|
+
bulkActions={[
|
|
204
|
+
{
|
|
205
|
+
id: 'delete',
|
|
206
|
+
label: 'Delete selected',
|
|
207
|
+
confirm: { title: 'Confirm delete' },
|
|
208
|
+
onClick: (keys) => console.log(keys),
|
|
209
|
+
},
|
|
210
|
+
]}
|
|
211
|
+
fetcher={fetchUsers}
|
|
212
|
+
columns={[/* ... */]}
|
|
213
|
+
/>
|
|
214
|
+
```
|
|
214
215
|
|
|
215
216
|
---
|
|
216
217
|
|
|
217
|
-
##
|
|
218
|
+
## Export (CSV)
|
|
219
|
+
|
|
220
|
+
```tsx
|
|
221
|
+
const exporter = async (query) => {
|
|
222
|
+
const res = await fetch('/api/export', {
|
|
223
|
+
method: 'POST',
|
|
224
|
+
body: JSON.stringify(query),
|
|
225
|
+
});
|
|
218
226
|
|
|
219
|
-
|
|
227
|
+
const { url } = await res.json();
|
|
228
|
+
return { url };
|
|
229
|
+
};
|
|
220
230
|
|
|
221
|
-
|
|
222
|
-
import '@rowakit/table/styles';
|
|
231
|
+
<RowaKitTable exporter={exporter} fetcher={fetchUsers} columns={[/* ... */]} />
|
|
223
232
|
```
|
|
224
233
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
* Override CSS variables for theming
|
|
228
|
-
* Use `className` to scope custom styles
|
|
229
|
-
* Skip default styles and fully style from scratch
|
|
234
|
+
Export is server-triggered and scales well for large datasets.
|
|
230
235
|
|
|
231
236
|
---
|
|
232
237
|
|
|
233
|
-
##
|
|
238
|
+
## Roadmap & Versioning
|
|
234
239
|
|
|
235
|
-
*
|
|
236
|
-
*
|
|
237
|
-
*
|
|
238
|
-
* **Business tables ≠ spreadsheets**
|
|
240
|
+
* Current: **1.0.0** (stable)
|
|
241
|
+
* No breaking changes in 1.x (breaking changes require v2.0.0)
|
|
242
|
+
* Public API stability policy applies from v1.0.0
|
|
239
243
|
|
|
240
|
-
See
|
|
244
|
+
See roadmap: [docs/ROADMAP.md](docs/ROADMAP.md)
|
|
241
245
|
|
|
242
246
|
---
|
|
243
247
|
|
|
244
|
-
##
|
|
248
|
+
## Support RowaKit
|
|
249
|
+
|
|
250
|
+
If RowaKit helps your team:
|
|
251
|
+
|
|
252
|
+
* ⭐ Star the repo
|
|
253
|
+
* 💖 [Sponsor on GitHub](https://github.com/sponsors/midflow)
|
|
254
|
+
* ☕ [Buy us a coffee](https://buymeacoffee.com/midflow)
|
|
245
255
|
|
|
246
|
-
|
|
247
|
-
* API is stable; patches are backward compatible
|
|
248
|
-
* Completed: Stages A-E with full feature set for internal business applications
|
|
249
|
-
* See [CHANGELOG.md](./CHANGELOG.md) for detailed v0.5.0 features and [docs/ROADMAP.md](../../docs/ROADMAP.md)
|
|
256
|
+
Every bit of support helps sustain long-term maintenance.
|
|
250
257
|
|
|
251
258
|
---
|
|
252
259
|
|
|
253
260
|
## License
|
|
254
261
|
|
|
255
|
-
MIT
|
|
262
|
+
MIT © RowaKit Contributors
|
|
256
263
|
|
|
257
264
|
---
|
|
258
265
|
|
|
259
|
-
Built for teams shipping
|
|
266
|
+
**Built for teams shipping internal tools, not demos.**
|
package/dist/index.cjs
CHANGED
|
@@ -569,11 +569,22 @@ function parseUrlState(params, defaultPageSize, pageSizeOptions) {
|
|
|
569
569
|
} catch {
|
|
570
570
|
}
|
|
571
571
|
}
|
|
572
|
+
if (!result.sorts) {
|
|
573
|
+
const sortStr = params.get("sort");
|
|
574
|
+
if (sortStr) {
|
|
575
|
+
const [field, dir] = sortStr.split(":");
|
|
576
|
+
if (field && (dir === "asc" || dir === "desc")) {
|
|
577
|
+
result.sort = { field, direction: dir };
|
|
578
|
+
result.sorts = [{ field, direction: dir, priority: 0 }];
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
}
|
|
572
582
|
if (!result.sorts) {
|
|
573
583
|
const sortField = params.get("sortField");
|
|
574
584
|
const sortDir = params.get("sortDirection");
|
|
575
585
|
if (sortField && (sortDir === "asc" || sortDir === "desc")) {
|
|
576
586
|
result.sort = { field: sortField, direction: sortDir };
|
|
587
|
+
result.sorts = [{ field: sortField, direction: sortDir, priority: 0 }];
|
|
577
588
|
}
|
|
578
589
|
}
|
|
579
590
|
const filtersStr = params.get("filters");
|
|
@@ -645,40 +656,59 @@ function useUrlSync({
|
|
|
645
656
|
setColumnWidths
|
|
646
657
|
}) {
|
|
647
658
|
const didHydrateUrlRef = react.useRef(false);
|
|
648
|
-
const didSkipInitialUrlSyncRef = react.useRef(false);
|
|
649
659
|
const urlSyncDebounceRef = react.useRef(null);
|
|
660
|
+
const isApplyingUrlStateRef = react.useRef(false);
|
|
661
|
+
const clearApplyingTimerRef = react.useRef(null);
|
|
662
|
+
const hasWrittenUrlRef = react.useRef(false);
|
|
663
|
+
const lastQueryForUrlRef = react.useRef(null);
|
|
664
|
+
const defaultPageSizeRef = react.useRef(defaultPageSize);
|
|
665
|
+
const pageSizeOptionsRef = react.useRef(pageSizeOptions);
|
|
666
|
+
const enableColumnResizingRef = react.useRef(enableColumnResizing);
|
|
667
|
+
const columnsRef = react.useRef(columns);
|
|
668
|
+
defaultPageSizeRef.current = defaultPageSize;
|
|
669
|
+
pageSizeOptionsRef.current = pageSizeOptions;
|
|
670
|
+
enableColumnResizingRef.current = enableColumnResizing;
|
|
671
|
+
columnsRef.current = columns;
|
|
650
672
|
react.useEffect(() => {
|
|
651
673
|
if (!syncToUrl) {
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
}
|
|
655
|
-
if (!didSkipInitialUrlSyncRef.current) {
|
|
656
|
-
didSkipInitialUrlSyncRef.current = true;
|
|
674
|
+
hasWrittenUrlRef.current = false;
|
|
675
|
+
lastQueryForUrlRef.current = null;
|
|
657
676
|
return;
|
|
658
677
|
}
|
|
678
|
+
if (!didHydrateUrlRef.current) return;
|
|
679
|
+
if (isApplyingUrlStateRef.current) return;
|
|
659
680
|
if (urlSyncDebounceRef.current) {
|
|
660
681
|
clearTimeout(urlSyncDebounceRef.current);
|
|
661
682
|
urlSyncDebounceRef.current = null;
|
|
662
683
|
}
|
|
663
|
-
const urlStr = serializeUrlState(query, filters, columnWidths,
|
|
684
|
+
const urlStr = serializeUrlState(query, filters, columnWidths, defaultPageSizeRef.current, enableColumnResizingRef.current);
|
|
664
685
|
const qs = urlStr ? `?${urlStr}` : "";
|
|
665
|
-
|
|
686
|
+
const nextUrl = `${window.location.pathname}${qs}${window.location.hash}`;
|
|
687
|
+
const prevQuery = lastQueryForUrlRef.current;
|
|
688
|
+
const shouldPush = hasWrittenUrlRef.current && prevQuery != null && prevQuery.page !== query.page;
|
|
689
|
+
if (shouldPush) {
|
|
690
|
+
window.history.pushState(null, "", nextUrl);
|
|
691
|
+
} else {
|
|
692
|
+
window.history.replaceState(null, "", nextUrl);
|
|
693
|
+
}
|
|
694
|
+
hasWrittenUrlRef.current = true;
|
|
695
|
+
lastQueryForUrlRef.current = query;
|
|
666
696
|
}, [
|
|
667
697
|
query,
|
|
668
698
|
filters,
|
|
669
699
|
syncToUrl,
|
|
670
|
-
enableColumnResizing,
|
|
671
|
-
defaultPageSize,
|
|
672
700
|
columnWidths
|
|
673
701
|
]);
|
|
674
702
|
react.useEffect(() => {
|
|
675
|
-
if (!syncToUrl || !
|
|
676
|
-
if (!
|
|
703
|
+
if (!syncToUrl || !enableColumnResizingRef.current) return;
|
|
704
|
+
if (!didHydrateUrlRef.current) return;
|
|
705
|
+
if (!hasWrittenUrlRef.current) return;
|
|
706
|
+
if (isApplyingUrlStateRef.current) return;
|
|
677
707
|
if (urlSyncDebounceRef.current) {
|
|
678
708
|
clearTimeout(urlSyncDebounceRef.current);
|
|
679
709
|
}
|
|
680
710
|
urlSyncDebounceRef.current = setTimeout(() => {
|
|
681
|
-
const urlStr = serializeUrlState(query, filters, columnWidths,
|
|
711
|
+
const urlStr = serializeUrlState(query, filters, columnWidths, defaultPageSizeRef.current, enableColumnResizingRef.current);
|
|
682
712
|
const qs = urlStr ? `?${urlStr}` : "";
|
|
683
713
|
window.history.replaceState(null, "", `${window.location.pathname}${qs}${window.location.hash}`);
|
|
684
714
|
urlSyncDebounceRef.current = null;
|
|
@@ -692,36 +722,52 @@ function useUrlSync({
|
|
|
692
722
|
}, [
|
|
693
723
|
columnWidths,
|
|
694
724
|
syncToUrl,
|
|
695
|
-
enableColumnResizing,
|
|
696
725
|
query,
|
|
697
|
-
filters
|
|
698
|
-
defaultPageSize
|
|
726
|
+
filters
|
|
699
727
|
]);
|
|
700
|
-
|
|
701
|
-
if (
|
|
702
|
-
|
|
703
|
-
|
|
728
|
+
function scheduleClearApplyingFlag() {
|
|
729
|
+
if (clearApplyingTimerRef.current) {
|
|
730
|
+
clearTimeout(clearApplyingTimerRef.current);
|
|
731
|
+
clearApplyingTimerRef.current = null;
|
|
704
732
|
}
|
|
705
|
-
|
|
706
|
-
|
|
733
|
+
clearApplyingTimerRef.current = setTimeout(() => {
|
|
734
|
+
isApplyingUrlStateRef.current = false;
|
|
735
|
+
clearApplyingTimerRef.current = null;
|
|
736
|
+
}, 0);
|
|
737
|
+
}
|
|
738
|
+
function applyUrlToState() {
|
|
707
739
|
const params = new URLSearchParams(window.location.search);
|
|
708
|
-
const parsed = parseUrlState(params,
|
|
709
|
-
|
|
740
|
+
const parsed = parseUrlState(params, defaultPageSizeRef.current, pageSizeOptionsRef.current);
|
|
741
|
+
const nextQuery = {
|
|
710
742
|
page: parsed.page,
|
|
711
743
|
pageSize: parsed.pageSize,
|
|
712
744
|
sort: parsed.sort,
|
|
713
745
|
sorts: parsed.sorts,
|
|
714
746
|
filters: parsed.filters
|
|
715
|
-
}
|
|
716
|
-
|
|
717
|
-
|
|
747
|
+
};
|
|
748
|
+
isApplyingUrlStateRef.current = true;
|
|
749
|
+
scheduleClearApplyingFlag();
|
|
750
|
+
setQuery(nextQuery);
|
|
751
|
+
setFilters(parsed.filters ?? {});
|
|
752
|
+
if (!hasWrittenUrlRef.current) {
|
|
753
|
+
const urlStr = serializeUrlState(
|
|
754
|
+
nextQuery,
|
|
755
|
+
parsed.filters ?? {},
|
|
756
|
+
parsed.columnWidths ?? {},
|
|
757
|
+
defaultPageSizeRef.current,
|
|
758
|
+
enableColumnResizingRef.current
|
|
759
|
+
);
|
|
760
|
+
const qs = urlStr ? `?${urlStr}` : "";
|
|
761
|
+
window.history.replaceState(null, "", `${window.location.pathname}${qs}${window.location.hash}`);
|
|
762
|
+
hasWrittenUrlRef.current = true;
|
|
763
|
+
lastQueryForUrlRef.current = nextQuery;
|
|
718
764
|
}
|
|
719
|
-
if (
|
|
765
|
+
if (enableColumnResizingRef.current && parsed.columnWidths) {
|
|
720
766
|
const clamped = {};
|
|
721
767
|
for (const [colId, rawWidth] of Object.entries(parsed.columnWidths)) {
|
|
722
768
|
const widthNum = typeof rawWidth === "number" ? rawWidth : Number(rawWidth);
|
|
723
769
|
if (!Number.isFinite(widthNum)) continue;
|
|
724
|
-
const colDef =
|
|
770
|
+
const colDef = columnsRef.current.find((c) => c.id === colId);
|
|
725
771
|
if (!colDef) continue;
|
|
726
772
|
const minW = colDef.minWidth ?? 80;
|
|
727
773
|
const maxW = colDef.maxWidth;
|
|
@@ -732,17 +778,44 @@ function useUrlSync({
|
|
|
732
778
|
clamped[colId] = finalW;
|
|
733
779
|
}
|
|
734
780
|
setColumnWidths(clamped);
|
|
781
|
+
} else if (enableColumnResizingRef.current) {
|
|
782
|
+
setColumnWidths({});
|
|
735
783
|
}
|
|
784
|
+
}
|
|
785
|
+
react.useEffect(() => {
|
|
786
|
+
if (!syncToUrl) {
|
|
787
|
+
didHydrateUrlRef.current = false;
|
|
788
|
+
hasWrittenUrlRef.current = false;
|
|
789
|
+
lastQueryForUrlRef.current = null;
|
|
790
|
+
return;
|
|
791
|
+
}
|
|
792
|
+
if (didHydrateUrlRef.current) return;
|
|
793
|
+
didHydrateUrlRef.current = true;
|
|
794
|
+
applyUrlToState();
|
|
736
795
|
}, [
|
|
737
796
|
syncToUrl,
|
|
738
|
-
defaultPageSize,
|
|
739
|
-
enableColumnResizing,
|
|
740
|
-
pageSizeOptions,
|
|
741
|
-
columns,
|
|
742
797
|
setQuery,
|
|
743
798
|
setFilters,
|
|
744
799
|
setColumnWidths
|
|
745
800
|
]);
|
|
801
|
+
react.useEffect(() => {
|
|
802
|
+
if (!syncToUrl) return;
|
|
803
|
+
const onPopState = () => {
|
|
804
|
+
applyUrlToState();
|
|
805
|
+
};
|
|
806
|
+
window.addEventListener("popstate", onPopState);
|
|
807
|
+
return () => {
|
|
808
|
+
window.removeEventListener("popstate", onPopState);
|
|
809
|
+
};
|
|
810
|
+
}, [syncToUrl, setQuery, setFilters, setColumnWidths]);
|
|
811
|
+
react.useEffect(() => {
|
|
812
|
+
return () => {
|
|
813
|
+
if (clearApplyingTimerRef.current) {
|
|
814
|
+
clearTimeout(clearApplyingTimerRef.current);
|
|
815
|
+
clearApplyingTimerRef.current = null;
|
|
816
|
+
}
|
|
817
|
+
};
|
|
818
|
+
}, []);
|
|
746
819
|
}
|
|
747
820
|
var FOCUSABLE_SELECTORS = [
|
|
748
821
|
"button",
|
|
@@ -1093,9 +1166,13 @@ function RowaKitTable({
|
|
|
1093
1166
|
const headerChecked = isAllSelected(selectedKeys, pageRowKeys);
|
|
1094
1167
|
const headerIndeterminate = isIndeterminate(selectedKeys, pageRowKeys);
|
|
1095
1168
|
react.useEffect(() => {
|
|
1096
|
-
if (!enableRowSelection) return;
|
|
1097
1169
|
setSelectedKeys(clearSelection());
|
|
1098
|
-
}, [
|
|
1170
|
+
}, [query.page]);
|
|
1171
|
+
react.useEffect(() => {
|
|
1172
|
+
if (!enableRowSelection) {
|
|
1173
|
+
setSelectedKeys(clearSelection());
|
|
1174
|
+
}
|
|
1175
|
+
}, [enableRowSelection]);
|
|
1099
1176
|
react.useEffect(() => {
|
|
1100
1177
|
if (!enableRowSelection || !onSelectionChange) return;
|
|
1101
1178
|
onSelectionChange(selectedKeys);
|
|
@@ -1772,7 +1849,7 @@ function RowaKitTable({
|
|
|
1772
1849
|
var SmartTable = RowaKitTable;
|
|
1773
1850
|
|
|
1774
1851
|
// src/index.ts
|
|
1775
|
-
var VERSION = "0.
|
|
1852
|
+
var VERSION = "1.0.0" ;
|
|
1776
1853
|
|
|
1777
1854
|
exports.RowaKitTable = RowaKitTable;
|
|
1778
1855
|
exports.SmartTable = SmartTable;
|