Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion TablePro/Core/Storage/ConnectionStorage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -358,6 +358,10 @@ private struct StoredConnection: Codable {
// AI policy
let aiPolicy: String?

// Group
let groupId: String?
let sortOrder: Int

init(from connection: DatabaseConnection) {
self.id = connection.id
self.name = connection.name
Expand Down Expand Up @@ -392,6 +396,10 @@ private struct StoredConnection: Codable {

// AI policy
self.aiPolicy = connection.aiPolicy?.rawValue

// Group
self.groupId = connection.groupId?.uuidString
self.sortOrder = connection.sortOrder
}

// Custom decoder to handle migration from old format
Expand Down Expand Up @@ -428,6 +436,8 @@ private struct StoredConnection: Codable {
groupId = try container.decodeIfPresent(String.self, forKey: .groupId)
isReadOnly = try container.decodeIfPresent(Bool.self, forKey: .isReadOnly) ?? false
aiPolicy = try container.decodeIfPresent(String.self, forKey: .aiPolicy)
groupId = try container.decodeIfPresent(String.self, forKey: .groupId)
sortOrder = try container.decodeIfPresent(Int.self, forKey: .sortOrder) ?? 0
}

func toConnection() -> DatabaseConnection {
Expand All @@ -452,6 +462,7 @@ private struct StoredConnection: Codable {
let parsedTagId = tagId.flatMap { UUID(uuidString: $0) }
let parsedGroupId = groupId.flatMap { UUID(uuidString: $0) }
let parsedAIPolicy = aiPolicy.flatMap { AIConnectionPolicy(rawValue: $0) }
let parsedGroupId = groupId.flatMap { UUID(uuidString: $0) }

return DatabaseConnection(
id: id,
Expand All @@ -467,7 +478,9 @@ private struct StoredConnection: Codable {
tagId: parsedTagId,
groupId: parsedGroupId,
isReadOnly: isReadOnly,
aiPolicy: parsedAIPolicy
aiPolicy: parsedAIPolicy,
groupId: parsedGroupId,
sortOrder: sortOrder
)
}
}
80 changes: 75 additions & 5 deletions TablePro/Core/Storage/GroupStorage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -47,9 +48,6 @@ final class GroupStorage {
/// Add a new group
func addGroup(_ group: ConnectionGroup) {
var groups = loadGroups()
guard !groups.contains(where: { $0.name.lowercased() == group.name.lowercased() }) else {
return
}
groups.append(group)
saveGroups(groups)
}
Expand All @@ -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)")
}
}

// 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))
}
return result
}
}
29 changes: 27 additions & 2 deletions TablePro/Models/ConnectionGroup.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,40 @@

import Foundation

