11 KiB
Table Pagination and Selector Usage
This guide explains how to build paginated tables in the NOW Database frontend, especially when you need to transform API rows with a selectorFn before rendering.
Detail tab table inventory (audit baseline)
The matrix below inventories current detail-tab table/list implementations under:
frontend/src/components/Locality/Tabs/*frontend/src/components/Species/Tabs/*frontend/src/components/Reference/Tabs/*frontend/src/components/TimeUnit/Tabs/*frontend/src/components/TimeBound/Tabs/*frontend/src/components/Museum/Tabs/*
Legend:
- Primitive:
SimpleTable,EditableTable,SelectingTable,LookupSelectingTable, or directTableViewusage. - Data source:
API-driven: tab issues a query hook in the tab component.Context-driven: table rows come from detail context payload (data/editData) and are rendered client-side.Mixed: selection is API-driven while edited/read table rows are context-driven.
- Edit-mode actions summarize current mutation UX.
User-requested URLs (explicit coverage)
| URL | Tab component | Primitive(s) | Data source | Edit-mode actions |
|---|---|---|---|---|
/locality/10003?tab=2 (Locality/Species) |
Locality/Tabs/SpeciesTab.tsx |
SelectingTable + EditableTable |
Mixed | Add new species form, copy taxonomy selector, select existing species, remove/re-add linked rows via rowState in editable table. |
/locality/10006?tab=8 (Locality/Museums) |
Locality/Tabs/MuseumTab.tsx |
SelectingTable + EditableTable |
Mixed | Select museum from lookup list, add link to now_mus, remove/re-add linked rows via editable table actions. |
/locality/10003?tab=9 (Locality/Projects) |
Locality/Tabs/ProjectTab.tsx |
SelectingTable + EditableTable |
Mixed | Select project from API list, duplicate guard message, remove/re-add project links via editable table actions. |
/species/10001?tab=6 (Species/Localities) |
Species/Tabs/LocalityTab.tsx |
SelectingTable + EditableTable |
Mixed | Select locality and append to now_ls, remove/re-add links via editable table actions. |
/species/10001?tab=7 (Species/Locality Species) |
Species/Tabs/LocalitySpeciesTab.tsx |
EditableTable |
Context-driven | "Add new Locality Species" modal is present (TODO save path), row remove/re-add handled by editable table actions. |
/reference/10029?tab=1 (Reference/Localities) |
Reference/Tabs/LocalityTab.tsx |
SimpleTable |
API-driven | No edit actions (read-only linked list with row navigation). |
/reference/10029?tab=2 (Reference/Species) |
Reference/Tabs/SpeciesTab.tsx |
SimpleTable |
API-driven | No edit actions (read-only linked list with row navigation). |
/time-unit/agenian?tab=1 (Time Unit/Localities) |
TimeUnit/Tabs/LocalityTab.tsx |
SimpleTable |
API-driven (+ client transform for checkmark/X columns) | No edit actions. |
/time-bound/9?tab=1 (Time Bound/Time Units) |
TimeBound/Tabs/TimeUnitTab.tsx |
SimpleTable |
API-driven | No edit actions. |
/museum/APM?tab=0 (Museum/Localities) |
Museum/Tabs/LocalityTab.tsx |
SimpleTable |
Context-driven (museum details payload) | No edit actions in this tab; guidance text shown for linking via Locality edit flow. |
Additional table/list tabs in audited folders
| Component | Primitive(s) | Data source | Edit-mode actions |
|---|---|---|---|
Locality/Tabs/TaphonomyTab.tsx |
LookupSelectingTable + EditableTable |
Mixed | Select collecting methods from lookup API and manage linked rows through editable row actions. |
Locality/Tabs/LithologyTab.tsx |
LookupSelectingTable + EditableTable |
Mixed | Select sedimentary structures from lookup API and manage links via editable row actions. |
Locality/Tabs/LocalityTab.tsx |
EditableTable |
Context-driven | Manage locality synonyms through editable row actions. |
Species/Tabs/SynonymTab.tsx |
EditableTable |
Context-driven | Add/edit/remove synonym rows on species detail payload. |
Species/Tabs/TaxonomyTab.tsx |
SelectingTable |
API-driven | Copy taxonomy from an existing species into taxonomy form fields. |
Reference/Tabs/AuthorTab.tsx |
SelectingTable + EditableTable |
Mixed | Select/add authors and manage relation rows in edit mode. |
Reference/Tabs/JournalTab.tsx |
SelectingTable + EditableTable |
Mixed | Select/add journals and manage relation rows in edit mode. |
Current DRY opportunity notes
SimpleTableandEditableTablecurrently disable advanced column/table interactions (sorting/column actions/filtering parity withTableViewis not present).- The dominant repeating pattern in edit tabs is selector table + editable linked table, which is a suitable extraction target for a shared detail-tab table abstraction.
- User-requested parity work should prioritize the ten explicitly listed URLs first, then roll over to additional table tabs listed above.
Core building blocks
| Concern | Implementation |
|---|---|
| Pagination metadata cache | frontend/src/redux/slices/tablesSlice.ts stores TablePaginationMetadata keyed by tableId. |
| Query wiring | frontend/src/hooks/usePaginatedQuery.ts derives pagination info from RTK Query (or other data hooks) and keeps the slice in sync. |
| Shared table chrome | frontend/src/components/Table/Table.tsx reads metadata for a tableId and conditionally renders TablePagination. |
| Controls | frontend/src/components/Table/TablePagination.tsx renders next/previous buttons, item counts, and loading states. |
Always pick a stable tableId (string) for each paginated table instance. The ID links the query hook, Redux metadata, and <Table> wrapper together.
Using usePaginatedQuery
- Choose or create a query hook (e.g. an RTK Query endpoint). Ensure the server response exposes pagination hints:
- Either wrap rows in an envelope
{ data, totalItems, pageIndex, pageSize }, - or include a
full_countfield on the first row (legacy convention).
- Either wrap rows in an envelope
- Call
usePaginatedQuerywith:- the query hook,
- a
tableId, - the current
queryArg(includelimit/offsetif supported), - an optional
selectorFnto map raw rows to the render shape.
- Use the returned
rows(from your hook) or the transformed output fromselectorFnto render table content. usePaginatedQueryautomatically:- normalises envelopes/arrays,
- preserves
full_countwhenselectorFnstrips it, - computes and stores
TablePaginationMetadatain Redux, - exposes helper booleans (
isLoading,isFetching,isError) from the underlying query result for UI state.
Selector-specific tips
- When mapping rows, ensure your selector returns a stable key for list rendering.
- If your mapped rows drop the
full_countcolumn,usePaginatedQueryre-injects it into the first item so pagination totals stay accurate. - Avoid deriving pagination totals inside the component—always trust the metadata from
usePaginatedQueryto keep controls and counts consistent.
Rendering the table
Wrap the body of your table inside <Table> to get controls for free:
<Table
tableId="time-unit-sequences"
onPageChange={handlePageChange}
isLoading={isFetching}
paginationPlacement="both"
renderEmptyState={() => <Alert severity="info">No sequences found</Alert>}
>
<SequenceRows rows={sequences} />
</Table>
Key props:
tableId: must match the ID passed tousePaginatedQuery.onPageChange: receives a zero-based page index. Update theoffset/pagein your query arg and letusePaginatedQueryrefetch.isLoading: disables pagination buttons while data is reloading.paginationPlacement: choose'top' | 'bottom' | 'both'.renderEmptyState: optional placeholder when there are zero results.hidePaginationWhenEmpty: hides controls whentotalItems === 0.
Working with query arguments
- Treat
pageIndexas zero-based throughout the UI. Convert to 1-based numbers only in copy (Page 1 of 3). - When using limit/offset APIs:
limit = pageSizeoffset = pageIndex * pageSize
- When an endpoint expects
page+pageSize, storepageIndexin component state and convert before dispatching the query.
Testing checklist
For every paginated table with a selector:
- Assert that the rendered pagination summary matches the metadata (e.g. "Showing 11-20 of 24").
- Simulate
Next/Previousclicks and assert that new query args are dispatched. - Confirm the rendered rows change between pages (catching stale selector memoisation).
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| Pagination controls do not render | Missing metadata in Redux | Ensure usePaginatedQuery wraps the query and that tableId matches the <Table> component. |
| Counts update but rows stay the same | selectorFn memoises rows or query arg never changes |
Derive new offset/page values on onPageChange and avoid caching rows outside React state. |
| Buttons never disable | isLoading not forwarded |
Pass `isFetching |
Keep this guide updated as new pagination utilities or patterns land in the codebase.
Unified DetailTabTable behavior (post-migration)
Detail tabs migrated to DetailTabTable now share the same interaction baseline:
- Column sorting via header click (ascending/descending).
- Shift-click multi-column sorting.
- Per-column filter popovers and quick search where column filter variants are enabled.
- Column visibility toggle menu.
- CSV export action for migrated tab tables.
Known exceptions and constraints
- Endpoints listed in backend tab-list validation currently reject non-empty server-side
columnfilterspayloads; filtering remains client-side for those datasets. - Edit-only mutation actions (select existing, add new, remove/re-add) are only rendered when detail mode and page edit rights allow mutation.
- Read-only viewers retain navigation and table interaction controls but do not see mutation controls.
Rollout checklist for tab migrations
Use this checklist whenever migrating a remaining tab list/table to DetailTabTable:
- Primitive migration
- Replace direct
SimpleTable/TableViewusage withDetailTabTablein the target tab. - Preserve row click navigation and return-stack behavior.
- Replace direct
- Query semantics
- Ensure endpoint supports validated
sortingand pagination (paginationorlimit/offset). - If server-side column filters are unsupported, confirm client-side filtering remains enabled in the table.
- Ensure endpoint supports validated
- Permission parity
- Verify read-only roles cannot access edit controls.
- Verify editors can still perform select/add/remove/re-add flows.
- Regression checks
- Confirm URL-provided sorting overrides defaults.
- Confirm default species ordering applies when no explicit sorting is set.
- Confirm export reflects current sorted/filtered/visible table state where supported.
- QA gates
- Run lint/typecheck at root.
- Run frontend unit tests for touched tabs.
- Run selected Cypress smoke coverage for navigation and read vs edit visibility.