antd-crud-table 0.5.0 โ 0.8.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 +326 -738
- package/dist/CrudImportModal.d.ts +31 -0
- package/dist/CrudTable-BfVtTGYV.cjs +3 -0
- package/dist/CrudTable-jy8Db0uQ.js +1072 -0
- package/dist/CrudTable.cjs +1 -1
- package/dist/CrudTable.d.ts +94 -17
- package/dist/CrudTable.js +1 -1
- package/dist/CrudTableLazy.cjs +1 -1
- package/dist/CrudTableLazy.d.ts +4 -3
- package/dist/CrudTableLazy.js +8 -7
- package/dist/core/CustomDataSource.d.ts +35 -0
- package/dist/core/InMemoryDataSource.d.ts +30 -0
- package/dist/core/LocalStorageDataSource.d.ts +30 -0
- package/dist/core/RestDataSource.d.ts +209 -0
- package/dist/core/StaticDataSource.d.ts +20 -0
- package/dist/core/identity.d.ts +28 -0
- package/dist/core/inMemoryQuery.d.ts +26 -0
- package/dist/core/index.d.ts +11 -0
- package/dist/core/types.d.ts +94 -0
- package/dist/en_US-DghHhwRq.js +60 -0
- package/dist/en_US-Q8nDeJhg.cjs +1 -0
- package/dist/exportData.cjs +3 -3
- package/dist/exportData.js +37 -24
- package/dist/fields/registry.d.ts +29 -17
- package/dist/fields/types.d.ts +29 -0
- package/dist/hooks/useCrudTable.d.ts +114 -58
- package/dist/hooks/useLocalStorageCrud.d.ts +12 -12
- package/dist/index.cjs +1 -1
- package/dist/index.d.ts +19 -8
- package/dist/index.js +50 -6
- package/dist/locale/en_US.d.ts +10 -0
- package/dist/locale/index.d.ts +4 -0
- package/dist/locale/types.d.ts +127 -0
- package/dist/locale/useResolvedLocale.d.ts +35 -0
- package/dist/useCrudTable-BVXxakfl.js +497 -0
- package/dist/useCrudTable-Ex2Imgll.cjs +1 -0
- package/dist/useCrudTable.cjs +1 -1
- package/dist/useCrudTable.js +2 -2
- package/dist/useLocalStorageCrud.cjs +1 -1
- package/dist/useLocalStorageCrud.js +12 -51
- package/dist/utils/exportData.d.ts +35 -14
- package/dist/utils/importData.d.ts +130 -0
- package/dist/utils/importSpreadsheetML.d.ts +8 -0
- package/dist/utils/importXlsx.d.ts +7 -0
- package/package.json +35 -9
- package/dist/CrudTable-DUfRGJW2.js +0 -524
- package/dist/CrudTable-TTRqkEoZ.cjs +0 -1
- package/dist/hooks/useCrudTable.test.d.ts +0 -1
- package/dist/hooks/useLocalStorageCrud.test.d.ts +0 -1
- package/dist/useCrudTable-CEY_riQg.cjs +0 -1
- package/dist/useCrudTable-DpF-oZHz.js +0 -246
- package/dist/utils/exportData.test.d.ts +0 -1
- package/dist/utils/query.d.ts +0 -13
- package/dist/utils/query.test.d.ts +0 -1
package/README.md
CHANGED
|
@@ -1,837 +1,425 @@
|
|
|
1
|
+
<div align="center">
|
|
1
2
|
|
|
3
|
+
# antd-crud-table
|
|
2
4
|
|
|
3
|
-
|
|
5
|
+
**A typed, schema-driven CRUD table for React โ built on [antd](https://ant.design) and [ProComponents](https://procomponents.ant.design).**
|
|
4
6
|
|
|
5
|
-
|
|
7
|
+
Describe your columns once and get a paginated, searchable, sortable table with a
|
|
8
|
+
create/edit form, delete confirmations, bulk actions and export โ wired to static
|
|
9
|
+
data, a REST API, `localStorage`, or anything you implement yourself.
|
|
6
10
|
|
|
7
|
-
|
|
11
|
+
[](https://www.npmjs.com/package/antd-crud-table)
|
|
12
|
+
[](./LICENSE)
|
|
13
|
+
[](https://react.dev)
|
|
14
|
+
[](https://www.typescriptlang.org)
|
|
8
15
|
|
|
9
|
-
|
|
16
|
+
[**Live demo**](https://maifeeulasad.github.io/antd-crud-table/) ยท
|
|
17
|
+
[**Storybook**](https://maifeeulasad.github.io/antd-crud-table/storybook/) ยท
|
|
18
|
+
[**API reference**](https://maifeeulasad.github.io/antd-crud-table/api/) ยท
|
|
19
|
+
[**Changelog**](./CHANGELOG.md)
|
|
10
20
|
|
|
11
|
-
|
|
12
|
-
|------|------|-------------|
|
|
13
|
-
| `title` | `string` | Table header title |
|
|
14
|
-
| `rowKey` | `keyof T` | Unique identifier for each row |
|
|
15
|
-
| `columns` | `CrudColumn<T>[]` | Column definitions with |
|
|
16
|
-
| `hookConfig` | `UseCrudTableConfig<T>` | Hook configuration for data operations |
|
|
17
|
-
| `defaultPageSize?` | `number` | Initial page size (default: 10) |
|
|
18
|
-
| `enableBulkOperations?` | `boolean` | Enable bulk select/delete (default: false) |
|
|
19
|
-
| `customActions?` | `(record, actions) => ReactNode[]` | Custom row actions |ased Architecture**
|
|
20
|
-
|
|
21
|
-
Latest version introduces a powerful hook-based architecture with multiple data source strategies:
|
|
22
|
-
- **Static Data**: Perfect for prototypes and small datasets
|
|
23
|
-
- **API Integration**: REST API support with automatic request handling
|
|
24
|
-
- **Custom Operations**: Full control with GraphQL, IndexedDB, or custom logic
|
|
25
|
-
- **Built-in State Management**: Loading states, error handling, optimistic updates
|
|
21
|
+
</div>
|
|
26
22
|
|
|
27
23
|
---
|
|
28
24
|
|
|
29
|
-
##
|
|
25
|
+
## Why
|
|
26
|
+
|
|
27
|
+
Most table libraries hand you a grid and leave the CRUD to you. This one takes a
|
|
28
|
+
column schema and derives the whole surface from it โ the cell renderer, the form
|
|
29
|
+
control, the validation, and the value conversion in both directions.
|
|
30
|
+
|
|
31
|
+
```tsx
|
|
32
|
+
<CrudTable<User, 'id'>
|
|
33
|
+
title="Users"
|
|
34
|
+
rowKey="id"
|
|
35
|
+
columns={[
|
|
36
|
+
{ dataIndex: 'name', title: 'Name', fieldType: 'string', formConfig: { required: true } },
|
|
37
|
+
{ dataIndex: 'email', title: 'Email', fieldType: 'email' },
|
|
38
|
+
{ dataIndex: 'joined', title: 'Joined', fieldType: 'date' },
|
|
39
|
+
]}
|
|
40
|
+
hookConfig={{
|
|
41
|
+
api: {
|
|
42
|
+
baseUrl: '/api',
|
|
43
|
+
endpoints: { list: '/users', create: '/users', update: '/users', remove: '/users' },
|
|
44
|
+
},
|
|
45
|
+
}}
|
|
46
|
+
/>
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
That renders a searchable, sortable, paginated table with a working create/edit
|
|
50
|
+
form, confirmed deletes, and CSV/JSON/Excel export.
|
|
51
|
+
|
|
52
|
+
## Highlights
|
|
53
|
+
|
|
54
|
+
- **Strictly typed.** `dataIndex` is bound to `keyof T`, record ids are `T[K]`, and
|
|
55
|
+
`customRender`/`transform` receive that property's own type. No `any` in the
|
|
56
|
+
public API.
|
|
57
|
+
- **20 field types** โ each one entry in a registry declaring its cell renderer,
|
|
58
|
+
form control, implied validation and value round-trip.
|
|
59
|
+
- **Four data strategies** behind one `CrudDataSource` interface: static, REST,
|
|
60
|
+
`localStorage`, or your own. Swap backends without touching your columns.
|
|
61
|
+
- **Localized.** Every string is overridable, and the table follows the antd
|
|
62
|
+
`ConfigProvider` around it.
|
|
63
|
+
- **Export** to CSV, JSON and Excel, covering the whole filtered result set โ
|
|
64
|
+
with CSV formula injection neutralised.
|
|
65
|
+
- **Zero runtime dependencies.** Everything is a peer you already have.
|
|
66
|
+
|
|
67
|
+
## Installation
|
|
30
68
|
|
|
31
69
|
```bash
|
|
32
70
|
npm install antd-crud-table
|
|
33
71
|
```
|
|
34
72
|
|
|
35
|
-
**Peer Dependencies:**
|
|
36
73
|
```bash
|
|
37
|
-
|
|
74
|
+
pnpm add antd-crud-table
|
|
38
75
|
```
|
|
39
76
|
|
|
40
|
-
|
|
77
|
+
### Peer dependencies
|
|
41
78
|
|
|
42
|
-
|
|
79
|
+
| Package | Version |
|
|
80
|
+
|---|---|
|
|
81
|
+
| `react`, `react-dom` | `^18` or `^19` |
|
|
82
|
+
| `antd` | `^6.3.6` |
|
|
83
|
+
| `@ant-design/icons` | `^6` |
|
|
84
|
+
| `@ant-design/pro-components` | `^2.8.10` |
|
|
85
|
+
| `dayjs` | `^1.11.13` |
|
|
43
86
|
|
|
44
|
-
|
|
87
|
+
### Stylesheet
|
|
45
88
|
|
|
46
|
-
|
|
89
|
+
The build extracts CSS to a separate file, so **import it once** โ importing the
|
|
90
|
+
component alone leaves the table unstyled:
|
|
47
91
|
|
|
48
|
-
```
|
|
49
|
-
import
|
|
50
|
-
|
|
51
|
-
// Static data example
|
|
52
|
-
const UserManagement = () => (
|
|
53
|
-
<CrudTable<User>
|
|
54
|
-
title="User Management"
|
|
55
|
-
rowKey="id"
|
|
56
|
-
hookConfig={{
|
|
57
|
-
staticData: users, // Your data array
|
|
58
|
-
optimisticUpdates: true,
|
|
59
|
-
}}
|
|
60
|
-
columns={[
|
|
61
|
-
{
|
|
62
|
-
title: 'Name',
|
|
63
|
-
dataIndex: 'name',
|
|
64
|
-
fieldType: 'string',
|
|
65
|
-
formConfig: { required: true },
|
|
66
|
-
},
|
|
67
|
-
{
|
|
68
|
-
title: 'Status',
|
|
69
|
-
dataIndex: 'status',
|
|
70
|
-
fieldType: 'enum',
|
|
71
|
-
enumOptions: {
|
|
72
|
-
active: { text: 'Active', color: 'green' },
|
|
73
|
-
inactive: { text: 'Inactive', color: 'orange' },
|
|
74
|
-
},
|
|
75
|
-
},
|
|
76
|
-
]}
|
|
77
|
-
/>
|
|
78
|
-
);
|
|
92
|
+
```ts
|
|
93
|
+
import 'antd-crud-table/styles.css';
|
|
79
94
|
```
|
|
80
95
|
|
|
81
|
-
|
|
96
|
+
## Quick start
|
|
82
97
|
|
|
83
98
|
```tsx
|
|
84
99
|
import { CrudTable } from 'antd-crud-table';
|
|
100
|
+
import type { CrudColumn } from 'antd-crud-table';
|
|
101
|
+
import 'antd-crud-table/styles.css';
|
|
102
|
+
|
|
103
|
+
interface User {
|
|
104
|
+
id: number;
|
|
105
|
+
name: string;
|
|
106
|
+
email: string;
|
|
107
|
+
status: 'active' | 'inactive';
|
|
108
|
+
joined: string;
|
|
109
|
+
}
|
|
85
110
|
|
|
86
|
-
const
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
111
|
+
const columns: CrudColumn<User>[] = [
|
|
112
|
+
{ dataIndex: 'name', title: 'Name', fieldType: 'string', formConfig: { required: true } },
|
|
113
|
+
{ dataIndex: 'email', title: 'Email', fieldType: 'email' },
|
|
114
|
+
{
|
|
115
|
+
dataIndex: 'status',
|
|
116
|
+
title: 'Status',
|
|
117
|
+
fieldType: 'enum',
|
|
118
|
+
enumOptions: {
|
|
119
|
+
active: { text: 'Active', color: 'green' },
|
|
120
|
+
inactive: { text: 'Inactive', color: 'red' },
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
{ dataIndex: 'joined', title: 'Joined', fieldType: 'date' },
|
|
124
|
+
];
|
|
92
125
|
|
|
93
|
-
const
|
|
94
|
-
<CrudTable
|
|
95
|
-
title="
|
|
126
|
+
export const Users = () => (
|
|
127
|
+
<CrudTable<User, 'id'>
|
|
128
|
+
title="Users"
|
|
96
129
|
rowKey="id"
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
formConfig: { required: true },
|
|
130
|
+
columns={columns}
|
|
131
|
+
defaultPageSize={10}
|
|
132
|
+
enableBulkOperations
|
|
133
|
+
hookConfig={{
|
|
134
|
+
api: {
|
|
135
|
+
baseUrl: '/api',
|
|
136
|
+
endpoints: { list: '/users', create: '/users', update: '/users', remove: '/users' },
|
|
105
137
|
},
|
|
106
|
-
|
|
138
|
+
}}
|
|
107
139
|
/>
|
|
108
140
|
);
|
|
109
141
|
```
|
|
110
142
|
|
|
111
|
-
|
|
143
|
+
The second type parameter is the row key. It is what makes ids typed: `remove(id)`
|
|
144
|
+
takes a `number` here, not `any`.
|
|
112
145
|
|
|
113
|
-
##
|
|
146
|
+
## Data strategies
|
|
114
147
|
|
|
115
|
-
|
|
148
|
+
Every strategy implements the same `CrudDataSource<T, K>` interface, so the
|
|
149
|
+
columns and behaviour are identical and only the wiring differs.
|
|
116
150
|
|
|
117
|
-
|
|
118
|
-
```tsx
|
|
119
|
-
<CrudTable
|
|
120
|
-
hookConfig={{
|
|
121
|
-
staticData: mockUsers,
|
|
122
|
-
optimisticUpdates: true,
|
|
123
|
-
}}
|
|
124
|
-
// ... other props
|
|
125
|
-
/>
|
|
126
|
-
```
|
|
151
|
+
### Static data
|
|
127
152
|
|
|
128
|
-
#### API Integration (Production Ready)
|
|
129
153
|
```tsx
|
|
130
|
-
|
|
131
|
-
hookConfig={{
|
|
132
|
-
api: {
|
|
133
|
-
baseUrl: 'https://api.example.com',
|
|
134
|
-
endpoints: {
|
|
135
|
-
list: '/users',
|
|
136
|
-
create: '/users',
|
|
137
|
-
update: '/users',
|
|
138
|
-
delete: '/users',
|
|
139
|
-
},
|
|
140
|
-
headers: {
|
|
141
|
-
'Authorization': 'Bearer your-token',
|
|
142
|
-
},
|
|
143
|
-
transform: {
|
|
144
|
-
response: (data) => ({
|
|
145
|
-
data: data.users,
|
|
146
|
-
total: data.totalCount,
|
|
147
|
-
}),
|
|
148
|
-
request: (data) => ({
|
|
149
|
-
...data,
|
|
150
|
-
updatedAt: new Date().toISOString(),
|
|
151
|
-
}),
|
|
152
|
-
},
|
|
153
|
-
},
|
|
154
|
-
onSuccess: (operation, data) => {
|
|
155
|
-
console.log(`${operation} completed`, data);
|
|
156
|
-
},
|
|
157
|
-
onError: (operation, error) => {
|
|
158
|
-
console.error(`${operation} failed`, error);
|
|
159
|
-
},
|
|
160
|
-
}}
|
|
161
|
-
// ... other props
|
|
162
|
-
/>
|
|
163
|
-
```
|
|
164
|
-
|
|
165
|
-
#### Custom Operations (Maximum Flexibility)
|
|
166
|
-
```tsx
|
|
167
|
-
<CrudTable
|
|
168
|
-
hookConfig={{
|
|
169
|
-
operations: {
|
|
170
|
-
getList: async (params) => {
|
|
171
|
-
const result = await myGraphQLQuery(params);
|
|
172
|
-
return {
|
|
173
|
-
data: result.users,
|
|
174
|
-
total: result.totalCount,
|
|
175
|
-
success: true,
|
|
176
|
-
};
|
|
177
|
-
},
|
|
178
|
-
create: async (data) => await myCreateMutation(data),
|
|
179
|
-
update: async (id, data) => await myUpdateMutation(id, data),
|
|
180
|
-
delete: async (id) => await myDeleteMutation(id),
|
|
181
|
-
},
|
|
182
|
-
}}
|
|
183
|
-
// ... other props
|
|
184
|
-
/>
|
|
154
|
+
hookConfig={{ staticData: users }}
|
|
185
155
|
```
|
|
186
156
|
|
|
187
|
-
|
|
157
|
+
In-memory, seeded once. Edits persist for the session. Good for demos, fixtures
|
|
158
|
+
and tests.
|
|
188
159
|
|
|
189
|
-
|
|
190
|
-
```tsx
|
|
191
|
-
<CrudTable
|
|
192
|
-
enableBulkOperations={true}
|
|
193
|
-
// Automatically adds bulk select and delete functionality
|
|
194
|
-
/>
|
|
195
|
-
```
|
|
196
|
-
|
|
197
|
-
#### Custom Actions
|
|
198
|
-
```tsx
|
|
199
|
-
<CrudTable
|
|
200
|
-
customActions={(record, actions) => [
|
|
201
|
-
<Button
|
|
202
|
-
key="export"
|
|
203
|
-
onClick={() => exportUser(record)}
|
|
204
|
-
>
|
|
205
|
-
Export
|
|
206
|
-
</Button>,
|
|
207
|
-
<Button
|
|
208
|
-
key="clone"
|
|
209
|
-
onClick={() => actions.create({...record, id: undefined})}
|
|
210
|
-
>
|
|
211
|
-
Clone
|
|
212
|
-
</Button>
|
|
213
|
-
]}
|
|
214
|
-
/>
|
|
215
|
-
```
|
|
160
|
+
### REST API
|
|
216
161
|
|
|
217
|
-
#### Validation
|
|
218
162
|
```tsx
|
|
219
|
-
|
|
220
|
-
{
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
fieldType: 'string',
|
|
224
|
-
formConfig: {
|
|
225
|
-
required: true,
|
|
226
|
-
rules: [
|
|
227
|
-
{ required: true, message: 'Email is required' },
|
|
228
|
-
{ type: 'email', message: 'Invalid email format' },
|
|
229
|
-
{
|
|
230
|
-
validator: async (_, value) => {
|
|
231
|
-
const exists = await checkEmailExists(value);
|
|
232
|
-
if (exists) throw new Error('Email already exists');
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
],
|
|
236
|
-
},
|
|
163
|
+
hookConfig={{
|
|
164
|
+
api: {
|
|
165
|
+
baseUrl: '/api',
|
|
166
|
+
endpoints: { list: '/users', create: '/users', update: '/users', remove: '/users' },
|
|
237
167
|
},
|
|
238
|
-
|
|
168
|
+
}}
|
|
239
169
|
```
|
|
240
170
|
|
|
241
|
-
|
|
171
|
+
Endpoints default to `/list`, `/create`, `/update` and `/delete` under `baseUrl`,
|
|
172
|
+
so a conventional collection needs them stated as above. Paging defaults to
|
|
173
|
+
`current`/`pageSize`, and updates to `PUT {update}/:id`. For an API that
|
|
174
|
+
speaks a different dialect, `paramNames`, `methods`, `serializeRequest` and
|
|
175
|
+
`parseResponse` cover most of it declaratively โ see
|
|
176
|
+
**[REST dialect recipes](./docs/rest-recipes.md)** for offset/limit, Django REST
|
|
177
|
+
Framework, JSON:API and auth.
|
|
242
178
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
#### Example 1: useUserCrud - Specialized User Management
|
|
246
|
-
```tsx
|
|
247
|
-
import { useCrudTable, type UseCrudTableConfig } from 'antd-crud-table';
|
|
248
|
-
|
|
249
|
-
export const useUserCrud = (baseConfig?: Partial<UseCrudTableConfig<any>['api']>) => {
|
|
250
|
-
const config: UseCrudTableConfig<any> = {
|
|
251
|
-
api: {
|
|
252
|
-
baseUrl: '/api/users',
|
|
253
|
-
headers: {
|
|
254
|
-
'Authorization': `Bearer ${localStorage.getItem('token')}`,
|
|
255
|
-
},
|
|
256
|
-
transform: {
|
|
257
|
-
request: (data) => ({
|
|
258
|
-
...data,
|
|
259
|
-
// Add default values or transformations
|
|
260
|
-
updatedAt: new Date().toISOString(),
|
|
261
|
-
}),
|
|
262
|
-
response: (data) => ({
|
|
263
|
-
data: data.users || data.data || [],
|
|
264
|
-
total: data.total || data.count || 0,
|
|
265
|
-
success: true,
|
|
266
|
-
}),
|
|
267
|
-
},
|
|
268
|
-
...baseConfig,
|
|
269
|
-
},
|
|
270
|
-
defaultPageSize: 10,
|
|
271
|
-
optimisticUpdates: true,
|
|
272
|
-
onSuccess: (operation, data) => {
|
|
273
|
-
console.log(`User ${operation} completed:`, data);
|
|
274
|
-
},
|
|
275
|
-
onError: (operation, error) => {
|
|
276
|
-
console.error(`User ${operation} failed:`, error);
|
|
277
|
-
// Could add toast notifications, error reporting, etc.
|
|
278
|
-
},
|
|
279
|
-
};
|
|
179
|
+
Failures throw `RestError`, carrying `status` and `body` so you can branch on a
|
|
180
|
+
422 rather than parsing a message.
|
|
280
181
|
|
|
281
|
-
|
|
282
|
-
};
|
|
283
|
-
```
|
|
182
|
+
### localStorage
|
|
284
183
|
|
|
285
|
-
#### Example 2: useLocalStorageCrud - Local Storage Persistence
|
|
286
184
|
```tsx
|
|
287
|
-
|
|
288
|
-
storageKey: string,
|
|
289
|
-
rowKey: keyof T,
|
|
290
|
-
initialData: T[] = []
|
|
291
|
-
) => {
|
|
292
|
-
const getStoredData = (): T[] => {
|
|
293
|
-
try {
|
|
294
|
-
const stored = localStorage.getItem(storageKey);
|
|
295
|
-
return stored ? JSON.parse(stored) : initialData;
|
|
296
|
-
} catch {
|
|
297
|
-
return initialData;
|
|
298
|
-
}
|
|
299
|
-
};
|
|
300
|
-
|
|
301
|
-
const setStoredData = (data: T[]) => {
|
|
302
|
-
localStorage.setItem(storageKey, JSON.stringify(data));
|
|
303
|
-
};
|
|
304
|
-
|
|
305
|
-
return useCrudTable(rowKey, {
|
|
306
|
-
operations: {
|
|
307
|
-
getList: async (params) => {
|
|
308
|
-
const data = getStoredData();
|
|
309
|
-
const { current = 1, pageSize = 10, ...filters } = params;
|
|
310
|
-
|
|
311
|
-
// Apply filters
|
|
312
|
-
let filteredData = data;
|
|
313
|
-
Object.entries(filters).forEach(([key, value]) => {
|
|
314
|
-
if (value !== undefined && value !== null && value !== '') {
|
|
315
|
-
filteredData = filteredData.filter(item =>
|
|
316
|
-
String(item[key]).toLowerCase().includes(String(value).toLowerCase())
|
|
317
|
-
);
|
|
318
|
-
}
|
|
319
|
-
});
|
|
320
|
-
|
|
321
|
-
// Apply pagination
|
|
322
|
-
const start = (current - 1) * pageSize;
|
|
323
|
-
const paginatedData = filteredData.slice(start, start + pageSize);
|
|
324
|
-
|
|
325
|
-
return {
|
|
326
|
-
data: paginatedData,
|
|
327
|
-
total: filteredData.length,
|
|
328
|
-
success: true,
|
|
329
|
-
};
|
|
330
|
-
},
|
|
331
|
-
|
|
332
|
-
create: async (newItem) => {
|
|
333
|
-
const data = getStoredData();
|
|
334
|
-
const maxId = Math.max(...data.map(item => Number(item[rowKey]) || 0), 0);
|
|
335
|
-
const created = {
|
|
336
|
-
[rowKey]: maxId + 1,
|
|
337
|
-
...newItem,
|
|
338
|
-
createdAt: new Date().toISOString(),
|
|
339
|
-
} as T;
|
|
340
|
-
|
|
341
|
-
data.push(created);
|
|
342
|
-
setStoredData(data);
|
|
343
|
-
return created;
|
|
344
|
-
},
|
|
345
|
-
|
|
346
|
-
update: async (id, updateData) => {
|
|
347
|
-
const data = getStoredData();
|
|
348
|
-
const index = data.findIndex(item => item[rowKey] === id);
|
|
349
|
-
if (index === -1) throw new Error('Item not found');
|
|
350
|
-
|
|
351
|
-
data[index] = {
|
|
352
|
-
...data[index],
|
|
353
|
-
...updateData,
|
|
354
|
-
updatedAt: new Date().toISOString(),
|
|
355
|
-
};
|
|
356
|
-
setStoredData(data);
|
|
357
|
-
return data[index];
|
|
358
|
-
},
|
|
359
|
-
|
|
360
|
-
delete: async (id) => {
|
|
361
|
-
const data = getStoredData();
|
|
362
|
-
const filtered = data.filter(item => item[rowKey] !== id);
|
|
363
|
-
setStoredData(filtered);
|
|
364
|
-
},
|
|
365
|
-
},
|
|
366
|
-
optimisticUpdates: true,
|
|
367
|
-
});
|
|
368
|
-
};
|
|
185
|
+
hookConfig={{ storageKey: 'my-users', initialData: seed }}
|
|
369
186
|
```
|
|
370
187
|
|
|
371
|
-
|
|
372
|
-
```tsx
|
|
373
|
-
export const useRealtimeCrud = <T extends Record<string, any>>(
|
|
374
|
-
rowKey: keyof T,
|
|
375
|
-
websocketUrl: string,
|
|
376
|
-
apiConfig: UseCrudTableConfig<T>['api']
|
|
377
|
-
) => {
|
|
378
|
-
const config: UseCrudTableConfig<T> = {
|
|
379
|
-
api: apiConfig,
|
|
380
|
-
optimisticUpdates: false, // Disable optimistic updates for realtime
|
|
381
|
-
};
|
|
382
|
-
|
|
383
|
-
const crud = useCrudTable(rowKey, config);
|
|
384
|
-
|
|
385
|
-
// In a real implementation, you would set up WebSocket listeners here
|
|
386
|
-
// useEffect(() => {
|
|
387
|
-
// const ws = new WebSocket(websocketUrl);
|
|
388
|
-
//
|
|
389
|
-
// ws.onmessage = (event) => {
|
|
390
|
-
// const { type, data } = JSON.parse(event.data);
|
|
391
|
-
// switch (type) {
|
|
392
|
-
// case 'created':
|
|
393
|
-
// case 'updated':
|
|
394
|
-
// case 'deleted':
|
|
395
|
-
// crud.refresh(); // Refresh data when changes occur
|
|
396
|
-
// break;
|
|
397
|
-
// }
|
|
398
|
-
// };
|
|
399
|
-
//
|
|
400
|
-
// return () => ws.close();
|
|
401
|
-
// }, [websocketUrl]);
|
|
402
|
-
|
|
403
|
-
return crud;
|
|
404
|
-
};
|
|
405
|
-
```
|
|
406
|
-
|
|
407
|
-
#### Example 4: useInfiniteScrollCrud - Infinite Scrolling
|
|
408
|
-
```tsx
|
|
409
|
-
export const useInfiniteScrollCrud = <T extends Record<string, any>>(
|
|
410
|
-
rowKey: keyof T,
|
|
411
|
-
baseConfig: UseCrudTableConfig<T>
|
|
412
|
-
) => {
|
|
413
|
-
// This would extend the base hook with infinite scroll capabilities
|
|
414
|
-
// Implementation would handle cursor-based pagination, data accumulation, etc.
|
|
415
|
-
|
|
416
|
-
const config: UseCrudTableConfig<T> = {
|
|
417
|
-
...baseConfig,
|
|
418
|
-
// Add infinite scroll specific configuration
|
|
419
|
-
};
|
|
420
|
-
|
|
421
|
-
return useCrudTable(rowKey, config);
|
|
422
|
-
};
|
|
423
|
-
```
|
|
188
|
+
Persists across reloads and stamps `createdAt` / `updatedAt`.
|
|
424
189
|
|
|
425
|
-
|
|
426
|
-
```tsx
|
|
427
|
-
export const useCachedCrud = <T extends Record<string, any>>(
|
|
428
|
-
rowKey: keyof T,
|
|
429
|
-
cacheKey: string,
|
|
430
|
-
baseConfig: UseCrudTableConfig<T>
|
|
431
|
-
) => {
|
|
432
|
-
const config: UseCrudTableConfig<T> = {
|
|
433
|
-
...baseConfig,
|
|
434
|
-
enableCache: true,
|
|
435
|
-
// In a real implementation, you might integrate with:
|
|
436
|
-
// - React Query
|
|
437
|
-
// - SWR
|
|
438
|
-
// - Redux Toolkit Query
|
|
439
|
-
// - Apollo Client
|
|
440
|
-
// etc.
|
|
441
|
-
};
|
|
442
|
-
|
|
443
|
-
return useCrudTable(rowKey, config);
|
|
444
|
-
};
|
|
445
|
-
};
|
|
446
|
-
```
|
|
190
|
+
### Your own
|
|
447
191
|
|
|
448
|
-
#### Usage Examples
|
|
449
192
|
```tsx
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
hookConfig={userCrud}
|
|
459
|
-
columns={userColumns}
|
|
460
|
-
/>
|
|
461
|
-
);
|
|
462
|
-
};
|
|
463
|
-
|
|
464
|
-
const OfflineTable = () => {
|
|
465
|
-
const offlineCrud = useLocalStorageCrud<User>('users-cache', 'id', mockUsers);
|
|
466
|
-
|
|
467
|
-
return (
|
|
468
|
-
<CrudTable
|
|
469
|
-
title="Offline Users"
|
|
470
|
-
rowKey="id"
|
|
471
|
-
hookConfig={offlineCrud}
|
|
472
|
-
columns={userColumns}
|
|
473
|
-
/>
|
|
474
|
-
);
|
|
475
|
-
};
|
|
476
|
-
|
|
477
|
-
const RealtimeTable = () => {
|
|
478
|
-
const realtimeCrud = useRealtimeCrud<User>(
|
|
479
|
-
'id',
|
|
480
|
-
'wss://api.example.com/ws',
|
|
481
|
-
{ baseUrl: '/api/users' }
|
|
482
|
-
);
|
|
483
|
-
|
|
484
|
-
return (
|
|
485
|
-
<CrudTable
|
|
486
|
-
title="Realtime Users"
|
|
487
|
-
rowKey="id"
|
|
488
|
-
hookConfig={realtimeCrud}
|
|
489
|
-
columns={userColumns}
|
|
490
|
-
/>
|
|
491
|
-
);
|
|
492
|
-
};
|
|
193
|
+
hookConfig={{
|
|
194
|
+
operations: {
|
|
195
|
+
list: async (query) => ({ items: await db.find(query), total: await db.count() }),
|
|
196
|
+
create: async (draft) => db.insert(draft),
|
|
197
|
+
update: async (id, draft) => db.update(id, draft),
|
|
198
|
+
remove: async (id) => db.delete(id),
|
|
199
|
+
},
|
|
200
|
+
}}
|
|
493
201
|
```
|
|
494
202
|
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
- ๐ **Performance Optimized**: Caching, lazy loading, optimistic updates
|
|
514
|
-
- ๐๏ธ **Bulk Operations**: Select and delete multiple rows
|
|
515
|
-
- ๐ฏ **Custom Actions**: Add your own row-level actions
|
|
516
|
-
- ๐ **Advanced Search**: Configurable column-level search
|
|
517
|
-
- โจ **Enhanced Validation**: Complex form validation rules
|
|
518
|
-
|
|
519
|
-
---
|
|
520
|
-
|
|
521
|
-
## API Reference
|
|
522
|
-
|
|
523
|
-
### CrudTable Props
|
|
524
|
-
|
|
525
|
-
| Prop | Type | Description |
|
|
526
|
-
|------|------|-------------|
|
|
527
|
-
| `title` | `string` | Table header title |
|
|
528
|
-
| `rowKey` | `keyof T` | Unique identifier for each row |
|
|
529
|
-
| `columns` | `CrudColumn<T>[]` | Column definitions with enhanced features |
|
|
530
|
-
| `hookConfig` | `UseCrudTableConfig<T>` | Hook configuration for data operations |
|
|
531
|
-
| `defaultPageSize?` | `number` | Initial page size (default: 10) |
|
|
532
|
-
| `enableBulkOperations?` | `boolean` | Enable bulk select/delete (default: false) |
|
|
533
|
-
| `customActions?` | `(record, actions) => ReactNode[]` | Custom row actions |
|
|
534
|
-
|
|
535
|
-
### CrudColumn<T>
|
|
536
|
-
|
|
537
|
-
| Prop | Type | Description |
|
|
538
|
-
|------|------|-------------|
|
|
539
|
-
| `dataIndex` | `keyof T` | Field key in your data |
|
|
540
|
-
| `title` | `string` | Column header text |
|
|
541
|
-
| `fieldType` | `FieldType` | See the field type table below (default: `"string"`) |
|
|
542
|
-
| `fieldEditable?` | `boolean` | Whether field can be edited (default: true) |
|
|
543
|
-
| `searchable?` | `boolean` | Whether field appears in search (default: true) |
|
|
544
|
-
| `enumOptions?` | `Record<string, {text: string, color?: string}>` | Options for enum fields |
|
|
545
|
-
| `customRender?` | `(value, record) => ReactNode` | Custom display renderer |
|
|
546
|
-
| `formConfig?` | `FormConfig` | Form field configuration |
|
|
547
|
-
|
|
548
|
-
### Field Types
|
|
549
|
-
|
|
550
|
-
| `fieldType` | Stored as | Table cell | Form control |
|
|
551
|
-
|-------------|-----------|------------|--------------|
|
|
552
|
-
| `string` (default) | `string` | text | `Input` |
|
|
553
|
-
| `textarea` | `string` | ellipsised text | `Input.TextArea` |
|
|
554
|
-
| `email` | `string` | `mailto:` link (validated) | `Input` |
|
|
555
|
-
| `url` | `string` | link (validated) | `Input` |
|
|
556
|
-
| `password` | `string` | masked, excluded from search | `Input.Password` |
|
|
557
|
-
| `number` | `number` | localized number | `InputNumber` |
|
|
558
|
-
| `money` | `number` | currency (via ProTable intl) | `InputNumber` |
|
|
559
|
-
| `percent` | `number` | percentage | `InputNumber` 0โ100 |
|
|
560
|
-
| `rating` | `number` | stars | `Rate` |
|
|
561
|
-
| `progress` | `number` | progress bar | `InputNumber` 0โ100 |
|
|
562
|
-
| `date` | ISO string | `YYYY-MM-DD HH:mm` | `DatePicker` |
|
|
563
|
-
| `time` | `HH:mm:ss` string | time | `TimePicker` |
|
|
564
|
-
| `dateRange` | `[startISO, endISO]` | `start ~ end` | `RangePicker` |
|
|
565
|
-
| `boolean` | `boolean` | Yes/No tag | `Switch` |
|
|
566
|
-
| `enum` | `string` | colored tag | `Select` |
|
|
567
|
-
| `tags` | `string[]` | tag list | `Select mode="tags"` |
|
|
568
|
-
| `image` | URL string | 48px preview | `Input` (URL, validated) |
|
|
569
|
-
| `color` | hex string | swatch + code | `ColorPicker` |
|
|
570
|
-
| `json` | object | inline code | validated `Input.TextArea` |
|
|
571
|
-
| `custom` | anything | `customRender` | `formConfig.component` |
|
|
572
|
-
|
|
573
|
-
Every field type is one entry in the exported `fieldRegistry` (`lib/fields/registry.tsx`), declaring its cell render, form control, implied validation rules and recordโform value conversion in one place.
|
|
574
|
-
|
|
575
|
-
### FormConfig
|
|
576
|
-
|
|
577
|
-
| Prop | Type | Description |
|
|
578
|
-
|------|------|-------------|
|
|
579
|
-
| `required?` | `boolean` | Whether field is required |
|
|
580
|
-
| `rules?` | `FormRule[]` | Ant Design validation rules |
|
|
581
|
-
| `component?` | `ReactNode` | Custom form component |
|
|
582
|
-
| `transform?` | `(value) => any` | Transform value before saving |
|
|
583
|
-
|
|
584
|
-
### UseCrudTableConfig<T>
|
|
585
|
-
|
|
586
|
-
Choose one approach:
|
|
587
|
-
|
|
588
|
-
```tsx
|
|
589
|
-
// Static data approach
|
|
590
|
-
{
|
|
591
|
-
staticData: T[];
|
|
592
|
-
optimisticUpdates?: boolean;
|
|
593
|
-
}
|
|
594
|
-
|
|
595
|
-
// API approach
|
|
596
|
-
{
|
|
597
|
-
api: {
|
|
598
|
-
baseUrl: string;
|
|
599
|
-
endpoints?: {...};
|
|
600
|
-
headers?: Record<string, string>;
|
|
601
|
-
transform?: {...};
|
|
602
|
-
};
|
|
603
|
-
}
|
|
604
|
-
|
|
605
|
-
// Custom operations approach
|
|
606
|
-
{
|
|
607
|
-
operations: {
|
|
608
|
-
getList: (params) => Promise<{data: T[], total: number}>;
|
|
609
|
-
create: (data: Partial<T>) => Promise<T>;
|
|
610
|
-
update: (id, data: Partial<T>) => Promise<T>;
|
|
611
|
-
delete: (id) => Promise<void>;
|
|
203
|
+
Omitted operations fail with a message naming what is missing, rather than on
|
|
204
|
+
`undefined`. For full control, construct a `CrudDataSource` and pass it as
|
|
205
|
+
`dataSource`.
|
|
206
|
+
|
|
207
|
+
## Columns
|
|
208
|
+
|
|
209
|
+
```ts
|
|
210
|
+
interface CrudColumnFor<T, K extends keyof T> {
|
|
211
|
+
dataIndex: K; // must be a real key of T
|
|
212
|
+
title: string; // header, and the form label
|
|
213
|
+
fieldType?: FieldType; // defaults to 'string'
|
|
214
|
+
enumOptions?: Record<string, EnumOption>; // for 'enum'
|
|
215
|
+
customRender?: (value: T[K], record: T) => ReactNode;
|
|
216
|
+
formConfig?: {
|
|
217
|
+
required?: boolean;
|
|
218
|
+
component?: ReactNode; // replace the control entirely
|
|
219
|
+
transform?: (value: T[K]) => T[K]; // applied before writing
|
|
220
|
+
rules?: FormRule[]; // antd validation rules
|
|
612
221
|
};
|
|
222
|
+
fieldEditable?: boolean; // default true
|
|
223
|
+
searchable?: boolean; // default true
|
|
613
224
|
}
|
|
614
225
|
```
|
|
615
226
|
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
227
|
+
`CrudColumn<T>` is the union across every key of `T`, so an array annotated
|
|
228
|
+
`CrudColumn<User>[]` keeps each column's callbacks bound to its own property type.
|
|
229
|
+
Naming a property that does not exist on `T` is a compile error.
|
|
230
|
+
|
|
231
|
+
### Field types
|
|
232
|
+
|
|
233
|
+
| Type | Cell | Form control |
|
|
234
|
+
|---|---|---|
|
|
235
|
+
| `string` | text | `Input` |
|
|
236
|
+
| `textarea` | truncated text | `Input.TextArea` |
|
|
237
|
+
| `number` | locale-grouped | `InputNumber` |
|
|
238
|
+
| `money` | currency | `InputNumber` (2 dp) |
|
|
239
|
+
| `percent` | percentage | `InputNumber` with `%` |
|
|
240
|
+
| `boolean` | Yes/No tag | `Switch` |
|
|
241
|
+
| `enum` | coloured tag | `Select` |
|
|
242
|
+
| `date` | formatted datetime | `DatePicker` |
|
|
243
|
+
| `time` | time | `TimePicker` |
|
|
244
|
+
| `dateRange` | `start ~ end` | `RangePicker` |
|
|
245
|
+
| `email` | `mailto:` link | `Input` + email rule |
|
|
246
|
+
| `url` | external link | `Input` + url rule |
|
|
247
|
+
| `password` | `โขโขโขโขโขโขโขโข` | `Input.Password` |
|
|
248
|
+
| `rating` | `Rate` | `Rate` |
|
|
249
|
+
| `progress` | `Progress` | `InputNumber` |
|
|
250
|
+
| `tags` | tag list | tag `Select` |
|
|
251
|
+
| `image` | thumbnail | `Input` |
|
|
252
|
+
| `color` | swatch + hex | `ColorPicker` |
|
|
253
|
+
| `json` | inline code | monospace `TextArea` |
|
|
254
|
+
| `custom` | your `customRender` | your `formConfig.component` |
|
|
255
|
+
|
|
256
|
+
Each type is one entry in `fieldRegistry` declaring its renderer, control,
|
|
257
|
+
validation and `toFormValue`/`fromFormValue` conversion โ so a value survives the
|
|
258
|
+
round-trip into the edit form and back. Browse them all in the
|
|
259
|
+
[Storybook](https://maifeeulasad.github.io/antd-crud-table/storybook/).
|
|
260
|
+
|
|
261
|
+
**Security note:** `password` values are masked in the table and excluded from
|
|
262
|
+
exports. `url`, `email` and `image` render only `http`/`https` (plus `mailto:`)
|
|
263
|
+
targets โ a `javascript:` value renders as inert text rather than a clickable
|
|
264
|
+
link.
|
|
265
|
+
|
|
266
|
+
## Options
|
|
267
|
+
|
|
268
|
+
| Prop | Type | Default | Description |
|
|
269
|
+
|---|---|---|---|
|
|
270
|
+
| `title` | `string` | โ | Header, and the export filename |
|
|
271
|
+
| `rowKey` | `K` | โ | Identity property |
|
|
272
|
+
| `columns` | `CrudColumn<T>[]` | โ | Column definitions |
|
|
273
|
+
| `hookConfig` | `UseCrudTableOptions<T, K>` | โ | Data strategy |
|
|
274
|
+
| `defaultPageSize` | `number` | `10` | Rows per page |
|
|
275
|
+
| `enableBulkOperations` | `boolean` | `false` | Row selection and bulk delete |
|
|
276
|
+
| `enableColumnSettings` | `boolean` | `true` | Column visibility and density |
|
|
277
|
+
| `enableExport` | `boolean` | `true` | Export menu entries |
|
|
278
|
+
| `exportScope` | `'all' \| 'page'` | `'all'` | Whole result set, or visible rows |
|
|
279
|
+
| `enableImport` | `boolean` | `false` | Import menu entry (CSV / `.xls` / `.xlsx`) |
|
|
280
|
+
| `importConcurrency` | `number` | `5` | Max simultaneous creates during import |
|
|
281
|
+
| `customActions` | `(record, actions) => ReactNode[]` | โ | Extra row controls |
|
|
282
|
+
| `locale` | `PartialCrudTableLocale` | English | String overrides |
|
|
283
|
+
|
|
284
|
+
## Localization
|
|
285
|
+
|
|
286
|
+
The table follows the antd `ConfigProvider` around it, so setting your app locale
|
|
287
|
+
once localizes pagination, date pickers, empty states and the ProTable chrome:
|
|
619
288
|
|
|
620
|
-
|
|
289
|
+
```tsx
|
|
290
|
+
import { ConfigProvider } from 'antd';
|
|
291
|
+
import frFR from 'antd/locale/fr_FR';
|
|
621
292
|
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
}
|
|
293
|
+
<ConfigProvider locale={frFR}>
|
|
294
|
+
<CrudTable {...config} />
|
|
295
|
+
</ConfigProvider>
|
|
626
296
|
```
|
|
627
297
|
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
## ๐ Notes
|
|
631
|
-
|
|
632
|
-
- Date fields are handled via `dayjs` (already required by antd) in both the form and display.
|
|
633
|
-
- All requests are async with error handling via `antd`'s `message` API.
|
|
634
|
-
- Add your own export logic or additional toolbar buttons as needed.
|
|
298
|
+
**With no `ConfigProvider` in the tree the table supplies English itself** โ antd
|
|
299
|
+
components otherwise fall back to their own built-in defaults.
|
|
635
300
|
|
|
636
|
-
|
|
301
|
+
The library's own wording comes from the `locale` prop. Supply only what you want
|
|
302
|
+
to change; anything omitted stays English:
|
|
637
303
|
|
|
638
|
-
## ๐จ Column Type Examples
|
|
639
|
-
|
|
640
|
-
### String Field
|
|
641
304
|
```tsx
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
}
|
|
652
|
-
|
|
305
|
+
<CrudTable
|
|
306
|
+
locale={{
|
|
307
|
+
actions: 'Aktionen',
|
|
308
|
+
edit: 'Bearbeiten',
|
|
309
|
+
delete: 'Lรถschen',
|
|
310
|
+
create: 'Neu',
|
|
311
|
+
confirmDeleteTitle: 'Wirklich lรถschen?',
|
|
312
|
+
deleteSelected: (count) => `${count} entfernen`,
|
|
313
|
+
}}
|
|
314
|
+
{...config}
|
|
315
|
+
/>
|
|
653
316
|
```
|
|
654
317
|
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
dataIndex: 'age',
|
|
659
|
-
title: 'Age',
|
|
660
|
-
fieldType: 'number',
|
|
661
|
-
formConfig: {
|
|
662
|
-
rules: [
|
|
663
|
-
{ type: 'number', min: 0, max: 120, message: 'Invalid age' }
|
|
664
|
-
]
|
|
665
|
-
},
|
|
666
|
-
}
|
|
667
|
-
```
|
|
318
|
+
Interpolated strings are functions rather than templates with placeholders, so a
|
|
319
|
+
translation cannot silently drop a value or reorder its arguments. The full
|
|
320
|
+
contract is `CrudTableLocale`; `enUS` is the exported default.
|
|
668
321
|
|
|
669
|
-
|
|
670
|
-
```tsx
|
|
671
|
-
{
|
|
672
|
-
dataIndex: 'createdAt',
|
|
673
|
-
title: 'Created Date',
|
|
674
|
-
fieldType: 'date',
|
|
675
|
-
searchable: false, // Exclude from search
|
|
676
|
-
}
|
|
677
|
-
```
|
|
322
|
+
## Export
|
|
678
323
|
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
title: 'Active Status',
|
|
684
|
-
fieldType: 'boolean',
|
|
685
|
-
}
|
|
686
|
-
```
|
|
324
|
+
The toolbar menu writes **the whole filtered result set**, not just the visible
|
|
325
|
+
page, using the data source's `listAll`. The labels state which they will do โ
|
|
326
|
+
`Export all as CSV` or `Export page as CSV` when the source cannot list without
|
|
327
|
+
pagination. Set `exportScope: 'page'` to opt out.
|
|
687
328
|
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
fieldType: 'enum',
|
|
694
|
-
enumOptions: {
|
|
695
|
-
active: { text: 'Active', color: 'green' },
|
|
696
|
-
pending: { text: 'Pending', color: 'orange' },
|
|
697
|
-
inactive: { text: 'Inactive', color: 'red' },
|
|
698
|
-
},
|
|
699
|
-
}
|
|
700
|
-
```
|
|
329
|
+
| Format | Output |
|
|
330
|
+
|---|---|
|
|
331
|
+
| `csv` | `.csv`, formula-injection safe |
|
|
332
|
+
| `json` | `.json`, the raw records |
|
|
333
|
+
| `excel` | `.xls`, Excel 2003 SpreadsheetML |
|
|
701
334
|
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
title: 'Custom Display',
|
|
707
|
-
fieldType: 'custom',
|
|
708
|
-
customRender: (value, record) => (
|
|
709
|
-
<div>
|
|
710
|
-
<Avatar src={record.avatar} />
|
|
711
|
-
<span>{record.name}</span>
|
|
712
|
-
</div>
|
|
713
|
-
),
|
|
714
|
-
formConfig: {
|
|
715
|
-
component: <MyCustomInput />,
|
|
716
|
-
},
|
|
717
|
-
}
|
|
718
|
-
```
|
|
335
|
+
Cells beginning `=`, `+`, `-`, `@`, tab or CR are prefixed so spreadsheets treat
|
|
336
|
+
them as text. Quoting alone does not prevent evaluation, and row content in a CRUD
|
|
337
|
+
table is exactly the untrusted input an attacker controls. Genuine numbers are
|
|
338
|
+
left alone.
|
|
719
339
|
|
|
720
|
-
|
|
340
|
+
> **Excel note:** `excel` emits SpreadsheetML in a `.xls` file rather than OOXML,
|
|
341
|
+
> which would mean taking on a ZIP implementation. Excel 2016+ shows a
|
|
342
|
+
> format/extension mismatch prompt on open; the file is intact. Use CSV if you
|
|
343
|
+
> need a prompt-free export.
|
|
721
344
|
|
|
722
|
-
##
|
|
345
|
+
## Import
|
|
723
346
|
|
|
724
|
-
|
|
347
|
+
Set `enableImport` to add an **Import** entry to the toolbar menu. It opens a
|
|
348
|
+
dialog that walks a file in: pick it, map its headers onto columns (auto-matched
|
|
349
|
+
on header text, and editable), preview the parsed rows with **per-row validation**
|
|
350
|
+
using the same `formConfig.rules` the create form uses, then create the valid rows.
|
|
725
351
|
|
|
726
352
|
```tsx
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
const { result } = renderHook(() =>
|
|
736
|
-
useCrudTable('id', {
|
|
737
|
-
staticData: mockData,
|
|
738
|
-
})
|
|
739
|
-
);
|
|
740
|
-
|
|
741
|
-
await act(async () => {
|
|
742
|
-
const created = await result.current.create({
|
|
743
|
-
name: 'Jane',
|
|
744
|
-
age: 25
|
|
745
|
-
});
|
|
746
|
-
expect(created).toBeTruthy();
|
|
747
|
-
});
|
|
748
|
-
|
|
749
|
-
expect(result.current.state.data).toHaveLength(2);
|
|
750
|
-
});
|
|
353
|
+
<CrudTable<User, 'id'>
|
|
354
|
+
title="Users"
|
|
355
|
+
rowKey="id"
|
|
356
|
+
columns={columns}
|
|
357
|
+
hookConfig={{ storageKey: 'users' }}
|
|
358
|
+
enableImport
|
|
359
|
+
/>
|
|
751
360
|
```
|
|
752
361
|
|
|
753
|
-
|
|
362
|
+
| Format | Read via |
|
|
363
|
+
|---|---|
|
|
364
|
+
| `.csv` | built-in RFC 4180 parser, no dependency |
|
|
365
|
+
| `.xls` | the Excel 2003 SpreadsheetML this library exports, parsed as plain XML |
|
|
366
|
+
| `.xlsx` | real OOXML, via [`to-spreadsheet`](https://github.com/maifeeulasad/to-spreadsheet)'s `readExcel` |
|
|
754
367
|
|
|
755
|
-
|
|
368
|
+
Behaviour worth knowing:
|
|
756
369
|
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
370
|
+
- **Values round-trip.** Export then import reproduces the original records. Enum
|
|
371
|
+
columns accept both the stored key and the exported label.
|
|
372
|
+
- **Partial success is honest.** Rows failing validation are skipped and reported
|
|
373
|
+
per row; a row that fails to create does not abort the rest.
|
|
374
|
+
- **Bounded concurrency.** A large file does not fire one request per row. A data
|
|
375
|
+
source may implement the optional `createMany(drafts)` to create server-side in
|
|
376
|
+
a batch; otherwise creates run through a small pool (`importConcurrency`, default
|
|
377
|
+
5).
|
|
378
|
+
- **Passwords are never imported**, the same way they are never exported.
|
|
762
379
|
|
|
763
|
-
|
|
764
|
-
```tsx
|
|
765
|
-
// For better UX with reliable backends
|
|
766
|
-
hookConfig={{
|
|
767
|
-
api: {...},
|
|
768
|
-
optimisticUpdates: true
|
|
769
|
-
}}
|
|
770
|
-
```
|
|
380
|
+
## Using the hook directly
|
|
771
381
|
|
|
772
|
-
|
|
773
|
-
```tsx
|
|
774
|
-
// Custom hook with caching
|
|
775
|
-
const useUserCrud = () => {
|
|
776
|
-
return useCrudTable('id', {
|
|
777
|
-
enableCache: true,
|
|
778
|
-
// ... other config
|
|
779
|
-
});
|
|
780
|
-
};
|
|
781
|
-
```
|
|
382
|
+
`useCrudTable` works without the table when you want your own UI:
|
|
782
383
|
|
|
783
|
-
### 4. **Lazy Load Components**
|
|
784
384
|
```tsx
|
|
785
|
-
|
|
786
|
-
// Component will be loaded when needed
|
|
787
|
-
```
|
|
788
|
-
|
|
789
|
-
---
|
|
790
|
-
|
|
791
|
-
## ๐ฎ Roadmap
|
|
385
|
+
const crud = useCrudTable<User, 'id'>('id', { storageKey: 'users' });
|
|
792
386
|
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
- ๐ **Advanced Filters**: Complex filtering UI
|
|
799
|
-
- ๐ฑ **Mobile Optimization**: Better mobile experience
|
|
387
|
+
crud.state; // { loading, error, data, total, page, pageSize }
|
|
388
|
+
await crud.create({ name: 'Ada' });
|
|
389
|
+
await crud.update(1, { name: 'Ada L.' });
|
|
390
|
+
await crud.remove(1);
|
|
391
|
+
```
|
|
800
392
|
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
- ๐ **Analytics Integration**: Built-in tracking
|
|
804
|
-
- ๐ **i18n Support**: Multi-language support
|
|
393
|
+
`onSuccess` and `onError` fire for every operation โ including list failures, with
|
|
394
|
+
the original `Error` rather than a flattened string.
|
|
805
395
|
|
|
806
|
-
|
|
396
|
+
## Documentation
|
|
807
397
|
|
|
808
|
-
|
|
398
|
+
| | |
|
|
399
|
+
|---|---|
|
|
400
|
+
| [Live demo](https://maifeeulasad.github.io/antd-crud-table/) | Every strategy, running |
|
|
401
|
+
| [Storybook](https://maifeeulasad.github.io/antd-crud-table/storybook/) | One story per field type |
|
|
402
|
+
| [API reference](https://maifeeulasad.github.io/antd-crud-table/api/) | Generated from the types |
|
|
403
|
+
| [REST recipes](./docs/rest-recipes.md) | Non-default API dialects |
|
|
404
|
+
| [Migration guide](./MIGRATION.md) | Upgrading from 0.5.x |
|
|
405
|
+
| [Changelog](./CHANGELOG.md) | Release history |
|
|
809
406
|
|
|
810
|
-
|
|
407
|
+
## Development
|
|
811
408
|
|
|
812
|
-
### Development Setup
|
|
813
409
|
```bash
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
410
|
+
pnpm install
|
|
411
|
+
pnpm dev # demo app
|
|
412
|
+
pnpm storybook # component playground
|
|
413
|
+
pnpm test # unit and component tests
|
|
414
|
+
pnpm test:coverage # with enforced thresholds
|
|
415
|
+
pnpm lint
|
|
416
|
+
pnpm build:lib # the published package
|
|
417
|
+
pnpm build:site # demo + storybook + api reference
|
|
818
418
|
```
|
|
819
419
|
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
## ๐ License
|
|
823
|
-
|
|
824
|
-
MIT License - feel free to use in personal and commercial projects.
|
|
825
|
-
|
|
826
|
-
---
|
|
827
|
-
|
|
828
|
-
๐ Build elegant CRUD interfaces faster than ever with `antd-crud-table`!
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
---
|
|
420
|
+
Contributions are welcome. Tests and lint run on every pull request against both
|
|
421
|
+
React 18 and 19.
|
|
832
422
|
|
|
833
|
-
##
|
|
423
|
+
## License
|
|
834
424
|
|
|
835
|
-
|
|
836
|
-
- GitHub: https://github.com/maifeeulasad/antd-crud-table/
|
|
837
|
-
- GitHub page (Live Demo): https://maifeeulasad.github.io/antd-crud-table/
|
|
425
|
+
[MIT](./LICENSE) ยฉ [Maifee Ul Asad](https://github.com/maifeeulasad)
|