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;
className?: string;
zIndex?: number;
triggerId?: string;
}
export function ColumnFilterPopover({
@@ -73,8 +74,10 @@ export function ColumnFilterPopover({
const isDraggingRef = React.useRef(false);
const dragStartRef = React.useRef<number | null>(null);
const safeUniqueValues = uniqueValues || [];
const safeSelectedValues = selectedValues || [];
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 filteredValuesRef = React.useRef<string[]>([]);
const didDragRef = React.useRef(false);
@@ -85,16 +88,16 @@ export function ColumnFilterPopover({
const listRef = React.useRef<HTMLDivElement>(null);
const filteredValues = useMemo(() => {
return uniqueValues.filter(v =>
return safeUniqueValues.filter(v =>
String(v || '').toLowerCase().includes(search.toLowerCase())
);
}, [uniqueValues, search]);
}, [safeUniqueValues, search]);
filteredValuesRef.current = filteredValues;
selectedValuesRef.current = selectedValues;
selectedValuesRef.current = safeSelectedValues;
onSelectAllRef.current = onSelectAll;
const isAllSelected = selectedValues.length === uniqueValues.length && uniqueValues.length > 0;
const isAllSelected = safeSelectedValues.length === safeUniqueValues.length && safeUniqueValues.length > 0;
const applyCondition = () => {
const results: string[] = [];
@@ -221,7 +224,7 @@ export function ColumnFilterPopover({
activeTab === 'values' ? "bg-blue-600 text-white" : "text-slate-400 hover:text-white"
)}
>
Values ({selectedValues.length})
Values ({safeSelectedValues.length})
</button>
<button
onClick={() => setActiveTab('condition')}
@@ -277,7 +280,7 @@ export function ColumnFilterPopover({
<div
key={val}
role="checkbox"
aria-checked={selectedValues.includes(val)}
aria-checked={safeSelectedValues.includes(val)}
tabIndex={0}
onMouseDown={(e) => { e.preventDefault(); handleMouseDown(idx); }}
onMouseEnter={() => handleMouseEnter(idx)}
@@ -287,7 +290,7 @@ export function ColumnFilterPopover({
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]);
const newSelected = new Set([...safeSelectedValues, ...itemsToSelect]);
onSelectAll(Array.from(newSelected));
} else {
onToggle(val);
@@ -304,7 +307,7 @@ export function ColumnFilterPopover({
"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) && <Check className="w-3 h-3 text-white" />}
{safeSelectedValues.includes(val) && <Check className="w-3 h-3 text-white" />}
</div>
<span className="text-xs text-slate-300 truncate" title={val}>{val || '(Empty)'}</span>
</div>
@@ -322,7 +325,7 @@ export function ColumnFilterPopover({
if (isAllSelected) {
onSelectAll([]);
} else {
onSelectAll(uniqueValues);
onSelectAll(safeUniqueValues);
}
}}
className="text-[10px] font-black text-indigo-400 hover:text-indigo-300 transition-colors uppercase tracking-tight"
+25 -24
View File
@@ -378,26 +378,26 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
: activeTerms.some(term => sku.includes(term));
});
}
if (lineMultiFilter.length > 0) {
result = result.filter(r => lineMultiFilter.includes(String(r.row[COLUMNS.LINE] || '')));
if ((lineMultiFilter || []).length > 0) {
result = result.filter(r => (lineMultiFilter || []).includes(String(r.row[COLUMNS.LINE] || '')));
}
if (classificationFilter.length > 0) {
result = result.filter(r => classificationFilter.includes(String(r.row[COLUMNS.CLASSIFICATION] || '')));
if ((classificationFilter || []).length > 0) {
result = result.filter(r => (classificationFilter || []).includes(String(r.row[COLUMNS.CLASSIFICATION] || '')));
}
if (productTypeFilter.length > 0) {
result = result.filter(r => productTypeFilter.includes(String(r.row[COLUMNS.PRODUCT_TYPE] || '')));
if ((productTypeFilter || []).length > 0) {
result = result.filter(r => (productTypeFilter || []).includes(String(r.row[COLUMNS.PRODUCT_TYPE] || '')));
}
if (unitsOuterFilter.length > 0) {
result = result.filter(r => unitsOuterFilter.includes(String(r.row[unitsOuterIdx] || '')));
if ((unitsOuterFilter || []).length > 0) {
result = result.filter(r => (unitsOuterFilter || []).includes(String(r.row[unitsOuterIdx] || '')));
}
if (outerWFilter.length > 0) {
result = result.filter(r => outerWFilter.includes(String(r.row[COLUMNS.OUTER_W] || '')));
if ((outerWFilter || []).length > 0) {
result = result.filter(r => (outerWFilter || []).includes(String(r.row[COLUMNS.OUTER_W] || '')));
}
if (outerLFilter.length > 0) {
result = result.filter(r => outerLFilter.includes(String(r.row[COLUMNS.OUTER_L] || '')));
if ((outerLFilter || []).length > 0) {
result = result.filter(r => (outerLFilter || []).includes(String(r.row[COLUMNS.OUTER_L] || '')));
}
if (outerHFilter.length > 0) {
result = result.filter(r => outerHFilter.includes(String(r.row[COLUMNS.OUTER_H] || '')));
if ((outerHFilter || []).length > 0) {
result = result.filter(r => (outerHFilter || []).includes(String(r.row[COLUMNS.OUTER_H] || '')));
}
// 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'); }}
className={cn(
'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" />
@@ -1250,8 +1250,8 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
{openFilter === 'line' && (
<ColumnFilterPopover
triggerId="filter-trigger-line"
uniqueValues={uniqueLines}
selectedValues={lineMultiFilter}
uniqueValues={uniqueLines || []}
selectedValues={lineMultiFilter || []}
onToggle={val => setLineMultiFilter(prev => prev.includes(val) ? prev.filter(v => v !== val) : [...prev, val])}
onSelectAll={vals => setLineMultiFilter(vals)}
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'); }}
className={cn(
'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" />
@@ -1288,8 +1288,8 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
{openFilter === 'classification' && (
<ColumnFilterPopover
triggerId="filter-trigger-class"
uniqueValues={uniqueClassifications}
selectedValues={classificationFilter}
uniqueValues={uniqueClassifications || []}
selectedValues={classificationFilter || []}
onToggle={val => setClassificationFilter(prev => prev.includes(val) ? prev.filter(v => v !== val) : [...prev, val])}
onSelectAll={vals => setClassificationFilter(vals)}
onClear={() => { setClassificationFilter([]); setOpenFilter(null); }}
@@ -1310,7 +1310,7 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
<button
id="filter-trigger-type"
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" />
</button>
@@ -1318,8 +1318,8 @@ export function PricingView({ data, headers, onSaveRow, onCaptureState, onEdit,
{openFilter === 'productType' && (
<ColumnFilterPopover
triggerId="filter-trigger-type"
uniqueValues={uniqueProductTypes}
selectedValues={productTypeFilter}
uniqueValues={uniqueProductTypes || []}
selectedValues={productTypeFilter || []}
onToggle={(val: string) => setProductTypeFilter((prev: string[]) => prev.includes(val) ? prev.filter((v: string) => v !== val) : [...prev, val])}
onSelectAll={vals => setProductTypeFilter(vals)}
onClear={() => { setProductTypeFilter([]); setOpenFilter(null); }}
@@ -2150,7 +2150,8 @@ function TextFilterPopover({ value, onChange, onClose, triggerId }: {
</button>
</div>
</div>,
document.body && document.body);
document.body
);
}
// ── Sort Icon Helper ───────────────────────────────────────────────────