next-data-kit 1.0.1 → 3.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 +224 -137
- package/dist/client/components/data-kit-table.d.ts.map +1 -1
- package/dist/client/components/data-kit-table.js +1 -1
- package/dist/client/components/data-kit-table.js.map +1 -1
- package/dist/client/hooks/useDataKit.d.ts.map +1 -1
- package/dist/client/hooks/useDataKit.js +21 -19
- package/dist/client/hooks/useDataKit.js.map +1 -1
- package/dist/index.cjs +77 -57
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +5 -2
- package/dist/index.d.ts +5 -2
- package/dist/index.js +77 -57
- package/dist/index.js.map +1 -1
- package/dist/{next-data-kit-DmZ7pNHV.d.cts → next-data-kit-DBl9PPWh.d.cts} +2 -2
- package/dist/{next-data-kit-DmZ7pNHV.d.ts → next-data-kit-DBl9PPWh.d.ts} +2 -2
- package/dist/server.cjs +71 -39
- package/dist/server.cjs.map +1 -1
- package/dist/server.d.cts +19 -13
- package/dist/server.d.ts +19 -13
- package/dist/server.js +71 -39
- package/dist/server.js.map +1 -1
- package/dist/types/index.cjs.map +1 -1
- package/dist/types/index.d.cts +2 -2
- package/dist/types/next-data-kit.d.ts +2 -2
- package/dist/types/next-data-kit.d.ts.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -4,14 +4,14 @@ A powerful table utility for server-side pagination, filtering, and sorting with
|
|
|
4
4
|
|
|
5
5
|
## Features
|
|
6
6
|
|
|
7
|
-
-
|
|
8
|
-
-
|
|
9
|
-
-
|
|
10
|
-
-
|
|
11
|
-
-
|
|
12
|
-
-
|
|
13
|
-
-
|
|
14
|
-
-
|
|
7
|
+
- 🚀 **Server-side pagination** - Efficient data fetching with page-based navigation
|
|
8
|
+
- 🔍 **Flexible filtering** - Support for regex, exact match, and custom filters
|
|
9
|
+
- 📊 **Multi-column sorting** - Sort by multiple columns with customizable order
|
|
10
|
+
- ⚛️ **React hooks** - `useDataKit`, `useSelection`, `usePagination` for state management
|
|
11
|
+
- 🎨 **Components** - `DataKitTable` for tables, `DataKit` for custom layouts
|
|
12
|
+
- 📝 **TypeScript** - Fully typed with generics support
|
|
13
|
+
- 🔌 **Framework agnostic** - Works with any database ORM/ODM (Mongoose, Prisma, etc.)
|
|
14
|
+
- 📦 **Tree-shakeable** - Import only what you need
|
|
15
15
|
|
|
16
16
|
## Installation
|
|
17
17
|
|
|
@@ -28,17 +28,17 @@ pnpm add next-data-kit
|
|
|
28
28
|
### Server-side (Next.js Server Action)
|
|
29
29
|
|
|
30
30
|
```typescript
|
|
31
|
-
|
|
31
|
+
'use server';
|
|
32
32
|
|
|
33
|
-
import { dataKitServerAction, createSearchFilter } from
|
|
34
|
-
import type { TDataKitInput } from
|
|
35
|
-
import UserModel from
|
|
33
|
+
import { dataKitServerAction, createSearchFilter } from 'next-data-kit/server';
|
|
34
|
+
import type { TDataKitInput } from 'next-data-kit/types';
|
|
35
|
+
import UserModel from '@/models/User';
|
|
36
36
|
|
|
37
37
|
export async function fetchUsers(input: TDataKitInput) {
|
|
38
38
|
return dataKitServerAction({
|
|
39
39
|
model: UserModel,
|
|
40
40
|
input,
|
|
41
|
-
item: async
|
|
41
|
+
item: async user => ({
|
|
42
42
|
id: user._id.toString(),
|
|
43
43
|
name: user.name,
|
|
44
44
|
email: user.email,
|
|
@@ -47,36 +47,41 @@ export async function fetchUsers(input: TDataKitInput) {
|
|
|
47
47
|
active: true,
|
|
48
48
|
}),
|
|
49
49
|
filterCustom: {
|
|
50
|
-
search: createSearchFilter([
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
50
|
+
search: createSearchFilter(['name', 'email']),
|
|
51
|
+
age: value => ({
|
|
52
|
+
age: {
|
|
53
|
+
$gte: value,
|
|
54
|
+
},
|
|
55
|
+
}),
|
|
56
56
|
},
|
|
57
|
+
// Only 'search' and 'age' filters are allowed (auto-extracted from filterCustom keys)
|
|
57
58
|
});
|
|
58
59
|
}
|
|
59
60
|
```
|
|
60
61
|
|
|
61
|
-
|
|
62
62
|
### Input Validation (Optional)
|
|
63
63
|
|
|
64
64
|
You can use the built-in Zod schema to validate inputs before processing:
|
|
65
65
|
|
|
66
66
|
```typescript
|
|
67
|
-
|
|
67
|
+
'use server';
|
|
68
68
|
|
|
69
|
-
import { dataKitServerAction, dataKitSchemaZod } from
|
|
69
|
+
import { dataKitServerAction, dataKitSchemaZod } from 'next-data-kit/server';
|
|
70
70
|
|
|
71
71
|
export async function fetchUsers(input: unknown) {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
72
|
+
// Validate input
|
|
73
|
+
const parsedInput = dataKitSchemaZod.parse(input);
|
|
74
|
+
|
|
75
|
+
return dataKitServerAction({
|
|
76
|
+
model: UserModel,
|
|
77
|
+
input: parsedInput,
|
|
78
|
+
item: user => ({ id: user._id.toString(), name: user.name }),
|
|
79
|
+
filterCustom: {
|
|
80
|
+
search: value => ({ name: { $regex: value, $options: 'i' } }),
|
|
81
|
+
role: value => ({ role: value }),
|
|
82
|
+
},
|
|
83
|
+
// Only 'search' and 'role' filters are allowed
|
|
84
|
+
});
|
|
80
85
|
}
|
|
81
86
|
```
|
|
82
87
|
|
|
@@ -85,10 +90,10 @@ export async function fetchUsers(input: unknown) {
|
|
|
85
90
|
Ready-to-use table with built-in filtering, sorting, and selection:
|
|
86
91
|
|
|
87
92
|
```tsx
|
|
88
|
-
|
|
93
|
+
'use client';
|
|
89
94
|
|
|
90
|
-
import { DataKitTable } from
|
|
91
|
-
import { fetchUsers } from
|
|
95
|
+
import { DataKitTable } from 'next-data-kit/client';
|
|
96
|
+
import { fetchUsers } from '@/actions/users';
|
|
92
97
|
|
|
93
98
|
export function UsersTable() {
|
|
94
99
|
return (
|
|
@@ -96,14 +101,14 @@ export function UsersTable() {
|
|
|
96
101
|
action={fetchUsers}
|
|
97
102
|
limit={{ default: 10 }}
|
|
98
103
|
filters={[
|
|
99
|
-
{ id:
|
|
104
|
+
{ id: 'search', label: 'Search', type: 'TEXT', placeholder: 'Search...' },
|
|
100
105
|
{
|
|
101
|
-
id:
|
|
102
|
-
label:
|
|
103
|
-
type:
|
|
106
|
+
id: 'role',
|
|
107
|
+
label: 'Role',
|
|
108
|
+
type: 'SELECT',
|
|
104
109
|
dataset: [
|
|
105
|
-
{ id:
|
|
106
|
-
{ id:
|
|
110
|
+
{ id: 'admin', name: 'admin', label: 'Admin' },
|
|
111
|
+
{ id: 'user', name: 'user', label: 'User' },
|
|
107
112
|
],
|
|
108
113
|
},
|
|
109
114
|
]}
|
|
@@ -111,9 +116,9 @@ export function UsersTable() {
|
|
|
111
116
|
enabled: true,
|
|
112
117
|
actions: {
|
|
113
118
|
delete: {
|
|
114
|
-
name:
|
|
115
|
-
function: async
|
|
116
|
-
await deleteUsers(items.map(
|
|
119
|
+
name: 'Delete Selected',
|
|
120
|
+
function: async items => {
|
|
121
|
+
await deleteUsers(items.map(i => i.id));
|
|
117
122
|
return [true, { deselectAll: true }];
|
|
118
123
|
},
|
|
119
124
|
},
|
|
@@ -123,7 +128,7 @@ export function UsersTable() {
|
|
|
123
128
|
{
|
|
124
129
|
head: <DataKitTable.Head>Name</DataKitTable.Head>,
|
|
125
130
|
body: ({ item }) => <DataKitTable.Cell>{item.name}</DataKitTable.Cell>,
|
|
126
|
-
sortable: { path:
|
|
131
|
+
sortable: { path: 'name', default: 0 },
|
|
127
132
|
},
|
|
128
133
|
{
|
|
129
134
|
head: <DataKitTable.Head>Email</DataKitTable.Head>,
|
|
@@ -140,22 +145,18 @@ export function UsersTable() {
|
|
|
140
145
|
Use `DataKit` for grids, cards, or any custom layout. It provides toolbar/pagination but lets you render content:
|
|
141
146
|
|
|
142
147
|
```tsx
|
|
143
|
-
|
|
148
|
+
'use client';
|
|
144
149
|
|
|
145
|
-
import { DataKit } from
|
|
146
|
-
import { fetchUsers } from
|
|
150
|
+
import { DataKit } from 'next-data-kit/client';
|
|
151
|
+
import { fetchUsers } from '@/actions/users';
|
|
147
152
|
|
|
148
153
|
export function UsersGrid() {
|
|
149
154
|
return (
|
|
150
|
-
<DataKit
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
{(dataKit) => (
|
|
156
|
-
<div className="grid grid-cols-4 gap-4">
|
|
157
|
-
{dataKit.items.map((user) => (
|
|
158
|
-
<div key={user.id} className="rounded-lg border p-4">
|
|
155
|
+
<DataKit action={fetchUsers} limit={{ default: 12 }} filters={[{ id: 'search', label: 'Search', type: 'TEXT' }]}>
|
|
156
|
+
{dataKit => (
|
|
157
|
+
<div className='grid grid-cols-4 gap-4'>
|
|
158
|
+
{dataKit.items.map(user => (
|
|
159
|
+
<div key={user.id} className='rounded-lg border p-4'>
|
|
159
160
|
<h3>{user.name}</h3>
|
|
160
161
|
<p>{user.email}</p>
|
|
161
162
|
</div>
|
|
@@ -171,10 +172,12 @@ export function UsersGrid() {
|
|
|
171
172
|
|
|
172
173
|
```tsx
|
|
173
174
|
<DataKit action={fetchUsers} manual>
|
|
174
|
-
{
|
|
175
|
+
{dataKit => (
|
|
175
176
|
<>
|
|
176
177
|
{dataKit.state.isLoading && <Spinner />}
|
|
177
|
-
{dataKit.items.map(
|
|
178
|
+
{dataKit.items.map(user => (
|
|
179
|
+
<Card key={user.id} user={user} />
|
|
180
|
+
))}
|
|
178
181
|
</>
|
|
179
182
|
)}
|
|
180
183
|
</DataKit>
|
|
@@ -185,10 +188,10 @@ export function UsersGrid() {
|
|
|
185
188
|
For fully custom implementations:
|
|
186
189
|
|
|
187
190
|
```tsx
|
|
188
|
-
|
|
191
|
+
'use client';
|
|
189
192
|
|
|
190
|
-
import { useDataKit } from
|
|
191
|
-
import { fetchUsers } from
|
|
193
|
+
import { useDataKit } from 'next-data-kit/client';
|
|
194
|
+
import { fetchUsers } from '@/actions/users';
|
|
192
195
|
|
|
193
196
|
export function UsersTable() {
|
|
194
197
|
const {
|
|
@@ -206,7 +209,7 @@ export function UsersTable() {
|
|
|
206
209
|
|
|
207
210
|
return (
|
|
208
211
|
<div>
|
|
209
|
-
<input placeholder=
|
|
212
|
+
<input placeholder='Search...' onChange={e => setFilter('search', e.target.value)} />
|
|
210
213
|
|
|
211
214
|
{isLoading ? (
|
|
212
215
|
<p>Loading...</p>
|
|
@@ -214,12 +217,12 @@ export function UsersTable() {
|
|
|
214
217
|
<table>
|
|
215
218
|
<thead>
|
|
216
219
|
<tr>
|
|
217
|
-
<th onClick={() => setSort(
|
|
218
|
-
<th onClick={() => setSort(
|
|
220
|
+
<th onClick={() => setSort('name', 1)}>Name</th>
|
|
221
|
+
<th onClick={() => setSort('email', 1)}>Email</th>
|
|
219
222
|
</tr>
|
|
220
223
|
</thead>
|
|
221
224
|
<tbody>
|
|
222
|
-
{items.map(
|
|
225
|
+
{items.map(user => (
|
|
223
226
|
<tr key={user.id}>
|
|
224
227
|
<td>{user.name}</td>
|
|
225
228
|
<td>{user.email}</td>
|
|
@@ -253,18 +256,101 @@ type TDataKitServerActionOptions<T, R> = {
|
|
|
253
256
|
model: TMongoModel<T>;
|
|
254
257
|
item: (item: T) => Promise<R> | R;
|
|
255
258
|
filter?: (filterInput?: Record<string, unknown>) => TMongoFilterQuery<T>;
|
|
259
|
+
// ** Custom filter configuration (defines allowed filter keys)
|
|
256
260
|
filterCustom?: TFilterCustomConfigWithFilter<T, TMongoFilterQuery<T>>;
|
|
257
261
|
defaultSort?: TSortOptions<T>;
|
|
262
|
+
// ** Maximum limit per page (default: 100)
|
|
263
|
+
maxLimit?: number;
|
|
264
|
+
// ** Whitelist of allowed query fields
|
|
265
|
+
queryAllowed?: string[];
|
|
258
266
|
};
|
|
259
267
|
```
|
|
260
268
|
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
// ... inside dataKitServerAction options
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
### Understanding `filter` vs `query`
|
|
277
|
+
|
|
278
|
+
There are two ways data reaches your database:
|
|
279
|
+
|
|
280
|
+
1. **`filter` (via `filterCustom`)** - For user-facing filters with transformations
|
|
281
|
+
- Client-side `filters` prop → sends values to `filter` parameter
|
|
282
|
+
- Server validates against `filterCustom` keys
|
|
283
|
+
- You define how values transform into database queries
|
|
284
|
+
- **Use for**: search boxes, dropdowns, date ranges, etc.
|
|
285
|
+
|
|
286
|
+
2. **`query` (via `queryAllowed`)** - For direct field matching
|
|
287
|
+
- Direct database field equality checks
|
|
288
|
+
- Must explicitly whitelist with `queryAllowed`
|
|
289
|
+
- **Use for**: fixed filters like `{ active: true }`, user-specific queries
|
|
290
|
+
|
|
291
|
+
```typescript
|
|
292
|
+
dataKitServerAction({
|
|
293
|
+
model: UserModel,
|
|
294
|
+
input,
|
|
295
|
+
item: u => u,
|
|
296
|
+
// Client filters go through filterCustom
|
|
297
|
+
filterCustom: {
|
|
298
|
+
search: createSearchFilter(['name', 'email']),
|
|
299
|
+
role: value => ({ role: value }),
|
|
300
|
+
},
|
|
301
|
+
// Direct queries need explicit whitelist
|
|
302
|
+
queryAllowed: ['organizationId', 'active'],
|
|
303
|
+
});
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
### Security Note: Strict Mode by Default
|
|
307
|
+
|
|
308
|
+
**Filter Security**: When you define `filterCustom`, ONLY those keys are allowed. Any other filter key from the client will **THROW AN ERROR**.
|
|
309
|
+
|
|
310
|
+
**Query Security**: When you provide `queryAllowed`, only those query fields are accepted. Any other query field will throw an error.
|
|
311
|
+
|
|
312
|
+
````typescript
|
|
313
|
+
// Strict Security Example
|
|
314
|
+
dataKitServerAction({
|
|
315
|
+
model: UserModel,
|
|
316
|
+
input,
|
|
317
|
+
item: u => ({ id: u._id.toString(), name: u.name }),
|
|
318
|
+
filterCustom: {
|
|
319
|
+
name: value => ({ name: { $regex: value, $options: 'i' } }),
|
|
320
|
+
email: value => ({ email: { $regex: value, $options: 'i' } }),
|
|
321
|
+
role: value => ({ role: value }),
|
|
322
|
+
},
|
|
323
|
+
// ONLY 'name', 'email', and 'role' filters are allowed
|
|
324
|
+
// If client sends { filter: { secret: "true" } }, this WILL THROW an Error!
|
|
325
|
+
|
|
326
|
+
// Query params need explicit whitelist
|
|
327
|
+
queryAllowed: ['status'],
|
|
328
|
+
});
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
### Error Handling on Client
|
|
332
|
+
|
|
333
|
+
When the server action throws an error (e.g., security violation), the client automatically handles it:
|
|
334
|
+
|
|
335
|
+
**`DataKitTable`**: Displays error in red within the table body
|
|
336
|
+
|
|
337
|
+
**`useDataKit`**: Error available in `state.error`
|
|
338
|
+
|
|
339
|
+
```tsx
|
|
340
|
+
const { state: { error } } = useDataKit({ action: fetchUsers });
|
|
341
|
+
|
|
342
|
+
if (error) {
|
|
343
|
+
return <div className="text-red-500">Error: {error.message}</div>;
|
|
344
|
+
}
|
|
345
|
+
```
|
|
346
|
+
|
|
261
347
|
#### `createSearchFilter(fields)`
|
|
262
348
|
|
|
263
|
-
Create a search filter for multiple fields.
|
|
349
|
+
Create a search filter for multiple fields. Use this in `filterCustom`.
|
|
264
350
|
|
|
265
351
|
```typescript
|
|
266
352
|
filterCustom: {
|
|
267
|
-
search: createSearchFilter([
|
|
353
|
+
search: createSearchFilter(['name', 'email', 'phone']),
|
|
268
354
|
}
|
|
269
355
|
```
|
|
270
356
|
|
|
@@ -296,38 +382,39 @@ filterCustom: {
|
|
|
296
382
|
priceRange: (value: { min: number; max: number }) => ({
|
|
297
383
|
price: { $gte: value.min, $lte: value.max },
|
|
298
384
|
}),
|
|
299
|
-
}
|
|
385
|
+
},
|
|
386
|
+
// Only 'search' and 'priceRange' filters are allowed from client
|
|
300
387
|
```
|
|
301
388
|
|
|
302
389
|
#### Understanding `filterCustom` Flow
|
|
303
390
|
|
|
304
391
|
To use custom filters effectively, you must match the **Key** on the client with the **Key** on the server.
|
|
305
392
|
|
|
306
|
-
1.
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
393
|
+
1. **Client-side**: Define a filter with a specific `id` (e.g., `'priceRange'`).
|
|
394
|
+
|
|
395
|
+
```tsx
|
|
396
|
+
// Client Component
|
|
397
|
+
<DataKitTable
|
|
398
|
+
filters={[{ id: 'priceRange', label: 'Price Range', type: 'TEXT' }]}
|
|
399
|
+
// ...
|
|
400
|
+
/>
|
|
401
|
+
```
|
|
402
|
+
|
|
403
|
+
_Note: When use interact with this filter, `DataKit` sends `{ filter: { priceRange: "value" } }` to the server._
|
|
404
|
+
|
|
405
|
+
2. **Server-side**: Handle that key in `filterCustom`.
|
|
406
|
+
|
|
407
|
+
```typescript
|
|
408
|
+
// Server Action
|
|
409
|
+
filterCustom: {
|
|
410
|
+
// MATCHES 'priceRange' FROM CLIENT
|
|
411
|
+
priceRange: value => ({
|
|
412
|
+
price: { $lte: Number(value) },
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
The `filterCustom` function intercepts the value sent from the client before it hits the database query builder, allowing you to transform simple values into complex queries.
|
|
331
418
|
|
|
332
419
|
**Client Usage:**
|
|
333
420
|
|
|
@@ -339,10 +426,10 @@ const {
|
|
|
339
426
|
});
|
|
340
427
|
|
|
341
428
|
// Trigger the manual search
|
|
342
|
-
setFilter(
|
|
429
|
+
setFilter('search', 'query string');
|
|
343
430
|
|
|
344
431
|
// Trigger the range filter
|
|
345
|
-
setFilter(
|
|
432
|
+
setFilter('priceRange', { min: 10, max: 100 });
|
|
346
433
|
```
|
|
347
434
|
|
|
348
435
|
### Client
|
|
@@ -351,29 +438,29 @@ setFilter("priceRange", { min: 10, max: 100 });
|
|
|
351
438
|
|
|
352
439
|
Full-featured table component with built-in UI.
|
|
353
440
|
|
|
354
|
-
| Prop
|
|
355
|
-
|
|
356
|
-
| `action`
|
|
357
|
-
| `table`
|
|
358
|
-
| `filters`
|
|
359
|
-
| `selectable`
|
|
360
|
-
| `limit`
|
|
361
|
-
| `controller`
|
|
362
|
-
| `className`
|
|
363
|
-
| `bordered`
|
|
364
|
-
| `refetchInterval` | `number`
|
|
441
|
+
| Prop | Type | Description |
|
|
442
|
+
| ----------------- | ---------------------------- | -------------------------- |
|
|
443
|
+
| `action` | `(input) => Promise<Result>` | Server action function |
|
|
444
|
+
| `table` | `Column[]` | Column definitions |
|
|
445
|
+
| `filters` | `FilterItem[]` | Filter configurations |
|
|
446
|
+
| `selectable` | `{ enabled, actions? }` | Selection & bulk actions |
|
|
447
|
+
| `limit` | `{ default: number }` | Items per page |
|
|
448
|
+
| `controller` | `Ref<Controller>` | External control ref |
|
|
449
|
+
| `className` | `string` | Container class |
|
|
450
|
+
| `bordered` | `boolean \| 'rounded'` | Border style |
|
|
451
|
+
| `refetchInterval` | `number` | Auto-refresh interval (ms) |
|
|
365
452
|
|
|
366
453
|
#### `<DataKit>` Component
|
|
367
454
|
|
|
368
455
|
Headless component for custom layouts (grids, cards, etc).
|
|
369
456
|
|
|
370
|
-
| Prop
|
|
371
|
-
|
|
372
|
-
| `action`
|
|
373
|
-
| `filters`
|
|
374
|
-
| `limit`
|
|
375
|
-
| `manual`
|
|
376
|
-
| `children` | `(dataKit) => ReactNode`
|
|
457
|
+
| Prop | Type | Description |
|
|
458
|
+
| ---------- | ---------------------------- | --------------------------------- |
|
|
459
|
+
| `action` | `(input) => Promise<Result>` | Server action function |
|
|
460
|
+
| `filters` | `FilterItem[]` | Filter configurations |
|
|
461
|
+
| `limit` | `{ default: number }` | Items per page |
|
|
462
|
+
| `manual` | `boolean` | Skip loading/empty state handling |
|
|
463
|
+
| `children` | `(dataKit) => ReactNode` | Render function |
|
|
377
464
|
|
|
378
465
|
#### `useDataKit(options)`
|
|
379
466
|
|
|
@@ -398,23 +485,23 @@ interface TDataKitControllerOptions<T, R> {
|
|
|
398
485
|
|
|
399
486
|
Returns:
|
|
400
487
|
|
|
401
|
-
-
|
|
402
|
-
-
|
|
403
|
-
-
|
|
404
|
-
-
|
|
405
|
-
-
|
|
406
|
-
-
|
|
407
|
-
-
|
|
408
|
-
-
|
|
409
|
-
-
|
|
410
|
-
-
|
|
411
|
-
-
|
|
412
|
-
-
|
|
413
|
-
-
|
|
414
|
-
-
|
|
415
|
-
-
|
|
416
|
-
-
|
|
417
|
-
-
|
|
488
|
+
- `items` - Current page items
|
|
489
|
+
- `page` - Current page number
|
|
490
|
+
- `limit` - Items per page
|
|
491
|
+
- `total` - Total document count
|
|
492
|
+
- `sorts` - Current sort configuration
|
|
493
|
+
- `filter` - Current filter values
|
|
494
|
+
- `state`
|
|
495
|
+
- `isLoading` - Loading state
|
|
496
|
+
- `error` - Error state
|
|
497
|
+
- `actions`
|
|
498
|
+
- `setPage(page)` - Go to a specific page
|
|
499
|
+
- `setLimit(limit)` - Set items per page
|
|
500
|
+
- `setSort(path, value)` - Set sort for a column
|
|
501
|
+
- `setFilter(key, value)` - Set a filter value
|
|
502
|
+
- `clearFilters()` - Clear all filters
|
|
503
|
+
- `refresh()` - Refresh the table data
|
|
504
|
+
- `reset()` - Reset to initial state
|
|
418
505
|
|
|
419
506
|
#### `useSelection<T>()`
|
|
420
507
|
|
|
@@ -455,7 +542,7 @@ const { pages, hasNextPage, hasPrevPage, totalPages } = usePagination({
|
|
|
455
542
|
|
|
456
543
|
```typescript
|
|
457
544
|
interface TDataKitInput<T = unknown> {
|
|
458
|
-
action?:
|
|
545
|
+
action?: 'FETCH';
|
|
459
546
|
page?: number;
|
|
460
547
|
limit?: number;
|
|
461
548
|
sort?: TSortOptions<T>;
|
|
@@ -470,7 +557,7 @@ interface TDataKitInput<T = unknown> {
|
|
|
470
557
|
|
|
471
558
|
```typescript
|
|
472
559
|
interface TDataKitResult<R> {
|
|
473
|
-
type:
|
|
560
|
+
type: 'ITEMS';
|
|
474
561
|
items: R[];
|
|
475
562
|
documentTotal: number;
|
|
476
563
|
}
|
|
@@ -481,7 +568,7 @@ interface TDataKitResult<R> {
|
|
|
481
568
|
```typescript
|
|
482
569
|
interface TFilterConfig {
|
|
483
570
|
[key: string]: {
|
|
484
|
-
type:
|
|
571
|
+
type: 'REGEX' | 'EXACT';
|
|
485
572
|
field?: string;
|
|
486
573
|
};
|
|
487
574
|
}
|
|
@@ -492,7 +579,7 @@ interface TFilterConfig {
|
|
|
492
579
|
The package provides generic database types that work with any ORM/ODM:
|
|
493
580
|
|
|
494
581
|
```typescript
|
|
495
|
-
import type { TModel, TMongoFilterQuery, TQueryBuilder } from
|
|
582
|
+
import type { TModel, TMongoFilterQuery, TQueryBuilder } from 'next-data-kit/types';
|
|
496
583
|
|
|
497
584
|
// Your model just needs to implement the Model interface
|
|
498
585
|
interface MyModel extends TModel<MyDocument> {
|
|
@@ -509,8 +596,8 @@ MIT © muhgholy
|
|
|
509
596
|
|
|
510
597
|
This repo includes a **dev-only** playground you can open in the browser to preview components and validate end-to-end behavior against a **temporary in-memory MongoDB**.
|
|
511
598
|
|
|
512
|
-
-
|
|
513
|
-
-
|
|
599
|
+
- It is **not exported** in `package.json#exports`.
|
|
600
|
+
- It is **not published** to npm because `package.json#files` only includes `dist`.
|
|
514
601
|
|
|
515
602
|
### Run
|
|
516
603
|
|
|
@@ -520,8 +607,8 @@ npm run playground:dev
|
|
|
520
607
|
|
|
521
608
|
Then open:
|
|
522
609
|
|
|
523
|
-
-
|
|
524
|
-
-
|
|
610
|
+
- Web UI: http://localhost:5173
|
|
611
|
+
- API health: http://127.0.0.1:8787/api/health
|
|
525
612
|
|
|
526
613
|
### Reset seed data
|
|
527
614
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"data-kit-table.d.ts","sourceRoot":"","sources":["../../../src/client/components/data-kit-table.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAmD,MAAM,OAAO,CAAC;AAKxE,OAAO,EAkBH,SAAS,EACT,SAAS,EAGZ,MAAM,MAAM,CAAC;AACd,OAAO,KAAK,EACR,aAAa,EACb,cAAc,EACd,uBAAuB,EACvB,kBAAkB,EAClB,kBAAkB,EAClB,2BAA2B,EAC3B,sBAAsB,EACtB,iBAAiB,EACjB,uBAAuB,EACvB,aAAa,EAChB,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"data-kit-table.d.ts","sourceRoot":"","sources":["../../../src/client/components/data-kit-table.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAmD,MAAM,OAAO,CAAC;AAKxE,OAAO,EAkBH,SAAS,EACT,SAAS,EAGZ,MAAM,MAAM,CAAC;AACd,OAAO,KAAK,EACR,aAAa,EACb,cAAc,EACd,uBAAuB,EACvB,kBAAkB,EAClB,kBAAkB,EAClB,2BAA2B,EAC3B,sBAAsB,EACtB,iBAAiB,EACjB,uBAAuB,EACvB,aAAa,EAChB,MAAM,aAAa,CAAC;AA4XrB,eAAO,MAAM,YAAY,IArXrB,OAAO,SAAS,CAAC,KAAK,EAAE,aAAa,CAAC,OAAO,CAAC,KAAK,OAAO,CAAC,cAAc,CAAC,sBAAsB,CAAC,CAAC,EAClG,SAAS,mBACJ,QAAQ,CAAC;IACd,MAAM,EAAE,OAAO,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAChC,YAAY,CAAC,EAAE,aAAa,CAAC;IAC7B,KAAK,EAAE,uBAAuB,CAAC,uBAAuB,CAAC,OAAO,CAAC,EAAE,SAAS,CAAC,EAAE,CAAC;IAC9E,OAAO,CAAC,EAAE,kBAAkB,EAAE,CAAC;IAC/B,UAAU,CAAC,EAAE;QACT,OAAO,EAAE,OAAO,CAAC;QACjB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,kBAAkB,CAAC,uBAAuB,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC;KAClF,CAAC;IACF,YAAY,CAAC,EAAE,SAAS,CAAC;IACzB,KAAK,CAAC,EAAE;QAAE,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC;IAC5B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IAC/B,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,KAAK,CAAC,EAAE,iBAAiB,CAAC;IAC1B,UAAU,CAAC,EAAE,KAAK,CAAC,gBAAgB,CAAC,2BAA2B,CAAC,uBAAuB,CAAC,OAAO,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC;CAC7G,CAAC;;;CAmWA,CAAC"}
|
|
@@ -118,7 +118,7 @@ const DataKitRoot = (props) => {
|
|
|
118
118
|
}, [refetchInterval, isVisible, dataKit.actions]);
|
|
119
119
|
useEffect(() => { selection.deselectAll(); }, [dataKit.items.length]);
|
|
120
120
|
// ** Render
|
|
121
|
-
return (_jsxs("div", { ref: tableRef, className: `space-y-3 ${className ?? ''}`, children: [_jsxs("div", { className: "flex items-center justify-between gap-2", children: [_jsx("div", { className: "flex items-center gap-2", children: filters.length > 0 && (_jsxs(Popover, { open: isFilterOpen, onOpenChange: setIsFilterOpen, children: [_jsx(PopoverTrigger, { asChild: true, children: _jsxs(Button, { variant: "outline", size: "sm", children: [_jsx(Filter, { className: "mr-1.5 size-4" }), "Filters"] }) }), _jsxs(PopoverContent, { align: "start", className: "w-80", container: overlayContainer, children: [_jsx("div", { className: "grid gap-3", children: filters.map((f) => (_jsxs("div", { className: "grid gap-1.5", children: [_jsx("label", { className: "text-sm font-medium", children: f.label }), f.type === 'TEXT' && (_jsx("input", { type: "text", className: "h-9 w-full rounded-md border bg-transparent px-3 text-sm outline-none focus:ring-2 focus:ring-ring", placeholder: f.placeholder, value: dataKit.filter[f.id] ?? '', onChange: (e) => dataKit.actions.setFilter(f.id, e.target.value) })), f.type === 'SELECT' && (_jsxs(Select, { value: String(dataKit.filter[f.id] || '__all__'), onValueChange: (v) => dataKit.actions.setFilter(f.id, v === '__all__' ? '' : v), children: [_jsx(SelectTrigger, { children: _jsx(SelectValue, {}) }), _jsxs(SelectContent, { container: overlayContainer, children: [_jsx(SelectItem, { value: "__all__", children: "All" }), f.dataset?.map((d) => (_jsx(SelectItem, { value: d.id, children: d.label }, d.id)))] })] })), f.type === 'BOOLEAN' && (_jsxs("div", { className: "flex items-center justify-between", children: [_jsx("span", { className: "text-sm text-muted-foreground", children: f.placeholder ?? 'Enable' }), _jsx(Switch, { checked: Boolean(dataKit.filter[f.id]), onCheckedChange: (c) => dataKit.actions.setFilter(f.id, c) })] }))] }, f.id))) }), _jsxs("div", { className: "mt-4 flex justify-between border-t pt-3", children: [_jsx(Button, { variant: "outline", size: "sm", onClick: handleResetFilters, children: "Reset" }), _jsx(Button, { size: "sm", onClick: () => setIsFilterOpen(false), children: "Done" })] })] })] })) }), _jsxs("div", { className: "flex items-center gap-1", children: [_jsxs("span", { className: "mr-2 text-sm text-muted-foreground", children: [dataKit.items.length, " of ", dataKit.total] }), _jsxs(Select, { value: String(dataKit.limit), onValueChange: (v) => dataKit.actions.setLimit(Number(v)), disabled: dataKit.state.isLoading, children: [_jsx(SelectTrigger, { className: "w-16", children: _jsx(SelectValue, {}) }), _jsx(SelectContent, { container: overlayContainer, children: [10, 25, 50, 100].map((v) => (_jsx(SelectItem, { value: String(v), children: v }, v))) })] })] })] }), _jsx("div", { className: `overflow-hidden border border-gray-200 dark:border-gray-800 ${bordered === 'rounded' ? 'rounded-lg' : bordered ? '' : 'rounded-lg'}`, children: _jsxs(Table, { children: [_jsx(TableHeader, { children: _jsxs(TableRow, { children: [selectable?.enabled && (_jsx(TableHead, { className: "w-12", children: _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Checkbox, { checked: selection.isIndeterminate ? 'indeterminate' : selection.isAllSelected, onCheckedChange: () => selection.toggleAll() }), selectable.actions && Object.keys(selectable.actions).length > 0 && (_jsxs(DropdownMenu, { open: actionsMenuOpen, onOpenChange: setActionsMenuOpen, children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsx(Button, { variant: "ghost", size: "icon", className: "size-6", disabled: selectedCount === 0 || !!actionLoading, children: actionLoading ? _jsx(Loader2, { className: "size-4 animate-spin" }) : _jsx(MoreHorizontal, { className: "size-4" }) }) }), _jsx(DropdownMenuContent, { align: "start", container: overlayContainer, children: Object.entries(selectable.actions).map(([key, action]) => (_jsx(DropdownMenuItem, { disabled: !!actionLoading, onSelect: () => handleSelectionAction(key), children: actionLoading === key ? 'Working…' : action.name }, key))) })] }))] }) })), columns.map((col, idx) => (_jsx(React.Fragment, { children: col.sortable ? (_jsx(TableHead, { ...(React.isValidElement(col.head) ? col.head.props : {}), children: _jsxs(Button, { variant: "ghost", size: "sm", className: "-ml-3", onClick: () => handleSort(col.sortable.path), children: [React.isValidElement(col.head) ? col.head.props.children : col.head, getSortFor(col.sortable.path) === 1 && _jsx(ArrowUp, { className: "ml-1 size-4" }), getSortFor(col.sortable.path) === -1 && _jsx(ArrowDown, { className: "ml-1 size-4" })] }) })) : (col.head) }, idx)))] }) }), _jsx(TableBody, { children: dataKit.state.isLoading ? (_jsx(TableRow, { children: _jsx(TableCell, { colSpan: colSpan, className: "h-24 text-center", children: _jsx(Loader2, { className: "mx-auto size-5 animate-spin" }) }) })) : dataKit.items.length === 0 ? (_jsx(TableRow, { children: _jsx(TableCell, { colSpan: colSpan, className: "h-24 text-center text-muted-foreground", children: "No results found." }) })) : (dataKit.items.map((item, idx) => (_jsxs(TableRow, { children: [selectable?.enabled && (_jsx(TableCell, { children: _jsx(Checkbox, { checked: selection.isSelected(item.id), onCheckedChange: () => selection.toggle(item.id) }) })), columns.map((col, colIdx) => (_jsx(React.Fragment, { children: col.body({
|
|
121
|
+
return (_jsxs("div", { ref: tableRef, className: `space-y-3 ${className ?? ''}`, children: [_jsxs("div", { className: "flex items-center justify-between gap-2", children: [_jsx("div", { className: "flex items-center gap-2", children: filters.length > 0 && (_jsxs(Popover, { open: isFilterOpen, onOpenChange: setIsFilterOpen, children: [_jsx(PopoverTrigger, { asChild: true, children: _jsxs(Button, { variant: "outline", size: "sm", children: [_jsx(Filter, { className: "mr-1.5 size-4" }), "Filters"] }) }), _jsxs(PopoverContent, { align: "start", className: "w-80", container: overlayContainer, children: [_jsx("div", { className: "grid gap-3", children: filters.map((f) => (_jsxs("div", { className: "grid gap-1.5", children: [_jsx("label", { className: "text-sm font-medium", children: f.label }), f.type === 'TEXT' && (_jsx("input", { type: "text", className: "h-9 w-full rounded-md border bg-transparent px-3 text-sm outline-none focus:ring-2 focus:ring-ring", placeholder: f.placeholder, value: dataKit.filter[f.id] ?? '', onChange: (e) => dataKit.actions.setFilter(f.id, e.target.value) })), f.type === 'SELECT' && (_jsxs(Select, { value: String(dataKit.filter[f.id] || '__all__'), onValueChange: (v) => dataKit.actions.setFilter(f.id, v === '__all__' ? '' : v), children: [_jsx(SelectTrigger, { children: _jsx(SelectValue, {}) }), _jsxs(SelectContent, { container: overlayContainer, children: [_jsx(SelectItem, { value: "__all__", children: "All" }), f.dataset?.map((d) => (_jsx(SelectItem, { value: d.id, children: d.label }, d.id)))] })] })), f.type === 'BOOLEAN' && (_jsxs("div", { className: "flex items-center justify-between", children: [_jsx("span", { className: "text-sm text-muted-foreground", children: f.placeholder ?? 'Enable' }), _jsx(Switch, { checked: Boolean(dataKit.filter[f.id]), onCheckedChange: (c) => dataKit.actions.setFilter(f.id, c) })] }))] }, f.id))) }), _jsxs("div", { className: "mt-4 flex justify-between border-t pt-3", children: [_jsx(Button, { variant: "outline", size: "sm", onClick: handleResetFilters, children: "Reset" }), _jsx(Button, { size: "sm", onClick: () => setIsFilterOpen(false), children: "Done" })] })] })] })) }), _jsxs("div", { className: "flex items-center gap-1", children: [_jsxs("span", { className: "mr-2 text-sm text-muted-foreground", children: [dataKit.items.length, " of ", dataKit.total] }), _jsxs(Select, { value: String(dataKit.limit), onValueChange: (v) => dataKit.actions.setLimit(Number(v)), disabled: dataKit.state.isLoading, children: [_jsx(SelectTrigger, { className: "w-16", children: _jsx(SelectValue, {}) }), _jsx(SelectContent, { container: overlayContainer, children: [10, 25, 50, 100].map((v) => (_jsx(SelectItem, { value: String(v), children: v }, v))) })] })] })] }), _jsx("div", { className: `overflow-hidden border border-gray-200 dark:border-gray-800 ${bordered === 'rounded' ? 'rounded-lg' : bordered ? '' : 'rounded-lg'}`, children: _jsxs(Table, { children: [_jsx(TableHeader, { children: _jsxs(TableRow, { children: [selectable?.enabled && (_jsx(TableHead, { className: "w-12", children: _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Checkbox, { checked: selection.isIndeterminate ? 'indeterminate' : selection.isAllSelected, onCheckedChange: () => selection.toggleAll() }), selectable.actions && Object.keys(selectable.actions).length > 0 && (_jsxs(DropdownMenu, { open: actionsMenuOpen, onOpenChange: setActionsMenuOpen, children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsx(Button, { variant: "ghost", size: "icon", className: "size-6", disabled: selectedCount === 0 || !!actionLoading, children: actionLoading ? _jsx(Loader2, { className: "size-4 animate-spin" }) : _jsx(MoreHorizontal, { className: "size-4" }) }) }), _jsx(DropdownMenuContent, { align: "start", container: overlayContainer, children: Object.entries(selectable.actions).map(([key, action]) => (_jsx(DropdownMenuItem, { disabled: !!actionLoading, onSelect: () => handleSelectionAction(key), children: actionLoading === key ? 'Working…' : action.name }, key))) })] }))] }) })), columns.map((col, idx) => (_jsx(React.Fragment, { children: col.sortable ? (_jsx(TableHead, { ...(React.isValidElement(col.head) ? col.head.props : {}), children: _jsxs(Button, { variant: "ghost", size: "sm", className: "-ml-3", onClick: () => handleSort(col.sortable.path), children: [React.isValidElement(col.head) ? col.head.props.children : col.head, getSortFor(col.sortable.path) === 1 && _jsx(ArrowUp, { className: "ml-1 size-4" }), getSortFor(col.sortable.path) === -1 && _jsx(ArrowDown, { className: "ml-1 size-4" })] }) })) : (col.head) }, idx)))] }) }), _jsx(TableBody, { children: dataKit.state.isLoading ? (_jsx(TableRow, { children: _jsx(TableCell, { colSpan: colSpan, className: "h-24 text-center", children: _jsx(Loader2, { className: "mx-auto size-5 animate-spin" }) }) })) : dataKit.state.error ? (_jsx(TableRow, { children: _jsxs(TableCell, { colSpan: colSpan, className: "h-24 text-center text-red-500", children: ["Error: ", dataKit.state.error.message] }) })) : dataKit.items.length === 0 ? (_jsx(TableRow, { children: _jsx(TableCell, { colSpan: colSpan, className: "h-24 text-center text-muted-foreground", children: "No results found." }) })) : (dataKit.items.map((item, idx) => (_jsxs(TableRow, { children: [selectable?.enabled && (_jsx(TableCell, { children: _jsx(Checkbox, { checked: selection.isSelected(item.id), onCheckedChange: () => selection.toggle(item.id) }) })), columns.map((col, colIdx) => (_jsx(React.Fragment, { children: col.body({
|
|
122
122
|
item,
|
|
123
123
|
index: idx,
|
|
124
124
|
state: initialState,
|