kmod-cli 1.5.0 → 1.7.1

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 CHANGED
@@ -41,64 +41,66 @@ npx kumod add button # add button component
41
41
  ## Components
42
42
 
43
43
  - access-denied
44
+ - api-service
44
45
  - breadcumb
45
- - count-down
46
- - count-input
47
46
  - button
47
+ - calculate
48
48
  - calendar
49
+ - color-by-text
50
+ - column-table
51
+ - config
52
+ - count-down
53
+ - count-input
54
+ - data-table
49
55
  - date-input
50
56
  - date-range-picker
51
- - label
52
- - popover
53
- - select
54
- - switch
55
57
  - datetime-picker
56
- - input
57
- - period-input
58
- - time-picker-input
59
- - time-picker-utils
60
- - time-picker
58
+ - fade-on-scroll
59
+ - feature-config
60
+ - feature-guard
61
61
  - gradient-outline
62
62
  - gradient-svg
63
63
  - grid-layout
64
+ - hash-aes
64
65
  - hydrate-guard
66
+ - idb
65
67
  - image
68
+ - input
69
+ - keys
70
+ - kookies
71
+ - label
72
+ - lib
73
+ - list-map
66
74
  - loader-slash-gradient
67
75
  - masonry-gallery
68
76
  - modal
69
77
  - multi-select
70
78
  - non-hydration
79
+ - period-input
80
+ - popover
71
81
  - portal
82
+ - query
83
+ - rbac
84
+ - readme
85
+ - refine-provider
86
+ - safe-action
72
87
  - segments-circle
88
+ - select
89
+ - simple-validate
73
90
  - single-select
91
+ - spam-guard
92
+ - storage
93
+ - stripe-effect
74
94
  - stroke-circle
75
- - column-table
76
- - data-table
77
- - readme
95
+ - switch
78
96
  - table
79
97
  - text-hover-effect
98
+ - time-picker
99
+ - time-picker-input
100
+ - time-picker-utils
80
101
  - timout-loader
81
102
  - toast
82
- - config
83
- - feature-config
84
- - keys
85
- - api-service
86
- - calculate
87
- - idb
88
- - lib
89
- - storage
90
- - fade-on-scroll
91
- - safe-action
92
- - spam-guard
93
103
  - utils
94
- - feature-guard
95
- - refine-provider
96
- - query
97
- - color-by-text
98
- - stripe-effect
99
- - kookies
100
- - hash-aes
101
- - simple-validate
102
104
  ## Contributing
103
105
 
104
106
  Contributions are welcome! Please open issues or pull requests.
@@ -13,10 +13,13 @@ const componentsConfig = JSON.parse(
13
13
  fs.readFileSync(componentsPath, 'utf8')
14
14
  );
15
15
 
16
+ // sort a-z
17
+ const componentSorted = Object.keys(componentsConfig).sort(
18
+ (a, b) => a.localeCompare(b)
19
+ )
20
+
16
21
  // build list
17
- const componentList = Object.keys(componentsConfig)
18
- .map((k) => `- ${k}`)
19
- .join('\n');
22
+ const componentList = componentSorted.map((k) => `- ${k}`).join('\n');
20
23
 
21
24
  // đọc README
22
25
  const readmeContent = fs.readFileSync(readmePath, 'utf8');
@@ -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
+ // }
@@ -55,7 +55,7 @@ export type TableClassNames = {
55
55
  };
56
56
  export type TableHeaderProps<TData> =
