Fix column selector popover behavior, improve product filters
This commit is contained in:
@@ -231,32 +231,40 @@ export function ProductFilters({
|
|||||||
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(""); // For 'between' operator
|
const [inputValue2, setInputValue2] = React.useState("");
|
||||||
const [searchValue, setSearchValue] = 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
|
// Handle keyboard shortcuts
|
||||||
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();
|
||||||
setShowCommand(prev => !prev);
|
if (!showCommand) {
|
||||||
}
|
setShowCommand(true);
|
||||||
// Escape to close or go back
|
|
||||||
if (e.key === 'Escape') {
|
|
||||||
if (selectedFilter) {
|
|
||||||
setSelectedFilter(null);
|
|
||||||
setInputValue("");
|
|
||||||
} else {
|
} else {
|
||||||
setShowCommand(false);
|
handlePopoverClose();
|
||||||
setSearchValue("");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// 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]);
|
}, [selectedFilter, showCommand]);
|
||||||
|
|
||||||
// Update filter options with dynamic data
|
// Update filter options with dynamic data
|
||||||
const filterOptions = React.useMemo(() => {
|
const filterOptions = React.useMemo(() => {
|
||||||
@@ -311,33 +319,16 @@ export function ProductFilters({
|
|||||||
};
|
};
|
||||||
|
|
||||||
onFilterChange(newFilters as Record<string, ActiveFilterValue>);
|
onFilterChange(newFilters as Record<string, ActiveFilterValue>);
|
||||||
|
handlePopoverClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleBackToFilters = () => {
|
||||||
setSelectedFilter(null);
|
setSelectedFilter(null);
|
||||||
setSelectedOperator('=');
|
setSelectedOperator('=');
|
||||||
setInputValue("");
|
setInputValue("");
|
||||||
setInputValue2("");
|
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(() => {
|
const activeFiltersList = React.useMemo(() => {
|
||||||
if (!activeFilters) return [];
|
if (!activeFilters) return [];
|
||||||
|
|
||||||
@@ -377,6 +368,15 @@ export function ProductFilters({
|
|||||||
|
|
||||||
const renderNumberInput = () => (
|
const renderNumberInput = () => (
|
||||||
<div className="flex flex-col gap-4 items-start">
|
<div className="flex flex-col gap-4 items-start">
|
||||||
|
<div className="mb-4">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
onClick={handleBackToFilters}
|
||||||
|
className="text-muted-foreground"
|
||||||
|
>
|
||||||
|
← Back to filters
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
{renderOperatorSelect()}
|
{renderOperatorSelect()}
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Input
|
<Input
|
||||||
@@ -400,6 +400,9 @@ export function ProductFilters({
|
|||||||
handleApplyFilter(val);
|
handleApplyFilter(val);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} 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"
|
className="w-[120px] [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none"
|
||||||
@@ -419,6 +422,9 @@ export function ProductFilters({
|
|||||||
if (!isNaN(val1) && !isNaN(val2)) {
|
if (!isNaN(val1) && !isNaN(val2)) {
|
||||||
handleApplyFilter([val1, val2]);
|
handleApplyFilter([val1, val2]);
|
||||||
}
|
}
|
||||||
|
} 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"
|
className="w-[120px] [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none"
|
||||||
@@ -448,18 +454,23 @@ export function ProductFilters({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const getFilterDisplayValue = (filter: ActiveFilter) => {
|
const getFilterDisplayValue = (filter: ActiveFilter) => {
|
||||||
if (Array.isArray(filter.value)) {
|
const filterValue = activeFilters[filter.id];
|
||||||
return `between ${filter.value[0]} and ${filter.value[1]}`;
|
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];
|
// For direct selections (select type) or text search
|
||||||
const operator = typeof filterValue === 'object' && 'operator' in filterValue
|
if (filterOption?.type === 'select' || filterOption?.type === 'text' || typeof filterValue !== 'object') {
|
||||||
? filterValue.operator
|
const value = typeof filterValue === 'object' ? filterValue.value : filterValue;
|
||||||
: '=';
|
return `${filter.label}: ${value}`;
|
||||||
const value = typeof filterValue === 'object' && 'value' in filterValue
|
}
|
||||||
? filterValue.value
|
|
||||||
: filterValue;
|
|
||||||
|
|
||||||
|
// For numeric filters with operators
|
||||||
|
const operator = filterValue.operator;
|
||||||
|
const value = filterValue.value;
|
||||||
const operatorDisplay = {
|
const operatorDisplay = {
|
||||||
'=': '=',
|
'=': '=',
|
||||||
'>': '>',
|
'>': '>',
|
||||||
@@ -469,13 +480,23 @@ export function ProductFilters({
|
|||||||
'between': 'between'
|
'between': 'between'
|
||||||
}[operator];
|
}[operator];
|
||||||
|
|
||||||
return `${operatorDisplay} ${value}`;
|
return `${filter.label} ${operatorDisplay} ${value}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="flex flex-wrap items-center gap-2">
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
<Popover open={showCommand} onOpenChange={setShowCommand}>
|
<Popover
|
||||||
|
open={showCommand}
|
||||||
|
onOpenChange={(open) => {
|
||||||
|
if (!open) {
|
||||||
|
handlePopoverClose();
|
||||||
|
} else {
|
||||||
|
setShowCommand(true);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
modal={true}
|
||||||
|
>
|
||||||
<PopoverTrigger asChild>
|
<PopoverTrigger asChild>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
@@ -489,14 +510,37 @@ export function ProductFilters({
|
|||||||
{showCommand ? "Cancel" : "Add Filter"}
|
{showCommand ? "Cancel" : "Add Filter"}
|
||||||
</Button>
|
</Button>
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
<PopoverContent className="p-0 w-[520px]" align="start">
|
<PopoverContent
|
||||||
<Command className="rounded-none border-0" shouldFilter={false}>
|
className="p-0 w-[520px]"
|
||||||
|
align="start"
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === 'Escape') {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
if (selectedFilter) {
|
||||||
|
handleBackToFilters();
|
||||||
|
} else {
|
||||||
|
handlePopoverClose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Command
|
||||||
|
className="rounded-none border-0"
|
||||||
|
shouldFilter={false}
|
||||||
|
>
|
||||||
{!selectedFilter ? (
|
{!selectedFilter ? (
|
||||||
<>
|
<>
|
||||||
<CommandInput
|
<CommandInput
|
||||||
placeholder="Search and select filters..."
|
placeholder="Search and select filters..."
|
||||||
value={searchValue}
|
value={searchValue}
|
||||||
onValueChange={setSearchValue}
|
onValueChange={setSearchValue}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === 'Escape') {
|
||||||
|
e.preventDefault();
|
||||||
|
handlePopoverClose();
|
||||||
|
}
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
<CommandList>
|
<CommandList>
|
||||||
<CommandEmpty>No filters found.</CommandEmpty>
|
<CommandEmpty>No filters found.</CommandEmpty>
|
||||||
@@ -540,16 +584,90 @@ export function ProductFilters({
|
|||||||
</>
|
</>
|
||||||
) : selectedFilter.type === 'number' ? (
|
) : selectedFilter.type === 'number' ? (
|
||||||
<div className="p-4">
|
<div className="p-4">
|
||||||
<div className="mb-4">
|
<div className="flex flex-col gap-4 items-start">
|
||||||
<Button
|
<div className="mb-4">
|
||||||
variant="ghost"
|
<Button
|
||||||
onClick={() => setSelectedFilter(null)}
|
variant="ghost"
|
||||||
className="text-muted-foreground"
|
onClick={handleBackToFilters}
|
||||||
>
|
className="text-muted-foreground"
|
||||||
← Back to filters
|
>
|
||||||
</Button>
|
← Back to filters
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
{renderOperatorSelect()}
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Input
|
||||||
|
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 (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' && (
|
||||||
|
<>
|
||||||
|
<span>and</span>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
placeholder={`Enter maximum`}
|
||||||
|
value={inputValue2}
|
||||||
|
onChange={(e) => 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"
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
if (selectedOperator === 'between') {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Apply
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{renderNumberInput()}
|
|
||||||
</div>
|
</div>
|
||||||
) : selectedFilter.type === 'select' ? (
|
) : selectedFilter.type === 'select' ? (
|
||||||
<>
|
<>
|
||||||
@@ -560,9 +678,10 @@ export function ProductFilters({
|
|||||||
onKeyDown={(e) => {
|
onKeyDown={(e) => {
|
||||||
if (e.key === 'Backspace' && !inputValue) {
|
if (e.key === 'Backspace' && !inputValue) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setSelectedFilter(null);
|
handleBackToFilters();
|
||||||
} else {
|
} else if (e.key === 'Escape') {
|
||||||
handleKeyDown(e);
|
e.preventDefault();
|
||||||
|
handleBackToFilters();
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@@ -570,10 +689,7 @@ export function ProductFilters({
|
|||||||
<CommandEmpty>No options found.</CommandEmpty>
|
<CommandEmpty>No options found.</CommandEmpty>
|
||||||
<CommandGroup>
|
<CommandGroup>
|
||||||
<CommandItem
|
<CommandItem
|
||||||
onSelect={() => {
|
onSelect={handleBackToFilters}
|
||||||
setSelectedFilter(null);
|
|
||||||
setInputValue("");
|
|
||||||
}}
|
|
||||||
className="cursor-pointer text-muted-foreground"
|
className="cursor-pointer text-muted-foreground"
|
||||||
>
|
>
|
||||||
← Back to filters
|
← Back to filters
|
||||||
@@ -587,10 +703,7 @@ export function ProductFilters({
|
|||||||
<CommandItem
|
<CommandItem
|
||||||
key={option.value}
|
key={option.value}
|
||||||
value={option.value}
|
value={option.value}
|
||||||
onSelect={() => {
|
onSelect={() => handleApplyFilter(option.value)}
|
||||||
handleApplyFilter(option.value);
|
|
||||||
setShowCommand(false);
|
|
||||||
}}
|
|
||||||
className="cursor-pointer"
|
className="cursor-pointer"
|
||||||
>
|
>
|
||||||
{option.label}
|
{option.label}
|
||||||
@@ -603,68 +716,35 @@ export function ProductFilters({
|
|||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<CommandInput
|
<CommandInput
|
||||||
placeholder={`Enter ${selectedFilter.label.toLowerCase()}`}
|
placeholder={`Enter ${selectedFilter.label.toLowerCase()}...`}
|
||||||
value={inputValue}
|
value={inputValue}
|
||||||
onValueChange={(value) => {
|
onValueChange={setInputValue}
|
||||||
if (selectedFilter.type === 'number') {
|
|
||||||
if (/^\d*\.?\d*$/.test(value)) {
|
|
||||||
setInputValue(value);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
setInputValue(value);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
onKeyDown={(e) => {
|
onKeyDown={(e) => {
|
||||||
if (e.key === 'Backspace' && !inputValue) {
|
if (e.key === 'Enter' && inputValue.trim()) {
|
||||||
|
handleApplyFilter(inputValue.trim());
|
||||||
|
} else if (e.key === 'Escape') {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setSelectedFilter(null);
|
handleBackToFilters();
|
||||||
} 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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<CommandList>
|
<CommandList>
|
||||||
<CommandGroup>
|
<CommandGroup>
|
||||||
<CommandItem
|
<CommandItem
|
||||||
onSelect={() => {
|
onSelect={handleBackToFilters}
|
||||||
setSelectedFilter(null);
|
|
||||||
setInputValue("");
|
|
||||||
}}
|
|
||||||
className="cursor-pointer text-muted-foreground"
|
className="cursor-pointer text-muted-foreground"
|
||||||
>
|
>
|
||||||
← Back to filters
|
← Back to filters
|
||||||
</CommandItem>
|
</CommandItem>
|
||||||
<CommandSeparator />
|
<CommandSeparator />
|
||||||
<CommandItem
|
{inputValue.trim() && (
|
||||||
onSelect={() => {
|
<CommandItem
|
||||||
if (selectedFilter.type === 'number') {
|
onSelect={() => handleApplyFilter(inputValue.trim())}
|
||||||
const numValue = parseFloat(inputValue);
|
className="cursor-pointer"
|
||||||
if (!isNaN(numValue)) {
|
>
|
||||||
handleApplyFilter(numValue);
|
Apply filter: {inputValue}
|
||||||
setShowCommand(false);
|
</CommandItem>
|
||||||
}
|
)}
|
||||||
} else {
|
|
||||||
if (inputValue.trim() !== '') {
|
|
||||||
handleApplyFilter(inputValue.trim());
|
|
||||||
setShowCommand(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
className="cursor-pointer"
|
|
||||||
>
|
|
||||||
Apply filter: {inputValue}
|
|
||||||
</CommandItem>
|
|
||||||
</CommandGroup>
|
</CommandGroup>
|
||||||
</CommandList>
|
</CommandList>
|
||||||
</>
|
</>
|
||||||
@@ -678,9 +758,7 @@ export function ProductFilters({
|
|||||||
variant="secondary"
|
variant="secondary"
|
||||||
className="flex items-center gap-1"
|
className="flex items-center gap-1"
|
||||||
>
|
>
|
||||||
<span>
|
<span>{getFilterDisplayValue(filter)}</span>
|
||||||
{filter.label}: {getFilterDisplayValue(filter)}
|
|
||||||
</span>
|
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
@@ -688,7 +766,6 @@ export function ProductFilters({
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
const newFilters = { ...activeFilters };
|
const newFilters = { ...activeFilters };
|
||||||
delete newFilters[filter.id];
|
delete newFilters[filter.id];
|
||||||
delete newFilters[`${filter.id}_operator`];
|
|
||||||
onFilterChange(newFilters);
|
onFilterChange(newFilters);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import * as DialogPrimitive from "@radix-ui/react-dialog"
|
import * as DialogPrimitive from "@radix-ui/react-dialog"
|
||||||
import { X } from "lucide-react"
|
import { X } from "lucide-react"
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import * as React from "react";
|
||||||
import { useState, useEffect, useMemo } from 'react';
|
import { useState, useEffect, useMemo } from 'react';
|
||||||
import { useQuery, keepPreviousData } from '@tanstack/react-query';
|
import { useQuery, keepPreviousData } from '@tanstack/react-query';
|
||||||
import { useSearchParams } from 'react-router-dom';
|
import { useSearchParams } from 'react-router-dom';
|
||||||
@@ -340,51 +341,77 @@ export function Products() {
|
|||||||
return acc;
|
return acc;
|
||||||
}, {} as Record<string, typeof AVAILABLE_COLUMNS>);
|
}, {} as Record<string, typeof AVAILABLE_COLUMNS>);
|
||||||
|
|
||||||
const renderColumnToggle = () => (
|
const renderColumnToggle = () => {
|
||||||
<DropdownMenu modal={false}>
|
const [open, setOpen] = React.useState(false);
|
||||||
<DropdownMenuTrigger asChild>
|
|
||||||
<Button variant="outline" className="ml-auto">
|
return (
|
||||||
<Settings2 className="mr-2 h-4 w-4" />
|
<DropdownMenu open={open} onOpenChange={setOpen} modal={false}>
|
||||||
Columns
|
<DropdownMenuTrigger asChild>
|
||||||
</Button>
|
<Button variant="outline" className="ml-auto">
|
||||||
</DropdownMenuTrigger>
|
<Settings2 className="mr-2 h-4 w-4" />
|
||||||
<DropdownMenuContent
|
Columns
|
||||||
align="end"
|
</Button>
|
||||||
className="w-[500px] max-h-[calc(100vh-4rem)] overflow-y-auto"
|
</DropdownMenuTrigger>
|
||||||
onCloseAutoFocus={(e) => e.preventDefault()}
|
<DropdownMenuContent
|
||||||
>
|
align="end"
|
||||||
<DropdownMenuLabel className="sticky top-0 bg-background z-10">Toggle columns</DropdownMenuLabel>
|
className="w-[500px] max-h-[calc(100vh-4rem)] overflow-y-auto"
|
||||||
<DropdownMenuSeparator className="sticky top-[29px] bg-background z-10" />
|
onCloseAutoFocus={(e) => e.preventDefault()}
|
||||||
<div className="grid grid-cols-2 gap-4">
|
onPointerDownOutside={(e) => {
|
||||||
{Object.entries(columnsByGroup).map(([group, columns]) => (
|
// Only close if clicking outside the dropdown
|
||||||
<div key={group}>
|
if (!(e.target as HTMLElement).closest('[role="dialog"]')) {
|
||||||
<DropdownMenuLabel className="text-xs font-normal text-muted-foreground">
|
setOpen(false);
|
||||||
{group}
|
}
|
||||||
</DropdownMenuLabel>
|
}}
|
||||||
{columns.map((column) => (
|
onInteractOutside={(e) => {
|
||||||
<DropdownMenuCheckboxItem
|
// Prevent closing when interacting with checkboxes
|
||||||
key={column.key}
|
if ((e.target as HTMLElement).closest('[role="dialog"]')) {
|
||||||
className="capitalize"
|
e.preventDefault();
|
||||||
checked={visibleColumns.has(column.key)}
|
}
|
||||||
onCheckedChange={(checked) => handleColumnVisibilityChange(column.key, checked)}
|
}}
|
||||||
>
|
|
||||||
{column.label}
|
|
||||||
</DropdownMenuCheckboxItem>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<DropdownMenuSeparator />
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
className="w-full justify-start"
|
|
||||||
onClick={resetColumnsToDefault}
|
|
||||||
>
|
>
|
||||||
Reset to Default
|
<DropdownMenuLabel className="sticky top-0 bg-background z-10">Toggle columns</DropdownMenuLabel>
|
||||||
</Button>
|
<DropdownMenuSeparator className="sticky top-[29px] bg-background z-10" />
|
||||||
</DropdownMenuContent>
|
<div className="grid grid-cols-2 gap-4">
|
||||||
</DropdownMenu>
|
{Object.entries(columnsByGroup).map(([group, columns]) => (
|
||||||
);
|
<div key={group}>
|
||||||
|
<DropdownMenuLabel className="text-xs font-normal text-muted-foreground">
|
||||||
|
{group}
|
||||||
|
</DropdownMenuLabel>
|
||||||
|
{columns.map((column) => (
|
||||||
|
<DropdownMenuCheckboxItem
|
||||||
|
key={column.key}
|
||||||
|
className="capitalize"
|
||||||
|
checked={visibleColumns.has(column.key)}
|
||||||
|
onCheckedChange={(checked) => {
|
||||||
|
handleColumnVisibilityChange(column.key, checked);
|
||||||
|
}}
|
||||||
|
onSelect={(e) => {
|
||||||
|
// Prevent closing by stopping propagation
|
||||||
|
e.preventDefault();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{column.label}
|
||||||
|
</DropdownMenuCheckboxItem>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
className="w-full justify-start"
|
||||||
|
onClick={(e) => {
|
||||||
|
resetColumnsToDefault();
|
||||||
|
// Prevent closing by stopping propagation
|
||||||
|
e.stopPropagation();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Reset to Default
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
// Calculate pagination numbers
|
// Calculate pagination numbers
|
||||||
const totalPages = data?.pagination.pages || 1;
|
const totalPages = data?.pagination.pages || 1;
|
||||||
|
|||||||
Reference in New Issue
Block a user