fix: resolve white screen crash in PricingView filters by adding comprehensive safety checks

This commit is contained in:
Christian Vidal Wolf
2026-04-24 20:19:01 +02:00
parent 127363a308
commit dcbaaea055
2 changed files with 39 additions and 35 deletions
+13 -10
View File
@@ -21,6 +21,7 @@ interface ColumnFilterPopoverProps {
title?: string; title?: string;
className?: string; className?: string;
zIndex?: number; zIndex?: number;
triggerId?: string;
} }
export function ColumnFilterPopover({ export function ColumnFilterPopover({
@@ -73,8 +74,10 @@ export function ColumnFilterPopover({
const isDraggingRef = React.useRef(false); const isDraggingRef = React.useRef(false);
const dragStartRef = React.useRef<number | null>(null); const dragStartRef = React.useRef<number | null>(null);
const safeUniqueValues = uniqueValues || [];
const safeSelectedValues = selectedValues || [];
const hoveredIndexRef = React.useRef<number | null>(null); const hoveredIndexRef = React.useRef<number | null>(null);
const selectedValuesRef = React.useRef<string[]>(selectedValues); const selectedValuesRef = React.useRef<string[]>(safeSelectedValues);
const onSelectAllRef = React.useRef(onSelectAll); const onSelectAllRef = React.useRef(onSelectAll);
const filteredValuesRef = React.useRef<string[]>([]); const filteredValuesRef = React.useRef<string[]>([]);
const didDragRef = React.useRef(false); const didDragRef = React.useRef(false);
@@ -85,16 +88,16 @@ export function ColumnFilterPopover({
const listRef = React.useRef<HTMLDivElement>(null); const listRef = React.useRef<HTMLDivElement>(null);
const filteredValues = useMemo(() => { const filteredValues = useMemo(() => {
return uniqueValues.filter(v => return safeUniqueValues.filter(v =>
String(v || '').toLowerCase().includes(search.toLowerCase()) String(v || '').toLowerCase().includes(search.toLowerCase())
); );
}, [uniqueValues, search]); }, [safeUniqueValues, search]);
filteredValuesRef.current = filteredValues; filteredValuesRef.current = filteredValues;
selectedValuesRef.current = selectedValues; selectedValuesRef.current = safeSelectedValues;
onSelectAllRef.current = onSelectAll; onSelectAllRef.current = onSelectAll;
const isAllSelected = selectedValues.length === uniqueValues.length && uniqueValues.length > 0; const isAllSelected = safeSelectedValues.length === safeUniqueValues.length && safeUniqueValues.length > 0;
const applyCondition = () => { const applyCondition = () => {
const results: string[] = []; const results: string[] = [];
@@ -221,7 +224,7 @@ export function ColumnFilterPopover({
activeTab === 'values' ? "bg-blue-600 text-white" : "text-slate-400 hover:text-white" activeTab === 'values' ? "bg-blue-600 text-white" : "text-slate-400 hover:text-white"
)} )}
> >
Values ({selectedValues.length}) Values ({safeSelectedValues.length})
</button> </button>
<button <button
onClick={() => setActiveTab('condition')} onClick={() => setActiveTab('condition')}
@@ -277,7 +280,7 @@ export function ColumnFilterPopover({
<div <div
key={val} key={val}
role="checkbox" role="checkbox"
aria-checked={selectedValues.includes(val)} aria-checked={safeSelectedValues.includes(val)}
tabIndex={0} tabIndex={0}
onMouseDown={(e) => { e.preventDefault(); handleMouseDown(idx); }} onMouseDown={(e) => { e.preventDefault(); handleMouseDown(idx); }}
onMouseEnter={() => handleMouseEnter(idx)} onMouseEnter={() => handleMouseEnter(idx)}
@@ -287,7 +290,7 @@ export function ColumnFilterPopover({
const start = Math.min(lastClickedIndex, idx); const start = Math.min(lastClickedIndex, idx);
const end = Math.max(lastClickedIndex, idx); const end = Math.max(lastClickedIndex, idx);
const itemsToSelect = filteredValues.slice(start, end + 1); const itemsToSelect = filteredValues.slice(start, end + 1);
const newSelected = new Set([...selectedValues, ...itemsToSelect]); const newSelected = new Set([...safeSelectedValues, ...itemsToSelect]);
onSelectAll(Array.from(newSelected)); onSelectAll(Array.from(newSelected));
} else { } else {
onToggle(val); onToggle(val);
@@ -304,7 +307,7 @@ export function ColumnFilterPopover({
"w-4 h-4 rounded border flex items-center justify-center shrink-0 transition-colors", "w-4 h-4 rounded border flex items-center justify-center shrink-0 transition-colors",
selectedValues.includes(val) ? "bg-blue-600 border-blue-600" : "border-slate-600 bg-slate-900 group-hover:border-slate-500" selectedValues.includes(val) ? "bg-blue-600 border-blue-600" : "border-slate-600 bg-slate-900 group-hover:border-slate-500"
)}> )}>
{selectedValues.includes(val) && <Check className="w-3 h-3 text-white" />} {safeSelectedValues.includes(val) && <Check className="w-3 h-3 text-white" />}
</div> </div>
<span className="text-xs text-slate-300 truncate" title={val}>{val || '(Empty)'}</span> <span className="text-xs text-slate-300 truncate" title={val}>{val || '(Empty)'}</span>
</div> </div>
@@ -322,7 +325,7 @@ export function ColumnFilterPopover({
if (isAllSelected) { if (isAllSelected) {
onSelectAll([]); onSelectAll([]);
} else { } else {
onSelectAll(uniqueValues); onSelectAll(safeUniqueValues);
} }
}} }}
className="text-[10px] font-black text-indigo-400 hover:text-indigo-300 transition-colors uppercase tracking-tight" className="text-[10px] font-black text-indigo-400 hover:text-indigo-300 transition-colors uppercase tracking-tight"
+26 -25
View File
@@ -378,26 +378,26 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
: activeTerms.some(term => sku.includes(term)); : activeTerms.some(term => sku.includes(term));
}); });
} }
if (lineMultiFilter.length > 0) { if ((lineMultiFilter || []).length > 0) {
result = result.filter(r => lineMultiFilter.includes(String(r.row[COLUMNS.LINE] || ''))); result = result.filter(r => (lineMultiFilter || []).includes(String(r.row[COLUMNS.LINE] || '')));
} }
if (classificationFilter.length > 0) { if ((classificationFilter || []).length > 0) {
result = result.filter(r => classificationFilter.includes(String(r.row[COLUMNS.CLASSIFICATION] || ''))); result = result.filter(r => (classificationFilter || []).includes(String(r.row[COLUMNS.CLASSIFICATION] || '')));
} }
if (productTypeFilter.length > 0) { if ((productTypeFilter || []).length > 0) {
result = result.filter(r => productTypeFilter.includes(String(r.row[COLUMNS.PRODUCT_TYPE] || ''))); result = result.filter(r => (productTypeFilter || []).includes(String(r.row[COLUMNS.PRODUCT_TYPE] || '')));
} }
if (unitsOuterFilter.length > 0) { if ((unitsOuterFilter || []).length > 0) {
result = result.filter(r => unitsOuterFilter.includes(String(r.row[unitsOuterIdx] || ''))); result = result.filter(r => (unitsOuterFilter || []).includes(String(r.row[unitsOuterIdx] || '')));
} }
if (outerWFilter.length > 0) { if ((outerWFilter || []).length > 0) {
result = result.filter(r => outerWFilter.includes(String(r.row[COLUMNS.OUTER_W] || ''))); result = result.filter(r => (outerWFilter || []).includes(String(r.row[COLUMNS.OUTER_W] || '')));
} }
if (outerLFilter.length > 0) { if ((outerLFilter || []).length > 0) {
result = result.filter(r => outerLFilter.includes(String(r.row[COLUMNS.OUTER_L] || ''))); result = result.filter(r => (outerLFilter || []).includes(String(r.row[COLUMNS.OUTER_L] || '')));
} }
if (outerHFilter.length > 0) { if ((outerHFilter || []).length > 0) {
result = result.filter(r => outerHFilter.includes(String(r.row[COLUMNS.OUTER_H] || ''))); result = result.filter(r => (outerHFilter || []).includes(String(r.row[COLUMNS.OUTER_H] || '')));
} }
// Dynamic column filters (SRP, container) // Dynamic column filters (SRP, container)
@@ -1241,7 +1241,7 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
onClick={(e) => { e.stopPropagation(); setOpenFilter(openFilter === 'line' ? null : 'line'); }} onClick={(e) => { e.stopPropagation(); setOpenFilter(openFilter === 'line' ? null : 'line'); }}
className={cn( className={cn(
'p-0.5 rounded hover:bg-slate-700 transition-colors shrink-0 filter-trigger-btn', 'p-0.5 rounded hover:bg-slate-700 transition-colors shrink-0 filter-trigger-btn',
lineMultiFilter.length > 0 ? 'text-blue-400 bg-blue-400/10' : 'text-slate-500 opacity-0 group-hover:opacity-100' (lineMultiFilter || []).length > 0 ? 'text-blue-400 bg-blue-400/10' : 'text-slate-500 opacity-0 group-hover:opacity-100'
)} )}
> >
<Filter className="w-3 h-3" /> <Filter className="w-3 h-3" />
@@ -1250,8 +1250,8 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
{openFilter === 'line' && ( {openFilter === 'line' && (
<ColumnFilterPopover <ColumnFilterPopover
triggerId="filter-trigger-line" triggerId="filter-trigger-line"
uniqueValues={uniqueLines} uniqueValues={uniqueLines || []}
selectedValues={lineMultiFilter} selectedValues={lineMultiFilter || []}
onToggle={val => setLineMultiFilter(prev => prev.includes(val) ? prev.filter(v => v !== val) : [...prev, val])} onToggle={val => setLineMultiFilter(prev => prev.includes(val) ? prev.filter(v => v !== val) : [...prev, val])}
onSelectAll={vals => setLineMultiFilter(vals)} onSelectAll={vals => setLineMultiFilter(vals)}
onClear={() => { setLineMultiFilter([]); setOpenFilter(null); }} onClear={() => { setLineMultiFilter([]); setOpenFilter(null); }}
@@ -1279,7 +1279,7 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
onClick={(e) => { e.stopPropagation(); setOpenFilter(openFilter === 'classification' ? null : 'classification'); }} onClick={(e) => { e.stopPropagation(); setOpenFilter(openFilter === 'classification' ? null : 'classification'); }}
className={cn( className={cn(
'p-0.5 rounded hover:bg-slate-700 transition-colors shrink-0 filter-trigger-btn', 'p-0.5 rounded hover:bg-slate-700 transition-colors shrink-0 filter-trigger-btn',
classificationFilter.length > 0 ? 'text-blue-400 bg-blue-400/10' : 'text-slate-500 opacity-0 group-hover:opacity-100' (classificationFilter || []).length > 0 ? 'text-blue-400 bg-blue-400/10' : 'text-slate-500 opacity-0 group-hover:opacity-100'
)} )}
> >
<Filter className="w-3 h-3" /> <Filter className="w-3 h-3" />
@@ -1288,8 +1288,8 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
{openFilter === 'classification' && ( {openFilter === 'classification' && (
<ColumnFilterPopover <ColumnFilterPopover
triggerId="filter-trigger-class" triggerId="filter-trigger-class"
uniqueValues={uniqueClassifications} uniqueValues={uniqueClassifications || []}
selectedValues={classificationFilter} selectedValues={classificationFilter || []}
onToggle={val => setClassificationFilter(prev => prev.includes(val) ? prev.filter(v => v !== val) : [...prev, val])} onToggle={val => setClassificationFilter(prev => prev.includes(val) ? prev.filter(v => v !== val) : [...prev, val])}
onSelectAll={vals => setClassificationFilter(vals)} onSelectAll={vals => setClassificationFilter(vals)}
onClear={() => { setClassificationFilter([]); setOpenFilter(null); }} onClear={() => { setClassificationFilter([]); setOpenFilter(null); }}
@@ -1310,7 +1310,7 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
<button <button
id="filter-trigger-type" id="filter-trigger-type"
onClick={(e: React.MouseEvent) => { e.stopPropagation(); setOpenFilter(openFilter === 'productType' ? null : 'productType'); }} onClick={(e: React.MouseEvent) => { e.stopPropagation(); setOpenFilter(openFilter === 'productType' ? null : 'productType'); }}
className={cn('ml-1 p-0.5 rounded transition-colors filter-trigger-btn', productTypeFilter.length > 0 ? 'text-purple-400 bg-purple-400/10' : 'text-slate-500 opacity-0 group-hover:opacity-100')} className={cn('ml-1 p-0.5 rounded transition-colors filter-trigger-btn', (productTypeFilter || []).length > 0 ? 'text-purple-400 bg-purple-400/10' : 'text-slate-500 opacity-0 group-hover:opacity-100')}
> >
<Filter className="w-3 h-3" /> <Filter className="w-3 h-3" />
</button> </button>
@@ -1318,8 +1318,8 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
{openFilter === 'productType' && ( {openFilter === 'productType' && (
<ColumnFilterPopover <ColumnFilterPopover
triggerId="filter-trigger-type" triggerId="filter-trigger-type"
uniqueValues={uniqueProductTypes} uniqueValues={uniqueProductTypes || []}
selectedValues={productTypeFilter} selectedValues={productTypeFilter || []}
onToggle={(val: string) => setProductTypeFilter((prev: string[]) => prev.includes(val) ? prev.filter((v: string) => v !== val) : [...prev, val])} onToggle={(val: string) => setProductTypeFilter((prev: string[]) => prev.includes(val) ? prev.filter((v: string) => v !== val) : [...prev, val])}
onSelectAll={vals => setProductTypeFilter(vals)} onSelectAll={vals => setProductTypeFilter(vals)}
onClear={() => { setProductTypeFilter([]); setOpenFilter(null); }} onClear={() => { setProductTypeFilter([]); setOpenFilter(null); }}
@@ -1380,7 +1380,7 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
<ColumnFilterPopover <ColumnFilterPopover
triggerId={`filter-trigger-${colKey}`} triggerId={`filter-trigger-${colKey}`}
uniqueValues={dynamicColUniqueValues[col.index] || []} uniqueValues={dynamicColUniqueValues[col.index] || []}
selectedValues={dynamicColFilters[col.index] || []} selectedValues={dynamicColFilters[col.index] || []}
onToggle={val => setDynamicColFilters(prev => { onToggle={val => setDynamicColFilters(prev => {
const current = prev[col.index] || []; const current = prev[col.index] || [];
return current.includes(val) return current.includes(val)
@@ -2150,7 +2150,8 @@ function TextFilterPopover({ value, onChange, onClose, triggerId }: {
</button> </button>
</div> </div>
</div>, </div>,
document.body && document.body); document.body
);
} }
// ── Sort Icon Helper ─────────────────────────────────────────────────── // ── Sort Icon Helper ───────────────────────────────────────────────────