From 3c600659e51f858013bd987475d6a4bf40835111 Mon Sep 17 00:00:00 2001 From: Matt Date: Wed, 15 Jan 2025 16:07:06 -0500 Subject: [PATCH] Fix column selector popover behavior, improve product filters --- .../components/products/ProductFilters.tsx | 313 +++++++++++------- inventory/src/components/ui/dialog.tsx | 2 + inventory/src/pages/Products.tsx | 115 ++++--- 3 files changed, 268 insertions(+), 162 deletions(-) diff --git a/inventory/src/components/products/ProductFilters.tsx b/inventory/src/components/products/ProductFilters.tsx index 56a80a3..0e5e739 100644 --- a/inventory/src/components/products/ProductFilters.tsx +++ b/inventory/src/components/products/ProductFilters.tsx @@ -231,32 +231,40 @@ export function ProductFilters({ const [selectedFilter, setSelectedFilter] = React.useState(null); const [selectedOperator, setSelectedOperator] = React.useState('='); const [inputValue, setInputValue] = React.useState(""); - const [inputValue2, setInputValue2] = React.useState(""); // For 'between' operator + const [inputValue2, setInputValue2] = React.useState(""); const [searchValue, setSearchValue] = React.useState(""); + // Reset states when popup closes + const handlePopoverClose = () => { + setShowCommand(false); + setSelectedFilter(null); + setSelectedOperator('='); + setInputValue(""); + setInputValue2(""); + setSearchValue(""); + }; + // Handle keyboard shortcuts React.useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { // Command/Ctrl + K to toggle filter if ((e.metaKey || e.ctrlKey) && e.key === 'k') { e.preventDefault(); - setShowCommand(prev => !prev); - } - // Escape to close or go back - if (e.key === 'Escape') { - if (selectedFilter) { - setSelectedFilter(null); - setInputValue(""); + if (!showCommand) { + setShowCommand(true); } else { - setShowCommand(false); - setSearchValue(""); + handlePopoverClose(); } } + // Only handle Escape at the root level + if (e.key === 'Escape' && !selectedFilter) { + handlePopoverClose(); + } }; window.addEventListener('keydown', handleKeyDown); return () => window.removeEventListener('keydown', handleKeyDown); - }, [selectedFilter]); + }, [selectedFilter, showCommand]); // Update filter options with dynamic data const filterOptions = React.useMemo(() => { @@ -311,33 +319,16 @@ export function ProductFilters({ }; onFilterChange(newFilters as Record); + handlePopoverClose(); + }; + + const handleBackToFilters = () => { setSelectedFilter(null); setSelectedOperator('='); setInputValue(""); setInputValue2(""); - setSearchValue(""); }; - const handleKeyDown = React.useCallback((e: React.KeyboardEvent) => { - if (e.key === 'Enter' && selectedFilter) { - if (selectedFilter.type === 'select') { - const option = selectedFilter.options?.find(opt => - opt.label.toLowerCase().includes(inputValue.toLowerCase()) - ); - if (option) { - handleApplyFilter(option.value); - } - } else if (selectedFilter.type === 'number') { - const numValue = parseFloat(inputValue); - if (!isNaN(numValue)) { - handleApplyFilter(numValue); - } - } else if (selectedFilter.type === 'text' && inputValue.trim() !== '') { - handleApplyFilter(inputValue.trim()); - } - } - }, [selectedFilter, inputValue]); - const activeFiltersList = React.useMemo(() => { if (!activeFilters) return []; @@ -377,6 +368,15 @@ export function ProductFilters({ const renderNumberInput = () => (
+
+ +
{renderOperatorSelect()}
{ - if (Array.isArray(filter.value)) { - return `between ${filter.value[0]} and ${filter.value[1]}`; + const filterValue = activeFilters[filter.id]; + const filterOption = filterOptions.find(opt => opt.id === filter.id); + + // For between ranges + if (Array.isArray(filterValue)) { + return `${filter.label} between ${filterValue[0]} and ${filterValue[1]}`; } - const filterValue = activeFilters[filter.id]; - const operator = typeof filterValue === 'object' && 'operator' in filterValue - ? filterValue.operator - : '='; - const value = typeof filterValue === 'object' && 'value' in filterValue - ? filterValue.value - : filterValue; - + // 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; + return `${filter.label}: ${value}`; + } + + // For numeric filters with operators + const operator = filterValue.operator; + const value = filterValue.value; const operatorDisplay = { '=': '=', '>': '>', @@ -469,13 +480,23 @@ export function ProductFilters({ 'between': 'between' }[operator]; - return `${operatorDisplay} ${value}`; + return `${filter.label} ${operatorDisplay} ${value}`; }; return (
- + { + if (!open) { + handlePopoverClose(); + } else { + setShowCommand(true); + } + }} + modal={true} + > +
+
+ +
+ {renderOperatorSelect()} +
+ setInputValue(e.target.value)} + onKeyDown={(e) => { + if (e.key === 'Enter') { + if (selectedOperator === 'between') { + if (inputValue2) { + const val1 = parseFloat(inputValue); + const val2 = parseFloat(inputValue2); + if (!isNaN(val1) && !isNaN(val2)) { + handleApplyFilter([val1, val2]); + } + } + } else { + const val = parseFloat(inputValue); + if (!isNaN(val)) { + handleApplyFilter(val); + } + } + } 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' && ( + <> + and + setInputValue2(e.target.value)} + onKeyDown={(e) => { + 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') { + e.preventDefault(); + handleBackToFilters(); + } + }} + className="w-[120px] [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none" + /> + + )} + +
- {renderNumberInput()}
) : selectedFilter.type === 'select' ? ( <> @@ -560,9 +678,10 @@ export function ProductFilters({ onKeyDown={(e) => { if (e.key === 'Backspace' && !inputValue) { e.preventDefault(); - setSelectedFilter(null); - } else { - handleKeyDown(e); + handleBackToFilters(); + } else if (e.key === 'Escape') { + e.preventDefault(); + handleBackToFilters(); } }} /> @@ -570,10 +689,7 @@ export function ProductFilters({ No options found. { - setSelectedFilter(null); - setInputValue(""); - }} + onSelect={handleBackToFilters} className="cursor-pointer text-muted-foreground" > ← Back to filters @@ -587,10 +703,7 @@ export function ProductFilters({ { - handleApplyFilter(option.value); - setShowCommand(false); - }} + onSelect={() => handleApplyFilter(option.value)} className="cursor-pointer" > {option.label} @@ -603,68 +716,35 @@ export function ProductFilters({ ) : ( <> { - if (selectedFilter.type === 'number') { - if (/^\d*\.?\d*$/.test(value)) { - setInputValue(value); - } - } else { - setInputValue(value); - } - }} + onValueChange={setInputValue} onKeyDown={(e) => { - if (e.key === 'Backspace' && !inputValue) { + if (e.key === 'Enter' && inputValue.trim()) { + handleApplyFilter(inputValue.trim()); + } else if (e.key === 'Escape') { e.preventDefault(); - setSelectedFilter(null); - } else if (e.key === 'Enter') { - if (selectedFilter.type === 'number') { - const numValue = parseFloat(inputValue); - if (!isNaN(numValue)) { - handleApplyFilter(numValue); - setShowCommand(false); - } - } else { - if (inputValue.trim() !== '') { - handleApplyFilter(inputValue.trim()); - setShowCommand(false); - } - } + handleBackToFilters(); } }} /> { - setSelectedFilter(null); - setInputValue(""); - }} + onSelect={handleBackToFilters} className="cursor-pointer text-muted-foreground" > ← Back to filters - { - if (selectedFilter.type === 'number') { - const numValue = parseFloat(inputValue); - if (!isNaN(numValue)) { - handleApplyFilter(numValue); - setShowCommand(false); - } - } else { - if (inputValue.trim() !== '') { - handleApplyFilter(inputValue.trim()); - setShowCommand(false); - } - } - }} - className="cursor-pointer" - > - Apply filter: {inputValue} - + {inputValue.trim() && ( + handleApplyFilter(inputValue.trim())} + className="cursor-pointer" + > + Apply filter: {inputValue} + + )} @@ -678,9 +758,7 @@ export function ProductFilters({ variant="secondary" className="flex items-center gap-1" > - - {filter.label}: {getFilterDisplayValue(filter)} - + {getFilterDisplayValue(filter)} - - e.preventDefault()} - > - Toggle columns - -
- {Object.entries(columnsByGroup).map(([group, columns]) => ( -
- - {group} - - {columns.map((column) => ( - handleColumnVisibilityChange(column.key, checked)} - > - {column.label} - - ))} -
- ))} -
- - + + e.preventDefault()} + onPointerDownOutside={(e) => { + // Only close if clicking outside the dropdown + if (!(e.target as HTMLElement).closest('[role="dialog"]')) { + setOpen(false); + } + }} + onInteractOutside={(e) => { + // Prevent closing when interacting with checkboxes + if ((e.target as HTMLElement).closest('[role="dialog"]')) { + e.preventDefault(); + } + }} > - Reset to Default - - - - ); + Toggle columns + +
+ {Object.entries(columnsByGroup).map(([group, columns]) => ( +
+ + {group} + + {columns.map((column) => ( + { + handleColumnVisibilityChange(column.key, checked); + }} + onSelect={(e) => { + // Prevent closing by stopping propagation + e.preventDefault(); + }} + > + {column.label} + + ))} +
+ ))} +
+ + +
+ + ); + }; // Calculate pagination numbers const totalPages = data?.pagination.pages || 1;