Skip to content

refactor(editor): rebuild the query tab around pure presentation models - #2892

Merged
datlechin merged 1 commit into
mainfrom
refactor/query-tab-native-revamp
Sep 15, 2026
Merged

datlechin merged 1 commit into
mainfrom
refactor/query-tab-native-revamp

Conversation

@datlechin

Copy link
Copy Markdown
Member

What this is

A rebuild of the query editor tab, editor through results, so that its behaviour is decided by pure
models instead of by conditionals inside SwiftUI bodies, and so that each control is the one macOS
actually provides for the job.

Root cause

The tab implemented three platform roles by hand inside its own content:

  • a faux toolbar (QueryEditorView's HStack, opening with Text("Query").font(.headline) naming
    the pane it was already inside, then three borderless icon buttons, a Divider, a .bordered
    .small Explain and a .borderedProminent .small Execute: three control sizes, two button
    styles, five controls, no grouping),
  • a faux tab strip (ResultTabBar, a 32pt band of hand-drawn tabs with hover-revealed close
    buttons, spent in every query tab including the common one with a single result),
  • and a bottom bar that had grown a second toolbar's worth of controls.

Underneath, MainEditorContentView.resultsSection was a ~100-line switch over ResultsViewMode
whose five arms each repeated the result chrome, wrapped around a five-way if/else if/else. Three
of those arms tested lastExecutedAt != nil && !isExecuting beside a different companion condition,
so the result states were only accidentally exclusive, and none of them could be checked without
mounting a view.

What changed

A pure resolver decides the results pane. QueryResultPresentation takes QueryResultInputs
and returns which content case to draw plus whether the filter chrome, find bar, result chooser,
error banner and status bar show. resultsSection is now a render of that one value. This follows
the pattern ResultStatusModel, StatusBarSnapshot and ResultsModeAvailability already set in
this same directory.

A pure model decides the commands. QueryCommandAvailability answers which of Run, Stop,
Explain, Format, Favorite, Clear Query and Clear Results are live, and carries the reason a dimmed
one is dimmed so the control can say why rather than only greying out.

The result strip is gone; result sets are chosen from the status bar. There is no native control
on macOS 14 that draws a closable, pinnable tab strip for non-window content, so the collection is
re-expressed rather than redrawn. Measured against the SDK headers for this toolchain:

  • NSTabViewItem has exactly ten properties and not one is a close, pin or reorder affordance;
    grepping closable|closeButton|isPinned|canReorder across NSTabView*.h and NSWindowTabGroup.h
    returns nothing.
  • NSWindowTabGroup.windows is [NSWindow], so the one native closable, reorderable, overflowing
    strip takes windows only.
  • SwiftUI's Tab, TabSection, SidebarAdaptableTabViewStyle and
    TabCustomizationBehavior.reorderable are all macOS 15+, and none declares a close affordance.

The HIG names the replacement: a pop-up button is the "reasonable alternative in cases where there
are too many panes" for a tab view, and result sets are unbounded. ResultSetMenu is that control,
in the bar's leading zone, which is where Postico 2 puts the same thing. Pin, Unpin, Close and Close
Others move across from the strip's context menu unchanged. It is deliberately shown even for a
single result, because Pin lives in its menu and hiding it there would leave the View menu as the
only route to pinning; ResultSetPolicyTests pins that invariant.

Query history moved to the trailing pane. TrailingPaneSurface gains a .history case beside
.inspector and .assistant, so the tab's third stacked band and its nested split view are gone.
HistoryPanelView's own list/detail split rotates from horizontal to vertical, which is what makes
it fit: the trailing pane is one shared NSSplitViewItem with a 270pt floor, and that file records
that per-surface minimums were measured and rejected because raising one force-grows the pane past
the width the reader chose. Its autosave name moves with the orientation, since a stored horizontal
divider offset means nothing in a vertical split.

Clear Query and Clear Results are two commands. They were one trash button whose tooltip and
accessibility label both read "Clear Query" while it also cleared the results, the execution record
and collapsed the results pane. Neither half had a menu-bar command. Both now do, and both ride the
Run button's menu.

