Skip to content

Staging#214

Merged
samseaver merged 19 commits into
ModelSEED:stagingfrom
VibhavSetlur:staging
May 28, 2026
Merged

Staging#214
samseaver merged 19 commits into
ModelSEED:stagingfrom
VibhavSetlur:staging

Conversation

@VibhavSetlur
Copy link
Copy Markdown
Collaborator

This pull request enhances the filtering and search experience across multiple data grid pages in the application by introducing the withQuickSearchHeaders utility and integrating the useToolbarGridFiltering hook where appropriate. These changes ensure a more consistent and powerful quick search/filtering capability in all major data tables. Additionally, some redundant props are removed for cleaner code.

Quick search and filtering improvements:

Code cleanup and simplification:

  • Removes the disableColumnMenu prop from all affected DataGrid components, simplifying the grid configuration and relying on the new quick search headers for column-level actions. [1] [2] [3] [4] [5] [6] [7]

These improvements provide a more unified and user-friendly filtering/search experience across the app's data tables.

VibhavSetlur and others added 8 commits May 27, 2026 21:52
Removes grid-level disableColumnMenu from every DataControlHeader page so
each column header exposes its kebab menu (Sort, Filter, Hide, Manage). The
per-column Filter entry is the "column-based search" users relied on to
filter a single column in one click — the global Filter & Columns popover
remains for multi-row use.

In DataControlHeader, sync grid-filter-model changes that originate outside
the toolbar (e.g. column-menu Filter) into the committed registry, so the
"Filter & Columns (N)" badge stays accurate and the search box no longer
wipes the column-menu filter. The sync has a guard so it doesn't fight
Community Edition's auto-truncation after a multi-row toolbar save.

Per-column disableColumnMenu: true on action/icon columns is preserved.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Each filterable column header now shows an always-visible search icon
(left of the on-hover kebab menu). Clicking it opens a small popover
with a TextField that filters just that column — much faster than going
through Filter & Columns → pick column → contains → type.

DataControlHeader's committed-filter registry gets a tiny pub/sub so
the per-column QuickSearchHeader (which writes to the shared registry
without prop access to the toolbar) can notify ToolbarFilterEditor to
re-sync its ref and refresh the "Filter & Columns (N)" badge count.
An onApplyFilterModel registry (apiRef → callback) lets the per-column
popover dispatch to the page's server-filter handler without prop
drilling.

Per-column filters coexist with the global search box and the multi-
row Filter & Columns editor: all three write to the same registry,
and on Community Edition (which truncates filterModel.items to one
entry) the quick-column filter takes the active slot so the visible
behavior matches the user's most recent action.

Wraps consumer columns with the new withQuickSearchHeaders helper in
every page/component that uses DataControlHeader.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Previously each per-column magnifying-glass filter replaced the previous
one — typing in column B wiped column A's filter.  Two causes:

1. DataControlHeader's QuickSearchHeader called apiRef.setFilterModel
   with items.slice(0, 1) after onApply.  That re-fires the page's
   onFilterModelChange with the truncated single item and races whatever
   state onApply just committed.  Skip the grid round-trip entirely when
   onApply is registered; the page (server fetch or local hook) owns the
   multi-item AND.  Bare client-side grids without a page handler fall
   back to the truncated setFilterModel (Community Edition can only honor
   one filter item).

2. The lazy client-side consumer pages (model, fba, gapfill, genome,
   ReactionKnockoutsDialog) had no page-level filter handler, so the
   DataGrid's Community filter engine was the only thing filtering rows
   — and it sees at most one item.  Wire each grid through
   useToolbarGridFiltering (which ANDs items locally via
   filterDocsByGridModel) and pass filterModel / onFilterModelChange /
   onApplyFilterModel so per-column quick filters and the Filter &
   Columns multi-row editor both produce true AND filtering.

For the model page, factor the per-tab DataGrid into a small
ModelTabDataGrid component so the hook lives at component top level
(rules of hooks) and the per-tab filter state is naturally isolated.
Filters are applied to the full row set before sort/paginate so the
visible page reflects the full filtered count.

