Clarify active grid filters
This commit is contained in:
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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] ?? [];
|
||||
|
||||
Reference in New Issue
Block a user