Skip to content

Commit d76f19e

Browse files
committed
fix(sync): propagate connection group membership changes to other devices
1 parent 5a38125 commit d76f19e

4 files changed

Lines changed: 55 additions & 8 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### Fixed
11+
12+
- Moving a connection into or out of a group now syncs across devices, instead of leaving it ungrouped on your other Macs.
13+
1014
## [0.46.0] - 2026-05-28
1115

1216
### Added

TablePro/Core/Storage/GroupStorage.swift

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -104,15 +104,15 @@ final class GroupStorage {
104104

105105
let storage = connectionStorageProvider()
106106
var connections = storage.loadConnections()
107-
var changed = false
107+
var changed: [DatabaseConnection] = []
108108
for i in connections.indices {
109109
if let gid = connections[i].groupId, allIdsToDelete.contains(gid) {
110110
connections[i].groupId = nil
111-
changed = true
111+
changed.append(connections[i])
112112
}
113113
}
114-
if changed {
115-
if !storage.saveConnections(connections) {
114+
if !changed.isEmpty {
115+
if !storage.updateConnections(changed) {
116116
Self.logger.error("Failed to clear groupId references after group deletion")
117117
}
118118
}

TablePro/ViewModels/WelcomeViewModel.swift

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -415,10 +415,12 @@ final class WelcomeViewModel {
415415

416416
func moveConnections(_ targets: [DatabaseConnection], toGroup groupId: UUID) {
417417
let ids = Set(targets.map(\.id))
418+
var updated: [DatabaseConnection] = []
418419
for i in connections.indices where ids.contains(connections[i].id) {
419420
connections[i].groupId = groupId
421+
updated.append(connections[i])
420422
}
421-
guard storage.saveConnections(connections) else {
423+
guard storage.updateConnections(updated) else {
422424
connections = storage.loadConnections()
423425
rebuildTree()
424426
return
@@ -428,10 +430,12 @@ final class WelcomeViewModel {
428430

429431
func removeFromGroup(_ targets: [DatabaseConnection]) {
430432
let ids = Set(targets.map(\.id))
433+
var updated: [DatabaseConnection] = []
431434
for i in connections.indices where ids.contains(connections[i].id) {
432435
connections[i].groupId = nil
436+
updated.append(connections[i])
433437
}
434-
guard storage.saveConnections(connections) else {
438+
guard storage.updateConnections(updated) else {
435439
connections = storage.loadConnections()
436440
rebuildTree()
437441
return

TableProTests/Core/Storage/GroupStorageTests.swift

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ final class GroupStorageTests: XCTestCase {
1414
private var syncDefaults: UserDefaults!
1515
private var syncSuiteName: String!
1616
private var storage: GroupStorage!
17+
private var tracker: SyncChangeTracker!
18+
private var connectionStorage: ConnectionStorage!
19+
private var connectionFileURL: URL!
1720

1821
override func setUp() {
1922
super.setUp()
@@ -23,18 +26,38 @@ final class GroupStorageTests: XCTestCase {
2326
syncSuiteName = "com.TablePro.tests.Sync.\(unique)"
2427
syncDefaults = UserDefaults(suiteName: syncSuiteName)!
2528
let metadata = SyncMetadataStorage(userDefaults: syncDefaults)
26-
let tracker = SyncChangeTracker(metadataStorage: metadata)
27-
storage = GroupStorage(userDefaults: defaults, syncTracker: tracker)
29+
tracker = SyncChangeTracker(metadataStorage: metadata)
30+
connectionFileURL = FileManager.default.temporaryDirectory
31+
.appendingPathComponent("tablepro-tests")
32+
.appendingPathComponent("group-connections_\(unique).json")
33+
try? FileManager.default.createDirectory(
34+
at: connectionFileURL.deletingLastPathComponent(),
35+
withIntermediateDirectories: true
36+
)
37+
connectionStorage = ConnectionStorage(
38+
fileURL: connectionFileURL,
39+
userDefaults: defaults,
40+
syncTracker: tracker
41+
)
42+
storage = GroupStorage(
43+
userDefaults: defaults,
44+
syncTracker: tracker,
45+
connectionStorage: connectionStorage
46+
)
2847
}
2948

3049
override func tearDown() {
3150
defaults.removePersistentDomain(forName: suiteName)
3251
syncDefaults.removePersistentDomain(forName: syncSuiteName)
52+
try? FileManager.default.removeItem(at: connectionFileURL)
3353
defaults = nil
3454
suiteName = nil
3555
syncDefaults = nil
3656
syncSuiteName = nil
3757
storage = nil
58+
tracker = nil
59+
connectionStorage = nil
60+
connectionFileURL = nil
3861
super.tearDown()
3962
}
4063

@@ -129,6 +152,22 @@ final class GroupStorageTests: XCTestCase {
129152
XCTAssertEqual(loaded[0].name, "Prod")
130153
}
131154

155+
func testDeleteGroupClearsMembershipAndMarksConnectionDirtyForSync() {
156+
let group = ConnectionGroup(name: "Dev", color: .green)
157+
storage.saveGroups([group])
158+
159+
let connection = DatabaseConnection(name: "Grouped", groupId: group.id)
160+
connectionStorage.addConnection(connection)
161+
tracker.clearAllDirty(.connection)
162+
163+
storage.deleteGroup(group)
164+
165+
let reloaded = connectionStorage.loadConnections()
166+
XCTAssertEqual(reloaded.count, 1)
167+
XCTAssertNil(reloaded[0].groupId)
168+
XCTAssertTrue(tracker.dirtyRecords(for: .connection).contains(connection.id.uuidString))
169+
}
170+
132171
// MARK: - Lookup
133172

134173
func testGroupForId() {

0 commit comments

Comments
 (0)