@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,820 +0,0 @@
1
- import { Columns, Eye, RefreshCcw, Search, Trash } from "lucide-react";
2
- import React, { useCallback, useRef, useState } from "react";
3
- import { useApp } from "../../core/AppContext";
4
- import { useDataContext } from "../../core/DataContext";
5
- import { DynamicComponent } from "../../core/DynamicComponent";
6
- import { usePageContext } from "../../core/PageContext";
7
- import Panel from "../../core/Panel";
8
- import useDependHandler from "../../core/UIDependHandler";
9
- import { replaceValues } from "../../lib/utils/BeanUtils";
10
- import { getUrlPageParams } from "../../lib/utils/PageUtils";
11
- import { ColumnDefinition, DataTable } from "./DataTable";
12
- import ListHandler, { ListActionHandler, ListHandlerConfig } from "./ListHandler";
13
- import { DataConfig, TableProvider, useTableContext } from "./TableContext";
14
- // import "./DataList.css";
15
-
16
- // ============================================================================
17
- // TYPE DEFINITIONS
18
- // ============================================================================
19
-
20
- export interface FilterDefinition {
21
- name: string;
22
- component: string;
23
- attr?: Record<string, any>;
24
- }
25
-
26
- export interface ActionDefinition {
27
- label?: string;
28
- icon?: string | React.ReactNode;
29
- component: string;
30
- attr: Record<string, any>;
31
- variant?: "default" | "primary" | "danger";
32
- show?: (row: any) => boolean;
33
- }
34
-
35
- export interface BulkActionDefinition {
36
- label: string;
37
- icon?: string;
38
- onClick: (selectedRows: any[]) => void;
39
- variant?: "default" | "primary" | "danger";
40
- }
41
-
42
- export interface ToolbarActionDefinition {
43
- label: string;
44
- icon?: string;
45
- component: string;
46
- attr: Record<string, any>;
47
- variant?: "default" | "primary";
48
- }
49
-
50
- // ============================================================================
51
- // ATTR — the only prop DataList accepts
52
- // ============================================================================
53
-
54
- export interface DataListAttr {
55
- // Required
56
- cols: ColumnDefinition[];
57
- data: DataConfig;
58
-
59
- // Pagination / fetch config
60
- rowsPerPage?: number;
61
- disableTotalCount?: boolean;
62
-
63
- // Display
64
- title?: string;
65
- emptyMessage?: string;
66
- errorMessage?: string;
67
- striped?: boolean;
68
- bordered?: boolean;
69
- hover?: boolean;
70
- dense?: boolean;
71
-
72
- showPagination?: boolean;
73
- paginationPosition?: "top" | "bottom" | "both";
74
- showPageInfo?: boolean;
75
- showTotalCount?: boolean;
76
- showRowsPerPage?: boolean;
77
- rowsPerPageOptions?: number[];
78
-
79
- // Search
80
- searchable?: boolean;
81
- searchPlaceholder?: string;
82
- searchDebounce?: number;
83
- onSearchChange?: (text: string) => void;
84
-
85
- // Filters
86
- filters?: FilterDefinition[] | FilterDefinition[][];
87
- showFilterPanel?: boolean;
88
- onFilterChange?: (filters: Record<string, any>) => void;
89
-
90
- // Sorting
91
- sortable?: boolean;
92
- defaultSort?: { column: string; direction: "asc" | "desc" };
93
- showSortIndicator?: boolean;
94
-
95
- // Selection
96
- selectable?: boolean;
97
- selectionMode?: "single" | "multiple";
98
- onSelectionChange?: (selectedRows: any[]) => void;
99
- selectOnRowClick?: boolean;
100
-
101
- // Row / action callbacks
102
- onRowClick?: (row: any, rowIndex: number) => void;
103
- onRowDoubleClick?: (row: any, rowIndex: number) => void;
104
- rowActions?: ActionDefinition[];
105
- bulkActions?: BulkActionDefinition[];
106
- showBulkActions?: boolean;
107
- toolbarActions?: ToolbarActionDefinition[];
108
- commonActions?: Record<string, any>;
109
-
110
- // Toolbar
111
- showToolbar?: boolean;
112
- toolbarTitle?: string;
113
- showRefreshButton?: boolean;
114
- showExportButton?: boolean;
115
-
116
- // Lifecycle callbacks
117
- onLoad?: () => void;
118
- onError?: (error: any) => void;
119
- onRefresh?: () => void;
120
- onExport?: () => void;
121
-
122
- // Styling
123
- className?: string;
124
- rowClassName?: (row: any, rowIndex: number) => string;
125
-
126
- depends?: string;
127
- handle?: InnerDataListHandle;
128
- }
129
-
130
- // ============================================================================
131
- // PUBLIC DATALIST PROPS — only attr, nothing else
132
- // ============================================================================
133
-
134
- export interface DataListProps {
135
- attr: DataListAttr;
136
- }
137
-
138
- // ============================================================================
139
- // INNER DATALIST — lives inside TableProvider, consumes context
140
- // ============================================================================
141
-
142
- type InnerProps = Omit<DataListAttr, "data" | "rowsPerPage" | "disableTotalCount">;
143
-
144
- const InnerDataList: React.FC<InnerProps> = ({
145
- cols,
146
- emptyMessage = "No data available",
147
- errorMessage,
148
- striped = false,
149
- bordered = false,
150
- hover = true,
151
- dense = false,
152
-
153
- showPagination = true,
154
- paginationPosition = "bottom",
155
- showPageInfo = true,
156
- showTotalCount = true,
157
- showRowsPerPage = true,
158
- rowsPerPageOptions = [5, 10, 20, 50, 100],
159
-
160
- searchable = false,
161
- searchPlaceholder = "Search...",
162
- searchDebounce = 300,
163
- onSearchChange,
164
-
165
- filters = [],
166
- showFilterPanel = false,
167
- onFilterChange,
168
-
169
- sortable = true,
170
- showSortIndicator = true,
171
-
172
- selectable = false,
173
- selectionMode = "multiple",
174
- onSelectionChange,
175
- selectOnRowClick = false,
176
-
177
- onRowClick,
178
- rowActions = [],
179
- bulkActions = [],
180
- showBulkActions = true,
181
- toolbarActions = [],
182
-
183
- showToolbar = true,
184
- toolbarTitle,
185
- showRefreshButton = true,
186
- showExportButton = false,
187
-
188
- onLoad,
189
- onError,
190
- onRefresh,
191
- onExport,
192
-
193
- className = "",
194
- rowClassName,
195
- depends,
196
-
197
- handle,
198
- }) => {
199
- const { listHandler, columns, rows, setRows, loading: ctxLoading, setLoading } = useTableContext();
200
-
201
- // ============================================================================
202
- // STATE
203
- // ============================================================================
204
-
205
- const [internalLoading, setInternalLoading] = useState(false);
206
- const [error, setError] = useState<string | null>(null);
207
- const [selectedRows, setSelectedRows] = useState<string[]>([]);
208
- const [searchText, setSearchText] = useState("");
209
- const [searchTimeout, setSearchTimeout] = useState<NodeJS.Timeout | null>(null);
210
- const [activeFilters, setActiveFilters] = useState<Record<string, any>>({});
211
- const [showColumnToggle, setShowColumnToggle] = useState(false);
212
- const [hiddenColumns, setHiddenColumns] = useState<string[]>([]);
213
-
214
- const isLoading = ctxLoading || internalLoading;
215
-
216
- const pageContext = usePageContext();
217
-
218
- const handleRef = {
219
- setFilter: async (filter: Record<string, any>): Promise<void> => {
220
- if (filter == null || listHandler == null) return;
221
-
222
- await listHandler.setFilter(filter);
223
- await loadData();
224
- },
225
- };
226
-
227
- if (handle != null) {
228
- handle.init(handleRef);
229
- }
230
-
231
- const refreshHandler = async () => {
232
- // console.log("refresh via notify depends", pageContext?.uuid);
233
- await handleRefresh();
234
- };
235
-
236
- useDependHandler({
237
- name: depends ?? "datalist",
238
- onRefresh: refreshHandler,
239
- });
240
-
241
- // ============================================================================
242
- // LOAD DATA
243
- // ============================================================================
244
-
245
- const loadData = useCallback(async () => {
246
- if (!listHandler) return;
247
- setInternalLoading(true);
248
- setLoading(true);
249
- setError(null);
250
- try {
251
- await listHandler.load();
252
- setRows([...listHandler.getData()]);
253
- onLoad?.();
254
- } catch (err: any) {
255
- setError(err?.message || "Failed to load data");
256
- onError?.(err);
257
- } finally {
258
- setInternalLoading(false);
259
- setLoading(false);
260
- }
261
- }, [listHandler, onLoad, onError, setRows, setLoading]);
262
-
263
- // NOTE: TableProvider already fires the initial load — no double-fetch here.
264
-
265
- // ============================================================================
266
- // SEARCH
267
- // ============================================================================
268
-
269
- const handleSearch = (text: string) => {
270
- setSearchText(text);
271
- if (searchTimeout) clearTimeout(searchTimeout);
272
- const timeout = setTimeout(async () => {
273
- if (!listHandler) return;
274
- await listHandler.doSearch(text);
275
- await loadData();
276
- onSearchChange?.(text);
277
- }, searchDebounce);
278
- setSearchTimeout(timeout);
279
- };
280
-
281
- // ============================================================================
282
- // FILTER
283
- // ============================================================================
284
-
285
- const handleFilterChange = async (field: string, value: any) => {
286
- if (!listHandler) return;
287
- const newFilters = { ...activeFilters, [field]: value };
288
- setActiveFilters(newFilters);
289
- await listHandler.setFilter(newFilters);
290
- await loadData();
291
- onFilterChange?.(newFilters);
292
- };
293
-
294
- const handleResetFilters = async () => {
295
- if (!listHandler) return;
296
- setActiveFilters({});
297
- await listHandler.resetFilter();
298
- await loadData();
299
- onFilterChange?.({});
300
- };
301
-
302
- // ============================================================================
303
- // SORT
304
- // ============================================================================
305
-
306
- const handleSort = async (columnId: string) => {
307
- if (!sortable || !listHandler) return;
308
- const currentSort = listHandler.getCurrentSortedColumn();
309
- const currentDirection = listHandler.getCurrentSortDirection();
310
- if (currentSort === columnId && currentDirection === "asc") {
311
- await listHandler.sortDesc(columnId);
312
- } else {
313
- await listHandler.sortAsc(columnId);
314
- }
315
- await loadData();
316
- };
317
-
318
- // ============================================================================
319
- // PAGINATION
320
- // ============================================================================
321
-
322
- const handlePageChange = async (page: number) => {
323
- if (!listHandler) return;
324
- await listHandler.moveToPage(page);
325
- await loadData();
326
- };
327
- const handleNextPage = async () => {
328
- if (!listHandler) return;
329
- await listHandler.moveNextPage();
330
- await loadData();
331
- };
332
- const handlePrevPage = async () => {
333
- if (!listHandler) return;
334
- await listHandler.movePrevPage();
335
- await loadData();
336
- };
337
- const handleFirstPage = async () => {
338
- if (!listHandler) return;
339
- await listHandler.moveFirstPage();
340
- await loadData();
341
- };
342
- const handleLastPage = async () => {
343
- if (!listHandler) return;
344
- await listHandler.moveLastPage();
345
- await loadData();
346
- };
347
- const handleRowsPerPage = async (n: number) => {
348
- if (!listHandler) return;
349
- await listHandler.setRowsPerPage(n);
350
- await loadData();
351
- };
352
-
353
- // ============================================================================
354
- // SELECTION
355
- // ============================================================================
356
-
357
- const handleSelectionChange = (keys: string[]) => {
358
- setSelectedRows(keys);
359
- if (onSelectionChange) {
360
- const selectedData = rows.filter((row) => keys.includes(row.id || JSON.stringify(row)));
361
- onSelectionChange(selectedData);
362
- }
363
- };
364
-
365
- // ============================================================================
366
- // ROW CLICK
367
- // ============================================================================
368
-
369
- const handleRowClick = (row: any, rowIndex: number) => {
370
- if (selectOnRowClick && selectable) {
371
- const key = row.id || JSON.stringify(row);
372
- if (selectionMode === "single") {
373
- handleSelectionChange([key]);
374
- } else {
375
- const newSelection = selectedRows.includes(key) ? selectedRows.filter((k) => k !== key) : [...selectedRows, key];
376
- handleSelectionChange(newSelection);
377
- }
378
- }
379
- onRowClick?.(row, rowIndex);
380
- };
381
-
382
- // ============================================================================
383
- // REFRESH
384
- // ============================================================================
385
-
386
- const handleRefresh = async () => {
387
- await loadData();
388
- onRefresh?.();
389
- };
390
-
391
- // ============================================================================
392
- // COLUMN VISIBILITY
393
- // ============================================================================
394
-
395
- const visibleUserColumns = columns.filter((col) => {
396
- const colId = col.id || String(col.title);
397
- return !hiddenColumns.includes(colId);
398
- });
399
-
400
- // ============================================================================
401
- // DISPLAY COLUMNS — actions + sort indicators
402
- // ============================================================================
403
-
404
- const renderActionCell = (row: any, rowIndex: number) => {
405
- const visible = rowActions.filter((a) => !a.show || a.show(row));
406
- if (visible.length === 0) return null;
407
- const uuid = Math.random().toString(36).slice(2);
408
- return (
409
- <div className="dl-actions flex justify-center items-center">
410
- {visible.map((action, i) => (
411
- <div key={`${uuid}-${i}`}>
412
- <DynamicComponent
413
- config={{
414
- component: action.component,
415
- attr: { ...action.attr, opt: { data: row } },
416
- }}
417
- />
418
- </div>
419
- ))}
420
- </div>
421
- );
422
- };
423
-
424
- const displayColumns: ColumnDefinition[] =
425
- rowActions.length > 0 && visibleUserColumns.length > 0
426
- ? [
427
- ...visibleUserColumns,
428
- {
429
- id: "_actions",
430
- title: <div className="text-center w-full">Actions</div>,
431
- align: "center",
432
- render: (_, row, rowIndex) => renderActionCell(row, rowIndex),
433
- },
434
- ]
435
- : visibleUserColumns;
436
-
437
- const sortableColumns = displayColumns.map((col) => {
438
- if (!sortable || !showSortIndicator || !listHandler) return col;
439
- const currentSort = listHandler.getCurrentSortedColumn();
440
- const currentDirection = listHandler.getCurrentSortDirection();
441
- return {
442
- ...col,
443
- title: (
444
- <div
445
- className="dl-sortable-header"
446
- onClick={() => col.id && col.id !== "_actions" && handleSort(col.id)}
447
- style={{ cursor: col.id !== "_actions" ? "pointer" : "default" }}
448
- >
449
- {col.title}
450
- {currentSort === col.id && <span className={`dl-sort-icon dl-sort-${currentDirection}`}>{currentDirection === "asc" ? "▲" : "▼"}</span>}
451
- </div>
452
- ),
453
- };
454
- });
455
-
456
- // ============================================================================
457
- // RENDER TOOLBAR
458
- // ============================================================================
459
-
460
- const renderToolbar = () => {
461
- if (!showToolbar) return null;
462
- const uuid = Math.random().toString(36).slice(2);
463
- return (
464
- <div className="dl-toolbar">
465
- <div className="dl-toolbar-left">
466
- {toolbarTitle && <h3 className="dl-toolbar-title">{toolbarTitle}</h3>}
467
- <div className="dl-toolbar-right">
468
- {showBulkActions &&
469
- selectedRows.length > 0 &&
470
- bulkActions.map((action, i) => (
471
- <button
472
- key={action.label || i}
473
- className={`dl-btn dl-btn-${action.variant || "default"}`}
474
- onClick={() => {
475
- const selectedData = rows.filter((row) => selectedRows.includes(row.id || JSON.stringify(row)));
476
- action.onClick(selectedData);
477
- }}
478
- >
479
- {action.icon && <span>{action.icon}</span>}
480
- {action.label}
481
- </button>
482
- ))}
483
- {toolbarActions.map((action, i) => (
484
- <div key={`${uuid}-${i}`}>
485
- <DynamicComponent config={{ component: action.component, attr: action.attr || {} }} />
486
- </div>
487
- ))}
488
- {showExportButton && (
489
- <button className="dl-btn" onClick={onExport}>
490
- <span>📥</span> Export
491
- </button>
492
- )}
493
- </div>
494
- </div>
495
-
496
- <div className="flex items-center">
497
- <RefreshButton show={showRefreshButton} onClick={handleRefresh} />
498
-
499
- <ColumnToggle columns={columns} hiddenColumns={hiddenColumns} setHiddenColumns={setHiddenColumns} />
500
-
501
- <SearchBox searchable={searchable} searchPlaceholder={searchPlaceholder} searchText={searchText} onSearch={handleSearch} />
502
- </div>
503
- </div>
504
- );
505
- };
506
-
507
- // ============================================================================
508
- // RENDER FILTER PANEL
509
- // ============================================================================
510
-
511
- const renderFilterPanel = () => {
512
- if (!filters) return null;
513
- return <Panel content={filters} />;
514
- };
515
-
516
- // ============================================================================
517
- // RENDER PAGINATION
518
- // ============================================================================
519
-
520
- const renderPagination = () => {
521
- if (!showPagination || !listHandler) return null;
522
- const currentPage = listHandler.getCurrentPage();
523
- const totalPages = listHandler.getTotalPageCount();
524
- const totalRecords = listHandler.getTotalRecordCount();
525
- const rowsPerPage = listHandler.getRowsPerPage();
526
- const hasNext = listHandler.hasNextPage();
527
- const hasPrev = listHandler.hasPrevPage();
528
- const startRecord = (currentPage - 1) * rowsPerPage + 1;
529
- const endRecord = Math.min(currentPage * rowsPerPage, totalRecords);
530
-
531
- return (
532
- <div className="dl-pagination">
533
- <div className="dl-pagination-info">
534
- {showPageInfo && showTotalCount && (
535
- <span>
536
- Showing {startRecord}-{endRecord} of {totalRecords} records
537
- </span>
538
- )}
539
- {showPageInfo && !showTotalCount && <span>Page {currentPage}</span>}
540
- </div>
541
- <div className="dl-pagination-controls">
542
- {showRowsPerPage && (
543
- <div className="dl-rows-per-page">
544
- <span>Rows per page:</span>
545
- <select value={rowsPerPage} onChange={(e) => handleRowsPerPage(Number(e.target.value))}>
546
- {rowsPerPageOptions.map((o) => (
547
- <option key={o} value={o}>
548
- {o}
549
- </option>
550
- ))}
551
- </select>
552
- </div>
553
- )}
554
- <button className="dl-page-btn" disabled={!hasPrev} onClick={handleFirstPage}>
555
- First
556
- </button>
557
- <button className="dl-page-btn" disabled={!hasPrev} onClick={handlePrevPage}>
558
- ← Prev
559
- </button>
560
- {totalPages > 0 &&
561
- [...Array(Math.min(totalPages, 5))].map((_, i) => {
562
- const page = i + 1;
563
- return (
564
- <button key={page} className={`dl-page-btn ${currentPage === page ? "active" : ""}`} onClick={() => handlePageChange(page)}>
565
- {page}
566
- </button>
567
- );
568
- })}
569
- <button className="dl-page-btn" disabled={!hasNext} onClick={handleNextPage}>
570
- Next →
571
- </button>
572
- <button className="dl-page-btn" disabled={!hasNext} onClick={handleLastPage}>
573
- Last
574
- </button>
575
- </div>
576
- </div>
577
- );
578
- };
579
-
580
- // ============================================================================
581
- // ERROR STATE
582
- // ============================================================================
583
-
584
- if (error && errorMessage) {
585
- return (
586
- <div className="dl-error">
587
- <p>{errorMessage}</p>
588
- <button onClick={handleRefresh}>Retry</button>
589
- </div>
590
- );
591
- }
592
-
593
- // ============================================================================
594
- // MAIN RENDER
595
- // ============================================================================
596
-
597
- return (
598
- <div className={`data-list ${className}`}>
599
- {renderToolbar()}
600
- {renderFilterPanel()}
601
- {(paginationPosition === "both" || paginationPosition === "top") && renderPagination()}
602
- <DataTable
603
- data={rows}
604
- columns={sortableColumns}
605
- loading={isLoading}
606
- emptyMessage={emptyMessage}
607
- striped={striped}
608
- bordered={bordered}
609
- hover={hover}
610
- dense={dense}
611
- rowKey="id"
612
- onRowClick={handleRowClick}
613
- selectedRows={selectedRows}
614
- onSelectionChange={handleSelectionChange}
615
- selectable={selectable}
616
- rowClassName={rowClassName}
617
- />
618
- {(paginationPosition === "both" || paginationPosition === "bottom") && rows.length > 0 && renderPagination()}
619
- </div>
620
- );
621
- };
622
-
623
- // ============================================================================
624
- // PUBLIC DATALIST
625
- // - Only accepts attr
626
- // - All hooks (useApp, usePageContext) called here
627
- // - factory defined here
628
- // - resolves dynamic filter params before passing to TableProvider
629
- // ============================================================================
630
-
631
- type InnerDataListHandle = {
632
- init: (ref: any) => void;
633
- };
634
-
635
- export const DataList: React.FC<DataListProps> = ({ attr }) => {
636
- const { depends, cols, data, rowsPerPage, disableTotalCount, commonActions, rowActions, toolbarActions, ...rest } = attr;
637
-
638
- // All hooks called here — this is a React component so hooks are valid
639
- const { tenant, module } = useApp();
640
- const pageContext = usePageContext();
641
- const dataContext = useDataContext();
642
-
643
- // Factory defined here — ListHandler is a local import, no prop needed
644
- const listHandlerFactory = (config: ListHandlerConfig): ListActionHandler => ListHandler(config);
645
-
646
- // Resolve dynamic placeholders in filter before ListHandler is constructed.
647
- // Merges pageContext.getAllData() (page state) and pageContext.getUrlParams()
648
- // (URL/route params) as the source so both {pageParams.x} and {urlParams.x}
649
- // style tokens are resolved.
650
- const resolveParams = (): Record<string, any> | undefined => {
651
- if (!data?.params) return undefined;
652
- //clone the filter
653
- const cloned = JSON.parse(JSON.stringify(data.params));
654
- const { params } = getUrlPageParams();
655
- return replaceValues(cloned, params);
656
- };
657
-
658
- const newRowActions = [...(rowActions || [])];
659
- if (commonActions?.viewPage) {
660
- newRowActions.unshift({
661
- component: "ViewPage",
662
- attr: {
663
- ...commonActions.viewPage,
664
- title: "View",
665
- icon: <Eye size={20} />,
666
- iconOnly: true,
667
- },
668
- });
669
- }
670
-
671
- if (commonActions?.deleteData) {
672
- newRowActions.push({
673
- component: "DeleteData",
674
- attr: {
675
- ...commonActions.deleteData,
676
- title: "Delete",
677
- icon: <Trash size={20} />,
678
- iconOnly: true,
679
- },
680
- });
681
- }
682
-
683
- const newToolbarActions = [...(toolbarActions || [])];
684
- if (commonActions?.newPage) {
685
- newToolbarActions.unshift({
686
- label: "New",
687
- component: "ViewPage",
688
- attr: {
689
- ...commonActions.newPage,
690
- title: "New",
691
- },
692
- });
693
- }
694
-
695
- if (commonActions?.filterPage) {
696
- newToolbarActions.push({
697
- label: "Filter",
698
- component: "LookupPage",
699
- attr: {
700
- ...commonActions.filterPage,
701
- name: "customFilter",
702
- title: "Filter",
703
- },
704
- });
705
- }
706
-
707
- const handleRef = useRef<any>(null);
708
-
709
- const onRefresh = () => {
710
- const filter = dataContext?.get("customFilter");
711
- handleRef.current?.setFilter(filter ?? {});
712
- };
713
-
714
- useDependHandler({ name: "customFilter", onRefresh });
715
-
716
- const innerHandle = {
717
- init: (ref: any) => {
718
- handleRef.current = ref;
719
- },
720
- } as InnerDataListHandle;
721
-
722
- return (
723
- <TableProvider
724
- data={data}
725
- columns={cols}
726
- rowsPerPage={rowsPerPage}
727
- disableTotalCount={disableTotalCount}
728
- listHandlerFactory={listHandlerFactory}
729
- tenant={tenant ?? ""}
730
- module={module ?? ""}
731
- resolvedParams={resolveParams()}
732
- >
733
- <InnerDataList {...rest} cols={cols} rowActions={newRowActions} toolbarActions={newToolbarActions} depends={depends} handle={innerHandle} />
734
- </TableProvider>
735
- );
736
- };
737
-
738
- export default DataList;
739
-
740
- interface RefreshButtonProps {
741
- show: boolean;
742
- onClick: () => void;
743
- size?: number;
744
- }
745
-
746
- const RefreshButton = ({ show, onClick, size = 18 }: RefreshButtonProps) => {
747
- if (!show) return null;
748
-
749
- return (
750
- <span className="text-gray-600 hover:bg-gray-200 rounded p-2">
751
- <RefreshCcw size={size} onClick={onClick} className="cursor-pointer" />
752
- </span>
753
- );
754
- };
755
-
756
- interface ColumnToggleProps {
757
- columns: ColumnDefinition[];
758
- hiddenColumns: string[];
759
- setHiddenColumns: (fn: (prev: string[]) => string[]) => void;
760
- }
761
-
762
- const ColumnToggle = ({ columns, hiddenColumns, setHiddenColumns }: ColumnToggleProps) => {
763
- const [showColumnToggle, setShowColumnToggle] = useState(false);
764
-
765
- return (
766
- <div className="relative">
767
- <span
768
- className="text-gray-600 hover:bg-gray-200 rounded p-2 cursor-pointer flex items-center"
769
- onClick={() => setShowColumnToggle(!showColumnToggle)}
770
- >
771
- <Columns size={18} />
772
- </span>
773
- {showColumnToggle && (
774
- <div className="absolute right-0 top-8 z-50 bg-white border border-gray-200 rounded-lg shadow-lg p-3 min-w-[180px]">
775
- <p className="text-xs font-semibold text-gray-500 uppercase tracking-wide mb-2">Columns</p>
776
- {columns.map((col) => {
777
- const colId = col.id || String(col.title);
778
- const isHidden = hiddenColumns.includes(colId);
779
- return (
780
- <label key={colId} className="flex items-center gap-2 py-1 cursor-pointer hover:bg-gray-50 px-1 rounded">
781
- <input
782
- type="checkbox"
783
- checked={!isHidden}
784
- onChange={() => setHiddenColumns((prev) => (isHidden ? prev.filter((id) => id !== colId) : [...prev, colId]))}
785
- />
786
- <span className="text-sm text-gray-700">{String(col.title || col.id)}</span>
787
- </label>
788
- );
789
- })}
790
- </div>
791
- )}
792
- </div>
793
- );
794
- };
795
-
796
- interface SearchBoxProps {
797
- searchable: boolean;
798
- searchPlaceholder?: string;
799
- searchText: string;
800
- onSearch: (value: string) => void;
801
- }
802
-
803
- const SearchBox = ({ searchable, searchPlaceholder, searchText, onSearch }: SearchBoxProps) => {
804
- if (!searchable) return null;
805
-
806
- return (
807
- <div className="dl-search-box ml-2">
808
- <span>
809
- <Search size={16} />
810
- </span>
811
- <input
812
- type="text"
813
- placeholder={searchPlaceholder}
814
- value={searchText}
815
- onChange={(e) => onSearch(e.target.value)}
816
- className="focus:outline-none focus:ring-0 focus:shadow-none focus:bg-white"
817
- />
818
- </div>
819
- );
820
- };