diff --git a/frontend/src/components/pages/ReportingPage.js b/frontend/src/components/pages/ReportingPage.js
index b443ebc..fba13f4 100644
--- a/frontend/src/components/pages/ReportingPage.js
+++ b/frontend/src/components/pages/ReportingPage.js
@@ -7,6 +7,9 @@ import { useAuth } from '../../contexts/AuthContext';
const API_BASE = process.env.REACT_APP_API_BASE || 'http://localhost:3001/api';
const STORAGE_KEY = 'steam_findings_columns_v2';
+// Sentinel used in filter Sets to represent cells with no value (blank / —)
+const EMPTY_SENTINEL = '__EMPTY__';
+
// ---------------------------------------------------------------------------
// Column definitions — source of truth for labels, sort behaviour, rendering
// ---------------------------------------------------------------------------
@@ -793,22 +796,28 @@ function FilterDropdown({ anchorEl, colKey, findings, activeFilter, onFilterChan
// Unique values from the full (unfiltered) findings list.
// Multi-value columns (e.g. cves) expand their array so each item is a separate option.
+ // EMPTY_SENTINEL is prepended when any finding has a blank/null cell.
const allValues = useMemo(() => {
const def = COLUMN_DEFS[colKey];
const vals = new Set();
+ let hasEmpty = false;
findings.forEach((f) => {
if (def?.multiValue) {
- (f[colKey] || []).forEach((v) => { if (String(v).trim()) vals.add(String(v).trim()); });
+ const arr = f[colKey] || [];
+ if (arr.length === 0) { hasEmpty = true; return; }
+ arr.forEach((v) => { if (String(v).trim()) vals.add(String(v).trim()); });
} else {
const v = getFilterVal(f, colKey).trim();
- if (v) vals.add(v);
+ if (v) vals.add(v); else hasEmpty = true;
}
});
- return [...vals].sort((a, b) => a.localeCompare(b, undefined, { numeric: true }));
+ const sorted = [...vals].sort((a, b) => a.localeCompare(b, undefined, { numeric: true }));
+ if (hasEmpty) sorted.unshift(EMPTY_SENTINEL);
+ return sorted;
}, [findings, colKey]);
const displayed = search.trim()
- ? allValues.filter((v) => v.toLowerCase().includes(search.toLowerCase()))
+ ? allValues.filter((v) => v === EMPTY_SENTINEL || v.toLowerCase().includes(search.toLowerCase()))
: allValues;
const isChecked = (val) => !activeFilter || activeFilter.has(val);
@@ -890,7 +899,10 @@ function FilterDropdown({ anchorEl, colKey, findings, activeFilter, onFilterChan
onChange={() => toggle(val)}
style={{ accentColor: '#0EA5E9', width: '12px', height: '12px', flexShrink: 0, cursor: 'pointer' }}
/>
- {val}
+ {val === EMPTY_SENTINEL
+ ? — empty —
+ : {val}
+ }
))}
@@ -1188,9 +1200,12 @@ export default function ReportingPage({ filterDate, filterEXC }) {
if (!vals || vals.size === 0) return false;
const def = COLUMN_DEFS[key];
if (def?.multiValue) {
- return (f[key] || []).some((v) => vals.has(String(v).trim()));
+ const arr = f[key] || [];
+ if (arr.length === 0) return vals.has(EMPTY_SENTINEL);
+ return arr.some((v) => vals.has(String(v).trim()));
}
- return vals.has(getFilterVal(f, key).trim());
+ const fval = getFilterVal(f, key).trim();
+ return fval === '' ? vals.has(EMPTY_SENTINEL) : vals.has(fval);
})
);
}