Skip to content

fix(hig): finish the unreleased surfaces so Escape, search and column reorder behave natively - #2591

Merged
datlechin merged 1 commit into
mainfrom
fix/polish-unreleased-ui
Sep 1, 2026
Merged

fix(hig): finish the unreleased surfaces so Escape, search and column reorder behave natively#2591
datlechin merged 1 commit into
mainfrom
fix/polish-unreleased-ui

Conversation

@datlechin

Copy link
Copy Markdown
Member

A UI/UX audit of everything added since 0.69.0, and the fixes it produced. The editor tab strip, the connections-strip cell and the statement-model split behind the Mongo shell came out clean. The findings concentrated in Copy To, column reorder, and plugin install progress.

Copy To and Duplicate Database (#2487)

Escape was inert on the Review and Result steps. Only .configuring and .copying carried a .cancelAction, so a user who read the script and changed their mind had to press Back then Cancel, and Escape did nothing on either step. The HIG asks every sheet to be dismissible with Escape, and the app's own SQLReviewSheet already is. Review now carries a real Cancel, and .onExitCommand covers the result step, whose only button owns Return. A copy in flight is deliberately excluded: the sheet is the only thing reporting what the run is doing.

Both exits go through close(), which cancels the session first. review() holds a task that reads both databases and promotes its weak self to a strong one before the first await, so dismissing without cancelling left the session and its metadata reads alive with nothing to show them to. backToConfiguring() already cancelled; the exits have to agree.

The object search was a text field wearing a magnifying glass, and Escape in it destroyed the configured copy. The app already ships NativeSearchField, an NSSearchField wrapper used in Settings, the welcome window and the type chooser. The hand-rolled version had no clear button, no recessed search shape, and no Escape-clears-first, so Escape after typing a filter went straight past it to the sheet's Cancel and discarded the target, the content choice and every tick. NativeSearchField takes the key itself while it holds text.

Also: Select All / Deselect All replace All / None, dimmed when they would do nothing; a count under the list reports the whole selection rather than the part the search is showing; the footer's error is selectable and reachable in full through a help tag; and the settings column scrolls, because duplicating a database adds every option that engine's CREATE DATABASE takes to a sheet whose height is fixed.

One command, one name. The Database menu said Copy Objects To… while the sidebar and the sheet said Copy To….

The endpoint picker had no search. Connections, then databases, then schemas, as a plain list 320 points wide. A server answering with hundreds of databases is not something scrolling finds. Each level now has its own search field, which also reaches Compare & Sync, where the picker is shared.

Column reorder (#2479)

Reordering was drag-only: no menu command, no keyboard, nothing VoiceOver could perform. The editor tab strip closed exactly this gap two commits earlier, in its own words: "a gesture with no menu equivalent cannot be found by a user who does not already expect it, and cannot be performed at all by VoiceOver." A column row's contextual menu now carries Move Column Up and Move Column Down, resolved through the same ColumnReorderPolicy the drag is offered on, so the two can never disagree.

They are built by StructureGridDelegate, not by the row view, because a column row raises two different menus. KeyHandlingTableView.rightMouseDown intercepts a click inside the selection and answers from DataGridRowView.contextMenu(for:); a click outside it falls through to StructureRowViewWithMenu.menu(for:). Items added only to the override would have been missing from select-then-right-click, which is the ordinary path and the one keyboard and assistive access take. One builder serves both, and the UI test now exercises both.

The reason a reorder was withheld lived only in a help tag on the row-number gutter. The row still lifted, the no-drop cursor appeared, the drop did nothing, and the explanation needed a two-second hover over a narrow gutter. It is now spelled out as a dimmed item under the two dimmed commands.

The index arithmetic moved out of the AppKit subclass into ColumnMove, a pure function, because desiredOrder speaks NSTableView's drop index and moving down by one lands two rows on.

Plugin install progress

All five installMissingPlugin call sites discarded the fraction it publishes, so Install closed the alert and the app looked hung for the length of a network download. PluginInstallStatusRow in the connection form was built to draw a bar and reaches the fraction only through PluginInstallTracker, which nothing on this path ever fed, so it always fell back to its indeterminate spinner.

installMissingPlugin now publishes to the tracker, which fixes the connection form by itself. The two paths with no progress surface of their own (a Finder open, and the install alert) get PluginInstallProgressPresenter: a sheet on the window the user was working in, or a panel when there is none, which is the Finder-open case. A panel rather than an NSAlert because AlertHelper runs an alert application-modal when no window qualifies, and a modal run loop cannot be updated from the await driving it.

Measured, not assumed: the installer publishes 0 before the transfer and nothing again until the whole file is down, so a determinate bar drawn from it would sit at 0% for exactly the wait it exists to explain. The obvious fix, download(from:delegate:) with a URLSessionDownloadDelegate, was written and then measured against a 25.7 MB download with a known Content-Length: zero didWriteData callbacks, because the async form routes only URLSessionTaskDelegate messages to a per-task delegate. That code was removed rather than shipped inert. Both the panel and PluginInstallStatusRow stay indeterminate until a fraction actually moves, which also retires a pre-existing 0% bar in the connection form.

Editor tab strip (#2438)

EditorTabStrip.tooltip was dead: the AppKit interaction view supplies the real tooltip from EditorTabStripInteraction.tooltip. Removed, and the doc comment above canKeepOpen moved to positionDescription, which is what it describes.

Deliberately not shipped: the copy failure-policy picker

ObjectCopySession.errorHandling is pinned to .stopAndRollback while ObjectCopyPlanner and ObjectCopyRunner read all three ImportErrorHandling cases, and Compare & Sync exposes the identical picker over the same machinery. Exposing it here was in this branch and has been taken back out, because two review passes established that the runner cannot honour any honest description of the three choices:

  • runStructure runs every cleanup group before any creation group under one transaction. Committing that phase on a stop, which is what .stopAndCommit would ask for, can leave DROP B committed when CREATE A failed and B's creation was never attempted. The DDL loop also represents Stop as both cancelled and stopped, so Stop itself would commit the destructive half.
  • For data-only Replace, clearsInsideDataTransaction is false under .skipAndContinue, so target tables are emptied in an earlier phase. A per-table transaction around the row stream cannot restore them, and a failed clear leaves the table in dataSteps so the runner appends into an uncleared table.
  • ObjectCopyRowCopier keeps its inserted count in a local, so a source-stream failure after committed batches reports zero rows copied. A retry duplicates them.
  • Each table is its own transaction, so objects already finished stay under every choice; no caption saying "the target is left as it was" can be true.

Making the picker safe means an object-atomic transaction model in ObjectCopyRunner plus a tested policy matrix over structure replacement, data-only replacement, structure-and-data replacement, engines without transactions, failure and cancellation. That is a runner change, not a UI polish change, so the capability stays dark and this is reported rather than shipped half-right.

Verification

  • verify.sh build PASS
  • verify.sh test PASS, 63 cases, over ObjectCopySessionTests, ObjectCopyPlanTests, ObjectCopyRowCopierTests, ColumnReorderPolicyTests, ColumnMoveTests, PluginInstallTrackerStagedTests
  • verify.sh uitest StructureColumnMoveUITests CopyObjectsUITests PASS, 8 cases
  • verify.sh lint TablePro TableProTests TableProUITests 0 violations. The one agent docs finding (AXCell, CLAUDE.md:220) is pre-existing and untouched by this branch.
  • docs/scripts/check-writing-style.sh and check-docs-against-source.py both pass
  • verify.sh plugins fails locally on oracle-nio's @TaskLocal macro (unknown attribute 'usableFromInlinenonisolated'), a known local-toolchain issue. This branch touches no plugin.
  • Reviewed by Codex, both review and adversarial-review. Every finding was verified against source before acting; three were fixed, and the fourth is why the picker was withdrawn.

New tests: ColumnMoveTests pins the drop-index arithmetic against desiredOrder rather than against the number it happens to produce; ObjectCopySessionTests gains the two list buttons and the count's independence from the search; StructureColumnMoveUITests proves both commands reach a column row's menu on both AppKit paths; CopyObjectsUITests proves Escape in the search field clears it and the second Escape closes the sheet.

CopyObjectsUITests also gained a scoped context-menu query. Database > Copy To… now carries the same title as the sidebar item, on purpose, and a closed menu bar submenu is still in the accessibility tree, so an app-rooted menuItems[title] matched two elements and refused to click either.

Not covered by automation

  • Escape at the Review step. It needs a chosen target and a completed read of both databases, which the sample SQLite database cannot supply without a second connection.
  • docs/features/copy-objects.mdx still has no screenshot, the only feature page without one. Capturing it needs a driven Debug build and a light/dark pair; the attempt here kept colliding with the running app, and committing a placeholder would put a fake screenshot in the docs repo. Left as it was rather than made worse.

@mintlify

mintlify Bot commented Sep 1, 2026

Copy link
Copy Markdown

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

Project Status Preview Updated (UTC)
TablePro 🟢 Ready View Preview Sep 1, 2026, 5:03 AM

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

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@datlechin
datlechin merged commit f2be244 into main Sep 1, 2026
9 checks passed
@datlechin
datlechin deleted the fix/polish-unreleased-ui branch September 1, 2026 05:05
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