Clarify active grid filters

This commit is contained in:
2026-07-22 22:57:25 +02:00
parent 01aff46b4f
commit d1615bcfee
3 changed files with 161 additions and 24 deletions
+99 -23
View File
@@ -315,6 +315,15 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
[columnFilters]
);
const activeColumnFilters = useMemo(
() =>
visibleColumns.flatMap((column) => {
const values = columnFilters[column.key] ?? [];
return values.length > 0 ? [{ column, values }] : [];
}),
[columnFilters, visibleColumns]
);
const distinctValuesByColumn = useMemo(() => {
// Filter options are generated only for visible columns to match current header UI.
// Hidden columns still exist in row model, but are intentionally not filterable from header.
@@ -333,6 +342,67 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
return buildVisibleGridRows(filteredSortedSections);
}, [filteredSortedSections]);
function clearColumnFilter(key: CellKey) {
setColumnFilters((current) => {
const next = { ...current };
delete next[key];
return next;
});
setOpenFilterColumn((current) => (current === key ? null : current));
}
function clearSortAndFilters() {
setSortState(null);
setColumnFilters({});
setOpenFilterColumn(null);
}
function renderActiveViewSummary() {
if (!sortState && activeColumnFilters.length === 0) {
return null;
}
const sortedColumn = sortState
? allColumns.find((column) => column.key === sortState.key)
: undefined;
return (
<div className="active-view-summary" aria-label="Active sort and filters">
<span className="active-view-label">Active view</span>
{sortState ? (
<button
type="button"
className="active-view-chip"
onClick={() => setSortState(null)}
title="Remove sorting"
>
Sort: {sortedColumn?.label ?? sortState.key} ({sortState.direction === "asc" ? "ascending" : "descending"})
<span aria-hidden="true"> ×</span>
</button>
) : null}
{activeColumnFilters.map(({ column, values }) => {
const shownValues = values.slice(0, 2).join(", ");
const valueSummary = values.length > 2 ? `${shownValues} +${values.length - 2}` : shownValues;
return (
<button
key={`active-filter-${column.key}`}
type="button"
className="active-view-chip"
onClick={() => clearColumnFilter(column.key)}
title={`Remove filter for ${column.label}`}
>
{column.label}: {valueSummary}
<span aria-hidden="true"> ×</span>
</button>
);
})}
<button type="button" className="active-view-reset" onClick={clearSortAndFilters}>
Reset view
</button>
</div>
);
}
const searchableProjectDevices = useMemo(() => {
const term = projectDeviceSearch.trim().toLowerCase();
if (!term) {
@@ -2247,11 +2317,7 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
</button>
<button
type="button"
onClick={() => {
setSortState(null);
setColumnFilters({});
setOpenFilterColumn(null);
}}
onClick={clearSortAndFilters}
disabled={!Boolean(sortState) && !hasActiveFilters}
>
Clear sort/filter
@@ -2303,6 +2369,7 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
</div>
) : null}
</div>
{renderActiveViewSummary()}
{error ? (
<div className="notice error editor-error-notice" role="alert">
<span>{error}</span>
@@ -2372,11 +2439,7 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
</button>
<button
type="button"
onClick={() => {
setSortState(null);
setColumnFilters({});
setOpenFilterColumn(null);
}}
onClick={clearSortAndFilters}
disabled={!isSortedView && !hasActiveFilters}
>
Clear sort/filter
@@ -2425,6 +2488,7 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
</div>
) : null}
</div>
{renderActiveViewSummary()}
{hasActiveSortOrFilter ? (
<div className="notice muted">Sorting/filtering is view-only. Renumber is disabled while sort/filter is active.</div>
) : null}
@@ -2587,24 +2651,36 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
type="button"
className={`header-filter-btn ${(columnFilters[column.key]?.length ?? 0) > 0 ? "active" : ""}`}
onClick={() => setOpenFilterColumn((current) => (current === column.key ? null : column.key))}
aria-pressed={(columnFilters[column.key]?.length ?? 0) > 0}
title={
(columnFilters[column.key]?.length ?? 0) > 0
? `${columnFilters[column.key]?.length} filter value(s) selected`
: `Filter ${column.label}`
}
>
Filter
Filter{(columnFilters[column.key]?.length ?? 0) > 0 ? ` (${columnFilters[column.key]?.length})` : ""}
</button>
</div>
{openFilterColumn === column.key ? (
<div className="header-filter-menu">
<button
type="button"
className="header-filter-clear"
onClick={() =>
setColumnFilters((current) => ({
...current,
[column.key]: [],
}))
}
>
Clear
</button>
<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
</button>
</div>
<div className="header-filter-values">
{distinctValuesByColumn[column.key].map((value) => {
const selected = columnFilters[column.key] ?? [];