@ramesesinc/platform-core 0.1.6 → 0.1.8

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.
Files changed (87) hide show
  1. package/dist/components/action/LookupPage.js +9 -31
  2. package/dist/components/action/ViewPage.d.ts +2 -0
  3. package/dist/components/action/ViewPage.js +25 -31
  4. package/dist/components/common/UIComponent.js +4 -3
  5. package/dist/components/table/DataList.js +2 -2
  6. package/dist/components/view/PopupView.d.ts +13 -0
  7. package/dist/components/view/PopupView.js +25 -20
  8. package/dist/core/DataContext.d.ts +7 -4
  9. package/dist/core/DataContext.js +16 -4
  10. package/dist/core/Page.js +25 -26
  11. package/dist/core/PageCache.js +7 -7
  12. package/dist/core/PageContext.js +17 -7
  13. package/dist/core/PageViewContext.d.ts +13 -1
  14. package/dist/core/PageViewContext.js +75 -2
  15. package/dist/core/PopupContext.d.ts +49 -0
  16. package/dist/core/PopupContext.js +380 -0
  17. package/dist/core/RowContext.js +1 -1
  18. package/dist/core/WindowContext.d.ts +15 -0
  19. package/dist/core/WindowContext.js +28 -0
  20. package/dist/core/index.d.ts +16 -0
  21. package/dist/index.css +25 -7
  22. package/dist/lib/utils/BeanUtils.js +7 -7
  23. package/dist/templates/DataListTemplate.js +7 -2
  24. package/dist/templates/ExplorerTemplate.js +1 -1
  25. package/package.json +5 -5
  26. package/dist/components/action/AlertMessage.tsx +0 -38
  27. package/dist/components/action/Button.tsx +0 -230
  28. package/dist/components/action/CancelEdit.tsx +0 -40
  29. package/dist/components/action/DeleteData.tsx +0 -73
  30. package/dist/components/action/Edit.tsx +0 -40
  31. package/dist/components/action/LookupPage.tsx +0 -113
  32. package/dist/components/action/ProcessRunner.tsx +0 -337
  33. package/dist/components/action/Refresh.tsx +0 -35
  34. package/dist/components/action/SaveData.tsx +0 -74
  35. package/dist/components/action/SelectData.tsx +0 -47
  36. package/dist/components/action/Undo.tsx +0 -50
  37. package/dist/components/action/UpdateData.tsx +0 -49
  38. package/dist/components/action/UpdateState.tsx +0 -40
  39. package/dist/components/action/ViewBackPage.tsx +0 -46
  40. package/dist/components/action/ViewPage.tsx +0 -141
  41. package/dist/components/common/UIComponent.tsx +0 -86
  42. package/dist/components/common/UIInput.tsx +0 -49
  43. package/dist/components/common/UIMenu.tsx +0 -91
  44. package/dist/components/index.ts +0 -51
  45. package/dist/components/input/CodeEditor.tsx +0 -188
  46. package/dist/components/input/DateField.tsx +0 -274
  47. package/dist/components/input/DayPicker.tsx +0 -5
  48. package/dist/components/input/HtmlCode.tsx +0 -203
  49. package/dist/components/input/JsonCode.tsx +0 -205
  50. package/dist/components/input/MonthPicker.tsx +0 -5
  51. package/dist/components/input/ScriptCode.tsx +0 -195
  52. package/dist/components/input/Select.tsx +0 -78
  53. package/dist/components/input/SqlCode.tsx +0 -162
  54. package/dist/components/input/StringDecision.tsx +0 -64
  55. package/dist/components/input/Text.tsx +0 -57
  56. package/dist/components/input/YearPicker.tsx +0 -81
  57. package/dist/components/list/IconMenu.tsx +0 -115
  58. package/dist/components/list/TabMenu.tsx +0 -127
  59. package/dist/components/list/TreeMenu.tsx +0 -279
  60. package/dist/components/list/TxnTaskList.tsx +0 -198
  61. package/dist/components/output/Label.tsx +0 -50
  62. package/dist/components/table/DataList.tsx +0 -820
  63. package/dist/components/table/DataTable.tsx +0 -572
  64. package/dist/components/table/ListHandler.ts +0 -276
  65. package/dist/components/table/TableContext.tsx +0 -122
  66. package/dist/components/view/ComponentView.tsx +0 -102
  67. package/dist/components/view/FilterView.tsx +0 -21
  68. package/dist/components/view/HtmlForm.tsx +0 -176
  69. package/dist/components/view/HtmlView.tsx +0 -98
  70. package/dist/components/view/IFrameView.tsx +0 -48
  71. package/dist/components/view/Modal.tsx +0 -72
  72. package/dist/components/view/PageView.tsx +0 -131
  73. package/dist/components/view/PopupView.tsx +0 -160
  74. package/dist/components/view/RootView.tsx +0 -109
  75. package/dist/components/view/WizardView.tsx +0 -48
  76. package/dist/lib/layouts/BorderLayout.tsx +0 -31
  77. package/dist/lib/layouts/CardLayout.tsx +0 -73
  78. package/dist/lib/layouts/CenterLayout.tsx +0 -20
  79. package/dist/lib/layouts/GridLayout.tsx +0 -20
  80. package/dist/lib/layouts/HPanel.tsx +0 -31
  81. package/dist/lib/layouts/HorizontalLayout.tsx +0 -29
  82. package/dist/lib/layouts/MainLayout.tsx +0 -16
  83. package/dist/lib/layouts/PageLayout.tsx +0 -29
  84. package/dist/lib/layouts/VPanel.tsx +0 -27
  85. package/dist/lib/layouts/XLayout.tsx +0 -29
  86. package/dist/lib/layouts/YLayout.tsx +0 -29
  87. package/dist/lib/layouts/index.ts +0 -13
