Redesign grid filter menu
This commit is contained in:
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user