Skip to content

Commit ea45b76

Browse files
authored
fix(coordinator): activate the existing tab instead of opening a duplicate (#1613) (#1623)
1 parent 74220dc commit ea45b76

7 files changed

Lines changed: 201 additions & 44 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2020
- The autocomplete popup now filters in place as you type instead of closing and reopening on every keystroke. (#1608)
2121
- Syntax highlighting no longer disappears after formatting a query. (#1612)
2222
- 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.
23+
- Clicking a table that's already open switches to its existing tab instead of opening a duplicate. (#1613)
2324
- MongoDB now connects over an SSH or Cloudflare tunnel instead of bypassing it and failing with a connection refused error. (#1621)
2425

2526
## [0.49.1] - 2026-06-06

TablePro/Core/Services/Infrastructure/TabRouter.swift

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -152,11 +152,7 @@ internal final class TabRouter {
152152
} ?? true
153153
return databaseMatches && schemaMatches
154154
}) else { continue }
155-
coordinator.tabManager.selectedTabId = match.id
156-
if let windowId = coordinator.windowId,
157-
let window = WindowLifecycleMonitor.shared.window(for: windowId) {
158-
window.makeKeyAndOrderFront(nil)
159-
}
155+
coordinator.selectTabAndFocusWindow(match.id)
160156
return true
161157
}
162158
return false

TablePro/Views/Main/Extensions/MainContentCoordinator+Navigation.swift

Lines changed: 52 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ extension MainContentCoordinator {
1818
func openTableTab(
1919
_ table: TableInfo,
2020
showStructure: Bool = false,
21-
redirectToSibling: Bool = false,
2221
forceNonPreview: Bool = false,
2322
activateGridFocus: Bool = false
2423
) {
@@ -27,7 +26,6 @@ extension MainContentCoordinator {
2726
schema: table.schema,
2827
showStructure: showStructure,
2928
isView: table.type == .view,
30-
redirectToSibling: redirectToSibling,
3129
forceNonPreview: forceNonPreview,
3230
activateGridFocus: activateGridFocus
3331
)
@@ -38,7 +36,6 @@ extension MainContentCoordinator {
3836
schema: String? = nil,
3937
showStructure: Bool = false,
4038
isView: Bool = false,
41-
redirectToSibling: Bool = false,
4239
forceNonPreview: Bool = false,
4340
activateGridFocus: Bool = false
4441
) {
@@ -59,18 +56,14 @@ extension MainContentCoordinator {
5956
let resolvedSchema = schema
6057
let createAsPreview = !forceNonPreview && AppSettingsManager.shared.tabs.enablePreviewTabs
6158

62-
// Fast path: if this table is already the active tab in the same database, skip all work
63-
if let current = tabManager.selectedTab,
64-
current.tabType == .table,
65-
current.tableContext.tableName == tableName,
66-
current.tableContext.databaseName == currentDatabase,
67-
current.tableContext.schemaName == resolvedSchema {
68-
if showStructure, let (_, tabIndex) = tabManager.selectedTabAndIndex {
69-
tabManager.mutate(at: tabIndex) { $0.display.resultsViewMode = .structure }
70-
}
71-
if activateGridFocus {
72-
focusActiveGrid()
73-
}
59+
if activateIfAlreadyOpen(
60+
tableName: tableName,
61+
databaseName: currentDatabase,
62+
schemaName: resolvedSchema,
63+
showStructure: showStructure,
64+
activateGridFocus: activateGridFocus,
65+
includeSiblings: navigationModel != .inPlace
66+
) {
7467
return
7568
}
7669

@@ -98,28 +91,6 @@ extension MainContentCoordinator {
9891
return
9992
}
10093

101-
// Opt-in cross-window navigation: if requested (e.g. quick switcher),
102-
// and another window already shows this table, focus that window.
103-
// Default-off so sidebar clicks and other window-local actions stay
104-
// window-local instead of stealing focus to a sibling.
105-
if redirectToSibling {
106-
for sibling in MainContentCoordinator.allActiveCoordinators()
107-
where sibling !== self && sibling.connectionId == connectionId {
108-
let hasMatch = sibling.tabManager.tabs.contains { tab in
109-
tab.tabType == .table
110-
&& tab.tableContext.tableName == tableName
111-
&& tab.tableContext.databaseName == currentDatabase
112-
&& tab.tableContext.schemaName == resolvedSchema
113-
}
114-
guard hasMatch,
115-
let windowId = sibling.windowId,
116-
let window = WindowLifecycleMonitor.shared.window(for: windowId) else { continue }
117-
pendingGridFocusOnOpen = false
118-
window.makeKeyAndOrderFront(nil)
119-
return
120-
}
121-
}
122-
12394
// If no tabs exist (empty state), add a table tab directly.
12495
if tabManager.tabs.isEmpty {
12596
addFirstTableTab(
@@ -191,6 +162,50 @@ extension MainContentCoordinator {
191162
WindowManager.shared.openTab(payload: payload)
192163
}
193164

165+
func activateIfAlreadyOpen(
166+
tableName: String,
167+
databaseName: String,
168+
schemaName: String?,
169+
showStructure: Bool,
170+
activateGridFocus: Bool,
171+
includeSiblings: Bool
172+
) -> Bool {
173+
func matches(_ tab: QueryTab) -> Bool {
174+
tab.tabType == .table
175+
&& tab.tableContext.tableName == tableName
176+
&& tab.tableContext.databaseName == databaseName
177+
&& tab.tableContext.schemaName == schemaName
178+
}
179+
180+
if let match = tabManager.tabs.first(where: matches) {
181+
if tabManager.selectedTabId != match.id {
182+
tabManager.selectedTabId = match.id
183+
}
184+
applyStructureMode(showStructure, toTab: match.id, in: tabManager)
185+
if activateGridFocus {
186+
requestGridFocus()
187+
}
188+
return true
189+
}
190+
191+
guard includeSiblings else { return false }
192+
193+
for sibling in MainContentCoordinator.allActiveCoordinators()
194+
where sibling !== self && sibling.connectionId == connectionId {
195+
guard let match = sibling.tabManager.tabs.first(where: matches) else { continue }
196+
sibling.pendingGridFocusOnOpen = activateGridFocus
197+
applyStructureMode(showStructure, toTab: match.id, in: sibling.tabManager)
198+
sibling.selectTabAndFocusWindow(match.id)
199+
return true
200+
}
201+
return false
202+
}
203+
204+
private func applyStructureMode(_ showStructure: Bool, toTab tabId: UUID, in tabManager: QueryTabManager) {
205+
guard showStructure, let index = tabManager.tabs.firstIndex(where: { $0.id == tabId }) else { return }
206+
tabManager.mutate(at: index) { $0.display.resultsViewMode = .structure }
207+
}
208+
194209
private func addFirstTableTab(
195210
tableName: String,
196211
currentDatabase: String,

TablePro/Views/Main/Extensions/MainContentCoordinator+QuickSwitcher.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,10 @@ extension MainContentCoordinator {
1515
func handleQuickSwitcherSelection(_ item: QuickSwitcherItem) {
1616
switch item.kind {
1717
case .table, .systemTable:
18-
openTableTab(item.name, redirectToSibling: true, activateGridFocus: true)
18+
openTableTab(item.name, activateGridFocus: true)
1919

2020
case .view:
21-
openTableTab(item.name, isView: true, redirectToSibling: true, activateGridFocus: true)
21+
openTableTab(item.name, isView: true, activateGridFocus: true)
2222

2323
case .database:
2424
Task {

TablePro/Views/Main/Extensions/MainContentCoordinator+WindowLifecycle.swift

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,13 @@ extension MainContentCoordinator {
9797
)
9898
}
9999

100+
func selectTabAndFocusWindow(_ tabId: UUID) {
101+
tabManager.selectedTabId = tabId
102+
guard let windowId,
103+
let window = WindowLifecycleMonitor.shared.window(for: windowId) else { return }
104+
window.makeKeyAndOrderFront(nil)
105+
}
106+
100107
// MARK: - Sidebar Sync
101108

102109
/// Update the window-scoped sidebar selection so the active table tab

TableProTests/Views/Main/MultiConnectionNavigationTests.swift

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -248,4 +248,58 @@ struct MultiConnectionNavigationTests {
248248
#expect(tabManagerB.tabs.count == tabCountBefore)
249249
#expect(tabManagerB.tabs.first?.tableContext.tableName == "orders")
250250
}
251+
252+
// MARK: - Cross-window deduplication (issue #1613)
253+
254+
@Test("openTableTab activates a sibling window's tab instead of duplicating when the table is already open")
255+
@MainActor
256+
func openTableTabActivatesSiblingInsteadOfDuplicating() throws {
257+
let connectionId = UUID()
258+
let (coordinatorA, tabManagerA) = makeCoordinator(id: connectionId, name: "Conn", database: "db_a")
259+
let (coordinatorB, tabManagerB) = makeCoordinator(id: connectionId, name: "Conn", database: "db_a")
260+
coordinatorA.registerEagerly()
261+
coordinatorB.registerEagerly()
262+
defer {
263+
coordinatorA.teardown()
264+
coordinatorB.teardown()
265+
}
266+
267+
try tabManagerA.addTableTab(tableName: "users", databaseType: .mysql, databaseName: "db_a")
268+
try tabManagerA.addTableTab(tableName: "accounts", databaseType: .mysql, databaseName: "db_a")
269+
#expect(tabManagerA.selectedTab?.tableContext.tableName == "accounts")
270+
try tabManagerB.addTableTab(tableName: "orders", databaseType: .mysql, databaseName: "db_a")
271+
272+
coordinatorB.openTableTab("users")
273+
274+
#expect(tabManagerB.tabs.count == 1)
275+
#expect(tabManagerB.tabs.first?.tableContext.tableName == "orders")
276+
#expect(tabManagerA.selectedTab?.tableContext.tableName == "users")
277+
}
278+
279+
@Test("openTableTab does not dedupe against a sibling on a different connection")
280+
@MainActor
281+
func openTableTabIgnoresSiblingOnDifferentConnection() throws {
282+
let (coordinatorA, tabManagerA) = makeCoordinator(name: "ConnA", database: "db_a")
283+
let (coordinatorB, tabManagerB) = makeCoordinator(name: "ConnB", database: "db_b")
284+
coordinatorA.registerEagerly()
285+
coordinatorB.registerEagerly()
286+
defer {
287+
coordinatorA.teardown()
288+
coordinatorB.teardown()
289+
}
290+
291+
try tabManagerA.addTableTab(tableName: "users", databaseType: .mysql, databaseName: "db_a")
292+
293+
let activated = coordinatorB.activateIfAlreadyOpen(
294+
tableName: "users",
295+
databaseName: "db_b",
296+
schemaName: nil,
297+
showStructure: false,
298+
activateGridFocus: false,
299+
includeSiblings: true
300+
)
301+
302+
#expect(activated == false)
303+
#expect(tabManagerB.tabs.isEmpty)
304+
}
251305
}

TableProTests/Views/Main/OpenTableTabTests.swift

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,90 @@ struct OpenTableTabTests {
194194
#expect(tabManager.selectedTab?.isPreview == false)
195195
}
196196

197+
// MARK: - Activate already-open tab (issue #1613)
198+
199+
@Test("Clicking a table open in a non-selected tab selects it instead of duplicating")
200+
@MainActor
201+
func clickingTableInNonSelectedTabSelectsIt() throws {
202+
let connection = TestFixtures.makeConnection(database: "db_a")
203+
let tabManager = QueryTabManager()
204+
let coordinator = MainContentCoordinator(
205+
connection: connection,
206+
tabManager: tabManager,
207+
changeManager: DataChangeManager(),
208+
toolbarState: ConnectionToolbarState()
209+
)
210+
defer { coordinator.teardown() }
211+
212+
try tabManager.addTableTab(tableName: "users", databaseType: connection.type, databaseName: "db_a")
213+
try tabManager.addTableTab(tableName: "orders", databaseType: connection.type, databaseName: "db_a")
214+
#expect(tabManager.tabs.count == 2)
215+
#expect(tabManager.selectedTab?.tableContext.tableName == "orders")
216+
217+
coordinator.openTableTab("users")
218+
219+
#expect(tabManager.tabs.count == 2)
220+
#expect(tabManager.selectedTab?.tableContext.tableName == "users")
221+
}
222+
223+
@Test("activateIfAlreadyOpen returns false when no open tab matches")
224+
@MainActor
225+
func activateIfAlreadyOpenReturnsFalseWhenNoMatch() throws {
226+
let connection = TestFixtures.makeConnection(database: "db_a")
227+
let tabManager = QueryTabManager()
228+
let coordinator = MainContentCoordinator(
229+
connection: connection,
230+
tabManager: tabManager,
231+
changeManager: DataChangeManager(),
232+
toolbarState: ConnectionToolbarState()
233+
)
234+
defer { coordinator.teardown() }
235+
236+
try tabManager.addTableTab(tableName: "orders", databaseType: connection.type, databaseName: "db_a")
237+
238+
let activated = coordinator.activateIfAlreadyOpen(
239+
tableName: "users",
240+
databaseName: "db_a",
241+
schemaName: nil,
242+
showStructure: false,
243+
activateGridFocus: false,
244+
includeSiblings: true
245+
)
246+
247+
#expect(activated == false)
248+
#expect(tabManager.selectedTab?.tableContext.tableName == "orders")
249+
}
250+
251+
@Test("activateIfAlreadyOpen selects an existing in-window tab and applies structure mode")
252+
@MainActor
253+
func activateIfAlreadyOpenSelectsExistingTabWithStructure() throws {
254+
let connection = TestFixtures.makeConnection(database: "db_a")
255+
let tabManager = QueryTabManager()
256+
let coordinator = MainContentCoordinator(
257+
connection: connection,
258+
tabManager: tabManager,
259+
changeManager: DataChangeManager(),
260+
toolbarState: ConnectionToolbarState()
261+
)
262+
defer { coordinator.teardown() }
263+
264+
try tabManager.addTableTab(tableName: "users", databaseType: connection.type, databaseName: "db_a")
265+
try tabManager.addTableTab(tableName: "orders", databaseType: connection.type, databaseName: "db_a")
266+
267+
let activated = coordinator.activateIfAlreadyOpen(
268+
tableName: "users",
269+
databaseName: "db_a",
270+
schemaName: nil,
271+
showStructure: true,
272+
activateGridFocus: false,
273+
includeSiblings: true
274+
)
275+
276+
#expect(activated == true)
277+
#expect(tabManager.selectedTab?.tableContext.tableName == "users")
278+
#expect(tabManager.selectedTab?.display.resultsViewMode == .structure)
279+
}
280+
197281
@MainActor
198282
private static func makeCoordinator() -> MainContentCoordinator {
199283
MainContentCoordinator(

0 commit comments

Comments
 (0)