Skip to content

Commit b9764dd

Browse files
authored
fix(sync): propagate connection group membership changes to other devices (#1477)
* fix(sync): propagate connection group membership changes to other devices * refactor(connections): route reorder through updateConnections and batch dirty marking
1 parent ceb4edb commit b9764dd

5 files changed

Lines changed: 65 additions & 23 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/ConnectionStorage.swift

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -189,9 +189,10 @@ final class ConnectionStorage {
189189
guard didMutate, saveConnections(connections) else {
190190
return false
191191
}
192-
for connection in updatesById.values where !connection.localOnly && !connection.isSample {
193-
syncTracker.markDirty(.connection, id: connection.id.uuidString)
194-
}
192+
let dirtyIds = updatesById.values
193+
.filter { !$0.localOnly && !$0.isSample }
194+
.map { $0.id.uuidString }
195+
syncTracker.markDirty(.connection, ids: dirtyIds)
195196
return true
196197
}
197198

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: 12 additions & 14 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
@@ -549,26 +553,23 @@ final class WelcomeViewModel {
549553

550554
let updatedValidGroupIds = Set(groups.map(\.id))
551555
var order = 0
552-
var dirtyIds: [String] = []
556+
var updated: [DatabaseConnection] = []
553557
for i in connections.indices {
554558
let isUngrouped = connections[i].groupId.map { !updatedValidGroupIds.contains($0) } ?? true
555559
if isUngrouped {
556560
if connections[i].sortOrder != order {
557561
connections[i].sortOrder = order
558-
dirtyIds.append(connections[i].id.uuidString)
562+
updated.append(connections[i])
559563
}
560564
order += 1
561565
}
562566
}
563567

564-
guard storage.saveConnections(connections) else {
568+
guard storage.updateConnections(updated) else {
565569
connections = storage.loadConnections()
566570
rebuildTree()
567571
return
568572
}
569-
if !dirtyIds.isEmpty {
570-
services.syncTracker.markDirty(.connection, ids: dirtyIds)
571-
}
572573
rebuildTree()
573574
}
574575

@@ -591,23 +592,20 @@ final class WelcomeViewModel {
591592
connections.move(fromOffsets: globalSource, toOffset: globalDestination)
592593

593594
var order = 0
594-
var dirtyIds: [String] = []
595+
var updated: [DatabaseConnection] = []
595596
for i in connections.indices where connections[i].groupId == group.id {
596597
if connections[i].sortOrder != order {
597598
connections[i].sortOrder = order
598-
dirtyIds.append(connections[i].id.uuidString)
599+
updated.append(connections[i])
599600
}
600601
order += 1
601602
}
602603

603-
guard storage.saveConnections(connections) else {
604+
guard storage.updateConnections(updated) else {
604605
connections = storage.loadConnections()
605606
rebuildTree()
606607
return
607608
}
608-
if !dirtyIds.isEmpty {
609-
services.syncTracker.markDirty(.connection, ids: dirtyIds)
610-
}
611609
rebuildTree()
612610
}
613611

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)