@ackplus/react-tanstack-data-table 1.1.17 → 1.1.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/lib/components/toolbar/column-filter-control.d.ts +3 -2
- package/dist/lib/components/toolbar/column-filter-control.d.ts.map +1 -1
- package/dist/lib/components/toolbar/column-filter-control.js +91 -92
- package/dist/lib/components/toolbar/table-refresh-control.d.ts.map +1 -1
- package/dist/lib/components/toolbar/table-refresh-control.js +3 -1
- package/dist/lib/features/column-filter.feature.d.ts +2 -1
- package/dist/lib/features/column-filter.feature.d.ts.map +1 -1
- package/dist/lib/features/column-filter.feature.js +14 -0
- package/dist/lib/hooks/use-data-table-engine.d.ts.map +1 -1
- package/dist/lib/hooks/use-data-table-engine.js +278 -14
- package/package.json +1 -1
- package/src/lib/components/toolbar/column-filter-control.tsx +270 -212
- package/src/lib/components/toolbar/table-refresh-control.tsx +11 -7
- package/src/lib/features/column-filter.feature.ts +15 -1
- package/src/lib/hooks/use-data-table-engine.ts +248 -14
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { FilterList } from
|
|
1
|
+
import { FilterList } from "@mui/icons-material";
|
|
2
2
|
import {
|
|
3
3
|
Box,
|
|
4
4
|
MenuItem,
|
|
@@ -11,242 +11,298 @@ import {
|
|
|
11
11
|
IconButton,
|
|
12
12
|
Divider,
|
|
13
13
|
Badge,
|
|
14
|
-
IconButtonProps,
|
|
15
|
-
SxProps,
|
|
16
|
-
} from
|
|
17
|
-
import React, {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
} from
|
|
25
|
-
|
|
26
|
-
import {
|
|
27
|
-
import {
|
|
28
|
-
import {
|
|
29
|
-
import {
|
|
14
|
+
type IconButtonProps,
|
|
15
|
+
type SxProps,
|
|
16
|
+
} from "@mui/material";
|
|
17
|
+
import React, {
|
|
18
|
+
useMemo,
|
|
19
|
+
useCallback,
|
|
20
|
+
useEffect,
|
|
21
|
+
useRef,
|
|
22
|
+
useState,
|
|
23
|
+
type ReactElement,
|
|
24
|
+
} from "react";
|
|
25
|
+
|
|
26
|
+
import { MenuDropdown } from "../droupdown/menu-dropdown";
|
|
27
|
+
import { useDataTableContext } from "../../contexts/data-table-context";
|
|
28
|
+
import { AddIcon, DeleteIcon } from "../../icons";
|
|
29
|
+
import { getColumnType, isColumnFilterable } from "../../utils/column-helpers";
|
|
30
|
+
import { getSlotComponent, mergeSlotProps, extractSlotProps } from "../../utils/slot-helpers";
|
|
31
|
+
import { FILTER_OPERATORS } from "../filters";
|
|
32
|
+
import { FilterValueInput } from "../filters/filter-value-input";
|
|
33
|
+
import type { ColumnFilterRule } from "../../features";
|
|
30
34
|
|
|
31
35
|
export interface ColumnFilterControlProps {
|
|
32
|
-
// Allow full customization of any prop
|
|
33
36
|
title?: string;
|
|
34
37
|
titleSx?: SxProps;
|
|
35
38
|
menuSx?: SxProps;
|
|
39
|
+
|
|
36
40
|
iconButtonProps?: IconButtonProps;
|
|
37
41
|
badgeProps?: any;
|
|
42
|
+
|
|
38
43
|
clearButtonProps?: any;
|
|
39
44
|
applyButtonProps?: any;
|
|
40
45
|
addButtonProps?: any;
|
|
46
|
+
deleteButtonProps?: any;
|
|
41
47
|
logicSelectProps?: any;
|
|
48
|
+
|
|
42
49
|
[key: string]: any;
|
|
43
50
|
}
|
|
44
51
|
|
|
52
|
+
/**
|
|
53
|
+
* Small helper component to sync MenuDropdown open state to parent state
|
|
54
|
+
* WITHOUT calling hooks inside render-prop callback.
|
|
55
|
+
*/
|
|
56
|
+
function OpenStateSync({
|
|
57
|
+
open,
|
|
58
|
+
onChange,
|
|
59
|
+
}: {
|
|
60
|
+
open: boolean;
|
|
61
|
+
onChange: (open: boolean) => void;
|
|
62
|
+
}) {
|
|
63
|
+
useEffect(() => onChange(open), [open, onChange]);
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
|
|
45
67
|
export function ColumnFilterControl(props: ColumnFilterControlProps = {}): ReactElement {
|
|
46
68
|
const { table, slots, slotProps } = useDataTableContext();
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
const filterState =
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
const filters = filterState.pendingFilters;
|
|
63
|
-
const filterLogic = filterState.pendingLogic;
|
|
64
|
-
|
|
65
|
-
// Active filters are the actual applied filters
|
|
69
|
+
|
|
70
|
+
const iconSlotProps = extractSlotProps(slotProps, "filterIcon");
|
|
71
|
+
const FilterIconSlot = getSlotComponent(slots, "filterIcon", FilterList);
|
|
72
|
+
|
|
73
|
+
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
|
74
|
+
const didAutoAddRef = useRef(false);
|
|
75
|
+
|
|
76
|
+
const filterState =
|
|
77
|
+
table?.getColumnFilterState?.() || ({
|
|
78
|
+
filters: [],
|
|
79
|
+
logic: "AND",
|
|
80
|
+
pendingFilters: [],
|
|
81
|
+
pendingLogic: "AND",
|
|
82
|
+
} as any);
|
|
83
|
+
|
|
84
|
+
const filters: ColumnFilterRule[] = filterState.pendingFilters || [];
|
|
85
|
+
const filterLogic: "AND" | "OR" = (filterState.pendingLogic || "AND") as any;
|
|
86
|
+
|
|
66
87
|
const activeFiltersCount = table?.getActiveColumnFilters?.()?.length || 0;
|
|
67
88
|
|
|
68
89
|
const filterableColumns = useMemo(() => {
|
|
69
|
-
return table?.getAllLeafColumns()
|
|
70
|
-
.filter(column => isColumnFilterable(column));
|
|
90
|
+
return table?.getAllLeafColumns().filter((column: any) => isColumnFilterable(column)) || [];
|
|
71
91
|
}, [table]);
|
|
72
92
|
|
|
73
|
-
const
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
93
|
+
const getOperatorsForColumn = useCallback(
|
|
94
|
+
(columnId: string) => {
|
|
95
|
+
const column = filterableColumns.find((col: any) => col.id === columnId);
|
|
96
|
+
const type = getColumnType(column as any);
|
|
97
|
+
return FILTER_OPERATORS[type as keyof typeof FILTER_OPERATORS] || FILTER_OPERATORS.text;
|
|
98
|
+
},
|
|
99
|
+
[filterableColumns]
|
|
100
|
+
);
|
|
77
101
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
const operators = FILTER_OPERATORS[columnType as keyof typeof FILTER_OPERATORS] || FILTER_OPERATORS.text;
|
|
82
|
-
defaultOperator = operators[0]?.value || 'contains';
|
|
83
|
-
}
|
|
102
|
+
const addFilter = useCallback(
|
|
103
|
+
(columnId?: string, operator?: string) => {
|
|
104
|
+
let defaultOperator = operator || "";
|
|
84
105
|
|
|
85
|
-
|
|
86
|
-
|
|
106
|
+
if (columnId && !operator) {
|
|
107
|
+
const column = filterableColumns.find((col: any) => col.id === columnId);
|
|
108
|
+
const columnType = getColumnType(column as any);
|
|
109
|
+
const operators =
|
|
110
|
+
FILTER_OPERATORS[columnType as keyof typeof FILTER_OPERATORS] || FILTER_OPERATORS.text;
|
|
111
|
+
defaultOperator = operators[0]?.value || "contains";
|
|
112
|
+
}
|
|
87
113
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
114
|
+
table?.addPendingColumnFilter?.(columnId || "", defaultOperator, "");
|
|
115
|
+
},
|
|
116
|
+
[table, filterableColumns]
|
|
117
|
+
);
|
|
91
118
|
|
|
92
|
-
const updateFilter = useCallback(
|
|
93
|
-
|
|
94
|
-
|
|
119
|
+
const updateFilter = useCallback(
|
|
120
|
+
(filterId: string, updates: Partial<ColumnFilterRule>) => {
|
|
121
|
+
table?.updatePendingColumnFilter?.(filterId, updates);
|
|
122
|
+
},
|
|
123
|
+
[table]
|
|
124
|
+
);
|
|
95
125
|
|
|
96
|
-
const removeFilter = useCallback(
|
|
97
|
-
|
|
98
|
-
|
|
126
|
+
const removeFilter = useCallback(
|
|
127
|
+
(filterId: string) => {
|
|
128
|
+
table?.removePendingColumnFilter?.(filterId);
|
|
129
|
+
},
|
|
130
|
+
[table]
|
|
131
|
+
);
|
|
99
132
|
|
|
100
133
|
const clearAllFilters = useCallback((closeDialog?: () => void) => {
|
|
101
|
-
//
|
|
102
|
-
table?.clearAllPendingColumnFilters?.();
|
|
103
|
-
// Immediately apply the clear (which will clear active filters too)
|
|
134
|
+
// Defer all work to avoid long-running click handler (prevents "[Violation] 'click' handler took Xms")
|
|
104
135
|
setTimeout(() => {
|
|
105
|
-
table?.
|
|
106
|
-
//
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
}
|
|
136
|
+
table?.resetColumnFilter?.();
|
|
137
|
+
// Prevent auto-add effect from adding a row when it sees empty state after clear
|
|
138
|
+
didAutoAddRef.current = true;
|
|
139
|
+
closeDialog?.();
|
|
110
140
|
}, 0);
|
|
111
141
|
}, [table]);
|
|
112
142
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
143
|
+
const handleLogicChange = useCallback(
|
|
144
|
+
(newLogic: "AND" | "OR") => {
|
|
145
|
+
table?.setPendingFilterLogic?.(newLogic);
|
|
146
|
+
},
|
|
147
|
+
[table]
|
|
148
|
+
);
|
|
117
149
|
|
|
118
|
-
// Apply all pending filters
|
|
119
150
|
const applyFilters = useCallback(() => {
|
|
120
151
|
table?.applyPendingColumnFilters?.();
|
|
121
152
|
}, [table]);
|
|
122
153
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
}, [filterableColumns]);
|
|
134
|
-
|
|
135
|
-
// Handle column selection change
|
|
136
|
-
const handleColumnChange = useCallback((filterId: string, newColumnId: string, currentFilter: ColumnFilterRule) => {
|
|
137
|
-
const newColumn = filterableColumns?.find(col => col.id === newColumnId);
|
|
138
|
-
const columnType = getColumnType(newColumn as any);
|
|
139
|
-
const operators = FILTER_OPERATORS[columnType as keyof typeof FILTER_OPERATORS] || FILTER_OPERATORS.text;
|
|
140
|
-
|
|
141
|
-
// Only reset operator if current operator is not valid for new column type
|
|
142
|
-
const currentOperatorValid = operators.some(op => op.value === currentFilter.operator);
|
|
143
|
-
const newOperator = currentOperatorValid ? currentFilter.operator : operators[0]?.value || '';
|
|
144
|
-
|
|
145
|
-
updateFilter(filterId, {
|
|
146
|
-
columnId: newColumnId,
|
|
147
|
-
operator: newOperator,
|
|
148
|
-
// Keep the current value unless operator is empty/notEmpty
|
|
149
|
-
value: ['isEmpty', 'isNotEmpty'].includes(newOperator) ? '' : currentFilter.value,
|
|
150
|
-
});
|
|
151
|
-
}, [filterableColumns, updateFilter]);
|
|
152
|
-
|
|
153
|
-
// Handle operator selection change
|
|
154
|
-
const handleOperatorChange = useCallback((filterId: string, newOperator: string, currentFilter: ColumnFilterRule) => {
|
|
155
|
-
updateFilter(filterId, {
|
|
156
|
-
operator: newOperator,
|
|
157
|
-
// Only reset value if operator is empty/notEmpty, otherwise preserve it
|
|
158
|
-
value: ['isEmpty', 'isNotEmpty'].includes(newOperator) ? '' : currentFilter.value,
|
|
159
|
-
});
|
|
160
|
-
}, [updateFilter]);
|
|
161
|
-
|
|
162
|
-
// Handle filter value change
|
|
163
|
-
const handleFilterValueChange = useCallback((filterId: string, value: any) => {
|
|
164
|
-
updateFilter(filterId, { value });
|
|
165
|
-
}, [updateFilter]);
|
|
166
|
-
|
|
167
|
-
// Handle filter removal
|
|
168
|
-
const handleRemoveFilter = useCallback((filterId: string) => {
|
|
169
|
-
removeFilter(filterId);
|
|
170
|
-
}, [removeFilter]);
|
|
171
|
-
|
|
172
|
-
// Count pending filters that are ready to apply (have column, operator, and value OR are empty/notEmpty operators)
|
|
173
|
-
const pendingFiltersCount = filters.filter(f => {
|
|
174
|
-
if (!f.columnId || !f.operator) return false;
|
|
175
|
-
// For empty/notEmpty operators, no value is needed
|
|
176
|
-
if (['isEmpty', 'isNotEmpty'].includes(f.operator)) return true;
|
|
177
|
-
// For other operators, value is required
|
|
178
|
-
return f.value && f.value.toString().trim() !== '';
|
|
179
|
-
}).length;
|
|
180
|
-
|
|
181
|
-
// Check if we need to show "Clear Applied Filters" button
|
|
182
|
-
const hasAppliedFilters = activeFiltersCount > 0;
|
|
154
|
+
const handleApplyFilters = useCallback(
|
|
155
|
+
(closeDialog: () => void) => {
|
|
156
|
+
// Defer so click handler returns immediately (prevents "[Violation] 'click' handler took Xms")
|
|
157
|
+
setTimeout(() => {
|
|
158
|
+
applyFilters();
|
|
159
|
+
closeDialog();
|
|
160
|
+
}, 0);
|
|
161
|
+
},
|
|
162
|
+
[applyFilters]
|
|
163
|
+
);
|
|
183
164
|
|
|
184
|
-
|
|
185
|
-
|
|
165
|
+
const handleColumnChange = useCallback(
|
|
166
|
+
(filterId: string, newColumnId: string, currentFilter: ColumnFilterRule) => {
|
|
167
|
+
const newColumn = filterableColumns.find((col: any) => col.id === newColumnId);
|
|
168
|
+
const columnType = getColumnType(newColumn as any);
|
|
169
|
+
const operators =
|
|
170
|
+
FILTER_OPERATORS[columnType as keyof typeof FILTER_OPERATORS] || FILTER_OPERATORS.text;
|
|
171
|
+
|
|
172
|
+
const currentOperatorValid = operators.some((op) => op.value === currentFilter.operator);
|
|
173
|
+
const newOperator = currentOperatorValid ? currentFilter.operator : operators[0]?.value || "";
|
|
174
|
+
|
|
175
|
+
updateFilter(filterId, {
|
|
176
|
+
columnId: newColumnId,
|
|
177
|
+
operator: newOperator,
|
|
178
|
+
value: ["isEmpty", "isNotEmpty"].includes(newOperator) ? "" : currentFilter.value,
|
|
179
|
+
});
|
|
180
|
+
},
|
|
181
|
+
[filterableColumns, updateFilter]
|
|
182
|
+
);
|
|
183
|
+
|
|
184
|
+
const handleOperatorChange = useCallback(
|
|
185
|
+
(filterId: string, newOperator: string, currentFilter: ColumnFilterRule) => {
|
|
186
|
+
updateFilter(filterId, {
|
|
187
|
+
operator: newOperator,
|
|
188
|
+
value: ["isEmpty", "isNotEmpty"].includes(newOperator) ? "" : currentFilter.value,
|
|
189
|
+
});
|
|
190
|
+
},
|
|
191
|
+
[updateFilter]
|
|
192
|
+
);
|
|
193
|
+
|
|
194
|
+
const handleFilterValueChange = useCallback(
|
|
195
|
+
(filterId: string, value: any) => {
|
|
196
|
+
updateFilter(filterId, { value });
|
|
197
|
+
},
|
|
198
|
+
[updateFilter]
|
|
199
|
+
);
|
|
200
|
+
|
|
201
|
+
const pendingReadyCount = useMemo(() => {
|
|
202
|
+
return filters.filter((f) => {
|
|
203
|
+
if (!f.columnId || !f.operator) return false;
|
|
204
|
+
if (["isEmpty", "isNotEmpty"].includes(f.operator)) return true;
|
|
205
|
+
return f.value != null && String(f.value).trim() !== "";
|
|
206
|
+
}).length;
|
|
207
|
+
}, [filters]);
|
|
208
|
+
|
|
209
|
+
const hasAppliedFilters = activeFiltersCount > 0;
|
|
210
|
+
const hasPendingChanges = pendingReadyCount > 0 || (filters.length === 0 && hasAppliedFilters);
|
|
186
211
|
|
|
187
|
-
// Auto-add
|
|
212
|
+
// Auto-add only once per open. If menu opened with existing filters, mark as processed so
|
|
213
|
+
// "Clear All" doesn't cause a new row to be auto-added when state becomes empty.
|
|
188
214
|
useEffect(() => {
|
|
189
|
-
if (
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
const operators = FILTER_OPERATORS[columnType as keyof typeof FILTER_OPERATORS] || FILTER_OPERATORS.text;
|
|
193
|
-
const defaultOperator = operators[0]?.value || 'contains';
|
|
194
|
-
// Add default filter with first column and its first operator
|
|
195
|
-
addFilter(firstColumn?.id, defaultOperator);
|
|
215
|
+
if (!isMenuOpen) {
|
|
216
|
+
didAutoAddRef.current = false;
|
|
217
|
+
return;
|
|
196
218
|
}
|
|
197
|
-
|
|
219
|
+
if (didAutoAddRef.current) return;
|
|
220
|
+
if (!filterableColumns.length) {
|
|
221
|
+
didAutoAddRef.current = true;
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
if (filters.length > 0 || activeFiltersCount > 0) {
|
|
225
|
+
// Already have filters this session; mark processed so clear won't re-trigger auto-add
|
|
226
|
+
didAutoAddRef.current = true;
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const firstColumn = filterableColumns[0];
|
|
231
|
+
const columnType = getColumnType(firstColumn as any);
|
|
232
|
+
const operators =
|
|
233
|
+
FILTER_OPERATORS[columnType as keyof typeof FILTER_OPERATORS] || FILTER_OPERATORS.text;
|
|
234
|
+
const defaultOperator = operators[0]?.value || "contains";
|
|
235
|
+
|
|
236
|
+
didAutoAddRef.current = true;
|
|
237
|
+
addFilter(firstColumn.id, defaultOperator);
|
|
238
|
+
}, [isMenuOpen, filterableColumns, filters.length, activeFiltersCount, addFilter]);
|
|
198
239
|
|
|
199
|
-
// Merge
|
|
240
|
+
// Merge props but do NOT spread non-icon props onto IconButton
|
|
200
241
|
const mergedProps = mergeSlotProps(
|
|
201
|
-
{
|
|
202
|
-
// Default props
|
|
203
|
-
size: 'small',
|
|
204
|
-
sx: { flexShrink: 0 },
|
|
205
|
-
},
|
|
242
|
+
{ size: "small", sx: { flexShrink: 0 } },
|
|
206
243
|
slotProps?.columnFilterControl || {},
|
|
207
244
|
props
|
|
208
245
|
);
|
|
209
246
|
|
|
247
|
+
const {
|
|
248
|
+
badgeProps,
|
|
249
|
+
menuSx,
|
|
250
|
+
title,
|
|
251
|
+
titleSx,
|
|
252
|
+
logicSelectProps,
|
|
253
|
+
clearButtonProps,
|
|
254
|
+
applyButtonProps,
|
|
255
|
+
addButtonProps,
|
|
256
|
+
deleteButtonProps,
|
|
257
|
+
iconButtonProps,
|
|
258
|
+
...iconButtonRestProps
|
|
259
|
+
} = mergedProps;
|
|
260
|
+
|
|
210
261
|
return (
|
|
211
262
|
<MenuDropdown
|
|
212
|
-
anchor={(
|
|
213
|
-
<
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
{
|
|
263
|
+
anchor={({ isOpen }) => (
|
|
264
|
+
<Box sx={{ display: "inline-flex" }}>
|
|
265
|
+
{/* sync dropdown open state to our local state */}
|
|
266
|
+
<OpenStateSync open={isOpen} onChange={setIsMenuOpen} />
|
|
267
|
+
|
|
268
|
+
<Badge
|
|
269
|
+
badgeContent={activeFiltersCount > 0 ? activeFiltersCount : 0}
|
|
270
|
+
color="primary"
|
|
271
|
+
invisible={activeFiltersCount === 0}
|
|
272
|
+
{...badgeProps}
|
|
221
273
|
>
|
|
222
|
-
<
|
|
223
|
-
{...
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
274
|
+
<IconButton
|
|
275
|
+
{...(iconButtonRestProps as IconButtonProps)}
|
|
276
|
+
{...(iconButtonProps as IconButtonProps)}
|
|
277
|
+
>
|
|
278
|
+
<FilterIconSlot {...iconSlotProps} />
|
|
279
|
+
</IconButton>
|
|
280
|
+
</Badge>
|
|
281
|
+
</Box>
|
|
227
282
|
)}
|
|
228
283
|
>
|
|
229
|
-
{({ handleClose }: { handleClose: () => void }) => (
|
|
284
|
+
{({ handleClose }: { handleClose: (event?: any) => void }) => (
|
|
230
285
|
<Box
|
|
231
286
|
sx={{
|
|
232
287
|
p: 2,
|
|
233
288
|
minWidth: 400,
|
|
234
289
|
maxWidth: 600,
|
|
235
|
-
...
|
|
290
|
+
...(menuSx || {}),
|
|
236
291
|
}}
|
|
292
|
+
onClick={(e) => e.stopPropagation()}
|
|
237
293
|
>
|
|
238
294
|
<Typography
|
|
239
295
|
variant="subtitle2"
|
|
240
296
|
sx={{
|
|
241
297
|
mb: 1,
|
|
242
|
-
...
|
|
298
|
+
...(titleSx || {}),
|
|
243
299
|
}}
|
|
244
300
|
>
|
|
245
|
-
{
|
|
301
|
+
{title || "Column Filters"}
|
|
246
302
|
</Typography>
|
|
303
|
+
|
|
247
304
|
<Divider sx={{ mb: 2 }} />
|
|
248
305
|
|
|
249
|
-
{/* Filter Logic Selection */}
|
|
250
306
|
{filters.length > 1 && (
|
|
251
307
|
<Box sx={{ mb: 2 }}>
|
|
252
308
|
<FormControl size="small" sx={{ minWidth: 120 }}>
|
|
@@ -254,8 +310,8 @@ export function ColumnFilterControl(props: ColumnFilterControlProps = {}): React
|
|
|
254
310
|
<Select
|
|
255
311
|
value={filterLogic}
|
|
256
312
|
label="Logic"
|
|
257
|
-
onChange={(e) => handleLogicChange(e.target.value as
|
|
258
|
-
{...
|
|
313
|
+
onChange={(e) => handleLogicChange(e.target.value as "AND" | "OR")}
|
|
314
|
+
{...logicSelectProps}
|
|
259
315
|
>
|
|
260
316
|
<MenuItem value="AND">AND</MenuItem>
|
|
261
317
|
<MenuItem value="OR">OR</MenuItem>
|
|
@@ -264,26 +320,26 @@ export function ColumnFilterControl(props: ColumnFilterControlProps = {}): React
|
|
|
264
320
|
</Box>
|
|
265
321
|
)}
|
|
266
322
|
|
|
267
|
-
{/* Filter Rules */}
|
|
268
323
|
<Stack spacing={2} sx={{ mb: 2 }}>
|
|
269
324
|
{filters.map((filter) => {
|
|
270
|
-
const selectedColumn = filterableColumns
|
|
325
|
+
const selectedColumn = filterableColumns.find((col: any) => col.id === filter.columnId);
|
|
271
326
|
const operators = filter.columnId ? getOperatorsForColumn(filter.columnId) : [];
|
|
272
|
-
const needsValue = ![
|
|
327
|
+
const needsValue = !["isEmpty", "isNotEmpty"].includes(filter.operator);
|
|
273
328
|
|
|
274
329
|
return (
|
|
275
330
|
<Stack key={filter.id} direction="row" spacing={1} alignItems="center">
|
|
276
|
-
{/* Column Selection */}
|
|
277
331
|
<FormControl size="small" sx={{ minWidth: 120 }}>
|
|
278
332
|
<InputLabel>Column</InputLabel>
|
|
279
333
|
<Select
|
|
280
|
-
value={filter.columnId ||
|
|
334
|
+
value={filter.columnId || ""}
|
|
281
335
|
label="Column"
|
|
282
|
-
onChange={(e) =>
|
|
336
|
+
onChange={(e) =>
|
|
337
|
+
handleColumnChange(filter.id, e.target.value as string, filter)
|
|
338
|
+
}
|
|
283
339
|
>
|
|
284
|
-
{filterableColumns
|
|
340
|
+
{filterableColumns.map((column: any) => (
|
|
285
341
|
<MenuItem key={column.id} value={column.id}>
|
|
286
|
-
{typeof column.columnDef.header ===
|
|
342
|
+
{typeof column.columnDef.header === "string"
|
|
287
343
|
? column.columnDef.header
|
|
288
344
|
: column.id}
|
|
289
345
|
</MenuItem>
|
|
@@ -291,16 +347,17 @@ export function ColumnFilterControl(props: ColumnFilterControlProps = {}): React
|
|
|
291
347
|
</Select>
|
|
292
348
|
</FormControl>
|
|
293
349
|
|
|
294
|
-
{/* Operator Selection */}
|
|
295
350
|
<FormControl size="small" sx={{ minWidth: 120 }}>
|
|
296
351
|
<InputLabel>Operator</InputLabel>
|
|
297
352
|
<Select
|
|
298
|
-
value={filter.operator ||
|
|
353
|
+
value={filter.operator || ""}
|
|
299
354
|
label="Operator"
|
|
300
|
-
onChange={(e) =>
|
|
355
|
+
onChange={(e) =>
|
|
356
|
+
handleOperatorChange(filter.id, e.target.value as string, filter)
|
|
357
|
+
}
|
|
301
358
|
disabled={!filter.columnId}
|
|
302
359
|
>
|
|
303
|
-
{operators.map(op => (
|
|
360
|
+
{operators.map((op: any) => (
|
|
304
361
|
<MenuItem key={op.value} value={op.value}>
|
|
305
362
|
{op.label}
|
|
306
363
|
</MenuItem>
|
|
@@ -308,7 +365,6 @@ export function ColumnFilterControl(props: ColumnFilterControlProps = {}): React
|
|
|
308
365
|
</Select>
|
|
309
366
|
</FormControl>
|
|
310
367
|
|
|
311
|
-
{/* Value Input */}
|
|
312
368
|
{needsValue && selectedColumn && (
|
|
313
369
|
<FilterValueInput
|
|
314
370
|
filter={filter}
|
|
@@ -317,52 +373,54 @@ export function ColumnFilterControl(props: ColumnFilterControlProps = {}): React
|
|
|
317
373
|
/>
|
|
318
374
|
)}
|
|
319
375
|
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
</IconButton>
|
|
376
|
+
<IconButton
|
|
377
|
+
size="small"
|
|
378
|
+
onClick={() => removeFilter(filter.id)}
|
|
379
|
+
color="error"
|
|
380
|
+
{...deleteButtonProps}
|
|
381
|
+
>
|
|
382
|
+
<DeleteIcon fontSize="small" />
|
|
383
|
+
</IconButton>
|
|
329
384
|
</Stack>
|
|
330
385
|
);
|
|
331
386
|
})}
|
|
332
387
|
</Stack>
|
|
333
388
|
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
{/* Action Buttons */}
|
|
389
|
+
<Button
|
|
390
|
+
variant="outlined"
|
|
391
|
+
size="small"
|
|
392
|
+
startIcon={<AddIcon />}
|
|
393
|
+
onClick={() => addFilter()}
|
|
394
|
+
disabled={filterableColumns.length === 0}
|
|
395
|
+
sx={{ mb: 2 }}
|
|
396
|
+
{...addButtonProps}
|
|
397
|
+
>
|
|
398
|
+
Add Filter
|
|
399
|
+
</Button>
|
|
400
|
+
|
|
348
401
|
<Stack direction="row" spacing={1} justifyContent="flex-end">
|
|
349
402
|
{hasAppliedFilters && (
|
|
350
403
|
<Button
|
|
351
404
|
variant="outlined"
|
|
352
405
|
size="small"
|
|
353
|
-
onClick={() =>
|
|
406
|
+
onClick={(e) => {
|
|
407
|
+
e.preventDefault();
|
|
408
|
+
e.stopPropagation();
|
|
409
|
+
clearAllFilters(handleClose);
|
|
410
|
+
}}
|
|
354
411
|
color="error"
|
|
355
|
-
{...
|
|
412
|
+
{...clearButtonProps}
|
|
356
413
|
>
|
|
357
414
|
Clear All
|
|
358
415
|
</Button>
|
|
359
416
|
)}
|
|
417
|
+
|
|
360
418
|
<Button
|
|
361
419
|
variant="contained"
|
|
362
420
|
size="small"
|
|
363
|
-
onClick={() => handleApplyFilters(handleClose)}
|
|
421
|
+
onClick={() => handleApplyFilters(() => handleClose?.())}
|
|
364
422
|
disabled={!hasPendingChanges}
|
|
365
|
-
{...
|
|
423
|
+
{...applyButtonProps}
|
|
366
424
|
>
|
|
367
425
|
Apply
|
|
368
426
|
</Button>
|
|
@@ -371,4 +429,4 @@ export function ColumnFilterControl(props: ColumnFilterControlProps = {}): React
|
|
|
371
429
|
)}
|
|
372
430
|
</MenuDropdown>
|
|
373
431
|
);
|
|
374
|
-
}
|
|
432
|
+
}
|