Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- The autocomplete popup now filters in place as you type instead of closing and reopening on every keystroke. (#1608)
- Syntax highlighting no longer disappears after formatting a query. (#1612)
- The GitHub Copilot provider no longer shows a Max output tokens field it ignores, and picking a Copilot model no longer leaves a stray model ID field behind.
- Clicking a table that's already open switches to its existing tab instead of opening a duplicate. (#1613)
- MongoDB now connects over an SSH or Cloudflare tunnel instead of bypassing it and failing with a connection refused error. (#1621)

## [0.49.1] - 2026-06-06
Expand Down
6 changes: 1 addition & 5 deletions TablePro/Core/Services/Infrastructure/TabRouter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -152,11 +152,7 @@ internal final class TabRouter {
} ?? true
return databaseMatches && schemaMatches
}) else { continue }
coordinator.tabManager.selectedTabId = match.id
if let windowId = coordinator.windowId,
let window = WindowLifecycleMonitor.shared.window(for: windowId) {
window.makeKeyAndOrderFront(nil)
}
coordinator.selectTabAndFocusWindow(match.id)
return true
}
return false
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ extension MainContentCoordinator {
func openTableTab(
_ table: TableInfo,
showStructure: Bool = false,
redirectToSibling: Bool = false,
forceNonPreview: Bool = false,
activateGridFocus: Bool = false
) {
Expand All @@ -27,7 +26,6 @@ extension MainContentCoordinator {
schema: table.schema,
showStructure: showStructure,
isView: table.type == .view,
redirectToSibling: redirectToSibling,
forceNonPreview: forceNonPreview,
activateGridFocus: activateGridFocus
)
Expand All @@ -38,7 +36,6 @@ extension MainContentCoordinator {
schema: String? = nil,
showStructure: Bool = false,
isView: Bool = false,
redirectToSibling: Bool = false,
forceNonPreview: Bool = false,
activateGridFocus: Bool = false
) {
Expand All @@ -59,18 +56,14 @@ extension MainContentCoordinator {
let resolvedSchema = schema
let createAsPreview = !forceNonPreview && AppSettingsManager.shared.tabs.enablePreviewTabs

// Fast path: if this table is already the active tab in the same database, skip all work
if let current = tabManager.selectedTab,
current.tabType == .table,
current.tableContext.tableName == tableName,
current.tableContext.databaseName == currentDatabase,
current.tableContext.schemaName == resolvedSchema {
if showStructure, let (_, tabIndex) = tabManager.selectedTabAndIndex {
tabManager.mutate(at: tabIndex) { $0.display.resultsViewMode = .structure }
}
if activateGridFocus {
focusActiveGrid()
}
if activateIfAlreadyOpen(
tableName: tableName,
databaseName: currentDatabase,
schemaName: resolvedSchema,
showStructure: showStructure,
activateGridFocus: activateGridFocus,
includeSiblings: navigationModel != .inPlace
) {
return
}

Expand Down Expand Up @@ -98,28 +91,6 @@ extension MainContentCoordinator {
return
}

// Opt-in cross-window navigation: if requested (e.g. quick switcher),
// and another window already shows this table, focus that window.
// Default-off so sidebar clicks and other window-local actions stay
// window-local instead of stealing focus to a sibling.
if redirectToSibling {
for sibling in MainContentCoordinator.allActiveCoordinators()
where sibling !== self && sibling.connectionId == connectionId {
let hasMatch = sibling.tabManager.tabs.contains { tab in
tab.tabType == .table
&& tab.tableContext.tableName == tableName
&& tab.tableContext.databaseName == currentDatabase
&& tab.tableContext.schemaName == resolvedSchema
}
guard hasMatch,
let windowId = sibling.windowId,
let window = WindowLifecycleMonitor.shared.window(for: windowId) else { continue }
pendingGridFocusOnOpen = false
window.makeKeyAndOrderFront(nil)
return
}
}

// If no tabs exist (empty state), add a table tab directly.
if tabManager.tabs.isEmpty {
addFirstTableTab(
Expand Down Expand Up @@ -191,6 +162,50 @@ extension MainContentCoordinator {
WindowManager.shared.openTab(payload: payload)
}

func activateIfAlreadyOpen(
tableName: String,
databaseName: String,
schemaName: String?,
showStructure: Bool,
activateGridFocus: Bool,
includeSiblings: Bool
) -> Bool {
func matches(_ tab: QueryTab) -> Bool {
tab.tabType == .table
&& tab.tableContext.tableName == tableName
&& tab.tableContext.databaseName == databaseName
&& tab.tableContext.schemaName == schemaName
}

if let match = tabManager.tabs.first(where: matches) {
if tabManager.selectedTabId != match.id {
tabManager.selectedTabId = match.id
}
applyStructureMode(showStructure, toTab: match.id, in: tabManager)
if activateGridFocus {
requestGridFocus()
Comment on lines +185 to +186

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Defer grid focus until the activated tab is attached

When activateGridFocus is true and the match is a non-selected tab in the same window, this calls requestGridFocus() immediately after changing selectedTabId. At that point SwiftUI has not rebuilt the content for the new tab yet, so dataTabDelegate can still point at the outgoing table; if its focusGrid() succeeds, pendingGridFocusOnOpen is cleared and the newly selected table never consumes it in KeyHandlingTableView.viewDidMoveToWindow(). Sidebar/quick-switcher opens of an already-open tab can therefore leave keyboard focus on the old grid instead of the activated table.

Useful? React with 👍 / 👎.

}
return true
Comment on lines +180 to +188

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Promote matching preview tabs on force-open

When forceNonPreview is used by the sidebar double-click path, this new early activation path still returns for any matching tab without checking whether the match is a preview tab or promoting it. If the table is already open in an inactive preview tab, double-clicking it selects that tab but leaves isPreview == true, so it can later be replaced by another single-click and won't be persisted, contrary to the existing force-non-preview behavior.

Useful? React with 👍 / 👎.

}

guard includeSiblings else { return false }

for sibling in MainContentCoordinator.allActiveCoordinators()
where sibling !== self && sibling.connectionId == connectionId {
guard let match = sibling.tabManager.tabs.first(where: matches) else { continue }
sibling.pendingGridFocusOnOpen = activateGridFocus
applyStructureMode(showStructure, toTab: match.id, in: sibling.tabManager)
sibling.selectTabAndFocusWindow(match.id)
return true
}
return false
}

private func applyStructureMode(_ showStructure: Bool, toTab tabId: UUID, in tabManager: QueryTabManager) {
guard showStructure, let index = tabManager.tabs.firstIndex(where: { $0.id == tabId }) else { return }
tabManager.mutate(at: index) { $0.display.resultsViewMode = .structure }
}

private func addFirstTableTab(
tableName: String,
currentDatabase: String,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,10 @@ extension MainContentCoordinator {
func handleQuickSwitcherSelection(_ item: QuickSwitcherItem) {
switch item.kind {
case .table, .systemTable:
openTableTab(item.name, redirectToSibling: true, activateGridFocus: true)
openTableTab(item.name, activateGridFocus: true)

case .view:
openTableTab(item.name, isView: true, redirectToSibling: true, activateGridFocus: true)
openTableTab(item.name, isView: true, activateGridFocus: true)

case .database:
Task {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,13 @@ extension MainContentCoordinator {
)
}

func selectTabAndFocusWindow(_ tabId: UUID) {
tabManager.selectedTabId = tabId
guard let windowId,
let window = WindowLifecycleMonitor.shared.window(for: windowId) else { return }
window.makeKeyAndOrderFront(nil)
}

// MARK: - Sidebar Sync

/// Update the window-scoped sidebar selection so the active table tab
Expand Down
54 changes: 54 additions & 0 deletions TableProTests/Views/Main/MultiConnectionNavigationTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -248,4 +248,58 @@ struct MultiConnectionNavigationTests {
#expect(tabManagerB.tabs.count == tabCountBefore)
#expect(tabManagerB.tabs.first?.tableContext.tableName == "orders")
}

// MARK: - Cross-window deduplication (issue #1613)

@Test("openTableTab activates a sibling window's tab instead of duplicating when the table is already open")
@MainActor
func openTableTabActivatesSiblingInsteadOfDuplicating() throws {
let connectionId = UUID()
let (coordinatorA, tabManagerA) = makeCoordinator(id: connectionId, name: "Conn", database: "db_a")
let (coordinatorB, tabManagerB) = makeCoordinator(id: connectionId, name: "Conn", database: "db_a")
coordinatorA.registerEagerly()
coordinatorB.registerEagerly()
defer {
coordinatorA.teardown()
coordinatorB.teardown()
}

try tabManagerA.addTableTab(tableName: "users", databaseType: .mysql, databaseName: "db_a")
try tabManagerA.addTableTab(tableName: "accounts", databaseType: .mysql, databaseName: "db_a")
#expect(tabManagerA.selectedTab?.tableContext.tableName == "accounts")
try tabManagerB.addTableTab(tableName: "orders", databaseType: .mysql, databaseName: "db_a")

coordinatorB.openTableTab("users")

#expect(tabManagerB.tabs.count == 1)
#expect(tabManagerB.tabs.first?.tableContext.tableName == "orders")
#expect(tabManagerA.selectedTab?.tableContext.tableName == "users")
}

@Test("openTableTab does not dedupe against a sibling on a different connection")
@MainActor
func openTableTabIgnoresSiblingOnDifferentConnection() throws {
let (coordinatorA, tabManagerA) = makeCoordinator(name: "ConnA", database: "db_a")
let (coordinatorB, tabManagerB) = makeCoordinator(name: "ConnB", database: "db_b")
coordinatorA.registerEagerly()
coordinatorB.registerEagerly()
defer {
coordinatorA.teardown()
coordinatorB.teardown()
}

try tabManagerA.addTableTab(tableName: "users", databaseType: .mysql, databaseName: "db_a")

let activated = coordinatorB.activateIfAlreadyOpen(
tableName: "users",
databaseName: "db_b",
schemaName: nil,
showStructure: false,
activateGridFocus: false,
includeSiblings: true
)

#expect(activated == false)
#expect(tabManagerB.tabs.isEmpty)
}
}
84 changes: 84 additions & 0 deletions TableProTests/Views/Main/OpenTableTabTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,90 @@ struct OpenTableTabTests {
#expect(tabManager.selectedTab?.isPreview == false)
}

// MARK: - Activate already-open tab (issue #1613)

@Test("Clicking a table open in a non-selected tab selects it instead of duplicating")
@MainActor
func clickingTableInNonSelectedTabSelectsIt() throws {
let connection = TestFixtures.makeConnection(database: "db_a")
let tabManager = QueryTabManager()
let coordinator = MainContentCoordinator(
connection: connection,
tabManager: tabManager,
changeManager: DataChangeManager(),
toolbarState: ConnectionToolbarState()
)
defer { coordinator.teardown() }

try tabManager.addTableTab(tableName: "users", databaseType: connection.type, databaseName: "db_a")
try tabManager.addTableTab(tableName: "orders", databaseType: connection.type, databaseName: "db_a")
#expect(tabManager.tabs.count == 2)
#expect(tabManager.selectedTab?.tableContext.tableName == "orders")

coordinator.openTableTab("users")

#expect(tabManager.tabs.count == 2)
#expect(tabManager.selectedTab?.tableContext.tableName == "users")
}

@Test("activateIfAlreadyOpen returns false when no open tab matches")
@MainActor
func activateIfAlreadyOpenReturnsFalseWhenNoMatch() throws {
let connection = TestFixtures.makeConnection(database: "db_a")
let tabManager = QueryTabManager()
let coordinator = MainContentCoordinator(
connection: connection,
tabManager: tabManager,
changeManager: DataChangeManager(),
toolbarState: ConnectionToolbarState()
)
defer { coordinator.teardown() }

try tabManager.addTableTab(tableName: "orders", databaseType: connection.type, databaseName: "db_a")

let activated = coordinator.activateIfAlreadyOpen(
tableName: "users",
databaseName: "db_a",
schemaName: nil,
showStructure: false,
activateGridFocus: false,
includeSiblings: true
)

#expect(activated == false)
#expect(tabManager.selectedTab?.tableContext.tableName == "orders")
}

@Test("activateIfAlreadyOpen selects an existing in-window tab and applies structure mode")
@MainActor
func activateIfAlreadyOpenSelectsExistingTabWithStructure() throws {
let connection = TestFixtures.makeConnection(database: "db_a")
let tabManager = QueryTabManager()
let coordinator = MainContentCoordinator(
connection: connection,
tabManager: tabManager,
changeManager: DataChangeManager(),
toolbarState: ConnectionToolbarState()
)
defer { coordinator.teardown() }

try tabManager.addTableTab(tableName: "users", databaseType: connection.type, databaseName: "db_a")
try tabManager.addTableTab(tableName: "orders", databaseType: connection.type, databaseName: "db_a")

let activated = coordinator.activateIfAlreadyOpen(
tableName: "users",
databaseName: "db_a",
schemaName: nil,
showStructure: true,
activateGridFocus: false,
includeSiblings: true
)

#expect(activated == true)
#expect(tabManager.selectedTab?.tableContext.tableName == "users")
#expect(tabManager.selectedTab?.display.resultsViewMode == .structure)
}

@MainActor
private static func makeCoordinator() -> MainContentCoordinator {
MainContentCoordinator(
Expand Down
Loading