Skip to content
Merged
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
4 changes: 2 additions & 2 deletions TablePro/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -612,8 +612,8 @@ 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
PluginManager.shared.loadAllPlugins()
// Discover and load plugins (discovery is synchronous, bundle loading is deferred)
PluginManager.shared.loadPlugins()

// Start license periodic validation
Task { @MainActor in
Expand Down
14 changes: 12 additions & 2 deletions TablePro/Core/Plugins/PluginManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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
Expand Down Expand Up @@ -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
}
Expand Down
10 changes: 10 additions & 0 deletions TablePro/Resources/Localizable.xcstrings
Original file line number Diff line number Diff line change
Expand Up @@ -3749,6 +3749,16 @@
}
}
},
"Connection Test Failed" : {
"localizations" : {
"vi" : {
"stringUnit" : {
"state" : "translated",
"value" : "Kiểm tra kết nối thất bại"
}
}
}
},
"Connection URL" : {
"localizations" : {
"vi" : {
Expand Down
84 changes: 26 additions & 58 deletions TablePro/Views/Connection/ConnectionFormView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -85,11 +85,11 @@ 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
@State private var showPluginInstallError: String?

@State private var pluginInstallConnection: DatabaseConnection?

// Tab selection
Expand All @@ -100,11 +100,6 @@ struct ConnectionFormView: View {

// MARK: - Enums

enum TestResult {
case success
case failure(String)
}

private enum FormTab: String, CaseIterable {
case general = "General"
case ssh = "SSH Tunnel"
Expand Down Expand Up @@ -139,7 +134,6 @@ struct ConnectionFormView: View {
isNew ? String(localized: "New Connection") : String(localized: "Edit Connection")
)
.onAppear {
PluginManager.shared.loadPendingPlugins()
loadConnectionData()
loadSSHConfig()
}
Expand Down Expand Up @@ -680,32 +674,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) {
Expand All @@ -714,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")
}
Expand Down Expand Up @@ -783,26 +751,10 @@ struct ConnectionFormView: View {
return basicValid
}

private var testResultIcon: String {
switch testResult {
case .success: return "checkmark.circle.fill"
case .failure: return "xmark.circle.fill"
case .none: return "bolt.horizontal"
}
}

private var testResultColor: Color {
switch testResult {
case .success: return .green
case .failure: return .red
case .none: return .secondary
}
}

private func installPluginForType(_ databaseType: DatabaseType) {
isInstallingPlugin = true
pluginInstallProgress = 0
showPluginInstallError = nil
let window = NSApp.keyWindow

Task {
do {
Expand All @@ -812,7 +764,11 @@ struct ConnectionFormView: View {
isInstallingPlugin = false
} catch {
isInstallingPlugin = false
showPluginInstallError = error.localizedDescription
AlertHelper.showErrorSheet(
title: String(localized: "Plugin Installation Failed"),
message: error.localizedDescription,
window: window
)
}
}
}
Expand Down Expand Up @@ -1012,7 +968,8 @@ struct ConnectionFormView: View {

func testConnection() {
isTesting = true
testResult = nil
testSucceeded = false
let window = NSApp.keyWindow

// Build SSH config
let sshConfig = SSHConfiguration(
Expand Down Expand Up @@ -1080,13 +1037,24 @@ struct ConnectionFormView: View {
testConn, sshPassword: sshPassword)
await MainActor.run {
isTesting = false
testResult =
success ? .success : .failure(String(localized: "Connection test failed"))
if success {
testSucceeded = true
} else {
AlertHelper.showErrorSheet(
title: String(localized: "Connection Test Failed"),
message: String(localized: "Connection test failed"),
window: window
)
}
}
} catch {
await MainActor.run {
isTesting = false
testResult = .failure(error.localizedDescription)
AlertHelper.showErrorSheet(
title: String(localized: "Connection Test Failed"),
message: error.localizedDescription,
window: window
)
}
}
}
Expand Down
1 change: 0 additions & 1 deletion TablePro/Views/Export/ExportDialog.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
1 change: 0 additions & 1 deletion TablePro/Views/Import/ImportDialog.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
3 changes: 0 additions & 3 deletions TablePro/Views/Settings/Plugins/InstalledPluginsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -81,9 +81,6 @@ struct InstalledPluginsView: View {
}
return true
}
.onAppear {
pluginManager.loadPendingPlugins()
}
.alert(errorAlertTitle, isPresented: $showErrorAlert) {
Button("OK") {}
} message: {
Expand Down
18 changes: 7 additions & 11 deletions TableProTests/Core/Services/ImportStateTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down Expand Up @@ -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)
}

Expand All @@ -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...")
Expand Down
26 changes: 10 additions & 16 deletions TableProTests/Views/Import/ImportServiceStateTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 == "")
}

Expand All @@ -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...")
}

Expand All @@ -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...")
}
}