@goplusvn/core 0.1.55 → 0.1.56
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/PLATFORM.md +44 -0
- package/package.json +3 -1
- package/src/crud/components/crud-import-dialog.tsx +23 -421
- package/src/crud/components/crud-page.tsx +7 -12
- package/src/import/__tests__/import-dialog.test.tsx +141 -0
- package/src/import/__tests__/import-engine.test.ts +235 -0
- package/src/import/import-dialog.tsx +570 -0
- package/src/import/import-engine.ts +357 -0
- package/src/import/index.ts +39 -0
- package/src/import/types.ts +73 -0
- package/src/import/use-import.ts +156 -0
- package/src/ui/filters/__tests__/advanced-filter-builder.test.tsx +194 -0
- package/src/ui/filters/advanced-filter-builder.tsx +380 -0
- package/src/ui/filters/index.ts +7 -0
- package/src/ui/index.tsx +1 -0
- package/src/crud/crud-filters/checkbox-filter.tsx +0 -87
- package/src/crud/crud-filters/datetime-filter.tsx +0 -82
- package/src/crud/crud-filters/filter-builder.tsx +0 -64
- package/src/crud/crud-filters/index.tsx +0 -78
- package/src/crud/crud-filters/radio-filter.tsx +0 -79
- package/src/crud/crud-filters/select-filter.tsx +0 -148
- package/src/crud/crud-filters/text-filter.tsx +0 -81
|
@@ -1,82 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
import { useEffect, useState } from "react";
|
|
4
|
-
|
|
5
|
-
import { useCrudContext } from "../components/crud-provider";
|
|
6
|
-
import { Calendar } from "../../ui/primitives/calendar";
|
|
7
|
-
import { Popover, PopoverContent, PopoverTrigger } from "../../ui/primitives/popover";
|
|
8
|
-
import { Button } from "../../ui/primitives/button";
|
|
9
|
-
import { cn } from "../../utils";
|
|
10
|
-
import { format } from "date-fns";
|
|
11
|
-
import { Calendar as CalendarIcon } from "lucide-react";
|
|
12
|
-
import { FilterConfig } from "../../types";
|
|
13
|
-
import { Label } from "../../ui/primitives/label"; // Imported Label
|
|
14
|
-
import { DateRangePicker } from "../../ui/forms/date-range-picker"; // Imported DateRangePicker
|
|
15
|
-
import { DatePicker } from "../../ui/forms/date-picker"; // Imported DatePicker
|
|
16
|
-
|
|
17
|
-
export function DatetimeFilter({ filter, mode: propMode }: { filter: FilterConfig; mode?: string }) {
|
|
18
|
-
const { filters, setFilters, addFilter } = useCrudContext(); // Added addFilter
|
|
19
|
-
const currentFilter = filters.find((f: any) => f.name === filter.name);
|
|
20
|
-
// Since 'mode' is removed, we assume single date selection.
|
|
21
|
-
// Wait, the code uses 'mode'.
|
|
22
|
-
// "const operator = mode === 'range' ? ..."
|
|
23
|
-
// I need to define mode.
|
|
24
|
-
const mode = propMode || filter.mode || "single";
|
|
25
|
-
|
|
26
|
-
const [value, setValue] = useState<Date | [Date, Date] | null>(
|
|
27
|
-
currentFilter?.value ? (currentFilter.value as Date) : null,
|
|
28
|
-
);
|
|
29
|
-
|
|
30
|
-
useEffect(() => {
|
|
31
|
-
const externalFilter = filters.find((f) => f.name === filter.name);
|
|
32
|
-
if (externalFilter && externalFilter.value !== value) {
|
|
33
|
-
setValue(
|
|
34
|
-
mode === "range"
|
|
35
|
-
? (externalFilter.value as [Date, Date])
|
|
36
|
-
: (externalFilter.value as Date),
|
|
37
|
-
);
|
|
38
|
-
}
|
|
39
|
-
}, [filters, filter.name, mode, value]);
|
|
40
|
-
|
|
41
|
-
const handleChange = (newValue: Date | [Date, Date] | null) => {
|
|
42
|
-
setValue(newValue);
|
|
43
|
-
|
|
44
|
-
if (!newValue) {
|
|
45
|
-
return;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
const operator =
|
|
49
|
-
mode === "range" ? filter.operator || "between" : filter.operator || "eq";
|
|
50
|
-
|
|
51
|
-
addFilter({
|
|
52
|
-
name: filter.name,
|
|
53
|
-
value: newValue,
|
|
54
|
-
operator,
|
|
55
|
-
});
|
|
56
|
-
};
|
|
57
|
-
|
|
58
|
-
return (
|
|
59
|
-
<div className="space-y-2">
|
|
60
|
-
<Label htmlFor={filter.name}>{filter.label}</Label>
|
|
61
|
-
{mode === "range" ? (
|
|
62
|
-
<DateRangePicker
|
|
63
|
-
value={
|
|
64
|
-
value && Array.isArray(value)
|
|
65
|
-
? { from: value[0], to: value[1] }
|
|
66
|
-
: undefined
|
|
67
|
-
}
|
|
68
|
-
onValueChange={(range) => {
|
|
69
|
-
handleChange(
|
|
70
|
-
range?.from && range?.to ? [range.from, range.to] : null,
|
|
71
|
-
);
|
|
72
|
-
}}
|
|
73
|
-
/>
|
|
74
|
-
) : (
|
|
75
|
-
<DatePicker
|
|
76
|
-
value={(value as Date) || undefined}
|
|
77
|
-
onValueChange={(date) => handleChange(date || null)}
|
|
78
|
-
/>
|
|
79
|
-
)}
|
|
80
|
-
</div>
|
|
81
|
-
);
|
|
82
|
-
}
|
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
import type { FilterConfig } from "../../types";
|
|
4
|
-
|
|
5
|
-
import { CheckboxFilter } from "./checkbox-filter";
|
|
6
|
-
import { DatetimeFilter } from "./datetime-filter";
|
|
7
|
-
import { RadioFilter } from "./radio-filter";
|
|
8
|
-
import { SelectFilter } from "./select-filter";
|
|
9
|
-
import { TextFilter } from "./text-filter";
|
|
10
|
-
|
|
11
|
-
interface FilterBuilderProps {
|
|
12
|
-
filters: FilterConfig[];
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export function FilterBuilder({ filters }: FilterBuilderProps) {
|
|
16
|
-
if (!filters || filters.length === 0) {
|
|
17
|
-
return null;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
return (
|
|
21
|
-
<div className="space-y-4">
|
|
22
|
-
{filters.map((filter) => {
|
|
23
|
-
switch (filter.type) {
|
|
24
|
-
case "text":
|
|
25
|
-
return <TextFilter key={filter.name} filter={filter} />;
|
|
26
|
-
case "datetime":
|
|
27
|
-
// Check if filter should be range based on operator
|
|
28
|
-
const isRange =
|
|
29
|
-
filter.operator === "between" ||
|
|
30
|
-
filter.name.toLowerCase().includes("range");
|
|
31
|
-
return (
|
|
32
|
-
<DatetimeFilter
|
|
33
|
-
key={filter.name}
|
|
34
|
-
filter={filter}
|
|
35
|
-
mode={isRange ? "range" : "single"}
|
|
36
|
-
/>
|
|
37
|
-
);
|
|
38
|
-
case "select":
|
|
39
|
-
return <SelectFilter key={filter.name} filter={filter} />;
|
|
40
|
-
case "checkbox":
|
|
41
|
-
return <CheckboxFilter key={filter.name} filter={filter} />;
|
|
42
|
-
case "radio":
|
|
43
|
-
return <RadioFilter key={filter.name} filter={filter} />;
|
|
44
|
-
case "custom":
|
|
45
|
-
// Custom filter renderer
|
|
46
|
-
if (filter.renderFilter) {
|
|
47
|
-
return (
|
|
48
|
-
<div key={filter.name}>
|
|
49
|
-
{filter.renderFilter({
|
|
50
|
-
filter,
|
|
51
|
-
name: filter.name,
|
|
52
|
-
label: filter.label,
|
|
53
|
-
})}
|
|
54
|
-
</div>
|
|
55
|
-
);
|
|
56
|
-
}
|
|
57
|
-
return null;
|
|
58
|
-
default:
|
|
59
|
-
return null;
|
|
60
|
-
}
|
|
61
|
-
})}
|
|
62
|
-
</div>
|
|
63
|
-
);
|
|
64
|
-
}
|
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
import { useState } from "react";
|
|
4
|
-
import { ChevronDown, Filter } from "lucide-react";
|
|
5
|
-
|
|
6
|
-
import type { FilterConfig } from "../../types";
|
|
7
|
-
|
|
8
|
-
import { Button } from "../../ui";
|
|
9
|
-
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "../../ui";
|
|
10
|
-
import { useCrudContext } from "../components/crud-provider";
|
|
11
|
-
import { FilterBuilder } from "./filter-builder";
|
|
12
|
-
|
|
13
|
-
interface CrudFiltersProps {
|
|
14
|
-
filters?: FilterConfig[];
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export function renderFilterInput(filter: FilterConfig) {
|
|
18
|
-
// This function body needs to be implemented based on the actual rendering logic
|
|
19
|
-
// For now, returning null or a placeholder
|
|
20
|
-
return null;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export function CrudFilters({ filters }: CrudFiltersProps) {
|
|
24
|
-
const { filters: activeFilters, clearFilters } = useCrudContext();
|
|
25
|
-
const [isOpen, setIsOpen] = useState(false);
|
|
26
|
-
|
|
27
|
-
if (!filters || filters.length === 0) {
|
|
28
|
-
return null;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
const hasActiveFilters = activeFilters.length > 0;
|
|
32
|
-
|
|
33
|
-
return (
|
|
34
|
-
<Collapsible open={isOpen} onOpenChange={setIsOpen} className="w-full">
|
|
35
|
-
<div className="flex items-center justify-between mb-2">
|
|
36
|
-
<CollapsibleTrigger asChild>
|
|
37
|
-
<Button variant="outline" size="sm" className="gap-2">
|
|
38
|
-
<Filter className="h-4 w-4" />
|
|
39
|
-
Filters
|
|
40
|
-
{hasActiveFilters && (
|
|
41
|
-
<span className="ml-1 rounded-full bg-primary px-2 py-0.5 text-xs text-primary-foreground">
|
|
42
|
-
{activeFilters.length}
|
|
43
|
-
</span>
|
|
44
|
-
)}
|
|
45
|
-
<ChevronDown
|
|
46
|
-
className={`h-4 w-4 transition-transform ${
|
|
47
|
-
isOpen ? "transform rotate-180" : ""
|
|
48
|
-
}`}
|
|
49
|
-
/>
|
|
50
|
-
</Button>
|
|
51
|
-
</CollapsibleTrigger>
|
|
52
|
-
{hasActiveFilters && (
|
|
53
|
-
<Button
|
|
54
|
-
variant="ghost"
|
|
55
|
-
size="sm"
|
|
56
|
-
onClick={clearFilters}
|
|
57
|
-
className="h-8"
|
|
58
|
-
>
|
|
59
|
-
Clear all
|
|
60
|
-
</Button>
|
|
61
|
-
)}
|
|
62
|
-
</div>
|
|
63
|
-
<CollapsibleContent className="space-y-4">
|
|
64
|
-
<div className="rounded-lg border p-4">
|
|
65
|
-
<FilterBuilder filters={filters} />
|
|
66
|
-
</div>
|
|
67
|
-
</CollapsibleContent>
|
|
68
|
-
</Collapsible>
|
|
69
|
-
);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
// Export all filter components
|
|
73
|
-
export { TextFilter } from "./text-filter";
|
|
74
|
-
export { DatetimeFilter } from "./datetime-filter";
|
|
75
|
-
export { SelectFilter } from "./select-filter";
|
|
76
|
-
export { CheckboxFilter } from "./checkbox-filter";
|
|
77
|
-
export { RadioFilter } from "./radio-filter";
|
|
78
|
-
export { FilterBuilder } from "./filter-builder";
|
|
@@ -1,79 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
import { useEffect, useState } from "react";
|
|
4
|
-
|
|
5
|
-
import { dataLoader } from "../lib/data-loader";
|
|
6
|
-
|
|
7
|
-
import { useCrudContext } from "../components/crud-provider";
|
|
8
|
-
import { RadioGroup, RadioGroupItem } from "../../ui/forms/radio-group";
|
|
9
|
-
import { Label } from "../../ui/primitives/label";
|
|
10
|
-
import { ScrollArea } from "../../ui/primitives/scroll-area";
|
|
11
|
-
import { FilterConfig } from "../../types";
|
|
12
|
-
|
|
13
|
-
export function RadioFilter({ filter }: { filter: FilterConfig }) {
|
|
14
|
-
const { filters, addFilter, removeFilter } = useCrudContext();
|
|
15
|
-
const currentFilter = filters.find((f: any) => f.name === filter.name);
|
|
16
|
-
const [value, setValue] = useState<string>(
|
|
17
|
-
currentFilter?.value
|
|
18
|
-
? (currentFilter.value as string)
|
|
19
|
-
: (filter.defaultValue as string) || "",
|
|
20
|
-
);
|
|
21
|
-
const [options, setOptions] = useState<
|
|
22
|
-
Array<{ label: string; value: string | number | boolean }>
|
|
23
|
-
>(filter.options || []);
|
|
24
|
-
|
|
25
|
-
// Load options from API if dataSource is provided
|
|
26
|
-
useEffect(() => {
|
|
27
|
-
if (filter.dataSource) {
|
|
28
|
-
dataLoader
|
|
29
|
-
.loadOptions(filter.dataSource)
|
|
30
|
-
.then(setOptions)
|
|
31
|
-
.catch((error) => {
|
|
32
|
-
console.error("Error loading filter options:", error);
|
|
33
|
-
});
|
|
34
|
-
}
|
|
35
|
-
}, [filter.dataSource]);
|
|
36
|
-
|
|
37
|
-
useEffect(() => {
|
|
38
|
-
const externalFilter = filters.find((f) => f.name === filter.name);
|
|
39
|
-
if (externalFilter && externalFilter.value !== value) {
|
|
40
|
-
setValue((externalFilter.value as string) || "");
|
|
41
|
-
}
|
|
42
|
-
}, [filters, filter.name, value]);
|
|
43
|
-
|
|
44
|
-
const handleChange = (newValue: string) => {
|
|
45
|
-
setValue(newValue);
|
|
46
|
-
|
|
47
|
-
if (!newValue) {
|
|
48
|
-
return;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
addFilter({
|
|
52
|
-
name: filter.name,
|
|
53
|
-
value: newValue,
|
|
54
|
-
operator: filter.operator || "eq",
|
|
55
|
-
});
|
|
56
|
-
};
|
|
57
|
-
|
|
58
|
-
return (
|
|
59
|
-
<div className="space-y-2">
|
|
60
|
-
<Label>{filter.label}</Label>
|
|
61
|
-
<RadioGroup value={value} onValueChange={handleChange}>
|
|
62
|
-
{options.map((option) => (
|
|
63
|
-
<div key={String(option.value)} className="flex items-center space-x-2">
|
|
64
|
-
<RadioGroupItem
|
|
65
|
-
value={String(option.value)}
|
|
66
|
-
id={`${filter.name}-${option.value}`}
|
|
67
|
-
/>
|
|
68
|
-
<Label
|
|
69
|
-
htmlFor={`${filter.name}-${option.value}`}
|
|
70
|
-
className="font-normal cursor-pointer"
|
|
71
|
-
>
|
|
72
|
-
{option.label}
|
|
73
|
-
</Label>
|
|
74
|
-
</div>
|
|
75
|
-
))}
|
|
76
|
-
</RadioGroup>
|
|
77
|
-
</div>
|
|
78
|
-
);
|
|
79
|
-
}
|
|
@@ -1,148 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
import { useEffect, useState } from "react";
|
|
4
|
-
|
|
5
|
-
import { useCrudContext } from "../components/crud-provider";
|
|
6
|
-
import { Label } from "../../ui/primitives/label";
|
|
7
|
-
import { FilterConfig } from "../../types";
|
|
8
|
-
import {
|
|
9
|
-
Select,
|
|
10
|
-
SelectContent,
|
|
11
|
-
SelectItem,
|
|
12
|
-
SelectTrigger,
|
|
13
|
-
SelectValue,
|
|
14
|
-
} from "../../ui/primitives/select";
|
|
15
|
-
import { dataLoader } from "../lib/data-loader";
|
|
16
|
-
|
|
17
|
-
export function SelectFilter({ filter }: { filter: FilterConfig }) {
|
|
18
|
-
const { filters, addFilter, removeFilter } = useCrudContext();
|
|
19
|
-
const currentFilter = filters.find((f: any) => f.name === filter.name);
|
|
20
|
-
|
|
21
|
-
// Determine if multiple selection is enabled
|
|
22
|
-
const multiple = (filter as any).multiple || (filter.type as string) === "select-multiple";
|
|
23
|
-
|
|
24
|
-
const [value, setValue] = useState<string | string[]>(
|
|
25
|
-
currentFilter?.value
|
|
26
|
-
? multiple
|
|
27
|
-
? (currentFilter.value as string[])
|
|
28
|
-
: (currentFilter.value as string)
|
|
29
|
-
: multiple
|
|
30
|
-
? []
|
|
31
|
-
: "",
|
|
32
|
-
);
|
|
33
|
-
|
|
34
|
-
const [options, setOptions] = useState<
|
|
35
|
-
Array<{ label: string; value: string | number | boolean }>
|
|
36
|
-
>(filter.options || []);
|
|
37
|
-
|
|
38
|
-
// Load options from API if dataSource is provided
|
|
39
|
-
useEffect(() => {
|
|
40
|
-
if (filter.dataSource) {
|
|
41
|
-
dataLoader
|
|
42
|
-
.loadOptions(filter.dataSource)
|
|
43
|
-
.then(setOptions)
|
|
44
|
-
.catch((error) => {
|
|
45
|
-
console.error("Error loading filter options:", error);
|
|
46
|
-
});
|
|
47
|
-
}
|
|
48
|
-
}, [filter.dataSource]);
|
|
49
|
-
|
|
50
|
-
useEffect(() => {
|
|
51
|
-
const externalFilter = filters.find((f) => f.name === filter.name);
|
|
52
|
-
if (externalFilter && externalFilter.value !== value) {
|
|
53
|
-
setValue(
|
|
54
|
-
multiple
|
|
55
|
-
? (externalFilter.value as string[])
|
|
56
|
-
: (externalFilter.value as string),
|
|
57
|
-
);
|
|
58
|
-
}
|
|
59
|
-
}, [filters, filter.name, multiple, value]);
|
|
60
|
-
|
|
61
|
-
const handleChange = (newValue: string | number | undefined) => {
|
|
62
|
-
if (newValue === undefined) {
|
|
63
|
-
removeFilter(filter.name);
|
|
64
|
-
return;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
const stringValue = String(newValue);
|
|
68
|
-
|
|
69
|
-
if (multiple) {
|
|
70
|
-
const currentValues = Array.isArray(value) ? value : [];
|
|
71
|
-
const updatedValues = currentValues.includes(stringValue)
|
|
72
|
-
? currentValues.filter((v) => v !== stringValue)
|
|
73
|
-
: [...currentValues, stringValue];
|
|
74
|
-
|
|
75
|
-
setValue(updatedValues);
|
|
76
|
-
|
|
77
|
-
if (updatedValues.length === 0) {
|
|
78
|
-
removeFilter(filter.name);
|
|
79
|
-
return;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
addFilter({
|
|
83
|
-
name: filter.name,
|
|
84
|
-
value: updatedValues,
|
|
85
|
-
operator: filter.operator || "in",
|
|
86
|
-
});
|
|
87
|
-
} else {
|
|
88
|
-
setValue(stringValue);
|
|
89
|
-
|
|
90
|
-
if (!stringValue || stringValue === "") {
|
|
91
|
-
removeFilter(filter.name);
|
|
92
|
-
return;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
addFilter({
|
|
96
|
-
name: filter.name,
|
|
97
|
-
value: stringValue,
|
|
98
|
-
operator: filter.operator || "eq",
|
|
99
|
-
});
|
|
100
|
-
}
|
|
101
|
-
};
|
|
102
|
-
|
|
103
|
-
if (multiple) {
|
|
104
|
-
const selectedValues = Array.isArray(value) ? value : [];
|
|
105
|
-
return (
|
|
106
|
-
<div className="space-y-2">
|
|
107
|
-
<Label>{filter.label}</Label>
|
|
108
|
-
<div className="space-y-2 max-h-40 overflow-y-auto border rounded p-2">
|
|
109
|
-
{options.map((option) => (
|
|
110
|
-
<label
|
|
111
|
-
key={String(option.value)}
|
|
112
|
-
className="flex items-center space-x-2 cursor-pointer"
|
|
113
|
-
>
|
|
114
|
-
<input
|
|
115
|
-
type="checkbox"
|
|
116
|
-
checked={selectedValues.includes(String(option.value))}
|
|
117
|
-
onChange={() => handleChange(String(option.value))}
|
|
118
|
-
className="rounded border-gray-300"
|
|
119
|
-
/>
|
|
120
|
-
<span>{option.label}</span>
|
|
121
|
-
</label>
|
|
122
|
-
))}
|
|
123
|
-
</div>
|
|
124
|
-
</div>
|
|
125
|
-
);
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
return (
|
|
129
|
-
<div className="space-y-2">
|
|
130
|
-
<Label htmlFor={filter.name}>{filter.label}</Label>
|
|
131
|
-
<Select
|
|
132
|
-
value={value as string}
|
|
133
|
-
onValueChange={handleChange}
|
|
134
|
-
>
|
|
135
|
-
<SelectTrigger id={filter.name}>
|
|
136
|
-
<SelectValue placeholder={`Select ${filter.label}`} />
|
|
137
|
-
</SelectTrigger>
|
|
138
|
-
<SelectContent>
|
|
139
|
-
{options.map((option) => (
|
|
140
|
-
<SelectItem key={String(option.value)} value={String(option.value)}>
|
|
141
|
-
{option.label}
|
|
142
|
-
</SelectItem>
|
|
143
|
-
))}
|
|
144
|
-
</SelectContent>
|
|
145
|
-
</Select>
|
|
146
|
-
</div>
|
|
147
|
-
);
|
|
148
|
-
}
|
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
import { useEffect, useRef, useState } from "react";
|
|
4
|
-
|
|
5
|
-
import { useCrudContext } from "../components/crud-provider";
|
|
6
|
-
import { Input } from "../../ui/primitives/input";
|
|
7
|
-
import { Label } from "../../ui/primitives/label";
|
|
8
|
-
import type { FilterConfig } from "../../types";
|
|
9
|
-
|
|
10
|
-
export function TextFilter({ filter }: { filter: FilterConfig }) {
|
|
11
|
-
const { filters, addFilter, removeFilter } = useCrudContext();
|
|
12
|
-
const currentFilter = filters.find((f: any) => f.name === filter.name);
|
|
13
|
-
const [value, setValue] = useState<string>(
|
|
14
|
-
(currentFilter?.value as string) || (filter.defaultValue as string) || "",
|
|
15
|
-
);
|
|
16
|
-
const isExternalUpdate = useRef(false);
|
|
17
|
-
const debounceMs = 500; // Define debounce delay
|
|
18
|
-
|
|
19
|
-
useEffect(() => {
|
|
20
|
-
// Sync with context when filters change externally
|
|
21
|
-
const externalFilter = filters.find((f) => f.name === filter.name);
|
|
22
|
-
const externalValue = (externalFilter?.value as string) || "";
|
|
23
|
-
const currentValue = value || "";
|
|
24
|
-
|
|
25
|
-
if (externalValue !== currentValue) {
|
|
26
|
-
isExternalUpdate.current = true;
|
|
27
|
-
setValue(externalValue);
|
|
28
|
-
}
|
|
29
|
-
}, [filters, filter.name]);
|
|
30
|
-
|
|
31
|
-
// Debounced update filter
|
|
32
|
-
useEffect(() => {
|
|
33
|
-
// Skip debounce if this is an external update
|
|
34
|
-
if (isExternalUpdate.current) {
|
|
35
|
-
isExternalUpdate.current = false;
|
|
36
|
-
return;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
const timer = setTimeout(() => {
|
|
40
|
-
if (value.trim() === "") {
|
|
41
|
-
// Remove filter if empty
|
|
42
|
-
removeFilter(filter.name);
|
|
43
|
-
} else {
|
|
44
|
-
// Add or update filter
|
|
45
|
-
addFilter({
|
|
46
|
-
name: filter.name,
|
|
47
|
-
value: value,
|
|
48
|
-
operator: filter.operator || "contains",
|
|
49
|
-
});
|
|
50
|
-
}
|
|
51
|
-
}, debounceMs);
|
|
52
|
-
|
|
53
|
-
return () => clearTimeout(timer);
|
|
54
|
-
}, [
|
|
55
|
-
value,
|
|
56
|
-
filter.name,
|
|
57
|
-
filter.operator,
|
|
58
|
-
debounceMs,
|
|
59
|
-
addFilter,
|
|
60
|
-
removeFilter,
|
|
61
|
-
]);
|
|
62
|
-
|
|
63
|
-
const handleChange = (newValue: string) => {
|
|
64
|
-
isExternalUpdate.current = false;
|
|
65
|
-
setValue(newValue);
|
|
66
|
-
};
|
|
67
|
-
|
|
68
|
-
return (
|
|
69
|
-
<div className="space-y-2">
|
|
70
|
-
<Label htmlFor={filter.name}>{filter.label}</Label>
|
|
71
|
-
<Input
|
|
72
|
-
id={filter.name}
|
|
73
|
-
type="text"
|
|
74
|
-
placeholder={filter.label}
|
|
75
|
-
value={value}
|
|
76
|
-
onChange={(e) => handleChange(e.target.value)}
|
|
77
|
-
className="w-full"
|
|
78
|
-
/>
|
|
79
|
-
</div>
|
|
80
|
-
);
|
|
81
|
-
}
|