From 994a233a28a794e7805263be415447be1a1eba95 Mon Sep 17 00:00:00 2001 From: Astrid Gealer Date: Fri, 23 Oct 2026 23:54:50 +0000 Subject: [PATCH 1/4] fix(frontend): label the categorical filter "Clear" button "Select none" In a categorical column-filter popover the footer button sets an explicit empty allow-list, which hides every row. Labelled "Clear", it reads as "remove this filter", so a user resetting a filter got an empty table instead (UX-28). The empty allow-list is deliberate (it lets you start from nothing and tick one value), so keep the behavior and name it for what it does. Numeric filters keep "Clear", which does remove the filter; (All) is how a categorical filter is removed. Applied to every popover that shares the pattern: Purchase History, the two RI Exchange tables, Plans' planned purchases and Opportunities. Refs #727 Co-Authored-By: Claude Opus 5.5 --- frontend/src/lib/history-filter-popover.ts | 5 ++++- frontend/src/plans.ts | 3 ++- frontend/src/recommendations.ts | 3 ++- frontend/src/riexchange.ts | 6 ++++-- 4 files changed, 12 insertions(+), 5 deletions(-) diff --git a/frontend/src/lib/history-filter-popover.ts b/frontend/src/lib/history-filter-popover.ts index 7c7a48a65..705f671a5 100644 --- a/frontend/src/lib/history-filter-popover.ts +++ b/frontend/src/lib/history-filter-popover.ts @@ -308,7 +308,10 @@ export function openHistoryColumnPopover( const clearBtn = document.createElement('button'); clearBtn.type = 'button'; clearBtn.className = 'column-filter-clear'; - clearBtn.textContent = 'Clear'; + // Issue #727 (UX-28): on a categorical filter this button selects + // nothing (an explicit empty allow-list, which hides every row), so it + // must not be called "Clear". (All) is how the filter is removed. + clearBtn.textContent = config.kind === 'numeric' ? 'Clear' : 'Select none'; clearBtn.addEventListener('click', () => { if (config.kind === 'numeric') { if (inputRef) inputRef.value = ''; diff --git a/frontend/src/plans.ts b/frontend/src/plans.ts index e9365d926..b8c27837f 100644 --- a/frontend/src/plans.ts +++ b/frontend/src/plans.ts @@ -516,7 +516,8 @@ function buildPlansPopoverContent( const clearBtn = document.createElement('button'); clearBtn.type = 'button'; clearBtn.className = 'column-filter-clear'; - clearBtn.textContent = 'Clear'; + // Categorical: selects nothing (hides every row), so not "Clear" (#727). + clearBtn.textContent = input ? 'Clear' : 'Select none'; clearBtn.addEventListener('click', () => { if (input) { state.setPlansColumnFilter(column, null); diff --git a/frontend/src/recommendations.ts b/frontend/src/recommendations.ts index 307ff2d7a..fbad39fe5 100644 --- a/frontend/src/recommendations.ts +++ b/frontend/src/recommendations.ts @@ -2433,7 +2433,8 @@ function buildPopoverContent( const clearBtn = document.createElement('button'); clearBtn.type = 'button'; clearBtn.className = 'column-filter-clear'; - clearBtn.textContent = 'Clear'; + // Categorical: selects nothing (hides every row), so not "Clear" (#727). + clearBtn.textContent = input ? 'Clear' : 'Select none'; clearBtn.addEventListener('click', () => { if (input) { // Numeric column: Clear drops the expression entirely (no filter). diff --git a/frontend/src/riexchange.ts b/frontend/src/riexchange.ts index 4b0855f7b..9f43a7c90 100644 --- a/frontend/src/riexchange.ts +++ b/frontend/src/riexchange.ts @@ -1024,7 +1024,8 @@ function activeRiBuildPopover(column: state.ActiveRiColumnId): ActiveRiPopoverSt const clearBtn = document.createElement('button'); clearBtn.type = 'button'; clearBtn.className = 'column-filter-clear'; - clearBtn.textContent = 'Clear'; + // Categorical: selects nothing (hides every row), so not "Clear" (#727). + clearBtn.textContent = input ? 'Clear' : 'Select none'; clearBtn.addEventListener('click', () => { if (input) { state.setActiveRiColumnFilter(column, null); @@ -1273,7 +1274,8 @@ function riexBuildPopover( const clearBtn = document.createElement('button'); clearBtn.type = 'button'; clearBtn.className = 'column-filter-clear'; - clearBtn.textContent = 'Clear'; + // Categorical: selects nothing (hides every row), so not "Clear" (#727). + clearBtn.textContent = input ? 'Clear' : 'Select none'; clearBtn.addEventListener('click', () => { if (input) { state.setRiExchangeColumnFilter(column, null); From 89a109a5a85dbffb733865e634816b9356191739 Mon Sep 17 00:00:00 2001 From: Astrid Gealer Date: Fri, 23 Oct 2026 23:54:50 +0000 Subject: [PATCH 2/4] fix(frontend): show user form validation next to the fields A refused user save was reported only through a toast (UX-33): it fades, sits away from the field, and gives screen-reader users no cue on the control. The email, required-groups and minlength checks were native bubbles that vanish on the next click, and they pre-empted the app's own group checks, so the admin met problems one at a time. New lib/field-errors.ts puts a .field-error message under the field, sets aria-invalid and appends the message to aria-describedby (existing help text stays announced), and clears it on the next edit of that field. #user-form is now novalidate; saveUser collects every problem at once (blank or malformed email, password requirements on create, no group or a contradictory group combination), marks each field and focuses the first. The group and password messages still toast as before. Opening the modal clears old messages. A forms.css rule lets the red border show on unfocused fields, since .form-group's border rule outranks the global [aria-invalid] one. Refs #727 Co-Authored-By: Claude Opus 5.5 --- frontend/src/index.html | 2 +- frontend/src/lib/field-errors.ts | 67 +++++++++++++++++++++++++++++ frontend/src/styles/forms.css | 7 ++++ frontend/src/users/userModals.ts | 72 ++++++++++++++++++++++---------- 4 files changed, 124 insertions(+), 24 deletions(-) create mode 100644 frontend/src/lib/field-errors.ts diff --git a/frontend/src/index.html b/frontend/src/index.html index ebbbdd24c..19f3bc59a 100644 --- a/frontend/src/index.html +++ b/frontend/src/index.html @@ -1055,7 +1055,7 @@

Create API Key