Redesign grid filter menu

This commit is contained in:
2026-07-22 23:04:03 +02:00
parent d1615bcfee
commit fafccca50f
3 changed files with 187 additions and 45 deletions
+102 -29
View File
@@ -197,6 +197,8 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
const [sortState, setSortState] = useState<{ key: CellKey; direction: SortDirection } | null>(null);
const [columnFilters, setColumnFilters] = useState<Partial<Record<CellKey, string[]>>>({});
const [openFilterColumn, setOpenFilterColumn] = useState<CellKey | null>(null);
const [filterDraftValues, setFilterDraftValues] = useState<string[]>([]);
const [filterValueSearch, setFilterValueSearch] = useState("");
const [visibleColumnKeys, setVisibleColumnKeys] = useState<CellKey[]>(defaultVisibleColumnKeys);
const [columnOrder, setColumnOrder] = useState<CellKey[]>(allColumns.map((column) => column.key));
const [isColumnMenuOpen, setIsColumnMenuOpen] = useState(false);
@@ -351,10 +353,45 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
setOpenFilterColumn((current) => (current === key ? null : current));
}
function closeColumnFilterMenu() {
setOpenFilterColumn(null);
setFilterDraftValues([]);
setFilterValueSearch("");
}
function toggleColumnFilterMenu(key: CellKey) {
if (openFilterColumn === key) {
closeColumnFilterMenu();
return;
}
const availableValues = distinctValuesByColumn[key] ?? [];
const activeValues = columnFilters[key] ?? [];
setFilterDraftValues(activeValues.length > 0 ? activeValues : availableValues);
setFilterValueSearch("");
setOpenFilterColumn(key);
}
function applyColumnFilter(key: CellKey) {
const availableValues = distinctValuesByColumn[key] ?? [];
const availableSet = new Set(availableValues);
const selectedValues = [...new Set(filterDraftValues)].filter((value) => availableSet.has(value));
if (selectedValues.length === 0) {
return;
}
if (selectedValues.length === availableValues.length) {
clearColumnFilter(key);
} else {
setColumnFilters((current) => ({ ...current, [key]: selectedValues }));
}
closeColumnFilterMenu();
}
function clearSortAndFilters() {
setSortState(null);
setColumnFilters({});
setOpenFilterColumn(null);
closeColumnFilterMenu();
}
function renderActiveViewSummary() {
@@ -368,7 +405,7 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
return (
<div className="active-view-summary" aria-label="Active sort and filters">
<span className="active-view-label">Active view</span>
<span className="active-view-label">Sort and filters</span>
{sortState ? (
<button
type="button"
@@ -2650,7 +2687,7 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
<button
type="button"
className={`header-filter-btn ${(columnFilters[column.key]?.length ?? 0) > 0 ? "active" : ""}`}
onClick={() => setOpenFilterColumn((current) => (current === column.key ? null : column.key))}
onClick={() => toggleColumnFilterMenu(column.key)}
aria-pressed={(columnFilters[column.key]?.length ?? 0) > 0}
title={
(columnFilters[column.key]?.length ?? 0) > 0
@@ -2663,47 +2700,83 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
</div>
{openFilterColumn === column.key ? (
<div className="header-filter-menu">
<div className="header-filter-explanation">
Select one or more values. A circuit is shown when its block contains any selected value.
</div>
<div className="header-filter-actions">
<span>
{(columnFilters[column.key]?.length ?? 0) > 0
? `${columnFilters[column.key]?.length} selected`
: "Showing all"}
</span>
<button
type="button"
className="header-filter-clear"
onClick={() => clearColumnFilter(column.key)}
disabled={(columnFilters[column.key]?.length ?? 0) === 0}
>
Show all
<div className="header-filter-title-row">
<strong>Filter {column.label}</strong>
<button type="button" className="header-filter-close" onClick={closeColumnFilterMenu} aria-label="Close filter">
×
</button>
</div>
<div className="header-filter-explanation">
Select the values that should remain visible. Changes take effect when you apply them.
</div>
<input
type="search"
className="header-filter-search"
value={filterValueSearch}
onChange={(event) => setFilterValueSearch(event.target.value)}
placeholder="Search values..."
aria-label={`Search values for ${column.label}`}
autoFocus
/>
<div className="header-filter-selection-actions">
<button
type="button"
onClick={() => setFilterDraftValues(distinctValuesByColumn[column.key] ?? [])}
>
Select all
</button>
<button type="button" onClick={() => setFilterDraftValues([])}>
Select none
</button>
<span>{filterDraftValues.length} / {(distinctValuesByColumn[column.key] ?? []).length}</span>
</div>
<div className="header-filter-values">
{distinctValuesByColumn[column.key].map((value) => {
const selected = columnFilters[column.key] ?? [];
const checked = selected.includes(value);
{(distinctValuesByColumn[column.key] ?? [])
.filter((value) => value.toLocaleLowerCase("de-DE").includes(filterValueSearch.trim().toLocaleLowerCase("de-DE")))
.map((value) => {
const checked = filterDraftValues.includes(value);
return (
<label key={`${column.key}-${value}`} className="header-filter-item">
<input
type="checkbox"
checked={checked}
onChange={(event) => {
setColumnFilters((current) => {
const previous = current[column.key] ?? [];
const next = event.target.checked
? [...previous, value]
: previous.filter((entry) => entry !== value);
return { ...current, [column.key]: next };
});
setFilterDraftValues((current) =>
event.target.checked
? [...current, value]
: current.filter((entry) => entry !== value)
);
}}
/>
<span>{value}</span>
</label>
);
})}
{(distinctValuesByColumn[column.key] ?? []).filter((value) =>
value.toLocaleLowerCase("de-DE").includes(filterValueSearch.trim().toLocaleLowerCase("de-DE"))
).length === 0 ? (
<div className="header-filter-empty">No matching values.</div>
) : null}
</div>
<div className="header-filter-footer">
{filterDraftValues.length === 0 ? (
<span className="header-filter-warning">Select at least one value.</span>
) : (
<span />
)}
<div className="header-filter-footer-actions">
<button type="button" onClick={closeColumnFilterMenu}>Cancel</button>
<button
type="button"
className="primary"
onClick={() => applyColumnFilter(column.key)}
disabled={filterDraftValues.length === 0}
>
{filterDraftValues.length === (distinctValuesByColumn[column.key] ?? []).length
? "Show all"
: "Apply filter"}
</button>
</div>
</div>
</div>
) : null}