fix(welcome): deliver New Connection and the File menu commands without a welcome window (#1975) - #1976
Merged
Merged
Conversation
…ut a welcome window (#1975)
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #1975.
The bug
With a connection open,
File > New Connection...and ⌘N did nothing. The menu item stayed enabled and inert, which the HIG explicitly rules out: an item that cannot act must be dimmed.Root cause
New Connection does not open a window directly. It asks the welcome window to present the database-type chooser, over an unbuffered Combine
PassthroughSubjectwhose only subscriber isWelcomeWindowView:TableProApp.swift→WindowOpener.openConnectionForm()→WindowOpenerBridge→AppCommands.presentDatabaseTypeChooser.send(payload)TabRouter.closeWelcomeWindows()genuinely closes the welcome window on every successful connect, from eight call sites. So "no welcome window alive" is not an edge case, it is the steady state as soon as you open one connection. The event reached zero subscribers and was dropped. The reporter's "2 windows" detail is incidental; one is enough.Why this is a refactor and not a one-line fix
Five sibling File-menu items worked around the same fault by calling
WindowOpener.openWelcome()immediately before.send(...). Commit b830044 added that prefix to all five and missed New Connection, so the workaround was already forgotten once and is still unreleased. A sixth hand-added prefix would be the third occurrence of the same footgun, waiting for a seventh command to forget it.The guarantee had to move off the call site.
WelcomeRouteralready implements the correct buffered pattern for this: stash a pending value, open the window, and letWelcomeViewModeldrain it on mount. That is howrouteImport,routeShare,routeErrorandroutePluginInstallalready work, with no ordering dependency and no live-subscriber requirement.What changed
WelcomeRoutergains aWelcomeRequestslot withroute(_:)andconsumePendingRequest(), covering all six commands. It is a separate slot from the four existing ones, drained first because it is the only user-invoked one, and last-wins on a single slot rather than a queue: these are mutually exclusive one-shot menu intents.PassthroughSubjects are deleted fromAppCommands, and the five menu items lose their pairedopenWelcome()call.WelcomeWindowView@StateontoWelcomeViewModel. This is load-bearing, not polish: the drain runs in the view model and cannot reach view-local state. It also moves the "is the driver installed" branch out of the view.DatabaseTypeChooserPayloadbecomesIdentifiable, which lets theWelcomeChooserStatewrapper disappear.AppServicesgainswelcomeRouterso the router is injectable, matching how the tests already inject storage.Taskarms asynchronously, so a request routed betweensetUp()and the task arming was previously invisible until some later mutation.Net effect on app code is a simplification: 154 lines deleted against 374 added, most of the additions being tests.
Behaviour
⌘N brings the welcome window forward with the database chooser on it, which is what the welcome screen's + button already does. That matches Postico, where ⌘N brings the favourites window forward, and TablePlus. The shortcut itself is unchanged, so
docs/features/keyboard-shortcuts.mdxneeds no edit.Tests
WelcomeRouterTests: request survives with no subscriber, consume-once, last-wins, payload round-trip, independence from the other pending slots.WelcomeViewModelTests: the cmd+n not working #1975 regression in both directions, welcome window closed and welcome window already open, plus the sibling commands and the drain ordering against a background plugin install.WindowOpenerTests: New Connection queues a chooser request with no bridge wired.NewConnectionCommandUITests: launch, close the welcome window, invoke New Connection from the menu, assert the chooser appears. This reproduces the exact failing state without needing a live connection.All 16 unit tests pass. The UI test passes standalone in ~33s. It fails if run in the same
xcodebuildinvocation as the unit tests, but so does the pre-existingTableProLaunchUITestsunder the same conditions, on its own first assertion; UI and unit tests are separate commands in CLAUDE.md.swiftlint lint --strictreports 0 violations across 1211 files.Flagged, not fixed
WindowOpenercachesopenWindowclosures captured from per-window SwiftUI environments and never resetsisWired. This is latent, not the active cause:AppDelegate.windowWillCloseand the dock menu both callopenWelcome()successfully from that detached state. Reworking the menu to read@Environment(\.openWindow)in theCommandsbody is behaviour-neutral today and carries real regression risk.consumePendingRouterActions()early-returns after the first non-nil slot, so two slots filled in one batch can starve one. Pre-existing, and contained: the only two that could race are in mutually exclusive branches. Draining all slots per pass risks colliding sheet presentations, so it belongs in its own change.