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
@@ -287,6 +287,8 @@ Implemented layout foundation:
- the toolbar remains reachable while the page scrolls
- programmatic grid focus preserves the viewport during undo/redo reloads
- wide tables no longer expand the surrounding browser layout
- active sorts and filters are summarized above the grid and can be removed individually or reset together
- column filter buttons show their selected-value count and filter menus explain their matching behavior
Acceptance criteria:
+60 -1
View File
@@ -71,6 +71,44 @@ body {
opacity: 0.45;
}
.active-view-summary {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.35rem;
padding: 0.4rem 0.5rem;
border: 1px solid #bfdbfe;
border-radius: 5px;
background: #eff6ff;
color: #1e3a5f;
font-size: 0.78rem;
}
.active-view-label {
font-weight: 600;
}
.active-view-chip,
.active-view-reset {
border: 1px solid #93b4df;
border-radius: 999px;
background: #fff;
color: #1e3a5f;
padding: 0.18rem 0.48rem;
font-size: 0.76rem;
cursor: pointer;
}
.active-view-chip:hover,
.active-view-reset:hover {
border-color: #2563eb;
}
.active-view-reset {
margin-left: auto;
font-weight: 600;
}
.column-settings-menu {
position: absolute;
z-index: 9;
@@ -278,6 +316,7 @@ body {
.tree-grid .header-filter-btn.active {
border-color: #2563eb;
color: #2563eb;
background: #eff6ff;
}
.tree-grid .header-filter-menu {
@@ -293,13 +332,33 @@ body {
padding: 0.35rem;
}
.tree-grid .header-filter-actions {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.4rem;
margin-bottom: 0.35rem;
font-size: 0.72rem;
}
.tree-grid .header-filter-explanation {
margin-bottom: 0.4rem;
color: #4b5563;
font-size: 0.72rem;
line-height: 1.3;
white-space: normal;
}
.tree-grid .header-filter-clear {
border: 1px solid #c4cddc;
background: #fff;
border-radius: 3px;
font-size: 0.72rem;
padding: 0.12rem 0.3rem;
margin-bottom: 0.25rem;
}
.tree-grid .header-filter-clear:disabled {
opacity: 0.45;
}
.tree-grid .header-filter-values {
+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] ?? [];