kmod-cli 1.3.6 → 1.4.6

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.
@@ -8,23 +8,22 @@ import {
8
8
  useState,
9
9
  } from 'react';
10
10
 
11
+ // Nếu bạn cần alias cho ITable type, dùng:
12
+ import type { Table as ITable } from '@tanstack/react-table';
11
13
  import {
14
+ Cell,
12
15
  ColumnDef,
13
16
  flexRender,
14
17
  getCoreRowModel,
15
18
  getFilteredRowModel,
16
19
  getPaginationRowModel,
17
- useReactTable,
18
- } from '@tanstack/react-table';
19
- import {
20
- Cell,
21
20
  getSortedRowModel,
22
21
  Header,
23
22
  HeaderGroup,
24
23
  InitialTableState,
25
24
  Row,
26
- Table as ITable,
27
- } from '@tanstack/table-core';
25
+ useReactTable,
26
+ } from '@tanstack/react-table';
28
27
 
29
28
  import { cn } from '../../lib/utils';
30
29
  import {
@@ -40,6 +39,7 @@ export type TableHeaderClassNames = {
40
39
  header?: string;
41
40
  row?: string;
42
41
  head?: string;
42
+ content?: string;
43
43
  };
44
44
  export type TableBodyClassNames = {
45
45
  body?: string;
@@ -137,20 +137,6 @@ export type UseTableProps<TData, TValue> = {
137
137
  cellHeadProps?: TableHeadProps<TData>;
138
138
  };
139
139
 
140
- // export type Handles = {
141
- // globalFilter?: () => void
142
- // setGlobalFilter?: () => void
143
- // sorting?: () => void
144
- // setSorting?: () => void
145
- // getColumn?: () => void
146
- // previousPage?: () => void
147
- // nextPage?: () => void
148
- // getCanPreviousPage?: () => void
149
- // getCanNextPage?: () => void
150
- // pageIndex?: () => void
151
- // pageSize?: () => void
152
- // }
153
-
154
140
  export type DataTableProps<TData, TValue> = {
155
141
  columns: ColumnDef<TData, TValue>[];
156
142
  data: TData[];
@@ -170,6 +156,8 @@ export type DataTableProps<TData, TValue> = {
170
156
  }) => ReactNode | ReactNode[];
171
157
  isLoading?: boolean;
172
158
  classNames?: TableClassNames;
159
+ alternate?: "even" | "odd";
160
+ alternateColor?: string;
173
161
  emptyLabel?: string;
174
162
  showSortIconHeader?: boolean;
175
163
  surfix?: ({
@@ -215,8 +203,10 @@ export function DataTable<TData, TValue>({
215
203
  enableSort = true,
216
204
  useTableProps,
217
205
  initialState,
218
- }: // handles
219
- DataTableProps<TData, TValue>) {
206
+ alternate = "even",
207
+ alternateColor = "#f5f5f5",
208
+ // handles
209
+ }: DataTableProps<TData, TValue>) {
220
210
  const table = useReactTable({
221
211
  data,
222
212
  columns,
@@ -242,34 +232,91 @@ DataTableProps<TData, TValue>) {
242
232
  pageSize: table.getState().pagination.pageSize,
243
233
  };
244
234
 
235
+ const getAlternateColor = (index: number) => {
236
+ if (alternate === "even") {
237
+ return index % 2 === 0 ? alternateColor : "";
238
+ } else {
239
+ return index % 2 === 0 ? "" : alternateColor;
240
+ }
241
+ };
242
+
243
+ const {
244
+ handleClick: tableHandleClick,
245
+ onClick: tableOnClick,
246
+ ...tableDomProps
247
+ } = useTableProps?.tableProps || {};
248
+
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 || {};
275
+
276
+ const {
277
+ handleClick: skRowHandleClick,
278
+ ...skRowDomProps
279
+ } = useTableProps?.rowBodyProps || {};
280
+
281
+ const {
282
+ handleClick: skCellHandleClick,
283
+ ...skCellDomProps
284
+ } = useTableProps?.cellBodyProps || {};
285
+
286
+
245
287
  return (
246
288
  <div className={cn("space-y-4", classNames?.wrapper)}>
247
289
  {toolbarTable && toolbarTable({ table, fns: toolbarFns })}
248
290
  <div className={cn(classNames?.container)}>
249
291
  <Table
250
292
  className={cn(classNames?.table)}
251
- {...useTableProps?.tableProps}
252
- onClick={(e) => useTableProps?.tableProps?.handleClick({ e, table })}
293
+ {...tableDomProps}
294
+ onClick={(e) => {
295
+ tableOnClick?.(e);
296
+ tableHandleClick?.({ e, table });
297
+ }}
253
298
  >
254
299
  <TableHeader
255
300
  className={cn(classNames?.header?.header)}
256
- {...useTableProps?.headerProps}
257
- onClick={(e) =>
258
- useTableProps?.headerProps?.handleClick({ e, table })
259
- }
301
+ {...headerDomProps}
302
+ onClick={(e) => {
303
+ headerOnClick?.(e);
304
+ headerHandleClick?.({ e, table });
305
+ }}
260
306
  >
261
307
  {table.getHeaderGroups().map((headerGroup) => (
262
308
  <TableRow
263
309
  key={headerGroup.id}
264
310
  className={cn(classNames?.header?.row)}
265
- {...useTableProps?.rowHeadProps}
266
- onClick={(e) =>
267
- useTableProps?.rowHeadProps?.handleClick({
311
+ {...rowHeadDomProps}
312
+ onClick={(e) => {
313
+ rowHeadOnClick?.(e);
314
+ rowHeadHandleClick?.({
268
315
  e,
269
316
  row: headerGroup,
270
317
  table,
271
- })
272
- }
318
+ });
319
+ }}
273
320
  >
274
321
  {headerGroup.headers.map((header) => (
275
322
  <TableHead
@@ -283,6 +330,9 @@ DataTableProps<TData, TValue>) {
283
330
  "cursor-pointer select-none",
284
331
  classNames?.header?.head
285
332
  )}
333
+ style={{
334
+ width: header.getSize() ? `${header.getSize()}px !important` : "auto",
335
+ }}
286
336
  onClick={(e) => {
287
337
  // Just call the parent's onClick if provided
288
338
  if (useTableProps?.cellHeadProps?.onClick) {
@@ -299,7 +349,7 @@ DataTableProps<TData, TValue>) {
299
349
  }
300
350
  }}
301
351
  >
302
- <div className="flex items-center gap-1">
352
+ <div className={cn("flex items-center gap-1 w-fit", classNames?.header?.content)}>
303
353
  {flexRender(
304
354
  header.column.columnDef.header,
305
355
  header.getContext()
@@ -316,8 +366,11 @@ DataTableProps<TData, TValue>) {
316
366
 
317
367
  <TableBody
318
368
  className={cn(classNames?.body?.body)}
319
- {...useTableProps?.bodyProps}
320
- onClick={(e) => useTableProps?.bodyProps?.handleClick({ e, table })}
369
+ {...bodyDomProps}
370
+ onClick={(e) => {
371
+ bodyOnClick?.(e);
372
+ bodyHandleClick?.({ e, table });
373
+ }}
321
374
  >
322
375
  {isLoading && (
323
376
  <TableSkeleton
@@ -330,61 +383,30 @@ DataTableProps<TData, TValue>) {
330
383
  )}
331
384
  {!isLoading &&
332
385
  table.getRowModel().rows.length > 0 &&
333
- table.getRowModel().rows.map((row) => {
386
+ table.getRowModel().rows.map((row, index) => {
334
387
  const { handleClick, onClick, ...rest } =
335
388
  useTableProps?.rowBodyProps || {};
336
389
 
337
390
  return (
338
391
  <TableRow
339
- {...(() => {
340
- const { handleClick, onClick, ...rest } =
341
- useTableProps?.rowBodyProps || {};
342
- return rest;
343
- })()}
392
+ {...rowBodyDomProps}
344
393
  key={row.id}
394
+ style={{...rowBodyStyle, backgroundColor: getAlternateColor(index)}}
345
395
  className={cn(classNames?.body?.row)}
346
396
  data-state={row.getIsSelected() && "selected"}
347
- // {...useTableProps?.rowBodyProps}
348
397
  onClick={(e) => {
349
- // Just call the parent's onClick if provided
350
- if (useTableProps?.rowBodyProps?.onClick) {
351
- useTableProps.rowBodyProps.onClick(e);
352
- }
353
-
354
- // Just call the parent's handleClick if provided
355
- if (useTableProps?.rowBodyProps?.handleClick) {
356
- useTableProps.rowBodyProps.handleClick({
357
- e,
358
- row,
359
- table,
360
- });
361
- }
398
+ rowBodyOnClick?.(e);
399
+ rowBodyHandleClick?.({ e, row, table });
362
400
  }}
363
401
  >
364
402
  {row.getVisibleCells().map((cell) => (
365
403
  <TableCell
366
- {...(() => {
367
- const { handleClick, onClick, ...rest } =
368
- useTableProps?.cellBodyProps || {};
369
- return rest;
370
- })()}
404
+ {...cellBodyDomProps}
371
405
  key={cell.id}
372
406
  className={cn(classNames?.body?.cell)}
373
- // {...useTableProps?.cellBodyProps}
374
407
  onClick={(e) => {
375
- // Just call the parent's onClick if provided
376
- if (useTableProps?.cellBodyProps?.onClick) {
377
- useTableProps.cellBodyProps.onClick(e);
378
- }
379
-
380
- // Just call the parent's handleClick if provided
381
- if (useTableProps?.cellBodyProps?.handleClick) {
382
- useTableProps.cellBodyProps.handleClick({
383
- e,
384
- cell,
385
- table,
386
- });
387
- }
408
+ cellBodyOnClick?.(e);
409
+ cellBodyHandleClick?.({ e, cell, table });
388
410
  }}
389
411
  >
390
412
  {flexRender(
@@ -400,12 +422,12 @@ DataTableProps<TData, TValue>) {
400
422
  <TableRow
401
423
  key="no-data"
402
424
  className={cn(classNames?.body?.row)}
403
- {...useTableProps?.rowBodyProps}
425
+ {...skRowDomProps}
404
426
  >
405
427
  <TableCell
406
428
  colSpan={columns.length}
407
429
  className={cn("h-24 text-center", classNames?.body?.cell)}
408
- {...useTableProps?.cellBodyProps}
430
+ {...skCellDomProps}
409
431
  >
410
432
  {emptyLabel}
411
433
  </TableCell>
@@ -455,17 +477,30 @@ export const TableSkeleton = <TData, TValue>({
455
477
  };
456
478
  }, [isLoading]);
457
479
 
480
+ const {
481
+ handleClick: _rowHandleClick,
482
+ onClick: _rowOnClick,
483
+ ...rowDomProps
484
+ } = props?.rowBodyProps || {};
485
+
486
+ const {
487
+ handleClick: _cellHandleClick,
488
+ onClick: _cellOnClick,
489
+ ...cellDomProps
490
+ } = props?.cellBodyProps || {};
491
+
492
+
458
493
  if (showNoData) {
459
494
  return (
460
495
  <TableRow
461
496
  key="no-data-skeleton"
462
497
  className={cn(classNames?.body?.row)}
463
- {...props?.rowBodyProps}
498
+ {...rowDomProps}
464
499
  >
465
500
  <TableCell
466
501
  colSpan={columns.length}
467
502
  className={cn("h-24 text-center", classNames?.body?.cell)}
468
- {...props?.cellBodyProps}
503
+ {...cellDomProps}
469
504
  >
470
505
  {emptyLabel}
471
506
  </TableCell>
@@ -478,13 +513,13 @@ export const TableSkeleton = <TData, TValue>({
478
513
  <TableRow
479
514
  key={`skeleton-${rowIndex}`}
480
515
  className={cn(classNames?.body?.row)}
481
- {...props?.rowBodyProps}
516
+ {...rowDomProps}
482
517
  >
483
518
  {columns.map((_, colIndex) => (
484
519
  <TableCell
485
520
  key={`skeleton-${rowIndex}-${colIndex}`}
486
521
  className={cn(classNames?.body?.cell)}
487
- {...props?.cellBodyProps}
522
+ {...cellDomProps}
488
523
  >
489
524
  <div className="shimmer h-4 w-full" />
490
525
  </TableCell>
@@ -494,3 +529,5 @@ export const TableSkeleton = <TData, TValue>({
494
529
  </>
495
530
  );
496
531
  };
532
+
533
+
@@ -0,0 +1,20 @@
1
+ .shimmer {
2
+ background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
3
+ background-size: 200% 100%;
4
+ animation: shimmer 2s infinite ease;
5
+ }
6
+
7
+ @keyframes shimmer {
8
+ 0% {
9
+ background-position: -200% 0;
10
+ }
11
+ 100% {
12
+ background-position: 200% 0;
13
+ }
14
+ }
15
+
16
+ .shimmer-element {
17
+ width: 100%;
18
+ height: 40px;
19
+ border-radius: 4px;
20
+ }
@@ -1,5 +1,7 @@
1
1
  import {
2
+ createContext,
2
3
  useCallback,
4
+ useContext,
3
5
  useEffect,
4
6
  useState,
5
7
  } from 'react';
@@ -9,6 +11,7 @@ import axios, {
9
11
  AxiosInstance,
10
12
  AxiosRequestConfig,
11
13
  } from 'axios';
14
+ import Cookies from 'js-cookie';
12
15
 
13
16
  // ============ TYPES ============
14
17
 
@@ -135,6 +138,7 @@ export interface DataProviderOptions {
135
138
  cacheTime?: number;
136
139
  retryCount?: number;
137
140
  retryDelay?: number;
141
+ debug?: boolean;
138
142
  }
139
143
 
140
144
  interface CacheItem<T = any> {
@@ -156,8 +160,54 @@ export interface UseMutationOptions<TData = any> {
156
160
  onError?: (error: DataProviderError) => void;
157
161
  }
158
162
 
163
+ // ============ UTILITY FUNCTIONS ============
164
+
165
+ /**
166
+ * Safely normalize API response data
167
+ */
168
+ function normalizeResponseData<T>(response: any): T[] {
169
+ // Case 1: Direct array response
170
+ if (Array.isArray(response)) {
171
+ return response;
172
+ }
173
+
174
+ // Case 2: Object with 'data' property
175
+ if (response && typeof response === 'object') {
176
+ return response
177
+ }
178
+
179
+ // Fallback: empty array
180
+ console.warn('Unable to extract array data from response:', response);
181
+ return [];
182
+ }
183
+
184
+ /**
185
+ * Extract total count from response
186
+ */
187
+ function extractTotalCount(response: any, dataLength: number): number {
188
+ // Check headers first
189
+ if (response.headers?.['x-total-count']) {
190
+ const count = parseInt(response.headers['x-total-count'], 10);
191
+ if (!isNaN(count)) return count;
192
+ }
193
+
194
+ // Check response body
195
+ const data = response.data;
196
+ if (data && typeof data === 'object') {
197
+ if (typeof data.total === 'number') return data.total;
198
+ if (typeof data.totalCount === 'number') return data.totalCount;
199
+ if (typeof data.count === 'number') return data.count;
200
+ }
201
+
202
+ // Fallback to data length
203
+ return dataLength;
204
+ }
205
+
159
206
  // ============ DATA PROVIDER CLASS ============
160
207
 
208
+ /**
209
+ * DataProvider class for handling API requests with caching and retry mechanisms
210
+ */
161
211
  class DataProvider {
162
212
  private apiUrl: string;
163
213
  private httpClient: AxiosInstance;
@@ -169,17 +219,24 @@ class DataProvider {
169
219
  httpClient: AxiosInstance = axios,
170
220
  options: DataProviderOptions = {}
171
221
  ) {
172
- this.apiUrl = apiUrl;
222
+ this.apiUrl = apiUrl.endsWith('/') ? apiUrl.slice(0, -1) : apiUrl;
173
223
  this.httpClient = httpClient;
174
224
  this.cache = new Map();
175
225
  this.options = {
176
226
  cacheTime: 5 * 60 * 1000,
177
227
  retryCount: 3,
178
228
  retryDelay: 1000,
229
+ debug: false,
179
230
  ...options
180
231
  };
181
232
  }
182
233
 
234
+ private log(message: string, data?: any): void {
235
+ if (this.options.debug) {
236
+ console.log(`[DataProvider] ${message}`, data || '');
237
+ }
238
+ }
239
+
183
240
  // Cache helpers
184
241
  private getCacheKey(resource: string, params?: any): string {
185
242
  return `${resource}:${JSON.stringify(params || {})}`;
@@ -192,9 +249,11 @@ class DataProvider {
192
249
  const now = Date.now();
193
250
  if (now - cached.timestamp > this.options.cacheTime) {
194
251
  this.cache.delete(key);
252
+ this.log('Cache expired', key);
195
253
  return null;
196
254
  }
197
255
 
256
+ this.log('Cache hit', key);
198
257
  return cached.data as T;
199
258
  }
200
259
 
@@ -203,19 +262,34 @@ class DataProvider {
203
262
  data,
204
263
  timestamp: Date.now()
205
264
  });
265
+ this.log('Cache set', key);
206
266
  }
207
267
 
208
- public invalidateCache(resource: string): void {
268
+ public invalidateCache(resource: string, id?: string | number): void {
209
269
  const keys = Array.from(this.cache.keys());
210
- keys.forEach(key => {
211
- if (key.startsWith(`${resource}:`)) {
212
- this.cache.delete(key);
213
- }
214
- });
270
+
271
+ if (id !== undefined) {
272
+ // Invalidate specific item and related lists
273
+ keys.forEach(key => {
274
+ if (key.startsWith(`${resource}:`) || key.startsWith(`${resource}/${id}:`)) {
275
+ this.cache.delete(key);
276
+ this.log('Cache invalidated', key);
277
+ }
278
+ });
279
+ } else {
280
+ // Invalidate entire resource
281
+ keys.forEach(key => {
282
+ if (key.startsWith(`${resource}:`)) {
283
+ this.cache.delete(key);
284
+ this.log('Cache invalidated', key);
285
+ }
286
+ });
287
+ }
215
288
  }
216
289
 
217
290
  public clearAllCache(): void {
218
291
  this.cache.clear();
292
+ this.log('All cache cleared');
219
293
  }
220
294
 
221
295
  // Retry logic
@@ -228,7 +302,7 @@ class DataProvider {
228
302
  } catch (error) {
229
303
  if (retries <= 0) throw error;
230
304
 
231
- // Không retry với lỗi 4xx (client errors)
305
+ // Don't retry on 4xx errors (client errors)
232
306
  const axiosError = error as AxiosError;
233
307
  if (
234
308
  axiosError.response &&
@@ -238,6 +312,8 @@ class DataProvider {
238
312
  throw error;
239
313
  }
240
314
 
315
+ this.log(`Retrying... (${this.options.retryCount - retries + 1}/${this.options.retryCount})`);
316
+
241
317
  await new Promise(resolve =>
242
318
  setTimeout(resolve, this.options.retryDelay)
243
319
  );
@@ -308,25 +384,19 @@ class DataProvider {
308
384
 
309
385
  try {
310
386
  const result = await this.retryRequest(async () => {
311
- const response = await this.httpClient.get<T[] | { data: T[], total: number }>(
312
- url,
313
- {
314
- params: query,
315
- ...meta
316
- }
317
- );
387
+ this.log(`GET ${url}`, query);
388
+
389
+ const response = await this.httpClient.get(url, {
390
+ params: query,
391
+ ...meta
392
+ });
318
393
 
319
- const isArrayResponse = Array.isArray(response.data);
320
- const data = isArrayResponse ? response.data : (response.data as any).data || [];
394
+ const data = normalizeResponseData<T>(response.data);
395
+ const total = extractTotalCount(response, data.length);
321
396
 
322
- const total = response.headers['x-total-count']
323
- ? parseInt(response.headers['x-total-count'] as string)
324
- : (isArrayResponse ? data.length : (response.data as any).total || 0);
397
+ this.log(`Response: ${data.length} items, total: ${total}`);
325
398
 
326
- return {
327
- data: Array.isArray(data) ? data : [],
328
- total
329
- };
399
+ return { data, total };
330
400
  });
331
401
 
332
402
  if (useCache) {
@@ -335,6 +405,7 @@ class DataProvider {
335
405
 
336
406
  return result;
337
407
  } catch (error) {
408
+ this.log('Error in getList', error);
338
409
  throw this.handleError(error);
339
410
  }
340
411
  }
@@ -356,8 +427,13 @@ class DataProvider {
356
427
 
357
428
  try {
358
429
  const result = await this.retryRequest(async () => {
430
+ this.log(`GET ${url}`);
359
431
  const response = await this.httpClient.get<T>(url, meta);
360
- return { data: response.data };
432
+
433
+ // Handle wrapped response
434
+ const data = (response.data as any)?.data || response.data;
435
+
436
+ return { data };
361
437
  });
362
438
 
363
439
  if (useCache) {
@@ -366,6 +442,7 @@ class DataProvider {
366
442
 
367
443
  return result;
368
444
  } catch (error) {
445
+ this.log('Error in getOne', error);
369
446
  throw this.handleError(error);
370
447
  }
371
448
  }
@@ -380,17 +457,21 @@ class DataProvider {
380
457
 
381
458
  try {
382
459
  const result = await this.retryRequest(async () => {
383
- const response = await this.httpClient.get<T[]>(url, {
460
+ this.log(`GET ${url} with ids`, ids);
461
+
462
+ const response = await this.httpClient.get(url, {
384
463
  params: { id: ids },
385
464
  ...meta
386
465
  });
387
- return {
388
- data: Array.isArray(response.data) ? response.data : []
389
- };
466
+
467
+ const data = normalizeResponseData<T>(response.data);
468
+
469
+ return { data };
390
470
  });
391
471
 
392
472
  return result;
393
473
  } catch (error) {
474
+ this.log('Error in getMany', error);
394
475
  throw this.handleError(error);
395
476
  }
396
477
  }
@@ -404,13 +485,19 @@ class DataProvider {
404
485
 
405
486
  try {
406
487
  const result = await this.retryRequest(async () => {
488
+ this.log(`POST ${url}`, variables);
407
489
  const response = await this.httpClient.post<T>(url, variables, meta);
408
- return { data: response.data };
490
+
491
+ // Handle wrapped response
492
+ const data = (response.data as any)?.data || response.data;
493
+
494
+ return { data };
409
495
  });
410
496
 
411
497
  this.invalidateCache(resource);
412
498
  return result;
413
499
  } catch (error) {
500
+ this.log('Error in create', error);
414
501
  throw this.handleError(error);
415
502
  }
416
503
  }
@@ -423,17 +510,23 @@ class DataProvider {
423
510
 
424
511
  try {
425
512
  const result = await this.retryRequest(async () => {
513
+ this.log(`POST MANY ${this.apiUrl}/${resource}`, variables);
514
+
426
515
  const responses = await Promise.all(
427
516
  variables.map(variable =>
428
517
  this.httpClient.post<T>(`${this.apiUrl}/${resource}`, variable, meta)
429
518
  )
430
519
  );
431
- return { data: responses.map(r => r.data) };
520
+
521
+ const data = responses.map(r => (r.data as any)?.data || r.data);
522
+
523
+ return { data };
432
524
  });
433
525
 
434
526
  this.invalidateCache(resource);
435
527
  return result;
436
528
  } catch (error) {
529
+ this.log('Error in createMany', error);
437
530
  throw this.handleError(error);
438
531
  }
439
532
  }
@@ -447,13 +540,19 @@ class DataProvider {
447
540
 
448
541
  try {
449
542
  const result = await this.retryRequest(async () => {
543
+ this.log(`PATCH ${url}`, variables);
450
544
  const response = await this.httpClient.patch<T>(url, variables, meta);
451
- return { data: response.data };
545
+
546
+ // Handle wrapped response
547
+ const data = (response.data as any)?.data || response.data;
548
+
549
+ return { data };
452
550
  });
453
551
 
454
- this.invalidateCache(resource);
552
+ this.invalidateCache(resource, id);
455
553
  return result;
456
554
  } catch (error) {
555
+ this.log('Error in update', error);
457
556
  throw this.handleError(error);
458
557
  }
459
558
  }
@@ -466,6 +565,8 @@ class DataProvider {
466
565
 
467
566
  try {
468
567
  const result = await this.retryRequest(async () => {
568
+ this.log(`PATCH MANY ${this.apiUrl}/${resource}`, { ids, variables });
569
+
469
570
  const responses = await Promise.all(
470
571
  ids.map(id =>
471
572
  this.httpClient.patch<T>(
@@ -475,12 +576,16 @@ class DataProvider {
475
576
  )
476
577
  )
477
578
  );
478
- return { data: responses.map(r => r.data) };
579
+
580
+ const data = responses.map(r => (r.data as any)?.data || r.data);
581
+
582
+ return { data };
479
583
  });
480
584
 
481
- this.invalidateCache(resource);
585
+ ids.forEach(id => this.invalidateCache(resource, id));
482
586
  return result;
483
587
  } catch (error) {
588
+ this.log('Error in updateMany', error);
484
589
  throw this.handleError(error);
485
590
  }
486
591
  }
@@ -494,13 +599,19 @@ class DataProvider {
494
599
 
495
600
  try {
496
601
  const result = await this.retryRequest(async () => {
602
+ this.log(`DELETE ${url}`);
497
603
  const response = await this.httpClient.delete<T>(url, meta);
498
- return { data: response.data };
604
+
605
+ // Handle wrapped response
606
+ const data = (response.data as any)?.data || response.data;
607
+
608
+ return { data };
499
609
  });
500
610
 
501
- this.invalidateCache(resource);
611
+ this.invalidateCache(resource, id);
502
612
  return result;
503
613
  } catch (error) {
614
+ this.log('Error in deleteOne', error);
504
615
  throw this.handleError(error);
505
616
  }
506
617
  }
@@ -513,17 +624,23 @@ class DataProvider {
513
624
 
514
625
  try {
515
626
  const result = await this.retryRequest(async () => {
627
+ this.log(`DELETE MANY ${this.apiUrl}/${resource}`, ids);
628
+
516
629
  const responses = await Promise.all(
517
630
  ids.map(id =>
518
631
  this.httpClient.delete<T>(`${this.apiUrl}/${resource}/${id}`, meta)
519
632
  )
520
633
  );
521
- return { data: responses.map(r => r.data) };
634
+
635
+ const data = responses.map(r => (r.data as any)?.data || r.data);
636
+
637
+ return { data };
522
638
  });
523
639
 
524
- this.invalidateCache(resource);
640
+ ids.forEach(id => this.invalidateCache(resource, id));
525
641
  return result;
526
642
  } catch (error) {
643
+ this.log('Error in deleteMany', error);
527
644
  throw this.handleError(error);
528
645
  }
529
646
  }
@@ -533,16 +650,22 @@ class DataProvider {
533
650
 
534
651
  try {
535
652
  return await this.retryRequest(async () => {
653
+ const fullUrl = url.startsWith('http') ? url : `${this.apiUrl}${url}`;
654
+ this.log(`${method.toUpperCase()} ${fullUrl}`, { payload, query });
655
+
536
656
  const response = await this.httpClient<T>({
537
- url: `${this.apiUrl}${url}`,
657
+ url: fullUrl,
538
658
  method,
539
659
  data: payload,
540
660
  params: query,
541
661
  headers,
542
662
  });
543
- return { data: response.data };
663
+
664
+
665
+ return { data: (response?.data as any) || response };
544
666
  });
545
667
  } catch (error) {
668
+ this.log('Error in custom', error);
546
669
  throw this.handleError(error);
547
670
  }
548
671
  }
@@ -551,14 +674,19 @@ class DataProvider {
551
674
  const axiosError = error as AxiosError<any>;
552
675
 
553
676
  if (axiosError.response) {
677
+ const responseData = axiosError.response.data;
678
+
554
679
  return {
555
- message: axiosError.response.data?.message || axiosError.message || 'Request failed',
680
+ message: responseData?.message ||
681
+ responseData?.error ||
682
+ axiosError.message ||
683
+ 'Request failed',
556
684
  statusCode: axiosError.response.status,
557
- errors: axiosError.response.data?.errors,
685
+ errors: responseData?.errors || responseData?.details,
558
686
  };
559
687
  } else if (axiosError.request) {
560
688
  return {
561
- message: 'Network error',
689
+ message: 'Network error - no response received',
562
690
  statusCode: 0,
563
691
  };
564
692
  }
@@ -570,24 +698,30 @@ class DataProvider {
570
698
  }
571
699
  }
572
700
 
701
+ export const DataProviderContext = createContext<DataProvider | null>(null);
702
+
573
703
  // ============ REACT HOOKS ============
574
704
 
575
705
  export function useList<T = any>(
576
- dataProvider: DataProvider,
577
706
  resource: string,
578
707
  params: GetListParams = {},
579
708
  options: UseListOptions = {}
580
709
  ) {
581
- const [data, setData] = useState<T[]>([]);
710
+ const [data, setData] = useState<T[] | T | any | null>(null);
582
711
  const [total, setTotal] = useState<number>(0);
583
712
  const [loading, setLoading] = useState<boolean>(true);
584
713
  const [error, setError] = useState<DataProviderError | null>(null);
714
+
715
+ const dataProvider = useDataProvider();
585
716
 
586
717
  const { refetchInterval, enabled = true } = options;
587
718
  const paramsStr = JSON.stringify(params);
588
719
 
589
720
  const refetch = useCallback(async () => {
590
- if (!enabled) return;
721
+ if (!enabled) {
722
+ setLoading(false);
723
+ return;
724
+ }
591
725
 
592
726
  try {
593
727
  setLoading(true);
@@ -615,11 +749,10 @@ export function useList<T = any>(
615
749
  }
616
750
  }, [refetchInterval, refetch, enabled]);
617
751
 
618
- return { data, total, loading, error, refetch };
752
+ return { data: data || [], total, loading, error, refetch };
619
753
  }
620
754
 
621
755
  export function useOne<T = any>(
622
- dataProvider: DataProvider,
623
756
  resource: string,
624
757
  id: string | number | null | undefined,
625
758
  options: UseOneOptions = {}
@@ -627,6 +760,8 @@ export function useOne<T = any>(
627
760
  const [data, setData] = useState<T | null>(null);
628
761
  const [loading, setLoading] = useState<boolean>(true);
629
762
  const [error, setError] = useState<DataProviderError | null>(null);
763
+
764
+ const dataProvider = useDataProvider();
630
765
 
631
766
  const { enabled = true } = options;
632
767
 
@@ -657,13 +792,14 @@ export function useOne<T = any>(
657
792
  }
658
793
 
659
794
  export function useCreate<T = any, V = any>(
660
- dataProvider: DataProvider,
661
795
  resource: string,
662
796
  options: UseMutationOptions<T> = {}
663
797
  ) {
664
798
  const [loading, setLoading] = useState<boolean>(false);
665
799
  const [error, setError] = useState<DataProviderError | null>(null);
666
800
 
801
+ const dataProvider = useDataProvider();
802
+
667
803
  const { onSuccess, onError } = options;
668
804
 
669
805
  const mutate = useCallback(async (variables: V): Promise<T> => {
@@ -692,12 +828,13 @@ export function useCreate<T = any, V = any>(
692
828
  }
693
829
 
694
830
  export function useUpdate<T = any, V = any>(
695
- dataProvider: DataProvider,
696
831
  resource: string,
697
832
  options: UseMutationOptions<T> = {}
698
833
  ) {
699
834
  const [loading, setLoading] = useState<boolean>(false);
700
835
  const [error, setError] = useState<DataProviderError | null>(null);
836
+
837
+ const dataProvider = useDataProvider();
701
838
 
702
839
  const { onSuccess, onError } = options;
703
840
 
@@ -730,12 +867,13 @@ export function useUpdate<T = any, V = any>(
730
867
  }
731
868
 
732
869
  export function useDelete<T = any>(
733
- dataProvider: DataProvider,
734
870
  resource: string,
735
871
  options: UseMutationOptions<T> = {}
736
872
  ) {
737
873
  const [loading, setLoading] = useState<boolean>(false);
738
874
  const [error, setError] = useState<DataProviderError | null>(null);
875
+
876
+ const dataProvider = useDataProvider();
739
877
 
740
878
  const { onSuccess, onError } = options;
741
879
 
@@ -767,165 +905,290 @@ export function useDelete<T = any>(
767
905
  return { mutate, loading, error };
768
906
  }
769
907
 
908
+ export function useCustom<T = any>(
909
+ resource: string,
910
+ options: UseMutationOptions<T> = {}
911
+ ) {
912
+ const [loading, setLoading] = useState<boolean>(false);
913
+ const [error, setError] = useState<DataProviderError | null>(null);
914
+ const [customData, setCustomData] = useState<T | null>(null);
915
+
916
+ const dataProvider = useDataProvider();
917
+
918
+ const { onSuccess, onError } = options;
919
+
920
+ const mutate = useCallback(
921
+ async (variables: any): Promise<T> => {
922
+ setLoading(true);
923
+ setError(null);
924
+
925
+ try {
926
+ const result = await dataProvider.custom<T>({ url: resource, ...variables });
927
+ if (onSuccess) {
928
+ onSuccess(result.data);
929
+ }
930
+ setCustomData(result.data);
931
+ return result.data;
932
+ } catch (err) {
933
+ const errorObj = err as DataProviderError;
934
+ setError(errorObj);
935
+ if (onError) {
936
+ onError(errorObj);
937
+ }
938
+ throw errorObj;
939
+ } finally {
940
+ setLoading(false);
941
+ }
942
+ },
943
+ [dataProvider, resource, onSuccess, onError]
944
+ );
945
+
946
+ return { mutate, loading, error, data: customData };
947
+ }
948
+
770
949
  export default DataProvider;
771
950
 
772
- /* ============ DỤ SỬ DỤNG ============
951
+ export const DataProviderContainer: React.FC<{
952
+ dataProvider: DataProvider;
953
+ children: React.ReactNode;
954
+ }> = ({ dataProvider, children }) => {
955
+ return (
956
+ <DataProviderContext.Provider value={dataProvider}>
957
+ {children}
958
+ </DataProviderContext.Provider>
959
+ );
960
+ };
961
+
962
+ export const useDataProvider = () => {
963
+ const ctx = useContext(DataProviderContext);
964
+ if (!ctx) {
965
+ throw new Error(
966
+ "useDataProvider must be used inside <DataProviderContainer />"
967
+ );
968
+ }
969
+ return ctx;
970
+ };
971
+
972
+ /**
973
+ * Create HTTP client with authentication
974
+ */
975
+ export function createHttpClient(
976
+ baseURL: string,
977
+ authTokenKey: string = 'token',
978
+ authTokenStorage: 'localStorage' | 'sessionStorage' | 'cookie' = 'cookie',
979
+ typeAuthorization: "Bearer" | "Basic" | string = "Bearer"
980
+ ): AxiosInstance {
981
+ const axiosInstance = axios.create({
982
+ baseURL: baseURL || 'https://api.example.com',
983
+ });
984
+
985
+ axiosInstance.interceptors.request.use(config => {
986
+ let token: string | null = null;
987
+
988
+ if (authTokenStorage === 'localStorage') {
989
+ token = localStorage.getItem(authTokenKey);
990
+ } else if (authTokenStorage === 'sessionStorage') {
991
+ token = sessionStorage.getItem(authTokenKey);
992
+ } else if (authTokenStorage === 'cookie') {
993
+ const match = document.cookie.match(new RegExp('(^| )' + authTokenKey + '=([^;]+)'));
994
+ if (match) token = match[2];
995
+ }
996
+
997
+ if (token) {
998
+ config.headers.Authorization = `${typeAuthorization} ${token}`;
999
+ }
1000
+
1001
+ return config;
1002
+ });
1003
+
1004
+ return axiosInstance;
1005
+ }
1006
+
1007
+ // ================================ AUTH ================================
1008
+
1009
+
1010
+ export interface AuthUser {
1011
+ id: string | number;
1012
+ email?: string;
1013
+ username?: string;
1014
+ role?: string;
1015
+ [key: string]: any;
1016
+ }
773
1017
 
774
- // 1. Setup DataProvider
775
- import DataProvider, { useList, useOne, useCreate, useUpdate, useDelete } from './DataProvider';
776
- import axios from 'axios';
1018
+ export interface LoginPayload {
1019
+ username?: string;
1020
+ email?: string;
1021
+ password: string;
1022
+ }
777
1023
 
778
- interface User {
779
- id: number;
780
- name: string;
781
- email: string;
782
- status: 'active' | 'inactive';
783
- createdAt: string;
1024
+ interface AuthContextValue {
1025
+ user: AuthUser | null;
1026
+ token: string | null | undefined;
1027
+ isAuthenticated: () => boolean;
1028
+ setUser: (user: AuthUser | null) => void;
1029
+ login: (payload: LoginPayload, type?: "full" | "simple") => Promise<any>;
1030
+ logout: () => void;
1031
+ getMe: (type?: "full" | "simple") => Promise<any>;
784
1032
  }
785
1033
 
786
- interface CreateUserInput {
787
- name: string;
788
- email: string;
789
- status?: 'active' | 'inactive';
1034
+ export interface AuthProviderProps {
1035
+ children: React.ReactNode;
1036
+ loginUrl: string;
1037
+ meUrl: string;
1038
+ tokenKey: string;
790
1039
  }
791
1040
 
792
- const axiosInstance = axios.create({
793
- baseURL: 'https://api.example.com',
794
- });
1041
+ export type TypeResponse = "full" | "simple";
1042
+
1043
+ const AuthContext = createContext<AuthContextValue | null>(null);
795
1044
 
796
- axiosInstance.interceptors.request.use(config => {
797
- const token = localStorage.getItem('token');
798
- if (token) {
799
- config.headers.Authorization = `Bearer ${token}`;
1045
+ export const useAuth = () => {
1046
+ const ctx = useContext(AuthContext);
1047
+ if (!ctx) {
1048
+ throw new Error("useAuth must be used inside <AuthProvider />");
800
1049
  }
801
- return config;
802
- });
803
-
804
- const dataProvider = new DataProvider('', axiosInstance, {
805
- cacheTime: 5 * 60 * 1000,
806
- retryCount: 3,
807
- retryDelay: 1000
808
- });
809
-
810
- // 2. Sử dụng trong component
811
- function UsersList() {
812
- const [page, setPage] = useState(1);
813
-
814
- const { data, total, loading, error, refetch } = useList<User>(
815
- dataProvider,
816
- 'users',
817
- {
818
- pagination: { current: page, pageSize: 10 },
819
- sorters: [{ field: 'createdAt', order: 'desc' }],
820
- filters: [{ field: 'status', operator: 'eq', value: 'active' }]
821
- },
822
- { refetchInterval: 30000 }
823
- );
824
-
825
- const { mutate: createUser, loading: creating } = useCreate<User, CreateUserInput>(
826
- dataProvider,
827
- 'users',
828
- {
829
- onSuccess: (data) => {
830
- console.log('Created:', data);
831
- refetch();
832
- },
833
- onError: (err) => {
834
- console.error('Error:', err);
1050
+ return ctx;
1051
+ };
1052
+
1053
+ export const AuthProvider: React.FC<AuthProviderProps> = ({
1054
+ children,
1055
+ loginUrl = '/auth/login',
1056
+ meUrl = '/auth/me',
1057
+ tokenKey = 'token',
1058
+ }) => {
1059
+ const dataProvider = useDataProvider();
1060
+ const [user, setUser] = useState<AuthUser | null>(null);
1061
+
1062
+ const login = useCallback(async (payload: LoginPayload, type: TypeResponse = "full") => {
1063
+ try {
1064
+ const res = await dataProvider.custom<any>({
1065
+ url: loginUrl,
1066
+ method: "post",
1067
+ payload,
1068
+ });
1069
+
1070
+ if (type === "simple") {
1071
+ return res.data;
1072
+ }
1073
+ return res;
1074
+ } catch (error) {
1075
+ throw error;
835
1076
  }
836
1077
  }
837
- );
838
-
839
- const { mutate: updateUser } = useUpdate<User, Partial<User>>(
840
- dataProvider,
841
- 'users',
842
- {
843
- onSuccess: () => refetch()
844
- }
845
- );
846
-
847
- const { mutate: deleteUser } = useDelete<User>(
848
- dataProvider,
849
- 'users',
850
- {
851
- onSuccess: () => refetch()
852
- }
853
- );
854
-
855
- const handleCreate = async () => {
856
- try {
857
- await createUser({
858
- name: 'John Doe',
859
- email: 'john@example.com',
860
- status: 'active'
861
- });
862
- } catch (err) {
863
- // Error handled
864
- }
865
- };
866
-
867
- const handleUpdate = async (id: number) => {
868
- try {
869
- await updateUser(id, { name: 'Jane Doe' });
870
- } catch (err) {
871
- // Error handled
872
- }
1078
+ , [dataProvider, loginUrl]);
1079
+
1080
+ const logout = useCallback(() => {
1081
+ cookiesProvider.remove(tokenKey);
1082
+ localStorage.clear();
1083
+ sessionStorage.clear();
1084
+ dataProvider.clearAllCache();
1085
+ }, [dataProvider]);
1086
+
1087
+ const isAuthenticated = () => {
1088
+ return (cookiesProvider.get(tokenKey) !== null && cookiesProvider.get(tokenKey) !== undefined && cookiesProvider.get(tokenKey) !== "" && typeof cookiesProvider.get(tokenKey) === "string" && user !== null) ? true : false;
873
1089
  };
874
-
875
- const handleDelete = async (id: number) => {
1090
+ const getToken = useCallback(() => {
1091
+ return cookiesProvider.get(tokenKey);
1092
+ }, [tokenKey]);
1093
+
1094
+ const getMe = useCallback(async (type?: TypeResponse) => {
876
1095
  try {
877
- await deleteUser(id);
878
- } catch (err) {
879
- // Error handled
1096
+ const res = await dataProvider.custom<AuthUser>({
1097
+ url: meUrl,
1098
+ method: 'get',
1099
+ });
1100
+
1101
+ if (type === "simple") {
1102
+ return res.data;
1103
+ }
1104
+ return res;
1105
+ } catch {
1106
+ return null;
880
1107
  }
1108
+ }, [dataProvider, meUrl, logout]);
1109
+
1110
+ const value: AuthContextValue = {
1111
+ user,
1112
+ setUser,
1113
+ token: getToken(),
1114
+ isAuthenticated: isAuthenticated,
1115
+ login,
1116
+ logout,
1117
+ getMe,
881
1118
  };
882
-
883
- if (loading) return <div>Loading...</div>;
884
- if (error) return <div>Error: {error.message}</div>;
885
-
886
- return (
887
- <div>
888
- <button onClick={handleCreate} disabled={creating}>
889
- Create User
890
- </button>
891
-
892
- {data.map(user => (
893
- <div key={user.id}>
894
- <span>{user.name}</span>
895
- <button onClick={() => handleUpdate(user.id)}>Edit</button>
896
- <button onClick={() => handleDelete(user.id)}>Delete</button>
897
- </div>
898
- ))}
899
-
900
- <button onClick={() => setPage(p => p - 1)} disabled={page === 1}>
901
- Previous
902
- </button>
903
- <button onClick={() => setPage(p => p + 1)}>Next</button>
904
-
905
- <div>Total: {total}</div>
906
- </div>
907
- );
908
- }
909
1119
 
910
- // 3. Chi tiết user
911
- function UserDetail({ userId }: { userId: number }) {
912
- const { data, loading, error, refetch } = useOne<User>(
913
- dataProvider,
914
- 'users',
915
- userId
916
- );
917
-
918
- if (loading) return <div>Loading...</div>;
919
- if (error) return <div>Error: {error.message}</div>;
920
- if (!data) return <div>No data</div>;
1120
+ return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
1121
+ };
1122
+
1123
+ export const cookiesProvider = {
1124
+ set: (name: string, value: string, days?: number) => {
1125
+ Cookies.set(name, value, {
1126
+ expires: days || 365 * 100, // Default to 100 years if not specified
1127
+ path: "/",
1128
+ secure: process.env.NODE_ENV === "production",
1129
+ sameSite: "Lax",
1130
+ });
1131
+ },
1132
+
1133
+ get: (name: string): string | undefined => {
1134
+ return Cookies.get(name);
1135
+ },
1136
+
1137
+ remove: (name: string) => {
1138
+ Cookies.remove(name, {path: "/"});
1139
+ },
1140
+
921
1141
 
922
- return (
923
- <div>
924
- <h1>{data.name}</h1>
925
- <p>{data.email}</p>
926
- <button onClick={refetch}>Refresh</button>
927
- </div>
928
- );
929
- }
1142
+ exists: (name: string): boolean => {
1143
+ return Cookies.get(name) !== undefined;
1144
+ },
1145
+ };
1146
+
1147
+
1148
+
1149
+ // =================== Example ===================
1150
+
1151
+ // wrapped all into:
1152
+ // <DataProvider>
1153
+ // <AuthProvider>
1154
+ // <App />
1155
+ // </AuthProvider>
1156
+ // </DataProvider>
1157
+
1158
+
1159
+ // use hooks to auth
1160
+
1161
+ // const { login, logout, refresh } = useAuth();
1162
+
1163
+ // use hook to call apis
1164
+
1165
+ // const { data, isLoading, error } = useList<DataResponse>({
1166
+ // url: '/route_name',
1167
+ // });
1168
+
1169
+ // const { data, isLoading, error } = useOne<DataResponse>({
1170
+ // url: '/route_name/:id',
1171
+ // id: 1,
1172
+ // });
1173
+
1174
+ // const { data, isLoading, error } = useCreate<DataResponse>({
1175
+ // url: '/route_name',
1176
+ // payload: {},
1177
+ // });
1178
+
1179
+ // const { data, isLoading, error } = useUpdate<DataResponse>({
1180
+ // url: '/route_name/:id',
1181
+ // id: 1,
1182
+ // payload: {},
1183
+ // });
1184
+
1185
+ // const { data, isLoading, error } = useDelete<DataResponse>({
1186
+ // url: '/route_name/:id',
1187
+ // id: 1,
1188
+ // });
930
1189
 
931
- */
1190
+ // const { data, isLoading, error } = useCustom<DataResponse>({
1191
+ // url: '/route_name',
1192
+ // method: 'post',
1193
+ // payload: {},
1194
+ // });
package/components.json CHANGED
@@ -215,8 +215,7 @@
215
215
  "data-table": {
216
216
  "path": "component-templates/components/table/data-table.tsx",
217
217
  "dependencies": [
218
- "@tanstack/react-table",
219
- "@tanstack/table-core"
218
+ "@tanstack/react-table"
220
219
  ],
221
220
  "devDependencies": []
222
221
  },
@@ -332,7 +331,8 @@
332
331
  "refine-provider": {
333
332
  "path": "component-templates/providers/refine-provider.tsx",
334
333
  "dependencies": [
335
- "axios"
334
+ "axios",
335
+ "js-cookie"
336
336
  ],
337
337
  "devDependencies": []
338
338
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kmod-cli",
3
- "version": "1.3.6",
3
+ "version": "1.4.6",
4
4
  "description": "Stack components utilities fast setup in projects",
5
5
  "author": "kumo_d",
6
6
  "license": "MIT",