Skip to content

enhancement(tables): one DataTable for paged and virtualized views with a shared feature set - #579

Merged
therealbrad merged 16 commits into
betafrom
data-table-merge
Aug 11, 2026
Merged

enhancement(tables): one DataTable for paged and virtualized views with a shared feature set#579
therealbrad merged 16 commits into
betafrom
data-table-merge

Conversation

@therealbrad

Copy link
Copy Markdown
Contributor

Description

Merges the two data-table components (DataTable and VirtualizedDataTable) into a single DataTable whose rendering mode is selected by a virtualized parameter, then uses the shared foundation to level the feature set across every table surface.

The merge

  • One public component with a discriminated props union: the default paged <table> engine, or the windowed infinite-scroll engine via virtualized
  • Shared internals (dataTableShared.tsx): sorting adapter, pinning init, expander column, grouped-cell rendering, column-order persistence, header menu items
  • All ~55 consumer surfaces migrated; per-mode DOM, test ids, and localStorage formats preserved

Features leveled up during UAT

  • Column drag-to-reorder, flex-column resizing, and the per-column header menu now work in virtualized tables
  • Sort ascending / Sort descending / Remove sort is the standard header control on every sortable column, both engines ("Remove sort" restores each surface's own default order); Hide column is offered wherever a Columns control owns visibility
  • Reviews inbox and the report drill-down drawer converted to virtualized rendering; the drawer fetches 500 records per page and both show the standard loaded-of-total count

Correctness fixes surfaced by UAT

  • Virtualized tables no longer re-sort server-ordered rows client-side (manualSorting) — fixes enum-column sort order and infinite-scroll page-seam scrambling; audited all 33 virtualized call sites for client-sort reliance first
  • Repository pagination no longer bounces to page 1 while an id-resolved sort re-resolves
  • The users page SCIM column sorts as the boolean it displays; the Access enum is rebuilt in least-to-most-privilege order (migration included, values unchanged)
  • JUnit results "Remove sort" restores its true executedAt desc default

Related Issue

N/A — interactive UAT session.

Type of Change

  • Bug fix (non-breaking change that fixes an issue)
  • New feature (non-breaking change that adds functionality)
  • Refactoring (no functional changes)
  • Performance improvement

How Has This Been Tested?

  • Unit tests — full suite green (12,405 passed); table suites extended for reorder, flex resize, menu, and manual sorting
  • Integration tests
  • E2E tests
  • Manual testing — page-by-page UAT across repository, audit logs, users, reviews, fields, issues, milestones, reports, and the remaining admin surfaces; adversarial multi-agent parity review of the engine merge found zero behavioral drift

Test Configuration:

  • OS: macOS (darwin 25.5)
  • Browser (if applicable): Chromium (Playwright)
  • Node version: 24

Checklist

  • My code follows the project's style guidelines
  • I have performed a self-review of my code
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation
  • My changes generate no new warnings
  • I have added tests that prove my fix is effective or that my feature works
  • New and existing unit tests pass locally with my changes

…single component

One DataTable with a discriminated union: the default paged <table> engine,
or the windowed infinite-scroll engine via the virtualized parameter. Both
engines share extracted internals (dataTableShared.tsx); per-mode DOM,
test ids, and localStorage formats are unchanged. Dead paged props
(relatedFieldKey, canEdit, subRowColumns, cellPinningStyleFn, and the
Set-based expansion trio) are removed; paged mode gains emptyMessage and
getRowId.
…solves

Branch the repository total on the id-resolved-sort MODE instead of the
resolved ids' presence. The ids flicker to undefined during a page flip,
and the old presence branch fell back to the list query's page-sized
totalCount in that gap - collapsing totalPages to 1 and letting the page
clamp yank the user back to page 1. The count-only query now also starts
as soon as the mode is active rather than waiting for the ids.
… virtualized tables

Ports the paged engine's drag-to-reorder to virtualized mode through a
shared usePersistedColumnOrder hook: grip handles on non-pinned headers,
order remembered per surface under the same key as widths, on by default
wherever the table already persists state. The flex column also becomes
resizable - the drag seeds from the rendered width so it tracks what is
on screen, and the chosen width becomes its minimum while it keeps
absorbing slack.
Ports the paged engine's per-column header menu (explicit sort direction
plus Hide column) to virtualized mode. The menu items are extracted into
a shared ColumnMenuItems component used by both engines; the menu appears
on a surface once it wires onSortColumn/onHideColumn, and Hide routes
through the Columns control via hideColumnRef so persistence and the
checkboxes stay in sync. Wires the admin users page as the first surface.
…d of re-sorting