Server-side reference-data pages (compounds, reactions, genomes,
Annotations, list-media, my-jobs, my-models, myMedia, Patric/Rast
genome tables) are unchanged at the page level but now benefit from
the cleaner dispatch — their existing toolbarSaveRef guard is no longer
racing the truncated round-trip, since it no longer happens.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The previous debounced auto-apply re-rendered the parent page mid-typing,
which made the column header (and the open Popover with it) remount and
discard the in-flight draft text — observed by the user as "I can only
type one letter and it's replaced by the next."

Switch to an explicit-commit model:
- Typing only updates local draft state — no parent re-render until commit
- Enter commits the filter and closes the popover
- Escape / click-outside cancels (closes without applying)
- The X icon clears the column's filter
- Reopening the icon seeds the draft with the currently-applied value so
  the user can extend or replace it

Multiple per-column filters AND together: each Enter adds (or replaces)
this column's entry in the shared committedFilterRegistry under a
predictable id (quick-col-<field>); the full multi-item model goes
through onApplyFilterModel so server pages re-fetch and client pages
(via useToolbarGridFiltering) re-derive filteredRows with all items
ANDed. The toolbar Filter & Columns badge stays in sync via the
existing committedFilter pub/sub.

Also memoize the wrapped-columns output by input identity so unrelated
parent re-renders (data refresh, loading state flips) don't force MUI
to rebuild the column header tree and tear down an open popover.

ToolbarSearchField (the global search box) and ToolbarFilterEditor
(Filter & Columns) are unchanged.

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

The Filter & Columns popover wasn't reflecting per-column quick filters
because ToolbarFilterEditor mirrored the committed registry into a mutable
ref (committedItemsRef).  Refs don't trigger re-renders, and openEditor
was reading the ref — so the badge count and the loaded draft rows could
go stale relative to the registry whenever the registry was updated by
a sibling (the per-column QuickSearchHeader Enter commit, the column-
menu Filter sync) and the ref-update path had any timing slack.

Replace the ref pair (committedItemsRef + committedLogicOperatorRef +
manual forceUpdate ticks) with a single useState mirror keyed by the
committedFilterRegistry pub/sub.  Any setCommittedFilter call from any
source — per-column header, toolbar save, column-menu sync — now drives
a setState and the badge label re-renders automatically.  openEditor
also reads the registry directly as a belt-and-suspenders source of
truth, so the popover always loads the latest per-column filters as
editable rows.

Net effect: applying a quick per-col filter immediately bumps the
"Filter & Columns (N)" badge and shows up as a row when the user opens
the popover; editing or X-ing that row in the popover and saving
propagates back to the per-column icon's active-state highlight.

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

When a column has any active filter (added via the quick search popover
OR via the Filter & Columns editor), the magnifying-glass icon now:

- Switches to a filled FilterAltIcon
- Gets a primary-color background pill with white icon
- Gets a small primary-color dot badge in the corner (extra glanceable)
- Shows a tooltip describing the applied operator + value
- The popover's helper text echoes the same summary

Active-state matching now considers ALL filter items on the column's
field, not just those added via the quick-search id, so filters added
through the toolbar editor also light up the column's icon.

