@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
package/dist/index.esm.js
CHANGED
|
@@ -8259,28 +8259,99 @@ const DEFAULT_PAGE_SIZE_OPTIONS = [
|
|
|
8259
8259
|
}
|
|
8260
8260
|
|
|
8261
8261
|
/**
|
|
8262
|
-
*
|
|
8263
|
-
*
|
|
8264
|
-
|
|
8265
|
-
|
|
8266
|
-
|
|
8267
|
-
|
|
8268
|
-
|
|
8269
|
-
|
|
8262
|
+
* Coerce a value to a number for range comparison. Returns
|
|
8263
|
+
* `null` if not coercible.
|
|
8264
|
+
*/ function toNumber(value) {
|
|
8265
|
+
if (value == null) return null;
|
|
8266
|
+
if (typeof value === 'number') return Number.isFinite(value) ? value : null;
|
|
8267
|
+
if (typeof value === 'bigint') return Number(value);
|
|
8268
|
+
if (typeof value === 'string') {
|
|
8269
|
+
const n = Number(value);
|
|
8270
|
+
return Number.isFinite(n) ? n : null;
|
|
8271
|
+
}
|
|
8272
|
+
return null;
|
|
8273
|
+
}
|
|
8274
|
+
/**
|
|
8275
|
+
* Coerce a value to a Date's epoch ms for date-range comparison.
|
|
8276
|
+
* Accepts `Date` instances + ISO 8601 strings. Returns `null` if
|
|
8277
|
+
* not coercible.
|
|
8278
|
+
*/ function toDateMs(value) {
|
|
8279
|
+
if (value == null) return null;
|
|
8280
|
+
if (value instanceof Date) return value.getTime();
|
|
8281
|
+
if (typeof value === 'string') {
|
|
8282
|
+
const d = new Date(value);
|
|
8283
|
+
const t = d.getTime();
|
|
8284
|
+
return Number.isNaN(t) ? null : t;
|
|
8285
|
+
}
|
|
8286
|
+
return null;
|
|
8287
|
+
}
|
|
8288
|
+
/**
|
|
8289
|
+
* Test one row × one filter. Returns `true` if the row passes the
|
|
8290
|
+
* filter. The operator decides the predicate:
|
|
8291
|
+
*
|
|
8292
|
+
* - `contains` → substring match on stringified value (text filter)
|
|
8293
|
+
* - `equals` → strict equality (boolean filter mostly)
|
|
8294
|
+
* - `between` → `value: [min, max]` tuple. Each end can be `null`
|
|
8295
|
+
* for an open range. Used by number range + date range filters.
|
|
8296
|
+
*
|
|
8297
|
+
* Empty filter is a no-op (passes every row).
|
|
8298
|
+
*/ function passesFilter(rowValue, op, filterValue) {
|
|
8299
|
+
switch(op){
|
|
8300
|
+
case 'contains':
|
|
8301
|
+
{
|
|
8302
|
+
const needle = typeof filterValue === 'string' ? filterValue.trim().toLowerCase() : '';
|
|
8303
|
+
if (!needle) return true;
|
|
8304
|
+
return stringifyForSearch(rowValue).toLowerCase().includes(needle);
|
|
8305
|
+
}
|
|
8306
|
+
case 'equals':
|
|
8307
|
+
{
|
|
8308
|
+
if (filterValue == null) return true;
|
|
8309
|
+
return rowValue === filterValue;
|
|
8310
|
+
}
|
|
8311
|
+
case 'between':
|
|
8312
|
+
{
|
|
8313
|
+
if (!Array.isArray(filterValue) || filterValue.length !== 2) return true;
|
|
8314
|
+
const [min, max] = filterValue;
|
|
8315
|
+
if (min == null && max == null) return true;
|
|
8316
|
+
// Try number range first.
|
|
8317
|
+
const rowNum = toNumber(rowValue);
|
|
8318
|
+
const minNum = min == null ? null : toNumber(min);
|
|
8319
|
+
const maxNum = max == null ? null : toNumber(max);
|
|
8320
|
+
if (rowNum != null && (minNum != null || maxNum != null)) {
|
|
8321
|
+
if (minNum != null && rowNum < minNum) return false;
|
|
8322
|
+
if (maxNum != null && rowNum > maxNum) return false;
|
|
8323
|
+
return true;
|
|
8324
|
+
}
|
|
8325
|
+
// Fall through to date range.
|
|
8326
|
+
const rowMs = toDateMs(rowValue);
|
|
8327
|
+
const minMs = min == null ? null : toDateMs(min);
|
|
8328
|
+
const maxMs = max == null ? null : toDateMs(max);
|
|
8329
|
+
if (rowMs != null && (minMs != null || maxMs != null)) {
|
|
8330
|
+
if (minMs != null && rowMs < minMs) return false;
|
|
8331
|
+
if (maxMs != null && rowMs > maxMs) return false;
|
|
8332
|
+
return true;
|
|
8333
|
+
}
|
|
8334
|
+
// If neither coercion worked, let the row pass (defensive).
|
|
8335
|
+
return true;
|
|
8336
|
+
}
|
|
8337
|
+
default:
|
|
8338
|
+
return true;
|
|
8339
|
+
}
|
|
8340
|
+
}
|
|
8341
|
+
/**
|
|
8342
|
+
* Filters `rows` by the per-column filter model. AND semantics
|
|
8343
|
+
* across columns — a row must pass EVERY active filter.
|
|
8344
|
+
*
|
|
8345
|
+
* Each filter item specifies an `op` (`'contains'` / `'equals'` /
|
|
8346
|
+
* `'between'`) and a `value` whose shape matches the operator
|
|
8347
|
+
* (string for contains; primitive for equals; `[min, max]` tuple
|
|
8348
|
+
* for between).
|
|
8270
8349
|
*/ function useTableFilter(rows, filterModel) {
|
|
8271
8350
|
return useMemo(()=>{
|
|
8272
8351
|
if (!filterModel || filterModel.length === 0) return rows;
|
|
8273
8352
|
return rows.filter((row)=>filterModel.every((filter)=>{
|
|
8274
8353
|
const value = getNestedValue(row, filter.field);
|
|
8275
|
-
|
|
8276
|
-
const needle = filter.value.trim().toLowerCase();
|
|
8277
|
-
if (!needle) return true; // empty filter == no-op
|
|
8278
|
-
switch(filter.op){
|
|
8279
|
-
case 'contains':
|
|
8280
|
-
return stringValue.includes(needle);
|
|
8281
|
-
default:
|
|
8282
|
-
return true;
|
|
8283
|
-
}
|
|
8354
|
+
return passesFilter(value, filter.op, filter.value);
|
|
8284
8355
|
}));
|
|
8285
8356
|
}, [
|
|
8286
8357
|
rows,
|
|
@@ -8592,10 +8663,23 @@ const DEFAULT_LABELS$1 = {
|
|
|
8592
8663
|
filterColumn: 'Filter',
|
|
8593
8664
|
filterApply: 'Apply',
|
|
8594
8665
|
filterClear: 'Clear',
|
|
8666
|
+
filterMin: 'Min',
|
|
8667
|
+
filterMax: 'Max',
|
|
8668
|
+
filterFrom: 'From',
|
|
8669
|
+
filterTo: 'To',
|
|
8670
|
+
filterAll: 'All',
|
|
8671
|
+
filterTrue: 'True',
|
|
8672
|
+
filterFalse: 'False',
|
|
8595
8673
|
density: 'Density',
|
|
8596
8674
|
densityCompact: 'Compact',
|
|
8597
8675
|
densityComfortable: 'Comfortable',
|
|
8598
|
-
densitySpacious: 'Spacious'
|
|
8676
|
+
densitySpacious: 'Spacious',
|
|
8677
|
+
columnsButton: 'Columns',
|
|
8678
|
+
columnsTitle: 'Manage columns',
|
|
8679
|
+
columnsDescription: 'Show or hide columns. Required columns cannot be toggled.',
|
|
8680
|
+
columnsShowAll: 'Show all',
|
|
8681
|
+
columnsHideAll: 'Hide all',
|
|
8682
|
+
columnsDone: 'Done'
|
|
8599
8683
|
};
|
|
8600
8684
|
const DEFAULT_GET_ROW_ID = (_row, index)=>String(index);
|
|
8601
8685
|
/**
|
|
@@ -9536,7 +9620,7 @@ function RowActionMenuItem(props) {
|
|
|
9536
9620
|
'border-t border-neutral-200',
|
|
9537
9621
|
'bg-neutral-50 shadow-md'
|
|
9538
9622
|
],
|
|
9539
|
-
// Sticky
|
|
9623
|
+
// Sticky LEFT column — uses `position: sticky; left: 0` + z-index
|
|
9540
9624
|
// so the column stays pinned during horizontal scroll. The
|
|
9541
9625
|
// `bg-neutral-50` ensures the column doesn't bleed when content
|
|
9542
9626
|
// scrolls underneath.
|
|
@@ -9544,6 +9628,12 @@ function RowActionMenuItem(props) {
|
|
|
9544
9628
|
// Sticky left × header intersection (top-left corner): higher
|
|
9545
9629
|
// z-index than both header (`z-10`) and sticky col (`z-[1]`).
|
|
9546
9630
|
stickyLeftHeaderCell: 'sticky left-0 z-20 bg-neutral-50',
|
|
9631
|
+
// Sticky RIGHT column — mirror of stickyLeftCell. The first
|
|
9632
|
+
// right-sticky column gets a `border-l` to separate it visually
|
|
9633
|
+
// from the scrollable area (the consumer can override via
|
|
9634
|
+
// slotProps if they don't want it).
|
|
9635
|
+
stickyRightCell: 'sticky right-0 z-[1] bg-neutral-50 border-l border-neutral-200',
|
|
9636
|
+
stickyRightHeaderCell: 'sticky right-0 z-20 bg-neutral-50 border-l border-neutral-200',
|
|
9547
9637
|
paginationFooter: 'flex justify-end mt-3'
|
|
9548
9638
|
},
|
|
9549
9639
|
variants: {
|
|
@@ -9748,6 +9838,690 @@ function RowActionMenuItem(props) {
|
|
|
9748
9838
|
};
|
|
9749
9839
|
}
|
|
9750
9840
|
|
|
9841
|
+
/**
|
|
9842
|
+
* Per-column filter UI for DataGrid.
|
|
9843
|
+
*
|
|
9844
|
+
* `<ColumnFilterTrigger>` renders the filter icon button in the
|
|
9845
|
+
* column header — when clicked, opens a Popover with the
|
|
9846
|
+
* appropriate filter UI for the column type:
|
|
9847
|
+
*
|
|
9848
|
+
* - `text` → `<TextFilter>` with single input (op: `contains`)
|
|
9849
|
+
* - `number` → `<NumberRangeFilter>` with min/max inputs (op: `between`)
|
|
9850
|
+
* - `boolean` → `<BooleanFilter>` with true/false/all radio (op: `equals`)
|
|
9851
|
+
* - `date` → `<DateRangeFilter>` with min/max date inputs (op: `between`)
|
|
9852
|
+
*
|
|
9853
|
+
* Each filter UI has Apply + Clear actions; Apply commits the
|
|
9854
|
+
* filter to the model, Clear removes any existing filter for that
|
|
9855
|
+
* column.
|
|
9856
|
+
*/ // ───── Helper: read/write filter model for a single column ─────
|
|
9857
|
+
function findFilter(model, field) {
|
|
9858
|
+
return model.find((f)=>f.field === field);
|
|
9859
|
+
}
|
|
9860
|
+
function setFilter(model, field, next) {
|
|
9861
|
+
const rest = model.filter((f)=>f.field !== field);
|
|
9862
|
+
if (next == null) return rest;
|
|
9863
|
+
return [
|
|
9864
|
+
...rest,
|
|
9865
|
+
next
|
|
9866
|
+
];
|
|
9867
|
+
}
|
|
9868
|
+
const DEFAULT_FILTER_LABELS = {
|
|
9869
|
+
filterColumn: 'Filter',
|
|
9870
|
+
filterApply: 'Apply',
|
|
9871
|
+
filterClear: 'Clear',
|
|
9872
|
+
filterMin: 'Min',
|
|
9873
|
+
filterMax: 'Max',
|
|
9874
|
+
filterFrom: 'From',
|
|
9875
|
+
filterTo: 'To',
|
|
9876
|
+
filterAll: 'All',
|
|
9877
|
+
filterTrue: 'True',
|
|
9878
|
+
filterFalse: 'False'
|
|
9879
|
+
};
|
|
9880
|
+
function ColumnFilterTrigger(props) {
|
|
9881
|
+
const { field, filterType, filterModel, onFilterChange, disabled, labels: labelsProp } = props;
|
|
9882
|
+
const labels = _extends({}, DEFAULT_FILTER_LABELS, labelsProp);
|
|
9883
|
+
const [open, setOpen] = useState(false);
|
|
9884
|
+
const active = findFilter(filterModel, field) != null;
|
|
9885
|
+
return jsx(Popover, {
|
|
9886
|
+
open: open,
|
|
9887
|
+
onOpenChange: setOpen,
|
|
9888
|
+
side: "bottom",
|
|
9889
|
+
align: "end",
|
|
9890
|
+
content: jsx(FilterPanel, {
|
|
9891
|
+
field: field,
|
|
9892
|
+
filterType: filterType,
|
|
9893
|
+
filterModel: filterModel,
|
|
9894
|
+
onFilterChange: (next)=>{
|
|
9895
|
+
onFilterChange(next);
|
|
9896
|
+
setOpen(false);
|
|
9897
|
+
},
|
|
9898
|
+
labels: labels
|
|
9899
|
+
}),
|
|
9900
|
+
children: jsx("button", {
|
|
9901
|
+
type: "button",
|
|
9902
|
+
"aria-label": labels.filterColumn,
|
|
9903
|
+
"aria-pressed": active,
|
|
9904
|
+
disabled: disabled,
|
|
9905
|
+
className: cn('inline-flex items-center justify-center h-6 w-6 rounded', 'text-neutral-500 hover:text-neutral-900', active && 'text-primary-700', 'focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-500', 'transition-colors motion-reduce:transition-none', 'disabled:opacity-50 disabled:cursor-not-allowed'),
|
|
9906
|
+
children: jsx(FilterIcon, {})
|
|
9907
|
+
})
|
|
9908
|
+
});
|
|
9909
|
+
}
|
|
9910
|
+
// ───── Panel — dispatches to the right filter UI ─────
|
|
9911
|
+
function FilterPanel(props) {
|
|
9912
|
+
const { field, filterType, filterModel, onFilterChange, labels } = props;
|
|
9913
|
+
const current = findFilter(filterModel, field);
|
|
9914
|
+
const commit = (next)=>{
|
|
9915
|
+
onFilterChange(setFilter(filterModel, field, next));
|
|
9916
|
+
};
|
|
9917
|
+
switch(filterType){
|
|
9918
|
+
case 'number':
|
|
9919
|
+
return jsx(NumberRangeFilter, {
|
|
9920
|
+
field: field,
|
|
9921
|
+
current: current,
|
|
9922
|
+
onCommit: commit,
|
|
9923
|
+
labels: labels
|
|
9924
|
+
});
|
|
9925
|
+
case 'boolean':
|
|
9926
|
+
return jsx(BooleanFilter, {
|
|
9927
|
+
field: field,
|
|
9928
|
+
current: current,
|
|
9929
|
+
onCommit: commit,
|
|
9930
|
+
labels: labels
|
|
9931
|
+
});
|
|
9932
|
+
case 'date':
|
|
9933
|
+
return jsx(DateRangeFilter, {
|
|
9934
|
+
field: field,
|
|
9935
|
+
current: current,
|
|
9936
|
+
onCommit: commit,
|
|
9937
|
+
labels: labels
|
|
9938
|
+
});
|
|
9939
|
+
case 'text':
|
|
9940
|
+
default:
|
|
9941
|
+
return jsx(TextFilter, {
|
|
9942
|
+
field: field,
|
|
9943
|
+
current: current,
|
|
9944
|
+
onCommit: commit,
|
|
9945
|
+
labels: labels
|
|
9946
|
+
});
|
|
9947
|
+
}
|
|
9948
|
+
}
|
|
9949
|
+
// ───── Text filter ─────
|
|
9950
|
+
function TextFilter(props) {
|
|
9951
|
+
const { field, current, onCommit, labels } = props;
|
|
9952
|
+
const [value, setValue] = useState(typeof (current == null ? void 0 : current.value) === 'string' ? current.value : '');
|
|
9953
|
+
return jsxs("div", {
|
|
9954
|
+
className: "flex flex-col gap-2 min-w-[200px]",
|
|
9955
|
+
children: [
|
|
9956
|
+
jsx("input", {
|
|
9957
|
+
type: "text",
|
|
9958
|
+
value: value,
|
|
9959
|
+
onChange: (e)=>setValue(e.target.value),
|
|
9960
|
+
placeholder: "\u2026",
|
|
9961
|
+
autoFocus: true,
|
|
9962
|
+
className: "rounded-md border border-neutral-300 bg-neutral-50 px-2 py-1.5 text-sm focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-500"
|
|
9963
|
+
}),
|
|
9964
|
+
jsx(FilterActions, {
|
|
9965
|
+
onApply: ()=>onCommit(value.trim() ? {
|
|
9966
|
+
field,
|
|
9967
|
+
op: 'contains',
|
|
9968
|
+
value
|
|
9969
|
+
} : null),
|
|
9970
|
+
onClear: ()=>{
|
|
9971
|
+
setValue('');
|
|
9972
|
+
onCommit(null);
|
|
9973
|
+
},
|
|
9974
|
+
labels: labels
|
|
9975
|
+
})
|
|
9976
|
+
]
|
|
9977
|
+
});
|
|
9978
|
+
}
|
|
9979
|
+
// ───── Number range filter ─────
|
|
9980
|
+
function NumberRangeFilter(props) {
|
|
9981
|
+
const { field, current, onCommit, labels } = props;
|
|
9982
|
+
const currentValue = current == null ? void 0 : current.value;
|
|
9983
|
+
const currentTuple = Array.isArray(currentValue) ? currentValue : [
|
|
9984
|
+
null,
|
|
9985
|
+
null
|
|
9986
|
+
];
|
|
9987
|
+
const [min, setMin] = useState(currentTuple[0] != null ? String(currentTuple[0]) : '');
|
|
9988
|
+
const [max, setMax] = useState(currentTuple[1] != null ? String(currentTuple[1]) : '');
|
|
9989
|
+
const apply = ()=>{
|
|
9990
|
+
const minN = min.trim() ? Number(min) : null;
|
|
9991
|
+
const maxN = max.trim() ? Number(max) : null;
|
|
9992
|
+
if ((minN == null || Number.isFinite(minN)) && (maxN == null || Number.isFinite(maxN))) {
|
|
9993
|
+
if (minN == null && maxN == null) {
|
|
9994
|
+
onCommit(null);
|
|
9995
|
+
} else {
|
|
9996
|
+
onCommit({
|
|
9997
|
+
field,
|
|
9998
|
+
op: 'between',
|
|
9999
|
+
value: [
|
|
10000
|
+
minN,
|
|
10001
|
+
maxN
|
|
10002
|
+
]
|
|
10003
|
+
});
|
|
10004
|
+
}
|
|
10005
|
+
}
|
|
10006
|
+
};
|
|
10007
|
+
return jsxs("div", {
|
|
10008
|
+
className: "flex flex-col gap-2 min-w-[220px]",
|
|
10009
|
+
children: [
|
|
10010
|
+
jsxs("div", {
|
|
10011
|
+
className: "grid grid-cols-2 gap-2",
|
|
10012
|
+
children: [
|
|
10013
|
+
jsxs("label", {
|
|
10014
|
+
className: "flex flex-col gap-1 text-xs text-neutral-700",
|
|
10015
|
+
children: [
|
|
10016
|
+
jsx("span", {
|
|
10017
|
+
children: labels.filterMin
|
|
10018
|
+
}),
|
|
10019
|
+
jsx("input", {
|
|
10020
|
+
type: "number",
|
|
10021
|
+
value: min,
|
|
10022
|
+
onChange: (e)=>setMin(e.target.value),
|
|
10023
|
+
autoFocus: true,
|
|
10024
|
+
className: "rounded-md border border-neutral-300 bg-neutral-50 px-2 py-1.5 text-sm focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-500"
|
|
10025
|
+
})
|
|
10026
|
+
]
|
|
10027
|
+
}),
|
|
10028
|
+
jsxs("label", {
|
|
10029
|
+
className: "flex flex-col gap-1 text-xs text-neutral-700",
|
|
10030
|
+
children: [
|
|
10031
|
+
jsx("span", {
|
|
10032
|
+
children: labels.filterMax
|
|
10033
|
+
}),
|
|
10034
|
+
jsx("input", {
|
|
10035
|
+
type: "number",
|
|
10036
|
+
value: max,
|
|
10037
|
+
onChange: (e)=>setMax(e.target.value),
|
|
10038
|
+
className: "rounded-md border border-neutral-300 bg-neutral-50 px-2 py-1.5 text-sm focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-500"
|
|
10039
|
+
})
|
|
10040
|
+
]
|
|
10041
|
+
})
|
|
10042
|
+
]
|
|
10043
|
+
}),
|
|
10044
|
+
jsx(FilterActions, {
|
|
10045
|
+
onApply: apply,
|
|
10046
|
+
onClear: ()=>{
|
|
10047
|
+
setMin('');
|
|
10048
|
+
setMax('');
|
|
10049
|
+
onCommit(null);
|
|
10050
|
+
},
|
|
10051
|
+
labels: labels
|
|
10052
|
+
})
|
|
10053
|
+
]
|
|
10054
|
+
});
|
|
10055
|
+
}
|
|
10056
|
+
// ───── Boolean filter ─────
|
|
10057
|
+
function BooleanFilter(props) {
|
|
10058
|
+
const { field, current, onCommit, labels } = props;
|
|
10059
|
+
const initial = (current == null ? void 0 : current.value) === true ? 'true' : (current == null ? void 0 : current.value) === false ? 'false' : 'all';
|
|
10060
|
+
const [value, setValue] = useState(initial);
|
|
10061
|
+
const apply = ()=>{
|
|
10062
|
+
if (value === 'all') onCommit(null);
|
|
10063
|
+
else onCommit({
|
|
10064
|
+
field,
|
|
10065
|
+
op: 'equals',
|
|
10066
|
+
value: value === 'true'
|
|
10067
|
+
});
|
|
10068
|
+
};
|
|
10069
|
+
return jsxs("div", {
|
|
10070
|
+
className: "flex flex-col gap-2 min-w-[160px]",
|
|
10071
|
+
children: [
|
|
10072
|
+
jsx("fieldset", {
|
|
10073
|
+
className: "flex flex-col gap-1",
|
|
10074
|
+
children: [
|
|
10075
|
+
'all',
|
|
10076
|
+
'true',
|
|
10077
|
+
'false'
|
|
10078
|
+
].map((opt)=>jsxs("label", {
|
|
10079
|
+
className: "flex items-center gap-2 text-sm cursor-pointer",
|
|
10080
|
+
children: [
|
|
10081
|
+
jsx("input", {
|
|
10082
|
+
type: "radio",
|
|
10083
|
+
name: `filter-${field}`,
|
|
10084
|
+
value: opt,
|
|
10085
|
+
checked: value === opt,
|
|
10086
|
+
onChange: ()=>setValue(opt),
|
|
10087
|
+
className: "cursor-pointer"
|
|
10088
|
+
}),
|
|
10089
|
+
jsx("span", {
|
|
10090
|
+
children: opt === 'all' ? labels.filterAll : opt === 'true' ? labels.filterTrue : labels.filterFalse
|
|
10091
|
+
})
|
|
10092
|
+
]
|
|
10093
|
+
}, opt))
|
|
10094
|
+
}),
|
|
10095
|
+
jsx(FilterActions, {
|
|
10096
|
+
onApply: apply,
|
|
10097
|
+
onClear: ()=>{
|
|
10098
|
+
setValue('all');
|
|
10099
|
+
onCommit(null);
|
|
10100
|
+
},
|
|
10101
|
+
labels: labels
|
|
10102
|
+
})
|
|
10103
|
+
]
|
|
10104
|
+
});
|
|
10105
|
+
}
|
|
10106
|
+
// ───── Date range filter ─────
|
|
10107
|
+
function DateRangeFilter(props) {
|
|
10108
|
+
const { field, current, onCommit, labels } = props;
|
|
10109
|
+
const currentValue = current == null ? void 0 : current.value;
|
|
10110
|
+
const currentTuple = Array.isArray(currentValue) ? currentValue : [
|
|
10111
|
+
null,
|
|
10112
|
+
null
|
|
10113
|
+
];
|
|
10114
|
+
const [from, setFrom] = useState(typeof currentTuple[0] === 'string' ? currentTuple[0] : '');
|
|
10115
|
+
const [to, setTo] = useState(typeof currentTuple[1] === 'string' ? currentTuple[1] : '');
|
|
10116
|
+
const apply = ()=>{
|
|
10117
|
+
if (!from && !to) {
|
|
10118
|
+
onCommit(null);
|
|
10119
|
+
} else {
|
|
10120
|
+
onCommit({
|
|
10121
|
+
field,
|
|
10122
|
+
op: 'between',
|
|
10123
|
+
value: [
|
|
10124
|
+
from || null,
|
|
10125
|
+
to || null
|
|
10126
|
+
]
|
|
10127
|
+
});
|
|
10128
|
+
}
|
|
10129
|
+
};
|
|
10130
|
+
return jsxs("div", {
|
|
10131
|
+
className: "flex flex-col gap-2 min-w-[220px]",
|
|
10132
|
+
children: [
|
|
10133
|
+
jsxs("div", {
|
|
10134
|
+
className: "grid grid-cols-2 gap-2",
|
|
10135
|
+
children: [
|
|
10136
|
+
jsxs("label", {
|
|
10137
|
+
className: "flex flex-col gap-1 text-xs text-neutral-700",
|
|
10138
|
+
children: [
|
|
10139
|
+
jsx("span", {
|
|
10140
|
+
children: labels.filterFrom
|
|
10141
|
+
}),
|
|
10142
|
+
jsx("input", {
|
|
10143
|
+
type: "date",
|
|
10144
|
+
value: from,
|
|
10145
|
+
onChange: (e)=>setFrom(e.target.value),
|
|
10146
|
+
autoFocus: true,
|
|
10147
|
+
className: "rounded-md border border-neutral-300 bg-neutral-50 px-2 py-1.5 text-sm focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-500"
|
|
10148
|
+
})
|
|
10149
|
+
]
|
|
10150
|
+
}),
|
|
10151
|
+
jsxs("label", {
|
|
10152
|
+
className: "flex flex-col gap-1 text-xs text-neutral-700",
|
|
10153
|
+
children: [
|
|
10154
|
+
jsx("span", {
|
|
10155
|
+
children: labels.filterTo
|
|
10156
|
+
}),
|
|
10157
|
+
jsx("input", {
|
|
10158
|
+
type: "date",
|
|
10159
|
+
value: to,
|
|
10160
|
+
onChange: (e)=>setTo(e.target.value),
|
|
10161
|
+
className: "rounded-md border border-neutral-300 bg-neutral-50 px-2 py-1.5 text-sm focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-500"
|
|
10162
|
+
})
|
|
10163
|
+
]
|
|
10164
|
+
})
|
|
10165
|
+
]
|
|
10166
|
+
}),
|
|
10167
|
+
jsx(FilterActions, {
|
|
10168
|
+
onApply: apply,
|
|
10169
|
+
onClear: ()=>{
|
|
10170
|
+
setFrom('');
|
|
10171
|
+
setTo('');
|
|
10172
|
+
onCommit(null);
|
|
10173
|
+
},
|
|
10174
|
+
labels: labels
|
|
10175
|
+
})
|
|
10176
|
+
]
|
|
10177
|
+
});
|
|
10178
|
+
}
|
|
10179
|
+
// ───── Apply + Clear actions ─────
|
|
10180
|
+
function FilterActions(props) {
|
|
10181
|
+
return jsxs("div", {
|
|
10182
|
+
className: "flex justify-end gap-2 pt-1 border-t border-neutral-200",
|
|
10183
|
+
children: [
|
|
10184
|
+
jsx("button", {
|
|
10185
|
+
type: "button",
|
|
10186
|
+
onClick: props.onClear,
|
|
10187
|
+
className: "text-xs text-neutral-600 hover:text-neutral-900 underline-offset-2 hover:underline focus:outline-none focus-visible:underline px-2 py-1",
|
|
10188
|
+
children: props.labels.filterClear
|
|
10189
|
+
}),
|
|
10190
|
+
jsx("button", {
|
|
10191
|
+
type: "button",
|
|
10192
|
+
onClick: props.onApply,
|
|
10193
|
+
className: "text-xs bg-primary-500 text-white rounded-md hover:bg-primary-600 focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-500 px-3 py-1",
|
|
10194
|
+
children: props.labels.filterApply
|
|
10195
|
+
})
|
|
10196
|
+
]
|
|
10197
|
+
});
|
|
10198
|
+
}
|
|
10199
|
+
// ───── Icon ─────
|
|
10200
|
+
function FilterIcon() {
|
|
10201
|
+
return jsx("svg", {
|
|
10202
|
+
width: "0.875em",
|
|
10203
|
+
height: "0.875em",
|
|
10204
|
+
viewBox: "0 0 16 16",
|
|
10205
|
+
fill: "none",
|
|
10206
|
+
stroke: "currentColor",
|
|
10207
|
+
strokeWidth: "1.5",
|
|
10208
|
+
strokeLinecap: "round",
|
|
10209
|
+
strokeLinejoin: "round",
|
|
10210
|
+
"aria-hidden": "true",
|
|
10211
|
+
children: jsx("path", {
|
|
10212
|
+
d: "M2 3h12M4 8h8M6 13h4"
|
|
10213
|
+
})
|
|
10214
|
+
});
|
|
10215
|
+
}
|
|
10216
|
+
|
|
10217
|
+
const DEFAULT_VISIBILITY_LABELS = {
|
|
10218
|
+
columnsButton: 'Columns',
|
|
10219
|
+
columnsTitle: 'Manage columns',
|
|
10220
|
+
columnsDescription: 'Show or hide columns. Required columns cannot be toggled.',
|
|
10221
|
+
columnsShowAll: 'Show all',
|
|
10222
|
+
columnsHideAll: 'Hide all',
|
|
10223
|
+
columnsDone: 'Done'
|
|
10224
|
+
};
|
|
10225
|
+
function ColumnVisibilityTrigger(props) {
|
|
10226
|
+
const { cols, hiddenColumns, onChange, disabled, labels: labelsProp } = props;
|
|
10227
|
+
const labels = _extends({}, DEFAULT_VISIBILITY_LABELS, labelsProp);
|
|
10228
|
+
const [open, setOpen] = useState(false);
|
|
10229
|
+
// Hideable cols are the only ones the user can toggle. `hideable`
|
|
10230
|
+
// defaults to `true` — a column is hideable unless explicitly opted
|
|
10231
|
+
// out with `hideable: false` (structurally required column).
|
|
10232
|
+
const hideableCols = cols.filter((c)=>c.hideable !== false);
|
|
10233
|
+
return jsxs(Fragment$1, {
|
|
10234
|
+
children: [
|
|
10235
|
+
jsxs("button", {
|
|
10236
|
+
type: "button",
|
|
10237
|
+
onClick: ()=>setOpen(true),
|
|
10238
|
+
disabled: disabled,
|
|
10239
|
+
className: cn('inline-flex items-center gap-1.5 px-2.5 py-1.5 rounded-md', 'text-sm text-neutral-700 hover:text-neutral-900', 'border border-neutral-300 bg-neutral-50 hover:bg-neutral-100', 'focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-500', 'transition-colors motion-reduce:transition-none', 'disabled:opacity-50 disabled:cursor-not-allowed'),
|
|
10240
|
+
children: [
|
|
10241
|
+
jsx(ColumnsIcon, {}),
|
|
10242
|
+
jsx("span", {
|
|
10243
|
+
children: labels.columnsButton
|
|
10244
|
+
})
|
|
10245
|
+
]
|
|
10246
|
+
}),
|
|
10247
|
+
jsx(Dialog, {
|
|
10248
|
+
open: open,
|
|
10249
|
+
onOpenChange: setOpen,
|
|
10250
|
+
title: labels.columnsTitle,
|
|
10251
|
+
description: labels.columnsDescription,
|
|
10252
|
+
size: "sm",
|
|
10253
|
+
children: jsx(ColumnVisibilityBody, {
|
|
10254
|
+
cols: hideableCols,
|
|
10255
|
+
hiddenColumns: hiddenColumns,
|
|
10256
|
+
onChange: onChange,
|
|
10257
|
+
onDone: ()=>setOpen(false),
|
|
10258
|
+
labels: labels
|
|
10259
|
+
})
|
|
10260
|
+
})
|
|
10261
|
+
]
|
|
10262
|
+
});
|
|
10263
|
+
}
|
|
10264
|
+
// ───── Dialog body ─────
|
|
10265
|
+
function ColumnVisibilityBody(props) {
|
|
10266
|
+
const { cols, hiddenColumns, onChange, onDone, labels } = props;
|
|
10267
|
+
// Local draft so the user can stage changes; commit on Done.
|
|
10268
|
+
const [draft, setDraft] = useState(hiddenColumns);
|
|
10269
|
+
// Re-sync when the dialog re-opens with a different external state.
|
|
10270
|
+
useEffect(()=>{
|
|
10271
|
+
setDraft(hiddenColumns);
|
|
10272
|
+
}, [
|
|
10273
|
+
hiddenColumns
|
|
10274
|
+
]);
|
|
10275
|
+
const toggle = (field)=>{
|
|
10276
|
+
setDraft((prev)=>prev.includes(field) ? prev.filter((f)=>f !== field) : [
|
|
10277
|
+
...prev,
|
|
10278
|
+
field
|
|
10279
|
+
]);
|
|
10280
|
+
};
|
|
10281
|
+
const showAll = ()=>setDraft([]);
|
|
10282
|
+
const hideAll = ()=>setDraft(cols.map((c)=>c.field));
|
|
10283
|
+
return jsxs("div", {
|
|
10284
|
+
className: "flex flex-col gap-3",
|
|
10285
|
+
children: [
|
|
10286
|
+
jsxs("div", {
|
|
10287
|
+
className: "flex items-center gap-2 pb-2 border-b border-neutral-200",
|
|
10288
|
+
children: [
|
|
10289
|
+
jsx("button", {
|
|
10290
|
+
type: "button",
|
|
10291
|
+
onClick: showAll,
|
|
10292
|
+
className: "text-xs text-neutral-600 hover:text-neutral-900 underline-offset-2 hover:underline focus:outline-none focus-visible:underline",
|
|
10293
|
+
children: labels.columnsShowAll
|
|
10294
|
+
}),
|
|
10295
|
+
jsx("span", {
|
|
10296
|
+
className: "text-neutral-300",
|
|
10297
|
+
children: "\u00B7"
|
|
10298
|
+
}),
|
|
10299
|
+
jsx("button", {
|
|
10300
|
+
type: "button",
|
|
10301
|
+
onClick: hideAll,
|
|
10302
|
+
className: "text-xs text-neutral-600 hover:text-neutral-900 underline-offset-2 hover:underline focus:outline-none focus-visible:underline",
|
|
10303
|
+
children: labels.columnsHideAll
|
|
10304
|
+
})
|
|
10305
|
+
]
|
|
10306
|
+
}),
|
|
10307
|
+
jsx("ul", {
|
|
10308
|
+
className: "flex flex-col gap-1.5 max-h-72 overflow-auto",
|
|
10309
|
+
children: cols.map((col)=>{
|
|
10310
|
+
const field = col.field;
|
|
10311
|
+
const hidden = draft.includes(field);
|
|
10312
|
+
const header = typeof col.header === 'function' ? col.header() : col.header;
|
|
10313
|
+
return jsx("li", {
|
|
10314
|
+
children: jsxs("label", {
|
|
10315
|
+
className: "flex items-center gap-2 cursor-pointer py-1 px-1 rounded hover:bg-neutral-100",
|
|
10316
|
+
children: [
|
|
10317
|
+
jsx("input", {
|
|
10318
|
+
type: "checkbox",
|
|
10319
|
+
checked: !hidden,
|
|
10320
|
+
onChange: ()=>toggle(field),
|
|
10321
|
+
className: "cursor-pointer"
|
|
10322
|
+
}),
|
|
10323
|
+
jsx("span", {
|
|
10324
|
+
className: "text-sm text-neutral-900",
|
|
10325
|
+
children: header
|
|
10326
|
+
})
|
|
10327
|
+
]
|
|
10328
|
+
})
|
|
10329
|
+
}, field);
|
|
10330
|
+
})
|
|
10331
|
+
}),
|
|
10332
|
+
jsx("div", {
|
|
10333
|
+
className: "flex justify-end gap-2 pt-2 border-t border-neutral-200",
|
|
10334
|
+
children: jsx("button", {
|
|
10335
|
+
type: "button",
|
|
10336
|
+
onClick: ()=>{
|
|
10337
|
+
onChange(draft);
|
|
10338
|
+
onDone();
|
|
10339
|
+
},
|
|
10340
|
+
className: "text-sm bg-primary-500 text-white rounded-md hover:bg-primary-600 focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-500 px-3 py-1.5",
|
|
10341
|
+
children: labels.columnsDone
|
|
10342
|
+
})
|
|
10343
|
+
})
|
|
10344
|
+
]
|
|
10345
|
+
});
|
|
10346
|
+
}
|
|
10347
|
+
// ───── Icon ─────
|
|
10348
|
+
function ColumnsIcon() {
|
|
10349
|
+
return jsxs("svg", {
|
|
10350
|
+
width: "0.875em",
|
|
10351
|
+
height: "0.875em",
|
|
10352
|
+
viewBox: "0 0 16 16",
|
|
10353
|
+
fill: "none",
|
|
10354
|
+
stroke: "currentColor",
|
|
10355
|
+
strokeWidth: "1.5",
|
|
10356
|
+
strokeLinecap: "round",
|
|
10357
|
+
strokeLinejoin: "round",
|
|
10358
|
+
"aria-hidden": "true",
|
|
10359
|
+
children: [
|
|
10360
|
+
jsx("rect", {
|
|
10361
|
+
x: "2",
|
|
10362
|
+
y: "3",
|
|
10363
|
+
width: "3.5",
|
|
10364
|
+
height: "10",
|
|
10365
|
+
rx: "0.5"
|
|
10366
|
+
}),
|
|
10367
|
+
jsx("rect", {
|
|
10368
|
+
x: "6.25",
|
|
10369
|
+
y: "3",
|
|
10370
|
+
width: "3.5",
|
|
10371
|
+
height: "10",
|
|
10372
|
+
rx: "0.5"
|
|
10373
|
+
}),
|
|
10374
|
+
jsx("rect", {
|
|
10375
|
+
x: "10.5",
|
|
10376
|
+
y: "3",
|
|
10377
|
+
width: "3.5",
|
|
10378
|
+
height: "10",
|
|
10379
|
+
rx: "0.5"
|
|
10380
|
+
})
|
|
10381
|
+
]
|
|
10382
|
+
});
|
|
10383
|
+
}
|
|
10384
|
+
|
|
10385
|
+
function useColumnResize(args) {
|
|
10386
|
+
const { widths, onChange, minWidth = 40, maxWidth = 1200 } = args;
|
|
10387
|
+
// We need the LATEST widths inside the pointermove closure. Stick
|
|
10388
|
+
// them in a ref so the closure captured at pointerdown stays valid
|
|
10389
|
+
// for the duration of the drag.
|
|
10390
|
+
const widthsRef = useRef(widths);
|
|
10391
|
+
widthsRef.current = widths;
|
|
10392
|
+
const startResize = useCallback((field, startWidth, clampMin, clampMax)=>{
|
|
10393
|
+
return (e)=>{
|
|
10394
|
+
// Only respond to the primary mouse button (or touch / pen).
|
|
10395
|
+
if (e.pointerType === 'mouse' && e.button !== 0) return;
|
|
10396
|
+
e.preventDefault();
|
|
10397
|
+
e.stopPropagation();
|
|
10398
|
+
const startX = e.clientX;
|
|
10399
|
+
const target = e.currentTarget;
|
|
10400
|
+
try {
|
|
10401
|
+
target.setPointerCapture(e.pointerId);
|
|
10402
|
+
} catch (unused) {
|
|
10403
|
+
// jsdom or older browsers — fine to fall back to global listeners.
|
|
10404
|
+
}
|
|
10405
|
+
const effectiveMin = Math.max(minWidth, clampMin);
|
|
10406
|
+
const effectiveMax = Math.min(maxWidth, clampMax);
|
|
10407
|
+
const handleMove = (ev)=>{
|
|
10408
|
+
const delta = ev.clientX - startX;
|
|
10409
|
+
const next = clamp(startWidth + delta, effectiveMin, effectiveMax);
|
|
10410
|
+
onChange(_extends({}, widthsRef.current, {
|
|
10411
|
+
[field]: next
|
|
10412
|
+
}));
|
|
10413
|
+
};
|
|
10414
|
+
const handleEnd = (ev)=>{
|
|
10415
|
+
try {
|
|
10416
|
+
target.releasePointerCapture(ev.pointerId);
|
|
10417
|
+
} catch (unused) {
|
|
10418
|
+
/* noop */ }
|
|
10419
|
+
target.removeEventListener('pointermove', handleMove);
|
|
10420
|
+
target.removeEventListener('pointerup', handleEnd);
|
|
10421
|
+
target.removeEventListener('pointercancel', handleEnd);
|
|
10422
|
+
};
|
|
10423
|
+
target.addEventListener('pointermove', handleMove);
|
|
10424
|
+
target.addEventListener('pointerup', handleEnd);
|
|
10425
|
+
target.addEventListener('pointercancel', handleEnd);
|
|
10426
|
+
};
|
|
10427
|
+
}, [
|
|
10428
|
+
onChange,
|
|
10429
|
+
minWidth,
|
|
10430
|
+
maxWidth
|
|
10431
|
+
]);
|
|
10432
|
+
return {
|
|
10433
|
+
startResize
|
|
10434
|
+
};
|
|
10435
|
+
}
|
|
10436
|
+
function clamp(n, min, max) {
|
|
10437
|
+
if (n < min) return min;
|
|
10438
|
+
if (n > max) return max;
|
|
10439
|
+
return n;
|
|
10440
|
+
}
|
|
10441
|
+
|
|
10442
|
+
function useColumnReorder(args) {
|
|
10443
|
+
const { order, onChange } = args;
|
|
10444
|
+
const sourceRef = useRef(null);
|
|
10445
|
+
const [dropTarget, setDropTarget] = useState(null);
|
|
10446
|
+
const [draggingField, setDraggingField] = useState(null);
|
|
10447
|
+
const dragStart = useCallback((field)=>(e)=>{
|
|
10448
|
+
sourceRef.current = field;
|
|
10449
|
+
setDraggingField(field);
|
|
10450
|
+
try {
|
|
10451
|
+
e.dataTransfer.effectAllowed = 'move';
|
|
10452
|
+
// Some browsers refuse to start a drag if no data is set.
|
|
10453
|
+
e.dataTransfer.setData('text/plain', field);
|
|
10454
|
+
} catch (unused) {
|
|
10455
|
+
/* noop — jsdom doesn't implement dataTransfer fully. */ }
|
|
10456
|
+
}, []);
|
|
10457
|
+
const dragOver = useCallback((field)=>(e)=>{
|
|
10458
|
+
if (sourceRef.current == null) return;
|
|
10459
|
+
if (field === sourceRef.current) return;
|
|
10460
|
+
e.preventDefault();
|
|
10461
|
+
e.dataTransfer.dropEffect = 'move';
|
|
10462
|
+
// Determine LEFT vs RIGHT half of the target.
|
|
10463
|
+
const rect = e.currentTarget.getBoundingClientRect();
|
|
10464
|
+
const midpoint = rect.left + rect.width / 2;
|
|
10465
|
+
const side = e.clientX < midpoint ? 'left' : 'right';
|
|
10466
|
+
setDropTarget((prev)=>prev && prev.field === field && prev.side === side ? prev : {
|
|
10467
|
+
field,
|
|
10468
|
+
side
|
|
10469
|
+
});
|
|
10470
|
+
}, []);
|
|
10471
|
+
const drop = useCallback((field)=>(e)=>{
|
|
10472
|
+
e.preventDefault();
|
|
10473
|
+
const source = sourceRef.current;
|
|
10474
|
+
sourceRef.current = null;
|
|
10475
|
+
setDraggingField(null);
|
|
10476
|
+
const target = dropTarget;
|
|
10477
|
+
setDropTarget(null);
|
|
10478
|
+
if (!source || source === field) return;
|
|
10479
|
+
const side = (target == null ? void 0 : target.field) === field ? target.side : 'right';
|
|
10480
|
+
const next = moveColumn(order, source, field, side);
|
|
10481
|
+
if (!arraysEqual(next, order)) onChange(next);
|
|
10482
|
+
}, [
|
|
10483
|
+
order,
|
|
10484
|
+
onChange,
|
|
10485
|
+
dropTarget
|
|
10486
|
+
]);
|
|
10487
|
+
const dragEnd = useCallback(()=>{
|
|
10488
|
+
sourceRef.current = null;
|
|
10489
|
+
setDraggingField(null);
|
|
10490
|
+
setDropTarget(null);
|
|
10491
|
+
}, []);
|
|
10492
|
+
return {
|
|
10493
|
+
dropTarget,
|
|
10494
|
+
draggingField,
|
|
10495
|
+
dragStart,
|
|
10496
|
+
dragOver,
|
|
10497
|
+
drop,
|
|
10498
|
+
dragEnd
|
|
10499
|
+
};
|
|
10500
|
+
}
|
|
10501
|
+
/**
|
|
10502
|
+
* Move `source` next to `target` in the `order` list.
|
|
10503
|
+
* `side='left'` inserts before; `side='right'` inserts after.
|
|
10504
|
+
* Exposed for unit testing.
|
|
10505
|
+
*/ function moveColumn(order, source, target, side) {
|
|
10506
|
+
const withoutSource = order.filter((f)=>f !== source);
|
|
10507
|
+
const targetIndex = withoutSource.indexOf(target);
|
|
10508
|
+
if (targetIndex === -1) return order;
|
|
10509
|
+
const insertAt = side === 'left' ? targetIndex : targetIndex + 1;
|
|
10510
|
+
return [
|
|
10511
|
+
...withoutSource.slice(0, insertAt),
|
|
10512
|
+
source,
|
|
10513
|
+
...withoutSource.slice(insertAt)
|
|
10514
|
+
];
|
|
10515
|
+
}
|
|
10516
|
+
function arraysEqual(a, b) {
|
|
10517
|
+
if (a === b) return true;
|
|
10518
|
+
if (a.length !== b.length) return false;
|
|
10519
|
+
for(let i = 0; i < a.length; i++){
|
|
10520
|
+
if (a[i] !== b[i]) return false;
|
|
10521
|
+
}
|
|
10522
|
+
return true;
|
|
10523
|
+
}
|
|
10524
|
+
|
|
9751
10525
|
const DEFAULT_LABELS = {
|
|
9752
10526
|
searchPlaceholder: 'Search...',
|
|
9753
10527
|
noData: 'No data',
|
|
@@ -9764,10 +10538,23 @@ const DEFAULT_LABELS = {
|
|
|
9764
10538
|
filterColumn: 'Filter',
|
|
9765
10539
|
filterApply: 'Apply',
|
|
9766
10540
|
filterClear: 'Clear',
|
|
10541
|
+
filterMin: 'Min',
|
|
10542
|
+
filterMax: 'Max',
|
|
10543
|
+
filterFrom: 'From',
|
|
10544
|
+
filterTo: 'To',
|
|
10545
|
+
filterAll: 'All',
|
|
10546
|
+
filterTrue: 'True',
|
|
10547
|
+
filterFalse: 'False',
|
|
9767
10548
|
density: 'Density',
|
|
9768
10549
|
densityCompact: 'Compact',
|
|
9769
10550
|
densityComfortable: 'Comfortable',
|
|
9770
|
-
densitySpacious: 'Spacious'
|
|
10551
|
+
densitySpacious: 'Spacious',
|
|
10552
|
+
columnsButton: 'Columns',
|
|
10553
|
+
columnsTitle: 'Manage columns',
|
|
10554
|
+
columnsDescription: 'Show or hide columns. Required columns cannot be toggled.',
|
|
10555
|
+
columnsShowAll: 'Show all',
|
|
10556
|
+
columnsHideAll: 'Hide all',
|
|
10557
|
+
columnsDone: 'Done'
|
|
9771
10558
|
};
|
|
9772
10559
|
/**
|
|
9773
10560
|
* Dashforge TW `<DataGrid>` — virtualized data table.
|
|
@@ -9796,7 +10583,7 @@ const DEFAULT_LABELS = {
|
|
|
9796
10583
|
* - `'visible'` — toggles only the current viewport window
|
|
9797
10584
|
*/ function DataGrid(props) {
|
|
9798
10585
|
var _slotProps_root, _slotProps_toolbar, _slotProps_search, _slotProps_scroll, _slotProps_table, _slotProps_thead, _slotProps_headerRow, _slotProps_headerCell, _slotProps_selectionCell, _slotProps_headerCell1, _slotProps_tbody, _slotProps_row, _slotProps_cell, _slotProps_emptyState, _slotProps_row1, _slotProps_cell1, _slotProps_cell2, _slotProps_selectionCell1, _slotProps_cell3, _slotProps_rowActionsCell, _slotProps_bulkActionFooter, _slotProps_pagination;
|
|
9799
|
-
const { rows, cols, getRowId, totalCount: totalCountProp, rowHeight, overscan = 5, height, sortModel: sortModelProp, onSortChange, defaultSortModel, serverSideSort = false, enableSearch = false, searchQuery: searchQueryProp, onSearchQueryChange, searchPlaceholder, searchDebounceMs = 200, serverSideSearch = false, filterModel: filterModelProp, onFilterChange, serverSideFilter = false, rowSelection = 'none', selectedRowIds: selectedRowIdsProp, onSelectionChange, bulkActions, selectAllScope = 'allLoaded', pagination, serverSidePagination = false, rowActions, loading = false, loadingRowCount = 8, emptyState, stickyHeader = true, caption, showCaption = false, access: gridAccess, labels: labelsProp, variant, size, density, sx, slotProps } = props;
|
|
10586
|
+
const { rows, cols, getRowId, totalCount: totalCountProp, rowHeight, overscan = 5, height, sortModel: sortModelProp, onSortChange, defaultSortModel, serverSideSort = false, enableSearch = false, searchQuery: searchQueryProp, onSearchQueryChange, searchPlaceholder, searchDebounceMs = 200, serverSideSearch = false, filterModel: filterModelProp, onFilterChange, serverSideFilter = false, hiddenColumns: hiddenColumnsProp, onHiddenColumnsChange, enableColumnVisibility = true, columnWidths: columnWidthsProp, onColumnWidthsChange, enableColumnResize = true, columnOrder: columnOrderProp, onColumnOrderChange, enableColumnReorder = true, rowSelection = 'none', selectedRowIds: selectedRowIdsProp, onSelectionChange, bulkActions, selectAllScope = 'allLoaded', pagination, serverSidePagination = false, rowActions, loading = false, loadingRowCount = 8, emptyState, stickyHeader = true, caption, showCaption = false, access: gridAccess, labels: labelsProp, variant, size, density, sx, slotProps } = props;
|
|
9800
10587
|
const labels = _extends({}, DEFAULT_LABELS, labelsProp);
|
|
9801
10588
|
const gridAccessState = useAccessState(gridAccess);
|
|
9802
10589
|
// ───── Controllable state ─────
|
|
@@ -9810,16 +10597,109 @@ const DEFAULT_LABELS = {
|
|
|
9810
10597
|
defaultValue: '',
|
|
9811
10598
|
onChange: onSearchQueryChange
|
|
9812
10599
|
});
|
|
9813
|
-
|
|
10600
|
+
// filterModel: in v1-bis the per-column header UI commits filter
|
|
10601
|
+
// items to this controllable state. Consumer can lift it via
|
|
10602
|
+
// filterModelProp + onFilterChange to drive server-side filtering.
|
|
10603
|
+
const [filterModel, setFilterModel] = useControllableState({
|
|
10604
|
+
controlledValue: filterModelProp,
|
|
10605
|
+
defaultValue: [],
|
|
10606
|
+
onChange: onFilterChange
|
|
10607
|
+
});
|
|
9814
10608
|
const [selectedRowIds, setSelectedRowIds] = useControllableState({
|
|
9815
10609
|
controlledValue: selectedRowIdsProp,
|
|
9816
10610
|
defaultValue: [],
|
|
9817
10611
|
onChange: onSelectionChange
|
|
9818
10612
|
});
|
|
9819
|
-
// ─────
|
|
9820
|
-
|
|
10613
|
+
// ───── Column visibility ─────
|
|
10614
|
+
// Seed default hidden set from `defaultHidden` flag on columns.
|
|
10615
|
+
// Memoized to avoid the seed shifting on every render — the
|
|
10616
|
+
// controllable state keeps internal state stable thereafter.
|
|
10617
|
+
const defaultHiddenColumns = useMemo(()=>cols.filter((c)=>c.defaultHidden === true).map((c)=>c.field), [
|
|
9821
10618
|
cols
|
|
9822
10619
|
]);
|
|
10620
|
+
const [hiddenColumns, setHiddenColumns] = useControllableState({
|
|
10621
|
+
controlledValue: hiddenColumnsProp,
|
|
10622
|
+
defaultValue: defaultHiddenColumns,
|
|
10623
|
+
onChange: onHiddenColumnsChange
|
|
10624
|
+
});
|
|
10625
|
+
// ───── Column widths (resize state) ─────
|
|
10626
|
+
// Seed from any `col.width` declared statically. Internal state is
|
|
10627
|
+
// applied only when the user actually starts dragging; until then
|
|
10628
|
+
// we fall back to the static width from the column def.
|
|
10629
|
+
const defaultColumnWidths = useMemo(()=>{
|
|
10630
|
+
const map = {};
|
|
10631
|
+
for (const c of cols){
|
|
10632
|
+
if (typeof c.width === 'number') {
|
|
10633
|
+
map[c.field] = c.width;
|
|
10634
|
+
}
|
|
10635
|
+
}
|
|
10636
|
+
return map;
|
|
10637
|
+
}, [
|
|
10638
|
+
cols
|
|
10639
|
+
]);
|
|
10640
|
+
const [columnWidths, setColumnWidths] = useControllableState({
|
|
10641
|
+
controlledValue: columnWidthsProp,
|
|
10642
|
+
defaultValue: defaultColumnWidths,
|
|
10643
|
+
onChange: onColumnWidthsChange
|
|
10644
|
+
});
|
|
10645
|
+
const { startResize } = useColumnResize({
|
|
10646
|
+
widths: columnWidths,
|
|
10647
|
+
onChange: setColumnWidths
|
|
10648
|
+
});
|
|
10649
|
+
// ───── Column order (reorder state) ─────
|
|
10650
|
+
const defaultColumnOrder = useMemo(()=>cols.map((c)=>c.field), [
|
|
10651
|
+
cols
|
|
10652
|
+
]);
|
|
10653
|
+
const [columnOrder, setColumnOrder] = useControllableState({
|
|
10654
|
+
controlledValue: columnOrderProp,
|
|
10655
|
+
defaultValue: defaultColumnOrder,
|
|
10656
|
+
onChange: onColumnOrderChange
|
|
10657
|
+
});
|
|
10658
|
+
const reorderHandlers = useColumnReorder({
|
|
10659
|
+
order: columnOrder,
|
|
10660
|
+
onChange: setColumnOrder
|
|
10661
|
+
});
|
|
10662
|
+
// Apply the active order to the columns list. Cols not in the
|
|
10663
|
+
// order array (e.g. newly-added columns at runtime) are appended
|
|
10664
|
+
// in their original position from `cols`.
|
|
10665
|
+
const orderedCols = useMemo(()=>{
|
|
10666
|
+
if (!columnOrder || columnOrder.length === 0) return cols;
|
|
10667
|
+
const byField = new Map(cols.map((c)=>[
|
|
10668
|
+
c.field,
|
|
10669
|
+
c
|
|
10670
|
+
]));
|
|
10671
|
+
const seen = new Set();
|
|
10672
|
+
const result = [];
|
|
10673
|
+
for (const field of columnOrder){
|
|
10674
|
+
const c = byField.get(field);
|
|
10675
|
+
if (c) {
|
|
10676
|
+
result.push(c);
|
|
10677
|
+
seen.add(field);
|
|
10678
|
+
}
|
|
10679
|
+
}
|
|
10680
|
+
for (const c of cols){
|
|
10681
|
+
if (!seen.has(c.field)) result.push(c);
|
|
10682
|
+
}
|
|
10683
|
+
return result;
|
|
10684
|
+
}, [
|
|
10685
|
+
cols,
|
|
10686
|
+
columnOrder
|
|
10687
|
+
]);
|
|
10688
|
+
// ───── RBAC + visibility + reorder: derive final column list ─────
|
|
10689
|
+
// - `access.onUnauthorized === 'hide'` removes a column entirely.
|
|
10690
|
+
// - `hiddenColumns` is the user-toggled hidden set from the
|
|
10691
|
+
// column-visibility dialog.
|
|
10692
|
+
// - `orderedCols` is the user-reordered version of `cols`.
|
|
10693
|
+
// NOTE: filtering happens on `orderedCols` so the active order is
|
|
10694
|
+
// preserved through visibility changes.
|
|
10695
|
+
const visibleCols = useMemo(()=>orderedCols.filter((col)=>{
|
|
10696
|
+
if (col.access && col.access.onUnauthorized === 'hide') return false;
|
|
10697
|
+
if (hiddenColumns.includes(col.field)) return false;
|
|
10698
|
+
return true;
|
|
10699
|
+
}), [
|
|
10700
|
+
orderedCols,
|
|
10701
|
+
hiddenColumns
|
|
10702
|
+
]);
|
|
9823
10703
|
// ───── Smart-default column types ─────
|
|
9824
10704
|
const columnTypes = useColumnAutoDetect(rows, visibleCols);
|
|
9825
10705
|
// ───── Pipeline: search → filter → sort (skip when server-side) ─────
|
|
@@ -9901,28 +10781,45 @@ const DEFAULT_LABELS = {
|
|
|
9901
10781
|
className: cn(v.root(), sx, slotProps == null ? void 0 : (_slotProps_root = slotProps.root) == null ? void 0 : _slotProps_root.className),
|
|
9902
10782
|
"data-disabled": !isInteractive ? 'true' : undefined,
|
|
9903
10783
|
children: [
|
|
9904
|
-
enableSearch &&
|
|
10784
|
+
(enableSearch || enableColumnVisibility) && jsxs("div", {
|
|
9905
10785
|
className: cn(v.toolbar(), slotProps == null ? void 0 : (_slotProps_toolbar = slotProps.toolbar) == null ? void 0 : _slotProps_toolbar.className),
|
|
9906
|
-
children:
|
|
9907
|
-
|
|
9908
|
-
|
|
9909
|
-
|
|
9910
|
-
|
|
9911
|
-
|
|
9912
|
-
|
|
9913
|
-
|
|
9914
|
-
|
|
9915
|
-
|
|
9916
|
-
|
|
9917
|
-
|
|
9918
|
-
|
|
9919
|
-
|
|
9920
|
-
|
|
9921
|
-
|
|
9922
|
-
|
|
9923
|
-
|
|
9924
|
-
|
|
9925
|
-
|
|
10786
|
+
children: [
|
|
10787
|
+
enableSearch && jsxs("label", {
|
|
10788
|
+
className: cn(v.search(), slotProps == null ? void 0 : (_slotProps_search = slotProps.search) == null ? void 0 : _slotProps_search.className),
|
|
10789
|
+
children: [
|
|
10790
|
+
jsx("span", {
|
|
10791
|
+
className: "sr-only",
|
|
10792
|
+
children: labels.searchPlaceholder
|
|
10793
|
+
}),
|
|
10794
|
+
jsx(SearchIcon, {
|
|
10795
|
+
className: "ml-2 shrink-0 text-neutral-500"
|
|
10796
|
+
}),
|
|
10797
|
+
jsx("input", {
|
|
10798
|
+
type: "text",
|
|
10799
|
+
value: searchQuery,
|
|
10800
|
+
onChange: (e)=>setSearchQuery(e.target.value),
|
|
10801
|
+
placeholder: searchPlaceholder != null ? searchPlaceholder : labels.searchPlaceholder,
|
|
10802
|
+
disabled: !isInteractive,
|
|
10803
|
+
className: cn('flex-1 bg-transparent border-0 outline-none', 'px-2 py-1.5', 'placeholder:text-neutral-400', 'disabled:opacity-50 disabled:cursor-not-allowed')
|
|
10804
|
+
})
|
|
10805
|
+
]
|
|
10806
|
+
}),
|
|
10807
|
+
enableColumnVisibility && jsx(ColumnVisibilityTrigger, {
|
|
10808
|
+
cols: cols.filter((c)=>// RBAC-hidden columns are never offered in the dialog.
|
|
10809
|
+
!c.access || c.access.onUnauthorized !== 'hide'),
|
|
10810
|
+
hiddenColumns: hiddenColumns,
|
|
10811
|
+
onChange: setHiddenColumns,
|
|
10812
|
+
disabled: !isInteractive,
|
|
10813
|
+
labels: {
|
|
10814
|
+
columnsButton: labels.columnsButton,
|
|
10815
|
+
columnsTitle: labels.columnsTitle,
|
|
10816
|
+
columnsDescription: labels.columnsDescription,
|
|
10817
|
+
columnsShowAll: labels.columnsShowAll,
|
|
10818
|
+
columnsHideAll: labels.columnsHideAll,
|
|
10819
|
+
columnsDone: labels.columnsDone
|
|
10820
|
+
}
|
|
10821
|
+
})
|
|
10822
|
+
]
|
|
9926
10823
|
}),
|
|
9927
10824
|
jsx("div", {
|
|
9928
10825
|
ref: scrollRef,
|
|
@@ -9965,8 +10862,15 @@ const DEFAULT_LABELS = {
|
|
|
9965
10862
|
sortModel: sortModel,
|
|
9966
10863
|
onSortClick: (e)=>handleSortClick(col, e, sortModel, setSortModel),
|
|
9967
10864
|
inferredType: columnTypes.get(col.field),
|
|
10865
|
+
filterModel: filterModel,
|
|
10866
|
+
onFilterChange: setFilterModel,
|
|
10867
|
+
effectiveWidth: resolveColumnWidth(col, columnWidths),
|
|
10868
|
+
resizable: enableColumnResize && col.resizable !== false,
|
|
10869
|
+
startResize: startResize,
|
|
10870
|
+
reorderable: enableColumnReorder && col.reorderable !== false,
|
|
10871
|
+
reorderHandlers: reorderHandlers,
|
|
9968
10872
|
labels: labels,
|
|
9969
|
-
className: cn(v.headerCell(), col.sticky === 'left' && v.stickyLeftHeaderCell(), slotProps == null ? void 0 : (_slotProps_headerCell = slotProps.headerCell) == null ? void 0 : _slotProps_headerCell.className),
|
|
10873
|
+
className: cn(v.headerCell(), col.sticky === 'left' && v.stickyLeftHeaderCell(), col.sticky === 'right' && v.stickyRightHeaderCell(), slotProps == null ? void 0 : (_slotProps_headerCell = slotProps.headerCell) == null ? void 0 : _slotProps_headerCell.className),
|
|
9970
10874
|
buttonClassName: v.headerCellButton(),
|
|
9971
10875
|
disabled: !isInteractive
|
|
9972
10876
|
}, col.field);
|
|
@@ -9999,6 +10903,7 @@ const DEFAULT_LABELS = {
|
|
|
9999
10903
|
renderedRows,
|
|
10000
10904
|
visibleCols,
|
|
10001
10905
|
columnTypes,
|
|
10906
|
+
columnWidths,
|
|
10002
10907
|
selection,
|
|
10003
10908
|
rowSelection,
|
|
10004
10909
|
showSelectionColumn,
|
|
@@ -10012,6 +10917,7 @@ const DEFAULT_LABELS = {
|
|
|
10012
10917
|
cell: cn(v.cell(), slotProps == null ? void 0 : (_slotProps_cell1 = slotProps.cell) == null ? void 0 : _slotProps_cell1.className),
|
|
10013
10918
|
selectionCell: cn(v.cell(), v.selectionCell(), v.stickyLeftCell(), slotProps == null ? void 0 : (_slotProps_cell2 = slotProps.cell) == null ? void 0 : _slotProps_cell2.className, slotProps == null ? void 0 : (_slotProps_selectionCell1 = slotProps.selectionCell) == null ? void 0 : _slotProps_selectionCell1.className),
|
|
10014
10919
|
stickyLeftCell: v.stickyLeftCell(),
|
|
10920
|
+
stickyRightCell: v.stickyRightCell(),
|
|
10015
10921
|
rowActionsCell: cn(v.cell(), v.rowActionsCell(), slotProps == null ? void 0 : (_slotProps_cell3 = slotProps.cell) == null ? void 0 : _slotProps_cell3.className, slotProps == null ? void 0 : (_slotProps_rowActionsCell = slotProps.rowActionsCell) == null ? void 0 : _slotProps_rowActionsCell.className)
|
|
10016
10922
|
}
|
|
10017
10923
|
})
|
|
@@ -10058,50 +10964,160 @@ const DEFAULT_LABELS = {
|
|
|
10058
10964
|
}
|
|
10059
10965
|
// ───── Header cell ─────
|
|
10060
10966
|
function HeaderCell(props) {
|
|
10061
|
-
const { col, sortModel, onSortClick, inferredType, labels, className, buttonClassName, disabled } = props;
|
|
10967
|
+
const { col, sortModel, onSortClick, inferredType, filterModel, onFilterChange, effectiveWidth, resizable, startResize, reorderable, reorderHandlers, labels, className, buttonClassName, disabled } = props;
|
|
10062
10968
|
const align = resolveAlign(col, inferredType);
|
|
10063
10969
|
const alignClass = align === 'right' ? 'text-right' : align === 'center' ? 'text-center' : 'text-left';
|
|
10064
10970
|
const sortEntry = sortModel.find((s)=>s.field === col.field);
|
|
10065
10971
|
const ariaSort = (sortEntry == null ? void 0 : sortEntry.direction) === 'asc' ? 'ascending' : (sortEntry == null ? void 0 : sortEntry.direction) === 'desc' ? 'descending' : 'none';
|
|
10066
10972
|
const headerContent = typeof col.header === 'function' ? col.header() : col.header;
|
|
10067
10973
|
const cellStyle = {
|
|
10068
|
-
width:
|
|
10974
|
+
width: effectiveWidth != null ? `${effectiveWidth}px` : undefined,
|
|
10069
10975
|
minWidth: col.minWidth != null ? `${col.minWidth}px` : undefined,
|
|
10070
|
-
flex: col.flex
|
|
10976
|
+
flex: effectiveWidth != null ? undefined : col.flex,
|
|
10977
|
+
position: resizable || reorderable ? 'relative' : undefined
|
|
10071
10978
|
};
|
|
10979
|
+
// Drop indicator for reorder: a 2px vertical bar at the LEFT or
|
|
10980
|
+
// RIGHT edge of this th when it's the active drop target.
|
|
10981
|
+
const dropTarget = reorderHandlers.dropTarget;
|
|
10982
|
+
const dropIndicatorClass = dropTarget != null && dropTarget.field === col.field ? dropTarget.side === 'left' ? 'before:absolute before:top-0 before:bottom-0 before:left-0 before:w-0.5 before:bg-primary-500' : 'after:absolute after:top-0 after:bottom-0 after:right-0 after:w-0.5 after:bg-primary-500' : '';
|
|
10983
|
+
const isDragging = reorderHandlers.draggingField === col.field;
|
|
10984
|
+
// Drag attributes only attach when reorderable; we DO NOT make the
|
|
10985
|
+
// entire th `draggable` if it isn't — keeps the keyboard focus
|
|
10986
|
+
// behavior unchanged.
|
|
10987
|
+
const dragAttrs = reorderable ? {
|
|
10988
|
+
draggable: true,
|
|
10989
|
+
onDragStart: reorderHandlers.dragStart(col.field),
|
|
10990
|
+
onDragOver: reorderHandlers.dragOver(col.field),
|
|
10991
|
+
onDrop: reorderHandlers.drop(col.field),
|
|
10992
|
+
onDragEnd: reorderHandlers.dragEnd
|
|
10993
|
+
} : {};
|
|
10994
|
+
const resizeHandle = resizable ? jsx("span", {
|
|
10995
|
+
role: "separator",
|
|
10996
|
+
"aria-orientation": "vertical",
|
|
10997
|
+
"aria-label": `Resize ${typeof col.header === 'string' ? col.header : col.field}`,
|
|
10998
|
+
onPointerDown: (e)=>{
|
|
10999
|
+
var _ref, _col_minWidth, _col_maxWidth;
|
|
11000
|
+
const handle = e.currentTarget;
|
|
11001
|
+
const th = handle.parentElement;
|
|
11002
|
+
const startWidth = (_ref = effectiveWidth != null ? effectiveWidth : th == null ? void 0 : th.getBoundingClientRect().width) != null ? _ref : 120;
|
|
11003
|
+
const min = (_col_minWidth = col.minWidth) != null ? _col_minWidth : 40;
|
|
11004
|
+
const max = (_col_maxWidth = col.maxWidth) != null ? _col_maxWidth : 1200;
|
|
11005
|
+
startResize(col.field, startWidth, min, max)(e);
|
|
11006
|
+
},
|
|
11007
|
+
className: cn('absolute top-0 bottom-0 right-0 w-1.5', 'cursor-col-resize select-none touch-none', 'hover:bg-primary-300 active:bg-primary-400', 'transition-colors motion-reduce:transition-none')
|
|
11008
|
+
}) : null;
|
|
11009
|
+
const filterType = col.filterable ? resolveFilterType(col, inferredType) : null;
|
|
11010
|
+
const filterTrigger = col.filterable && filterType ? jsx(ColumnFilterTrigger, {
|
|
11011
|
+
field: col.field,
|
|
11012
|
+
filterType: filterType,
|
|
11013
|
+
filterModel: filterModel,
|
|
11014
|
+
onFilterChange: onFilterChange,
|
|
11015
|
+
disabled: disabled,
|
|
11016
|
+
labels: {
|
|
11017
|
+
filterColumn: labels.filterColumn,
|
|
11018
|
+
filterApply: labels.filterApply,
|
|
11019
|
+
filterClear: labels.filterClear,
|
|
11020
|
+
filterMin: labels.filterMin,
|
|
11021
|
+
filterMax: labels.filterMax,
|
|
11022
|
+
filterFrom: labels.filterFrom,
|
|
11023
|
+
filterTo: labels.filterTo,
|
|
11024
|
+
filterAll: labels.filterAll,
|
|
11025
|
+
filterTrue: labels.filterTrue,
|
|
11026
|
+
filterFalse: labels.filterFalse
|
|
11027
|
+
}
|
|
11028
|
+
}) : null;
|
|
11029
|
+
// Layout strategy:
|
|
11030
|
+
// - sortable only → sort button is the cell content
|
|
11031
|
+
// - filterable only → header text + filter trigger in a flex row
|
|
11032
|
+
// - both → sort button + filter trigger in a flex row
|
|
11033
|
+
// - neither → plain header text
|
|
11034
|
+
const wrapperJustify = align === 'right' ? 'justify-end' : align === 'center' ? 'justify-center' : 'justify-start';
|
|
10072
11035
|
if (col.sortable) {
|
|
10073
|
-
|
|
11036
|
+
const sortButton = jsxs("button", {
|
|
11037
|
+
type: "button",
|
|
11038
|
+
onClick: onSortClick,
|
|
11039
|
+
disabled: disabled,
|
|
11040
|
+
"aria-label": ariaSort === 'ascending' ? labels.ariaSortAscending : ariaSort === 'descending' ? labels.ariaSortDescending : labels.ariaSortNone,
|
|
11041
|
+
className: cn(buttonClassName, align === 'right' && 'flex-row-reverse', 'disabled:cursor-not-allowed disabled:opacity-50'),
|
|
11042
|
+
children: [
|
|
11043
|
+
jsx("span", {
|
|
11044
|
+
children: headerContent
|
|
11045
|
+
}),
|
|
11046
|
+
jsx(SortIcon, {
|
|
11047
|
+
direction: sortEntry == null ? void 0 : sortEntry.direction
|
|
11048
|
+
})
|
|
11049
|
+
]
|
|
11050
|
+
});
|
|
11051
|
+
return jsxs("th", _extends({
|
|
10074
11052
|
scope: "col",
|
|
10075
11053
|
"aria-sort": ariaSort,
|
|
10076
|
-
className: cn(className, alignClass),
|
|
10077
|
-
style: cellStyle
|
|
10078
|
-
|
|
10079
|
-
|
|
10080
|
-
|
|
10081
|
-
|
|
10082
|
-
|
|
10083
|
-
|
|
11054
|
+
className: cn(className, alignClass, dropIndicatorClass, isDragging && 'opacity-50'),
|
|
11055
|
+
style: cellStyle
|
|
11056
|
+
}, dragAttrs, {
|
|
11057
|
+
children: [
|
|
11058
|
+
filterTrigger ? jsxs("div", {
|
|
11059
|
+
className: cn('flex items-center gap-1', wrapperJustify),
|
|
11060
|
+
children: [
|
|
11061
|
+
sortButton,
|
|
11062
|
+
filterTrigger
|
|
11063
|
+
]
|
|
11064
|
+
}) : sortButton,
|
|
11065
|
+
resizeHandle
|
|
11066
|
+
]
|
|
11067
|
+
}));
|
|
11068
|
+
}
|
|
11069
|
+
return jsxs("th", _extends({
|
|
11070
|
+
scope: "col",
|
|
11071
|
+
className: cn(className, alignClass, dropIndicatorClass, isDragging && 'opacity-50'),
|
|
11072
|
+
style: cellStyle
|
|
11073
|
+
}, dragAttrs, {
|
|
11074
|
+
children: [
|
|
11075
|
+
filterTrigger ? jsxs("div", {
|
|
11076
|
+
className: cn('flex items-center gap-1', wrapperJustify),
|
|
10084
11077
|
children: [
|
|
10085
11078
|
jsx("span", {
|
|
10086
11079
|
children: headerContent
|
|
10087
11080
|
}),
|
|
10088
|
-
|
|
10089
|
-
direction: sortEntry == null ? void 0 : sortEntry.direction
|
|
10090
|
-
})
|
|
11081
|
+
filterTrigger
|
|
10091
11082
|
]
|
|
10092
|
-
})
|
|
10093
|
-
|
|
11083
|
+
}) : headerContent,
|
|
11084
|
+
resizeHandle
|
|
11085
|
+
]
|
|
11086
|
+
}));
|
|
11087
|
+
}
|
|
11088
|
+
/**
|
|
11089
|
+
* Resolve the effective width (px) for a column. User-driven resize
|
|
11090
|
+
* (`columnWidths[field]`) wins over the static `col.width`. Returns
|
|
11091
|
+
* `undefined` if neither source has a width set — the consumer falls
|
|
11092
|
+
* back to the column's `flex` value or browser auto-layout.
|
|
11093
|
+
*/ function resolveColumnWidth(col, columnWidths) {
|
|
11094
|
+
const resized = columnWidths[col.field];
|
|
11095
|
+
if (resized != null) return resized;
|
|
11096
|
+
return col.width;
|
|
11097
|
+
}
|
|
11098
|
+
/**
|
|
11099
|
+
* Resolve the filter UI type for a column. Explicit `col.filterType`
|
|
11100
|
+
* wins; otherwise we map the auto-detected primitive type:
|
|
11101
|
+
* - `number` → number range
|
|
11102
|
+
* - `boolean` → true/false/all select
|
|
11103
|
+
* - `date` → date range
|
|
11104
|
+
* - anything else (string / unknown) → text contains
|
|
11105
|
+
*/ function resolveFilterType(col, inferred) {
|
|
11106
|
+
if (col.filterType) return col.filterType;
|
|
11107
|
+
switch(inferred){
|
|
11108
|
+
case 'number':
|
|
11109
|
+
return 'number';
|
|
11110
|
+
case 'boolean':
|
|
11111
|
+
return 'boolean';
|
|
11112
|
+
case 'date':
|
|
11113
|
+
return 'date';
|
|
11114
|
+
default:
|
|
11115
|
+
return 'text';
|
|
10094
11116
|
}
|
|
10095
|
-
return jsx("th", {
|
|
10096
|
-
scope: "col",
|
|
10097
|
-
className: cn(className, alignClass),
|
|
10098
|
-
style: cellStyle,
|
|
10099
|
-
children: headerContent
|
|
10100
|
-
});
|
|
10101
11117
|
}
|
|
10102
11118
|
// ───── Virtualized body ─────
|
|
10103
11119
|
function renderVirtualizedBody(args) {
|
|
10104
|
-
const { vWindow, renderedRows, visibleCols, columnTypes, selection, rowSelection, showSelectionColumn, showRowActionsColumn, rowActions, getRowId, isInteractive, labels, classes } = args;
|
|
11120
|
+
const { vWindow, renderedRows, visibleCols, columnTypes, columnWidths, selection, rowSelection, showSelectionColumn, showRowActionsColumn, rowActions, getRowId, isInteractive, labels, classes } = args;
|
|
10105
11121
|
return jsxs(Fragment$1, {
|
|
10106
11122
|
children: [
|
|
10107
11123
|
vWindow.paddingTop > 0 && jsx("tr", {
|
|
@@ -10136,8 +11152,10 @@ function renderVirtualizedBody(args) {
|
|
|
10136
11152
|
rowIndex: absoluteIdx,
|
|
10137
11153
|
isSelected: isSelected,
|
|
10138
11154
|
inferredType: columnTypes.get(col.field),
|
|
11155
|
+
effectiveWidth: resolveColumnWidth(col, columnWidths),
|
|
10139
11156
|
cellClass: classes.cell,
|
|
10140
|
-
stickyLeftClass: classes.stickyLeftCell
|
|
11157
|
+
stickyLeftClass: classes.stickyLeftCell,
|
|
11158
|
+
stickyRightClass: classes.stickyRightCell
|
|
10141
11159
|
}, col.field)),
|
|
10142
11160
|
showRowActionsColumn && rowActions && jsx("td", {
|
|
10143
11161
|
className: classes.rowActionsCell,
|
|
@@ -10157,7 +11175,7 @@ function renderVirtualizedBody(args) {
|
|
|
10157
11175
|
}
|
|
10158
11176
|
// ───── Data cell ─────
|
|
10159
11177
|
function DataCell(props) {
|
|
10160
|
-
const { col, row, rowIndex, isSelected, inferredType, cellClass, stickyLeftClass } = props;
|
|
11178
|
+
const { col, row, rowIndex, isSelected, inferredType, effectiveWidth, cellClass, stickyLeftClass, stickyRightClass } = props;
|
|
10161
11179
|
const value = getNestedValue(row, col.field);
|
|
10162
11180
|
const align = resolveAlign(col, inferredType);
|
|
10163
11181
|
const tabularNums = resolveTabularNums(col, inferredType);
|
|
@@ -10177,9 +11195,9 @@ function DataCell(props) {
|
|
|
10177
11195
|
access: columnAccessState
|
|
10178
11196
|
}) : renderDefaultCell(value);
|
|
10179
11197
|
return jsx("td", {
|
|
10180
|
-
className: cn(cellClass, alignClass, tabularNums && 'tabular-nums', monospace && 'font-mono', col.sticky === 'left' && stickyLeftClass),
|
|
11198
|
+
className: cn(cellClass, alignClass, tabularNums && 'tabular-nums', monospace && 'font-mono', col.sticky === 'left' && stickyLeftClass, col.sticky === 'right' && stickyRightClass),
|
|
10181
11199
|
style: {
|
|
10182
|
-
width:
|
|
11200
|
+
width: effectiveWidth != null ? `${effectiveWidth}px` : undefined,
|
|
10183
11201
|
minWidth: col.minWidth != null ? `${col.minWidth}px` : undefined
|
|
10184
11202
|
},
|
|
10185
11203
|
children: content
|