@@ -1,572 +0,0 @@
1
- import React, { memo, ReactNode, useMemo } from "react";
2
- import { RowProvider } from "../../core/RowContext";
3
- import { render } from "../../lib/utils/ExprUtil";
4
- import { useTableContext } from "./TableContext";
5
-
6
- // import "./DataTable.css";
7
-
8
- // ============================================================================
9
- // TYPE DEFINITIONS (unchanged from original)
10
- // ============================================================================
11
-
12
- export interface ColumnDefinition {
13
- id?: string;
14
- title?: string | React.ReactNode;
15
- width?: string | number;
16
- align?: "left" | "center" | "right";
17
- render?: (value: any, row: any, rowIndex: number) => React.ReactNode;
18
- formatter?: (value: any, row?: any) => string;
19
- colspan?: number;
20
- rowspan?: number;
21
- headerRow?: number;
22
- datatype?: "string" | "number" | "date" | "boolean" | "currency" | "expr";
23
- subrow?: number;
24
- visible?: boolean;
25
- sortable?: boolean;
26
- expr?: string;
27
- primary?: boolean;
28
- component?: string | null;
29
- }
30
-
31
- export interface DataTableProps {
32
- // When used INSIDE TableProvider these can be omitted; the context drives them.
33
- data?: any[];
34
- columns?: ColumnDefinition[];
35
-
36
- loading?: boolean;
37
- emptyMessage?: string;
38
- striped?: boolean;
39
- bordered?: boolean;
40
- hover?: boolean;
41
- dense?: boolean;
42
- stickyHeader?: boolean;
43
- maxHeight?: string | number;
44
- rowKey?: string;
45
- onRowClick?: (row: any, rowIndex: number) => void;
46
- onCellClick?: (value: any, row: any, column: ColumnDefinition, rowIndex: number, colIndex: number) => void;
47
- selectedRows?: string[];
48
- onSelectionChange?: (selectedKeys: string[]) => void;
49
- selectable?: boolean;
50
- className?: string;
51
- rowClassName?: (row: any, rowIndex: number) => string;
52
- cellRenderer?: (value: any, row: any, column: ColumnDefinition) => React.ReactNode;
53
- headerRenderer?: (column: ColumnDefinition) => React.ReactNode;
54
- multiRowHeader?: ColumnDefinition[][];
55
- rowActions?: (row: any, rowIndex: number) => React.ReactNode;
56
- rowsPerItem?: number;
57
- }
58
-
59
- interface SortConfig {
60
- key: string | null;
61
- direction: "asc" | "desc" | null;
62
- }
63
-
64
- // ============================================================================
65
- // HEADER sub-component – pure, only re-renders when columns change
66
- // ============================================================================
67
-
68
- interface TableHeaderProps {
69
- visibleColumns: ColumnDefinition[];
70
- selectable: boolean;
71
- rowActions?: (row: any, rowIndex: number) => React.ReactNode;
72
- data: any[];
73
- selectedRows: string[];
74
- handleSelectAll: (e: React.ChangeEvent<HTMLInputElement>) => void;
75
- sortConfig: SortConfig;
76
- handleSort: (columnId: string, sortable?: boolean) => void;
77
- headerRenderer?: (column: ColumnDefinition) => React.ReactNode;
78
- multiRowHeader?: ColumnDefinition[][];
79
- }
80
-
81
- const TableHeader = memo(
82
- ({
83
- visibleColumns,
84
- selectable,
85
- rowActions,
86
- data,
87
- selectedRows,
88
- handleSelectAll,
89
- sortConfig,
90
- handleSort,
91
- headerRenderer,
92
- multiRowHeader,
93
- }: TableHeaderProps) => {
94
- const getColumnKey = (column: ColumnDefinition, index: number): string => column.id || column.expr || `col-${index}`;
95
-
96
- const getColumnTitle = (column: ColumnDefinition): React.ReactNode => column.title || column.id || "";
97
-
98
- const renderHeader = (column: ColumnDefinition) => {
99
- if (headerRenderer) return headerRenderer(column);
100
- const title = getColumnTitle(column);
101
- if (!title) return null;
102
- return (
103
- <div
104
- style={{
105
- display: "flex",
106
- alignItems: "center",
107
- justifyContent: column.align === "center" ? "center" : column.align === "right" ? "flex-end" : "flex-start",
108
- gap: "6px",
109
- cursor: column.sortable ? "pointer" : "default",
110
- userSelect: "none",
111
- }}
112
- onClick={() => column.id && handleSort(column.id, column.sortable)}
113
- >
114
- {title}
115
- {column.sortable && column.id && sortConfig.key === column.id && (
116
- <span style={{ fontSize: "10px", color: "#3b82f6" }}>{sortConfig.direction === "asc" ? "▲" : "▼"}</span>
117
- )}
118
- </div>
119
- );
120
- };
121
-
122
- if (multiRowHeader && multiRowHeader.length > 0) {
123
- return (
124
- <>
125
- {multiRowHeader.map((headerRow, rowIndex) => (
126
- <tr key={`header-row-${rowIndex}`}>
127
- {selectable && rowIndex === 0 && (
128
- <th className="dt-checkbox-cell" rowSpan={multiRowHeader.length}>
129
- <input type="checkbox" onChange={handleSelectAll} checked={data.length > 0 && selectedRows.length === data.length} />
130
- </th>
131
- )}
132
- {headerRow.map((column, colIndex) => (
133
- <th
134
- key={getColumnKey(column, colIndex)}
135
- style={{ width: column.width, textAlign: column.align || "left" }}
136
- colSpan={column.colspan}
137
- rowSpan={column.rowspan}
138
- >
139
- {renderHeader(column)}
140
- </th>
141
- ))}
142
- {rowActions && rowIndex === 0 && (
143
- <th className="dt-actions-cell" rowSpan={multiRowHeader.length}>
144
- Actions
145
- </th>
146
- )}
147
- </tr>
148
- ))}
149
- </>
150
- );
151
- }
152
-
153
- // Auto multi-row detection
154
- const needsMultiRow = visibleColumns.some(
155
- (col) => (col.rowspan && col.rowspan > 1) || (col.colspan && col.colspan > 1) || (col.headerRow !== undefined && col.headerRow > 0),
156
- );
157
-
158
- if (needsMultiRow) {
159
- const maxHeaderRow = Math.max(...visibleColumns.filter((col) => !col.subrow || col.subrow === 0).map((col) => col.headerRow ?? 0), 0);
160
- const totalHeaderRows = maxHeaderRow + 1;
161
- const headerRows: JSX.Element[] = [];
162
-
163
- for (let rowIdx = 0; rowIdx <= maxHeaderRow; rowIdx++) {
164
- const colsInRow = visibleColumns.filter((col) => {
165
- if (col.subrow && col.subrow > 0) return false;
166
- return (col.headerRow ?? 0) === rowIdx;
167
- });
168
- if (colsInRow.length === 0) continue;
169
-
170
- headerRows.push(
171
- <tr key={`header-row-${rowIdx}`}>
172
- {selectable && rowIdx === 0 && (
173
- <th className="dt-checkbox-cell" rowSpan={totalHeaderRows}>
174
- <input type="checkbox" onChange={handleSelectAll} checked={data.length > 0 && selectedRows.length === data.length} />
175
- </th>
176
- )}
177
- {colsInRow.map((column, index) => (
178
- <th
179
- key={getColumnKey(column, index)}
180
- style={{
181
- width: column.width,
182
- textAlign: column.align || (column.colspan ? "center" : "left"),
183
- }}
184
- rowSpan={column.rowspan || 1}
185
- colSpan={column.colspan || 1}
186
- className={column.sortable ? "sortable" : ""}
187
- >
188
- {renderHeader(column)}
189
- </th>
190
- ))}
191
- {rowActions && rowIdx === 0 && (
192
- <th className="dt-actions-cell" rowSpan={totalHeaderRows}>
193
- Actions
194
- </th>
195
- )}
196
- </tr>,
197
- );
198
- }
199
- return <>{headerRows}</>;
200
- }
201
-
202
- // Simple single-row header
203
- return (
204
- <tr>
205
- {selectable && (
206
- <th className="dt-checkbox-cell">
207
- <input type="checkbox" onChange={handleSelectAll} checked={data.length > 0 && selectedRows.length === data.length} />
208
- </th>
209
- )}
210
- {visibleColumns.map((column, index) => {
211
- if (column.subrow && column.subrow > 0) return null;
212
- return (
213
- <th
214
- key={getColumnKey(column, index)}
215
- style={{ width: column.width, textAlign: column.align || "left" }}
216
- className={column.sortable ? "sortable" : ""}
217
- >
218
- {renderHeader(column)}
219
- </th>
220
- );
221
- })}
222
- {rowActions && <th className="dt-actions-cell">Actions</th>}
223
- </tr>
224
- );
225
- },
226
- );
227
-
228
- // ============================================================================
229
- // BODY sub-component – pure, only re-renders when rows change
230
- // ============================================================================
231
-
232
- interface TableBodyProps {
233
- sortedData: any[];
234
- visibleColumns: ColumnDefinition[];
235
- emptyMessage: string;
236
- selectable: boolean;
237
- rowActions?: (row: any, rowIndex: number) => React.ReactNode;
238
- selectedRows: string[];
239
- handleSelectRow: (row: any) => void;
240
- handleRowClick: (row: any, rowIndex: number) => void;
241
- handleCellClick: (e: React.MouseEvent, value: any, row: any, column: ColumnDefinition, rowIndex: number, colIndex: number) => void;
242
- rowClassName?: (row: any, rowIndex: number) => string;
243
- rowKey: string;
244
- rowsPerItem: number;
245
- cellRenderer?: (value: any, row: any, column: ColumnDefinition) => React.ReactNode;
246
- }
247
-
248
- const TableBody = memo(
249
- ({
250
- sortedData,
251
- visibleColumns,
252
- emptyMessage,
253
- selectable,
254
- rowActions,
255
- selectedRows,
256
- handleSelectRow,
257
- handleRowClick,
258
- handleCellClick,
259
- rowClassName,
260
- rowKey,
261
- rowsPerItem,
262
- cellRenderer,
263
- }: TableBodyProps) => {
264
- const generateKey = () => `RP-${Math.random().toString(36).slice(2)}`;
265
- const getNestedValue = (obj: any, path?: string): any => {
266
- if (!path) return null;
267
- return path.split(".").reduce((acc, part) => acc && acc[part], obj);
268
- };
269
-
270
- const getRowKey = (row: any): string => row[rowKey] || JSON.stringify(row);
271
- const isRowSelected = (row: any): boolean => selectedRows.includes(getRowKey(row));
272
- const getColumnKey = (column: ColumnDefinition, index: number): string => column.id || column.expr || `col-${index}`;
273
-
274
- /*
275
- const renderExpression = (expr: string, row: any): string => {
276
- try {
277
- return expr.replace(/\{\{(\w+(?:\.\w+)*)\}\}/g, (_match, path) => {
278
- const value = getNestedValue(row, path);
279
- return value != null ? String(value) : "";
280
- });
281
- } catch (err: any) {
282
- return `Error: ${err.message}`;
283
- }
284
- };
285
- */
286
-
287
- const formatValue = (value: any, datatype = "string", row: any): string => {
288
- if (value === null || value === undefined) return "";
289
- switch (datatype) {
290
- case "number":
291
- return typeof value === "number" ? value.toLocaleString() : value;
292
- case "currency":
293
- return typeof value === "number" ? `$${value.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}` : value;
294
- case "date":
295
- return value instanceof Date ? value.toLocaleDateString() : new Date(value).toLocaleDateString();
296
- case "boolean":
297
- return value ? "✓" : "✗";
298
- default:
299
- return String(value);
300
- }
301
- };
302
-
303
- const renderCellContent = (row: any, column: ColumnDefinition, rowIndex: number) => {
304
- if (column.expr) {
305
- const rendered = render(column.expr, row);
306
- if (column.render) return column.render(rendered, row, rowIndex);
307
- return <span dangerouslySetInnerHTML={{ __html: rendered }} />;
308
- }
309
- const value = column.id ? getNestedValue(row, column.id) : null;
310
- if (cellRenderer) return cellRenderer(value, row, column);
311
- if (column.render) return column.render(value, row, rowIndex);
312
- if (column.formatter) return column.formatter(value, row);
313
- return formatValue(value, column.datatype || "string", row);
314
- };
315
-
316
- if (!sortedData || sortedData.length === 0) {
317
- return (
318
- <tr>
319
- <td colSpan={visibleColumns.length + (selectable ? 1 : 0) + (rowActions ? 1 : 0)} style={{ textAlign: "center", padding: "24px" }}>
320
- {emptyMessage}
321
- </td>
322
- </tr>
323
- );
324
- }
325
-
326
- return (
327
- <>
328
- {sortedData.map((row, itemIndex) => {
329
- const isSelected = isRowSelected(row);
330
- const customRowClass = rowClassName ? rowClassName(row, itemIndex) : "";
331
- const rows: ReactNode[] = [];
332
-
333
- for (let subrowIndex = 0; subrowIndex < rowsPerItem; subrowIndex++) {
334
- const rowClasses = [isSelected && "dt-selected", customRowClass].filter(Boolean).join(" ");
335
-
336
- rows.push(
337
- <RowProvider key={generateKey()} data={row}>
338
- <tr key={`${getRowKey(row)}-subrow-${subrowIndex}`} className={rowClasses} onClick={() => handleRowClick(row, itemIndex)}>
339
- {selectable && subrowIndex === 0 && (
340
- <td className="dt-checkbox-cell" rowSpan={rowsPerItem}>
341
- <input type="checkbox" checked={isSelected} onChange={() => handleSelectRow(row)} onClick={(e) => e.stopPropagation()} />
342
- </td>
343
- )}
344
- {visibleColumns.map((column, colIndex) => {
345
- if (!column.id && !column.expr) return null;
346
- if ((column.subrow || 0) !== subrowIndex) return null;
347
- if (column.subrow && column.subrow > 0 && subrowIndex > 0) return null;
348
-
349
- const value = column.id ? getNestedValue(row, column.id) : null;
350
- return (
351
- <td
352
- key={getColumnKey(column, colIndex)}
353
- style={{ textAlign: column.align || "left" }}
354
- className={`
355
- ${column.datatype === "number" || column.datatype === "currency" ? "dt-numeric" : ""}
356
- ${column.datatype === "boolean" ? "dt-boolean" : ""}
357
- `.trim()}
358
- onClick={(e) => handleCellClick(e, value, row, column, itemIndex, colIndex)}
359
- >
360
- {renderCellContent(row, column, itemIndex)}
361
- </td>
362
- );
363
- })}
364
- {rowActions && subrowIndex === 0 && (
365
- <td className="dt-actions-cell" rowSpan={rowsPerItem}>
366
- {rowActions(row, itemIndex)}
367
- </td>
368
- )}
369
- </tr>
370
- </RowProvider>,
371
- );
372
- }
373
- return rows;
374
- })}
375
- </>
376
- );
377
- },
378
- );
379
-
380
- // ============================================================================
381
- // DATATABLE COMPONENT
382
- // ============================================================================
383
-
384
- export const DataTable: React.FC<DataTableProps> = ({
385
- // Direct props (fallback when used outside TableProvider)
386
- data: propData,
387
- columns: propColumns,
388
-
389
- loading: propLoading = false,
390
- emptyMessage = "No data available",
391
- striped = false,
392
- bordered = false,
393
- hover = true,
394
- dense = false,
395
- stickyHeader = false,
396
- maxHeight,
397
- rowKey = "id",
398
- onRowClick,
399
- onCellClick,
400
- selectedRows = [],
401
- onSelectionChange,
402
- selectable = false,
403
- className = "",
404
- rowClassName,
405
- cellRenderer,
406
- headerRenderer,
407
- multiRowHeader,
408
- rowActions,
409
- rowsPerItem = 1,
410
- }) => {
411
- // Try to consume context; if not inside a provider, ctx will be null.
412
- let ctxColumns: ColumnDefinition[] | undefined;
413
- let ctxRows: any[] | undefined;
414
- let ctxLoading: boolean | undefined;
415
-
416
- try {
417
- // eslint-disable-next-line react-hooks/rules-of-hooks
418
- const ctx = useTableContext();
419
- ctxColumns = ctx.columns;
420
- ctxRows = ctx.rows;
421
- ctxLoading = ctx.loading;
422
- } catch {
423
- // Not inside TableProvider – use props directly
424
- }
425
-
426
- // Context wins over direct props
427
- const data: any[] = ctxRows ?? propData ?? [];
428
- const columns: ColumnDefinition[] = propColumns ?? ctxColumns ?? [];
429
- const loading = ctxLoading ?? propLoading;
430
-
431
- // ---- Sort state ----
432
- const [sortConfig, setSortConfig] = React.useState<SortConfig>({
433
- key: null,
434
- direction: null,
435
- });
436
-
437
- // ---- Derived values ----
438
- const visibleColumns = useMemo(
439
- () =>
440
- columns.filter((col) => {
441
- if (col.visible === false) return false;
442
- return col.title || col.id || col.expr || col.render;
443
- }),
444
- [columns],
445
- );
446
-
447
- const sortedData = useMemo(() => {
448
- if (!sortConfig.key) return data;
449
- const getNestedValue = (obj: any, path: string): any => path.split(".").reduce((acc, part) => acc && acc[part], obj);
450
-
451
- return [...data].sort((a, b) => {
452
- const aVal = getNestedValue(a, sortConfig.key!);
453
- const bVal = getNestedValue(b, sortConfig.key!);
454
- if (aVal === bVal) return 0;
455
- if (aVal == null) return 1;
456
- if (bVal == null) return -1;
457
- const cmp = aVal < bVal ? -1 : 1;
458
- return sortConfig.direction === "asc" ? cmp : -cmp;
459
- });
460
- }, [data, sortConfig]);
461
-
462
- // ---- Handlers ----
463
- const handleRowClick = (row: any, rowIndex: number) => onRowClick?.(row, rowIndex);
464
-
465
- const handleCellClick = (e: React.MouseEvent, value: any, row: any, column: ColumnDefinition, rowIndex: number, colIndex: number) => {
466
- e.stopPropagation();
467
- onCellClick?.(value, row, column, rowIndex, colIndex);
468
- };
469
-
470
- const getRowKey = (row: any): string => row[rowKey] || JSON.stringify(row);
471
-
472
- const handleSelectAll = (e: React.ChangeEvent<HTMLInputElement>) => {
473
- if (!onSelectionChange) return;
474
- if (e.target.checked) {
475
- onSelectionChange(data.map((row) => getRowKey(row)));
476
- } else {
477
- onSelectionChange([]);
478
- }
479
- };
480
-
481
- const handleSelectRow = (row: any) => {
482
- if (!onSelectionChange) return;
483
- const key = getRowKey(row);
484
- if (selectedRows.includes(key)) {
485
- onSelectionChange(selectedRows.filter((k) => k !== key));
486
- } else {
487
- onSelectionChange([...selectedRows, key]);
488
- }
489
- };
490
-
491
- const handleSort = (columnId: string, sortable?: boolean) => {
492
- if (!sortable) return;
493
- setSortConfig((prev) => {
494
- if (prev.key === columnId) {
495
- if (prev.direction === "asc") return { key: columnId, direction: "desc" };
496
- if (prev.direction === "desc") return { key: null, direction: null };
497
- }
498
- return { key: columnId, direction: "asc" };
499
- });
500
- };
501
-
502
- // ---- CSS ----
503
- const tableClasses = [
504
- "data-table",
505
- striped && "dt-striped",
506
- bordered && "dt-bordered",
507
- hover && "dt-hover",
508
- dense && "dt-dense",
509
- stickyHeader && "dt-sticky-header",
510
- className,
511
- ]
512
- .filter(Boolean)
513
- .join(" ");
514
-
515
- const containerStyle: React.CSSProperties = {};
516
- if (maxHeight) {
517
- containerStyle.maxHeight = typeof maxHeight === "number" ? `${maxHeight}px` : maxHeight;
518
- containerStyle.overflowY = "auto";
519
- }
520
-
521
- // ---- Render ----
522
- if (loading) {
523
- return (
524
- <div className="dt-loading">
525
- <div className="dt-spinner"></div>
526
- <p>Loading...</p>
527
- </div>
528
- );
529
- }
530
-
531
- return (
532
- <div className="dt-container" style={containerStyle}>
533
- <table className={tableClasses}>
534
- <thead>
535
- {/* TableHeader only re-renders when columns or sort config changes */}
536
- <TableHeader
537
- visibleColumns={visibleColumns}
538
- selectable={selectable}
539
- rowActions={rowActions}
540
- data={data}
541
- selectedRows={selectedRows}
542
- handleSelectAll={handleSelectAll}
543
- sortConfig={sortConfig}
544
- handleSort={handleSort}
545
- headerRenderer={headerRenderer}
546
- multiRowHeader={multiRowHeader}
547
- />
548
- </thead>
549
- <tbody>
550
- {/* TableBody only re-renders when rows change */}
551
- <TableBody
552
- sortedData={sortedData}
553
- visibleColumns={visibleColumns}
554
- emptyMessage={emptyMessage}
555
- selectable={selectable}
556
- rowActions={rowActions}
557
- selectedRows={selectedRows}
558
- handleSelectRow={handleSelectRow}
559
- handleRowClick={handleRowClick}
560
- handleCellClick={handleCellClick}
561
- rowClassName={rowClassName}
562
- rowKey={rowKey}
563
- rowsPerItem={rowsPerItem}
564
- cellRenderer={cellRenderer}
565
- />
566
- </tbody>
567
- </table>
568
- </div>
569
- );
570
- };
571
-
572
- export default DataTable;