From 8d03d38d766db19de779fb31c3d496402b86cbb0 Mon Sep 17 00:00:00 2001 From: Ngo Quoc Dat Date: Sat, 28 Mar 2026 19:25:56 +0700 Subject: [PATCH] fix: preserve SSH profile on sync round-trip and save fallback config --- TablePro/Core/Storage/SSHProfileStorage.swift | 2 +- TablePro/Core/Sync/SyncRecordMapper.swift | 5 +++ .../Connection/ConnectionGroupTree.swift | 5 +-- .../Views/Connection/ConnectionFormView.swift | 41 +++++++++++-------- 4 files changed, 31 insertions(+), 22 deletions(-) diff --git a/TablePro/Core/Storage/SSHProfileStorage.swift b/TablePro/Core/Storage/SSHProfileStorage.swift index 73a1ff1c..6a3f2e2e 100644 --- a/TablePro/Core/Storage/SSHProfileStorage.swift +++ b/TablePro/Core/Storage/SSHProfileStorage.swift @@ -14,7 +14,7 @@ final class SSHProfileStorage { private let defaults = UserDefaults.standard private let encoder = JSONEncoder() private let decoder = JSONDecoder() - private var lastLoadFailed = false + private(set) var lastLoadFailed = false private init() {} diff --git a/TablePro/Core/Sync/SyncRecordMapper.swift b/TablePro/Core/Sync/SyncRecordMapper.swift index 15dfe67e..5a6c3c1e 100644 --- a/TablePro/Core/Sync/SyncRecordMapper.swift +++ b/TablePro/Core/Sync/SyncRecordMapper.swift @@ -83,6 +83,9 @@ struct SyncRecordMapper { if let startupCommands = connection.startupCommands { record["startupCommands"] = startupCommands as CKRecordValue } + if let sshProfileId = connection.sshProfileId { + record["sshProfileId"] = sshProfileId.uuidString as CKRecordValue + } // Encode complex structs as JSON Data do { @@ -130,6 +133,7 @@ struct SyncRecordMapper { let aiPolicyRaw = record["aiPolicy"] as? String let redisDatabase = (record["redisDatabase"] as? Int64).map { Int($0) } let startupCommands = record["startupCommands"] as? String + let sshProfileId = (record["sshProfileId"] as? String).flatMap { UUID(uuidString: $0) } var sshConfig = SSHConfiguration() if let sshData = record["sshConfigJson"] as? Data { @@ -159,6 +163,7 @@ struct SyncRecordMapper { color: ConnectionColor(rawValue: colorRaw) ?? .none, tagId: tagId, groupId: groupId, + sshProfileId: sshProfileId, safeModeLevel: SafeModeLevel(rawValue: safeModeLevelRaw) ?? .silent, aiPolicy: aiPolicyRaw.flatMap { AIConnectionPolicy(rawValue: $0) }, redisDatabase: redisDatabase, diff --git a/TablePro/Models/Connection/ConnectionGroupTree.swift b/TablePro/Models/Connection/ConnectionGroupTree.swift index bf2dee4c..527fda13 100644 --- a/TablePro/Models/Connection/ConnectionGroupTree.swift +++ b/TablePro/Models/Connection/ConnectionGroupTree.swift @@ -5,7 +5,7 @@ import Foundation -enum ConnectionGroupTreeNode: Identifiable, Hashable { +enum ConnectionGroupTreeNode: Identifiable { case group(ConnectionGroup, children: [ConnectionGroupTreeNode]) case connection(DatabaseConnection) @@ -15,9 +15,6 @@ enum ConnectionGroupTreeNode: Identifiable, Hashable { case .connection(let c): "conn-\(c.id)" } } - - static func == (lhs: Self, rhs: Self) -> Bool { lhs.id == rhs.id } - func hash(into hasher: inout Hasher) { hasher.combine(id) } } // MARK: - Tree Building diff --git a/TablePro/Views/Connection/ConnectionFormView.swift b/TablePro/Views/Connection/ConnectionFormView.swift index 929317a2..2a930e64 100644 --- a/TablePro/Views/Connection/ConnectionFormView.swift +++ b/TablePro/Views/Connection/ConnectionFormView.swift @@ -613,8 +613,9 @@ struct ConnectionFormView: View { // swiftlint:disable:this type_body_length private func reloadProfiles() { sshProfiles = SSHProfileStorage.shared.loadProfiles() - // If the edited/deleted profile no longer exists, clear the selection - if let id = sshProfileId, !sshProfiles.contains(where: { $0.id == id }) { + if let id = sshProfileId, + !SSHProfileStorage.shared.lastLoadFailed, + !sshProfiles.contains(where: { $0.id == id }) { sshProfileId = nil } } @@ -1176,21 +1177,27 @@ struct ConnectionFormView: View { // swiftlint:disable:this type_body_length } private func saveConnection() { - let sshConfig = SSHConfiguration( - enabled: sshEnabled, - host: sshHost, - port: Int(sshPort) ?? 22, - username: sshUsername, - authMethod: sshAuthMethod, - privateKeyPath: sshPrivateKeyPath, - useSSHConfig: !selectedSSHConfigHost.isEmpty, - agentSocketPath: resolvedSSHAgentSocketPath, - jumpHosts: jumpHosts, - totpMode: totpMode, - totpAlgorithm: totpAlgorithm, - totpDigits: totpDigits, - totpPeriod: totpPeriod - ) + let sshConfig: SSHConfiguration + if let profileId = sshProfileId, + let profile = sshProfiles.first(where: { $0.id == profileId }) { + sshConfig = profile.toSSHConfiguration() + } else { + sshConfig = SSHConfiguration( + enabled: sshEnabled, + host: sshHost, + port: Int(sshPort) ?? 22, + username: sshUsername, + authMethod: sshAuthMethod, + privateKeyPath: sshPrivateKeyPath, + useSSHConfig: !selectedSSHConfigHost.isEmpty, + agentSocketPath: resolvedSSHAgentSocketPath, + jumpHosts: jumpHosts, + totpMode: totpMode, + totpAlgorithm: totpAlgorithm, + totpDigits: totpDigits, + totpPeriod: totpPeriod + ) + } let sslConfig = SSLConfiguration( mode: sslMode,