-
-
Notifications
You must be signed in to change notification settings - Fork 298
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
Closed
Closed
Changes from 8 commits
Commits
Show all changes
24 commits
Select commit
Hold shift + click to select a range
3ff40ec
feat: add connection groups
imhuytq 4c5d8e2
update
imhuytq e00e14e
chore: update localization strings
imhuytq 7aab5b9
fix
imhuytq 1f961ca
Merge branch 'main' into feat/connection-groups
imhuytq 1af3852
feat: support nested group selection in form sheet and context menus
imhuytq f7ef8ce
docs: add connection groups to changelog
imhuytq 2fd698b
refactor: rename ConnectionGroupEditor to ConnectionGroupPicker, fix …
imhuytq b76144f
fix: address Copilot PR review comments and add backspace delete support
imhuytq 2d2bfb7
feat: multi-selection, 2-step delete confirmation, and drag-drop impr…
imhuytq 6c797af
Merge branch 'main' into feat/connection-groups
imhuytq 1e9bb1d
docs: add dedicated download page with direct DMG links
datlechin 334b5fa
fix: reset pbxproj to main and regenerate Localizable.xcstrings
imhuytq 185d2af
Merge branch 'main' into feat/connection-groups
imhuytq deb5c35
fix: regenerate Localizable.xcstrings after merge
imhuytq 4c71fc0
fix: address PR review issues in connection groups
datlechin f96489f
fix: sortOrder migration for legacy data, multi-drag position, and fo…
imhuytq 864f32a
fix: group drag-drop position when reordering after last group
imhuytq bd1effe
fix: correct drag-drop reorder position when moving items forward
datlechin d311858
Merge remote-tracking branch 'origin/main' into feat/connection-groups
datlechin 5169968
merge: remove stale PR files superseded by main's architecture
datlechin 20e713e
merge: remove accidentally added submodules
datlechin 6d14b40
merge: add sortOrder to DatabaseConnection, use main's ConnectionGrou…
datlechin 6022d60
merge: fix sortOrder parameter ordering in DatabaseConnection init calls
datlechin File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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() | ||
|
|
@@ -47,9 +48,6 @@ final class GroupStorage { | |
| /// Add a new group | ||
| func addGroup(_ group: ConnectionGroup) { | ||
| var groups = loadGroups() | ||
|
datlechin marked this conversation as resolved.
|
||
| guard !groups.contains(where: { $0.name.lowercased() == group.name.lowercased() }) else { | ||
| return | ||
| } | ||
| groups.append(group) | ||
| saveGroups(groups) | ||
| } | ||
|
|
@@ -63,15 +61,87 @@ final class GroupStorage { | |
| } | ||
| } | ||
|
|
||
| /// Delete a group | ||
| /// Delete a group and all its descendants. | ||
| /// Member connections become ungrouped. | ||
| func deleteGroup(_ group: ConnectionGroup) { | ||
| 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) | ||
|
|
||
| // Ungroup connections that belonged to deleted groups | ||
| let storage = ConnectionStorage.shared | ||
| var connections = storage.loadConnections() | ||
| var changed = false | ||
| for index in connections.indices { | ||
| if let gid = connections[index].groupId, allDeletedIds.contains(gid) { | ||
| connections[index].groupId = nil | ||
| changed = true | ||
| } | ||
| } | ||
| if changed { | ||
| storage.saveConnections(connections) | ||
| } | ||
| } | ||
|
|
||
| /// 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)") | ||
| } | ||
| } | ||
|
datlechin marked this conversation as resolved.
|
||
|
|
||
| // MARK: - Helpers | ||
|
|
||
| /// Recursively collect all descendant group IDs | ||
| private 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 | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.