Tabular review filtering by flag, verified state, and per-column text

nwhitehouse added a filter bar above the tabular review table that lets reviewers narrow rows by flag color, whether a cell has been manually verified, and per-column text predicates. The filter logic is pure and unit-tested; four commits address UX issues - mostly popover rendering against a sticky-column z-index stack - found through smoke testing.

contract-reviewworkflow

Migration 006_tabular_cells_verified.sql adds verified bool default false, verified_at timestamptz, and verified_by uuid to tabular_cells - all nullable or defaulted, safe to apply against live data. PATCH /tabular-review/:reviewId/cells/verify toggles a single cell and records who and when, giving a future audit story something to work with without a schema change.

trFilterPredicate.ts is the clean part: a pure (cell, filter) => boolean function plus computeVisibleDocIds(docs, cells, filters) => Set<docId>, with AND semantics across filters. Text predicates support contains, does_not_contain, is, is_not, is_empty, and is_not_empty. The module ships 11 unit tests.

TRFilterBar.tsx renders active filters as dismissible pills with a "+ Add filter" popover. Filter state is localStorage-backed per (review, device).

The four-commit bug sequence is worth reading as a cautionary tale. First: both the filter popover and the ColumnVisibilityMenu were at z-30; the sticky document-name column in TRTable runs at z-[60] and painted over the middle of both popovers. Fix: bump to z-[80]. Second: adding the same Flag or Verified filter twice ANDed them (Flag=yellow AND Flag=unverified is logically empty), so the fix limits those two filter types to one each - adding a second replaces the first. Text predicates can still stack across columns. The same commit also anchors the popover at right-0 so it grows leftward from the trigger. Third: right-0 always clips the popover when the trigger is at the left edge of an empty filter row. The final fix uses useLayoutEffect to measure getBoundingClientRect on open and picks left-0 if there's at least 320px of space to the right, otherwise right-0.

So what Worth importing for any product showing reviewable LLM output at scale. The verified-state plus flag filtering combination is what reviewers ask for once the table grows, and the pure-predicate split with unit tests is straightforward to adapt to a different schema. The popover z-index and dynamic-side positioning fixes are what you'd want to copy if you build a similar filtering surface over a sticky-column table.

View this fork on GitHub →

Spotted something wrong? Or know the PR text has fresher detail than the writeup above?

Commits in this thread

4 commits from nwhitehouse/mike, oldest first. Source extracted verbatim from the harvested git log.

SHA Subject Author Date
80763429 [feat-023] Tabular review filtering + cell verified state Nick Whitehouse 2026-05-07 ↗ GitHub
commit body
User-facing: filter the table by flag, verified state, and per-column
text predicates. Mark cells verified with a hover ✓ button so the
verified-state filter has something to bite on. Critical when reviews
have more than ~50 docs and the user is hunting for "just the red
ones" or "the cells I haven't checked yet".

Backend:
- Migration 006: tabular_cells gains verified bool (default false),
  verified_at timestamptz, verified_by uuid → auth.users. Additive,
  safe against live data.
- PATCH /tabular-review/:reviewId/cells/verify - toggles a single
  cell's verified state. Records who/when. Reuses ensureReviewAccess.

Frontend:
- trFilterPredicate.ts - pure (cell, filter) → boolean +
  computeVisibleDocIds(docs, cells, filters) → Set<docId>. Operators:
  contains / does not contain / is / is not / is empty / is not empty.
  AND across multiple filters. Flag + verified filters union per-doc;
  text filters scoped to their column. 11 unit tests pass.
- TRFilterBar.tsx - pill row above the table. Active-filter pills with
  ✕ removal. + Add filter popover with Flag (multi-select) / Verified
  (radio) / Column-value (column → operator → value) tabs.
  "Showing N of M documents matching K filters" caption. Clear all link.
- TabularCell.tsx - hover-revealed ✓ button (top-left). Always visible
  when verified (subtle green check); fades in on hover when not.
- TabularReviewView wires filter state in localStorage per
  (review, device); applies via computeVisibleDocIds composed on top of
  the existing doc-name search; mounts TRFilterBar above the table when
  any columns exist; passes onToggleVerifyCell to TRTable.
- TRTable threads the verify handler through to TabularCell and grows
  pl-4 padding when verify is enabled so the ✓ doesn't overlap content.
- TabularCell type extended with verified / verified_at / verified_by;
  no API mapping change needed (Supabase JS auto-includes selected
  columns, and TabularReviewDetailOut → TabularCell[] passes through).

mikeApi: setTabularCellVerified helper added.

Verified: tsc clean both sides; 11 new frontend predicate tests pass via
bun test; 16 backend tests still pass; migration 006 applied locally
(verified column non-null with default false; verified_at + verified_by
nullable).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
e95c36c7 [feat-023] Lift filter + column popovers above sticky table cells Nick Whitehouse 2026-05-07 ↗ GitHub
commit body
User reported the Add filter popover rendered with empty middle -
turned out the sticky doc-name column in TRTable runs at z-[60] and
the popovers were at z-30, so the table cells were painting over the
popover content. Bumped both popovers (TRFilterBar add-filter +
ColumnVisibilityMenu) to z-[80] so they sit above the sticky cells
but below modal overlays at z-[100]+.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
c6c92bc7 [feat-023] Filter UX: dedupe single-kind filters + anchor popover right Nick Whitehouse 2026-05-07 ↗ GitHub
commit body
Two real bugs from smoke testing:

- Adding the same flag/verified filter multiple times stacked them and
  silently AND'd the conditions (Flag: Yellow + Flag: Yellow,Red →
  matches docs that have BOTH a yellow cell AND a {yellow|red} cell).
  Worse, Verified-only AND Unverified-only is logically empty (no cell
  is both).
  Fix: at most one Flag filter and one Verified filter. Selecting
  either kind in the Add Filter popover replaces the existing one
  rather than stacking. Text predicates can still stack (different
  columns are useful AND-combinations).
  The popover form pre-fills from the existing filter of the same
  kind, so re-opening edits in place.

- The popover anchored at left-0 of the trigger; with many active
  filters the trigger sits at the right end of the row and the
  popover ran off the right edge of the viewport, half-clipped.
  Fix: anchor at right-0 so it grows leftward from the trigger and
  always stays in-viewport.

Bonus: the popover's default tab now picks the *missing* kind
(Flag → Verified → Column value), so building a multi-facet filter
set takes one fewer click.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
515592d0 [feat-023] Add-filter popover: dynamic side based on trigger position Nick Whitehouse 2026-05-07 ↗ GitHub
commit body
The earlier fix flipped the popover to right-0 always, which clipped
off the LEFT edge when the trigger was at the start of the row (the
common no-filters-yet case). Now measures the trigger's
getBoundingClientRect on open: if there's >= 320px of room to the
right of the trigger use left-0; otherwise right-0. Recomputed on
every open since the trigger moves as filters wrap.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Capture this thread into my fork

Download a single Markdown prompt that tells Claude how to port every commit above into your working tree — adapting paths and structure to match your repo. Run it via claude -p < capture-thread-153.md from inside the repo you want the changes in.

⬇ Download capture-thread-153.md