Row count in the toolbar (CustomPagination's "1-25 of N") already
reflects each filter change automatically:
- Server-side pages: the rowCount prop = numFound and refetches
- Client-side useToolbarGridFiltering pages: rows.length = filteredRows.length
- Model lazy tabs: rowCount = preparedRows.length (sort(filteredRows))

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The R extends Record<string, unknown> constraint rejected typed row
interfaces (Compound, Reaction, ModelReaction, etc.) because they lack
an implicit index signature, causing the generic to fall back to the
default and return GridColDef<Record<string, unknown>>[]. That then
forced DataGrid's R to infer as Record<string, unknown>, cascading
into rows/getRowId type errors across many pages.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings May 28, 2026 05:27
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a per-column quick-search header (magnifying-glass icon + popover) to MUI DataGrid usage across the app via a new withQuickSearchHeaders helper, and wires more grids into the shared useToolbarGridFiltering hook so column filters AND together correctly under MUI Community Edition's single-filter truncation. DataControlHeader is refactored to drive the badge label from a state-mirrored, pub/sub-backed shared filter registry, and the FBA results page and ModelDetailPage (via a new ModelTabDataGrid wrapper) gain controlled filter models.

Changes:

  • New withQuickSearchHeaders and QuickSearchHeader in DataControlHeader.tsx, plus onApplyFilterModelRegistry and pub/sub on the committed-filter registry to share state across toolbar editor, header icons, and the page.
  • All major data grids (biochem, genomes, list-media, my-models, my-jobs, myMedia, gapfill, genome, fba, model, ReactionKnockoutsDialog, Rast/Patric genomes) now wrap their columns with withQuickSearchHeaders and (where applicable) integrate useToolbarGridFiltering; redundant disableColumnMenu props removed.
  • app/model/[...path]/page.tsx extracts a ModelTabDataGrid wrapper that owns per-tab useToolbarGridFiltering and applies filters before sort/paginate for lazy large tabs.

Reviewed changes

Copilot reviewed 16 out of 16 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
components/layout/DataControlHeader.tsx Adds withQuickSearchHeaders/QuickSearchHeader, onApplyFilterModelRegistry, pub/sub registry, and reworks toolbar editor to use a state-mirrored committed filter snapshot.
components/ui/ReactionKnockoutsDialog.tsx Wires useToolbarGridFiltering + withQuickSearchHeaders and controlled filterModel on the dialog grid.
components/build-model/RastGenomesTable.tsx Wraps columns with withQuickSearchHeaders; removes disableColumnMenu.
components/build-model/PatricGenomesTable.tsx Wraps columns with withQuickSearchHeaders; removes disableColumnMenu.
app/model/[...path]/page.tsx Extracts ModelTabDataGrid with per-tab useToolbarGridFiltering, filters before sort/paginate, wraps tab columns with withQuickSearchHeaders.
app/genome/[...path]/page.tsx Adds feature/annotation filtering via useToolbarGridFiltering and column-header quick search.
app/gapfill/[...path]/page.tsx Adds gapfill grid filtering via useToolbarGridFiltering and column-header quick search.
app/fba/[...path]/page.tsx Adds reactions/exchanges/pathways filtering hooks, controlled filter model, and column-header quick search.
app/(user-data)/myMedia/page.tsx Wraps columns with withQuickSearchHeaders; removes disableColumnMenu.
app/(user-data)/my-models/page.tsx Wraps columns with withQuickSearchHeaders; removes disableColumnMenu.
app/(user-data)/my-jobs/page.tsx Wraps columns with withQuickSearchHeaders.
app/(reference-data)/list-media/page.tsx Wraps columns with withQuickSearchHeaders; removes disableColumnMenu.
app/(reference-data)/genomes/page.tsx Wraps columns with withQuickSearchHeaders; removes disableColumnMenu.
app/(reference-data)/genomes/Annotations/page.tsx Wraps columns with withQuickSearchHeaders; removes disableColumnMenu.
app/(reference-data)/biochem/reactions/page.tsx Wraps columns with withQuickSearchHeaders; removes disableColumnMenu.
app/(reference-data)/biochem/compounds/page.tsx Wraps columns with withQuickSearchHeaders; removes disableColumnMenu.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +2581 to +2591
columns={
tab.key === 'reactions'
? reactionColumns
: tab.key === 'compounds'
? compoundColumns
: tab.key === 'fba'
? fbaColumns
: tab.key === 'pathways'
? pathwayColumns
: tableConfig[tab.key].columns
}
Comment thread components/layout/DataControlHeader.tsx Outdated
Comment on lines +1055 to +1061
const [, forceTick] = useState(0);
const [committed, setCommitted] = useState<{ items: GridFilterItem[]; logicOperator: GridLogicOperator } | undefined>();
useEffect(() => {
setCommitted(committedFilterRegistry.get(apiRef.current));
return subscribeCommittedFilter(apiRef.current, () => {
setCommitted(committedFilterRegistry.get(apiRef.current));
forceTick((n) => n + 1);
VibhavSetlur and others added 11 commits May 28, 2026 08:51
…er-column filters

Removes the 300ms keystroke debounce in ToolbarSearchField that raced with
the multi-column-filter state cascade on pages re-rendering frequently (e.g.
my-models polls tracked-job status every 15s), which dropped keystrokes after
the first letter when two per-column quick filters were already committed.

Now mirrors the per-column QuickSearchHeader contract: typing is purely local
state, Enter commits, Escape reverts or clears, and the CSS Highlight effect
is driven by the committed term so highlights appear only on Enter. Other
committed column filters are preserved (applySearch still reads them from
the shared registry).

E2E specs updated to press Enter after fill on the global search input.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces the prevCommitted ref + useEffect setState pattern (flagged by
react-hooks/set-state-in-effect) with React's recommended "adjust state
when a prop changes" approach: store prevCommitted in useState and update
both prevCommitted and draft synchronously during render when the grid's
committed quick-filter value diverges.

Functionally equivalent — same divergence guard (only overwrite draft when
the user hasn't typed past the previously committed value) — and removes
the now-unused useRef import. Unblocks CI on staging.
https://react.dev/learn/you-might-not-need-an-effect#adjusting-some-state-when-a-prop-changes

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace the per-column magnifying-glass single-input popover with a 3-field
form mirroring one row of the Filter & Columns editor: locked column, operator
dropdown (defaulting to the type's quick-search operator — `contains` for
strings), and value input matched to the operator. Apply/Enter commits,
Cancel/Escape discards, Clear removes this column's quick item. Re-opening on
an already-filtered column pre-seeds operator and value from the registry.
Filter writes still flow through the shared committed-filter registry and the
page's onApplyFilterModel handler, so the data control header remains the
single owner of multi-row + AND/OR state.

Also suppress MUI's 3-dot column menu on every column the helper sees, so each
header is just `Name + magnifying-glass icon`.

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

Reactions Transport column was a JS boolean rendered as Yes/No but declared
as the implicit string type, so the quick filter built `is_transport:*No*` in
Solr and never matched. Declaring `type: 'boolean'` routes the per-column
quick filter through the boolean operator list (is / is not / isEmpty /
isNotEmpty) with a Yes/No dropdown that emits true/false — matching how Solr
indexes the field. Boolean Value selects in both the toolbar editor and the
per-column popover now show Yes/No labels (still emit true/false on the
wire).

Add a `between` operator for number and date columns. Dates default to it on
opening the quick filter, with two From/To pickers side-by-side; either side
may be left empty for an open-ended bound. The toolbar editor accepts the
same operator via `from, to` text input. Values are stored as a 2-element
array preserving empty positions, then routed through:

- Server: `buildFilterClause` emits Solr `field:[from TO to]` with `*` sentinels
  for empty sides.
- Client: `matchesFilterItem` compares numerically when bounds + field parse as
  numbers, else by date, else lexically.

Toolbar editor and per-column popover both delegate value coercion to a
shared `coerceFilterValue` so they always produce identical GridFilterItem
shapes for the same operator.

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

`autoFocus` on the TextField wasn't winning against MUI Popover's FocusTrap,
which latched onto the Operator select and forced an extra click before
typing. Focus the value field (or the From field for `between`) explicitly
via the Popover's TransitionProps.onEntered, and select any pre-seeded text
so typing replaces it immediately.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The icon used to sit immediately to the right of short header text in wide
columns. Root cause: the renderHeader Box was sized to fit content, so the
flex layout had no extra space for the icon to drift into. Make the Box
`flex: 1, width: 100%` so it spans MUI's title-content slot, and push the
icon to the far right with a spacer wrapper carrying `ml: auto, pl: 1`.

The spacer is a separate Box rather than padding on the Badge — padding on
the Badge would shift its absolutely-positioned dot indicator off the icon
corner.

Sort still triggers on clicks anywhere in the header except the icon: the
name text doesn't stopPropagation, so MUI's column-header click handler
receives the bubbled event; the icon's openPopover handler stops it.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@samseaver samseaver merged commit 3b395b4 into ModelSEED:staging May 28, 2026
2 checks passed
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.

3 participants