-
-
Notifications
You must be signed in to change notification settings - Fork 98
feat: connection groups with multi-selection and hierarchical management #140
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
3ff40ec
4c5d8e2
e00e14e
7aab5b9
1f961ca
1af3852
f7ef8ce
2fd698b
b76144f
2d2bfb7
6c797af
1e9bb1d
334b5fa
185d2af
deb5c35
4c71fc0
f96489f
864f32a
bd1effe
d311858
5169968
20e713e
6d14b40
6022d60
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -12,6 +12,7 @@ final class GroupStorage { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private static let logger = Logger(subsystem: "com.TablePro", category: "GroupStorage") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private let groupsKey = "com.TablePro.groups" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private let expandedGroupsKey = "com.TablePro.expandedGroups" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private let defaults = UserDefaults.standard | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private let encoder = JSONEncoder() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private let decoder = JSONDecoder() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -27,13 +28,26 @@ final class GroupStorage { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| do { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return try decoder.decode([ConnectionGroup].self, from: data) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| var groups = try decoder.decode([ConnectionGroup].self, from: data) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| migrateSortOrderIfNeeded(&groups) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return groups | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } catch { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Self.logger.error("Failed to load groups: \(error)") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return [] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// Assign sequential sortOrder when all items have default 0 (legacy migration). | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private func migrateSortOrderIfNeeded(_ groups: inout [ConnectionGroup]) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| guard groups.count > 1, groups.allSatisfy({ $0.sortOrder == 0 }) else { return } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for index in groups.indices { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| groups[index].sortOrder = index | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| saveGroups(groups) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let count = groups.count | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Self.logger.info("Migrated sortOrder for \(count) groups") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// Save all groups | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| func saveGroups(_ groups: [ConnectionGroup]) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| do { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -45,14 +59,22 @@ final class GroupStorage { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// Add a new group | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| func addGroup(_ group: ConnectionGroup) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// Add a new group (rejects case-insensitive duplicate names among siblings). | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// Returns `true` if the group was added, `false` if a sibling with the same name exists. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @discardableResult | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| func addGroup(_ group: ConnectionGroup) -> Bool { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| var groups = loadGroups() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| guard !groups.contains(where: { $0.name.lowercased() == group.name.lowercased() }) else { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let hasDuplicate = groups.contains { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| $0.parentGroupId == group.parentGroupId | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| && $0.name.caseInsensitiveCompare(group.name) == .orderedSame | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if hasDuplicate { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Self.logger.debug("Ignoring attempt to add duplicate group name: \(group.name, privacy: .public)") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return false | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| groups.append(group) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| saveGroups(groups) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return true | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// Update an existing group | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -64,16 +86,101 @@ final class GroupStorage { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// Delete a group | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// Delete a group and all its descendants, including their connections. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| func deleteGroup(_ group: ConnectionGroup) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| SyncChangeTracker.shared.markDeleted(.group, id: group.id.uuidString) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| var groups = loadGroups() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| groups.removeAll { $0.id == group.id } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let deletedIds = collectDescendantIds(of: group.id, in: groups) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let allDeletedIds = deletedIds.union([group.id]) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Remove deleted groups | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| groups.removeAll { allDeletedIds.contains($0.id) } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| saveGroups(groups) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Delete connections that belonged to deleted groups | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let storage = ConnectionStorage.shared | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let connections = storage.loadConnections() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| var remaining: [DatabaseConnection] = [] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for conn in connections { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if let gid = conn.groupId, allDeletedIds.contains(gid) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Clean up keychain entries | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| storage.deletePassword(for: conn.id) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| storage.deleteSSHPassword(for: conn.id) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| storage.deleteKeyPassphrase(for: conn.id) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| remaining.append(conn) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| storage.saveConnections(remaining) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+100
to
+115
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Group deletion misses connection tombstones and TOTP keychain cleanup. In Lines 105–114, deleted connections are removed from persisted data, but:
This can leave stale synced records and orphaned secrets. 🛠️ Suggested fix for conn in connections {
if let gid = conn.groupId, allDeletedIds.contains(gid) {
+ SyncChangeTracker.shared.markDeleted(.connection, id: conn.id.uuidString)
// Clean up keychain entries
storage.deletePassword(for: conn.id)
storage.deleteSSHPassword(for: conn.id)
storage.deleteKeyPassphrase(for: conn.id)
+ storage.deleteTOTPSecret(for: conn.id)
} else {
remaining.append(conn)
}
}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// Count all connections inside a group and its descendants. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| func connectionCount(for group: ConnectionGroup) -> Int { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let allGroups = loadGroups() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let descendantIds = collectDescendantIds(of: group.id, in: allGroups) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let allGroupIds = descendantIds.union([group.id]) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let connections = ConnectionStorage.shared.loadConnections() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return connections.filter { conn in | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| guard let gid = conn.groupId else { return false } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return allGroupIds.contains(gid) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }.count | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// Get group by ID | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| func group(for id: UUID) -> ConnectionGroup? { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| loadGroups().first { $0.id == id } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// Get child groups of a parent, sorted by sortOrder | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| func childGroups(of parentId: UUID?) -> [ConnectionGroup] { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| loadGroups() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .filter { $0.parentGroupId == parentId } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .sorted { $0.sortOrder < $1.sortOrder } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// Get the next sort order for a new item in a parent context | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| func nextSortOrder(parentId: UUID?) -> Int { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let siblings = loadGroups().filter { $0.parentGroupId == parentId } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return (siblings.map(\.sortOrder).max() ?? -1) + 1 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // MARK: - Expanded State | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// Load the set of expanded group IDs | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| func loadExpandedGroupIds() -> Set<UUID> { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| guard let data = defaults.data(forKey: expandedGroupsKey) else { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return [] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| do { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let ids = try decoder.decode([UUID].self, from: data) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return Set(ids) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } catch { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Self.logger.error("Failed to load expanded groups: \(error)") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return [] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// Save the set of expanded group IDs | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| func saveExpandedGroupIds(_ ids: Set<UUID>) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| do { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let data = try encoder.encode(Array(ids)) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| defaults.set(data, forKey: expandedGroupsKey) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } catch { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Self.logger.error("Failed to save expanded groups: \(error)") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // MARK: - Helpers | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /// Recursively collect all descendant group IDs | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| func collectDescendantIds(of groupId: UUID, in groups: [ConnectionGroup]) -> Set<UUID> { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| var result = Set<UUID>() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let children = groups.filter { $0.parentGroupId == groupId } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for child in children { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| result.insert(child.id) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| result.formUnion(collectDescendantIds(of: child.id, in: groups)) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+177
to
+183
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Descendant traversal needs cycle protection.
🛡️ Suggested fix- func collectDescendantIds(of groupId: UUID, in groups: [ConnectionGroup]) -> Set<UUID> {
- var result = Set<UUID>()
- let children = groups.filter { $0.parentGroupId == groupId }
- for child in children {
- result.insert(child.id)
- result.formUnion(collectDescendantIds(of: child.id, in: groups))
- }
- return result
- }
+ private func collectDescendantIds(of groupId: UUID, in groups: [ConnectionGroup]) -> Set<UUID> {
+ var visited: Set<UUID> = []
+ return collectDescendantIds(of: groupId, in: groups, visited: &visited)
+ }
+
+ private func collectDescendantIds(
+ of groupId: UUID,
+ in groups: [ConnectionGroup],
+ visited: inout Set<UUID>
+ ) -> Set<UUID> {
+ guard visited.insert(groupId).inserted else { return [] }
+ var result = Set<UUID>()
+ let children = groups.filter { $0.parentGroupId == groupId }
+ for child in children {
+ result.insert(child.id)
+ result.formUnion(collectDescendantIds(of: child.id, in: groups, visited: &visited))
+ }
+ return result
+ }🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return result | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -81,7 +81,7 @@ struct ConnectionFormView: View { // swiftlint:disable:this type_body_length | |||||||||||||||||||||||||||||||||||||
| @State private var sslClientCertPath: String = "" | ||||||||||||||||||||||||||||||||||||||
| @State private var sslClientKeyPath: String = "" | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| // Color and Tag | ||||||||||||||||||||||||||||||||||||||
| // Color, Tag, and Group | ||||||||||||||||||||||||||||||||||||||
| @State private var connectionColor: ConnectionColor = .none | ||||||||||||||||||||||||||||||||||||||
| @State private var selectedTagId: UUID? | ||||||||||||||||||||||||||||||||||||||
| @State private var selectedGroupId: UUID? | ||||||||||||||||||||||||||||||||||||||
|
|
@@ -1053,7 +1053,10 @@ struct ConnectionFormView: View { // swiftlint:disable:this type_body_length | |||||||||||||||||||||||||||||||||||||
| connectToDatabase(connectionToSave) | ||||||||||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||||||||||
| if let index = savedConnections.firstIndex(where: { $0.id == connectionToSave.id }) { | ||||||||||||||||||||||||||||||||||||||
| savedConnections[index] = connectionToSave | ||||||||||||||||||||||||||||||||||||||
| // Preserve sortOrder from existing connection | ||||||||||||||||||||||||||||||||||||||
| var updated = connectionToSave | ||||||||||||||||||||||||||||||||||||||
| updated.sortOrder = savedConnections[index].sortOrder | ||||||||||||||||||||||||||||||||||||||
| savedConnections[index] = updated | ||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+1056
to
+1059
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Preserving At Line 1058, 🔧 Suggested fix if let index = savedConnections.firstIndex(where: { $0.id == connectionToSave.id }) {
- // Preserve sortOrder from existing connection
var updated = connectionToSave
- updated.sortOrder = savedConnections[index].sortOrder
+ let previous = savedConnections[index]
+ if previous.groupId == updated.groupId {
+ updated.sortOrder = previous.sortOrder
+ } else {
+ let nextSortOrder = (
+ savedConnections
+ .filter { $0.id != updated.id && $0.groupId == updated.groupId }
+ .map(\.sortOrder)
+ .max() ?? -1
+ ) + 1
+ updated.sortOrder = nextSortOrder
+ }
savedConnections[index] = updated
storage.saveConnections(savedConnections)
SyncChangeTracker.shared.markDirty(.connection, id: connectionToSave.id.uuidString)
}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||
| storage.saveConnections(savedConnections) | ||||||||||||||||||||||||||||||||||||||
| SyncChangeTracker.shared.markDirty(.connection, id: connectionToSave.id.uuidString) | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Move this entry to
[Unreleased]instead of[0.14.1].Line 239 documents a new feature in a historical release section. This should be listed under
## [Unreleased](### Added) until release.📘 Suggested changelog placement
As per coding guidelines: "Update CHANGELOG.md under [Unreleased] section (Added/Fixed/Changed) for new features and notable changes."
📝 Committable suggestion
🤖 Prompt for AI Agents