import React, { useState, useMemo } from 'react'; import { Search, Check, X, Filter } from 'lucide-react'; import { cn } from '../lib/utils'; type FilterType = 'equals' | 'notEquals' | 'contains' | 'startsWith' | 'endsWith' | 'greaterThan' | 'lessThan' | 'between'; interface FilterCondition { type: FilterType; value: string; value2?: string; } interface ColumnFilterPopoverProps { uniqueValues: string[]; selectedValues: string[]; onToggle: (val: string) => void; onSelectAll: (vals: string[]) => void; onClear: () => void; onClose: () => void; title?: string; className?: string; } export function ColumnFilterPopover({ uniqueValues, selectedValues, onToggle, onSelectAll, onClear, onClose, title, className }: ColumnFilterPopoverProps) { const [search, setSearch] = useState(''); const [activeTab, setActiveTab] = useState<'values' | 'condition'>('values'); const [condition, setCondition] = useState({ type: 'equals', value: '' }); const [conditionResult, setConditionResult] = useState([]); const isDraggingRef = React.useRef(false); const dragStartRef = React.useRef(null); const hoveredIndexRef = React.useRef(null); const selectedValuesRef = React.useRef(selectedValues); const onSelectAllRef = React.useRef(onSelectAll); const filteredValuesRef = React.useRef([]); const didDragRef = React.useRef(false); const [isDragging, setIsDragging] = useState(false); const [dragStart, setDragStart] = useState(null); const [hoveredIndex, setHoveredIndex] = useState(null); const [lastClickedIndex, setLastClickedIndex] = useState(null); const listRef = React.useRef(null); const filteredValues = useMemo(() => { return uniqueValues.filter(v => String(v || '').toLowerCase().includes(search.toLowerCase()) ); }, [uniqueValues, search]); filteredValuesRef.current = filteredValues; selectedValuesRef.current = selectedValues; onSelectAllRef.current = onSelectAll; const isAllSelected = selectedValues.length === uniqueValues.length && uniqueValues.length > 0; const applyCondition = () => { const results: string[] = []; const valNum = parseFloat(condition.value); const val2Num = condition.value2 ? parseFloat(condition.value2) : 0; filteredValues.forEach(v => { const strVal = String(v || ''); const numVal = parseFloat(v); let matches = false; switch (condition.type) { case 'equals': matches = strVal.toLowerCase() === condition.value.toLowerCase(); break; case 'notEquals': matches = strVal.toLowerCase() !== condition.value.toLowerCase(); break; case 'contains': matches = strVal.toLowerCase().includes(condition.value.toLowerCase()); break; case 'startsWith': matches = strVal.toLowerCase().startsWith(condition.value.toLowerCase()); break; case 'endsWith': matches = strVal.toLowerCase().endsWith(condition.value.toLowerCase()); break; case 'greaterThan': matches = !isNaN(numVal) && !isNaN(valNum) && numVal > valNum; break; case 'lessThan': matches = !isNaN(numVal) && !isNaN(valNum) && numVal < valNum; break; case 'between': matches = !isNaN(numVal) && !isNaN(val2Num) && numVal >= valNum && numVal <= val2Num; break; } if (matches) results.push(v); }); setConditionResult(results); onSelectAll(results); setActiveTab('values'); }; const handleMouseDown = (index: number) => { isDraggingRef.current = true; dragStartRef.current = index; hoveredIndexRef.current = index; didDragRef.current = false; setIsDragging(true); setDragStart(index); setHoveredIndex(index); }; const handleMouseEnter = (index: number) => { if (isDraggingRef.current && dragStartRef.current !== null) { hoveredIndexRef.current = index; setHoveredIndex(index); } }; React.useEffect(() => { const handleGlobalMouseUp = () => { if (isDraggingRef.current) { const start = dragStartRef.current; const end = hoveredIndexRef.current; if (start !== null && end !== null && start !== end) { const s = Math.min(start, end); const e = Math.max(start, end); const itemsToSelect = filteredValuesRef.current.slice(s, e + 1); const newSelected = new Set([...selectedValuesRef.current]); itemsToSelect.forEach(v => newSelected.add(v)); onSelectAllRef.current(Array.from(newSelected)); didDragRef.current = true; setTimeout(() => { didDragRef.current = false; }, 100); } isDraggingRef.current = false; dragStartRef.current = null; hoveredIndexRef.current = null; setIsDragging(false); setDragStart(null); setHoveredIndex(null); } }; document.addEventListener('mouseup', handleGlobalMouseUp); return () => document.removeEventListener('mouseup', handleGlobalMouseUp); }, []); return (
e.stopPropagation()} > {title &&
{title}
}
{activeTab === 'values' ? ( <>
setSearch(e.target.value)} className="w-full bg-slate-900 border border-slate-700 rounded p-1.5 pl-8 pr-7 text-xs text-white focus:outline-none focus:border-blue-500" autoFocus /> {search && ( )}
{filteredValues.map((val, idx) => { const isDragSelected = isDragging && dragStart !== null && hoveredIndex !== null && ((idx >= dragStart && idx <= hoveredIndex) || (idx <= dragStart && idx >= hoveredIndex)); return (
{ e.preventDefault(); handleMouseDown(idx); }} onMouseEnter={() => handleMouseEnter(idx)} onClick={(e) => { if (didDragRef.current) return; if (e.shiftKey && lastClickedIndex !== null) { const start = Math.min(lastClickedIndex, idx); const end = Math.max(lastClickedIndex, idx); const itemsToSelect = filteredValues.slice(start, end + 1); const newSelected = new Set([...selectedValues, ...itemsToSelect]); onSelectAll(Array.from(newSelected)); } else { onToggle(val); } setLastClickedIndex(idx); }} onKeyDown={(e) => { if (e.key === ' ' || e.key === 'Enter') { e.preventDefault(); onToggle(val); setLastClickedIndex(idx); } }} className={cn( "flex items-center gap-2 p-1.5 rounded cursor-pointer group transition-colors select-none", isDragSelected ? "bg-blue-600/40" : "hover:bg-slate-700/50" )} >
{selectedValues.includes(val) && }
{val || '(Empty)'}
); })} {filteredValues.length === 0 && (
No values found
)}
) : ( <>
setCondition({ ...condition, value: e.target.value })} placeholder="Enter value..." className="w-full mt-1 bg-slate-900 border border-slate-700 rounded p-1.5 text-xs text-white focus:outline-none focus:border-blue-500" />
{condition.type === 'between' && (
setCondition({ ...condition, value2: e.target.value })} placeholder="Enter second value..." className="w-full mt-1 bg-slate-900 border border-slate-700 rounded p-1.5 text-xs text-white focus:outline-none focus:border-blue-500" />
)}
{conditionResult.length > 0 && (
Found {conditionResult.length} matching value{conditionResult.length !== 1 ? 's' : ''}
)} )}
); }