/// A named group (folder) for organizing database connections
/// A group for organizing database connections into folders
struct ConnectionGroup: Identifiable, Hashable, Codable {
let id: UUID
var name: String
var color: ConnectionColor
var parentGroupId: UUID?
var sortOrder: Int

init(id: UUID = UUID(), name: String, color: ConnectionColor = .none) {
init(
id: UUID = UUID(),
name: String,
color: ConnectionColor = .blue,
parentGroupId: UUID? = nil,
sortOrder: Int = 0
) {
self.id = id
self.name = name
self.color = color
self.parentGroupId = parentGroupId
self.sortOrder = sortOrder
}

// MARK: - Codable (Migration Support)

enum CodingKeys: String, CodingKey {
case id, name, color, parentGroupId, sortOrder
}

init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
id = try container.decode(UUID.self, forKey: .id)
name = try container.decode(String.self, forKey: .name)
color = try container.decodeIfPresent(ConnectionColor.self, forKey: .color) ?? .blue
parentGroupId = try container.decodeIfPresent(UUID.self, forKey: .parentGroupId)
sortOrder = try container.decodeIfPresent(Int.self, forKey: .sortOrder) ?? 0
}
}
8 changes: 7 additions & 1 deletion TablePro/Models/DatabaseConnection.swift
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,8 @@ struct DatabaseConnection: Identifiable, Hashable {
var aiPolicy: AIConnectionPolicy?
var mongoReadPreference: String?
var mongoWriteConcern: String?
var groupId: UUID?
var sortOrder: Int

init(
id: UUID = UUID(),
Expand All @@ -270,7 +272,9 @@ struct DatabaseConnection: Identifiable, Hashable {
isReadOnly: Bool = false,
aiPolicy: AIConnectionPolicy? = nil,
mongoReadPreference: String? = nil,
mongoWriteConcern: String? = nil
mongoWriteConcern: String? = nil,
groupId: UUID? = nil,
sortOrder: Int = 0
) {
self.id = id
self.name = name
Expand All @@ -288,6 +292,8 @@ struct DatabaseConnection: Identifiable, Hashable {
self.aiPolicy = aiPolicy
self.mongoReadPreference = mongoReadPreference
self.mongoWriteConcern = mongoWriteConcern
self.groupId = groupId
self.sortOrder = sortOrder
}

/// Returns the display color (custom color or database type color)
Expand Down
61 changes: 52 additions & 9 deletions TablePro/Resources/Localizable.xcstrings
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@
}
},
"(%@)" : {
"extractionState" : "stale",
"localizations" : {
"vi" : {
"stringUnit" : {
Expand Down Expand Up @@ -289,6 +290,18 @@
}
}
},
"%@%@" : {
"comment" : "A button label that includes a dot representing the group's color, followed by the group's name",
"isCommentAutoGenerated" : true,
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "new",
"value" : "%1$@%2$@"
}
}
}
},
"%@ms" : {
"localizations" : {
"vi" : {
Expand Down Expand Up @@ -1225,6 +1238,10 @@
}
}
},
"Are you sure you want to delete \"%@\"? Connections will be ungrouped." : {
"comment" : "A confirmation dialog message asking the user to confirm the deletion of a connection group.",
"isCommentAutoGenerated" : true
},
"Are you sure you want to disconnect from this database?" : {
"localizations" : {
"vi" : {
Expand Down Expand Up @@ -2396,12 +2413,10 @@
}
}
}
},
"Create New Group" : {

},
"Create New Group..." : {

"comment" : "A menu item that allows users to create a new connection group.",
"isCommentAutoGenerated" : true
},
"Create New Tag" : {
"localizations" : {
Expand Down Expand Up @@ -2959,7 +2974,8 @@
}
},
"Delete Group" : {

"comment" : "A confirmation dialog title for deleting a database connection group.",
"isCommentAutoGenerated" : true
},
"Delete Index" : {
"extractionState" : "stale",
Expand Down Expand Up @@ -3234,6 +3250,14 @@
}
}
},
"Edit Group" : {
"comment" : "A label indicating that the view is for editing a group.",
"isCommentAutoGenerated" : true
},
"Edit Group..." : {
"comment" : "A button label for editing an existing group.",
"isCommentAutoGenerated" : true
},
"Edit Provider" : {
"localizations" : {
"vi" : {
Expand Down Expand Up @@ -4311,10 +4335,12 @@
}
},
"Group" : {

"comment" : "A label for editing the user's connection group.",
"isCommentAutoGenerated" : true
},
"Group name" : {

"comment" : "A label for the text field that lets them enter a name for the group.",
"isCommentAutoGenerated" : true
},
"Help improve TablePro by sharing anonymous usage statistics (no personal data or queries)." : {
"localizations" : {
Expand Down Expand Up @@ -5061,6 +5087,10 @@
}
}
},
"Manage Groups" : {
"comment" : "A menu header that allows users to manage their connection groups.",
"isCommentAutoGenerated" : true
},
"Manage Tags" : {
"localizations" : {
"vi" : {
Expand Down Expand Up @@ -5229,6 +5259,10 @@
}
}
},
"Move to Group" : {
"comment" : "A menu title that allows users to move a connection to a different group.",
"isCommentAutoGenerated" : true
},
"Move Up" : {
"extractionState" : "stale",
"localizations" : {
Expand Down Expand Up @@ -5347,7 +5381,12 @@
}
},
"New Group" : {

"comment" : "A title for a form sheet used to create a new connection group.",
"isCommentAutoGenerated" : true
},
"New Group..." : {
"comment" : "A menu item that allows users to create a new group.",
"isCommentAutoGenerated" : true
},
"New query tab" : {
"extractionState" : "stale",
Expand Down Expand Up @@ -5381,6 +5420,10 @@
}
}
},
"New Subgroup..." : {
"comment" : "A menu item that allows creating a new subgroup within an existing group.",
"isCommentAutoGenerated" : true
},
"New Tab" : {
"localizations" : {
"vi" : {
Expand Down Expand Up @@ -9171,5 +9214,5 @@
}
}
},
"version" : "1.0"
"version" : "1.1"
}
Loading