Skip to content

Replace Members inline filters with a Filter drawer, faceted counts, and job-title normalization - #39

Open
Bjjj834 wants to merge 2 commits into
mainfrom
feature/member-filter-drawer
Open

Replace Members inline filters with a Filter drawer, faceted counts, and job-title normalization#39
Bjjj834 wants to merge 2 commits into
mainfrom
feature/member-filter-drawer

Conversation

@Bjjj834

@Bjjj834 Bjjj834 commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Problem

The Members page (previous state, PR #37 still unmerged) had Company and Job Title as separate dropdowns inline next to the search bar. This PR replaces that with the requested UX: a single search bar + one "Filter" button that opens a drawer, plus faceted counts and job-title normalization. Built fresh off main since #37 hasn't merged yet.

Investigation (done before writing code)

  • Data source, pagination, and search are unchanged from prior investigations: the entire ~993-row Google Sheet is fetched once into the browser; there is no backend, database, or paginated API.
  • Conclusion: full-dataset filtering and faceted aggregation counts are both achievable with zero backend work, since the complete dataset is already in memory. This PR implements true faceted counts, not a simplified global-count fallback — there was no external-backend blocker.

UX design

  • Main page: search bar + one Filter button, showing an active-filter count badge (Filter (2)) when applicable (search itself doesn't count).
  • Clicking Filter opens a right-side drawer (near-full-width on mobile) with two collapsible sections: Company and Job Title, each with its own search-within box and a scrollable multi-select checkbox list showing label — count.
  • Selected filters render as removable chips in the main area (Company: Anote ×) and can be cleared individually or all at once (drawer's "Clear all").
  • Drawer has a backdrop, closes on Escape or the × button, and traps Tab focus while open (useFocusTrap.js).

Filtering logic

  • OR within a section (Company: Anote OR Google), AND across sections (Company AND Job Title), combined with the general search — all in memberFacets.js.
  • Company matching is exact (trimmed, case-insensitive) — never fuzzy-merged.
  • Job Title matching uses the normalized group key (see below) — exact per canonical group, not free-text keyword search (this is a checkbox multi-select now, not a combobox).
  • Blank/null and placeholder values (N/A, NA, -, none, etc.) are excluded from filter options entirely (found and fixed during manual QA — real data had "N/A"/"NA" showing up as selectable "companies").

Job-title normalization rules (jobTitleNormalization.js)

Documented in the file itself. Summary:

  1. cleanTitle — trims/collapses whitespace only.
  2. normalizeSeparators — rewrites &, +, ,, and the word "and" into a single canonical " / " separator. Safe to apply unconditionally: it only changes how words are joined, never which words are present, so it can't accidentally merge titles with different words (e.g. "Senior Product Manager" vs "Product Manager" are untouched — different word sets).
  3. jobTitleGroupKey — lowercases the separator-normalized string; this is the identity used for grouping/matching (so "CEO" vs "ceo" collapse, but "CEO" alone and "Founder / CEO" never do — different token sets).
  4. CANONICAL_JOB_TITLE_MAP — an explicit, hand-maintained table for cases a generic separator swap can't infer alone (e.g. "Founder CEO" with no punctuation at all). Each entry is documented with why it's an equivalence, and new mappings can be added here as new variants are discovered in the data.

A member's original title field is never overwritten — normalization is used only for building filter options and matching, never for display in the table.

Count behavior

  • Company counts respect the current search + selected Job Titles (not the Company selection itself).
  • Job Title counts respect the current search + selected Companies (not the Job Title selection itself).
  • A selected option stays visible in its section (at count 0) even if another active filter would otherwise zero it out — verified via a dedicated test and manually in the browser (a URL with two mutually-exclusive selections shows both chips + both checkboxes checked, with an empty result state).

Backend/API changes

None. Confirmed via investigation this repo doesn't need any — see above.

Manual verification (real ~993-row dataset, not mocks)

  • Selected "Anote" → Job Title facet immediately re-scoped to only Anote-relevant titles (AI Research Fellow 76, Anote GTM 4, CEO 4, Founder / CEO 2, etc.), Company facet showed 132.
  • Selected "Anote" + "Founder / CEO" → exactly 2 members, correctly excluding Anote members whose title is plain "CEO" (a distinct, unmerged group) — chips, Filter (2), and URL (?company=Anote&jobTitle=Founder+%2F+CEO) all correct.
  • Refreshed directly on that URL — filters and results persisted.
  • Verified at mobile viewport (375px) — drawer renders full-width, both sections' checked state and counts correct.
  • Confirmed "N/A"/"NA" no longer appear as company options after the placeholder-exclusion fix.

Tests completed

45 tests across 4 suites (npm test):

  • jobTitleNormalization.test.js — separator merging, case-insensitivity, placeholder exclusion, and explicit non-merge cases (seniority levels, related-but-distinct roles, single-role vs combined-role).
  • memberFacets.test.js — facet building/counts across the full dataset, blank/placeholder exclusion, AND/OR filter logic, faceted cross-filtering, "selected option stays visible at zero count."
  • ArmorMembers.test.js — drawer open/close (click, Escape, close button), full-dataset facet counts and options, search-within-section, multi-select OR, AND between sections, combining with general search, page-reset-on-filter-change, filter persistence across pagination, clear all, individual chip removal, active-filter count badge, empty state, URL hydration/sync, loading/error states, and an explicit "full dataset, not just the visible page" regression test.

npm run lint: 0 errors (160 pre-existing warnings elsewhere in the repo, none introduced here). npm run build (CI=false, matching this repo's own ci.yml/deploy.yml convention): succeeds.

Known limitations

Bjjj834 added 2 commits July 30, 2026 16:54
jest-dom was already a devDependency but no setupTests.js imported it,
so matchers like toBeInTheDocument/toHaveValue were unavailable to any
test in the suite.
Reworks the Members page filtering UX per feedback: the page now shows
only a search bar and a single "Filter" button (with an active-filter
count badge), instead of separate Company/Job Title dropdowns inline.
Clicking Filter opens a right-side drawer (near-full-width on mobile)
with two collapsible, searchable, multi-select checkbox sections.

Since the full ~993-member dataset is already loaded client-side (see
prior investigation), all of this — filtering, pagination, and facet
counts — operates on the complete dataset, not just the visible page.
No backend changes were needed or made.

- jobTitleNormalization.js: explicit, documented canonical-mapping
  utility that merges job titles differing only in separator/formatting
  (e.g. "Founder & CEO" / "Founder + CEO" / "Founder and CEO" / "Founder
  CEO" -> "Founder / CEO"), while never merging distinct roles or
  seniority levels (e.g. "Product Manager" vs "Senior Product Manager",
  "CEO" vs "Founder / CEO"). A member's original stored title is never
  overwritten — normalization is used only for grouping/filtering.
- memberFacets.js: filtering (AND across Company/Job Title sections, OR
  within a section, combined with general search) plus true faceted
  counts — Company counts respect the current search + selected Job
  Titles, Job Title counts respect search + selected Companies. A
  selected option stays visible (count 0) even if another filter zeroes
  it out. Blank/null/placeholder values ("N/A", "-", etc.) are excluded
  from filter options entirely.
- FilterSection.js / FilterDrawer.js: collapsible search-within-section
  checkbox lists, backdrop + Escape-to-close + manual focus trap
  (useFocusTrap.js), "Clear all" and "Done" actions.
- ArmorMembers.js: wires it all together — active-filter chips in the
  main area, empty state, search/company/jobTitle/page synced to URL
  query params (comma-separated multi-values, empty params omitted),
  pagination resets to page 1 on filter change but not on initial
  URL-hydrated load, and debounces the free-text search box.

Verified manually against the live ~993-row dataset in a browser
(desktop + mobile viewports): faceted counts, cross-filtering, URL
refresh/share, and the "N/A"/"NA" placeholder exclusion all behave
correctly.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant