Clarify active grid filters
This commit is contained in:
@@ -287,6 +287,8 @@ Implemented layout foundation:
|
|||||||
- the toolbar remains reachable while the page scrolls
|
- the toolbar remains reachable while the page scrolls
|
||||||
- programmatic grid focus preserves the viewport during undo/redo reloads
|
- programmatic grid focus preserves the viewport during undo/redo reloads
|
||||||
- wide tables no longer expand the surrounding browser layout
|
- 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:
|
Acceptance criteria:
|
||||||
|
|
||||||
|
|||||||
+60
-1
@@ -71,6 +71,44 @@ body {
|
|||||||
opacity: 0.45;
|
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 {
|
.column-settings-menu {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
z-index: 9;
|
z-index: 9;
|
||||||
@@ -278,6 +316,7 @@ body {
|
|||||||
.tree-grid .header-filter-btn.active {
|
.tree-grid .header-filter-btn.active {
|
||||||
border-color: #2563eb;
|
border-color: #2563eb;
|
||||||
color: #2563eb;
|
color: #2563eb;
|
||||||
|
background: #eff6ff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tree-grid .header-filter-menu {
|
.tree-grid .header-filter-menu {
|
||||||
@@ -293,13 +332,33 @@ body {
|
|||||||
padding: 0.35rem;
|
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 {
|
.tree-grid .header-filter-clear {
|
||||||
border: 1px solid #c4cddc;
|
border: 1px solid #c4cddc;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
font-size: 0.72rem;
|
font-size: 0.72rem;
|
||||||
padding: 0.12rem 0.3rem;
|
padding: 0.12rem 0.3rem;
|
||||||
margin-bottom: 0.25rem;
|
}
|
||||||
|
|
||||||
|
.tree-grid .header-filter-clear:disabled {
|
||||||
|
opacity: 0.45;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tree-grid .header-filter-values {
|
.tree-grid .header-filter-values {
|
||||||
|
|||||||
@@ -315,6 +315,15 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
|||||||
[columnFilters]
|
[columnFilters]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const activeColumnFilters = useMemo(
|
||||||
|
() =>
|
||||||
|
visibleColumns.flatMap((column) => {
|
||||||
|
const values = columnFilters[column.key] ?? [];
|
||||||
|
return values.length > 0 ? [{ column, values }] : [];
|
||||||
|
}),
|
||||||
|
[columnFilters, visibleColumns]
|
||||||
|
);
|
||||||
|
|
||||||
const distinctValuesByColumn = useMemo(() => {
|
const distinctValuesByColumn = useMemo(() => {
|
||||||
// Filter options are generated only for visible columns to match current header UI.
|
// 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.
|
// 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);
|
return buildVisibleGridRows(filteredSortedSections);
|
||||||
}, [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 searchableProjectDevices = useMemo(() => {
|
||||||
const term = projectDeviceSearch.trim().toLowerCase();
|
const term = projectDeviceSearch.trim().toLowerCase();
|
||||||
if (!term) {
|
if (!term) {
|
||||||
@@ -2247,11 +2317,7 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => {
|
onClick={clearSortAndFilters}
|
||||||
setSortState(null);
|
|
||||||
setColumnFilters({});
|
|
||||||
setOpenFilterColumn(null);
|
|
||||||
}}
|
|
||||||
disabled={!Boolean(sortState) && !hasActiveFilters}
|
disabled={!Boolean(sortState) && !hasActiveFilters}
|
||||||
>
|
>
|
||||||
Clear sort/filter
|
Clear sort/filter
|
||||||
@@ -2303,6 +2369,7 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
|||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
{renderActiveViewSummary()}
|
||||||
{error ? (
|
{error ? (
|
||||||
<div className="notice error editor-error-notice" role="alert">
|
<div className="notice error editor-error-notice" role="alert">
|
||||||
<span>{error}</span>
|
<span>{error}</span>
|
||||||
@@ -2372,11 +2439,7 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => {
|
onClick={clearSortAndFilters}
|
||||||
setSortState(null);
|
|
||||||
setColumnFilters({});
|
|
||||||
setOpenFilterColumn(null);
|
|
||||||
}}
|
|
||||||
disabled={!isSortedView && !hasActiveFilters}
|
disabled={!isSortedView && !hasActiveFilters}
|
||||||
>
|
>
|
||||||
Clear sort/filter
|
Clear sort/filter
|
||||||
@@ -2425,6 +2488,7 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
|||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
{renderActiveViewSummary()}
|
||||||
{hasActiveSortOrFilter ? (
|
{hasActiveSortOrFilter ? (
|
||||||
<div className="notice muted">Sorting/filtering is view-only. Renumber is disabled while sort/filter is active.</div>
|
<div className="notice muted">Sorting/filtering is view-only. Renumber is disabled while sort/filter is active.</div>
|
||||||
) : null}
|
) : null}
|
||||||
@@ -2587,24 +2651,36 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
|||||||
type="button"
|
type="button"
|
||||||
className={`header-filter-btn ${(columnFilters[column.key]?.length ?? 0) > 0 ? "active" : ""}`}
|
className={`header-filter-btn ${(columnFilters[column.key]?.length ?? 0) > 0 ? "active" : ""}`}
|
||||||
onClick={() => setOpenFilterColumn((current) => (current === column.key ? null : column.key))}
|
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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{openFilterColumn === column.key ? (
|
{openFilterColumn === column.key ? (
|
||||||
<div className="header-filter-menu">
|
<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
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="header-filter-clear"
|
className="header-filter-clear"
|
||||||
onClick={() =>
|
onClick={() => clearColumnFilter(column.key)}
|
||||||
setColumnFilters((current) => ({
|
disabled={(columnFilters[column.key]?.length ?? 0) === 0}
|
||||||
...current,
|
|
||||||
[column.key]: [],
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
Clear
|
Show all
|
||||||
</button>
|
</button>
|
||||||
|
</div>
|
||||||
<div className="header-filter-values">
|
<div className="header-filter-values">
|
||||||
{distinctValuesByColumn[column.key].map((value) => {
|
{distinctValuesByColumn[column.key].map((value) => {
|
||||||
const selected = columnFilters[column.key] ?? [];
|
const selected = columnFilters[column.key] ?? [];
|
||||||
|
|||||||
Reference in New Issue
Block a user