@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.
- package/dist/components/action/LookupPage.js +9 -31
- package/dist/components/action/ViewPage.d.ts +2 -0
- package/dist/components/action/ViewPage.js +25 -31
- package/dist/components/common/UIComponent.js +4 -3
- package/dist/components/table/DataList.js +2 -2
- package/dist/components/view/PopupView.d.ts +13 -0
- package/dist/components/view/PopupView.js +25 -20
- package/dist/core/DataContext.d.ts +7 -4
- package/dist/core/DataContext.js +16 -4
- package/dist/core/Page.js +25 -26
- package/dist/core/PageCache.js +7 -7
- package/dist/core/PageContext.js +17 -7
- package/dist/core/PageViewContext.d.ts +13 -1
- package/dist/core/PageViewContext.js +75 -2
- package/dist/core/PopupContext.d.ts +49 -0
- package/dist/core/PopupContext.js +380 -0
- package/dist/core/RowContext.js +1 -1
- package/dist/core/WindowContext.d.ts +15 -0
- package/dist/core/WindowContext.js +28 -0
- package/dist/core/index.d.ts +16 -0
- package/dist/index.css +25 -7
- package/dist/lib/utils/BeanUtils.js +7 -7
- package/dist/templates/DataListTemplate.js +7 -2
- package/dist/templates/ExplorerTemplate.js +1 -1
- package/package.json +5 -5
- package/dist/components/action/AlertMessage.tsx +0 -38
- package/dist/components/action/Button.tsx +0 -230
- package/dist/components/action/CancelEdit.tsx +0 -40
- package/dist/components/action/DeleteData.tsx +0 -73
- package/dist/components/action/Edit.tsx +0 -40
- package/dist/components/action/LookupPage.tsx +0 -113
- package/dist/components/action/ProcessRunner.tsx +0 -337
- package/dist/components/action/Refresh.tsx +0 -35
- package/dist/components/action/SaveData.tsx +0 -74
- package/dist/components/action/SelectData.tsx +0 -47
- package/dist/components/action/Undo.tsx +0 -50
- package/dist/components/action/UpdateData.tsx +0 -49
- package/dist/components/action/UpdateState.tsx +0 -40
- package/dist/components/action/ViewBackPage.tsx +0 -46
- package/dist/components/action/ViewPage.tsx +0 -141
- package/dist/components/common/UIComponent.tsx +0 -86
- package/dist/components/common/UIInput.tsx +0 -49
- package/dist/components/common/UIMenu.tsx +0 -91
- package/dist/components/index.ts +0 -51
- package/dist/components/input/CodeEditor.tsx +0 -188
- package/dist/components/input/DateField.tsx +0 -274
- package/dist/components/input/DayPicker.tsx +0 -5
- package/dist/components/input/HtmlCode.tsx +0 -203
- package/dist/components/input/JsonCode.tsx +0 -205
- package/dist/components/input/MonthPicker.tsx +0 -5
- package/dist/components/input/ScriptCode.tsx +0 -195
- package/dist/components/input/Select.tsx +0 -78
- package/dist/components/input/SqlCode.tsx +0 -162
- package/dist/components/input/StringDecision.tsx +0 -64
- package/dist/components/input/Text.tsx +0 -57
- package/dist/components/input/YearPicker.tsx +0 -81
- package/dist/components/list/IconMenu.tsx +0 -115
- package/dist/components/list/TabMenu.tsx +0 -127
- package/dist/components/list/TreeMenu.tsx +0 -279
- package/dist/components/list/TxnTaskList.tsx +0 -198
- package/dist/components/output/Label.tsx +0 -50
- package/dist/components/table/DataList.tsx +0 -820
- package/dist/components/table/DataTable.tsx +0 -572
- package/dist/components/table/ListHandler.ts +0 -276
- package/dist/components/table/TableContext.tsx +0 -122
- package/dist/components/view/ComponentView.tsx +0 -102
- package/dist/components/view/FilterView.tsx +0 -21
- package/dist/components/view/HtmlForm.tsx +0 -176
- package/dist/components/view/HtmlView.tsx +0 -98
- package/dist/components/view/IFrameView.tsx +0 -48
- package/dist/components/view/Modal.tsx +0 -72
- package/dist/components/view/PageView.tsx +0 -131
- package/dist/components/view/PopupView.tsx +0 -160
- package/dist/components/view/RootView.tsx +0 -109
- package/dist/components/view/WizardView.tsx +0 -48
- package/dist/lib/layouts/BorderLayout.tsx +0 -31
- package/dist/lib/layouts/CardLayout.tsx +0 -73
- package/dist/lib/layouts/CenterLayout.tsx +0 -20
- package/dist/lib/layouts/GridLayout.tsx +0 -20
- package/dist/lib/layouts/HPanel.tsx +0 -31
- package/dist/lib/layouts/HorizontalLayout.tsx +0 -29
- package/dist/lib/layouts/MainLayout.tsx +0 -16
- package/dist/lib/layouts/PageLayout.tsx +0 -29
- package/dist/lib/layouts/VPanel.tsx +0 -27
- package/dist/lib/layouts/XLayout.tsx +0 -29
- package/dist/lib/layouts/YLayout.tsx +0 -29
- 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
|
-
};
|