react-restyle-components 0.4.50 → 0.4.51
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/lib/src/core-components/src/components/Table/Table.js +93 -18
- package/lib/src/core-components/src/components/Table/filters.d.ts +203 -10
- package/lib/src/core-components/src/components/Table/filters.js +319 -203
- package/lib/src/core-components/src/components/Table/index.d.ts +2 -2
- package/lib/src/core-components/src/components/Table/index.js +1 -1
- package/lib/src/core-components/src/components/Table/types.d.ts +10 -3
- package/lib/src/core-components/src/tc.global.css +18 -1
- package/lib/src/core-components/src/tc.module.css +3 -1
- package/package.json +1 -1
|
@@ -1,149 +1,28 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
3
|
import { useState, useCallback, useEffect, useRef } from 'react';
|
|
4
|
-
import { styled, css } from 'styled-components';
|
|
5
|
-
import { tokens } from '../../utils/designTokens';
|
|
6
4
|
import { useDebouncedValue } from '../../utils/hooks/useDebouncedValue';
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
height: 22px;
|
|
16
|
-
padding: 0 5px;
|
|
17
|
-
font-size: 10px;
|
|
18
|
-
font-weight: normal;
|
|
19
|
-
color: #000000;
|
|
20
|
-
border: 1px solid ${tokens.outline || '#e2e8f0'};
|
|
21
|
-
border-radius: 2px;
|
|
22
|
-
background: white;
|
|
23
|
-
transition: all 0.15s ease;
|
|
24
|
-
|
|
25
|
-
&:hover {
|
|
26
|
-
border-color: ${tokens.primary || '#94a3b8'};
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
&:focus {
|
|
30
|
-
outline: none;
|
|
31
|
-
border-color: ${tokens.primary || '#3b82f6'};
|
|
32
|
-
box-shadow: 0 0 0 1px
|
|
33
|
-
${tokens.primary ? `${tokens.primary}20` : '#3b82f620'};
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
&::placeholder {
|
|
37
|
-
color: ${tokens.onSurface ? `${tokens.onSurface}50` : '#94a3af'};
|
|
38
|
-
font-size: 9px;
|
|
39
|
-
}
|
|
40
|
-
`;
|
|
41
|
-
const FilterSelectBase = styled.select `
|
|
42
|
-
width: 100%;
|
|
43
|
-
height: 22px;
|
|
44
|
-
padding: 0 5px;
|
|
45
|
-
font-size: 10px;
|
|
46
|
-
font-weight: normal;
|
|
47
|
-
color: #000000;
|
|
48
|
-
border: 1px solid ${tokens.outline || '#e2e8f0'};
|
|
49
|
-
border-radius: 2px;
|
|
50
|
-
background: white;
|
|
51
|
-
cursor: pointer;
|
|
52
|
-
transition: all 0.15s ease;
|
|
53
|
-
|
|
54
|
-
&:hover {
|
|
55
|
-
border-color: ${tokens.primary || '#94a3b8'};
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
&:focus {
|
|
59
|
-
outline: none;
|
|
60
|
-
border-color: ${tokens.primary || '#3b82f6'};
|
|
61
|
-
box-shadow: 0 0 0 1px
|
|
62
|
-
${tokens.primary ? `${tokens.primary}20` : '#3b82f620'};
|
|
63
|
-
}
|
|
64
|
-
`;
|
|
65
|
-
const ComparatorSelect = styled.select `
|
|
66
|
-
width: 38px;
|
|
67
|
-
height: 22px;
|
|
68
|
-
padding: 0 2px;
|
|
69
|
-
font-size: 9px;
|
|
70
|
-
font-weight: normal;
|
|
71
|
-
border: 1px solid ${tokens.outline || '#e2e8f0'};
|
|
72
|
-
border-radius: 2px;
|
|
73
|
-
background: white;
|
|
74
|
-
cursor: pointer;
|
|
75
|
-
flex-shrink: 0;
|
|
76
|
-
text-align: center;
|
|
77
|
-
transition: all 0.15s ease;
|
|
78
|
-
|
|
79
|
-
&:hover {
|
|
80
|
-
border-color: ${tokens.primary || '#94a3b8'};
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
&:focus {
|
|
84
|
-
outline: none;
|
|
85
|
-
border-color: ${tokens.primary || '#3b82f6'};
|
|
86
|
-
}
|
|
87
|
-
`;
|
|
88
|
-
const DateInput = styled.input `
|
|
89
|
-
flex: 1;
|
|
90
|
-
height: 22px;
|
|
91
|
-
padding: 0 4px;
|
|
92
|
-
font-size: 10px;
|
|
93
|
-
border: 1px solid ${tokens.outline || '#e2e8f0'};
|
|
94
|
-
border-radius: 2px;
|
|
95
|
-
background: white;
|
|
96
|
-
min-width: 80px;
|
|
97
|
-
transition: all 0.15s ease;
|
|
98
|
-
|
|
99
|
-
&:hover {
|
|
100
|
-
border-color: ${tokens.primary || '#94a3b8'};
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
&:focus {
|
|
104
|
-
outline: none;
|
|
105
|
-
border-color: ${tokens.primary || '#3b82f6'};
|
|
106
|
-
box-shadow: 0 0 0 1px
|
|
107
|
-
${tokens.primary ? `${tokens.primary}20` : '#3b82f620'};
|
|
108
|
-
}
|
|
109
|
-
`;
|
|
110
|
-
const ToggleButton = styled.button `
|
|
111
|
-
height: 22px;
|
|
112
|
-
padding: 0 5px;
|
|
113
|
-
font-size: 9px;
|
|
114
|
-
font-weight: normal;
|
|
115
|
-
border: 1px solid ${tokens.outline || '#e2e8f0'};
|
|
116
|
-
border-radius: 2px;
|
|
117
|
-
cursor: pointer;
|
|
118
|
-
transition: all 0.15s ease;
|
|
119
|
-
white-space: nowrap;
|
|
120
|
-
|
|
121
|
-
${({ $active }) => $active
|
|
122
|
-
? css `
|
|
123
|
-
background: ${tokens.primary || '#3b82f6'};
|
|
124
|
-
color: white;
|
|
125
|
-
border-color: ${tokens.primary || '#3b82f6'};
|
|
126
|
-
`
|
|
127
|
-
: css `
|
|
128
|
-
background: white;
|
|
129
|
-
color: ${tokens.onSurface || '#374151'};
|
|
130
|
-
|
|
131
|
-
&:hover {
|
|
132
|
-
background: #f8fafc;
|
|
133
|
-
border-color: ${tokens.primary || '#94a3b8'};
|
|
134
|
-
}
|
|
135
|
-
`}
|
|
136
|
-
`;
|
|
5
|
+
import s from '../../tc.module.css';
|
|
6
|
+
import { cn } from '../../utils';
|
|
7
|
+
// Tailwind CSS class helpers for filter components
|
|
8
|
+
const filterInputClass = cn(s['leading-4'], s['p-1'], s['focus:outline-none'], s['focus:ring'], s['shadow-sm'], s['text-xs'], s['font-normal'], s['border'], s['border-gray-300'], s['rounded-md'], s['text-black'], s['w-full'], s['bg-white'], s['placeholder-gray-400'], s['transition-all']);
|
|
9
|
+
const filterSelectClass = cn(s['leading-4'], s['p-1'], s['focus:outline-none'], s['focus:ring'], s['shadow-sm'], s['text-xs'], s['font-normal'], s['border'], s['border-gray-300'], s['rounded-md'], s['text-black'], s['w-full'], s['bg-white'], s['cursor-pointer'], s['transition-all']);
|
|
10
|
+
const comparatorSelectClass = cn(s['leading-4'], s['p-1'], s['focus:outline-none'], s['focus:ring'], s['shadow-sm'], s['text-xs'], s['font-normal'], s['border'], s['border-gray-300'], s['rounded-md'], s['text-black'], s['bg-white'], s['cursor-pointer'], s['w-12']);
|
|
11
|
+
const dateInputClass = cn(s['leading-4'], s['p-1'], s['focus:outline-none'], s['focus:ring'], s['shadow-sm'], s['text-xs'], s['font-normal'], s['border'], s['border-gray-300'], s['rounded-md'], s['text-black'], s['bg-white'], s['flex-1'], s['transition-all']);
|
|
12
|
+
const filterContainerClass = cn(s['flex'], s['flex-row'], s['gap-1'], s['items-center'], s['w-full']);
|
|
137
13
|
/**
|
|
138
14
|
* Internal Text filter component with options support
|
|
139
15
|
*/
|
|
140
|
-
const TextFilterComponent = ({ column, value, onChange, options }) => {
|
|
141
|
-
const { placeholder, className, style, defaultValue, delay = 500, getFilter, onFilter, id, disabled, } = options || {};
|
|
16
|
+
const TextFilterComponent = ({ column, value, onChange, onFilter: onFilterProp, options }) => {
|
|
17
|
+
const { placeholder, className, style, defaultValue, delay = 500, getFilter, onFilter: onFilterOption, id, disabled, } = options || {};
|
|
18
|
+
// Support both onChange and onFilter props (onFilter is alias for onChange)
|
|
19
|
+
const onChangeCallback = onChange || onFilterProp || (() => { });
|
|
20
|
+
const onFilter = onFilterOption;
|
|
142
21
|
// Local state for immediate input updates (maintains focus)
|
|
143
22
|
const [internalValue, setInternalValue] = useState(value || defaultValue || '');
|
|
144
23
|
const internalValueRef = useRef(internalValue);
|
|
145
24
|
const inputRef = useRef(null);
|
|
146
|
-
const onChangeRef = useRef(
|
|
25
|
+
const onChangeRef = useRef(onChangeCallback);
|
|
147
26
|
const onFilterRef = useRef(onFilter);
|
|
148
27
|
// Track if the last change was from user input (internal) vs external (e.g., clear all)
|
|
149
28
|
const lastInternalValueRef = useRef(internalValue);
|
|
@@ -152,9 +31,9 @@ const TextFilterComponent = ({ column, value, onChange, options }) => {
|
|
|
152
31
|
internalValueRef.current = internalValue;
|
|
153
32
|
}, [internalValue]);
|
|
154
33
|
useEffect(() => {
|
|
155
|
-
onChangeRef.current =
|
|
34
|
+
onChangeRef.current = onChangeCallback;
|
|
156
35
|
onFilterRef.current = onFilter;
|
|
157
|
-
}, [
|
|
36
|
+
}, [onChangeCallback, onFilter]);
|
|
158
37
|
// Sync internal value when external value changes (e.g., from clear all filters)
|
|
159
38
|
// Only sync when external value differs from what we last sent to parent
|
|
160
39
|
useEffect(() => {
|
|
@@ -220,19 +99,7 @@ const TextFilterComponent = ({ column, value, onChange, options }) => {
|
|
|
220
99
|
});
|
|
221
100
|
}
|
|
222
101
|
});
|
|
223
|
-
|
|
224
|
-
fontWeight: 400,
|
|
225
|
-
...style,
|
|
226
|
-
};
|
|
227
|
-
// If custom className is provided, use plain input to allow full CSS control
|
|
228
|
-
if (className) {
|
|
229
|
-
return (_jsx("input", { ref: inputRef, type: "text", id: id, "data-filter-field": column.dataField, value: internalValue, onChange: handleChange, onFocus: handleFocus, onBlur: handleBlur, placeholder: placeholder || column.filterPlaceholder || `Filter ${column.text}...`, className: className, style: {
|
|
230
|
-
width: '100%',
|
|
231
|
-
fontWeight: 400,
|
|
232
|
-
...style,
|
|
233
|
-
}, disabled: disabled }));
|
|
234
|
-
}
|
|
235
|
-
return (_jsx(FilterInputBase, { ref: inputRef, type: "text", id: id, "data-filter-field": column.dataField, value: internalValue, onChange: handleChange, onFocus: handleFocus, onBlur: handleBlur, placeholder: placeholder || column.filterPlaceholder || `Filter ${column.text}...`, style: inputStyle, disabled: disabled }));
|
|
102
|
+
return (_jsx("input", { ref: inputRef, type: "text", id: id, "data-filter-field": column.dataField, value: internalValue, onChange: handleChange, onFocus: handleFocus, onBlur: handleBlur, placeholder: placeholder || column.filterPlaceholder || `Filter ${column.text}...`, className: className || filterInputClass, style: style, disabled: disabled }));
|
|
236
103
|
};
|
|
237
104
|
export function TextFilter(optionsOrProps) {
|
|
238
105
|
// Check if it's being used as a factory function (options object without column/value/onChange)
|
|
@@ -243,6 +110,9 @@ export function TextFilter(optionsOrProps) {
|
|
|
243
110
|
const options = optionsOrProps;
|
|
244
111
|
const FilterWithOptions = (props) => (_jsx(TextFilterComponent, { ...props, options: options }));
|
|
245
112
|
FilterWithOptions.displayName = 'TextFilter';
|
|
113
|
+
// Attach options as props for backwards compatibility
|
|
114
|
+
// This allows accessing: column.filter.props.getFilter
|
|
115
|
+
FilterWithOptions.props = options;
|
|
246
116
|
return FilterWithOptions;
|
|
247
117
|
}
|
|
248
118
|
// Direct component usage
|
|
@@ -252,15 +122,18 @@ export function TextFilter(optionsOrProps) {
|
|
|
252
122
|
/**
|
|
253
123
|
* Internal Number filter component with options support
|
|
254
124
|
*/
|
|
255
|
-
const NumberFilterComponent = ({ column, value, onChange, options }) => {
|
|
256
|
-
const { placeholder, className, style, defaultValue, delay = 500, defaultComparator = '=', allowDecimal = true, getFilter, onFilter, id, disabled, hideComparator, comparators = ['=', '!=', '>', '>=', '<', '<='], } = options || {};
|
|
125
|
+
const NumberFilterComponent = ({ column, value, onChange, onFilter: onFilterProp, options }) => {
|
|
126
|
+
const { placeholder, className, style, defaultValue, delay = 500, defaultComparator = '=', allowDecimal = true, getFilter, onFilter: onFilterOption, id, disabled, hideComparator, comparators = ['=', '!=', '>', '>=', '<', '<='], } = options || {};
|
|
127
|
+
// Support both onChange and onFilter props (onFilter is alias for onChange)
|
|
128
|
+
const onChangeCallback = onChange || onFilterProp || (() => { });
|
|
129
|
+
const onFilter = onFilterOption;
|
|
257
130
|
// Local state for immediate input updates (maintains focus)
|
|
258
131
|
const [number, setNumber] = useState(value?.number || defaultValue?.number || '');
|
|
259
132
|
const [comparator, setComparator] = useState(value?.comparator || defaultValue?.comparator || defaultComparator);
|
|
260
133
|
const numberRef = useRef(number);
|
|
261
134
|
const comparatorRef = useRef(comparator);
|
|
262
135
|
const inputRef = useRef(null);
|
|
263
|
-
const onChangeRef = useRef(
|
|
136
|
+
const onChangeRef = useRef(onChangeCallback);
|
|
264
137
|
const onFilterRef = useRef(onFilter);
|
|
265
138
|
// Track last value sent to parent to prevent sync loops
|
|
266
139
|
const lastNumberRef = useRef(number);
|
|
@@ -271,9 +144,9 @@ const NumberFilterComponent = ({ column, value, onChange, options }) => {
|
|
|
271
144
|
comparatorRef.current = comparator;
|
|
272
145
|
}, [number, comparator]);
|
|
273
146
|
useEffect(() => {
|
|
274
|
-
onChangeRef.current =
|
|
147
|
+
onChangeRef.current = onChangeCallback;
|
|
275
148
|
onFilterRef.current = onFilter;
|
|
276
|
-
}, [
|
|
149
|
+
}, [onChangeCallback, onFilter]);
|
|
277
150
|
// Sync internal value when external value changes (e.g., from clear all filters)
|
|
278
151
|
useEffect(() => {
|
|
279
152
|
const externalNumber = value?.number || '';
|
|
@@ -299,7 +172,8 @@ const NumberFilterComponent = ({ column, value, onChange, options }) => {
|
|
|
299
172
|
? { number: debouncedNumber, comparator: comparatorRef.current }
|
|
300
173
|
: null;
|
|
301
174
|
onChangeRef.current(newValue);
|
|
302
|
-
|
|
175
|
+
// onFilter passes string value directly
|
|
176
|
+
onFilterRef.current?.(debouncedNumber || null);
|
|
303
177
|
}, [debouncedNumber]);
|
|
304
178
|
// Provide filter instance via getFilter callback - only on mount
|
|
305
179
|
useEffect(() => {
|
|
@@ -311,11 +185,13 @@ const NumberFilterComponent = ({ column, value, onChange, options }) => {
|
|
|
311
185
|
: null;
|
|
312
186
|
},
|
|
313
187
|
setValue: (newValue) => {
|
|
314
|
-
if (newValue) {
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
188
|
+
if (newValue !== null && newValue !== undefined) {
|
|
189
|
+
const num = typeof newValue === 'object' ? newValue.number : String(newValue);
|
|
190
|
+
const comp = typeof newValue === 'object' ? newValue.comparator : comparatorRef.current;
|
|
191
|
+
setNumber(num);
|
|
192
|
+
setComparator(comp);
|
|
193
|
+
onChangeRef.current({ number: num, comparator: comp });
|
|
194
|
+
onFilterRef.current?.(num);
|
|
319
195
|
}
|
|
320
196
|
else {
|
|
321
197
|
setNumber('');
|
|
@@ -356,9 +232,8 @@ const NumberFilterComponent = ({ column, value, onChange, options }) => {
|
|
|
356
232
|
const handleComparatorChange = useCallback((newComparator) => {
|
|
357
233
|
setComparator(newComparator);
|
|
358
234
|
if (number) {
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
onFilterRef.current?.(newValue);
|
|
235
|
+
onChangeRef.current({ number, comparator: newComparator });
|
|
236
|
+
onFilterRef.current?.(number);
|
|
362
237
|
}
|
|
363
238
|
}, [number]);
|
|
364
239
|
const comparatorSymbols = {
|
|
@@ -369,29 +244,19 @@ const NumberFilterComponent = ({ column, value, onChange, options }) => {
|
|
|
369
244
|
'<': '<',
|
|
370
245
|
'<=': '≤',
|
|
371
246
|
};
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
const
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
if (pattern.test(val)) {
|
|
386
|
-
setNumber(val);
|
|
387
|
-
}
|
|
388
|
-
},
|
|
389
|
-
onFocus: handleFocus,
|
|
390
|
-
onBlur: handleBlur,
|
|
391
|
-
placeholder: placeholder || column.filterPlaceholder || 'Number...',
|
|
392
|
-
disabled,
|
|
393
|
-
};
|
|
394
|
-
return (_jsxs(FilterContainer, { children: [!hideComparator && (_jsx(ComparatorSelect, { value: comparator, onChange: (e) => handleComparatorChange(e.target.value), disabled: disabled, children: comparators.map((comp) => (_jsx("option", { value: comp, children: comparatorSymbols[comp] || comp }, comp))) })), className ? (_jsx("input", { ref: inputRef, ...inputProps, className: className, style: { flex: 1, width: '100%', fontWeight: 400, ...style } })) : (_jsx(FilterInputBase, { ref: inputRef, ...inputProps, style: inputStyle }))] }));
|
|
247
|
+
// Resolve placeholder: own options > column.filter.props.placeholder > column.filterPlaceholder > default
|
|
248
|
+
const resolvedPlaceholder = placeholder ||
|
|
249
|
+
column?.filter?.props?.placeholder ||
|
|
250
|
+
column.filterPlaceholder ||
|
|
251
|
+
'Number...';
|
|
252
|
+
const handleInputChange = useCallback((e) => {
|
|
253
|
+
const val = e.target.value;
|
|
254
|
+
const pattern = allowDecimal ? /^[0-9.,]*$/ : /^[0-9]*$/;
|
|
255
|
+
if (pattern.test(val)) {
|
|
256
|
+
setNumber(val);
|
|
257
|
+
}
|
|
258
|
+
}, [allowDecimal]);
|
|
259
|
+
return (_jsxs("div", { className: filterContainerClass, children: [!hideComparator && (_jsx("select", { value: comparator, onChange: (e) => handleComparatorChange(e.target.value), disabled: disabled, className: className || comparatorSelectClass, children: comparators.map((comp) => (_jsx("option", { value: comp, children: comparatorSymbols[comp] || comp }, comp))) })), _jsx("input", { ref: inputRef, type: "text", id: id, "data-filter-field": column.dataField, value: number, onChange: handleInputChange, onFocus: handleFocus, onBlur: handleBlur, placeholder: resolvedPlaceholder, className: className || filterInputClass, style: style, disabled: disabled })] }));
|
|
395
260
|
};
|
|
396
261
|
export function NumberFilter(optionsOrProps) {
|
|
397
262
|
if (!('column' in optionsOrProps) &&
|
|
@@ -400,6 +265,8 @@ export function NumberFilter(optionsOrProps) {
|
|
|
400
265
|
const options = optionsOrProps;
|
|
401
266
|
const FilterWithOptions = (props) => (_jsx(NumberFilterComponent, { ...props, options: options }));
|
|
402
267
|
FilterWithOptions.displayName = 'NumberFilter';
|
|
268
|
+
// Attach options as props for backwards compatibility
|
|
269
|
+
FilterWithOptions.props = options;
|
|
403
270
|
return FilterWithOptions;
|
|
404
271
|
}
|
|
405
272
|
const props = optionsOrProps;
|
|
@@ -408,15 +275,18 @@ export function NumberFilter(optionsOrProps) {
|
|
|
408
275
|
/**
|
|
409
276
|
* Internal Date filter component with options support
|
|
410
277
|
*/
|
|
411
|
-
const DateFilterComponent = ({ column, value, onChange, options }) => {
|
|
412
|
-
const { className, style, defaultValue, defaultComparator = '=', defaultRangeMode = false, getFilter, onFilter, id, disabled, minDate, maxDate, } = options || {};
|
|
278
|
+
const DateFilterComponent = ({ column, value, onChange, onFilter: onFilterProp, options }) => {
|
|
279
|
+
const { className, style, defaultValue, defaultComparator = '=', defaultRangeMode = false, getFilter, onFilter: onFilterOption, id, disabled, minDate, maxDate, } = options || {};
|
|
280
|
+
// Support both onChange and onFilter props (onFilter is alias for onChange)
|
|
281
|
+
const handleChange = onChange || onFilterProp || (() => { });
|
|
282
|
+
const onFilter = onFilterOption;
|
|
413
283
|
// Local state for immediate updates
|
|
414
284
|
const [startDate, setStartDate] = useState(value?.startDate || defaultValue?.startDate || '');
|
|
415
285
|
const [endDate, setEndDate] = useState(value?.endDate || defaultValue?.endDate || '');
|
|
416
286
|
const [diffFlag, setDiffFlag] = useState(value?.diffFlag ?? defaultValue?.diffFlag ?? defaultRangeMode);
|
|
417
287
|
const [comparator, setComparator] = useState(value?.comparator || defaultValue?.comparator || defaultComparator);
|
|
418
288
|
const stateRef = useRef({ startDate, endDate, diffFlag, comparator });
|
|
419
|
-
const onChangeRef = useRef(
|
|
289
|
+
const onChangeRef = useRef(handleChange);
|
|
420
290
|
const onFilterRef = useRef(onFilter);
|
|
421
291
|
// Track last values sent to parent to prevent sync loops
|
|
422
292
|
const lastValuesRef = useRef({
|
|
@@ -430,9 +300,9 @@ const DateFilterComponent = ({ column, value, onChange, options }) => {
|
|
|
430
300
|
stateRef.current = { startDate, endDate, diffFlag, comparator };
|
|
431
301
|
}, [startDate, endDate, diffFlag, comparator]);
|
|
432
302
|
useEffect(() => {
|
|
433
|
-
onChangeRef.current =
|
|
303
|
+
onChangeRef.current = handleChange;
|
|
434
304
|
onFilterRef.current = onFilter;
|
|
435
|
-
}, [
|
|
305
|
+
}, [handleChange, onFilter]);
|
|
436
306
|
// Sync internal value when external value changes (e.g., from clear all filters)
|
|
437
307
|
useEffect(() => {
|
|
438
308
|
const externalStartDate = value?.startDate || '';
|
|
@@ -531,7 +401,25 @@ const DateFilterComponent = ({ column, value, onChange, options }) => {
|
|
|
531
401
|
const handleComparatorChange = useCallback((e) => {
|
|
532
402
|
setComparator(e.target.value);
|
|
533
403
|
}, []);
|
|
534
|
-
|
|
404
|
+
// Show/hide the expanded filter UI - initially collapsed (search icon only)
|
|
405
|
+
const [isExpanded, setIsExpanded] = useState(false);
|
|
406
|
+
// Collapsed view: show column text + search icon
|
|
407
|
+
if (!isExpanded) {
|
|
408
|
+
return (_jsxs("div", { className: cn(s['flex'], s['flex-row'], s['gap-2'], s['items-center'], s['cursor-pointer']), onClick: () => setIsExpanded(true), title: "Click to open date filter", children: [_jsx("span", { className: cn(s['text-white'], s['text-xs'], s['font-normal']), children: column.text }), _jsxs("svg", { xmlns: "http://www.w3.org/2000/svg", width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", style: { color: 'white', cursor: 'pointer', flexShrink: 0 }, children: [_jsx("circle", { cx: "11", cy: "11", r: "8" }), _jsx("line", { x1: "21", y1: "21", x2: "16.65", y2: "16.65" })] })] }));
|
|
409
|
+
}
|
|
410
|
+
// Expanded view: full date filter UI
|
|
411
|
+
return (_jsxs("div", { className: cn(filterContainerClass, className), style: style, children: [_jsx("button", { type: "button", onClick: handleDiffFlagChange, title: "Date range mode", disabled: disabled, className: cn(s['text-xs'], s['font-normal'], s['px-2'], s['py-1'], s['border'], s['rounded-md'], s['cursor-pointer'], s['whitespace-nowrap'], s['transition-all'], s['focus:outline-none'], diffFlag
|
|
412
|
+
? cn(s['bg-blue-500'], s['text-white'], s['border-blue-500'])
|
|
413
|
+
: cn(s['bg-white'], s['text-black'], s['border-gray-300'], s['hover:bg-gray-50'])), children: diffFlag ? 'Range' : 'Single' }), !diffFlag && (_jsxs("select", { value: comparator, onChange: handleComparatorChange, disabled: disabled, className: comparatorSelectClass, children: [_jsx("option", { value: "=", children: "=" }), _jsx("option", { value: ">=", children: "\u2265" }), _jsx("option", { value: "<", children: "<" })] })), _jsx("input", { type: "date", id: id, value: startDate, onChange: handleStartDateChange, disabled: disabled, min: minDate, max: maxDate, className: dateInputClass }), diffFlag && (_jsxs(_Fragment, { children: [_jsx("span", { className: cn(s['text-xs'], s['text-gray-500'], s['font-normal']), children: "to" }), _jsx("input", { type: "date", value: endDate, onChange: handleEndDateChange, disabled: disabled, min: minDate, max: maxDate, className: dateInputClass })] })), _jsx("button", { type: "button", onClick: () => {
|
|
414
|
+
setIsExpanded(false);
|
|
415
|
+
// Clear filter when collapsing
|
|
416
|
+
setStartDate('');
|
|
417
|
+
setEndDate('');
|
|
418
|
+
setDiffFlag(defaultRangeMode);
|
|
419
|
+
setComparator(defaultComparator);
|
|
420
|
+
onChangeRef.current(null);
|
|
421
|
+
onFilterRef.current?.(null);
|
|
422
|
+
}, title: "Close date filter", className: cn(s['text-xs'], s['p-1'], s['cursor-pointer'], s['text-gray-400'], s['hover:text-red-500'], s['focus:outline-none'], s['transition-all']), children: _jsxs("svg", { xmlns: "http://www.w3.org/2000/svg", width: "12", height: "12", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [_jsx("line", { x1: "18", y1: "6", x2: "6", y2: "18" }), _jsx("line", { x1: "6", y1: "6", x2: "18", y2: "18" })] }) })] }));
|
|
535
423
|
};
|
|
536
424
|
export function DateFilter(optionsOrProps) {
|
|
537
425
|
if (!('column' in optionsOrProps) &&
|
|
@@ -540,6 +428,8 @@ export function DateFilter(optionsOrProps) {
|
|
|
540
428
|
const options = optionsOrProps;
|
|
541
429
|
const FilterWithOptions = (props) => (_jsx(DateFilterComponent, { ...props, options: options }));
|
|
542
430
|
FilterWithOptions.displayName = 'DateFilter';
|
|
431
|
+
// Attach options as props for backwards compatibility
|
|
432
|
+
FilterWithOptions.props = options;
|
|
543
433
|
return FilterWithOptions;
|
|
544
434
|
}
|
|
545
435
|
const props = optionsOrProps;
|
|
@@ -548,12 +438,15 @@ export function DateFilter(optionsOrProps) {
|
|
|
548
438
|
/**
|
|
549
439
|
* Internal Select filter component with options support
|
|
550
440
|
*/
|
|
551
|
-
const SelectFilterComponent = ({ column, value, onChange, options }) => {
|
|
552
|
-
const { placeholder = 'All', className, style, defaultValue, delay = 300, options: customOptions, getFilter, onFilter, id, disabled, } = options || {};
|
|
441
|
+
const SelectFilterComponent = ({ column, value, onChange, onFilter: onFilterProp, options }) => {
|
|
442
|
+
const { placeholder = 'All', className, style, defaultValue, delay = 300, options: customOptions, getFilter, onFilter: onFilterOption, id, disabled, } = options || {};
|
|
443
|
+
// Support both onChange and onFilter props (onFilter is alias for onChange)
|
|
444
|
+
const onChangeCallback = onChange || onFilterProp || (() => { });
|
|
445
|
+
const onFilter = onFilterOption;
|
|
553
446
|
// Local state for immediate updates
|
|
554
447
|
const [selectedValue, setSelectedValue] = useState(value || defaultValue || '');
|
|
555
448
|
const selectedValueRef = useRef(selectedValue);
|
|
556
|
-
const onChangeRef = useRef(
|
|
449
|
+
const onChangeRef = useRef(onChangeCallback);
|
|
557
450
|
const onFilterRef = useRef(onFilter);
|
|
558
451
|
// Track last value sent to parent to prevent sync loops
|
|
559
452
|
const lastValueRef = useRef(selectedValue);
|
|
@@ -562,9 +455,9 @@ const SelectFilterComponent = ({ column, value, onChange, options }) => {
|
|
|
562
455
|
selectedValueRef.current = selectedValue;
|
|
563
456
|
}, [selectedValue]);
|
|
564
457
|
useEffect(() => {
|
|
565
|
-
onChangeRef.current =
|
|
458
|
+
onChangeRef.current = onChangeCallback;
|
|
566
459
|
onFilterRef.current = onFilter;
|
|
567
|
-
}, [
|
|
460
|
+
}, [onChangeCallback, onFilter]);
|
|
568
461
|
// Sync internal value when external value changes (e.g., from clear all filters)
|
|
569
462
|
useEffect(() => {
|
|
570
463
|
const externalValue = value || '';
|
|
@@ -611,11 +504,7 @@ const SelectFilterComponent = ({ column, value, onChange, options }) => {
|
|
|
611
504
|
// Use custom options if provided, otherwise fall back to column.filterOptions
|
|
612
505
|
const selectOptions = customOptions || column.filterOptions || [];
|
|
613
506
|
const selectContent = (_jsxs(_Fragment, { children: [_jsx("option", { value: "", children: placeholder }), selectOptions.map((opt) => (_jsx("option", { value: opt.value, children: opt.label }, opt.value)))] }));
|
|
614
|
-
|
|
615
|
-
if (className) {
|
|
616
|
-
return (_jsx("select", { id: id, value: selectedValue, onChange: handleChange, className: className, style: { width: '100%', fontWeight: 400, ...style }, disabled: disabled, children: selectContent }));
|
|
617
|
-
}
|
|
618
|
-
return (_jsx(FilterSelectBase, { id: id, value: selectedValue, onChange: handleChange, style: { fontWeight: 400, ...style }, disabled: disabled, children: selectContent }));
|
|
507
|
+
return (_jsx("select", { id: id, value: selectedValue, onChange: handleChange, className: className || filterSelectClass, style: style, disabled: disabled, children: selectContent }));
|
|
619
508
|
};
|
|
620
509
|
export function SelectFilter(optionsOrProps) {
|
|
621
510
|
if (!('column' in optionsOrProps) &&
|
|
@@ -624,11 +513,234 @@ export function SelectFilter(optionsOrProps) {
|
|
|
624
513
|
const options = optionsOrProps;
|
|
625
514
|
const FilterWithOptions = (props) => (_jsx(SelectFilterComponent, { ...props, options: options }));
|
|
626
515
|
FilterWithOptions.displayName = 'SelectFilter';
|
|
516
|
+
// Attach options as props for backwards compatibility
|
|
517
|
+
FilterWithOptions.props = options;
|
|
627
518
|
return FilterWithOptions;
|
|
628
519
|
}
|
|
629
520
|
const props = optionsOrProps;
|
|
630
521
|
return _jsx(SelectFilterComponent, { ...props });
|
|
631
522
|
}
|
|
523
|
+
/**
|
|
524
|
+
* Internal Custom filter component with options support
|
|
525
|
+
*/
|
|
526
|
+
const CustomFilterComponent = ({ column, value, onChange, onFilter: onFilterProp, options }) => {
|
|
527
|
+
const { render, placeholder, className, style, defaultValue, delay = 300, getFilter, onFilter: onFilterOption, id, disabled, } = options;
|
|
528
|
+
// Support both onChange and onFilter props (onFilter is alias for onChange)
|
|
529
|
+
const onChangeCallback = onChange || onFilterProp || (() => { });
|
|
530
|
+
const onFilter = onFilterOption;
|
|
531
|
+
// Local state for immediate updates
|
|
532
|
+
const [filterValue, setFilterValue] = useState(value ?? defaultValue ?? null);
|
|
533
|
+
const filterValueRef = useRef(filterValue);
|
|
534
|
+
const onChangeRef = useRef(onChangeCallback);
|
|
535
|
+
const onFilterRef = useRef(onFilter);
|
|
536
|
+
// Track last value sent to parent to prevent sync loops
|
|
537
|
+
const lastValueRef = useRef(filterValue);
|
|
538
|
+
// Keep refs in sync with state
|
|
539
|
+
useEffect(() => {
|
|
540
|
+
filterValueRef.current = filterValue;
|
|
541
|
+
}, [filterValue]);
|
|
542
|
+
useEffect(() => {
|
|
543
|
+
onChangeRef.current = onChangeCallback;
|
|
544
|
+
onFilterRef.current = onFilter;
|
|
545
|
+
}, [onChangeCallback, onFilter]);
|
|
546
|
+
// Sync internal value when external value changes (e.g., from clear all filters)
|
|
547
|
+
useEffect(() => {
|
|
548
|
+
const externalValue = value ?? null;
|
|
549
|
+
// Only sync if different from what we last sent to parent
|
|
550
|
+
if (JSON.stringify(externalValue) !== JSON.stringify(lastValueRef.current)) {
|
|
551
|
+
setFilterValue(externalValue);
|
|
552
|
+
lastValueRef.current = externalValue;
|
|
553
|
+
}
|
|
554
|
+
}, [value]);
|
|
555
|
+
// Debounce the filter value
|
|
556
|
+
const [debouncedValue] = useDebouncedValue(filterValue, { wait: delay });
|
|
557
|
+
// Propagate debounced value to parent
|
|
558
|
+
useEffect(() => {
|
|
559
|
+
// Update ref to track what we're sending to parent
|
|
560
|
+
lastValueRef.current = debouncedValue;
|
|
561
|
+
onChangeRef.current(debouncedValue);
|
|
562
|
+
onFilterRef.current?.(debouncedValue);
|
|
563
|
+
}, [debouncedValue]);
|
|
564
|
+
// Provide filter instance via getFilter callback - only on mount
|
|
565
|
+
useEffect(() => {
|
|
566
|
+
if (getFilter) {
|
|
567
|
+
getFilter({
|
|
568
|
+
get value() {
|
|
569
|
+
return filterValueRef.current;
|
|
570
|
+
},
|
|
571
|
+
setValue: (newValue) => {
|
|
572
|
+
setFilterValue(newValue);
|
|
573
|
+
onChangeRef.current(newValue);
|
|
574
|
+
onFilterRef.current?.(newValue);
|
|
575
|
+
},
|
|
576
|
+
clear: () => {
|
|
577
|
+
setFilterValue(null);
|
|
578
|
+
onChangeRef.current(null);
|
|
579
|
+
onFilterRef.current?.(null);
|
|
580
|
+
},
|
|
581
|
+
});
|
|
582
|
+
}
|
|
583
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
584
|
+
}, [getFilter]);
|
|
585
|
+
// Handler for value changes from custom render
|
|
586
|
+
const handleChange = useCallback((newValue) => {
|
|
587
|
+
setFilterValue(newValue);
|
|
588
|
+
}, []);
|
|
589
|
+
// Clear handler
|
|
590
|
+
const handleClear = useCallback(() => {
|
|
591
|
+
setFilterValue(null);
|
|
592
|
+
}, []);
|
|
593
|
+
// Build render props
|
|
594
|
+
const renderProps = {
|
|
595
|
+
value: filterValue,
|
|
596
|
+
onChange: handleChange,
|
|
597
|
+
column,
|
|
598
|
+
clear: handleClear,
|
|
599
|
+
placeholder,
|
|
600
|
+
className,
|
|
601
|
+
style,
|
|
602
|
+
id,
|
|
603
|
+
disabled,
|
|
604
|
+
};
|
|
605
|
+
// If no render function provided, render a default text input
|
|
606
|
+
// This allows using CustomFilter with column.filterRenderer
|
|
607
|
+
if (!render) {
|
|
608
|
+
return (_jsx("input", { type: "text", id: id, "data-filter-field": column.dataField, value: filterValue || '', onChange: (e) => handleChange(e.target.value || null), placeholder: placeholder || column.filterPlaceholder || `Filter ${column.text}...`, className: className || filterInputClass, style: style, disabled: disabled }));
|
|
609
|
+
}
|
|
610
|
+
// Render the custom filter UI
|
|
611
|
+
return _jsx(_Fragment, { children: render(renderProps) });
|
|
612
|
+
};
|
|
613
|
+
/**
|
|
614
|
+
* Custom filter - allows rendering any custom filter component
|
|
615
|
+
*
|
|
616
|
+
* @example
|
|
617
|
+
* // Basic custom filter with input
|
|
618
|
+
* filter: CustomFilter({
|
|
619
|
+
* render: ({ value, onChange }) => (
|
|
620
|
+
* <input
|
|
621
|
+
* type="text"
|
|
622
|
+
* value={value || ''}
|
|
623
|
+
* onChange={(e) => onChange(e.target.value || null)}
|
|
624
|
+
* placeholder="Custom filter..."
|
|
625
|
+
* />
|
|
626
|
+
* ),
|
|
627
|
+
* })
|
|
628
|
+
*
|
|
629
|
+
* @example
|
|
630
|
+
* // Using render props (placeholder, className, etc.)
|
|
631
|
+
* filter: CustomFilter({
|
|
632
|
+
* placeholder: 'Search...',
|
|
633
|
+
* className: 'my-custom-input',
|
|
634
|
+
* render: ({ value, onChange, placeholder, className }) => (
|
|
635
|
+
* <input
|
|
636
|
+
* type="text"
|
|
637
|
+
* value={value || ''}
|
|
638
|
+
* onChange={(e) => onChange(e.target.value || null)}
|
|
639
|
+
* placeholder={placeholder}
|
|
640
|
+
* className={className}
|
|
641
|
+
* />
|
|
642
|
+
* ),
|
|
643
|
+
* })
|
|
644
|
+
*
|
|
645
|
+
* @example
|
|
646
|
+
* // Using with column.filterRenderer (simple options pattern)
|
|
647
|
+
* // This allows using existing filter components with custom configuration
|
|
648
|
+
* {
|
|
649
|
+
* dataField: 'picture',
|
|
650
|
+
* text: 'Picture',
|
|
651
|
+
* filter: CustomFilter({
|
|
652
|
+
* placeholder: 'Picture',
|
|
653
|
+
* getFilter: (filter) => {
|
|
654
|
+
* pictureFilterRef.current = filter;
|
|
655
|
+
* },
|
|
656
|
+
* }),
|
|
657
|
+
* filterRenderer: (onFilter, column) => (
|
|
658
|
+
* <NumberFilter onFilter={onFilter} column={column} />
|
|
659
|
+
* ),
|
|
660
|
+
* }
|
|
661
|
+
*
|
|
662
|
+
* @example
|
|
663
|
+
* // Custom range filter
|
|
664
|
+
* filter: CustomFilter({
|
|
665
|
+
* render: ({ value, onChange }) => (
|
|
666
|
+
* <div style={{ display: 'flex', gap: 4 }}>
|
|
667
|
+
* <input
|
|
668
|
+
* type="number"
|
|
669
|
+
* placeholder="Min"
|
|
670
|
+
* value={value?.min || ''}
|
|
671
|
+
* onChange={(e) => onChange({ ...value, min: e.target.value })}
|
|
672
|
+
* />
|
|
673
|
+
* <input
|
|
674
|
+
* type="number"
|
|
675
|
+
* placeholder="Max"
|
|
676
|
+
* value={value?.max || ''}
|
|
677
|
+
* onChange={(e) => onChange({ ...value, max: e.target.value })}
|
|
678
|
+
* />
|
|
679
|
+
* </div>
|
|
680
|
+
* ),
|
|
681
|
+
* filterFunction: (cellValue, filterValue) => {
|
|
682
|
+
* if (!filterValue) return true;
|
|
683
|
+
* const { min, max } = filterValue;
|
|
684
|
+
* const num = Number(cellValue);
|
|
685
|
+
* if (min && num < Number(min)) return false;
|
|
686
|
+
* if (max && num > Number(max)) return false;
|
|
687
|
+
* return true;
|
|
688
|
+
* },
|
|
689
|
+
* })
|
|
690
|
+
*
|
|
691
|
+
* @example
|
|
692
|
+
* // Custom multi-select filter with checkboxes
|
|
693
|
+
* filter: CustomFilter({
|
|
694
|
+
* render: ({ value, onChange }) => {
|
|
695
|
+
* const selected = value || [];
|
|
696
|
+
* const options = ['Active', 'Inactive', 'Pending'];
|
|
697
|
+
* return (
|
|
698
|
+
* <div>
|
|
699
|
+
* {options.map(opt => (
|
|
700
|
+
* <label key={opt}>
|
|
701
|
+
* <input
|
|
702
|
+
* type="checkbox"
|
|
703
|
+
* checked={selected.includes(opt)}
|
|
704
|
+
* onChange={(e) => {
|
|
705
|
+
* if (e.target.checked) {
|
|
706
|
+
* onChange([...selected, opt]);
|
|
707
|
+
* } else {
|
|
708
|
+
* onChange(selected.filter(s => s !== opt));
|
|
709
|
+
* }
|
|
710
|
+
* }}
|
|
711
|
+
* />
|
|
712
|
+
* {opt}
|
|
713
|
+
* </label>
|
|
714
|
+
* ))}
|
|
715
|
+
* </div>
|
|
716
|
+
* );
|
|
717
|
+
* },
|
|
718
|
+
* filterFunction: (cellValue, filterValue) => {
|
|
719
|
+
* if (!filterValue?.length) return true;
|
|
720
|
+
* return filterValue.includes(cellValue);
|
|
721
|
+
* },
|
|
722
|
+
* })
|
|
723
|
+
*
|
|
724
|
+
* @example
|
|
725
|
+
* // External control with getFilter
|
|
726
|
+
* filter: CustomFilter({
|
|
727
|
+
* placeholder: 'Custom...',
|
|
728
|
+
* getFilter: (filter) => {
|
|
729
|
+
* customFilterRef.current = filter;
|
|
730
|
+
* // filter.value - get current value
|
|
731
|
+
* // filter.setValue(newValue) - set value programmatically
|
|
732
|
+
* // filter.clear() - clear the filter
|
|
733
|
+
* },
|
|
734
|
+
* })
|
|
735
|
+
*/
|
|
736
|
+
export function CustomFilter(options) {
|
|
737
|
+
const FilterWithOptions = (props) => (_jsx(CustomFilterComponent, { ...props, options: options }));
|
|
738
|
+
FilterWithOptions.displayName = 'CustomFilter';
|
|
739
|
+
// Attach options as props for backwards compatibility with filter-comp.tsx pattern
|
|
740
|
+
// This allows accessing: column.filter.props.getFilter
|
|
741
|
+
FilterWithOptions.props = options;
|
|
742
|
+
return FilterWithOptions;
|
|
743
|
+
}
|
|
632
744
|
/**
|
|
633
745
|
* Get filter component based on type
|
|
634
746
|
*/
|
|
@@ -641,6 +753,10 @@ export const getFilterComponent = (type) => {
|
|
|
641
753
|
return DateFilter;
|
|
642
754
|
case 'select':
|
|
643
755
|
return SelectFilter;
|
|
756
|
+
case 'custom':
|
|
757
|
+
// For 'custom' type, return a placeholder - actual custom filters
|
|
758
|
+
// should be created using CustomFilter factory function
|
|
759
|
+
return TextFilter;
|
|
644
760
|
default:
|
|
645
761
|
return TextFilter;
|
|
646
762
|
}
|