Skip to content

fix(datagrid): keep the reader's place on refresh and start new views at the first row - #2825

Open
datlechin wants to merge 1 commit into
mainfrom
fix/grid-viewport-after-reload
Open

fix(datagrid): keep the reader's place on refresh and start new views at the first row#2825
datlechin wants to merge 1 commit into
mainfrom
fix/grid-viewport-after-reload

Conversation

@datlechin

Copy link
Copy Markdown
Member

Problem

A user reported that after a refresh the new data is there but the grid does not scroll to it, so an insert looks like it failed.

Root cause

Nothing owned where a table tab's data grid lands when a result replaces its rows.

  • NSTableView.reloadData() keeps the clip view's origin and drops the selection, so every reload nobody made a decision for showed the old result's pixel offset over new rows. Refresh anchored nothing, so rows inserted above the reader shifted everything under them.
  • Only two reload causes decided anything: pagination through pendingScrollToTopAfterReplace and Back/Forward through pendingRowAnchors. Both were per-tab flags consumed only when the result landed on the selected, mounted grid, so a failed, cancelled, empty or background load left them set and they fired on a later, unrelated reload.
  • Sort and filter changes rebuilt page 1 of a new order but left the grid scrolled wherever it was.
  • Every table-tab result reaches the grid twice, so a selection made after the first reload (Back's restored row) was cleared by the second.
  • applyFullReplace() left the inline cell editor open over a display position that now held another record.

Fix

Each table-tab reload cause declares a GridReloadIntent, carried by the execution that produces the rows, so it dies with a failed or superseded run:

Cause Intent
Page, page size, sort, filter, key search, database change first row
Refresh, Execute, Save, rewind, inspector save, column hide keep place
Back / Forward select and reveal the recorded row
  • GridViewportResolver (pure) snapshots the grid's first visible row by primary key before the rows are replaced and resolves it against the new rows. A key is used only when it comes from a saved, unedited row and matches exactly one new row; otherwise the position holds. A grid scrolled to the very top stays at the top, so rows added above come into view.
  • The placement is staged on TabSession with the buffer epoch it was resolved for, only when a table tab's grid is on screen, and the grid applies it at the end of its update pass, after the last reload for that content. pendingScrollToTopAfterReplace and pendingRowAnchors are gone; the Back/Forward anchor lives on QueryTab.restoredRowAnchor and any install of rows spends it.
  • Placement and sampling read the header inset from the clip view's contentInsets, where AppKit puts the header height (28pt, 42pt with column comments; measured).
  • A reload that does not replace the rows keeps the row selection by row identity.
  • applyFullReplace() dismisses the inline editor. The discard gates commit an active edit first, and Refresh and column visibility restore the row buffer before clearing edits, so a discarded key never becomes an anchor.
  • Query tabs are unchanged.

Refresh keeps place rather than jumping to the first row, which is what Sequel Ace and DBeaver do; users of TablePlus and Postico have filed complaints about losing their place, and DataGrip closed "Reload doesn't display newly-added record" as works-as-intended.

Deliberately not fixed here

Selecting and revealing the rows a Save inserted. Three designs were built and reviewed, and each round found a new way to select the wrong row or none:

  • Matching typed values against reloaded rows misses every value the server reformats (5 comes back as 5.00, a date gains 00:00:00).
  • Treating keys that are new to the page as the inserted rows picks up an existing row that a delete or an update in the same save shifted onto the page, and rows still tagged as unsaved after a save that finished in a background tab.

DataWriteStepResult carries no key the server assigned, so the client cannot know which row it created. Doing this properly needs the write path to return the inserted key (RETURNING on PostgreSQL and SQLite, LAST_INSERT_ID() on MySQL, OUTPUT on SQL Server) and a reveal keyed on it. That is its own change across the drivers.

Verification

  • Build: passes.
  • Lint: 0 violations on every changed file.
  • Docs: check-writing-style.sh and check-docs-against-source.py pass.
  • Unit tests: 242 cases across 20 suites, including the new GridViewportResolverTests, GridReloadViewportTests and DataGridViewportPlacementTests, with 241 passing. MainContentCoordinatorRefreshTests/singleRequestRefreshHasNoTrailing failed only in that parallel run; its 400 ms sleep races a 250 ms coalescing task under main-actor load. It passes when run on its own (13/13).
  • Header inset: DataGridViewportPlacementTests checks the real clip-view inset from an NSTableHeaderView, not a value set by hand.
  • SaveCompletionTests: left out of the local runs. Two of its tests reach the real Save Changes confirmation, which runs app-modal when no window is key and hangs the test host (sampled stack: saveChangesOperationConfirming.confirmrunStatementConfirmation). That is on main and unrelated to this change.
  • UI test: DataGridReloadViewportUITests (Refresh keeps the place, a header sort lands on the first row) was added but not run locally. macOS held an "Enable UI Automation" password prompt, and every run ended with "Timed out while enabling automation mode". CI runs it.
  • Screenshots: none. The change is where the grid scrolls, not how anything looks.
  • Reviews: the Codex review found 5 issues, all fixed; the Codex adversarial pass stalled on usage limits. Three rounds of multi-agent adversarial review with per-finding refutation followed, and the last round led to the scope cut described above.

https://claude.ai/code/session_01EPyw1skNf2cexdYTa82zLw

@mintlify

mintlify Bot commented Sep 13, 2026

Copy link
Copy Markdown

Preview deployment for your docs. Learn more about Mintlify Previews.

Project Status Preview Updated
TablePro 🟢 Ready View Preview Sep 13, 2026, 5:23 PM

💡 Tip: Enable Automations to automatically generate PRs for you.

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