feat(admin): overhaul the users filter bar and list (BUNYIP-410 follow-up) #408
Loading…
Reference in a new issue
No description provided.
Delete branch "feat/BUNYIP-410-users-filter-overhaul"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Rebuilds
/admin/usersfrom four stacked full-width controls into a real filter bar + sortable list. Before/after: the old page (ref_user.png) had ambiguous full-width selects showing "Verified"/"Early Adopter" as if chosen, a dead "Filter" button, and unstyled Active/Suspended buttons.Filter bar
<details>dropdowns (not native selects) at content width, each with a persistent prefix ("Verification: Any", "Tier: Any") and an explicit Any default shown on first load, so a filtered state is never ambiguous.State + feedback
aria-liveregion. Chips + count in reserved-height rows (no shift).List
titleon the relative "Joined", whole-row link with hover bg + focus-visible ring + chevron.suspendedflag) are not links and keep an inline Reactivate.Dynamic behaviour
aria-live. Component CSS/JS inline (no stylesheet rebuild, no stale-cache risk). Light/dark/contrast.Deliverable notes
Endpoint - did filter/sort already exist? Filtering (search/tier/verification/status) already existed from BUNYIP-410. Server-side sort did not and was added here:
list_paginatedgains a whitelistedsort_by+sort_descwith a stableidtiebreak, andactivebecomes tri-state so "All" returns both live and suspended rows. No client-side array filtering.Bulk actions - my call: not built (deferred). There are no bulk API endpoints (suspend / resend-verification are per-user), so it needs backend work + selection UI + a contextual action bar. A checkbox column with no working actions is worse than a clean deferral. Recommend a follow-up ticket naming the exact bulk operations wanted; I'll wire the endpoints + selection bar then.
Real avatar photos - deferred to letter fallback. Serving another user's photo needs an admin avatar-serve endpoint (the current serve is self-only,
/v1/users/me/avatar) plus an avatar field on the admin user response. The letter badge is in place; photos are a small follow-up.Verification
just check-container(toolchain-less fmt + clippy-D warnings+ build + test) green: 134 web tests, incl. the query-state URL builder, sort toggle, filter-resets-page, is-filtered, and a panel render (count + segmented + sortable headers + chips).🤖 Generated with Claude Code
Rebuilds the /admin/users page from four stacked full-width controls into a proper filter bar + sortable list, addressing the ambiguity, layout, and usability complaints. Filter bar (single wrapping row): the search input grows to fill; the Verification and Tier filters are the app's own `<details>` dropdowns (not native selects) at content width, each with a persistent prefix ("Verification: Any", "Tier: Any") so a filtered state is never ambiguous and every dropdown has an explicit "Any" default shown on first load. Active / All / Suspended is a single segmented control (radiogroup, one selected, arrow-key navigable). The "Filter" button is gone: filters apply on change, search is debounced 250ms. State + feedback: all filter/sort/page state lives in the URL (linkable, refresh, back), applied filters render as removable chips with a "Clear all" that only appears when something is non-default, the heading reflects the segment ("Active users" / "All accounts" / "Suspended accounts"), and the count shows "N of M users" via an aria-live region. Chips and count sit in reserved-height rows so nothing shifts. List: a grid with column headers where Email, Tier, Verification, and Joined are sortable (server-side, whitelisted columns, direction toggles on repeat click, indicator on the active column, sort persisted in the URL). Each row gains a letter-initial avatar, an absolute-date title on the relative "Joined" label, and is a whole-row link with hover background + focus-visible ring + chevron. Suspended rows (surfaced on the new "All" segment via a new `suspended` flag on the user response) are not links and keep an inline Reactivate. Prev/next pager + a page-size control (10/20/50/100). Dynamic behaviour uses htmx to swap just the panel (search-as-you-type keeps focus and scroll, no full reload), with a skeleton overlay that does not collapse the container height, an error state with Retry, and the URL pushed on every change. It degrades to full-page GET navigation without JS. Keyboard: Tab reaches every control, the segmented control responds to arrow keys, sort headers activate on Enter (native) and Space (added), and the count is aria-live. Component CSS/JS are inline (no stylesheet rebuild, no stale-cache risk). Endpoint: filtering (search / tier / verification / status) already existed from BUNYIP-410; server-side SORT did not and was added here (list_paginated gains a whitelisted `sort_by` + `sort_desc` with a stable `id` tiebreak, and `active` becomes tri-state so "All" returns both live and suspended rows). No client-side array filtering. Bulk selection: intentionally NOT built. There are no bulk API endpoints (suspend / resend-verification are per-user), so it needs backend work plus selection UI plus a contextual action bar; a checkbox column with no working actions is worse than a clean deferral. Recommend a follow-up ticket that names the exact bulk operations. Real avatar photos are also deferred to a letter fallback: serving another user's photo needs an admin avatar endpoint (the current serve is self-only) and an avatar field on the admin user response. Verified: `just check-container` (the toolchain-less fmt + clippy -D warnings + build + test) green, 134 web tests including the query-state URL builder, sort toggle, filter-resets-page, is-filtered, and a panel render (count + segmented + sortable headers + chips). The sort SQL and tri-state status were verified against a throwaway Postgres. No em-dashes in the diff. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L5dcYueNHByRnWJDYoDX1W