Skip to content

Commit caeea76

Browse files
authored
fix(welcome): rebuild connection tree when a new group is created (#1704) (#1709)
1 parent e289fdf commit caeea76

4 files changed

Lines changed: 158 additions & 11 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2323
- Redis key browsing now lists every key in a database or namespace and pages through them correctly. It was reading only the first SCAN batch, so large keyspaces showed a partial, fixed set of keys. (#1701)
2424
- A dropped Redis connection now reconnects on the next command and replays auth and the selected database, instead of failing until the next health check. (#1701)
2525
- DuckDB VARIANT columns now show their value as text instead of an empty cell.
26+
- A new database group now appears in the connection list right away instead of only after restarting the app. (#1704)
2627

2728
## [0.51.1] - 2026-06-16
2829

TablePro/ViewModels/WelcomeViewModel.swift

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -454,6 +454,22 @@ final class WelcomeViewModel {
454454
rebuildTree()
455455
}
456456

457+
func createGroup(name: String, color: ConnectionColor, parentId: UUID?) {
458+
let group = ConnectionGroup(name: name, color: color, parentId: parentId)
459+
groupStorage.addGroup(group)
460+
groups = groupStorage.loadGroups()
461+
guard groups.contains(where: { $0.id == group.id }) else { return }
462+
expandedGroupIds.insert(group.id)
463+
if let parentId {
464+
expandedGroupIds.insert(parentId)
465+
}
466+
if !pendingMoveToNewGroup.isEmpty {
467+
moveConnections(pendingMoveToNewGroup, toGroup: group.id)
468+
pendingMoveToNewGroup = []
469+
}
470+
rebuildTree()
471+
}
472+
457473
func createSubgroup(under parentId: UUID) {
458474
activeSheet = .newGroup(parentId: parentId)
459475
}

TablePro/Views/Connection/WelcomeWindowView.swift

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -92,17 +92,7 @@ struct WelcomeWindowView: View {
9292
switch sheet {
9393
case .newGroup(let parentId):
9494
CreateGroupSheet(parentId: parentId) { name, color, pid in
95-
let group = ConnectionGroup(name: name, color: color, parentId: pid)
96-
GroupStorage.shared.addGroup(group)
97-
vm.groups = GroupStorage.shared.loadGroups()
98-
vm.expandedGroupIds.insert(group.id)
99-
if let pid {
100-
vm.expandedGroupIds.insert(pid)
101-
}
102-
if !vm.pendingMoveToNewGroup.isEmpty {
103-
vm.moveConnections(vm.pendingMoveToNewGroup, toGroup: group.id)
104-
vm.pendingMoveToNewGroup = []
105-
}
95+
vm.createGroup(name: name, color: color, parentId: pid)
10696
}
10797
case .activation:
10898
LicenseActivationSheet()
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
//
2+
// WelcomeViewModelTests.swift
3+
// TableProTests
4+
//
5+
6+
@testable import TablePro
7+
import TableProPluginKit
8+
import XCTest
9+
10+
@MainActor
11+
final class WelcomeViewModelTests: XCTestCase {
12+
private var suiteName: String!
13+
private var defaults: UserDefaults!
14+
private var syncSuiteName: String!
15+
private var syncDefaults: UserDefaults!
16+
private var connectionFileURL: URL!
17+
private var groupStorage: GroupStorage!
18+
private var connectionStorage: ConnectionStorage!
19+
private var viewModel: WelcomeViewModel!
20+
21+
override func setUp() {
22+
super.setUp()
23+
let unique = UUID().uuidString
24+
suiteName = "com.TablePro.tests.WelcomeViewModel.\(unique)"
25+
syncSuiteName = "com.TablePro.tests.WelcomeViewModel.sync.\(unique)"
26+
guard let defaults = UserDefaults(suiteName: suiteName),
27+
let syncDefaults = UserDefaults(suiteName: syncSuiteName) else {
28+
XCTFail("Could not create isolated UserDefaults suites")
29+
return
30+
}
31+
self.defaults = defaults
32+
self.syncDefaults = syncDefaults
33+
let tracker = SyncChangeTracker(metadataStorage: SyncMetadataStorage(userDefaults: syncDefaults))
34+
connectionFileURL = FileManager.default.temporaryDirectory
35+
.appendingPathComponent("tablepro-tests")
36+
.appendingPathComponent("welcome-connections_\(unique).json")
37+
try? FileManager.default.createDirectory(
38+
at: connectionFileURL.deletingLastPathComponent(),
39+
withIntermediateDirectories: true
40+
)
41+
connectionStorage = ConnectionStorage(
42+
fileURL: connectionFileURL,
43+
userDefaults: defaults,
44+
syncTracker: tracker
45+
)
46+
groupStorage = GroupStorage(
47+
userDefaults: defaults,
48+
syncTracker: tracker,
49+
connectionStorage: self.connectionStorage
50+
)
51+
viewModel = WelcomeViewModel(services: makeServices())
52+
}
53+
54+
override func tearDown() {
55+
defaults.removePersistentDomain(forName: suiteName)
56+
syncDefaults.removePersistentDomain(forName: syncSuiteName)
57+
try? FileManager.default.removeItem(at: connectionFileURL)
58+
viewModel = nil
59+
groupStorage = nil
60+
connectionStorage = nil
61+
defaults = nil
62+
syncDefaults = nil
63+
suiteName = nil
64+
syncSuiteName = nil
65+
connectionFileURL = nil
66+
super.tearDown()
67+
}
68+
69+
private func makeServices() -> AppServices {
70+
let live = AppServices.live
71+
return AppServices(
72+
appEvents: live.appEvents,
73+
appSettings: live.appSettings,
74+
appSettingsStorage: live.appSettingsStorage,
75+
connectionStorage: connectionStorage,
76+
databaseManager: live.databaseManager,
77+
pluginManager: live.pluginManager,
78+
schemaService: live.schemaService,
79+
schemaProviderRegistry: live.schemaProviderRegistry,
80+
sqlFavoriteManager: live.sqlFavoriteManager,
81+
favoriteTablesStorage: live.favoriteTablesStorage,
82+
aiChatStorage: live.aiChatStorage,
83+
aiKeyStorage: live.aiKeyStorage,
84+
groupStorage: groupStorage,
85+
tagStorage: live.tagStorage,
86+
sshProfileStorage: live.sshProfileStorage,
87+
licenseManager: live.licenseManager,
88+
conflictResolver: live.conflictResolver,
89+
syncMetadataStorage: live.syncMetadataStorage,
90+
favoritesExpansionState: live.favoritesExpansionState,
91+
linkedFolderWatcher: live.linkedFolderWatcher,
92+
queryHistoryManager: live.queryHistoryManager,
93+
dateFormattingService: live.dateFormattingService,
94+
copilotService: live.copilotService,
95+
mcpServerManager: live.mcpServerManager,
96+
syncTracker: live.syncTracker,
97+
themeEngine: live.themeEngine
98+
)
99+
}
100+
101+
private func groupIds(in nodes: [ConnectionGroupTreeNode]) -> [UUID] {
102+
nodes.flatMap { node -> [UUID] in
103+
guard case .group(let group, let children) = node else { return [] }
104+
return [group.id] + groupIds(in: children)
105+
}
106+
}
107+
108+
func testCreateGroupShowsImmediatelyInTree() throws {
109+
XCTAssertTrue(groupIds(in: viewModel.treeItems).isEmpty)
110+
111+
viewModel.createGroup(name: "Production", color: .red, parentId: nil)
112+
113+
let created = try XCTUnwrap(groupStorage.loadGroups().first { $0.name == "Production" })
114+
XCTAssertTrue(groupIds(in: viewModel.treeItems).contains(created.id))
115+
XCTAssertTrue(viewModel.expandedGroupIds.contains(created.id))
116+
}
117+
118+
func testCreateSubgroupExpandsParentAndChild() throws {
119+
viewModel.createGroup(name: "Parent", color: .none, parentId: nil)
120+
let parentId = try XCTUnwrap(groupStorage.loadGroups().first { $0.name == "Parent" }?.id)
121+
122+
viewModel.createGroup(name: "Child", color: .none, parentId: parentId)
123+
let childId = try XCTUnwrap(groupStorage.loadGroups().first { $0.name == "Child" }?.id)
124+
125+
XCTAssertTrue(groupIds(in: viewModel.treeItems).contains(parentId))
126+
XCTAssertTrue(groupIds(in: viewModel.treeItems).contains(childId))
127+
XCTAssertTrue(viewModel.expandedGroupIds.contains(parentId))
128+
XCTAssertTrue(viewModel.expandedGroupIds.contains(childId))
129+
}
130+
131+
func testCreateDuplicateNameDoesNotAddSecondNode() {
132+
viewModel.createGroup(name: "Staging", color: .orange, parentId: nil)
133+
viewModel.createGroup(name: "staging", color: .blue, parentId: nil)
134+
135+
let stagingNodes = groupIds(in: viewModel.treeItems).filter { id in
136+
viewModel.groups.first { $0.id == id }?.name.lowercased() == "staging"
137+
}
138+
XCTAssertEqual(stagingNodes.count, 1)
139+
}
140+
}

0 commit comments

Comments
 (0)