Why the commands are in the tab and not the window toolbar

This was built the other way first, with Run, Stop, Explain, Format and Favorite as NSToolbarItems,
and reverted. NSToolbar belongs to the window, and a window here hosts table, structure, dashboard
and diagram tabs that have nothing to run, so the items sat permanently dimmed on all of them.
Varying the item set per tab is not available: NSToolbar.itemIdentifiers is macOS 15 (above the
14.0 target) and its own header says it "will override any customizations the user has made" when
allowsUserCustomization is enabled, which this toolbar has. A control that belongs to the editor
lives with the editor, which is what Xcode's jump bar and Script Editor's navigation bar do. The
window toolbar is unchanged by this PR.

Run and Stop are one control rather than two, because the bar is inside the pane and a permanently
dimmed second button costs width the editor wants. TablePlus does the same: its Cancel appears for a
long query rather than standing there dimmed. QueryCommandAvailabilityTests asserts the two are
never both actionable and never both dead.

Defects fixed along the way

Defect Where Fix
A stale error banner sat over a pinned result after clearing the results of a failed query, clearable only by its own Dismiss MainContentCoordinator+SidebarActions.swift The pinned branch of clearActiveQueryResults returned before clearing execution.errorMessage, which the other branch cleared. It now clears it.
A query plan gave up the status bar, which was harmless while the strip carried the chooser above it and would have left a plan unable to be switched away from or pinned QueryResultPresentation, StatusBarSnapshot The bar stays for a plan and gives up only its row readout, via a new isQueryPlan flag the status model reads.
One trash button performed two destructive acts under one name, with no menu command for either QueryEditorView.swift Split into Clear Query and Clear Results, each with a Query menu command.

Verification

  • verify.sh build: PASS
  • verify.sh test QueryResultPresentationTests ResultSetMenuModelTests ResultSetPolicyTests ResultStatusBarLayoutTests ResultPinningTests MainWindowToolbarValidationTests QueryCommandAvailabilityTests: PASS, 144 executed, 144 passed
  • verify.sh lint TablePro: PASS, 0 violations
  • verify.sh docs: PASS

New suites: QueryResultPresentationTests (17 cases over the state matrix, including one that walks
every reachable combination), QueryCommandAvailabilityTests, ResultSetMenuModelTests.
ResultTabBarPolicyTests became ResultSetPolicyTests and now asks the resolver, so the pane and
the bar cannot disagree about whether there is a result to choose.

UI suites rewritten against the new controls: QueryRunUITests (was QueryExecuteMenuUITests),
ResultSetPinUITests (was ResultTabPinUITests), ResultStatementLinkUITests,
QueryPlanResultUITests. They were not executed locally; they run in CI.

Also in the diff: a pre-existing legacy_swiftui_aspect_ratio violation in
WelcomeOutlineRows.swift, unrelated to this work and already on main. SwiftLint scans all of
TablePro/ regardless of the path passed, so it made this branch's lint gate red. One line.

Not done

No before and after screenshots. osascript does not have Accessibility permission on this
machine, so the app cannot be driven to a query tab with results, and capturing the installed 0.74.0
copy instead would have put real connections and data into this PR. The visual claims here are the
removed 32pt result strip, the removed "Query" heading, the rebuilt command bar and history in the
trailing pane. Screenshots for docs/features/query-results.mdx and
docs/features/query-history.mdx are consequently still the old ones and need re-capturing; the
prose on both pages is updated.

@mintlify

mintlify Bot commented Sep 15, 2026

Copy link
Copy Markdown

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

Project Status Preview Updated
TablePro 🟢 Ready View Preview Sep 15, 2026, 9:08 AM

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

@datlechin
datlechin force-pushed the refactor/query-tab-native-revamp branch from f4b47b6 to 705bb5f Compare September 15, 2026 09:24
@datlechin
datlechin merged commit bfcbf58 into main Sep 15, 2026
8 of 9 checks passed
@datlechin
datlechin deleted the refactor/query-tab-native-revamp branch September 15, 2026 09:28
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