Skip to content

fix: header sort is a no-op on first click for default-sorted columns#681

Open
Silex wants to merge 2 commits into
netbirdio:mainfrom
Silex:fix/table-header-sort-noop
Open

fix: header sort is a no-op on first click for default-sorted columns#681
Silex wants to merge 2 commits into
netbirdio:mainfrom
Silex:fix/table-header-sort-noop

Conversation

@Silex

@Silex Silex commented Jun 25, 2026

Copy link
Copy Markdown

Summary

Clicking a column header that is part of a table's default multi-sort does
nothing on the first click. To reproduce: open a Group → Peers, then click
the Name header — the list does not reorder. Clicking Address first,
and then Name, makes Name sorting work.

Root cause

The peers tables (MinimalPeersTable) default to a multi-sort of
[connected desc, last_seen desc, name asc]. A header click calls
column.toggleSorting(), whose non-multi branch in @tanstack/table-core
8.21.3 only replaces the sort when the clicked column is not the last
entry of the current sort:

if (old?.length && existingIndex !== old.length - 1) sortAction = 'replace'
else if (existingSorting)                            sortAction = 'toggle'
else                                                 sortAction = 'replace'

name is the last entry, so it toggles in place — only flipping its desc
behind the dominant connected/last_seen sorts — and the order appears
unchanged. Any column that is the lowest-priority entry of a default sort is
affected.

Fix

  • Expose the tanstack table instance through a small React context
    (DataTableContext) mounted by DataTable, mirroring the existing
    ServerPaginationProvider pattern. This avoids threading table through
    the ~40 DataTableHeader call sites.
  • DataTableHeader now forces a single-column sort via
    table.setSorting([{ id, desc }]), replacing the sort regardless of the
    column's position. It falls back to column.toggleSorting() when no provider
    is present.

This makes every column behave consistently (single-column sort on click,
which is already the de-facto behaviour for non-last columns).

Note: This change was generated with AI assistance. The reasoning and the
root cause have been verified against the @tanstack/table-core 8.21.3
source, but the test suite / lint / build were not run locally (no
node_modules in the working environment). The fix looks correct; please rely
on CI and review before merging.

Issue ticket number and link

N/A — reported internally, no public issue.

Documentation

Select exactly one:

  • I added/updated documentation for this change
  • Documentation is not needed for this change (explain why)

Internal table sort-behaviour bug fix with no user-facing API or UI surface
change to document.

E2E tests

management-cloud-tag: main
reverse-proxy-tag: main

Summary by CodeRabbit

  • New Features

    • Table headers now support more consistent single-column sorting when using the built-in table behavior.
    • Table components can now share sorting state more reliably across nested table elements.
  • Bug Fixes

    • Improved sort handling so table columns behave correctly whether or not custom sort logic is configured.
    • Sorting updates continue to work smoothly with server-side pagination.

Silex added 2 commits June 25, 2026 09:29
Header cells only receive their column, not the table instance, so they
cannot force a full-sort replace. Add a lightweight context that exposes
the table to anything rendered inside DataTable (mirrors the existing
ServerPaginationProvider pattern), avoiding threading the table through
~40 DataTableHeader call sites.
Clicking a header called column.toggleSorting(), which in tanstack's
normal (non-multi) mode only toggles a column in place when it is the
lowest-priority entry of an existing multi-sort (existingIndex ===
old.length - 1) instead of replacing the sort. The peers tables default
to [connected, last_seen, name], so the first click on Name only flipped
its desc behind the dominant connected/last_seen sorts and appeared to do
nothing until another column was clicked first.

Use the table instance from context to setSorting() to a single column,
forcing a replace regardless of the column's position. Falls back to the
previous toggleSorting() when no provider is present.
@CLAassistant

CLAassistant commented Jun 25, 2026

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

@coderabbitai

coderabbitai Bot commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

DataTable now provides its table instance through context, and DataTableHeader uses that context when applying sort changes. When no table instance is available, the existing column-toggle fallback remains.

Changes

Context and sorting flow

