@dashforge/tw 0.7.0-beta → 0.8.0-beta
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/CHANGELOG.md +193 -0
- package/dist/index.esm.js +1092 -74
- package/dist/src/components/DataGrid/DataGrid.d.ts.map +1 -1
- package/dist/src/components/DataGrid/dataGrid.types.d.ts +43 -0
- package/dist/src/components/DataGrid/dataGrid.types.d.ts.map +1 -1
- package/dist/src/components/DataGrid/dataGrid.variants.d.ts +6 -0
- package/dist/src/components/DataGrid/dataGrid.variants.d.ts.map +1 -1
- package/dist/src/components/DataGrid/filters/ColumnFilters.d.ts +23 -0
- package/dist/src/components/DataGrid/filters/ColumnFilters.d.ts.map +1 -0
- package/dist/src/components/DataGrid/reorder/useColumnReorder.d.ts +50 -0
- package/dist/src/components/DataGrid/reorder/useColumnReorder.d.ts.map +1 -0
- package/dist/src/components/DataGrid/resize/useColumnResize.d.ts +32 -0
- package/dist/src/components/DataGrid/resize/useColumnResize.d.ts.map +1 -0
- package/dist/src/components/DataGrid/visibility/ColumnVisibilityDialog.d.ts +33 -0
- package/dist/src/components/DataGrid/visibility/ColumnVisibilityDialog.d.ts.map +1 -0
- package/dist/src/components/Table/Table.d.ts.map +1 -1
- package/dist/src/components/Table/table.types.d.ts +69 -5
- package/dist/src/components/Table/table.types.d.ts.map +1 -1
- package/dist/src/components/_shared/data/useTableFilter.d.ts +30 -7
- package/dist/src/components/_shared/data/useTableFilter.d.ts.map +1 -1
- package/dist/src/index.d.ts +1 -1
- package/dist/src/index.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/components/DataGrid/DataGrid.test.tsx +422 -1
- package/src/components/DataGrid/DataGrid.tsx +421 -53
- package/src/components/DataGrid/dataGrid.types.ts +50 -1
- package/src/components/DataGrid/dataGrid.variants.ts +9 -1
- package/src/components/DataGrid/filters/ColumnFilters.tsx +432 -0
- package/src/components/DataGrid/reorder/useColumnReorder.test.ts +38 -0
- package/src/components/DataGrid/reorder/useColumnReorder.ts +144 -0
- package/src/components/DataGrid/resize/useColumnResize.ts +106 -0
- package/src/components/DataGrid/visibility/ColumnVisibilityDialog.tsx +200 -0
- package/src/components/Table/Table.tsx +14 -0
- package/src/components/Table/table.types.ts +74 -5
- package/src/components/_shared/data/useTableFilter.test.ts +166 -0
- package/src/components/_shared/data/useTableFilter.ts +97 -16
- package/src/index.ts +2 -0
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
useMemo,
|
|
3
|
+
type ReactNode,
|
|
4
|
+
type MouseEvent,
|
|
5
|
+
type PointerEvent as RPointerEvent,
|
|
6
|
+
} from 'react';
|
|
2
7
|
import { cn } from '../../utils/cn.js';
|
|
3
8
|
import { useAccessState } from '../../hooks/useAccessState.js';
|
|
4
9
|
import { Skeleton } from '../Skeleton/Skeleton.js';
|
|
@@ -20,11 +25,20 @@ import {
|
|
|
20
25
|
} from '../_shared/data/useColumnAutoDetect.js';
|
|
21
26
|
import type {
|
|
22
27
|
TableColumn,
|
|
28
|
+
TableColumnInferredType,
|
|
29
|
+
TableFilterType,
|
|
23
30
|
TableSortModel,
|
|
24
31
|
TableFilterModel,
|
|
25
32
|
TableLabels,
|
|
26
33
|
} from '../Table/table.types.js';
|
|
27
34
|
import type { DataGridProps } from './dataGrid.types.js';
|
|
35
|
+
import { ColumnFilterTrigger } from './filters/ColumnFilters.js';
|
|
36
|
+
import { ColumnVisibilityTrigger } from './visibility/ColumnVisibilityDialog.js';
|
|
37
|
+
import { useColumnResize } from './resize/useColumnResize.js';
|
|
38
|
+
import {
|
|
39
|
+
useColumnReorder,
|
|
40
|
+
type ColumnReorderHandlers,
|
|
41
|
+
} from './reorder/useColumnReorder.js';
|
|
28
42
|
|
|
29
43
|
const DEFAULT_LABELS: Required<TableLabels> = {
|
|
30
44
|
searchPlaceholder: 'Search...',
|
|
@@ -42,10 +56,24 @@ const DEFAULT_LABELS: Required<TableLabels> = {
|
|
|
42
56
|
filterColumn: 'Filter',
|
|
43
57
|
filterApply: 'Apply',
|
|
44
58
|
filterClear: 'Clear',
|
|
59
|
+
filterMin: 'Min',
|
|
60
|
+
filterMax: 'Max',
|
|
61
|
+
filterFrom: 'From',
|
|
62
|
+
filterTo: 'To',
|
|
63
|
+
filterAll: 'All',
|
|
64
|
+
filterTrue: 'True',
|
|
65
|
+
filterFalse: 'False',
|
|
45
66
|
density: 'Density',
|
|
46
67
|
densityCompact: 'Compact',
|
|
47
68
|
densityComfortable: 'Comfortable',
|
|
48
69
|
densitySpacious: 'Spacious',
|
|
70
|
+
columnsButton: 'Columns',
|
|
71
|
+
columnsTitle: 'Manage columns',
|
|
72
|
+
columnsDescription:
|
|
73
|
+
'Show or hide columns. Required columns cannot be toggled.',
|
|
74
|
+
columnsShowAll: 'Show all',
|
|
75
|
+
columnsHideAll: 'Hide all',
|
|
76
|
+
columnsDone: 'Done',
|
|
49
77
|
};
|
|
50
78
|
|
|
51
79
|
/**
|
|
@@ -101,6 +129,18 @@ export function DataGrid<T extends object>(props: DataGridProps<T>) {
|
|
|
101
129
|
onFilterChange,
|
|
102
130
|
serverSideFilter = false,
|
|
103
131
|
|
|
132
|
+
hiddenColumns: hiddenColumnsProp,
|
|
133
|
+
onHiddenColumnsChange,
|
|
134
|
+
enableColumnVisibility = true,
|
|
135
|
+
|
|
136
|
+
columnWidths: columnWidthsProp,
|
|
137
|
+
onColumnWidthsChange,
|
|
138
|
+
enableColumnResize = true,
|
|
139
|
+
|
|
140
|
+
columnOrder: columnOrderProp,
|
|
141
|
+
onColumnOrderChange,
|
|
142
|
+
enableColumnReorder = true,
|
|
143
|
+
|
|
104
144
|
rowSelection = 'none',
|
|
105
145
|
selectedRowIds: selectedRowIdsProp,
|
|
106
146
|
onSelectionChange,
|
|
@@ -147,24 +187,116 @@ export function DataGrid<T extends object>(props: DataGridProps<T>) {
|
|
|
147
187
|
defaultValue: '',
|
|
148
188
|
onChange: onSearchQueryChange,
|
|
149
189
|
});
|
|
150
|
-
// filterModel:
|
|
151
|
-
//
|
|
152
|
-
|
|
153
|
-
const filterModel
|
|
190
|
+
// filterModel: in v1-bis the per-column header UI commits filter
|
|
191
|
+
// items to this controllable state. Consumer can lift it via
|
|
192
|
+
// filterModelProp + onFilterChange to drive server-side filtering.
|
|
193
|
+
const [filterModel, setFilterModel] = useControllableState<TableFilterModel>({
|
|
194
|
+
controlledValue: filterModelProp,
|
|
195
|
+
defaultValue: [],
|
|
196
|
+
onChange: onFilterChange,
|
|
197
|
+
});
|
|
154
198
|
const [selectedRowIds, setSelectedRowIds] = useControllableState<string[]>({
|
|
155
199
|
controlledValue: selectedRowIdsProp,
|
|
156
200
|
defaultValue: [],
|
|
157
201
|
onChange: onSelectionChange,
|
|
158
202
|
});
|
|
159
203
|
|
|
160
|
-
// ─────
|
|
161
|
-
|
|
204
|
+
// ───── Column visibility ─────
|
|
205
|
+
// Seed default hidden set from `defaultHidden` flag on columns.
|
|
206
|
+
// Memoized to avoid the seed shifting on every render — the
|
|
207
|
+
// controllable state keeps internal state stable thereafter.
|
|
208
|
+
const defaultHiddenColumns = useMemo(
|
|
162
209
|
() =>
|
|
163
|
-
cols
|
|
164
|
-
(
|
|
165
|
-
|
|
210
|
+
cols
|
|
211
|
+
.filter((c) => c.defaultHidden === true)
|
|
212
|
+
.map((c) => c.field as string),
|
|
166
213
|
[cols],
|
|
167
214
|
);
|
|
215
|
+
const [hiddenColumns, setHiddenColumns] = useControllableState<string[]>({
|
|
216
|
+
controlledValue: hiddenColumnsProp,
|
|
217
|
+
defaultValue: defaultHiddenColumns,
|
|
218
|
+
onChange: onHiddenColumnsChange,
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
// ───── Column widths (resize state) ─────
|
|
222
|
+
// Seed from any `col.width` declared statically. Internal state is
|
|
223
|
+
// applied only when the user actually starts dragging; until then
|
|
224
|
+
// we fall back to the static width from the column def.
|
|
225
|
+
const defaultColumnWidths = useMemo(() => {
|
|
226
|
+
const map: Record<string, number> = {};
|
|
227
|
+
for (const c of cols) {
|
|
228
|
+
if (typeof c.width === 'number') {
|
|
229
|
+
map[c.field as string] = c.width;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
return map;
|
|
233
|
+
}, [cols]);
|
|
234
|
+
const [columnWidths, setColumnWidths] = useControllableState<
|
|
235
|
+
Record<string, number>
|
|
236
|
+
>({
|
|
237
|
+
controlledValue: columnWidthsProp,
|
|
238
|
+
defaultValue: defaultColumnWidths,
|
|
239
|
+
onChange: onColumnWidthsChange,
|
|
240
|
+
});
|
|
241
|
+
const { startResize } = useColumnResize({
|
|
242
|
+
widths: columnWidths,
|
|
243
|
+
onChange: setColumnWidths,
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
// ───── Column order (reorder state) ─────
|
|
247
|
+
const defaultColumnOrder = useMemo(
|
|
248
|
+
() => cols.map((c) => c.field as string),
|
|
249
|
+
[cols],
|
|
250
|
+
);
|
|
251
|
+
const [columnOrder, setColumnOrder] = useControllableState<string[]>({
|
|
252
|
+
controlledValue: columnOrderProp,
|
|
253
|
+
defaultValue: defaultColumnOrder,
|
|
254
|
+
onChange: onColumnOrderChange,
|
|
255
|
+
});
|
|
256
|
+
const reorderHandlers = useColumnReorder({
|
|
257
|
+
order: columnOrder,
|
|
258
|
+
onChange: setColumnOrder,
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
// Apply the active order to the columns list. Cols not in the
|
|
262
|
+
// order array (e.g. newly-added columns at runtime) are appended
|
|
263
|
+
// in their original position from `cols`.
|
|
264
|
+
const orderedCols = useMemo(() => {
|
|
265
|
+
if (!columnOrder || columnOrder.length === 0) return cols;
|
|
266
|
+
const byField = new Map(
|
|
267
|
+
cols.map((c) => [c.field as string, c] as const),
|
|
268
|
+
);
|
|
269
|
+
const seen = new Set<string>();
|
|
270
|
+
const result: typeof cols = [];
|
|
271
|
+
for (const field of columnOrder) {
|
|
272
|
+
const c = byField.get(field);
|
|
273
|
+
if (c) {
|
|
274
|
+
result.push(c);
|
|
275
|
+
seen.add(field);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
for (const c of cols) {
|
|
279
|
+
if (!seen.has(c.field as string)) result.push(c);
|
|
280
|
+
}
|
|
281
|
+
return result;
|
|
282
|
+
}, [cols, columnOrder]);
|
|
283
|
+
|
|
284
|
+
// ───── RBAC + visibility + reorder: derive final column list ─────
|
|
285
|
+
// - `access.onUnauthorized === 'hide'` removes a column entirely.
|
|
286
|
+
// - `hiddenColumns` is the user-toggled hidden set from the
|
|
287
|
+
// column-visibility dialog.
|
|
288
|
+
// - `orderedCols` is the user-reordered version of `cols`.
|
|
289
|
+
// NOTE: filtering happens on `orderedCols` so the active order is
|
|
290
|
+
// preserved through visibility changes.
|
|
291
|
+
const visibleCols = useMemo(
|
|
292
|
+
() =>
|
|
293
|
+
orderedCols.filter((col) => {
|
|
294
|
+
if (col.access && col.access.onUnauthorized === 'hide') return false;
|
|
295
|
+
if (hiddenColumns.includes(col.field as string)) return false;
|
|
296
|
+
return true;
|
|
297
|
+
}),
|
|
298
|
+
[orderedCols, hiddenColumns],
|
|
299
|
+
);
|
|
168
300
|
|
|
169
301
|
// ───── Smart-default column types ─────
|
|
170
302
|
const columnTypes = useColumnAutoDetect(rows, visibleCols);
|
|
@@ -257,25 +389,47 @@ export function DataGrid<T extends object>(props: DataGridProps<T>) {
|
|
|
257
389
|
data-disabled={!isInteractive ? 'true' : undefined}
|
|
258
390
|
>
|
|
259
391
|
{/* ───── Toolbar ───── */}
|
|
260
|
-
{enableSearch && (
|
|
392
|
+
{(enableSearch || enableColumnVisibility) && (
|
|
261
393
|
<div className={cn(v.toolbar(), slotProps?.toolbar?.className)}>
|
|
262
|
-
|
|
263
|
-
<
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
394
|
+
{enableSearch && (
|
|
395
|
+
<label className={cn(v.search(), slotProps?.search?.className)}>
|
|
396
|
+
<span className="sr-only">{labels.searchPlaceholder}</span>
|
|
397
|
+
<SearchIcon className="ml-2 shrink-0 text-neutral-500" />
|
|
398
|
+
<input
|
|
399
|
+
type="text"
|
|
400
|
+
value={searchQuery}
|
|
401
|
+
onChange={(e) => setSearchQuery(e.target.value)}
|
|
402
|
+
placeholder={searchPlaceholder ?? labels.searchPlaceholder}
|
|
403
|
+
disabled={!isInteractive}
|
|
404
|
+
className={cn(
|
|
405
|
+
'flex-1 bg-transparent border-0 outline-none',
|
|
406
|
+
'px-2 py-1.5',
|
|
407
|
+
'placeholder:text-neutral-400',
|
|
408
|
+
'disabled:opacity-50 disabled:cursor-not-allowed',
|
|
409
|
+
)}
|
|
410
|
+
/>
|
|
411
|
+
</label>
|
|
412
|
+
)}
|
|
413
|
+
{enableColumnVisibility && (
|
|
414
|
+
<ColumnVisibilityTrigger
|
|
415
|
+
cols={cols.filter(
|
|
416
|
+
(c) =>
|
|
417
|
+
// RBAC-hidden columns are never offered in the dialog.
|
|
418
|
+
!c.access || c.access.onUnauthorized !== 'hide',
|
|
276
419
|
)}
|
|
420
|
+
hiddenColumns={hiddenColumns}
|
|
421
|
+
onChange={setHiddenColumns}
|
|
422
|
+
disabled={!isInteractive}
|
|
423
|
+
labels={{
|
|
424
|
+
columnsButton: labels.columnsButton,
|
|
425
|
+
columnsTitle: labels.columnsTitle,
|
|
426
|
+
columnsDescription: labels.columnsDescription,
|
|
427
|
+
columnsShowAll: labels.columnsShowAll,
|
|
428
|
+
columnsHideAll: labels.columnsHideAll,
|
|
429
|
+
columnsDone: labels.columnsDone,
|
|
430
|
+
}}
|
|
277
431
|
/>
|
|
278
|
-
|
|
432
|
+
)}
|
|
279
433
|
</div>
|
|
280
434
|
)}
|
|
281
435
|
|
|
@@ -336,10 +490,22 @@ export function DataGrid<T extends object>(props: DataGridProps<T>) {
|
|
|
336
490
|
handleSortClick(col, e, sortModel, setSortModel)
|
|
337
491
|
}
|
|
338
492
|
inferredType={columnTypes.get(col.field as string)}
|
|
493
|
+
filterModel={filterModel}
|
|
494
|
+
onFilterChange={setFilterModel}
|
|
495
|
+
effectiveWidth={resolveColumnWidth(col, columnWidths)}
|
|
496
|
+
resizable={
|
|
497
|
+
enableColumnResize && col.resizable !== false
|
|
498
|
+
}
|
|
499
|
+
startResize={startResize}
|
|
500
|
+
reorderable={
|
|
501
|
+
enableColumnReorder && col.reorderable !== false
|
|
502
|
+
}
|
|
503
|
+
reorderHandlers={reorderHandlers}
|
|
339
504
|
labels={labels}
|
|
340
505
|
className={cn(
|
|
341
506
|
v.headerCell(),
|
|
342
507
|
col.sticky === 'left' && v.stickyLeftHeaderCell(),
|
|
508
|
+
col.sticky === 'right' && v.stickyRightHeaderCell(),
|
|
343
509
|
slotProps?.headerCell?.className,
|
|
344
510
|
)}
|
|
345
511
|
buttonClassName={v.headerCellButton()}
|
|
@@ -389,6 +555,7 @@ export function DataGrid<T extends object>(props: DataGridProps<T>) {
|
|
|
389
555
|
renderedRows,
|
|
390
556
|
visibleCols,
|
|
391
557
|
columnTypes,
|
|
558
|
+
columnWidths,
|
|
392
559
|
selection,
|
|
393
560
|
rowSelection,
|
|
394
561
|
showSelectionColumn,
|
|
@@ -409,6 +576,7 @@ export function DataGrid<T extends object>(props: DataGridProps<T>) {
|
|
|
409
576
|
slotProps?.selectionCell?.className,
|
|
410
577
|
),
|
|
411
578
|
stickyLeftCell: v.stickyLeftCell(),
|
|
579
|
+
stickyRightCell: v.stickyRightCell(),
|
|
412
580
|
rowActionsCell: cn(
|
|
413
581
|
v.cell(),
|
|
414
582
|
v.rowActionsCell(),
|
|
@@ -475,6 +643,18 @@ function HeaderCell<T extends object>(props: {
|
|
|
475
643
|
sortModel: TableSortModel;
|
|
476
644
|
onSortClick: (e: MouseEvent<HTMLButtonElement>) => void;
|
|
477
645
|
inferredType: string | undefined;
|
|
646
|
+
filterModel: TableFilterModel;
|
|
647
|
+
onFilterChange: (model: TableFilterModel) => void;
|
|
648
|
+
effectiveWidth: number | undefined;
|
|
649
|
+
resizable: boolean;
|
|
650
|
+
startResize: (
|
|
651
|
+
field: string,
|
|
652
|
+
startWidth: number,
|
|
653
|
+
clampMin: number,
|
|
654
|
+
clampMax: number,
|
|
655
|
+
) => (e: RPointerEvent<HTMLElement>) => void;
|
|
656
|
+
reorderable: boolean;
|
|
657
|
+
reorderHandlers: ColumnReorderHandlers;
|
|
478
658
|
labels: Required<TableLabels>;
|
|
479
659
|
className: string;
|
|
480
660
|
buttonClassName: string;
|
|
@@ -485,6 +665,13 @@ function HeaderCell<T extends object>(props: {
|
|
|
485
665
|
sortModel,
|
|
486
666
|
onSortClick,
|
|
487
667
|
inferredType,
|
|
668
|
+
filterModel,
|
|
669
|
+
onFilterChange,
|
|
670
|
+
effectiveWidth,
|
|
671
|
+
resizable,
|
|
672
|
+
startResize,
|
|
673
|
+
reorderable,
|
|
674
|
+
reorderHandlers,
|
|
488
675
|
labels,
|
|
489
676
|
className,
|
|
490
677
|
buttonClassName,
|
|
@@ -507,50 +694,213 @@ function HeaderCell<T extends object>(props: {
|
|
|
507
694
|
typeof col.header === 'function' ? col.header() : col.header;
|
|
508
695
|
|
|
509
696
|
const cellStyle = {
|
|
510
|
-
width:
|
|
697
|
+
width: effectiveWidth != null ? `${effectiveWidth}px` : undefined,
|
|
511
698
|
minWidth: col.minWidth != null ? `${col.minWidth}px` : undefined,
|
|
512
|
-
flex: col.flex,
|
|
699
|
+
flex: effectiveWidth != null ? undefined : col.flex,
|
|
700
|
+
position: resizable || reorderable ? ('relative' as const) : undefined,
|
|
513
701
|
};
|
|
514
702
|
|
|
703
|
+
// Drop indicator for reorder: a 2px vertical bar at the LEFT or
|
|
704
|
+
// RIGHT edge of this th when it's the active drop target.
|
|
705
|
+
const dropTarget = reorderHandlers.dropTarget;
|
|
706
|
+
const dropIndicatorClass =
|
|
707
|
+
dropTarget != null && dropTarget.field === (col.field as string)
|
|
708
|
+
? dropTarget.side === 'left'
|
|
709
|
+
? 'before:absolute before:top-0 before:bottom-0 before:left-0 before:w-0.5 before:bg-primary-500'
|
|
710
|
+
: 'after:absolute after:top-0 after:bottom-0 after:right-0 after:w-0.5 after:bg-primary-500'
|
|
711
|
+
: '';
|
|
712
|
+
|
|
713
|
+
const isDragging = reorderHandlers.draggingField === (col.field as string);
|
|
714
|
+
|
|
715
|
+
// Drag attributes only attach when reorderable; we DO NOT make the
|
|
716
|
+
// entire th `draggable` if it isn't — keeps the keyboard focus
|
|
717
|
+
// behavior unchanged.
|
|
718
|
+
const dragAttrs = reorderable
|
|
719
|
+
? {
|
|
720
|
+
draggable: true,
|
|
721
|
+
onDragStart: reorderHandlers.dragStart(col.field as string),
|
|
722
|
+
onDragOver: reorderHandlers.dragOver(col.field as string),
|
|
723
|
+
onDrop: reorderHandlers.drop(col.field as string),
|
|
724
|
+
onDragEnd: reorderHandlers.dragEnd,
|
|
725
|
+
}
|
|
726
|
+
: {};
|
|
727
|
+
|
|
728
|
+
const resizeHandle = resizable ? (
|
|
729
|
+
<span
|
|
730
|
+
role="separator"
|
|
731
|
+
aria-orientation="vertical"
|
|
732
|
+
aria-label={`Resize ${typeof col.header === 'string' ? col.header : col.field}`}
|
|
733
|
+
onPointerDown={(e) => {
|
|
734
|
+
const handle = e.currentTarget;
|
|
735
|
+
const th = handle.parentElement as HTMLElement | null;
|
|
736
|
+
const startWidth =
|
|
737
|
+
effectiveWidth ?? th?.getBoundingClientRect().width ?? 120;
|
|
738
|
+
const min = col.minWidth ?? 40;
|
|
739
|
+
const max = col.maxWidth ?? 1200;
|
|
740
|
+
startResize(col.field as string, startWidth, min, max)(e);
|
|
741
|
+
}}
|
|
742
|
+
className={cn(
|
|
743
|
+
'absolute top-0 bottom-0 right-0 w-1.5',
|
|
744
|
+
'cursor-col-resize select-none touch-none',
|
|
745
|
+
'hover:bg-primary-300 active:bg-primary-400',
|
|
746
|
+
'transition-colors motion-reduce:transition-none',
|
|
747
|
+
)}
|
|
748
|
+
/>
|
|
749
|
+
) : null;
|
|
750
|
+
|
|
751
|
+
const filterType = col.filterable
|
|
752
|
+
? resolveFilterType(col, inferredType as TableColumnInferredType | undefined)
|
|
753
|
+
: null;
|
|
754
|
+
|
|
755
|
+
const filterTrigger =
|
|
756
|
+
col.filterable && filterType ? (
|
|
757
|
+
<ColumnFilterTrigger
|
|
758
|
+
field={col.field as string}
|
|
759
|
+
filterType={filterType}
|
|
760
|
+
filterModel={filterModel}
|
|
761
|
+
onFilterChange={onFilterChange}
|
|
762
|
+
disabled={disabled}
|
|
763
|
+
labels={{
|
|
764
|
+
filterColumn: labels.filterColumn,
|
|
765
|
+
filterApply: labels.filterApply,
|
|
766
|
+
filterClear: labels.filterClear,
|
|
767
|
+
filterMin: labels.filterMin,
|
|
768
|
+
filterMax: labels.filterMax,
|
|
769
|
+
filterFrom: labels.filterFrom,
|
|
770
|
+
filterTo: labels.filterTo,
|
|
771
|
+
filterAll: labels.filterAll,
|
|
772
|
+
filterTrue: labels.filterTrue,
|
|
773
|
+
filterFalse: labels.filterFalse,
|
|
774
|
+
}}
|
|
775
|
+
/>
|
|
776
|
+
) : null;
|
|
777
|
+
|
|
778
|
+
// Layout strategy:
|
|
779
|
+
// - sortable only → sort button is the cell content
|
|
780
|
+
// - filterable only → header text + filter trigger in a flex row
|
|
781
|
+
// - both → sort button + filter trigger in a flex row
|
|
782
|
+
// - neither → plain header text
|
|
783
|
+
const wrapperJustify =
|
|
784
|
+
align === 'right'
|
|
785
|
+
? 'justify-end'
|
|
786
|
+
: align === 'center'
|
|
787
|
+
? 'justify-center'
|
|
788
|
+
: 'justify-start';
|
|
789
|
+
|
|
515
790
|
if (col.sortable) {
|
|
791
|
+
const sortButton = (
|
|
792
|
+
<button
|
|
793
|
+
type="button"
|
|
794
|
+
onClick={onSortClick}
|
|
795
|
+
disabled={disabled}
|
|
796
|
+
aria-label={
|
|
797
|
+
ariaSort === 'ascending'
|
|
798
|
+
? labels.ariaSortAscending
|
|
799
|
+
: ariaSort === 'descending'
|
|
800
|
+
? labels.ariaSortDescending
|
|
801
|
+
: labels.ariaSortNone
|
|
802
|
+
}
|
|
803
|
+
className={cn(
|
|
804
|
+
buttonClassName,
|
|
805
|
+
align === 'right' && 'flex-row-reverse',
|
|
806
|
+
'disabled:cursor-not-allowed disabled:opacity-50',
|
|
807
|
+
)}
|
|
808
|
+
>
|
|
809
|
+
<span>{headerContent}</span>
|
|
810
|
+
<SortIcon direction={sortEntry?.direction} />
|
|
811
|
+
</button>
|
|
812
|
+
);
|
|
516
813
|
return (
|
|
517
814
|
<th
|
|
518
815
|
scope="col"
|
|
519
816
|
aria-sort={ariaSort}
|
|
520
|
-
className={cn(
|
|
817
|
+
className={cn(
|
|
818
|
+
className,
|
|
819
|
+
alignClass,
|
|
820
|
+
dropIndicatorClass,
|
|
821
|
+
isDragging && 'opacity-50',
|
|
822
|
+
)}
|
|
521
823
|
style={cellStyle}
|
|
824
|
+
{...dragAttrs}
|
|
522
825
|
>
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
: labels.ariaSortNone
|
|
533
|
-
}
|
|
534
|
-
className={cn(
|
|
535
|
-
buttonClassName,
|
|
536
|
-
align === 'right' && 'flex-row-reverse',
|
|
537
|
-
'disabled:cursor-not-allowed disabled:opacity-50',
|
|
538
|
-
)}
|
|
539
|
-
>
|
|
540
|
-
<span>{headerContent}</span>
|
|
541
|
-
<SortIcon direction={sortEntry?.direction} />
|
|
542
|
-
</button>
|
|
826
|
+
{filterTrigger ? (
|
|
827
|
+
<div className={cn('flex items-center gap-1', wrapperJustify)}>
|
|
828
|
+
{sortButton}
|
|
829
|
+
{filterTrigger}
|
|
830
|
+
</div>
|
|
831
|
+
) : (
|
|
832
|
+
sortButton
|
|
833
|
+
)}
|
|
834
|
+
{resizeHandle}
|
|
543
835
|
</th>
|
|
544
836
|
);
|
|
545
837
|
}
|
|
546
838
|
|
|
547
839
|
return (
|
|
548
|
-
<th
|
|
549
|
-
|
|
840
|
+
<th
|
|
841
|
+
scope="col"
|
|
842
|
+
className={cn(
|
|
843
|
+
className,
|
|
844
|
+
alignClass,
|
|
845
|
+
dropIndicatorClass,
|
|
846
|
+
isDragging && 'opacity-50',
|
|
847
|
+
)}
|
|
848
|
+
style={cellStyle}
|
|
849
|
+
{...dragAttrs}
|
|
850
|
+
>
|
|
851
|
+
{filterTrigger ? (
|
|
852
|
+
<div className={cn('flex items-center gap-1', wrapperJustify)}>
|
|
853
|
+
<span>{headerContent}</span>
|
|
854
|
+
{filterTrigger}
|
|
855
|
+
</div>
|
|
856
|
+
) : (
|
|
857
|
+
headerContent
|
|
858
|
+
)}
|
|
859
|
+
{resizeHandle}
|
|
550
860
|
</th>
|
|
551
861
|
);
|
|
552
862
|
}
|
|
553
863
|
|
|
864
|
+
/**
|
|
865
|
+
* Resolve the effective width (px) for a column. User-driven resize
|
|
866
|
+
* (`columnWidths[field]`) wins over the static `col.width`. Returns
|
|
867
|
+
* `undefined` if neither source has a width set — the consumer falls
|
|
868
|
+
* back to the column's `flex` value or browser auto-layout.
|
|
869
|
+
*/
|
|
870
|
+
function resolveColumnWidth<T extends object>(
|
|
871
|
+
col: TableColumn<T>,
|
|
872
|
+
columnWidths: Record<string, number>,
|
|
873
|
+
): number | undefined {
|
|
874
|
+
const resized = columnWidths[col.field as string];
|
|
875
|
+
if (resized != null) return resized;
|
|
876
|
+
return col.width;
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
/**
|
|
880
|
+
* Resolve the filter UI type for a column. Explicit `col.filterType`
|
|
881
|
+
* wins; otherwise we map the auto-detected primitive type:
|
|
882
|
+
* - `number` → number range
|
|
883
|
+
* - `boolean` → true/false/all select
|
|
884
|
+
* - `date` → date range
|
|
885
|
+
* - anything else (string / unknown) → text contains
|
|
886
|
+
*/
|
|
887
|
+
function resolveFilterType<T extends object>(
|
|
888
|
+
col: TableColumn<T>,
|
|
889
|
+
inferred: TableColumnInferredType | undefined,
|
|
890
|
+
): TableFilterType {
|
|
891
|
+
if (col.filterType) return col.filterType;
|
|
892
|
+
switch (inferred) {
|
|
893
|
+
case 'number':
|
|
894
|
+
return 'number';
|
|
895
|
+
case 'boolean':
|
|
896
|
+
return 'boolean';
|
|
897
|
+
case 'date':
|
|
898
|
+
return 'date';
|
|
899
|
+
default:
|
|
900
|
+
return 'text';
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
|
|
554
904
|
// ───── Virtualized body ─────
|
|
555
905
|
|
|
556
906
|
function renderVirtualizedBody<T extends object>(args: {
|
|
@@ -559,6 +909,7 @@ function renderVirtualizedBody<T extends object>(args: {
|
|
|
559
909
|
renderedRows: T[];
|
|
560
910
|
visibleCols: TableColumn<T>[];
|
|
561
911
|
columnTypes: Map<string, string>;
|
|
912
|
+
columnWidths: Record<string, number>;
|
|
562
913
|
selection: ReturnType<typeof useTableSelection>;
|
|
563
914
|
rowSelection: 'none' | 'single' | 'multiple';
|
|
564
915
|
showSelectionColumn: boolean;
|
|
@@ -573,6 +924,7 @@ function renderVirtualizedBody<T extends object>(args: {
|
|
|
573
924
|
cell: string;
|
|
574
925
|
selectionCell: string;
|
|
575
926
|
stickyLeftCell: string;
|
|
927
|
+
stickyRightCell: string;
|
|
576
928
|
rowActionsCell: string;
|
|
577
929
|
};
|
|
578
930
|
}): ReactNode {
|
|
@@ -581,6 +933,7 @@ function renderVirtualizedBody<T extends object>(args: {
|
|
|
581
933
|
renderedRows,
|
|
582
934
|
visibleCols,
|
|
583
935
|
columnTypes,
|
|
936
|
+
columnWidths,
|
|
584
937
|
selection,
|
|
585
938
|
rowSelection,
|
|
586
939
|
showSelectionColumn,
|
|
@@ -628,8 +981,10 @@ function renderVirtualizedBody<T extends object>(args: {
|
|
|
628
981
|
rowIndex={absoluteIdx}
|
|
629
982
|
isSelected={isSelected}
|
|
630
983
|
inferredType={columnTypes.get(col.field as string)}
|
|
984
|
+
effectiveWidth={resolveColumnWidth(col, columnWidths)}
|
|
631
985
|
cellClass={classes.cell}
|
|
632
986
|
stickyLeftClass={classes.stickyLeftCell}
|
|
987
|
+
stickyRightClass={classes.stickyRightCell}
|
|
633
988
|
/>
|
|
634
989
|
))}
|
|
635
990
|
{showRowActionsColumn && rowActions && (
|
|
@@ -653,10 +1008,22 @@ function DataCell<T extends object>(props: {
|
|
|
653
1008
|
rowIndex: number;
|
|
654
1009
|
isSelected: boolean;
|
|
655
1010
|
inferredType: string | undefined;
|
|
1011
|
+
effectiveWidth: number | undefined;
|
|
656
1012
|
cellClass: string;
|
|
657
1013
|
stickyLeftClass: string;
|
|
1014
|
+
stickyRightClass: string;
|
|
658
1015
|
}) {
|
|
659
|
-
const {
|
|
1016
|
+
const {
|
|
1017
|
+
col,
|
|
1018
|
+
row,
|
|
1019
|
+
rowIndex,
|
|
1020
|
+
isSelected,
|
|
1021
|
+
inferredType,
|
|
1022
|
+
effectiveWidth,
|
|
1023
|
+
cellClass,
|
|
1024
|
+
stickyLeftClass,
|
|
1025
|
+
stickyRightClass,
|
|
1026
|
+
} = props;
|
|
660
1027
|
const value = getNestedValue(row, col.field as string);
|
|
661
1028
|
const align = resolveAlign(col, inferredType as never);
|
|
662
1029
|
const tabularNums = resolveTabularNums(col, inferredType as never);
|
|
@@ -691,9 +1058,10 @@ function DataCell<T extends object>(props: {
|
|
|
691
1058
|
tabularNums && 'tabular-nums',
|
|
692
1059
|
monospace && 'font-mono',
|
|
693
1060
|
col.sticky === 'left' && stickyLeftClass,
|
|
1061
|
+
col.sticky === 'right' && stickyRightClass,
|
|
694
1062
|
)}
|
|
695
1063
|
style={{
|
|
696
|
-
width:
|
|
1064
|
+
width: effectiveWidth != null ? `${effectiveWidth}px` : undefined,
|
|
697
1065
|
minWidth: col.minWidth != null ? `${col.minWidth}px` : undefined,
|
|
698
1066
|
}}
|
|
699
1067
|
>
|