@syuttechnologies/layout 1.0.2 → 1.0.21
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/README.md +536 -0
- package/dist/components/ChangePasswordModal.d.ts.map +1 -1
- package/dist/components/ChangePasswordModal.js +22 -11
- package/dist/components/EnterpriseLayout.d.ts.map +1 -1
- package/dist/components/EnterpriseLayout.js +21 -6
- package/dist/components/ui/ActionMenu/ActionMenu.d.ts +52 -0
- package/dist/components/ui/ActionMenu/ActionMenu.d.ts.map +1 -0
- package/dist/components/ui/ActionMenu/ActionMenu.js +116 -0
- package/dist/components/ui/ActionMenu/index.d.ts +3 -0
- package/dist/components/ui/ActionMenu/index.d.ts.map +1 -0
- package/dist/components/ui/ActionMenu/index.js +2 -0
- package/dist/components/ui/ModuleHeader/ModuleHeader.d.ts +90 -0
- package/dist/components/ui/ModuleHeader/ModuleHeader.d.ts.map +1 -0
- package/dist/components/ui/ModuleHeader/ModuleHeader.js +433 -0
- package/dist/components/ui/ModuleHeader/index.d.ts +3 -0
- package/dist/components/ui/ModuleHeader/index.d.ts.map +1 -0
- package/dist/components/ui/ModuleHeader/index.js +1 -0
- package/dist/components/ui/SyutGrid/SyutGrid.d.ts +74 -0
- package/dist/components/ui/SyutGrid/SyutGrid.d.ts.map +1 -0
- package/dist/components/ui/SyutGrid/SyutGrid.js +306 -0
- package/dist/components/ui/SyutGrid/index.d.ts +3 -0
- package/dist/components/ui/SyutGrid/index.d.ts.map +1 -0
- package/dist/components/ui/SyutGrid/index.js +2 -0
- package/dist/components/ui/SyutSelect/SyutSelectUnified.d.ts +128 -0
- package/dist/components/ui/SyutSelect/SyutSelectUnified.d.ts.map +1 -0
- package/dist/components/ui/SyutSelect/SyutSelectUnified.js +679 -0
- package/dist/components/ui/SyutSelect/index.d.ts +3 -0
- package/dist/components/ui/SyutSelect/index.d.ts.map +1 -0
- package/dist/components/ui/SyutSelect/index.js +2 -0
- package/dist/icon-collection/icon-systems.d.ts +89 -0
- package/dist/icon-collection/icon-systems.d.ts.map +1 -0
- package/dist/icon-collection/icon-systems.js +70 -0
- package/dist/icon-collection/index.d.ts +4 -0
- package/dist/icon-collection/index.d.ts.map +1 -0
- package/dist/icon-collection/index.js +8 -0
- package/dist/index.d.ts +12 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +19 -1
- package/package.json +9 -4
- package/src/components/ChangePasswordModal.tsx +26 -14
- package/src/components/EnterpriseLayout.tsx +23 -8
- package/src/components/ui/ActionMenu/ActionMenu.tsx +222 -0
- package/src/components/ui/ActionMenu/index.ts +2 -0
- package/src/components/ui/ModuleHeader/ModuleHeader.tsx +722 -0
- package/src/components/ui/ModuleHeader/index.ts +9 -0
- package/src/components/ui/SyutGrid/SyutGrid.tsx +483 -0
- package/src/components/ui/SyutGrid/index.ts +2 -0
- package/src/components/ui/SyutSelect/SyutSelectUnified.tsx +1115 -0
- package/src/components/ui/SyutSelect/index.ts +3 -0
- package/src/icon-collection/icon-systems.tsx +464 -0
- package/src/icon-collection/index.ts +13 -0
- package/src/index.ts +47 -1
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -0,0 +1,483 @@
|
|
|
1
|
+
import React, { useMemo, useState, useCallback, ReactNode, useEffect } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
useReactTable,
|
|
4
|
+
getCoreRowModel,
|
|
5
|
+
getSortedRowModel,
|
|
6
|
+
getFilteredRowModel,
|
|
7
|
+
getPaginationRowModel,
|
|
8
|
+
flexRender,
|
|
9
|
+
SortingState,
|
|
10
|
+
ColumnFiltersState,
|
|
11
|
+
PaginationState,
|
|
12
|
+
RowSelectionState,
|
|
13
|
+
VisibilityState,
|
|
14
|
+
ColumnDef,
|
|
15
|
+
Row,
|
|
16
|
+
Updater,
|
|
17
|
+
Column,
|
|
18
|
+
} from '@tanstack/react-table';
|
|
19
|
+
import IconSystem from '../../../icon-collection/icon-systems';
|
|
20
|
+
import { ActionMenu, ActionMenuItem, ProcessedActionMenuItem } from '../ActionMenu';
|
|
21
|
+
|
|
22
|
+
if (typeof document !== 'undefined') {
|
|
23
|
+
const styleId = 'syut-grid-styles';
|
|
24
|
+
if (!document.getElementById(styleId)) {
|
|
25
|
+
const style = document.createElement('style');
|
|
26
|
+
style.id = styleId;
|
|
27
|
+
style.textContent = `
|
|
28
|
+
.syut-grid-component { display: flex; flex-direction: column; height: 100%; font-family: inherit; }
|
|
29
|
+
.syut-grid-wrapper { flex: 1; overflow: auto; min-height: 0; border: 1px solid var(--border-color, #e2e8f0); border-radius: 8px 8px 0 0; }
|
|
30
|
+
.syut-grid { width: 100%; border-collapse: collapse; font-size: 0.875rem; background: var(--bg-primary, #ffffff); }
|
|
31
|
+
.syut-grid thead { position: sticky; top: 0; z-index: 3; }
|
|
32
|
+
.syut-grid th { padding: 0.75rem 1rem; text-align: left; font-weight: 600; color: var(--text-primary, #1e293b); background: var(--bg-secondary, #f8fafc); border-bottom: 2px solid var(--border-color, #e2e8f0); white-space: nowrap; user-select: none; }
|
|
33
|
+
.syut-grid th.frozen-column { background: var(--bg-secondary, #f8fafc); box-shadow: -2px 0 4px rgba(0, 0, 0, 0.1); z-index: 4; }
|
|
34
|
+
.syut-grid td { padding: 0.625rem 1rem; color: var(--text-primary, #1e293b); border-bottom: 1px solid var(--border-color, #e2e8f0); background: var(--bg-primary, #ffffff); }
|
|
35
|
+
.syut-grid td.frozen-column { background: var(--bg-primary, #ffffff); box-shadow: -2px 0 4px rgba(0, 0, 0, 0.1); z-index: 1; }
|
|
36
|
+
.syut-grid tbody tr:hover td { background: var(--bg-secondary, #f8fafc); }
|
|
37
|
+
.syut-grid tbody tr:hover td.frozen-column { background: var(--bg-secondary, #f8fafc); }
|
|
38
|
+
.syut-grid th.action-column, .syut-grid td.action-column { text-align: center; width: 80px; }
|
|
39
|
+
.syut-grid .row-select, .syut-grid .row-select-all { width: 16px; height: 16px; cursor: pointer; accent-color: var(--primary, #3b82f6); }
|
|
40
|
+
.syut-grid-component .pagination-footer { display: flex; align-items: center; justify-content: space-between; padding: 0.75rem 1rem; background: var(--bg-secondary, #f8fafc); border: 1px solid var(--border-color, #e2e8f0); border-top: none; border-radius: 0 0 8px 8px; flex-shrink: 0; gap: 0.5rem; flex-wrap: wrap; }
|
|
41
|
+
.syut-grid-component .pagination-info { font-size: 0.85rem; color: var(--text-muted, #64748b); white-space: nowrap; }
|
|
42
|
+
.syut-grid-component .pagination-controls { display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap; }
|
|
43
|
+
.syut-grid-component .pagination-controls select { padding: 0.375rem 0.5rem; border-radius: 0.375rem; border: 1px solid var(--border-color, #e2e8f0); font-size: 0.85rem; background: var(--bg-primary, #ffffff); color: var(--text-primary, #1e293b); cursor: pointer; }
|
|
44
|
+
.syut-grid-component .page-btn { display: inline-flex; align-items: center; justify-content: center; padding: 0.375rem 0.5rem; border: 1px solid var(--border-color, #e2e8f0); border-radius: 0.375rem; background: var(--bg-primary, #ffffff); color: var(--text-primary, #1e293b); cursor: pointer; transition: all 0.15s ease; }
|
|
45
|
+
.syut-grid-component .page-btn:hover:not(:disabled) { background: var(--bg-secondary, #f8fafc); border-color: var(--primary, #3b82f6); color: var(--primary, #3b82f6); }
|
|
46
|
+
.syut-grid-component .page-btn:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
47
|
+
.syut-grid-component .page-info { padding: 0 0.75rem; font-size: 0.85rem; white-space: nowrap; }
|
|
48
|
+
.syut-grid-component .page-nav-buttons { display: flex; align-items: center; gap: 0.25rem; }
|
|
49
|
+
|
|
50
|
+
/* Mobile responsive styles for pagination footer */
|
|
51
|
+
@media (max-width: 768px) {
|
|
52
|
+
.syut-grid-component .pagination-footer {
|
|
53
|
+
flex-direction: column;
|
|
54
|
+
align-items: stretch;
|
|
55
|
+
padding: 0.625rem 0.75rem;
|
|
56
|
+
gap: 0.625rem;
|
|
57
|
+
}
|
|
58
|
+
.syut-grid-component .pagination-info {
|
|
59
|
+
text-align: center;
|
|
60
|
+
font-size: 0.8rem;
|
|
61
|
+
}
|
|
62
|
+
.syut-grid-component .pagination-controls {
|
|
63
|
+
justify-content: center;
|
|
64
|
+
gap: 0.375rem;
|
|
65
|
+
}
|
|
66
|
+
.syut-grid-component .pagination-controls select {
|
|
67
|
+
padding: 0.3rem 0.4rem;
|
|
68
|
+
font-size: 0.8rem;
|
|
69
|
+
margin-right: 0.5rem;
|
|
70
|
+
}
|
|
71
|
+
.syut-grid-component .page-btn {
|
|
72
|
+
padding: 0.3rem 0.4rem;
|
|
73
|
+
}
|
|
74
|
+
.syut-grid-component .page-info {
|
|
75
|
+
font-size: 0.8rem;
|
|
76
|
+
padding: 0 0.5rem;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
@media (max-width: 480px) {
|
|
81
|
+
.syut-grid-component .pagination-footer {
|
|
82
|
+
padding: 0.5rem;
|
|
83
|
+
gap: 0.5rem;
|
|
84
|
+
}
|
|
85
|
+
.syut-grid-component .pagination-info {
|
|
86
|
+
font-size: 0.75rem;
|
|
87
|
+
}
|
|
88
|
+
.syut-grid-component .pagination-controls {
|
|
89
|
+
flex-wrap: wrap;
|
|
90
|
+
justify-content: center;
|
|
91
|
+
}
|
|
92
|
+
.syut-grid-component .pagination-controls select {
|
|
93
|
+
font-size: 0.75rem;
|
|
94
|
+
margin-right: 0;
|
|
95
|
+
margin-bottom: 0.25rem;
|
|
96
|
+
}
|
|
97
|
+
.syut-grid-component .page-info {
|
|
98
|
+
font-size: 0.75rem;
|
|
99
|
+
padding: 0 0.25rem;
|
|
100
|
+
order: -1;
|
|
101
|
+
width: 100%;
|
|
102
|
+
text-align: center;
|
|
103
|
+
margin-bottom: 0.25rem;
|
|
104
|
+
}
|
|
105
|
+
.syut-grid-component .page-nav-buttons {
|
|
106
|
+
width: 100%;
|
|
107
|
+
justify-content: center;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
`;
|
|
111
|
+
document.head.appendChild(style);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export interface SyutGridColumn<T> {
|
|
116
|
+
id?: string;
|
|
117
|
+
header?: string | ((column: any) => ReactNode);
|
|
118
|
+
accessorKey?: keyof T;
|
|
119
|
+
cell?: (info: { row: Row<T>; getValue: () => any }) => ReactNode;
|
|
120
|
+
size?: number;
|
|
121
|
+
enableSorting?: boolean;
|
|
122
|
+
enableFiltering?: boolean;
|
|
123
|
+
frozen?: 'left' | 'right';
|
|
124
|
+
// Legacy props for backward compatibility
|
|
125
|
+
key?: string;
|
|
126
|
+
width?: number;
|
|
127
|
+
sortable?: boolean;
|
|
128
|
+
visible?: boolean;
|
|
129
|
+
render?: (value: any, row: T) => ReactNode;
|
|
130
|
+
title?: string; // Legacy alias for header
|
|
131
|
+
dataIndex?: string; // Legacy alias for accessorKey
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
interface PaginationConfig {
|
|
135
|
+
enabled?: boolean;
|
|
136
|
+
pageSize?: number;
|
|
137
|
+
showPageSizeOptions?: boolean;
|
|
138
|
+
pageSizeOptions?: number[];
|
|
139
|
+
// Legacy pagination props
|
|
140
|
+
current?: number;
|
|
141
|
+
total?: number;
|
|
142
|
+
onChange?: (page: number, size: number) => void;
|
|
143
|
+
showSizeChanger?: boolean;
|
|
144
|
+
// More legacy pagination props
|
|
145
|
+
currentPage?: number;
|
|
146
|
+
totalPages?: number;
|
|
147
|
+
totalItems?: number;
|
|
148
|
+
onPageChange?: () => void;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export interface SyutGridProps<T extends { id: string }> {
|
|
152
|
+
data?: T[];
|
|
153
|
+
columns: SyutGridColumn<T>[];
|
|
154
|
+
enableRowSelection?: boolean;
|
|
155
|
+
enablePagination?: boolean;
|
|
156
|
+
pageSizeOptions?: number[];
|
|
157
|
+
defaultPageSize?: number;
|
|
158
|
+
enableSorting?: boolean;
|
|
159
|
+
enableColumnFilters?: boolean;
|
|
160
|
+
rowActions?: ((row: T) => ActionMenuItem<T>[]) | ActionMenuItem<T>[];
|
|
161
|
+
emptyMessage?: string;
|
|
162
|
+
/** Callback when row selection changes. When using legacy selectedRows prop, receives string[]. Otherwise receives T[]. */
|
|
163
|
+
onSelectionChange?: (selectedRows: string[]) => void;
|
|
164
|
+
isLoading?: boolean;
|
|
165
|
+
className?: string;
|
|
166
|
+
frozenLeftColumns?: string[];
|
|
167
|
+
frozenRightColumns?: string[];
|
|
168
|
+
visibleColumns?: string[];
|
|
169
|
+
rowKey?: string;
|
|
170
|
+
selectable?: boolean;
|
|
171
|
+
selectedRows?: string[];
|
|
172
|
+
pagination?: PaginationConfig;
|
|
173
|
+
// Legacy props
|
|
174
|
+
dataSource?: T[]; // Legacy alias for data
|
|
175
|
+
rowSelection?: { // Legacy row selection config
|
|
176
|
+
selectedRowKeys?: string[];
|
|
177
|
+
onChange?: (selectedRowKeys: string[]) => void;
|
|
178
|
+
};
|
|
179
|
+
loading?: boolean; // Legacy alias for isLoading
|
|
180
|
+
emptyText?: string; // Legacy alias for emptyMessage
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
interface FrozenColumnInfo {
|
|
184
|
+
frozen: 'left' | 'right' | null;
|
|
185
|
+
leftOffset: number;
|
|
186
|
+
rightOffset: number;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function ColumnFilter<T>({ column, placeholder }: { column: Column<T, unknown>; placeholder?: string }) {
|
|
190
|
+
const columnFilterValue = column.getFilterValue() as string;
|
|
191
|
+
return (
|
|
192
|
+
<input
|
|
193
|
+
type="text"
|
|
194
|
+
value={columnFilterValue ?? ''}
|
|
195
|
+
onChange={(e) => column.setFilterValue(e.target.value)}
|
|
196
|
+
placeholder={placeholder || 'Filter...'}
|
|
197
|
+
onClick={(e) => e.stopPropagation()}
|
|
198
|
+
style={{ width: '100%', padding: '0.25rem 0.5rem', fontSize: '0.75rem', border: '1px solid var(--border-color)', borderRadius: '0.25rem', marginTop: '0.375rem', background: 'var(--bg-primary)' }}
|
|
199
|
+
/>
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export function SyutGrid<T extends { id: string }>({
|
|
204
|
+
data: dataProp,
|
|
205
|
+
columns,
|
|
206
|
+
enableRowSelection: enableRowSelectionProp = false,
|
|
207
|
+
enablePagination: enablePaginationProp,
|
|
208
|
+
pageSizeOptions: pageSizeOptionsProp = [10, 25, 50, 100],
|
|
209
|
+
defaultPageSize: defaultPageSizeProp = 10,
|
|
210
|
+
enableSorting = true,
|
|
211
|
+
enableColumnFilters = false,
|
|
212
|
+
rowActions,
|
|
213
|
+
emptyMessage: emptyMessageProp = 'No data found',
|
|
214
|
+
onSelectionChange,
|
|
215
|
+
isLoading: isLoadingProp = false,
|
|
216
|
+
className = '',
|
|
217
|
+
frozenLeftColumns = [],
|
|
218
|
+
frozenRightColumns = [],
|
|
219
|
+
visibleColumns,
|
|
220
|
+
selectable,
|
|
221
|
+
selectedRows: selectedRowsProp,
|
|
222
|
+
pagination: paginationConfig,
|
|
223
|
+
// Legacy props
|
|
224
|
+
dataSource,
|
|
225
|
+
rowSelection: rowSelectionConfig,
|
|
226
|
+
loading,
|
|
227
|
+
emptyText,
|
|
228
|
+
}: SyutGridProps<T>) {
|
|
229
|
+
// Handle legacy props
|
|
230
|
+
const data = dataProp || dataSource || [];
|
|
231
|
+
const isLoading = loading ?? isLoadingProp;
|
|
232
|
+
const emptyMessage = emptyText || emptyMessageProp;
|
|
233
|
+
const enableRowSelection = selectable ?? (rowSelectionConfig !== undefined ? true : enableRowSelectionProp);
|
|
234
|
+
const enablePagination = paginationConfig?.enabled ?? enablePaginationProp ?? true;
|
|
235
|
+
const pageSizeOptions = paginationConfig?.pageSizeOptions ?? pageSizeOptionsProp;
|
|
236
|
+
const defaultPageSize = paginationConfig?.pageSize ?? defaultPageSizeProp;
|
|
237
|
+
|
|
238
|
+
// Get initial selected rows from either new or legacy prop
|
|
239
|
+
const initialSelectedRows = selectedRowsProp || rowSelectionConfig?.selectedRowKeys || [];
|
|
240
|
+
|
|
241
|
+
const [sorting, setSorting] = useState<SortingState>([]);
|
|
242
|
+
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]);
|
|
243
|
+
const [rowSelection, setRowSelection] = useState<RowSelectionState>(() => {
|
|
244
|
+
if (initialSelectedRows.length > 0) {
|
|
245
|
+
const selection: RowSelectionState = {};
|
|
246
|
+
initialSelectedRows.forEach(id => { selection[id] = true; });
|
|
247
|
+
return selection;
|
|
248
|
+
}
|
|
249
|
+
return {};
|
|
250
|
+
});
|
|
251
|
+
const [pagination, setPagination] = useState<PaginationState>({ pageIndex: 0, pageSize: defaultPageSize });
|
|
252
|
+
|
|
253
|
+
useEffect(() => {
|
|
254
|
+
const selectedRows = selectedRowsProp || rowSelectionConfig?.selectedRowKeys;
|
|
255
|
+
if (selectedRows) {
|
|
256
|
+
const selection: RowSelectionState = {};
|
|
257
|
+
selectedRows.forEach(id => { selection[id] = true; });
|
|
258
|
+
setRowSelection(selection);
|
|
259
|
+
}
|
|
260
|
+
}, [selectedRowsProp, rowSelectionConfig?.selectedRowKeys]);
|
|
261
|
+
|
|
262
|
+
const normalizedColumns = useMemo(() => {
|
|
263
|
+
return columns.map(col => ({
|
|
264
|
+
...col,
|
|
265
|
+
id: col.id || col.key || '',
|
|
266
|
+
header: col.header || col.title || '',
|
|
267
|
+
// Use accessorKey, or dataIndex, or key as the accessor for getting cell values
|
|
268
|
+
accessorKey: col.accessorKey || (col.dataIndex as keyof T) || (col.key as keyof T),
|
|
269
|
+
size: col.size || col.width,
|
|
270
|
+
enableSorting: col.enableSorting ?? col.sortable ?? true,
|
|
271
|
+
}));
|
|
272
|
+
}, [columns]);
|
|
273
|
+
|
|
274
|
+
const columnVisibility = useMemo<VisibilityState>(() => {
|
|
275
|
+
const legacyVisibility: VisibilityState = {};
|
|
276
|
+
normalizedColumns.forEach(col => {
|
|
277
|
+
if (col.visible !== undefined) legacyVisibility[col.id] = col.visible;
|
|
278
|
+
});
|
|
279
|
+
if (Object.keys(legacyVisibility).length > 0) return legacyVisibility;
|
|
280
|
+
if (!visibleColumns) return {};
|
|
281
|
+
const visibility: VisibilityState = {};
|
|
282
|
+
const allColumnIds = [...(enableRowSelection ? ['select'] : []), ...normalizedColumns.map(col => col.id), ...(rowActions ? ['actions'] : [])];
|
|
283
|
+
allColumnIds.forEach(colId => { visibility[colId] = visibleColumns.includes(colId); });
|
|
284
|
+
return visibility;
|
|
285
|
+
}, [visibleColumns, normalizedColumns, enableRowSelection, rowActions]);
|
|
286
|
+
|
|
287
|
+
const frozenColumnMap = useMemo(() => {
|
|
288
|
+
const map: Record<string, FrozenColumnInfo> = {};
|
|
289
|
+
const getColumnWidth = (colId: string): number => {
|
|
290
|
+
if (colId === 'select') return 50;
|
|
291
|
+
if (colId === 'actions') return 80;
|
|
292
|
+
const col = normalizedColumns.find(c => c.id === colId);
|
|
293
|
+
return col?.size || 150;
|
|
294
|
+
};
|
|
295
|
+
let leftOffset = 0;
|
|
296
|
+
frozenLeftColumns.forEach((colId) => { map[colId] = { frozen: 'left', leftOffset, rightOffset: 0 }; leftOffset += getColumnWidth(colId); });
|
|
297
|
+
let rightOffset = 0;
|
|
298
|
+
[...frozenRightColumns].reverse().forEach((colId) => { map[colId] = { frozen: 'right', leftOffset: 0, rightOffset }; rightOffset += getColumnWidth(colId); });
|
|
299
|
+
return map;
|
|
300
|
+
}, [normalizedColumns, frozenLeftColumns, frozenRightColumns]);
|
|
301
|
+
|
|
302
|
+
const getStickyStyles = useCallback((columnId: string): React.CSSProperties => {
|
|
303
|
+
const frozenInfo = frozenColumnMap[columnId];
|
|
304
|
+
if (!frozenInfo || !frozenInfo.frozen) return {};
|
|
305
|
+
return { position: 'sticky', [frozenInfo.frozen]: frozenInfo.frozen === 'left' ? frozenInfo.leftOffset : frozenInfo.rightOffset, zIndex: 1 };
|
|
306
|
+
}, [frozenColumnMap]);
|
|
307
|
+
|
|
308
|
+
const tableColumns = useMemo(() => {
|
|
309
|
+
const cols: ColumnDef<T, any>[] = [];
|
|
310
|
+
if (enableRowSelection) {
|
|
311
|
+
cols.push({
|
|
312
|
+
id: 'select',
|
|
313
|
+
header: ({ table }) => (<input type="checkbox" className="row-select-all" checked={table.getIsAllRowsSelected()} onChange={table.getToggleAllRowsSelectedHandler()} />),
|
|
314
|
+
cell: ({ row }) => (<input type="checkbox" className="row-select" checked={row.getIsSelected()} onChange={row.getToggleSelectedHandler()} />),
|
|
315
|
+
size: 50,
|
|
316
|
+
enableColumnFilter: false,
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
normalizedColumns.forEach((col) => {
|
|
320
|
+
const canFilter = enableColumnFilters && col.enableFiltering !== false;
|
|
321
|
+
let cellRenderer: ColumnDef<T, any>['cell'];
|
|
322
|
+
if (col.cell) { cellRenderer = (info) => col.cell!({ row: info.row, getValue: info.getValue }); }
|
|
323
|
+
else if (col.render) {
|
|
324
|
+
// Support both render signatures:
|
|
325
|
+
// 1. render(value, row) - value first, row second (when column has accessor)
|
|
326
|
+
// 2. render(row) - just the row object (when column has no accessor)
|
|
327
|
+
// We detect based on whether there's an accessor key defined
|
|
328
|
+
const hasAccessor = col.accessorKey || col.dataIndex || col.key;
|
|
329
|
+
cellRenderer = (info) => {
|
|
330
|
+
if (hasAccessor) {
|
|
331
|
+
// Column has accessor - pass value and row (legacy pattern)
|
|
332
|
+
return col.render!(info.getValue(), info.row.original);
|
|
333
|
+
} else {
|
|
334
|
+
// No accessor - pass just the row object
|
|
335
|
+
return (col.render as (row: T) => React.ReactNode)(info.row.original);
|
|
336
|
+
}
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
else { cellRenderer = (info) => info.getValue(); }
|
|
340
|
+
cols.push({
|
|
341
|
+
id: col.id,
|
|
342
|
+
accessorKey: col.accessorKey,
|
|
343
|
+
header: typeof col.header === 'string' ? ({ column }) => {
|
|
344
|
+
const headerText = col.header as string;
|
|
345
|
+
const isSortable = enableSorting && col.enableSorting !== false;
|
|
346
|
+
return (<div><div style={{ cursor: isSortable ? 'pointer' : 'default', display: 'flex', alignItems: 'center', gap: '4px' }} onClick={() => isSortable && column.toggleSorting()}><span>{headerText}</span>{isSortable && (<span>{column.getIsSorted() === 'asc' ? (<IconSystem.chevronUp size={14} />) : column.getIsSorted() === 'desc' ? (<IconSystem.chevronDown size={14} />) : (<IconSystem.chevronUp size={14} style={{ opacity: 0.3 }} />)}</span>)}</div>{canFilter && (<ColumnFilter column={column} placeholder={`Filter ${headerText}...`} />)}</div>);
|
|
347
|
+
} : col.header,
|
|
348
|
+
cell: cellRenderer,
|
|
349
|
+
size: col.size,
|
|
350
|
+
enableSorting: enableSorting && col.enableSorting !== false,
|
|
351
|
+
enableColumnFilter: canFilter,
|
|
352
|
+
});
|
|
353
|
+
});
|
|
354
|
+
if (rowActions) {
|
|
355
|
+
cols.push({
|
|
356
|
+
id: 'actions',
|
|
357
|
+
header: 'Actions',
|
|
358
|
+
cell: ({ row }) => {
|
|
359
|
+
// Handle both patterns: function that returns items, or array of items with row-accepting onClick
|
|
360
|
+
let items: ProcessedActionMenuItem[];
|
|
361
|
+
if (typeof rowActions === 'function') {
|
|
362
|
+
// Function pattern - items are already processed
|
|
363
|
+
items = rowActions(row.original) as ProcessedActionMenuItem[];
|
|
364
|
+
} else {
|
|
365
|
+
// Array pattern - need to filter by isVisible and wrap onClick with row
|
|
366
|
+
items = rowActions
|
|
367
|
+
.filter(item => !item.isVisible || item.isVisible(row.original))
|
|
368
|
+
.map(item => ({
|
|
369
|
+
id: item.id,
|
|
370
|
+
label: item.label,
|
|
371
|
+
icon: item.icon,
|
|
372
|
+
variant: item.variant,
|
|
373
|
+
disabled: item.disabled,
|
|
374
|
+
onClick: () => (item.onClick as (row: T) => void)(row.original)
|
|
375
|
+
}));
|
|
376
|
+
}
|
|
377
|
+
return <ActionMenu items={items} />;
|
|
378
|
+
},
|
|
379
|
+
size: 80,
|
|
380
|
+
enableColumnFilter: false
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
return cols;
|
|
384
|
+
}, [normalizedColumns, enableRowSelection, enableSorting, enableColumnFilters, rowActions]);
|
|
385
|
+
|
|
386
|
+
const handleRowSelectionChange = useCallback((updaterOrValue: Updater<RowSelectionState>) => {
|
|
387
|
+
setRowSelection((prev) => {
|
|
388
|
+
const newSelection = typeof updaterOrValue === 'function' ? updaterOrValue(prev) : updaterOrValue;
|
|
389
|
+
const selectedIds = Object.keys(newSelection).filter((key) => newSelection[key]);
|
|
390
|
+
// Call new API callback
|
|
391
|
+
if (onSelectionChange) {
|
|
392
|
+
onSelectionChange(selectedIds);
|
|
393
|
+
}
|
|
394
|
+
// Call legacy API callback
|
|
395
|
+
if (rowSelectionConfig?.onChange) {
|
|
396
|
+
rowSelectionConfig.onChange(selectedIds);
|
|
397
|
+
}
|
|
398
|
+
return newSelection;
|
|
399
|
+
});
|
|
400
|
+
}, [onSelectionChange, rowSelectionConfig]);
|
|
401
|
+
|
|
402
|
+
const table = useReactTable({
|
|
403
|
+
data,
|
|
404
|
+
columns: tableColumns,
|
|
405
|
+
state: { sorting, columnFilters, rowSelection, pagination, columnVisibility },
|
|
406
|
+
onSortingChange: setSorting,
|
|
407
|
+
onColumnFiltersChange: setColumnFilters,
|
|
408
|
+
onRowSelectionChange: handleRowSelectionChange,
|
|
409
|
+
onPaginationChange: setPagination,
|
|
410
|
+
getCoreRowModel: getCoreRowModel(),
|
|
411
|
+
getSortedRowModel: getSortedRowModel(),
|
|
412
|
+
getFilteredRowModel: getFilteredRowModel(),
|
|
413
|
+
getPaginationRowModel: enablePagination ? getPaginationRowModel() : undefined,
|
|
414
|
+
enableRowSelection,
|
|
415
|
+
getRowId: (row) => row.id,
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
return (
|
|
419
|
+
<div className={`syut-grid-component ${className}`} style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
|
|
420
|
+
<div className="syut-grid-wrapper" style={{ flex: 1, overflow: 'auto', minHeight: 0 }}>
|
|
421
|
+
<table className="syut-grid">
|
|
422
|
+
<thead>
|
|
423
|
+
{table.getHeaderGroups().map((headerGroup) => (
|
|
424
|
+
<tr key={headerGroup.id}>
|
|
425
|
+
{headerGroup.headers.map((header) => {
|
|
426
|
+
const stickyStyles = getStickyStyles(header.id);
|
|
427
|
+
const isFrozen = !!frozenColumnMap[header.id];
|
|
428
|
+
return (<th key={header.id} style={{ width: header.getSize() !== 150 ? header.getSize() : undefined, ...stickyStyles, ...(isFrozen && { zIndex: 4 }) }} className={`${header.id === 'actions' ? 'action-column' : ''} ${isFrozen ? 'frozen-column' : ''}`}>{header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())}</th>);
|
|
429
|
+
})}
|
|
430
|
+
</tr>
|
|
431
|
+
))}
|
|
432
|
+
</thead>
|
|
433
|
+
<tbody>
|
|
434
|
+
{isLoading ? (<tr><td colSpan={table.getAllColumns().length} style={{ textAlign: 'center', padding: '2rem', color: 'var(--text-muted)' }}>Loading...</td></tr>) : table.getRowModel().rows.length === 0 ? (<tr><td colSpan={table.getAllColumns().length} style={{ textAlign: 'center', padding: '2rem', color: 'var(--text-muted)' }}>{emptyMessage}</td></tr>) : (
|
|
435
|
+
table.getRowModel().rows.map((row) => (
|
|
436
|
+
<tr key={row.id}>
|
|
437
|
+
{row.getVisibleCells().map((cell) => {
|
|
438
|
+
const stickyStyles = getStickyStyles(cell.column.id);
|
|
439
|
+
const isFrozen = !!frozenColumnMap[cell.column.id];
|
|
440
|
+
const isActionsColumn = cell.column.id === 'actions';
|
|
441
|
+
return (<td key={cell.id} className={`${isActionsColumn ? 'action-column' : ''} ${isFrozen ? 'frozen-column' : ''}`} style={{ ...stickyStyles, ...(isActionsColumn && { overflow: 'visible' }) }}>{flexRender(cell.column.columnDef.cell, cell.getContext())}</td>);
|
|
442
|
+
})}
|
|
443
|
+
</tr>
|
|
444
|
+
))
|
|
445
|
+
)}
|
|
446
|
+
</tbody>
|
|
447
|
+
</table>
|
|
448
|
+
</div>
|
|
449
|
+
{enablePagination && (
|
|
450
|
+
<div className="pagination-footer">
|
|
451
|
+
<div className="pagination-info">
|
|
452
|
+
Showing {table.getRowModel().rows.length} of {table.getFilteredRowModel().rows.length} items
|
|
453
|
+
{Object.keys(rowSelection).length > 0 && (
|
|
454
|
+
<span style={{ marginLeft: '0.5rem', color: 'var(--primary)' }}>({Object.keys(rowSelection).length} selected)</span>
|
|
455
|
+
)}
|
|
456
|
+
</div>
|
|
457
|
+
<div className="pagination-controls">
|
|
458
|
+
<select value={pagination.pageSize} onChange={(e) => table.setPageSize(Number(e.target.value))}>
|
|
459
|
+
{pageSizeOptions.map((size) => (<option key={size} value={size}>{size} / page</option>))}
|
|
460
|
+
</select>
|
|
461
|
+
<div className="page-nav-buttons">
|
|
462
|
+
<button className="page-btn" onClick={() => table.firstPage()} disabled={!table.getCanPreviousPage()} title="First page">
|
|
463
|
+
<IconSystem.chevronLeft size={16} /><IconSystem.chevronLeft size={16} style={{ marginLeft: '-8px' }} />
|
|
464
|
+
</button>
|
|
465
|
+
<button className="page-btn" onClick={() => table.previousPage()} disabled={!table.getCanPreviousPage()} title="Previous page">
|
|
466
|
+
<IconSystem.chevronLeft size={16} />
|
|
467
|
+
</button>
|
|
468
|
+
<span className="page-info">Page {table.getState().pagination.pageIndex + 1} of {table.getPageCount()}</span>
|
|
469
|
+
<button className="page-btn" onClick={() => table.nextPage()} disabled={!table.getCanNextPage()} title="Next page">
|
|
470
|
+
<IconSystem.chevronRight size={16} />
|
|
471
|
+
</button>
|
|
472
|
+
<button className="page-btn" onClick={() => table.lastPage()} disabled={!table.getCanNextPage()} title="Last page">
|
|
473
|
+
<IconSystem.chevronRight size={16} /><IconSystem.chevronRight size={16} style={{ marginLeft: '-8px' }} />
|
|
474
|
+
</button>
|
|
475
|
+
</div>
|
|
476
|
+
</div>
|
|
477
|
+
</div>
|
|
478
|
+
)}
|
|
479
|
+
</div>
|
|
480
|
+
);
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
export default SyutGrid;
|