Layer / File(s) Summary
Context provider wiring
src/components/table/DataTableContext.tsx, src/components/table/DataTable.tsx
Defines a client context for a TanStack table instance and wraps the DataTable render tree in the provider.
Header sorting
src/components/table/DataTableHeader.tsx
Reads the optional table instance in DataTableHeader and uses it to set sorting before falling back to column.toggleSorting.

Sequence Diagram

sequenceDiagram
  participant DataTable
  participant DataTableInstanceProvider
  participant DataTableHeader
  participant useOptionalDataTable
  participant TanStackTable
  participant column

  DataTable->>DataTableInstanceProvider: render table={table}
  DataTableHeader->>DataTableHeader: handleSort()
  DataTableHeader->>useOptionalDataTable: read table context
  useOptionalDataTable-->>DataTableHeader: table or null
  alt table present
    DataTableHeader->>TanStackTable: setSorting([{ id, desc }])
  else no table
    DataTableHeader->>column: toggleSorting(...)
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

(/) I hopped through the sorted rows,
and shared the table where context grows.
One click, one set, the columns sway,
the fallback hop still finds its way.
🐰✨ All tidy now; let carrots play.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly describes the sorting bug fix introduced by the PR.
Description check ✅ Passed The description matches the template well and includes the required issue, documentation choice, and e2e tags sections.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

src/components/table/DataTable.tsx

