kmod-cli 1.4.15 → 1.6.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.
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
interface ListProps<T> {
|
|
4
|
+
items: T[];
|
|
5
|
+
keyExtractor: (item: T) => React.Key;
|
|
6
|
+
renderItem: (item: T, index: number) => React.ReactNode;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function List<T>({ items, keyExtractor, renderItem }: ListProps<T>) {
|
|
10
|
+
return (
|
|
11
|
+
<>
|
|
12
|
+
{items.map((item, index) => (
|
|
13
|
+
<ListItem
|
|
14
|
+
key={keyExtractor(item)}
|
|
15
|
+
item={item}
|
|
16
|
+
index={index}
|
|
17
|
+
renderItem={renderItem}
|
|
18
|
+
/>
|
|
19
|
+
))}
|
|
20
|
+
</>
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface ListItemProps<T> {
|
|
25
|
+
item: T;
|
|
26
|
+
index: number;
|
|
27
|
+
renderItem: (item: T, index: number) => React.ReactNode;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Generic memo component
|
|
31
|
+
function _ListItem<T>({ item, index, renderItem }: ListItemProps<T>) {
|
|
32
|
+
return <>{renderItem(item, index)}</>;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const ListItem = React.memo(_ListItem) as <T>(
|
|
36
|
+
props: ListItemProps<T>
|
|
37
|
+
) => React.ReactNode;
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
// Example usage of the List component
|
|
41
|
+
// interface User {
|
|
42
|
+
// id: number;
|
|
43
|
+
// name: string;
|
|
44
|
+
// }
|
|
45
|
+
|
|
46
|
+
// const users: User[] = [
|
|
47
|
+
// { id: 1, name: 'Alice' },
|
|
48
|
+
// { id: 2, name: 'Bob' },
|
|
49
|
+
// { id: 3, name: 'Charlie' },
|
|
50
|
+
// ];
|
|
51
|
+
|
|
52
|
+
// export function UserList() {
|
|
53
|
+
// return (
|
|
54
|
+
// <List
|
|
55
|
+
// items={users}
|
|
56
|
+
// keyExtractor={(user) => user.id}
|
|
57
|
+
// renderItem={(user) => <div>{user.name}</div>}
|
|
58
|
+
// />
|
|
59
|
+
// );
|
|
60
|
+
// }
|
|
@@ -8,7 +8,6 @@ import {
|
|
|
8
8
|
useState,
|
|
9
9
|
} from 'react';
|
|
10
10
|
|
|
11
|
-
// Nếu bạn cần alias cho ITable type, dùng:
|
|
12
11
|
import type { Table as ITable } from '@tanstack/react-table';
|
|
13
12
|
import {
|
|
14
13
|
Cell,
|
|
@@ -55,7 +54,7 @@ export type TableClassNames = {
|
|
|
55
54
|
};
|
|
56
55
|
export type TableHeaderProps<TData> =
|
|
57
56
|
HTMLAttributes<HTMLTableSectionElement> & {
|
|
58
|
-
handleClick
|
|
57
|
+
handleClick?: ({
|
|
59
58
|
e,
|
|
60
59
|
table,
|
|
61
60
|
}: {
|
|
@@ -64,7 +63,7 @@ export type TableHeaderProps<TData> =
|
|
|
64
63
|
}) => void;
|
|
65
64
|
};
|
|
66
65
|
export type TableBodyProps<TData> = HTMLAttributes<HTMLTableSectionElement> & {
|
|
67
|
-
handleClick
|
|
66
|
+
handleClick?: ({
|
|
68
67
|
e,
|
|
69
68
|
table,
|
|
70
69
|
}: {
|
|
@@ -73,7 +72,8 @@ export type TableBodyProps<TData> = HTMLAttributes<HTMLTableSectionElement> & {
|
|
|
73
72
|
}) => void;
|
|
74
73
|
};
|
|
75
74
|
export type TableHeadProps<TData> = HTMLAttributes<HTMLTableCellElement> & {
|
|
76
|
-
|
|
75
|
+
classNameCondition?: | (({cell, table}: {cell?: Header<TData, unknown>; table?: ITable<TData>}) => string) | string;
|
|
76
|
+
handleClick?: ({
|
|
77
77
|
e,
|
|
78
78
|
table,
|
|
79
79
|
cell,
|
|
@@ -85,7 +85,8 @@ export type TableHeadProps<TData> = HTMLAttributes<HTMLTableCellElement> & {
|
|
|
85
85
|
};
|
|
86
86
|
export type TableCellProps<TData, TValue> =
|
|
87
87
|
HTMLAttributes<HTMLTableCellElement> & {
|
|
88
|
-
|
|
88
|
+
classNameCondition?: | (({cell, table}: {cell?: Cell<TData, unknown>; table?: ITable<TData>}) => string) | string;
|
|
89
|
+
handleClick?: ({
|
|
89
90
|
e,
|
|
90
91
|
table,
|
|
91
92
|
cell,
|
|
@@ -96,7 +97,7 @@ export type TableCellProps<TData, TValue> =
|
|
|
96
97
|
}) => void;
|
|
97
98
|
};
|
|
98
99
|
export type TableRowHeadProps<TData> = HTMLAttributes<HTMLTableRowElement> & {
|
|
99
|
-
handleClick
|
|
100
|
+
handleClick?: ({
|
|
100
101
|
e,
|
|
101
102
|
table,
|
|
102
103
|
row,
|
|
@@ -107,7 +108,8 @@ export type TableRowHeadProps<TData> = HTMLAttributes<HTMLTableRowElement> & {
|
|
|
107
108
|
}) => void;
|
|
108
109
|
};
|
|
109
110
|
export type TableRowBodyProps<TData> = HTMLAttributes<HTMLTableRowElement> & {
|
|
110
|
-
|
|
111
|
+
classNameCondition?: | (({row, table}: {row?: Row<TData>; table?: ITable<TData>}) => string) | string;
|
|
112
|
+
handleClick?: ({
|
|
111
113
|
e,
|
|
112
114
|
table,
|
|
113
115
|
row,
|
|
@@ -118,7 +120,7 @@ export type TableRowBodyProps<TData> = HTMLAttributes<HTMLTableRowElement> & {
|
|
|
118
120
|
}) => void;
|
|
119
121
|
};
|
|
120
122
|
export type TableProps<TData> = HTMLAttributes<HTMLTableElement> & {
|
|
121
|
-
handleClick
|
|
123
|
+
handleClick?: ({
|
|
122
124
|
e,
|
|
123
125
|
table,
|
|
124
126
|
}: {
|
|
@@ -173,6 +175,15 @@ export type DataTableProps<TData, TValue> = {
|
|
|
173
175
|
// handles?: Handles
|
|
174
176
|
};
|
|
175
177
|
|
|
178
|
+
export interface UseTablePropsFn<TData, TValue> {
|
|
179
|
+
table: ITable<TData>;
|
|
180
|
+
row?: Row<TData>;
|
|
181
|
+
cell?: Cell<TData, TValue>;
|
|
182
|
+
header?: Header<TData, TValue>;
|
|
183
|
+
headerGroup?: HeaderGroup<TData>;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
|
|
176
187
|
export type DataTableToolbarFns<TData> = {
|
|
177
188
|
globalFilter: string;
|
|
178
189
|
setGlobalFilter: (value: string) => void;
|
|
@@ -204,7 +215,7 @@ export function DataTable<TData, TValue>({
|
|
|
204
215
|
useTableProps,
|
|
205
216
|
initialState,
|
|
206
217
|
alternate = "even",
|
|
207
|
-
alternateColor = "#
|
|
218
|
+
alternateColor = "#fbfbfb",
|
|
208
219
|
// handles
|
|
209
220
|
}: DataTableProps<TData, TValue>) {
|
|
210
221
|
const table = useReactTable({
|
|
@@ -241,47 +252,74 @@ export function DataTable<TData, TValue>({
|
|
|
241
252
|
};
|
|
242
253
|
|
|
243
254
|
const {
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
} = useTableProps?.tableProps || {};
|
|
255
|
+
handleClick: tableHandleClick,
|
|
256
|
+
onClick: tableOnClick,
|
|
257
|
+
...tableDomProps
|
|
258
|
+
} = useTableProps?.tableProps || {};
|
|
248
259
|
|
|
249
|
-
const {
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
} = useTableProps?.headerProps || {};
|
|
254
|
-
const {
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
} = useTableProps?.rowHeadProps || {};
|
|
259
|
-
const {
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
} = useTableProps?.
|
|
270
|
-
const {
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
260
|
+
const {
|
|
261
|
+
handleClick: headerHandleClick,
|
|
262
|
+
onClick: headerOnClick,
|
|
263
|
+
...headerDomProps
|
|
264
|
+
} = useTableProps?.headerProps || {};
|
|
265
|
+
const {
|
|
266
|
+
handleClick: rowHeadHandleClick,
|
|
267
|
+
onClick: rowHeadOnClick,
|
|
268
|
+
...rowHeadDomProps
|
|
269
|
+
} = useTableProps?.rowHeadProps || {};
|
|
270
|
+
const {
|
|
271
|
+
handleClick: cellHeadHandleClick,
|
|
272
|
+
onClick: cellHeadOnClick,
|
|
273
|
+
classNameCondition: cellHeadClassNameCondition,
|
|
274
|
+
...cellHeadDomProps
|
|
275
|
+
} = useTableProps?.cellHeadProps || {};
|
|
276
|
+
const {
|
|
277
|
+
handleClick: bodyHandleClick,
|
|
278
|
+
onClick: bodyOnClick,
|
|
279
|
+
...bodyDomProps
|
|
280
|
+
} = useTableProps?.bodyProps || {};
|
|
281
|
+
const {
|
|
282
|
+
handleClick: rowBodyHandleClick,
|
|
283
|
+
onClick: rowBodyOnClick,
|
|
284
|
+
style: rowBodyStyle,
|
|
285
|
+
classNameCondition: rowBodyClassNameCondition,
|
|
286
|
+
...rowBodyDomProps
|
|
287
|
+
} = useTableProps?.rowBodyProps || {};
|
|
288
|
+
const {
|
|
289
|
+
handleClick: cellBodyHandleClick,
|
|
290
|
+
onClick: cellBodyOnClick,
|
|
291
|
+
classNameCondition: cellBodyClassNameCondition,
|
|
292
|
+
...cellBodyDomProps
|
|
293
|
+
} = useTableProps?.cellBodyProps || {};
|
|
275
294
|
|
|
276
|
-
const {
|
|
277
|
-
|
|
278
|
-
...skRowDomProps
|
|
279
|
-
} = useTableProps?.rowBodyProps || {};
|
|
295
|
+
const { handleClick: skRowHandleClick, classNameCondition: skRowClassNameCondition, ...skRowDomProps } =
|
|
296
|
+
useTableProps?.rowBodyProps || {};
|
|
280
297
|
|
|
281
|
-
const {
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
298
|
+
const { handleClick: skCellHandleClick, classNameCondition: skCellClassNameCondition, ...skCellDomProps } =
|
|
299
|
+
useTableProps?.cellBodyProps || {};
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
function getCellHeadClassNameByCondition({cell,table}: {cell?: Header<TData, unknown>; table?: ITable<TData>}) {
|
|
303
|
+
if(!cell || !table) return "";
|
|
304
|
+
const classNameCondition = useTableProps?.cellHeadProps?.classNameCondition;
|
|
305
|
+
if(!classNameCondition) return "";
|
|
306
|
+
if(typeof classNameCondition === "string") return classNameCondition;
|
|
307
|
+
return classNameCondition({cell,table});
|
|
308
|
+
}
|
|
309
|
+
function getCellBodyClassNameByCondition({cell,table}: {cell?: Cell<TData, unknown>; table?: ITable<TData>}) {
|
|
310
|
+
if(!cell || !table) return "";
|
|
311
|
+
const classNameCondition = useTableProps?.cellBodyProps?.classNameCondition;
|
|
312
|
+
if(!classNameCondition) return "";
|
|
313
|
+
if(typeof classNameCondition === "string") return classNameCondition;
|
|
314
|
+
return classNameCondition({cell,table});
|
|
315
|
+
}
|
|
316
|
+
function getRowBodyClassNameByCondition({row,table}: {row?: Row<TData>; table?: ITable<TData>}) {
|
|
317
|
+
if(!row || !table) return "";
|
|
318
|
+
const classNameCondition = useTableProps?.rowBodyProps?.classNameCondition;
|
|
319
|
+
if(!classNameCondition) return "";
|
|
320
|
+
if(typeof classNameCondition === "string") return classNameCondition;
|
|
321
|
+
return classNameCondition({row,table});
|
|
322
|
+
}
|
|
285
323
|
|
|
286
324
|
|
|
287
325
|
return (
|
|
@@ -321,11 +359,7 @@ const {
|
|
|
321
359
|
{headerGroup.headers.map((header) => (
|
|
322
360
|
<TableHead
|
|
323
361
|
key={header.id}
|
|
324
|
-
{...
|
|
325
|
-
const { handleClick, onClick, ...rest } =
|
|
326
|
-
useTableProps?.cellHeadProps || {};
|
|
327
|
-
return rest;
|
|
328
|
-
})()}
|
|
362
|
+
{...cellHeadDomProps}
|
|
329
363
|
className={cn(
|
|
330
364
|
"cursor-pointer select-none",
|
|
331
365
|
classNames?.header?.head
|
|
@@ -334,22 +368,11 @@ const {
|
|
|
334
368
|
width: header.getSize() ? `${header.getSize()}px !important` : "auto",
|
|
335
369
|
}}
|
|
336
370
|
onClick={(e) => {
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
useTableProps.cellHeadProps.onClick(e);
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
// Just call the parent's handleClick if provided
|
|
343
|
-
if (useTableProps?.cellHeadProps?.handleClick) {
|
|
344
|
-
useTableProps.cellHeadProps.handleClick({
|
|
345
|
-
e,
|
|
346
|
-
cell: header,
|
|
347
|
-
table,
|
|
348
|
-
});
|
|
349
|
-
}
|
|
371
|
+
cellHeadOnClick?.(e);
|
|
372
|
+
cellHeadHandleClick?.({ e, table, cell: header });
|
|
350
373
|
}}
|
|
351
374
|
>
|
|
352
|
-
<div className={cn("flex items-center gap-1 w-fit", classNames?.header?.content)}>
|
|
375
|
+
<div className={cn("flex items-center gap-1 w-fit", classNames?.header?.content, getCellHeadClassNameByCondition({cell: header, table}))}>
|
|
353
376
|
{flexRender(
|
|
354
377
|
header.column.columnDef.header,
|
|
355
378
|
header.getContext()
|
|
@@ -384,15 +407,13 @@ const {
|
|
|
384
407
|
{!isLoading &&
|
|
385
408
|
table.getRowModel().rows.length > 0 &&
|
|
386
409
|
table.getRowModel().rows.map((row, index) => {
|
|
387
|
-
const { handleClick, onClick, ...rest } =
|
|
388
|
-
useTableProps?.rowBodyProps || {};
|
|
389
410
|
|
|
390
411
|
return (
|
|
391
412
|
<TableRow
|
|
392
413
|
{...rowBodyDomProps}
|
|
393
414
|
key={row.id}
|
|
394
415
|
style={{...rowBodyStyle, backgroundColor: getAlternateColor(index)}}
|
|
395
|
-
className={cn(classNames?.body?.row)}
|
|
416
|
+
className={cn(classNames?.body?.row, getRowBodyClassNameByCondition({row,table}))}
|
|
396
417
|
data-state={row.getIsSelected() && "selected"}
|
|
397
418
|
onClick={(e) => {
|
|
398
419
|
rowBodyOnClick?.(e);
|
|
@@ -403,7 +424,7 @@ const {
|
|
|
403
424
|
<TableCell
|
|
404
425
|
{...cellBodyDomProps}
|
|
405
426
|
key={cell.id}
|
|
406
|
-
className={cn(classNames?.body?.cell)}
|
|
427
|
+
className={cn(classNames?.body?.cell, getCellBodyClassNameByCondition({cell, table}))}
|
|
407
428
|
onClick={(e) => {
|
|
408
429
|
cellBodyOnClick?.(e);
|
|
409
430
|
cellBodyHandleClick?.({ e, cell, table });
|
|
@@ -480,26 +501,27 @@ export const TableSkeleton = <TData, TValue>({
|
|
|
480
501
|
const {
|
|
481
502
|
handleClick: _rowHandleClick,
|
|
482
503
|
onClick: _rowOnClick,
|
|
504
|
+
classNameCondition: rowClassNameCondition,
|
|
483
505
|
...rowDomProps
|
|
484
506
|
} = props?.rowBodyProps || {};
|
|
485
507
|
|
|
486
508
|
const {
|
|
487
509
|
handleClick: _cellHandleClick,
|
|
488
510
|
onClick: _cellOnClick,
|
|
511
|
+
classNameCondition: cellClassNameCondition,
|
|
489
512
|
...cellDomProps
|
|
490
513
|
} = props?.cellBodyProps || {};
|
|
491
514
|
|
|
492
|
-
|
|
493
515
|
if (showNoData) {
|
|
494
516
|
return (
|
|
495
517
|
<TableRow
|
|
496
518
|
key="no-data-skeleton"
|
|
497
|
-
className={cn(classNames?.body?.row)}
|
|
519
|
+
className={cn(classNames?.body?.row, )}
|
|
498
520
|
{...rowDomProps}
|
|
499
521
|
>
|
|
500
522
|
<TableCell
|
|
501
523
|
colSpan={columns.length}
|
|
502
|
-
className={cn("h-24 text-center"
|
|
524
|
+
className={cn("h-24 text-center")}
|
|
503
525
|
{...cellDomProps}
|
|
504
526
|
>
|
|
505
527
|
{emptyLabel}
|
|
@@ -1,95 +1,200 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import
|
|
3
|
+
import React from 'react';
|
|
4
4
|
|
|
5
|
-
import { ColumnDef } from
|
|
5
|
+
import { ColumnDef } from '@tanstack/react-table';
|
|
6
6
|
|
|
7
|
-
import {
|
|
7
|
+
import { cn } from '../../lib/utils';
|
|
8
|
+
import {
|
|
9
|
+
DataTable,
|
|
10
|
+
UseTableProps,
|
|
11
|
+
} from './data-table'; // chỉnh path cho đúng
|
|
8
12
|
|
|
13
|
+
/* ======================================================
|
|
14
|
+
* 1. Data type
|
|
15
|
+
* ====================================================== */
|
|
9
16
|
type User = {
|
|
10
17
|
id: string;
|
|
11
18
|
name: string;
|
|
12
19
|
email: string;
|
|
20
|
+
role: "admin" | "user";
|
|
21
|
+
status: "active" | "inactive";
|
|
13
22
|
};
|
|
14
23
|
|
|
15
|
-
|
|
24
|
+
/* ======================================================
|
|
25
|
+
* 2. Mock data
|
|
26
|
+
* ====================================================== */
|
|
27
|
+
const USERS: User[] = [
|
|
28
|
+
{
|
|
29
|
+
id: "1",
|
|
30
|
+
name: "John Doe",
|
|
31
|
+
email: "john@example.com",
|
|
32
|
+
role: "admin",
|
|
33
|
+
status: "active",
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
id: "2",
|
|
37
|
+
name: "Jane Smith",
|
|
38
|
+
email: "jane@example.com",
|
|
39
|
+
role: "user",
|
|
40
|
+
status: "inactive",
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
id: "3",
|
|
44
|
+
name: "Alex Johnson",
|
|
45
|
+
email: "alex@example.com",
|
|
46
|
+
role: "user",
|
|
47
|
+
status: "active",
|
|
48
|
+
},
|
|
49
|
+
];
|
|
50
|
+
|
|
51
|
+
/* ======================================================
|
|
52
|
+
* 3. Columns
|
|
53
|
+
* ====================================================== */
|
|
16
54
|
const columns: ColumnDef<User>[] = [
|
|
17
55
|
{
|
|
18
56
|
accessorKey: "name",
|
|
19
57
|
header: "Name",
|
|
20
|
-
cell: ({ row }) =>
|
|
21
|
-
|
|
58
|
+
cell: ({ row }) => (
|
|
59
|
+
<span className="font-medium">{row.original.name}</span>
|
|
60
|
+
),
|
|
22
61
|
},
|
|
23
62
|
{
|
|
24
63
|
accessorKey: "email",
|
|
25
64
|
header: "Email",
|
|
26
|
-
|
|
65
|
+
cell: ({ getValue }) => (
|
|
66
|
+
<span className="text-blue-600">{getValue<string>()}</span>
|
|
67
|
+
),
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
accessorKey: "role",
|
|
71
|
+
header: "Role",
|
|
72
|
+
cell: ({ getValue }) => (
|
|
73
|
+
<span className="capitalize">{getValue<string>()}</span>
|
|
74
|
+
),
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
accessorKey: "status",
|
|
78
|
+
header: "Status",
|
|
79
|
+
cell: ({ getValue }) => {
|
|
80
|
+
const value = getValue<string>();
|
|
81
|
+
return (
|
|
82
|
+
<span
|
|
83
|
+
className={cn(
|
|
84
|
+
"px-2 py-0.5 rounded text-xs font-medium",
|
|
85
|
+
value === "active"
|
|
86
|
+
? "bg-green-100 text-green-700"
|
|
87
|
+
: "bg-red-100 text-red-700"
|
|
88
|
+
)}
|
|
89
|
+
>
|
|
90
|
+
{value}
|
|
91
|
+
</span>
|
|
92
|
+
);
|
|
93
|
+
},
|
|
27
94
|
},
|
|
28
95
|
];
|
|
29
96
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
97
|
+
/* ======================================================
|
|
98
|
+
* 4. Toolbar
|
|
99
|
+
* ====================================================== */
|
|
100
|
+
const Toolbar = ({ fns }: any) => {
|
|
101
|
+
return (
|
|
102
|
+
<div className="flex items-center gap-2">
|
|
103
|
+
<input
|
|
104
|
+
value={fns.globalFilter ?? ""}
|
|
105
|
+
onChange={(e) => fns.setGlobalFilter(e.target.value)}
|
|
106
|
+
placeholder="Search..."
|
|
107
|
+
className="border rounded px-2 py-1 text-sm"
|
|
108
|
+
/>
|
|
109
|
+
</div>
|
|
110
|
+
);
|
|
111
|
+
};
|
|
37
112
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
113
|
+
/* ======================================================
|
|
114
|
+
* 5. Pagination
|
|
115
|
+
* ====================================================== */
|
|
116
|
+
const Pagination = ({ fns }: any) => {
|
|
117
|
+
return (
|
|
118
|
+
<div className="flex items-center justify-end gap-2">
|
|
119
|
+
<button
|
|
120
|
+
onClick={fns.previousPage}
|
|
121
|
+
disabled={!fns.getCanPreviousPage()}
|
|
122
|
+
className="px-3 py-1 border rounded disabled:opacity-50"
|
|
123
|
+
>
|
|
124
|
+
Prev
|
|
125
|
+
</button>
|
|
126
|
+
|
|
127
|
+
<span className="text-sm">
|
|
128
|
+
Page {fns.pageIndex + 1}
|
|
129
|
+
</span>
|
|
130
|
+
|
|
131
|
+
<button
|
|
132
|
+
onClick={fns.nextPage}
|
|
133
|
+
disabled={!fns.getCanNextPage()}
|
|
134
|
+
className="px-3 py-1 border rounded disabled:opacity-50"
|
|
135
|
+
>
|
|
136
|
+
Next
|
|
137
|
+
</button>
|
|
138
|
+
</div>
|
|
139
|
+
);
|
|
140
|
+
};
|
|
42
141
|
|
|
142
|
+
/* ======================================================
|
|
143
|
+
* 6. useTableProps (row / cell behavior)
|
|
144
|
+
* ====================================================== */
|
|
145
|
+
const useTableProps: UseTableProps<User, unknown> = {
|
|
146
|
+
rowBodyProps: {
|
|
147
|
+
classNameCondition: ({ row }) =>
|
|
148
|
+
row?.original.status === "inactive"
|
|
149
|
+
? "opacity-60"
|
|
150
|
+
: "",
|
|
151
|
+
handleClick: ({ row }) => {
|
|
152
|
+
console.log("Row clicked:", row.original);
|
|
153
|
+
},
|
|
154
|
+
},
|
|
155
|
+
cellBodyProps: {
|
|
156
|
+
classNameCondition: ({ cell }) =>
|
|
157
|
+
cell?.column.id === "email"
|
|
158
|
+
? "underline"
|
|
159
|
+
: "",
|
|
160
|
+
},
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
/* ======================================================
|
|
164
|
+
* 7. Final Component
|
|
165
|
+
* ====================================================== */
|
|
166
|
+
export default function UserTableExample() {
|
|
43
167
|
return (
|
|
44
|
-
<
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
<div className="flex items-center justify-end gap-4">
|
|
76
|
-
<button
|
|
77
|
-
onClick={fns.previousPage}
|
|
78
|
-
disabled={!fns.getCanPreviousPage()}
|
|
79
|
-
className="px-3 py-1 rounded-md bg-gray-200 disabled:opacity-50 dark:bg-gray-700 dark:text-white"
|
|
80
|
-
>
|
|
81
|
-
Trang trước
|
|
82
|
-
</button>
|
|
83
|
-
<span>Trang {fns.pageIndex + 1}</span>
|
|
84
|
-
<button
|
|
85
|
-
onClick={fns.nextPage}
|
|
86
|
-
disabled={!fns.getCanNextPage()}
|
|
87
|
-
className="px-3 py-1 rounded-md bg-gray-200 disabled:opacity-50 dark:bg-gray-700 dark:text-white"
|
|
88
|
-
>
|
|
89
|
-
Trang sau
|
|
90
|
-
</button>
|
|
91
|
-
</div>
|
|
92
|
-
)}
|
|
93
|
-
/>
|
|
168
|
+
<div className="p-4 space-y-4">
|
|
169
|
+
<h1 className="text-lg font-semibold">User Table</h1>
|
|
170
|
+
|
|
171
|
+
<DataTable<User, unknown>
|
|
172
|
+
data={USERS}
|
|
173
|
+
columns={columns}
|
|
174
|
+
enableSort
|
|
175
|
+
alternate="even"
|
|
176
|
+
alternateColor="#f9fafb"
|
|
177
|
+
emptyLabel="No users found"
|
|
178
|
+
initialState={{
|
|
179
|
+
pagination: {
|
|
180
|
+
pageSize: 5,
|
|
181
|
+
pageIndex: 0,
|
|
182
|
+
},
|
|
183
|
+
}}
|
|
184
|
+
toolbarTable={({ fns }) => <Toolbar fns={fns} />}
|
|
185
|
+
paginationTable={({ fns }) => <Pagination fns={fns} />}
|
|
186
|
+
useTableProps={useTableProps}
|
|
187
|
+
classNames={{
|
|
188
|
+
table: "border rounded-md",
|
|
189
|
+
header: {
|
|
190
|
+
head: "bg-gray-50 text-sm font-semibold",
|
|
191
|
+
},
|
|
192
|
+
body: {
|
|
193
|
+
row: "hover:bg-gray-50 cursor-pointer",
|
|
194
|
+
cell: "text-sm",
|
|
195
|
+
},
|
|
196
|
+
}}
|
|
197
|
+
/>
|
|
198
|
+
</div>
|
|
94
199
|
);
|
|
95
200
|
}
|
|
@@ -994,33 +994,70 @@ export const useDataProvider = () => {
|
|
|
994
994
|
|
|
995
995
|
/**
|
|
996
996
|
* Create HTTP client with authentication
|
|
997
|
+
* @param url Base URL for the HTTP client
|
|
998
|
+
* @param options Configuration options for the HTTP client
|
|
999
|
+
* @param options.tokenName Name of the token to use for authentication
|
|
1000
|
+
* @param options.tokenStorage Storage mechanism for the token
|
|
1001
|
+
* @param options.authorizationType Type of authorization (e.g., Bearer, Basic)
|
|
1002
|
+
* @param options.withCredentials Whether to send cookies with requests (defaults to true if tokenStorage is "http-only")
|
|
1003
|
+
* @returns Configured Axios instance
|
|
997
1004
|
*/
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1005
|
+
|
|
1006
|
+
export interface ICreateHttpClientOptions {
|
|
1007
|
+
tokenName?: string ;
|
|
1008
|
+
tokenStorage?: "local" | "session" | "cookie" | "http-only";
|
|
1009
|
+
authorizationType?: "Bearer" | "Basic" | string;
|
|
1010
|
+
withCredentials?: boolean;
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
export interface ICreateHttpClient {
|
|
1014
|
+
url?: string;
|
|
1015
|
+
options?: ICreateHttpClientOptions
|
|
1016
|
+
}
|
|
1017
|
+
export function createHttpClient({url, options = {}}:ICreateHttpClient): AxiosInstance {
|
|
1018
|
+
const {
|
|
1019
|
+
tokenName = "token",
|
|
1020
|
+
tokenStorage = "http-only",
|
|
1021
|
+
authorizationType = "Bearer",
|
|
1022
|
+
} = options;
|
|
1023
|
+
|
|
1024
|
+
const withCredentials =
|
|
1025
|
+
options.withCredentials ?? tokenStorage === "http-only";
|
|
1026
|
+
|
|
1027
|
+
|
|
1004
1028
|
const axiosInstance = axios.create({
|
|
1005
|
-
baseURL:
|
|
1029
|
+
baseURL: url || "https://api.example.com",
|
|
1030
|
+
withCredentials,
|
|
1006
1031
|
});
|
|
1007
1032
|
|
|
1008
|
-
axiosInstance.interceptors.request.use(config => {
|
|
1033
|
+
axiosInstance.interceptors.request.use((config) => {
|
|
1009
1034
|
let token: string | null = null;
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1035
|
+
|
|
1036
|
+
switch (options.tokenStorage) {
|
|
1037
|
+
case "local":
|
|
1038
|
+
token = localStorage.getItem(tokenName);
|
|
1039
|
+
break;
|
|
1040
|
+
|
|
1041
|
+
case "session":
|
|
1042
|
+
token = sessionStorage.getItem(tokenName);
|
|
1043
|
+
break;
|
|
1044
|
+
|
|
1045
|
+
case "cookie":
|
|
1046
|
+
token = cookiesProvider.get(tokenName) ?? null;
|
|
1047
|
+
break;
|
|
1048
|
+
|
|
1049
|
+
case "http-only":
|
|
1050
|
+
// NO READ
|
|
1051
|
+
// HttpOnly cookies are not accessible via JavaScript
|
|
1052
|
+
// Browser will send it automatically
|
|
1053
|
+
break;
|
|
1018
1054
|
}
|
|
1019
|
-
|
|
1055
|
+
|
|
1056
|
+
// Just set token if available in storage
|
|
1020
1057
|
if (token) {
|
|
1021
|
-
config.headers.Authorization = `${
|
|
1022
|
-
}
|
|
1023
|
-
|
|
1058
|
+
config.headers.Authorization = `${authorizationType} ${token}`;
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1024
1061
|
return config;
|
|
1025
1062
|
});
|
|
1026
1063
|
|
|
@@ -1050,15 +1087,36 @@ interface AuthContextValue {
|
|
|
1050
1087
|
isAuthenticated: () => boolean;
|
|
1051
1088
|
setUser: (user: AuthUser | null) => void;
|
|
1052
1089
|
login: (payload: LoginPayload, type?: "full" | "simple") => Promise<any>;
|
|
1053
|
-
logout: () =>
|
|
1090
|
+
logout: (params: CustomParams, type?: TypeResponse) => Promise<any>;
|
|
1054
1091
|
getMe: (type?: "full" | "simple") => Promise<any>;
|
|
1055
1092
|
}
|
|
1056
1093
|
|
|
1094
|
+
/**
|
|
1095
|
+
* Authentication Provider Urls Props
|
|
1096
|
+
* @param loginUrl - URL for login API
|
|
1097
|
+
* @param logoutUrl - URL for logout API
|
|
1098
|
+
* @param meUrl - URL for fetching current user info
|
|
1099
|
+
* @returns AuthProviderUrls
|
|
1100
|
+
*/
|
|
1101
|
+
export interface AuthProviderUrls {
|
|
1102
|
+
loginUrl: string;
|
|
1103
|
+
logoutUrl?: string;
|
|
1104
|
+
meUrl?: string;
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
/**
|
|
1108
|
+
* Authentication Provider Props
|
|
1109
|
+
* @param children - React children nodes
|
|
1110
|
+
* @param urls - AuthProviderUrls
|
|
1111
|
+
* @param tokenKey - Key for token storage
|
|
1112
|
+
* @param keysCleanUpOnLogout - Additional keys to clean up on logout
|
|
1113
|
+
* @returns AuthProviderProps
|
|
1114
|
+
*/
|
|
1057
1115
|
export interface AuthProviderProps {
|
|
1058
1116
|
children: React.ReactNode;
|
|
1059
|
-
|
|
1060
|
-
meUrl: string;
|
|
1117
|
+
urls: AuthProviderUrls;
|
|
1061
1118
|
tokenKey: string;
|
|
1119
|
+
keysCleanUpOnLogout?: string[] | string;
|
|
1062
1120
|
}
|
|
1063
1121
|
|
|
1064
1122
|
export type TypeResponse = "full" | "simple";
|
|
@@ -1075,13 +1133,21 @@ export const useAuth = () => {
|
|
|
1075
1133
|
|
|
1076
1134
|
export const AuthProvider: React.FC<AuthProviderProps> = ({
|
|
1077
1135
|
children,
|
|
1078
|
-
|
|
1079
|
-
meUrl = '/auth/me',
|
|
1136
|
+
urls,
|
|
1080
1137
|
tokenKey = 'token',
|
|
1138
|
+
keysCleanUpOnLogout = ["token"],
|
|
1081
1139
|
}) => {
|
|
1082
1140
|
const dataProvider = useDataProvider();
|
|
1083
1141
|
const [user, setUser] = useState<AuthUser | null>(null);
|
|
1084
1142
|
|
|
1143
|
+
const { loginUrl, meUrl, logoutUrl } = urls;
|
|
1144
|
+
|
|
1145
|
+
function removeKeys(key: string) {
|
|
1146
|
+
cookiesProvider.remove(key);
|
|
1147
|
+
localStorage.removeItem(key);
|
|
1148
|
+
sessionStorage.removeItem(key);
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1085
1151
|
const login = useCallback(async (payload: LoginPayload, type: TypeResponse = "full") => {
|
|
1086
1152
|
try {
|
|
1087
1153
|
const res = await dataProvider.custom<any>({
|
|
@@ -1100,15 +1166,37 @@ export const AuthProvider: React.FC<AuthProviderProps> = ({
|
|
|
1100
1166
|
}
|
|
1101
1167
|
, [dataProvider, loginUrl]);
|
|
1102
1168
|
|
|
1103
|
-
const logout = useCallback(() => {
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1169
|
+
const logout = useCallback(async (params: CustomParams, type: TypeResponse = "full") => {
|
|
1170
|
+
|
|
1171
|
+
try {
|
|
1172
|
+
const res = await dataProvider.custom<any>({
|
|
1173
|
+
url: params.url || logoutUrl,
|
|
1174
|
+
...params,
|
|
1175
|
+
});
|
|
1176
|
+
|
|
1177
|
+
if (type === "simple") {
|
|
1178
|
+
return res.data;
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
return res;
|
|
1182
|
+
} catch (error) {
|
|
1183
|
+
throw error;
|
|
1184
|
+
} finally {
|
|
1185
|
+
setUser(null);
|
|
1186
|
+
dataProvider.clearAllCache();
|
|
1187
|
+
if(Array.isArray(keysCleanUpOnLogout)){
|
|
1188
|
+
keysCleanUpOnLogout.forEach((key) => {
|
|
1189
|
+
removeKeys(key);
|
|
1190
|
+
});
|
|
1191
|
+
} else {
|
|
1192
|
+
removeKeys(keysCleanUpOnLogout);
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
}, [dataProvider, logoutUrl]);
|
|
1109
1197
|
|
|
1110
1198
|
const isAuthenticated = () => {
|
|
1111
|
-
return
|
|
1199
|
+
return user !== null;
|
|
1112
1200
|
};
|
|
1113
1201
|
const getToken = useCallback(() => {
|
|
1114
1202
|
return cookiesProvider.get(tokenKey);
|
|
@@ -1128,12 +1216,17 @@ export const AuthProvider: React.FC<AuthProviderProps> = ({
|
|
|
1128
1216
|
} catch {
|
|
1129
1217
|
return null;
|
|
1130
1218
|
}
|
|
1131
|
-
}, [dataProvider, meUrl
|
|
1219
|
+
}, [dataProvider, meUrl]);
|
|
1220
|
+
|
|
1221
|
+
useEffect(() => {
|
|
1222
|
+
getMe().then((u: any) => u && setUser(u));
|
|
1223
|
+
}, [getMe]);
|
|
1224
|
+
|
|
1132
1225
|
|
|
1133
1226
|
const value: AuthContextValue = {
|
|
1134
1227
|
user,
|
|
1135
1228
|
setUser,
|
|
1136
|
-
token: getToken(),
|
|
1229
|
+
token: getToken() ?? null,
|
|
1137
1230
|
isAuthenticated: isAuthenticated,
|
|
1138
1231
|
login,
|
|
1139
1232
|
logout,
|
|
@@ -1171,28 +1264,39 @@ export const cookiesProvider = {
|
|
|
1171
1264
|
|
|
1172
1265
|
// =================== Example ===================
|
|
1173
1266
|
|
|
1174
|
-
// create httpClient
|
|
1267
|
+
// create httpClient - (can create multiple httpClients for different apis)
|
|
1175
1268
|
|
|
1176
1269
|
// const TOKEN = "token";
|
|
1177
1270
|
|
|
1178
|
-
// const httpClient = createHttpClient(
|
|
1179
|
-
// `${process.env.NEXT_PUBLIC_API_URL}`,
|
|
1180
|
-
//
|
|
1181
|
-
//
|
|
1182
|
-
//
|
|
1183
|
-
//
|
|
1271
|
+
// const httpClient = createHttpClient({
|
|
1272
|
+
// url: `${process.env.NEXT_PUBLIC_API_URL}`,
|
|
1273
|
+
// options: {
|
|
1274
|
+
// authorizationType: "Bearer",
|
|
1275
|
+
// tokenName: TOKEN,
|
|
1276
|
+
// tokenStorage: "cookie",
|
|
1277
|
+
// withCredentials: true, --- optionals (default to true if tokenStorage is "http-only")
|
|
1278
|
+
// },
|
|
1279
|
+
// });
|
|
1184
1280
|
|
|
1185
1281
|
|
|
1186
1282
|
// create dataProvider
|
|
1187
1283
|
|
|
1188
1284
|
// const dataProvider = useDataProvider(httpClient);
|
|
1189
1285
|
|
|
1286
|
+
// const urls = {
|
|
1287
|
+
// loginUrl: "/auth/login", --> api_login
|
|
1288
|
+
// logoutUrl: "/auth/logout", --> api_logout
|
|
1289
|
+
// meUrl: "/auth/me", --> api_get_me_by_token
|
|
1290
|
+
// }
|
|
1291
|
+
|
|
1292
|
+
// const keysRemoveOnLogout = [TOKEN, "refreshToken", "user"];
|
|
1293
|
+
|
|
1190
1294
|
// wrapped all into:
|
|
1191
1295
|
// <DataProvider dataProvider={dataProvider}>
|
|
1192
1296
|
// <AuthProvider
|
|
1193
|
-
//
|
|
1297
|
+
// urls={urls} --> api_login
|
|
1194
1298
|
// tokenKey={TOKEN}
|
|
1195
|
-
//
|
|
1299
|
+
// keysCleanUpOnLogout={keysRemoveOnLogout} --> optional (default to ["token"]) - additional keys to clean up on logout
|
|
1196
1300
|
// >
|
|
1197
1301
|
// <App />
|
|
1198
1302
|
// </AuthProvider>
|