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
|
-
|
|
27
|
-
} from '@tanstack/table
|
|
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
|
-
|
|
219
|
-
|
|
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
|
-
{...
|
|
252
|
-
onClick={(e) =>
|
|
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
|
-
{...
|
|
257
|
-
onClick={(e) =>
|
|
258
|
-
|
|
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
|
-
{...
|
|
266
|
-
onClick={(e) =>
|
|
267
|
-
|
|
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
|
-
{...
|
|
320
|
-
onClick={(e) =>
|
|
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
|
-
|
|
350
|
-
|
|
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
|
-
|
|
376
|
-
|
|
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
|
-
{...
|
|
425
|
+
{...skRowDomProps}
|
|
404
426
|
>
|
|
405
427
|
<TableCell
|
|
406
428
|
colSpan={columns.length}
|
|
407
429
|
className={cn("h-24 text-center", classNames?.body?.cell)}
|
|
408
|
-
{...
|
|
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
|
-
{...
|
|
498
|
+
{...rowDomProps}
|
|
464
499
|
>
|
|
465
500
|
<TableCell
|
|
466
501
|
colSpan={columns.length}
|
|
467
502
|
className={cn("h-24 text-center", classNames?.body?.cell)}
|
|
468
|
-
{...
|
|
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
|
-
{...
|
|
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
|
-
{...
|
|
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
|
-
|
|
211
|
-
|
|
212
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
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
|
|
320
|
-
const
|
|
394
|
+
const data = normalizeResponseData<T>(response.data);
|
|
395
|
+
const total = extractTotalCount(response, data.length);
|
|
321
396
|
|
|
322
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
388
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
657
|
+
url: fullUrl,
|
|
538
658
|
method,
|
|
539
659
|
data: payload,
|
|
540
660
|
params: query,
|
|
541
661
|
headers,
|
|
542
662
|
});
|
|
543
|
-
|
|
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:
|
|
680
|
+
message: responseData?.message ||
|
|
681
|
+
responseData?.error ||
|
|
682
|
+
axiosError.message ||
|
|
683
|
+
'Request failed',
|
|
556
684
|
statusCode: axiosError.response.status,
|
|
557
|
-
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)
|
|
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
|
-
|
|
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
|
-
|
|
775
|
-
|
|
776
|
-
|
|
1018
|
+
export interface LoginPayload {
|
|
1019
|
+
username?: string;
|
|
1020
|
+
email?: string;
|
|
1021
|
+
password: string;
|
|
1022
|
+
}
|
|
777
1023
|
|
|
778
|
-
interface
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
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
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
1034
|
+
export interface AuthProviderProps {
|
|
1035
|
+
children: React.ReactNode;
|
|
1036
|
+
loginUrl: string;
|
|
1037
|
+
meUrl: string;
|
|
1038
|
+
tokenKey: string;
|
|
790
1039
|
}
|
|
791
1040
|
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
1041
|
+
export type TypeResponse = "full" | "simple";
|
|
1042
|
+
|
|
1043
|
+
const AuthContext = createContext<AuthContextValue | null>(null);
|
|
795
1044
|
|
|
796
|
-
|
|
797
|
-
const
|
|
798
|
-
if (
|
|
799
|
-
|
|
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
|
|
802
|
-
}
|
|
803
|
-
|
|
804
|
-
const
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
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
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
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
|
-
|
|
1090
|
+
const getToken = useCallback(() => {
|
|
1091
|
+
return cookiesProvider.get(tokenKey);
|
|
1092
|
+
}, [tokenKey]);
|
|
1093
|
+
|
|
1094
|
+
const getMe = useCallback(async (type?: TypeResponse) => {
|
|
876
1095
|
try {
|
|
877
|
-
await
|
|
878
|
-
|
|
879
|
-
|
|
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
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
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
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
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
|
},
|