Oops! Something went wrong! :(

ESLint: 9.39.3

TypeError: Converting circular structure to JSON
--> starting at object with constructor 'Object'
| property 'configs' -> object with constructor 'Object'
| property 'flat' -> object with constructor 'Object'
| ...
| property 'plugins' -> object with constructor 'Object'
--- property 'react' closes the circle
Referenced from: /.eslintrc.json
at JSON.stringify ()
at /node_modules/@eslint/eslintrc/dist/eslintrc.cjs:2255:45
at Array.map ()
at ConfigValidator.formatErrors (/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:2246:23)
at ConfigValidator.validateConfigSchema (/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:2277:84)
at ConfigArrayFactory._normalizeConfigData (/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:3123:19)
at ConfigArrayFactory._loadConfigData (/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:3088:21)
at ConfigArrayFactory._loadExtendedShareableConfig (/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:3393:21)
at ConfigArrayFactory._loadExtends (/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:3261:25)
at ConfigArrayFactory._normalizeObjectConfigDataBody (/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:3199:25)
(node:2) ESLintRCWarning: You are using an eslintrc configuration file, which is deprecated and support will be removed in v10.0.0. Please migrate to an eslint.config.js file. See https://eslint.org/docs/latest/use/configure/migration-guide for details. An eslintrc configuration file is used because you have the ESLINT_USE_FLAT_CONFIG environment variable set to false. If you want to use an eslint.config.js file, remove the environment variable. If you want to find the location of the eslintrc configuration file, use the --debug flag.
(Use node --trace-warnings ... to show where the warning was created)

src/components/table/DataTableContext.tsx

Oops! Something went wrong! :(

ESLint: 9.39.3

TypeError: Converting circular structure to JSON
--> starting at object with constructor 'Object'
| property 'configs' -> object with constructor 'Object'
| property 'flat' -> object with constructor 'Object'
| ...
| property 'plugins' -> object with constructor 'Object'
--- property 'react' closes the circle
Referenced from: /.eslintrc.json
at JSON.stringify ()
at /node_modules/@eslint/eslintrc/dist/eslintrc.cjs:2255:45
at Array.map ()
at ConfigValidator.formatErrors (/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:2246:23)
at ConfigValidator.validateConfigSchema (/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:2277:84)
at ConfigArrayFactory._normalizeConfigData (/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:3123:19)
at ConfigArrayFactory._loadConfigData (/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:3088:21)
at ConfigArrayFactory._loadExtendedShareableConfig (/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:3393:21)
at ConfigArrayFactory._loadExtends (/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:3261:25)
at ConfigArrayFactory._normalizeObjectConfigDataBody (/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:3199:25)
(node:2) ESLintRCWarning: You are using an eslintrc configuration file, which is deprecated and support will be removed in v10.0.0. Please migrate to an eslint.config.js file. See https://eslint.org/docs/latest/use/configure/migration-guide for details. An eslintrc configuration file is used because you have the ESLINT_USE_FLAT_CONFIG environment variable set to false. If you want to use an eslint.config.js file, remove the environment variable. If you want to find the location of the eslintrc configuration file, use the --debug flag.
(Use node --trace-warnings ... to show where the warning was created)

src/components/table/DataTableHeader.tsx

Oops! Something went wrong! :(

ESLint: 9.39.3

TypeError: Converting circular structure to JSON
--> starting at object with constructor 'Object'
| property 'configs' -> object with constructor 'Object'
| property 'flat' -> object with constructor 'Object'
| ...
| property 'plugins' -> object with constructor 'Object'
--- property 'react' closes the circle
Referenced from: /.eslintrc.json
at JSON.stringify ()
at /node_modules/@eslint/eslintrc/dist/eslintrc.cjs:2255:45
at Array.map ()
at ConfigValidator.formatErrors (/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:2246:23)
at ConfigValidator.validateConfigSchema (/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:2277:84)
at ConfigArrayFactory._normalizeConfigData (/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:3123:19)
at ConfigArrayFactory._loadConfigData (/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:3088:21)
at ConfigArrayFactory._loadExtendedShareableConfig (/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:3393:21)
at ConfigArrayFactory._loadExtends (/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:3261:25)
at ConfigArrayFactory._normalizeObjectConfigDataBody (/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:3199:25)
(node:2) ESLintRCWarning: You are using an eslintrc configuration file, which is deprecated and support will be removed in v10.0.0. Please migrate to an eslint.config.js file. See https://eslint.org/docs/latest/use/configure/migration-guide for details. An eslintrc configuration file is used because you have the ESLINT_USE_FLAT_CONFIG environment variable set to false. If you want to use an eslint.config.js file, remove the environment variable. If you want to find the location of the eslintrc configuration file, use the --debug flag.
(Use node --trace-warnings ... to show where the warning was created)


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
src/components/table/DataTableContext.tsx (1)

3-3: 📐 Maintainability & Code Quality | 🔵 Trivial

Import Table from @tanstack/react-table to ensure stability with strict package managers.

While @tanstack/react-table re-exports the Table type, importing directly from @tanstack/table-core relies on a transitive dependency. To prevent potential build failures with strict package managers (like pnpm) and ensure consistency with the rest of the codebase, explicitly use the @tanstack/react-table package for all TanStack Table interactions in this React project.

♻️ Suggested change
-import type { Table as TanStackTable } from "`@tanstack/table-core`";
+import type { Table as TanStackTable } from "`@tanstack/react-table`";
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/table/DataTableContext.tsx` at line 3, The DataTableContext
import is pulling the Table type from the transitive `@tanstack/table-core`
package instead of the React-facing package. Update the TanStack type import in
DataTableContext to use `@tanstack/react-table` so it stays consistent with the
rest of the React table code and avoids strict package manager issues; keep the
Table alias usage in the context/types unchanged so any references to
TanStackTable continue to work.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@src/components/table/DataTableContext.tsx`:
- Line 3: The DataTableContext import is pulling the Table type from the
transitive `@tanstack/table-core` package instead of the React-facing package.
Update the TanStack type import in DataTableContext to use `@tanstack/react-table`
so it stays consistent with the rest of the React table code and avoids strict
package manager issues; keep the Table alias usage in the context/types
unchanged so any references to TanStackTable continue to work.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: d09ba431-f8b7-45ca-a312-a31c57428abf

📥 Commits

Reviewing files that changed from the base of the PR and between 76529fc and ac07869.

📒 Files selected for processing (3)
  • src/components/table/DataTable.tsx
  • src/components/table/DataTableContext.tsx
  • src/components/table/DataTableHeader.tsx

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.

2 participants