Files
unleash/patches/@tanstack__table-core@8.21.3.patch
T
Thomas Heartman 3190cd8c4e chore: migrate react table v7 to v8 (#11967)
Migrate from react-table v7 to TanStack Table v8

## Summary

Replaces `react-table` v7 with `@tanstack/react-table` v8 across the
frontend. The migration was staged in ~25 small phases (phase 1
foundation → phase 5 v7 removal) so each table could be cut over and
verified independently before the v7 dependency was dropped.

Net behavior should be the same. Internal API and column-definition
shape changes; nothing intended user-facing.

## Why v8

- v7 is unmaintained; v8 is the canonical successor (TanStack Table).
- v8 is headless and dependency-free, ships proper TypeScript types, and
works with React 18/19's stricter rendering rules.
- Lets us delete a sizable amount of legacy plugin glue
(`useFlexLayout`, `useSortBy`, custom `react-table-config.d.ts`,
`sortTypes` helpers).

## What changed conceptually

A handful of v7 idioms had no direct v8 equivalent and required
deliberate replacements. These are the migration's main pitfalls — most
of the bugs we hit during testing trace back to one of them.

### 1. Column definitions: `Header`/`accessor`/`Cell` →
`header`/`accessorKey`/`cell`

Capitalization changes, plus `accessor` splits into `accessorKey`
(string) and `accessorFn` (function). Mechanical rename.

### 2. Layout fields move to `meta`

v7 accepted `width`/`minWidth`/`maxWidth`/`align`/`styles` directly on
the column. v8 has no built-in layout system, so these now live on
`column.meta`, declared via the module-augmentation in
`frontend/src/types/react-table-v8.d.ts`.

```ts
// v7
{ Header: 'Name', accessor: 'name', minWidth: 200, align: 'left' }

// v8
{ id: 'name', header: 'Name', accessorKey: 'name', meta: { minWidth: 200, align: 'left' } }
```

### 3. `useFlexLayout` is gone

v7's `useFlexLayout` plugin auto-injected `display: flex` onto
header/body rows and gave every column a default `width: 150` if none
was set. v8 has nothing equivalent. We replicate it manually in our
`VirtualizedTableV8` and `SortableTableHeaderV8`:

- Header and body rows get `style={{ display: 'flex' }}` when the table
is flex-layout (i.e. virtualized).
- Body cells compute width as `meta.width ?? meta.maxWidth ??
meta.minWidth`.
- Header cells use the same fallback (matching the body).

Two practical consequences for column authors:

- **Every column in a virtualized table needs an explicit width** in
`meta` (`width`, `minWidth`, or `maxWidth`). Omitting them used to be
free in v7; in v8 the cell shrinks to its content, which differs per row
and visibly misaligns the table.
- **The `width: 1` / `width: '1%'` trick is dead.** In v7 non-flex
tables, those values were silently ignored; in flex tables they meant
"as narrow as possible". In v8 they're applied verbatim to the `<th>`,
which forces the header into a 1-pixel box and cascades a layout
collapse through the rest of the columns. Drop them.

### 4. Sorting flips from opt-in to opt-out

v7 required the `useSortBy` plugin for any sorting; columns not in a
sorting table just rendered as plain headers. v8 ships sorting in core:
every column is sortable by default and renders as a sort button unless
told otherwise.

If a v7 table didn't use `useSortBy`, the v8 equivalent needs
`enableSorting: false` (per-column or via the table option), otherwise
headers grow phantom sort affordances that either do nothing (no
`getSortedRowModel`) or sort when they previously didn't.

### 5. Missing `header` no longer means empty

v7: a column without `Header` rendered as an empty `<th>`.

v8: TanStack's default column injects a header renderer that returns
`accessorKey` (or `column.id` if there's only an `accessorFn`). So a v7
column like `{ accessor: 'id', Cell: <Icon/> }` now shows the literal
text `"id"` above the icon.

Any column without a meaningful header needs `header: ''` (or `header:
() => null`).

### 6. Default-column `Cell` → explicit cell wrapper

v7's `defaultColumn: { Cell: TextCell }` worked because v7 passed
`value` to the cell renderer. v8 passes a `CellContext`, so the default
cell now needs to extract the value:

```ts
defaultColumn: {
    cell: ({ getValue }) => <TextCell value={String(getValue() ?? '')} />,
}
```

### 7. Disable/reset flags renamed

- `disableSortBy` → `enableSorting: false`
- `disableMultiSort` → `enableMultiSort: false`
- `disableSortRemove` → `enableSortingRemoval: false`
- `disableGlobalFilter` → `enableGlobalFilter: false`
- `autoResetSortBy`/`autoResetHiddenColumns`/`autoResetGlobalFilter` →
`autoResetAll: false`
- `sortType` → `sortingFn`; the `sortTypes` helper module is gone.

### 8. Custom hooks rewritten

`useConditionallyHiddenColumns` → `useConditionallyHiddenColumnsV8`,
which takes `table.setColumnVisibility` and the column array instead of
a v7 `headerGroups` object.

## Bugs caught and fixed during review

Each fix was squashed into the migration phase that introduced the
regression.

| Phase | File | Bug | Fix |
|---|---|---|---|
| `ktykzuvt` (phase 2 / foundation) | `SortableTableHeaderV8.tsx` |
Virtualized table header row had no `display: flex`, so `<th>` cells
stacked vertically (the original API tokens / project list report). |
Set `style={{ display: 'flex' }}` on the header `<TableRow>` when `flex`
is true. |
| `ktykzuvt` (phase 2 / foundation) | `SortableTableHeaderV8.tsx` |
Header width didn't fall back to `meta.maxWidth` / `meta.minWidth` like
the body did; `maxWidth`-only columns ended up with header narrower than
body and shifted every following column. | Match the body's fallback:
`width = meta.width ?? meta.maxWidth ?? meta.minWidth`. |
| `nounsmrs` (phase 3d) | `LoginHistoryTable.tsx` | `ip` and
`successful` columns had no width metadata at all — relied on the v7
`useFlexLayout` default of 150. | Add `meta: { width: 150 }` and `meta:
{ width: 100 }` respectively. |
| `nounsmrs` (phase 3d) | `FeatureTypesList.tsx` | Icon column rendered
the literal text `"id"` as its header because v8 defaults a missing
`header` to `accessorKey`. | Add `header: ''`. |
| `kozplyyx` (phase 3g) | `ProjectsListTable.tsx` | Owners column had no
width meta; rows misaligned per content. Headers also showed as sortable
when the v7 table never enabled sorting. | Add `meta: { width: 150 }` to
owners; add `enableSorting: false` to the table options. |
| `onztzlsv` (phase 3f) |
`ChangeRequestConfiguration/ChangeRequestTable.tsx` | "Required
approvals" header wrapped to two lines because `meta.width: 100` was
added during migration; v7 never had it (the v7 table didn't use
`useFlexLayout`, so the value would have been ignored anyway). | Drop
`width: 100` from both `requiredApprovals` and `changeRequestEnabled` so
the table goes back to auto-layout. |
| `klyokypo` (phase 3n) | `ProjectEnvironment.tsx` | `enabled` column
had `meta.width: 1` carried over from v7 (where it was a no-op without
`useFlexLayout`). In v8 it sets `<th style="width: 1px">`, which
collapsed the "Visible in project" header and cascaded a width squeeze
through the other columns. | Drop `width: 1`. |

## Still to verify post-merge

I'm submitting with these tables not yet hand-verified:

- `ProjectActionsTable`
- ~~`ProjectGroupView`~~ Checked
- Signal tables (`SignalEndpointsTable`, `SignalEndpointsTokens`)
- ~~Billing history~~ Checked and fixed
- ~~Application instances (needs a real connection; sandbox-only)~~
Checked; looks the same
- ~~Feature metrics table (needs SDK traffic)~~ Checked, looks
**better**
- DORA metrics

All other migrated tables have been checked visually against the v7
build. Anyone reviewing one of the above — please cross-check the new
render against `main` and apply the same column-meta or `enableSorting`
adjustments if anything looks off; the patterns in the bug table above
cover the categories you're likely to hit.

## Test plan

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-05-21 13:07:06 +02:00

27 lines
1.1 KiB
Diff

diff --git a/build/lib/index.mjs b/build/lib/index.mjs
index b00ad055d1f95ef714072fba3ea0a528ea545ded..90dcfb62db759c22ba38add601c57d7b63c098ef 100644
--- a/build/lib/index.mjs
+++ b/build/lib/index.mjs
@@ -2561,7 +2561,7 @@ const RowSorting = {
},
createColumn: (column, table) => {
column.getAutoSortingFn = () => {
- const firstRows = table.getFilteredRowModel().flatRows.slice(10);
+ const firstRows = table.getFilteredRowModel().flatRows.slice(0, 10);
let isString = false;
for (const row of firstRows) {
const value = row == null ? void 0 : row.getValue(column.id);
diff --git a/src/features/RowSorting.ts b/src/features/RowSorting.ts
index c2e7c32d53ef08b1dd680a23dd8435c7c04fbb5c..53557710a9be5335c6a74ed2e971a0574f7ea4d2 100644
--- a/src/features/RowSorting.ts
+++ b/src/features/RowSorting.ts
@@ -306,7 +306,7 @@ export const RowSorting: TableFeature = {
table: Table<TData>
): void => {
column.getAutoSortingFn = () => {
- const firstRows = table.getFilteredRowModel().flatRows.slice(10)
+ const firstRows = table.getFilteredRowModel().flatRows.slice(0, 10)
let isString = false