From d39f9ea482b0c7c18852c50332e424d3b4ec6a63 Mon Sep 17 00:00:00 2001 From: Ngo Quoc Dat Date: Tue, 10 Mar 2026 08:36:24 +0700 Subject: [PATCH 1/5] fix: update import tests to match current ImportState properties --- .../Core/Services/ImportStateTests.swift | 18 +++++-------- .../Import/ImportServiceStateTests.swift | 26 +++++++------------ 2 files changed, 17 insertions(+), 27 deletions(-) diff --git a/TableProTests/Core/Services/ImportStateTests.swift b/TableProTests/Core/Services/ImportStateTests.swift index f4418eab..73365ea0 100644 --- a/TableProTests/Core/Services/ImportStateTests.swift +++ b/TableProTests/Core/Services/ImportStateTests.swift @@ -16,9 +16,8 @@ struct ImportStateTests { let state = ImportState() #expect(state.isImporting == false) #expect(state.progress == 0.0) - #expect(state.currentStatement == "") - #expect(state.currentStatementIndex == 0) - #expect(state.totalStatements == 0) + #expect(state.processedStatements == 0) + #expect(state.estimatedTotalStatements == 0) #expect(state.statusMessage == "") #expect(state.errorMessage == nil) } @@ -46,7 +45,7 @@ struct ImportStateTests { #expect(state.isImporting == true) #expect(state.progress == 0.0) - #expect(state.currentStatement == "") + #expect(state.processedStatements == 0) #expect(state.errorMessage == nil) } @@ -60,14 +59,11 @@ struct ImportStateTests { state.progress = 0.75 #expect(state.progress == 0.75) - state.currentStatement = "CREATE TABLE test" - #expect(state.currentStatement == "CREATE TABLE test") + state.processedStatements = 5 + #expect(state.processedStatements == 5) - state.currentStatementIndex = 5 - #expect(state.currentStatementIndex == 5) - - state.totalStatements = 20 - #expect(state.totalStatements == 20) + state.estimatedTotalStatements = 20 + #expect(state.estimatedTotalStatements == 20) state.statusMessage = "Importing..." #expect(state.statusMessage == "Importing...") diff --git a/TableProTests/Views/Import/ImportServiceStateTests.swift b/TableProTests/Views/Import/ImportServiceStateTests.swift index 7cf3fc23..bdadc909 100644 --- a/TableProTests/Views/Import/ImportServiceStateTests.swift +++ b/TableProTests/Views/Import/ImportServiceStateTests.swift @@ -20,9 +20,8 @@ struct ImportServiceStateTests { #expect(state.service == nil) #expect(state.isImporting == false) - #expect(state.currentStatement == "") - #expect(state.currentStatementIndex == 0) - #expect(state.totalStatements == 0) + #expect(state.processedStatements == 0) + #expect(state.estimatedTotalStatements == 0) #expect(state.statusMessage == "") } @@ -36,18 +35,16 @@ struct ImportServiceStateTests { service.state = ImportState( isImporting: true, - currentStatement: "CREATE TABLE users", - currentStatementIndex: 3, - totalStatements: 10, + processedStatements: 3, + estimatedTotalStatements: 10, statusMessage: "Importing..." ) state.setService(service) #expect(state.isImporting == true) - #expect(state.currentStatement == "CREATE TABLE users") - #expect(state.currentStatementIndex == 3) - #expect(state.totalStatements == 10) + #expect(state.processedStatements == 3) + #expect(state.estimatedTotalStatements == 10) #expect(state.statusMessage == "Importing...") } @@ -62,18 +59,15 @@ struct ImportServiceStateTests { state.setService(service) #expect(state.isImporting == false) - #expect(state.currentStatement == "") service.state.isImporting = true - service.state.currentStatement = "INSERT INTO orders" - service.state.currentStatementIndex = 7 - service.state.totalStatements = 20 + service.state.processedStatements = 7 + service.state.estimatedTotalStatements = 20 service.state.statusMessage = "Processing statements..." #expect(state.isImporting == true) - #expect(state.currentStatement == "INSERT INTO orders") - #expect(state.currentStatementIndex == 7) - #expect(state.totalStatements == 20) + #expect(state.processedStatements == 7) + #expect(state.estimatedTotalStatements == 20) #expect(state.statusMessage == "Processing statements...") } } From 0c5ae8d414bea7a8bb2e327e8c38d73f8b70ccab Mon Sep 17 00:00:00 2001 From: Ngo Quoc Dat Date: Tue, 10 Mar 2026 08:36:29 +0700 Subject: [PATCH 2/5] perf: load plugins eagerly at startup without blocking app launch --- TablePro/AppDelegate.swift | 9 ++++++++- TablePro/Views/Connection/ConnectionFormView.swift | 1 - TablePro/Views/Export/ExportDialog.swift | 1 - TablePro/Views/Import/ImportDialog.swift | 1 - .../Views/Settings/Plugins/InstalledPluginsView.swift | 3 --- 5 files changed, 8 insertions(+), 7 deletions(-) diff --git a/TablePro/AppDelegate.swift b/TablePro/AppDelegate.swift index 11655f0d..f20d982f 100644 --- a/TablePro/AppDelegate.swift +++ b/TablePro/AppDelegate.swift @@ -612,9 +612,16 @@ class AppDelegate: NSObject, NSApplicationDelegate { // Enable native macOS window tabbing (Finder/Safari-style tabs) NSWindow.allowsAutomaticWindowTabbing = true - // Load plugins (driver plugins, etc.) before any connections are created + // Discover plugins synchronously (fast — just reads Info.plist) PluginManager.shared.loadAllPlugins() + // Load plugin bundles on next run loop iteration so it doesn't block app launch. + // By the time the user opens a connection form, plugins will be loaded. + // DatabaseDriverFactory has a fallback loadPendingPlugins() if needed. + Task { @MainActor in + PluginManager.shared.loadPendingPlugins() + } + // Start license periodic validation Task { @MainActor in LicenseManager.shared.startPeriodicValidation() diff --git a/TablePro/Views/Connection/ConnectionFormView.swift b/TablePro/Views/Connection/ConnectionFormView.swift index 97785b2a..1aaf7730 100644 --- a/TablePro/Views/Connection/ConnectionFormView.swift +++ b/TablePro/Views/Connection/ConnectionFormView.swift @@ -139,7 +139,6 @@ struct ConnectionFormView: View { isNew ? String(localized: "New Connection") : String(localized: "Edit Connection") ) .onAppear { - PluginManager.shared.loadPendingPlugins() loadConnectionData() loadSSHConfig() } diff --git a/TablePro/Views/Export/ExportDialog.swift b/TablePro/Views/Export/ExportDialog.swift index afec5710..49801e01 100644 --- a/TablePro/Views/Export/ExportDialog.swift +++ b/TablePro/Views/Export/ExportDialog.swift @@ -63,7 +63,6 @@ struct ExportDialog: View { .frame(width: dialogWidth) .background(Color(nsColor: .windowBackgroundColor)) .onAppear { - PluginManager.shared.loadPendingPlugins() let available = availableFormats if !available.contains(where: { type(of: $0).formatId == config.formatId }) { if let first = available.first { diff --git a/TablePro/Views/Import/ImportDialog.swift b/TablePro/Views/Import/ImportDialog.swift index c0b11d10..0e870ff6 100644 --- a/TablePro/Views/Import/ImportDialog.swift +++ b/TablePro/Views/Import/ImportDialog.swift @@ -67,7 +67,6 @@ struct ImportDialog: View { } .background(Color(nsColor: .windowBackgroundColor)) .onAppear { - PluginManager.shared.loadPendingPlugins() let available = availableFormats if !available.contains(where: { type(of: $0).formatId == selectedFormatId }) { if let first = available.first { diff --git a/TablePro/Views/Settings/Plugins/InstalledPluginsView.swift b/TablePro/Views/Settings/Plugins/InstalledPluginsView.swift index 93110d8a..507dc199 100644 --- a/TablePro/Views/Settings/Plugins/InstalledPluginsView.swift +++ b/TablePro/Views/Settings/Plugins/InstalledPluginsView.swift @@ -81,9 +81,6 @@ struct InstalledPluginsView: View { } return true } - .onAppear { - pluginManager.loadPendingPlugins() - } .alert(errorAlertTitle, isPresented: $showErrorAlert) { Button("OK") {} } message: { From 2f639993e43de512cd36383df731838986c71376 Mon Sep 17 00:00:00 2001 From: Ngo Quoc Dat Date: Tue, 10 Mar 2026 08:38:56 +0700 Subject: [PATCH 3/5] refactor: combine plugin discover and load into single loadPlugins method --- TablePro/AppDelegate.swift | 11 ++--------- TablePro/Core/Plugins/PluginManager.swift | 14 ++++++++++++-- 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/TablePro/AppDelegate.swift b/TablePro/AppDelegate.swift index f20d982f..7b21e4be 100644 --- a/TablePro/AppDelegate.swift +++ b/TablePro/AppDelegate.swift @@ -612,15 +612,8 @@ class AppDelegate: NSObject, NSApplicationDelegate { // Enable native macOS window tabbing (Finder/Safari-style tabs) NSWindow.allowsAutomaticWindowTabbing = true - // Discover plugins synchronously (fast — just reads Info.plist) - PluginManager.shared.loadAllPlugins() - - // Load plugin bundles on next run loop iteration so it doesn't block app launch. - // By the time the user opens a connection form, plugins will be loaded. - // DatabaseDriverFactory has a fallback loadPendingPlugins() if needed. - Task { @MainActor in - PluginManager.shared.loadPendingPlugins() - } + // Discover and load plugins (discovery is synchronous, bundle loading is deferred) + PluginManager.shared.loadPlugins() // Start license periodic validation Task { @MainActor in diff --git a/TablePro/Core/Plugins/PluginManager.swift b/TablePro/Core/Plugins/PluginManager.swift index 43273b71..953166f5 100644 --- a/TablePro/Core/Plugins/PluginManager.swift +++ b/TablePro/Core/Plugins/PluginManager.swift @@ -45,7 +45,16 @@ final class PluginManager { // MARK: - Loading - func loadAllPlugins() { + /// Discover and load all plugins. Discovery is synchronous (reads Info.plist), + /// then bundle loading is deferred to the next run loop iteration so it doesn't block app launch. + func loadPlugins() { + discoverAllPlugins() + Task { @MainActor in + self.loadPendingPlugins() + } + } + + private func discoverAllPlugins() { let fm = FileManager.default if !fm.fileExists(atPath: userPluginsDir.path) { do { @@ -65,7 +74,7 @@ final class PluginManager { } /// Load all discovered but not-yet-loaded plugin bundles. - /// Called on first driver request or when the plugins settings screen opens. + /// Safety fallback for code paths that need plugins before the deferred Task completes. func loadPendingPlugins() { guard !pendingPluginURLs.isEmpty else { return } let pending = pendingPluginURLs @@ -285,6 +294,7 @@ final class PluginManager { // MARK: - Driver Availability func isDriverAvailable(for databaseType: DatabaseType) -> Bool { + // Safety fallback: loads pending plugins if the deferred startup Task hasn't completed yet loadPendingPlugins() return driverPlugins[databaseType.pluginTypeId] != nil } From b93c683b6dd5b525cb37cc452fe1cc70bbb8ce02 Mon Sep 17 00:00:00 2001 From: Ngo Quoc Dat Date: Tue, 10 Mar 2026 08:50:17 +0700 Subject: [PATCH 4/5] refactor: replace inline error messages with native NSAlert sheets in ConnectionFormView --- TablePro/Resources/Localizable.xcstrings | 3 + .../Views/Connection/ConnectionFormView.swift | 57 +++++++------------ 2 files changed, 25 insertions(+), 35 deletions(-) diff --git a/TablePro/Resources/Localizable.xcstrings b/TablePro/Resources/Localizable.xcstrings index 63762007..5727f98c 100644 --- a/TablePro/Resources/Localizable.xcstrings +++ b/TablePro/Resources/Localizable.xcstrings @@ -3748,6 +3748,9 @@ } } } + }, + "Connection Test Failed" : { + }, "Connection URL" : { "localizations" : { diff --git a/TablePro/Views/Connection/ConnectionFormView.swift b/TablePro/Views/Connection/ConnectionFormView.swift index 1aaf7730..bab2fbd9 100644 --- a/TablePro/Views/Connection/ConnectionFormView.swift +++ b/TablePro/Views/Connection/ConnectionFormView.swift @@ -89,7 +89,7 @@ struct ConnectionFormView: View { @State private var isInstallingPlugin = false @State private var pluginInstallProgress: Double = 0 - @State private var showPluginInstallError: String? + @State private var pluginInstallConnection: DatabaseConnection? // Tab selection @@ -102,7 +102,6 @@ struct ConnectionFormView: View { enum TestResult { case success - case failure(String) } private enum FormTab: String, CaseIterable { @@ -679,32 +678,6 @@ struct ConnectionFormView: View { .padding(.top, 8) } - if case .failure(let message) = testResult { - HStack(alignment: .top, spacing: 6) { - Image(systemName: "exclamationmark.triangle.fill") - .foregroundStyle(.red) - Text(message) - .font(.caption) - .foregroundStyle(.red) - .fixedSize(horizontal: false, vertical: true) - } - .padding(.horizontal, 16) - .padding(.top, 8) - } - - if let pluginError = showPluginInstallError { - HStack(alignment: .top, spacing: 6) { - Image(systemName: "exclamationmark.triangle.fill") - .foregroundStyle(.red) - Text(pluginError) - .font(.caption) - .foregroundStyle(.red) - .fixedSize(horizontal: false, vertical: true) - } - .padding(.horizontal, 16) - .padding(.top, 8) - } - HStack { // Test connection Button(action: testConnection) { @@ -785,7 +758,6 @@ struct ConnectionFormView: View { private var testResultIcon: String { switch testResult { case .success: return "checkmark.circle.fill" - case .failure: return "xmark.circle.fill" case .none: return "bolt.horizontal" } } @@ -793,7 +765,6 @@ struct ConnectionFormView: View { private var testResultColor: Color { switch testResult { case .success: return .green - case .failure: return .red case .none: return .secondary } } @@ -801,7 +772,6 @@ struct ConnectionFormView: View { private func installPluginForType(_ databaseType: DatabaseType) { isInstallingPlugin = true pluginInstallProgress = 0 - showPluginInstallError = nil Task { do { @@ -811,7 +781,11 @@ struct ConnectionFormView: View { isInstallingPlugin = false } catch { isInstallingPlugin = false - showPluginInstallError = error.localizedDescription + AlertHelper.showErrorSheet( + title: String(localized: "Plugin Installation Failed"), + message: error.localizedDescription, + window: NSApp.keyWindow + ) } } } @@ -1079,13 +1053,26 @@ struct ConnectionFormView: View { testConn, sshPassword: sshPassword) await MainActor.run { isTesting = false - testResult = - success ? .success : .failure(String(localized: "Connection test failed")) + if success { + testResult = .success + } else { + testResult = nil + AlertHelper.showErrorSheet( + title: String(localized: "Connection Test Failed"), + message: String(localized: "Connection test failed"), + window: NSApp.keyWindow + ) + } } } catch { await MainActor.run { isTesting = false - testResult = .failure(error.localizedDescription) + testResult = nil + AlertHelper.showErrorSheet( + title: String(localized: "Connection Test Failed"), + message: error.localizedDescription, + window: NSApp.keyWindow + ) } } } From 171aa238494de029fd87feb44035f8631ee7b518 Mon Sep 17 00:00:00 2001 From: Ngo Quoc Dat Date: Tue, 10 Mar 2026 08:53:01 +0700 Subject: [PATCH 5/5] fix: capture window ref before async, simplify TestResult to bool, add Vietnamese localization --- TablePro/Resources/Localizable.xcstrings | 9 ++++- .../Views/Connection/ConnectionFormView.swift | 38 +++++-------------- 2 files changed, 18 insertions(+), 29 deletions(-) diff --git a/TablePro/Resources/Localizable.xcstrings b/TablePro/Resources/Localizable.xcstrings index 5727f98c..2e807fe4 100644 --- a/TablePro/Resources/Localizable.xcstrings +++ b/TablePro/Resources/Localizable.xcstrings @@ -3750,7 +3750,14 @@ } }, "Connection Test Failed" : { - + "localizations" : { + "vi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Kiểm tra kết nối thất bại" + } + } + } }, "Connection URL" : { "localizations" : { diff --git a/TablePro/Views/Connection/ConnectionFormView.swift b/TablePro/Views/Connection/ConnectionFormView.swift index bab2fbd9..131eb4c8 100644 --- a/TablePro/Views/Connection/ConnectionFormView.swift +++ b/TablePro/Views/Connection/ConnectionFormView.swift @@ -85,7 +85,7 @@ struct ConnectionFormView: View { @State private var startupCommands: String = "" @State private var isTesting: Bool = false - @State private var testResult: TestResult? + @State private var testSucceeded: Bool = false @State private var isInstallingPlugin = false @State private var pluginInstallProgress: Double = 0 @@ -100,10 +100,6 @@ struct ConnectionFormView: View { // MARK: - Enums - enum TestResult { - case success - } - private enum FormTab: String, CaseIterable { case general = "General" case ssh = "SSH Tunnel" @@ -686,8 +682,8 @@ struct ConnectionFormView: View { ProgressView() .controlSize(.small) } else { - Image(systemName: testResultIcon) - .foregroundStyle(testResultColor) + Image(systemName: testSucceeded ? "checkmark.circle.fill" : "bolt.horizontal") + .foregroundStyle(testSucceeded ? .green : .secondary) } Text("Test Connection") } @@ -755,23 +751,10 @@ struct ConnectionFormView: View { return basicValid } - private var testResultIcon: String { - switch testResult { - case .success: return "checkmark.circle.fill" - case .none: return "bolt.horizontal" - } - } - - private var testResultColor: Color { - switch testResult { - case .success: return .green - case .none: return .secondary - } - } - private func installPluginForType(_ databaseType: DatabaseType) { isInstallingPlugin = true pluginInstallProgress = 0 + let window = NSApp.keyWindow Task { do { @@ -784,7 +767,7 @@ struct ConnectionFormView: View { AlertHelper.showErrorSheet( title: String(localized: "Plugin Installation Failed"), message: error.localizedDescription, - window: NSApp.keyWindow + window: window ) } } @@ -985,7 +968,8 @@ struct ConnectionFormView: View { func testConnection() { isTesting = true - testResult = nil + testSucceeded = false + let window = NSApp.keyWindow // Build SSH config let sshConfig = SSHConfiguration( @@ -1054,24 +1038,22 @@ struct ConnectionFormView: View { await MainActor.run { isTesting = false if success { - testResult = .success + testSucceeded = true } else { - testResult = nil AlertHelper.showErrorSheet( title: String(localized: "Connection Test Failed"), message: String(localized: "Connection test failed"), - window: NSApp.keyWindow + window: window ) } } } catch { await MainActor.run { isTesting = false - testResult = nil AlertHelper.showErrorSheet( title: String(localized: "Connection Test Failed"), message: error.localizedDescription, - window: NSApp.keyWindow + window: window ) } }