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