The engine passed controlled sorting state into TanStack with
getSortedRowModel and without manualSorting, so loaded rows were re-sorted
client-side by each column's accessor - fighting the server's order. Enum
columns (user access, integration/issue status) sort by Postgres enum
order server-side but alphabetically client-side, and infinite-scroll page
seams could be scrambled. Callers own row order (server orderBy or their
own sort), so the engine now renders it verbatim; audited all 33
virtualized call sites - none relied on the client re-sort.
The column shows Yes/No but sorted by the hidden scimGivenName text with
nulls always last, so ascending and descending looked identical - the
SCIM block stayed on top either way. Direction now means what it says:
ascending puts non-SCIM (No) first, descending puts SCIM-managed (Yes)
first, with the given name ordering rows within the SCIM block.
The item clears the active sort and returns to the surface's default
order, which only the repository reads as manual order - everywhere else
the old label was a puzzle. Includes the i18n sync's reconciliation of
the run-completion notification key rename in the non-English locales.
schema.zmodel already declares NONE, USER, PROJECTADMIN, ADMIN, but the
live type has NONE appended at the end (enum values can only be added,
not reordered, in place) - so access sorts put No Access after System
Admin. The migration recreates the type in the declared order and
re-points the four dependent columns, dropping and restoring their
defaults around the swap. Data values are unchanged.
The inbox fetched the whole result set and rendered every row through the
paged engine with no pagination controls, so a large inbox put every row
in the DOM. The table now renders in virtualized mode (windowed rows,
viewport-filling scroll region) and gains column drag-reorder and width
persistence. The per-tab pin swap, server-owned sorting, details-panel row
highlight, and row test ids are unchanged; the paged-era row-height and
table-width CSS hacks are gone.
The list pane rendered the No pending reviews empty copy while the query
was still in flight; it now shows the shared Loading component until the
result lands. Adds the same muted showing/total line the other
virtualized list pages carry.
…cell

The inbox wrapped UserNameCell in a one-off bg-secondary pill, which no
other surface does - and the cell's current-user star and deleted-user
label, colored for the plain row surface, fell below contrast on the
pill in dark themes. The pill is gone; both columns now render the bare
cell exactly like the fourteen other user-name columns.
…lumn

Sort ascending / Sort descending / Remove sort is now the standard header
control wherever a column is sortable, in both engines. Every surface
that owns sort state gains an explicit-direction handler whose Remove
branch restores that page's own default order; pages that already render
a Columns control also gain Hide column through the visibility owner. The
paged engine's menu default now matches the virtualized engine (on when a
handler is wired). The two remaining opt-outs are deliberate: the
workflows drag-order list, where sorting would fight manual ordering, and
the sort-less statuses table.
…ecords per fetch

The drawer rendered every loaded record through the paged engine and
drove pagination with its own IntersectionObserver, so a large drill-down
piled thousands of rows into the DOM 50 at a time. The table now renders
in virtualized mode - the engine owns the fetch-on-scroll trigger and the
loading-more skeleton - and each fetch brings 500 records. The header
shows the standard Showing loaded-of-total line instead of a bare count.
# Conflicts:
#	testplanit/app/[locale]/projects/repository/[projectId]/Cases.tsx
@therealbrad
therealbrad merged commit 035cd61 into beta Aug 11, 2026
4 checks passed
@therealbrad
therealbrad deleted the data-table-merge branch August 11, 2026 00:55
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