Fix Esc handling in filter popover
This commit is contained in:
@@ -18,13 +18,10 @@ import {
|
|||||||
PopoverTrigger,
|
PopoverTrigger,
|
||||||
} from "@/components/ui/popover";
|
} from "@/components/ui/popover";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import {
|
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
|
||||||
ToggleGroup,
|
|
||||||
ToggleGroupItem,
|
|
||||||
} from "@/components/ui/toggle-group";
|
|
||||||
|
|
||||||
type FilterValue = string | number | boolean;
|
type FilterValue = string | number | boolean;
|
||||||
type ComparisonOperator = '=' | '>' | '>=' | '<' | '<=' | 'between';
|
type ComparisonOperator = "=" | ">" | ">=" | "<" | "<=" | "between";
|
||||||
|
|
||||||
interface FilterValueWithOperator {
|
interface FilterValueWithOperator {
|
||||||
value: FilterValue | [number, number];
|
value: FilterValue | [number, number];
|
||||||
@@ -43,7 +40,7 @@ interface ActiveFilter {
|
|||||||
interface FilterOption {
|
interface FilterOption {
|
||||||
id: string;
|
id: string;
|
||||||
label: string;
|
label: string;
|
||||||
type: 'select' | 'number' | 'boolean' | 'text';
|
type: "select" | "number" | "boolean" | "text";
|
||||||
options?: { label: string; value: string }[];
|
options?: { label: string; value: string }[];
|
||||||
group: string;
|
group: string;
|
||||||
operators?: ComparisonOperator[];
|
operators?: ComparisonOperator[];
|
||||||
@@ -51,163 +48,163 @@ interface FilterOption {
|
|||||||
|
|
||||||
const FILTER_OPTIONS: FilterOption[] = [
|
const FILTER_OPTIONS: FilterOption[] = [
|
||||||
// Basic Info Group
|
// Basic Info Group
|
||||||
{ id: 'search', label: 'Search', type: 'text', group: 'Basic Info' },
|
{ id: "search", label: "Search", type: "text", group: "Basic Info" },
|
||||||
{ id: 'sku', label: 'SKU', type: 'text', group: 'Basic Info' },
|
{ id: "sku", label: "SKU", type: "text", group: "Basic Info" },
|
||||||
{ id: 'vendor', label: 'Vendor', type: 'select', group: 'Basic Info' },
|
{ id: "vendor", label: "Vendor", type: "select", group: "Basic Info" },
|
||||||
{ id: 'brand', label: 'Brand', type: 'select', group: 'Basic Info' },
|
{ id: "brand", label: "Brand", type: "select", group: "Basic Info" },
|
||||||
{ id: 'category', label: 'Category', type: 'select', group: 'Basic Info' },
|
{ id: "category", label: "Category", type: "select", group: "Basic Info" },
|
||||||
|
|
||||||
// Inventory Group
|
// Inventory Group
|
||||||
{
|
{
|
||||||
id: 'stockStatus',
|
id: "stockStatus",
|
||||||
label: 'Stock Status',
|
label: "Stock Status",
|
||||||
type: 'select',
|
type: "select",
|
||||||
options: [
|
options: [
|
||||||
{ label: 'Critical', value: 'critical' },
|
{ label: "Critical", value: "critical" },
|
||||||
{ label: 'At Risk', value: 'at-risk' },
|
{ label: "At Risk", value: "at-risk" },
|
||||||
{ label: 'Reorder', value: 'reorder' },
|
{ label: "Reorder", value: "reorder" },
|
||||||
{ label: 'Healthy', value: 'healthy' },
|
{ label: "Healthy", value: "healthy" },
|
||||||
{ label: 'Overstocked', value: 'overstocked' },
|
{ label: "Overstocked", value: "overstocked" },
|
||||||
{ label: 'New', value: 'new' }
|
{ label: "New", value: "new" },
|
||||||
],
|
],
|
||||||
group: 'Inventory'
|
group: "Inventory",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'stock',
|
id: "stock",
|
||||||
label: 'Stock Quantity',
|
label: "Stock Quantity",
|
||||||
type: 'number',
|
type: "number",
|
||||||
group: 'Inventory',
|
group: "Inventory",
|
||||||
operators: ['=', '>', '>=', '<', '<=', 'between']
|
operators: ["=", ">", ">=", "<", "<=", "between"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'daysOfStock',
|
id: "daysOfStock",
|
||||||
label: 'Days of Stock',
|
label: "Days of Stock",
|
||||||
type: 'number',
|
type: "number",
|
||||||
group: 'Inventory',
|
group: "Inventory",
|
||||||
operators: ['=', '>', '>=', '<', '<=', 'between']
|
operators: ["=", ">", ">=", "<", "<=", "between"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'replenishable',
|
id: "replenishable",
|
||||||
label: 'Replenishable',
|
label: "Replenishable",
|
||||||
type: 'select',
|
type: "select",
|
||||||
options: [
|
options: [
|
||||||
{ label: 'Yes', value: 'true' },
|
{ label: "Yes", value: "true" },
|
||||||
{ label: 'No', value: 'false' }
|
{ label: "No", value: "false" },
|
||||||
],
|
],
|
||||||
group: 'Inventory'
|
group: "Inventory",
|
||||||
},
|
},
|
||||||
|
|
||||||
// Pricing Group
|
// Pricing Group
|
||||||
{
|
{
|
||||||
id: 'price',
|
id: "price",
|
||||||
label: 'Price',
|
label: "Price",
|
||||||
type: 'number',
|
type: "number",
|
||||||
group: 'Pricing',
|
group: "Pricing",
|
||||||
operators: ['=', '>', '>=', '<', '<=', 'between']
|
operators: ["=", ">", ">=", "<", "<=", "between"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'costPrice',
|
id: "costPrice",
|
||||||
label: 'Cost Price',
|
label: "Cost Price",
|
||||||
type: 'number',
|
type: "number",
|
||||||
group: 'Pricing',
|
group: "Pricing",
|
||||||
operators: ['=', '>', '>=', '<', '<=', 'between']
|
operators: ["=", ">", ">=", "<", "<=", "between"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'landingCost',
|
id: "landingCost",
|
||||||
label: 'Landing Cost',
|
label: "Landing Cost",
|
||||||
type: 'number',
|
type: "number",
|
||||||
group: 'Pricing',
|
group: "Pricing",
|
||||||
operators: ['=', '>', '>=', '<', '<=', 'between']
|
operators: ["=", ">", ">=", "<", "<=", "between"],
|
||||||
},
|
},
|
||||||
|
|
||||||
// Sales Metrics Group
|
// Sales Metrics Group
|
||||||
{
|
{
|
||||||
id: 'dailySalesAvg',
|
id: "dailySalesAvg",
|
||||||
label: 'Daily Sales Avg',
|
label: "Daily Sales Avg",
|
||||||
type: 'number',
|
type: "number",
|
||||||
group: 'Sales Metrics',
|
group: "Sales Metrics",
|
||||||
operators: ['=', '>', '>=', '<', '<=', 'between']
|
operators: ["=", ">", ">=", "<", "<=", "between"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'weeklySalesAvg',
|
id: "weeklySalesAvg",
|
||||||
label: 'Weekly Sales Avg',
|
label: "Weekly Sales Avg",
|
||||||
type: 'number',
|
type: "number",
|
||||||
group: 'Sales Metrics',
|
group: "Sales Metrics",
|
||||||
operators: ['=', '>', '>=', '<', '<=', 'between']
|
operators: ["=", ">", ">=", "<", "<=", "between"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'monthlySalesAvg',
|
id: "monthlySalesAvg",
|
||||||
label: 'Monthly Sales Avg',
|
label: "Monthly Sales Avg",
|
||||||
type: 'number',
|
type: "number",
|
||||||
group: 'Sales Metrics',
|
group: "Sales Metrics",
|
||||||
operators: ['=', '>', '>=', '<', '<=', 'between']
|
operators: ["=", ">", ">=", "<", "<=", "between"],
|
||||||
},
|
},
|
||||||
|
|
||||||
// Financial Metrics Group
|
// Financial Metrics Group
|
||||||
{
|
{
|
||||||
id: 'margin',
|
id: "margin",
|
||||||
label: 'Margin %',
|
label: "Margin %",
|
||||||
type: 'number',
|
type: "number",
|
||||||
group: 'Financial Metrics',
|
group: "Financial Metrics",
|
||||||
operators: ['=', '>', '>=', '<', '<=', 'between']
|
operators: ["=", ">", ">=", "<", "<=", "between"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'gmroi',
|
id: "gmroi",
|
||||||
label: 'GMROI',
|
label: "GMROI",
|
||||||
type: 'number',
|
type: "number",
|
||||||
group: 'Financial Metrics',
|
group: "Financial Metrics",
|
||||||
operators: ['=', '>', '>=', '<', '<=', 'between']
|
operators: ["=", ">", ">=", "<", "<=", "between"],
|
||||||
},
|
},
|
||||||
|
|
||||||
// Lead Time & Stock Coverage Group
|
// Lead Time & Stock Coverage Group
|
||||||
{
|
{
|
||||||
id: 'leadTime',
|
id: "leadTime",
|
||||||
label: 'Lead Time (Days)',
|
label: "Lead Time (Days)",
|
||||||
type: 'number',
|
type: "number",
|
||||||
group: 'Lead Time & Coverage',
|
group: "Lead Time & Coverage",
|
||||||
operators: ['=', '>', '>=', '<', '<=', 'between']
|
operators: ["=", ">", ">=", "<", "<=", "between"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'leadTimeStatus',
|
id: "leadTimeStatus",
|
||||||
label: 'Lead Time Status',
|
label: "Lead Time Status",
|
||||||
type: 'select',
|
type: "select",
|
||||||
options: [
|
options: [
|
||||||
{ label: 'On Target', value: 'on_target' },
|
{ label: "On Target", value: "on_target" },
|
||||||
{ label: 'Warning', value: 'warning' },
|
{ label: "Warning", value: "warning" },
|
||||||
{ label: 'Critical', value: 'critical' }
|
{ label: "Critical", value: "critical" },
|
||||||
],
|
],
|
||||||
group: 'Lead Time & Coverage'
|
group: "Lead Time & Coverage",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'stockCoverage',
|
id: "stockCoverage",
|
||||||
label: 'Stock Coverage Ratio',
|
label: "Stock Coverage Ratio",
|
||||||
type: 'number',
|
type: "number",
|
||||||
group: 'Lead Time & Coverage',
|
group: "Lead Time & Coverage",
|
||||||
operators: ['=', '>', '>=', '<', '<=', 'between']
|
operators: ["=", ">", ">=", "<", "<=", "between"],
|
||||||
},
|
},
|
||||||
|
|
||||||
// Classification Group
|
// Classification Group
|
||||||
{
|
{
|
||||||
id: 'abcClass',
|
id: "abcClass",
|
||||||
label: 'ABC Class',
|
label: "ABC Class",
|
||||||
type: 'select',
|
type: "select",
|
||||||
options: [
|
options: [
|
||||||
{ label: 'A', value: 'A' },
|
{ label: "A", value: "A" },
|
||||||
{ label: 'B', value: 'B' },
|
{ label: "B", value: "B" },
|
||||||
{ label: 'C', value: 'C' }
|
{ label: "C", value: "C" },
|
||||||
],
|
],
|
||||||
group: 'Classification'
|
group: "Classification",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'managingStock',
|
id: "managingStock",
|
||||||
label: 'Managing Stock',
|
label: "Managing Stock",
|
||||||
type: 'select',
|
type: "select",
|
||||||
options: [
|
options: [
|
||||||
{ label: 'Yes', value: 'true' },
|
{ label: "Yes", value: "true" },
|
||||||
{ label: 'No', value: 'false' }
|
{ label: "No", value: "false" },
|
||||||
],
|
],
|
||||||
group: 'Classification'
|
group: "Classification",
|
||||||
}
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
interface ProductFiltersProps {
|
interface ProductFiltersProps {
|
||||||
@@ -229,16 +226,21 @@ export function ProductFilters({
|
|||||||
}: ProductFiltersProps) {
|
}: ProductFiltersProps) {
|
||||||
const [showCommand, setShowCommand] = React.useState(false);
|
const [showCommand, setShowCommand] = React.useState(false);
|
||||||
const [selectedFilter, setSelectedFilter] = React.useState<FilterOption | null>(null);
|
const [selectedFilter, setSelectedFilter] = React.useState<FilterOption | null>(null);
|
||||||
const [selectedOperator, setSelectedOperator] = React.useState<ComparisonOperator>('=');
|
const [selectedOperator, setSelectedOperator] = React.useState<ComparisonOperator>("=");
|
||||||
const [inputValue, setInputValue] = React.useState("");
|
const [inputValue, setInputValue] = React.useState("");
|
||||||
const [inputValue2, setInputValue2] = React.useState("");
|
const [inputValue2, setInputValue2] = React.useState("");
|
||||||
const [searchValue, setSearchValue] = React.useState("");
|
const [searchValue, setSearchValue] = React.useState("");
|
||||||
|
|
||||||
|
// Add refs for the inputs
|
||||||
|
const numberInputRef = React.useRef<HTMLInputElement>(null);
|
||||||
|
const selectInputRef = React.useRef<HTMLInputElement>(null);
|
||||||
|
const textInputRef = React.useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
// Reset states when popup closes
|
// Reset states when popup closes
|
||||||
const handlePopoverClose = () => {
|
const handlePopoverClose = () => {
|
||||||
setShowCommand(false);
|
setShowCommand(false);
|
||||||
setSelectedFilter(null);
|
setSelectedFilter(null);
|
||||||
setSelectedOperator('=');
|
setSelectedOperator("=");
|
||||||
setInputValue("");
|
setInputValue("");
|
||||||
setInputValue2("");
|
setInputValue2("");
|
||||||
setSearchValue("");
|
setSearchValue("");
|
||||||
@@ -248,7 +250,7 @@ export function ProductFilters({
|
|||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
const handleKeyDown = (e: KeyboardEvent) => {
|
const handleKeyDown = (e: KeyboardEvent) => {
|
||||||
// Command/Ctrl + K to toggle filter
|
// Command/Ctrl + K to toggle filter
|
||||||
if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
|
if ((e.metaKey || e.ctrlKey) && e.key === "k") {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (!showCommand) {
|
if (!showCommand) {
|
||||||
setShowCommand(true);
|
setShowCommand(true);
|
||||||
@@ -256,35 +258,31 @@ export function ProductFilters({
|
|||||||
handlePopoverClose();
|
handlePopoverClose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Only handle Escape at the root level
|
|
||||||
if (e.key === 'Escape' && !selectedFilter) {
|
|
||||||
handlePopoverClose();
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
window.addEventListener('keydown', handleKeyDown);
|
window.addEventListener("keydown", handleKeyDown);
|
||||||
return () => window.removeEventListener('keydown', handleKeyDown);
|
return () => window.removeEventListener("keydown", handleKeyDown);
|
||||||
}, [selectedFilter, showCommand]);
|
}, [showCommand]);
|
||||||
|
|
||||||
// Update filter options with dynamic data
|
// Update filter options with dynamic data
|
||||||
const filterOptions = React.useMemo(() => {
|
const filterOptions = React.useMemo(() => {
|
||||||
return FILTER_OPTIONS.map(option => {
|
return FILTER_OPTIONS.map((option) => {
|
||||||
if (option.id === 'category') {
|
if (option.id === "category") {
|
||||||
return {
|
return {
|
||||||
...option,
|
...option,
|
||||||
options: categories.map(cat => ({ label: cat, value: cat }))
|
options: categories.map((cat) => ({ label: cat, value: cat })),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (option.id === 'vendor') {
|
if (option.id === "vendor") {
|
||||||
return {
|
return {
|
||||||
...option,
|
...option,
|
||||||
options: vendors.map(vendor => ({ label: vendor, value: vendor }))
|
options: vendors.map((vendor) => ({ label: vendor, value: vendor })),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (option.id === 'brand') {
|
if (option.id === "brand") {
|
||||||
return {
|
return {
|
||||||
...option,
|
...option,
|
||||||
options: brands.map(brand => ({ label: brand, value: brand }))
|
options: brands.map((brand) => ({ label: brand, value: brand })),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return option;
|
return option;
|
||||||
@@ -296,15 +294,27 @@ export function ProductFilters({
|
|||||||
if (!searchValue) return filterOptions;
|
if (!searchValue) return filterOptions;
|
||||||
|
|
||||||
const search = searchValue.toLowerCase();
|
const search = searchValue.toLowerCase();
|
||||||
return filterOptions.filter(option =>
|
return filterOptions.filter(
|
||||||
option.label.toLowerCase().includes(search) ||
|
(option) =>
|
||||||
option.group.toLowerCase().includes(search)
|
option.label.toLowerCase().includes(search) ||
|
||||||
|
option.group.toLowerCase().includes(search)
|
||||||
);
|
);
|
||||||
}, [filterOptions, searchValue]);
|
}, [filterOptions, searchValue]);
|
||||||
|
|
||||||
const handleSelectFilter = React.useCallback((filter: FilterOption) => {
|
const handleSelectFilter = React.useCallback((filter: FilterOption) => {
|
||||||
setSelectedFilter(filter);
|
setSelectedFilter(filter);
|
||||||
setInputValue("");
|
setInputValue("");
|
||||||
|
|
||||||
|
// Focus the appropriate input after state updates
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
if (filter.type === "number") {
|
||||||
|
numberInputRef.current?.focus();
|
||||||
|
} else if (filter.type === "select") {
|
||||||
|
selectInputRef.current?.focus();
|
||||||
|
} else {
|
||||||
|
textInputRef.current?.focus();
|
||||||
|
}
|
||||||
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleApplyFilter = (value: FilterValue | [number, number]) => {
|
const handleApplyFilter = (value: FilterValue | [number, number]) => {
|
||||||
@@ -314,8 +324,8 @@ export function ProductFilters({
|
|||||||
...activeFilters,
|
...activeFilters,
|
||||||
[selectedFilter.id]: {
|
[selectedFilter.id]: {
|
||||||
value,
|
value,
|
||||||
operator: selectedOperator
|
operator: selectedOperator,
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
onFilterChange(newFilters as Record<string, ActiveFilterValue>);
|
onFilterChange(newFilters as Record<string, ActiveFilterValue>);
|
||||||
@@ -324,7 +334,7 @@ export function ProductFilters({
|
|||||||
|
|
||||||
const handleBackToFilters = () => {
|
const handleBackToFilters = () => {
|
||||||
setSelectedFilter(null);
|
setSelectedFilter(null);
|
||||||
setSelectedOperator('=');
|
setSelectedOperator("=");
|
||||||
setInputValue("");
|
setInputValue("");
|
||||||
setInputValue2("");
|
setInputValue2("");
|
||||||
};
|
};
|
||||||
@@ -333,11 +343,13 @@ export function ProductFilters({
|
|||||||
if (!activeFilters) return [];
|
if (!activeFilters) return [];
|
||||||
|
|
||||||
return Object.entries(activeFilters).map(([id, value]): ActiveFilter => {
|
return Object.entries(activeFilters).map(([id, value]): ActiveFilter => {
|
||||||
const option = filterOptions.find(opt => opt.id === id);
|
const option = filterOptions.find((opt) => opt.id === id);
|
||||||
let displayValue = String(value);
|
let displayValue = String(value);
|
||||||
|
|
||||||
if (option?.type === 'select' && option.options) {
|
if (option?.type === "select" && option.options) {
|
||||||
const optionLabel = option.options.find(opt => opt.value === value)?.label;
|
const optionLabel = option.options.find(
|
||||||
|
(opt) => opt.value === value
|
||||||
|
)?.label;
|
||||||
if (optionLabel) displayValue = optionLabel;
|
if (optionLabel) displayValue = optionLabel;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -345,7 +357,7 @@ export function ProductFilters({
|
|||||||
id,
|
id,
|
||||||
label: option?.label || id,
|
label: option?.label || id,
|
||||||
value,
|
value,
|
||||||
displayValue
|
displayValue,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}, [activeFilters, filterOptions]);
|
}, [activeFilters, filterOptions]);
|
||||||
@@ -354,15 +366,29 @@ export function ProductFilters({
|
|||||||
<ToggleGroup
|
<ToggleGroup
|
||||||
type="single"
|
type="single"
|
||||||
value={selectedOperator}
|
value={selectedOperator}
|
||||||
onValueChange={(value: ComparisonOperator) => value && setSelectedOperator(value)}
|
onValueChange={(value: ComparisonOperator) =>
|
||||||
|
value && setSelectedOperator(value)
|
||||||
|
}
|
||||||
className="flex-wrap"
|
className="flex-wrap"
|
||||||
>
|
>
|
||||||
<ToggleGroupItem value="=" aria-label="equals">=</ToggleGroupItem>
|
<ToggleGroupItem value="=" aria-label="equals">
|
||||||
<ToggleGroupItem value=">" aria-label="greater than">{'>'}</ToggleGroupItem>
|
=
|
||||||
<ToggleGroupItem value=">=" aria-label="greater than or equal">≥</ToggleGroupItem>
|
</ToggleGroupItem>
|
||||||
<ToggleGroupItem value="<" aria-label="less than">{'<'}</ToggleGroupItem>
|
<ToggleGroupItem value=">" aria-label="greater than">
|
||||||
<ToggleGroupItem value="<=" aria-label="less than or equal">≤</ToggleGroupItem>
|
{">"}
|
||||||
<ToggleGroupItem value="between" aria-label="between">Between</ToggleGroupItem>
|
</ToggleGroupItem>
|
||||||
|
<ToggleGroupItem value=">=" aria-label="greater than or equal">
|
||||||
|
≥
|
||||||
|
</ToggleGroupItem>
|
||||||
|
<ToggleGroupItem value="<" aria-label="less than">
|
||||||
|
{"<"}
|
||||||
|
</ToggleGroupItem>
|
||||||
|
<ToggleGroupItem value="<=" aria-label="less than or equal">
|
||||||
|
≤
|
||||||
|
</ToggleGroupItem>
|
||||||
|
<ToggleGroupItem value="between" aria-label="between">
|
||||||
|
Between
|
||||||
|
</ToggleGroupItem>
|
||||||
</ToggleGroup>
|
</ToggleGroup>
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -380,13 +406,14 @@ export function ProductFilters({
|
|||||||
{renderOperatorSelect()}
|
{renderOperatorSelect()}
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Input
|
<Input
|
||||||
|
ref={numberInputRef}
|
||||||
type="number"
|
type="number"
|
||||||
placeholder={`Enter ${selectedFilter?.label.toLowerCase()}`}
|
placeholder={`Enter ${selectedFilter?.label.toLowerCase()}`}
|
||||||
value={inputValue}
|
value={inputValue}
|
||||||
onChange={(e) => setInputValue(e.target.value)}
|
onChange={(e) => setInputValue(e.target.value)}
|
||||||
onKeyDown={(e) => {
|
onKeyDown={(e) => {
|
||||||
if (e.key === 'Enter') {
|
if (e.key === "Enter") {
|
||||||
if (selectedOperator === 'between') {
|
if (selectedOperator === "between") {
|
||||||
if (inputValue2) {
|
if (inputValue2) {
|
||||||
const val1 = parseFloat(inputValue);
|
const val1 = parseFloat(inputValue);
|
||||||
const val2 = parseFloat(inputValue2);
|
const val2 = parseFloat(inputValue2);
|
||||||
@@ -400,14 +427,14 @@ export function ProductFilters({
|
|||||||
handleApplyFilter(val);
|
handleApplyFilter(val);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (e.key === 'Escape') {
|
} else if (e.key === "Escape") {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
handleBackToFilters();
|
handleBackToFilters();
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
className="w-[120px] [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none"
|
className="w-[120px] [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none"
|
||||||
/>
|
/>
|
||||||
{selectedOperator === 'between' && (
|
{selectedOperator === "between" && (
|
||||||
<>
|
<>
|
||||||
<span>and</span>
|
<span>and</span>
|
||||||
<Input
|
<Input
|
||||||
@@ -416,13 +443,13 @@ export function ProductFilters({
|
|||||||
value={inputValue2}
|
value={inputValue2}
|
||||||
onChange={(e) => setInputValue2(e.target.value)}
|
onChange={(e) => setInputValue2(e.target.value)}
|
||||||
onKeyDown={(e) => {
|
onKeyDown={(e) => {
|
||||||
if (e.key === 'Enter') {
|
if (e.key === "Enter") {
|
||||||
const val1 = parseFloat(inputValue);
|
const val1 = parseFloat(inputValue);
|
||||||
const val2 = parseFloat(inputValue2);
|
const val2 = parseFloat(inputValue2);
|
||||||
if (!isNaN(val1) && !isNaN(val2)) {
|
if (!isNaN(val1) && !isNaN(val2)) {
|
||||||
handleApplyFilter([val1, val2]);
|
handleApplyFilter([val1, val2]);
|
||||||
}
|
}
|
||||||
} else if (e.key === 'Escape') {
|
} else if (e.key === "Escape") {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
handleBackToFilters();
|
handleBackToFilters();
|
||||||
}
|
}
|
||||||
@@ -433,7 +460,7 @@ export function ProductFilters({
|
|||||||
)}
|
)}
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (selectedOperator === 'between') {
|
if (selectedOperator === "between") {
|
||||||
const val1 = parseFloat(inputValue);
|
const val1 = parseFloat(inputValue);
|
||||||
const val2 = parseFloat(inputValue2);
|
const val2 = parseFloat(inputValue2);
|
||||||
if (!isNaN(val1) && !isNaN(val2)) {
|
if (!isNaN(val1) && !isNaN(val2)) {
|
||||||
@@ -455,7 +482,7 @@ export function ProductFilters({
|
|||||||
|
|
||||||
const getFilterDisplayValue = (filter: ActiveFilter) => {
|
const getFilterDisplayValue = (filter: ActiveFilter) => {
|
||||||
const filterValue = activeFilters[filter.id];
|
const filterValue = activeFilters[filter.id];
|
||||||
const filterOption = filterOptions.find(opt => opt.id === filter.id);
|
const filterOption = filterOptions.find((opt) => opt.id === filter.id);
|
||||||
|
|
||||||
// For between ranges
|
// For between ranges
|
||||||
if (Array.isArray(filterValue)) {
|
if (Array.isArray(filterValue)) {
|
||||||
@@ -463,8 +490,13 @@ export function ProductFilters({
|
|||||||
}
|
}
|
||||||
|
|
||||||
// For direct selections (select type) or text search
|
// For direct selections (select type) or text search
|
||||||
if (filterOption?.type === 'select' || filterOption?.type === 'text' || typeof filterValue !== 'object') {
|
if (
|
||||||
const value = typeof filterValue === 'object' ? filterValue.value : filterValue;
|
filterOption?.type === "select" ||
|
||||||
|
filterOption?.type === "text" ||
|
||||||
|
typeof filterValue !== "object"
|
||||||
|
) {
|
||||||
|
const value =
|
||||||
|
typeof filterValue === "object" ? filterValue.value : filterValue;
|
||||||
return `${filter.label}: ${value}`;
|
return `${filter.label}: ${value}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -472,12 +504,12 @@ export function ProductFilters({
|
|||||||
const operator = filterValue.operator;
|
const operator = filterValue.operator;
|
||||||
const value = filterValue.value;
|
const value = filterValue.value;
|
||||||
const operatorDisplay = {
|
const operatorDisplay = {
|
||||||
'=': '=',
|
"=": "=",
|
||||||
'>': '>',
|
">": ">",
|
||||||
'>=': '≥',
|
">=": "≥",
|
||||||
'<': '<',
|
"<": "<",
|
||||||
'<=': '≤',
|
"<=": "≤",
|
||||||
'between': 'between'
|
between: "between",
|
||||||
}[operator];
|
}[operator];
|
||||||
|
|
||||||
return `${filter.label} ${operatorDisplay} ${value}`;
|
return `${filter.label} ${operatorDisplay} ${value}`;
|
||||||
@@ -498,37 +530,29 @@ export function ProductFilters({
|
|||||||
modal={true}
|
modal={true}
|
||||||
>
|
>
|
||||||
<PopoverTrigger asChild>
|
<PopoverTrigger asChild>
|
||||||
<Button
|
<Button variant="outline" size="sm" className="h-8 border-dashed">
|
||||||
variant="outline"
|
<Plus
|
||||||
size="sm"
|
className={cn(
|
||||||
className="h-8 border-dashed"
|
"mr-2 h-4 w-4 transition-transform duration-200",
|
||||||
>
|
showCommand && "rotate-[135deg]"
|
||||||
<Plus className={cn(
|
)}
|
||||||
"mr-2 h-4 w-4 transition-transform duration-200",
|
/>
|
||||||
showCommand && "rotate-[135deg]"
|
|
||||||
)} />
|
|
||||||
{showCommand ? "Cancel" : "Add Filter"}
|
{showCommand ? "Cancel" : "Add Filter"}
|
||||||
</Button>
|
</Button>
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
<PopoverContent
|
<PopoverContent
|
||||||
className="p-0 w-[520px]"
|
className="p-0 w-[520px]"
|
||||||
align="start"
|
align="start"
|
||||||
onKeyDown={(e) => {
|
onEscapeKeyDown={(event) => {
|
||||||
if (e.key === 'Escape') {
|
console.log('Escape pressed, selectedFilter:', selectedFilter); // Debug log
|
||||||
e.preventDefault();
|
if (selectedFilter) {
|
||||||
e.stopPropagation();
|
event.preventDefault();
|
||||||
if (selectedFilter) {
|
event.stopPropagation();
|
||||||
handleBackToFilters();
|
handleBackToFilters();
|
||||||
} else {
|
|
||||||
handlePopoverClose();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Command
|
<Command className="rounded-none border-0" shouldFilter={false}>
|
||||||
className="rounded-none border-0"
|
|
||||||
shouldFilter={false}
|
|
||||||
>
|
|
||||||
{!selectedFilter ? (
|
{!selectedFilter ? (
|
||||||
<>
|
<>
|
||||||
<CommandInput
|
<CommandInput
|
||||||
@@ -536,7 +560,7 @@ export function ProductFilters({
|
|||||||
value={searchValue}
|
value={searchValue}
|
||||||
onValueChange={setSearchValue}
|
onValueChange={setSearchValue}
|
||||||
onKeyDown={(e) => {
|
onKeyDown={(e) => {
|
||||||
if (e.key === 'Escape') {
|
if (e.key === "Escape") {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
handlePopoverClose();
|
handlePopoverClose();
|
||||||
}
|
}
|
||||||
@@ -545,11 +569,14 @@ export function ProductFilters({
|
|||||||
<CommandList>
|
<CommandList>
|
||||||
<CommandEmpty>No filters found.</CommandEmpty>
|
<CommandEmpty>No filters found.</CommandEmpty>
|
||||||
{Object.entries(
|
{Object.entries(
|
||||||
filteredOptions.reduce<Record<string, FilterOption[]>>((acc, filter) => {
|
filteredOptions.reduce<Record<string, FilterOption[]>>(
|
||||||
if (!acc[filter.group]) acc[filter.group] = [];
|
(acc, filter) => {
|
||||||
acc[filter.group].push(filter);
|
if (!acc[filter.group]) acc[filter.group] = [];
|
||||||
return acc;
|
acc[filter.group].push(filter);
|
||||||
}, {})
|
return acc;
|
||||||
|
},
|
||||||
|
{}
|
||||||
|
)
|
||||||
).map(([group, filters]) => (
|
).map(([group, filters]) => (
|
||||||
<React.Fragment key={group}>
|
<React.Fragment key={group}>
|
||||||
<CommandGroup heading={group}>
|
<CommandGroup heading={group}>
|
||||||
@@ -559,7 +586,7 @@ export function ProductFilters({
|
|||||||
value={`${filter.id} ${filter.label}`}
|
value={`${filter.id} ${filter.label}`}
|
||||||
onSelect={() => {
|
onSelect={() => {
|
||||||
handleSelectFilter(filter);
|
handleSelectFilter(filter);
|
||||||
if (filter.type !== 'select') {
|
if (filter.type !== "select") {
|
||||||
setInputValue("");
|
setInputValue("");
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
@@ -582,7 +609,7 @@ export function ProductFilters({
|
|||||||
))}
|
))}
|
||||||
</CommandList>
|
</CommandList>
|
||||||
</>
|
</>
|
||||||
) : selectedFilter.type === 'number' ? (
|
) : selectedFilter.type === "number" ? (
|
||||||
<div className="p-4">
|
<div className="p-4">
|
||||||
<div className="flex flex-col gap-4 items-start">
|
<div className="flex flex-col gap-4 items-start">
|
||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
@@ -597,13 +624,14 @@ export function ProductFilters({
|
|||||||
{renderOperatorSelect()}
|
{renderOperatorSelect()}
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Input
|
<Input
|
||||||
|
ref={numberInputRef}
|
||||||
type="number"
|
type="number"
|
||||||
placeholder={`Enter ${selectedFilter?.label.toLowerCase()}`}
|
placeholder={`Enter ${selectedFilter?.label.toLowerCase()}`}
|
||||||
value={inputValue}
|
value={inputValue}
|
||||||
onChange={(e) => setInputValue(e.target.value)}
|
onChange={(e) => setInputValue(e.target.value)}
|
||||||
onKeyDown={(e) => {
|
onKeyDown={(e) => {
|
||||||
if (e.key === 'Enter') {
|
if (e.key === "Enter") {
|
||||||
if (selectedOperator === 'between') {
|
if (selectedOperator === "between") {
|
||||||
if (inputValue2) {
|
if (inputValue2) {
|
||||||
const val1 = parseFloat(inputValue);
|
const val1 = parseFloat(inputValue);
|
||||||
const val2 = parseFloat(inputValue2);
|
const val2 = parseFloat(inputValue2);
|
||||||
@@ -617,14 +645,14 @@ export function ProductFilters({
|
|||||||
handleApplyFilter(val);
|
handleApplyFilter(val);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (e.key === 'Escape') {
|
} else if (e.key === "Escape") {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
handleBackToFilters();
|
handleBackToFilters();
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
className="w-[120px] [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none"
|
className="w-[120px] [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none"
|
||||||
/>
|
/>
|
||||||
{selectedOperator === 'between' && (
|
{selectedOperator === "between" && (
|
||||||
<>
|
<>
|
||||||
<span>and</span>
|
<span>and</span>
|
||||||
<Input
|
<Input
|
||||||
@@ -633,13 +661,13 @@ export function ProductFilters({
|
|||||||
value={inputValue2}
|
value={inputValue2}
|
||||||
onChange={(e) => setInputValue2(e.target.value)}
|
onChange={(e) => setInputValue2(e.target.value)}
|
||||||
onKeyDown={(e) => {
|
onKeyDown={(e) => {
|
||||||
if (e.key === 'Enter') {
|
if (e.key === "Enter") {
|
||||||
const val1 = parseFloat(inputValue);
|
const val1 = parseFloat(inputValue);
|
||||||
const val2 = parseFloat(inputValue2);
|
const val2 = parseFloat(inputValue2);
|
||||||
if (!isNaN(val1) && !isNaN(val2)) {
|
if (!isNaN(val1) && !isNaN(val2)) {
|
||||||
handleApplyFilter([val1, val2]);
|
handleApplyFilter([val1, val2]);
|
||||||
}
|
}
|
||||||
} else if (e.key === 'Escape') {
|
} else if (e.key === "Escape") {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
handleBackToFilters();
|
handleBackToFilters();
|
||||||
}
|
}
|
||||||
@@ -650,7 +678,7 @@ export function ProductFilters({
|
|||||||
)}
|
)}
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (selectedOperator === 'between') {
|
if (selectedOperator === "between") {
|
||||||
const val1 = parseFloat(inputValue);
|
const val1 = parseFloat(inputValue);
|
||||||
const val2 = parseFloat(inputValue2);
|
const val2 = parseFloat(inputValue2);
|
||||||
if (!isNaN(val1) && !isNaN(val2)) {
|
if (!isNaN(val1) && !isNaN(val2)) {
|
||||||
@@ -669,17 +697,18 @@ export function ProductFilters({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : selectedFilter.type === 'select' ? (
|
) : selectedFilter.type === "select" ? (
|
||||||
<>
|
<>
|
||||||
<CommandInput
|
<CommandInput
|
||||||
|
ref={selectInputRef}
|
||||||
placeholder={`Select ${selectedFilter.label.toLowerCase()}...`}
|
placeholder={`Select ${selectedFilter.label.toLowerCase()}...`}
|
||||||
value={inputValue}
|
value={inputValue}
|
||||||
onValueChange={setInputValue}
|
onValueChange={setInputValue}
|
||||||
onKeyDown={(e) => {
|
onKeyDown={(e) => {
|
||||||
if (e.key === 'Backspace' && !inputValue) {
|
if (e.key === "Backspace" && !inputValue) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
handleBackToFilters();
|
handleBackToFilters();
|
||||||
} else if (e.key === 'Escape') {
|
} else if (e.key === "Escape") {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
handleBackToFilters();
|
handleBackToFilters();
|
||||||
}
|
}
|
||||||
@@ -696,8 +725,10 @@ export function ProductFilters({
|
|||||||
</CommandItem>
|
</CommandItem>
|
||||||
<CommandSeparator />
|
<CommandSeparator />
|
||||||
{selectedFilter.options
|
{selectedFilter.options
|
||||||
?.filter(option =>
|
?.filter((option) =>
|
||||||
option.label.toLowerCase().includes(inputValue.toLowerCase())
|
option.label
|
||||||
|
.toLowerCase()
|
||||||
|
.includes(inputValue.toLowerCase())
|
||||||
)
|
)
|
||||||
.map((option) => (
|
.map((option) => (
|
||||||
<CommandItem
|
<CommandItem
|
||||||
@@ -708,21 +739,21 @@ export function ProductFilters({
|
|||||||
>
|
>
|
||||||
{option.label}
|
{option.label}
|
||||||
</CommandItem>
|
</CommandItem>
|
||||||
))
|
))}
|
||||||
}
|
|
||||||
</CommandGroup>
|
</CommandGroup>
|
||||||
</CommandList>
|
</CommandList>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<CommandInput
|
<CommandInput
|
||||||
|
ref={textInputRef}
|
||||||
placeholder={`Enter ${selectedFilter.label.toLowerCase()}...`}
|
placeholder={`Enter ${selectedFilter.label.toLowerCase()}...`}
|
||||||
value={inputValue}
|
value={inputValue}
|
||||||
onValueChange={setInputValue}
|
onValueChange={setInputValue}
|
||||||
onKeyDown={(e) => {
|
onKeyDown={(e) => {
|
||||||
if (e.key === 'Enter' && inputValue.trim()) {
|
if (e.key === "Enter" && inputValue.trim()) {
|
||||||
handleApplyFilter(inputValue.trim());
|
handleApplyFilter(inputValue.trim());
|
||||||
} else if (e.key === 'Escape') {
|
} else if (e.key === "Escape") {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
handleBackToFilters();
|
handleBackToFilters();
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user