57
57
  HTMLAttributes<HTMLTableSectionElement> & {
58
- handleClick: ({
58
+ handleClick?: ({
59
59
  e,
60
60
  table,
61
61
  }: {
@@ -64,7 +64,7 @@ export type TableHeaderProps<TData> =
64
64
  }) => void;
65
65
  };
66
66
  export type TableBodyProps<TData> = HTMLAttributes<HTMLTableSectionElement> & {
67
- handleClick: ({
67
+ handleClick?: ({
68
68
  e,
69
69
  table,
70
70
  }: {
@@ -73,7 +73,8 @@ export type TableBodyProps<TData> = HTMLAttributes<HTMLTableSectionElement> & {
73
73
  }) => void;
74
74
  };
75
75
  export type TableHeadProps<TData> = HTMLAttributes<HTMLTableCellElement> & {
76
- handleClick: ({
76
+ classNameCondition?: | (({cell, table}: {cell?: Header<TData, unknown>; table?: ITable<TData>}) => string) | string;
77
+ handleClick?: ({
77
78
  e,
78
79
  table,
79
80
  cell,
@@ -85,18 +86,21 @@ export type TableHeadProps<TData> = HTMLAttributes<HTMLTableCellElement> & {
85
86
  };
86
87
  export type TableCellProps<TData, TValue> =
87
88
  HTMLAttributes<HTMLTableCellElement> & {
88
- handleClick: ({
89
+ classNameCondition?: | (({cell, table}: {cell?: Cell<TData, unknown>; table?: ITable<TData>}) => string) | string;
90
+ handleClick?: ({
89
91
  e,
90
92
  table,
91
93
  cell,
94
+ row
92
95
  }: {
93
96
  e: React.MouseEvent<HTMLTableCellElement>;
94
97
  cell: Cell<TData, TValue>;
98
+ row: Row<TData>;
95
99
  table: ITable<TData>;
96
100
  }) => void;
97
101
  };
98
102
  export type TableRowHeadProps<TData> = HTMLAttributes<HTMLTableRowElement> & {
99
- handleClick: ({
103
+ handleClick?: ({
100
104
  e,
101
105
  table,
102
106
  row,
@@ -107,7 +111,8 @@ export type TableRowHeadProps<TData> = HTMLAttributes<HTMLTableRowElement> & {
107
111
  }) => void;
108
112
  };
109
113
  export type TableRowBodyProps<TData> = HTMLAttributes<HTMLTableRowElement> & {
110
- handleClick: ({
114
+ classNameCondition?: | (({row, table}: {row?: Row<TData>; table?: ITable<TData>}) => string) | string;
115
+ handleClick?: ({
111
116
  e,
112
117
  table,
113
118
  row,
@@ -118,7 +123,7 @@ export type TableRowBodyProps<TData> = HTMLAttributes<HTMLTableRowElement> & {
118
123
  }) => void;
119
124
  };
120
125
  export type TableProps<TData> = HTMLAttributes<HTMLTableElement> & {
121
- handleClick: ({
126
+ handleClick?: ({
122
127
  e,
123
128
  table,
124
129
  }: {
@@ -173,6 +178,15 @@ export type DataTableProps<TData, TValue> = {
173
178
  // handles?: Handles
174
179
  };
175
180
 
181
+ export interface UseTablePropsFn<TData, TValue> {
182
+ table: ITable<TData>;
183
+ row?: Row<TData>;
184
+ cell?: Cell<TData, TValue>;
185
+ header?: Header<TData, TValue>;
186
+ headerGroup?: HeaderGroup<TData>;
187
+ }
188
+
189
+
176
190
  export type DataTableToolbarFns<TData> = {
177
191
  globalFilter: string;
178
192
  setGlobalFilter: (value: string) => void;
@@ -204,7 +218,7 @@ export function DataTable<TData, TValue>({
204
218
  useTableProps,
205
219
  initialState,
206
220
  alternate = "even",
207
- alternateColor = "#f5f5f5",
221
+ alternateColor = "#fbfbfb",
208
222
  // handles
209
223
  }: DataTableProps<TData, TValue>) {
210
224
  const table = useReactTable({
@@ -241,47 +255,74 @@ export function DataTable<TData, TValue>({
241
255
  };
242
256
 
243
257
  const {
244
- handleClick: tableHandleClick,
245
- onClick: tableOnClick,
246
- ...tableDomProps
247
- } = useTableProps?.tableProps || {};
258
+ handleClick: tableHandleClick,
259
+ onClick: tableOnClick,
260
+ ...tableDomProps
261
+ } = useTableProps?.tableProps || {};
248
262
 
249
- const {
250
- handleClick: headerHandleClick,
251
- onClick: headerOnClick,
252
- ...headerDomProps
253
- } = useTableProps?.headerProps || {};
254
- const {
255
- handleClick: rowHeadHandleClick,
256
- onClick: rowHeadOnClick,
257
- ...rowHeadDomProps
258
- } = useTableProps?.rowHeadProps || {};
259
- const {
260
- handleClick: bodyHandleClick,
261
- onClick: bodyOnClick,
262
- ...bodyDomProps
263
- } = useTableProps?.bodyProps || {};
264
- const {
265
- handleClick: rowBodyHandleClick,
266
- onClick: rowBodyOnClick,
267
- style: rowBodyStyle,
268
- ...rowBodyDomProps
269
- } = useTableProps?.rowBodyProps || {};
270
- const {
271
- handleClick: cellBodyHandleClick,
272
- onClick: cellBodyOnClick,
273
- ...cellBodyDomProps
274
- } = useTableProps?.cellBodyProps || {};
263
+ const {
264
+ handleClick: headerHandleClick,
265
+ onClick: headerOnClick,
266
+ ...headerDomProps
267
+ } = useTableProps?.headerProps || {};
268
+ const {
269
+ handleClick: rowHeadHandleClick,
270
+ onClick: rowHeadOnClick,
271
+ ...rowHeadDomProps
272
+ } = useTableProps?.rowHeadProps || {};
273
+ const {
274
+ handleClick: cellHeadHandleClick,
275
+ onClick: cellHeadOnClick,
276
+ classNameCondition: cellHeadClassNameCondition,
277
+ ...cellHeadDomProps
278
+ } = useTableProps?.cellHeadProps || {};
279
+ const {
280
+ handleClick: bodyHandleClick,
281
+ onClick: bodyOnClick,
282
+ ...bodyDomProps
283
+ } = useTableProps?.bodyProps || {};
284
+ const {
285
+ handleClick: rowBodyHandleClick,
286
+ onClick: rowBodyOnClick,
287
+ style: rowBodyStyle,
288
+ classNameCondition: rowBodyClassNameCondition,
289
+ ...rowBodyDomProps
290
+ } = useTableProps?.rowBodyProps || {};
291
+ const {
292
+ handleClick: cellBodyHandleClick,
293
+ onClick: cellBodyOnClick,
294
+ classNameCondition: cellBodyClassNameCondition,
295
+ ...cellBodyDomProps
296
+ } = useTableProps?.cellBodyProps || {};
275
297
 
276
- const {
277
- handleClick: skRowHandleClick,
278
- ...skRowDomProps
279
- } = useTableProps?.rowBodyProps || {};
298
+ const { handleClick: skRowHandleClick, classNameCondition: skRowClassNameCondition, ...skRowDomProps } =
299
+ useTableProps?.rowBodyProps || {};
280
300
 
281
- const {
282
- handleClick: skCellHandleClick,
283
- ...skCellDomProps
284
- } = useTableProps?.cellBodyProps || {};
301
+ const { handleClick: skCellHandleClick, classNameCondition: skCellClassNameCondition, ...skCellDomProps } =
302
+ useTableProps?.cellBodyProps || {};
303
+
304
+
305
+ function getCellHeadClassNameByCondition({cell,table}: {cell?: Header<TData, unknown>; table?: ITable<TData>}) {
306
+ if(!cell || !table) return "";
307
+ const classNameCondition = useTableProps?.cellHeadProps?.classNameCondition;
308
+ if(!classNameCondition) return "";
309
+ if(typeof classNameCondition === "string") return classNameCondition;
310
+ return classNameCondition({cell,table});
311
+ }
312
+ function getCellBodyClassNameByCondition({cell,table}: {cell?: Cell<TData, unknown>; table?: ITable<TData>}) {
313
+ if(!cell || !table) return "";
314
+ const classNameCondition = useTableProps?.cellBodyProps?.classNameCondition;
315
+ if(!classNameCondition) return "";
316
+ if(typeof classNameCondition === "string") return classNameCondition;
317
+ return classNameCondition({cell,table});
318
+ }
319
+ function getRowBodyClassNameByCondition({row,table}: {row?: Row<TData>; table?: ITable<TData>}) {
320
+ if(!row || !table) return "";
321
+ const classNameCondition = useTableProps?.rowBodyProps?.classNameCondition;
322
+ if(!classNameCondition) return "";
323
+ if(typeof classNameCondition === "string") return classNameCondition;
324
+ return classNameCondition({row,table});
325
+ }
285
326
 
286
327
 
287
328
  return (
@@ -321,35 +362,28 @@ const {
321
362
  {headerGroup.headers.map((header) => (
322
363
  <TableHead
323
364
  key={header.id}
324
- {...(() => {
325
- const { handleClick, onClick, ...rest } =
326
- useTableProps?.cellHeadProps || {};
327
- return rest;
328
- })()}
365
+ {...cellHeadDomProps}
329
366
  className={cn(
330
367
  "cursor-pointer select-none",
331
368
  classNames?.header?.head
332
369
  )}
333
370
  style={{
334
- width: header.getSize() ? `${header.getSize()}px !important` : "auto",
371
+ width: header.getSize()
372
+ ? `${header.getSize()}px !important`
373
+ : "auto",
335
374
  }}
336
375
  onClick={(e) => {
337
- // Just call the parent's onClick if provided
338
- if (useTableProps?.cellHeadProps?.onClick) {
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
- }
376
+ cellHeadOnClick?.(e);
377
+ cellHeadHandleClick?.({ e, table, cell: header });
350
378
  }}
351
379
  >
352
- <div className={cn("flex items-center gap-1 w-fit", classNames?.header?.content)}>
380
+ <div
381
+ className={cn(
382
+ "flex items-center gap-1 w-fit",
383
+ classNames?.header?.content,
384
+ getCellHeadClassNameByCondition({ cell: header, table })
385
+ )}
386
+ >
353
387
  {flexRender(
354
388
  header.column.columnDef.header,
355
389
  header.getContext()
@@ -384,15 +418,18 @@ const {
384
418
  {!isLoading &&
385
419
  table.getRowModel().rows.length > 0 &&
386
420
  table.getRowModel().rows.map((row, index) => {
387
- const { handleClick, onClick, ...rest } =
388
- useTableProps?.rowBodyProps || {};
389
-
390
421
  return (
391
422
  <TableRow
392
423
  {...rowBodyDomProps}
393
424
  key={row.id}
394
- style={{...rowBodyStyle, backgroundColor: getAlternateColor(index)}}
395
- className={cn(classNames?.body?.row)}
425
+ style={{
426
+ ...rowBodyStyle,
427
+ backgroundColor: getAlternateColor(index),
428
+ }}
429
+ className={cn(
430
+ classNames?.body?.row,
431
+ getRowBodyClassNameByCondition({ row, table })
432
+ )}
396
433
  data-state={row.getIsSelected() && "selected"}
397
434
  onClick={(e) => {
398
435
  rowBodyOnClick?.(e);
@@ -403,10 +440,13 @@ const {
403
440
  <TableCell
404
441
  {...cellBodyDomProps}
405
442
  key={cell.id}
406
- className={cn(classNames?.body?.cell)}
443
+ className={cn(
444
+ classNames?.body?.cell,
445
+ getCellBodyClassNameByCondition({ cell, table })
446
+ )}
407
447
  onClick={(e) => {
408
448
  cellBodyOnClick?.(e);
409
- cellBodyHandleClick?.({ e, cell, table });
449
+ cellBodyHandleClick?.({ e, cell, row, table });
410
450
  }}
411
451
  >
412
452
  {flexRender(
@@ -480,26 +520,27 @@ export const TableSkeleton = <TData, TValue>({
480
520
  const {
481
521
  handleClick: _rowHandleClick,
482
522
  onClick: _rowOnClick,
523
+ classNameCondition: rowClassNameCondition,
483
524
  ...rowDomProps
484
525
  } = props?.rowBodyProps || {};
485
526
 
486
527
  const {
487
528
  handleClick: _cellHandleClick,
488
529
  onClick: _cellOnClick,
530
+ classNameCondition: cellClassNameCondition,
489
531
  ...cellDomProps
490
532
  } = props?.cellBodyProps || {};
491
533
 
492
-
493
534
  if (showNoData) {
494
535
  return (
495
536
  <TableRow
496
537
  key="no-data-skeleton"
497
- className={cn(classNames?.body?.row)}
538
+ className={cn(classNames?.body?.row, )}
498
539
  {...rowDomProps}
499
540
  >
500
541
  <TableCell
501
542
  colSpan={columns.length}
502
- className={cn("h-24 text-center", classNames?.body?.cell)}
543
+ className={cn("h-24 text-center")}
503
544
  {...cellDomProps}
504
545
  >
505
546
  {emptyLabel}
@@ -1,95 +1,200 @@
1
1
  "use client";
2
2
 
3
- import { useState } from "react";
3
+ import React from 'react';
4
4
 
5
- import { ColumnDef } from "@tanstack/react-table";
5
+ import { ColumnDef } from '@tanstack/react-table';
6
6
 
7
- import { DataTable } from "./data-table";
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
- // 1. Columns
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 }) => <div className="font-medium">{row.getValue("name")}</div>,
21
- enableSorting: true,
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
- enableSorting: true,
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
- // 2. Dummy Data
31
- const users: User[] = [
32
- { id: "1", name: "Nguyễn Văn A", email: "a@example.com" },
33
- { id: "2", name: "Trần Thị B", email: "b@example.com" },
34
- { id: "3", name: "Lê Văn C", email: "c@example.com" },
35
- { id: "4", name: "Phạm Thị D", email: "d@example.com" },
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
- // 3. Component chính
39
- export default function UserTableExample() {
40
- const [data, setData] = useState<User[]>(users);
41
- const [isLoading, setIsLoading] = useState(false);
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
- <DataTable
45
- data={data}
46
- columns={columns}
47
- isLoading={isLoading}
48
- emptyLabel="Không có dữ liệu nào"
49
- classNames={{
50
- wrapper: "p-4",
51
- table: "bg-transparent",
52
- header: {
53
- header: "bg-muted",
54
- row: "border-b",
55
- head: "text-left text-sm font-semibold",
56
- },
57
- body: {
58
- body: "bg-transparent",
59
- row: "hover:bg-muted/30",
60
- cell: "text-sm px-2 py-3",
61
- },
62
- }}
63
- toolbarTable={({ table, fns }) => (
64
- <div className="flex items-center gap-2">
65
- <input
66
- type="text"
67
- placeholder="Tìm kiếm..."
68
- value={fns.globalFilter ?? ""}
69
- onChange={(e) => fns.setGlobalFilter(e.target.value)}
70
- className="px-3 py-2 border rounded-md w-64"
71
- />
72
- </div>
73
- )}
74
- paginationTable={({ fns }) => (
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
  }
@@ -0,0 +1,255 @@
1
+
2
+ /* ============================================================
3
+ RBAC FINAL – BACKEND + FRONTEND – SINGLE FILE
4
+ ============================================================ */
5
+
6
+ /* =======================
7
+ REACT HELPERS (OPTIONAL)
8
+ ======================= */
9
+ import React, {
10
+ createContext,
11
+ useContext,
12
+ } from 'react';
13
+
14
+ /* =======================
15
+ SHARED CONCEPT
16
+ ======================= */
17
+ export type ID = string;
18
+ /**
19
+ * @example "read"
20
+ */
21
+ export type Action = string;
22
+ /**
23
+ * @description Resource is route name
24
+ * @example "user"
25
+ */
26
+ export type Resource = string;
27
+ /**
28
+ * @example "user:read"
29
+ */
30
+ export type PermissionKey = `${Resource}:${Action}`;
31
+
32
+ /* =======================
33
+ BACKEND RBAC (AUTHORITATIVE)
34
+ ======================= */
35
+
36
+ export interface Permission {
37
+ id: ID;
38
+ action: Action;
39
+ resource: Resource;
40
+ }
41
+
42
+ export interface Role {
43
+ id: ID;
44
+ name: string;
45
+ permissions: ID[];
46
+ inherits?: ID[];
47
+ }
48
+
49
+ export type ConditionOperator = 'eq' | 'ne' | 'in';
50
+
51
+ export interface Condition {
52
+ field: string; // "user.id", "resource.ownerId", "user.tenantId"
53
+ op: ConditionOperator;
54
+ value: any;
55
+ }
56
+
57
+ export interface Policy {
58
+ id: ID;
59
+ permissionId: ID;
60
+ effect: 'allow' | 'deny';
61
+ conditions?: Condition[];
62
+ }
63
+
64
+ export interface UserIdentity {
65
+ id: ID;
66
+ roles: ID[];
67
+ tenantId?: ID;
68
+ [k: string]: any;
69
+ }
70
+
71
+ /* =======================
72
+ RBAC STORE INTERFACE
73
+ ======================= */
74
+ export interface RBACStore {
75
+ getRoles(ids: ID[]): Promise<Role[]>;
76
+ getPermissionId(action: Action, resource: Resource): Promise<ID | null>;
77
+ getPolicies(permissionId: ID): Promise<Policy[]>;
78
+ }
79
+
80
+ /* =======================
81
+ BACKEND RBAC ENGINE
82
+ ======================= */
83
+
84
+ const getByPath = (obj: any, path: string) =>
85
+ path.split('.').reduce((o, k) => o?.[k], obj);
86
+
87
+ const evalCondition = (cond: Condition, ctx: any) => {
88
+ const left = getByPath(ctx, cond.field);
89
+ if (cond.op === 'eq') return left === cond.value;
90
+ if (cond.op === 'ne') return left !== cond.value;
91
+ if (cond.op === 'in') return Array.isArray(cond.value) && cond.value.includes(left);
92
+ return false;
93
+ };
94
+
95
+ const evalConditions = (conds: Condition[] = [], ctx: any) =>
96
+ conds.every(c => evalCondition(c, ctx));
97
+
98
+ export async function canAccess(
99
+ store: RBACStore,
100
+ user: UserIdentity,
101
+ action: Action,
102
+ resource: Resource,
103
+ context: {
104
+ resource?: any;
105
+ request?: any;
106
+ } = {}
107
+ ): Promise<boolean> {
108
+ const permissionId = await store.getPermissionId(action, resource);
109
+ if (!permissionId) return false;
110
+
111
+ /* ---- Resolve role inheritance ---- */
112
+ const visited = new Set<ID>();
113
+ const collect = async (roleId: ID) => {
114
+ if (visited.has(roleId)) return;
115
+ visited.add(roleId);
116
+ const [role] = await store.getRoles([roleId]);
117
+ for (const p of role?.inherits ?? []) await collect(p);
118
+ };
119
+ for (const r of user.roles) await collect(r);
120
+
121
+ const roles = await store.getRoles([...visited]);
122
+ const hasRoleGrant = roles.some(r => r.permissions.includes(permissionId));
123
+
124
+ /* ---- Evaluate policies ---- */
125
+ const policies = await store.getPolicies(permissionId);
126
+ const ctx = {
127
+ user,
128
+ resource: context.resource,
129
+ request: context.request,
130
+ };
131
+
132
+ for (const p of policies) {
133
+ if (!evalConditions(p.conditions, ctx)) continue;
134
+ if (p.effect === 'deny') return false;
135
+ if (p.effect === 'allow') return hasRoleGrant;
136
+ }
137
+
138
+ return hasRoleGrant;
139
+ }
140
+
141
+ /* =======================
142
+ BACKEND MIDDLEWARE
143
+ ======================= */
144
+
145
+ export const requirePermission =
146
+ (store: RBACStore, action: Action, resource: Resource) =>
147
+ async (req: any, res: any, next: any) => {
148
+ if (!req.user) return res.status(401).end();
149
+
150
+ const ok = await canAccess(store, req.user, action, resource, {
151
+ resource: req.body ?? req.params,
152
+ request: req,
153
+ });
154
+
155
+ if (!ok) return res.status(403).end();
156
+ next();
157
+ };
158
+
159
+ /* =======================
160
+ MEMORY STORE (DEV / DEMO)
161
+ ======================= */
162
+
163
+ export function createMemoryRBACStore(data: {
164
+ roles: Role[];
165
+ permissions: Permission[];
166
+ policies: Policy[];
167
+ }): RBACStore {
168
+ const roleMap = new Map(data.roles.map(r => [r.id, r]));
169
+ const permMap = new Map<PermissionKey, ID>();
170
+ const policyMap = new Map<ID, Policy[]>();
171
+
172
+ data.permissions.forEach(p =>
173
+ permMap.set(`${p.resource}:${p.action}`, p.id)
174
+ );
175
+
176
+ data.policies.forEach(p => {
177
+ const arr = policyMap.get(p.permissionId) ?? [];
178
+ arr.push(p);
179
+ policyMap.set(p.permissionId, arr);
180
+ });
181
+
182
+ return {
183
+ getRoles: async ids => ids.map(id => roleMap.get(id)!).filter(Boolean),
184
+ getPermissionId: async (a, r) => permMap.get(`${r}:${a}`) ?? null,
185
+ getPolicies: async pid => policyMap.get(pid) ?? [],
186
+ };
187
+ }
188
+
189
+ /* =======================
190
+ FRONTEND RBAC (UI ONLY)
191
+ ======================= */
192
+
193
+ export class FrontendRBAC {
194
+ private permissions: Set<PermissionKey>;
195
+
196
+ constructor(perms: PermissionKey[]) {
197
+ this.permissions = new Set(perms);
198
+ }
199
+
200
+ can(action: Action, resource: Resource): boolean {
201
+ return this.permissions.has(`${resource}:${action}`);
202
+ }
203
+ }
204
+
205
+ const RBACContext = createContext<FrontendRBAC | null>(null);
206
+
207
+ export const RBACProvider = ({
208
+ rbac,
209
+ children,
210
+ }: {
211
+ rbac: FrontendRBAC;
212
+ children: React.ReactNode;
213
+ }) => (
214
+ <RBACContext.Provider value={rbac}>
215
+ {children}
216
+ </RBACContext.Provider>
217
+ );
218
+
219
+ export const useCan = (action: Action, resource: Resource) => {
220
+ const rbac = useContext(RBACContext);
221
+ return rbac?.can(action, resource) ?? false;
222
+ };
223
+
224
+ export const Can = ({
225
+ action,
226
+ resource,
227
+ children,
228
+ fallback = null,
229
+ }: any) => {
230
+ const ok = useCan(action, resource);
231
+ return ok ? children : fallback;
232
+ };
233
+
234
+
235
+ // fully usage fe
236
+
237
+ // const rbac = new FrontendRBAC(['user:read', 'user:update']);
238
+ // const store = createMemoryRBACStore({
239
+ // roles: [],
240
+ // permissions: [],
241
+ // policies: [],
242
+ // });
243
+
244
+ // <RBACProvider rbac={rbac}>
245
+ // <Can action="user:read" resource="user">
246
+ // <div>Can read user</div>
247
+ // </Can>
248
+ // </RBACProvider>
249
+
250
+ // fully usage be
251
+
252
+ // import { requirePermission } from './rbac';
253
+ // router.use(requirePermission(store, 'read', 'user'));
254
+
255
+ // app.use(requirePermission(store, 'read', 'user'));
package/components.json CHANGED
@@ -147,6 +147,11 @@
147
147
  "dependencies": [],
148
148
  "devDependencies": []
149
149
  },
150
+ "list-map": {
151
+ "path": "component-templates/components/list-map.tsx",
152
+ "dependencies": [],
153
+ "devDependencies": []
154
+ },
150
155
  "loader-slash-gradient": {
151
156
  "path": "component-templates/components/loader-slash-gradient.tsx",
152
157
  "dependencies": [],
@@ -328,6 +333,11 @@
328
333
  "dependencies": [],
329
334
  "devDependencies": []
330
335
  },
336
+ "rbac": {
337
+ "path": "component-templates/providers/rbac.tsx",
338
+ "dependencies": [],
339
+ "devDependencies": []
340
+ },
331
341
  "refine-provider": {
332
342
  "path": "component-templates/providers/refine-provider.tsx",
333
343
  "dependencies": [
@@ -358,9 +368,7 @@
358
368
  "dependencies": [
359
369
  "js-cookie"
360
370
  ],
361
- "devDependencies": [
362
- "@types/js-cookie"
363
- ]
371
+ "devDependencies": []
364
372
  },
365
373
  "hash-aes": {
366
374
  "path": "component-templates/utils/hash/hash-aes.ts",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kmod-cli",
3
- "version": "1.5.0",
3
+ "version": "1.7.1",
4
4
  "description": "Stack components utilities fast setup in projects",
5
5
  "author": "kumo_d",
6
6
  "license": "MIT",