Skip to content
Merged
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

- Keyboard focus navigation (Tab, Ctrl+J/K/N/P, arrow keys) for connection list, quick switcher, and database switcher
- MongoDB `mongodb+srv://` URI support with SRV toggle, Auth Mechanism dropdown, and Replica Set field (#419)
- Show all available database types in connection form with install status badge (#418)

### Changed

Expand Down
8 changes: 8 additions & 0 deletions TablePro/Core/Plugins/PluginManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -647,6 +647,14 @@ final class PluginManager {
return types.sorted { $0.rawValue < $1.rawValue }
}

var allAvailableDatabaseTypes: [DatabaseType] {
var types = Set(availableDatabaseTypes)
for type in DatabaseType.allKnownTypes {
types.insert(type)
}
return types.sorted { $0.rawValue < $1.rawValue }
}

// MARK: - Driver Availability

func isDriverAvailable(for databaseType: DatabaseType) -> Bool {
Expand Down
73 changes: 67 additions & 6 deletions TablePro/Views/Connection/ConnectionFormView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ struct ConnectionFormView: View { // swiftlint:disable:this type_body_length
private var isNew: Bool { connectionId == nil }

private var availableDatabaseTypes: [DatabaseType] {
PluginManager.shared.availableDatabaseTypes
PluginManager.shared.allAvailableDatabaseTypes
}

private var additionalConnectionFields: [ConnectionField] {
Expand Down Expand Up @@ -126,6 +126,8 @@ struct ConnectionFormView: View { // swiftlint:disable:this type_body_length
@State private var testSucceeded: Bool = false

@State private var pluginInstallConnection: DatabaseConnection?
@State private var isInstallingPlugin: Bool = false
@State private var pluginInstallError: String?

// Tab selection
@State private var selectedTab: FormTab = .general
Expand Down Expand Up @@ -185,6 +187,8 @@ struct ConnectionFormView: View { // swiftlint:disable:this type_body_length
if !visibleTabs.contains(selectedTab) {
selectedTab = .general
}
isInstallingPlugin = false
pluginInstallError = nil
}
.pluginInstallPrompt(connection: $pluginInstallConnection) { connection in
connectAfterInstall(connection)
Expand Down Expand Up @@ -237,9 +241,12 @@ struct ConnectionFormView: View { // swiftlint:disable:this type_body_length
HStack {
Text(t.rawValue)
if t.isDownloadablePlugin && !PluginManager.shared.isDriverLoaded(for: t) {
Image(systemName: "arrow.down.circle")
.foregroundStyle(.secondary)
Text("Not Installed")
.font(.caption)
.foregroundStyle(.secondary)
.padding(.horizontal, 4)
.padding(.vertical, 1)
.background(.quaternary, in: RoundedRectangle(cornerRadius: 3))
}
}
} icon: {
Expand All @@ -251,6 +258,7 @@ struct ConnectionFormView: View { // swiftlint:disable:this type_body_length
.tag(t)
}
}
.disabled(isInstallingPlugin)
TextField(
String(localized: "Name"),
text: $name,
Expand All @@ -263,7 +271,41 @@ struct ConnectionFormView: View { // swiftlint:disable:this type_body_length
}
}

if PluginManager.shared.connectionMode(for: type) == .fileBased {
if type.isDownloadablePlugin && !PluginManager.shared.isDriverLoaded(for: type) {
Section {
LabeledContent(String(localized: "Plugin")) {
if isInstallingPlugin {
HStack(spacing: 6) {
ProgressView()
.controlSize(.small)
Text("Installing…")
.foregroundStyle(.secondary)
}
} else if let error = pluginInstallError {
HStack(spacing: 6) {
Text(error)
.foregroundStyle(.red)
.font(.caption)
.lineLimit(2)
Button("Retry") {
pluginInstallError = nil
installPlugin(for: type)
}
.controlSize(.small)
}
} else {
HStack(spacing: 6) {
Text("Not Installed")
.foregroundStyle(.secondary)
Button("Install") {
installPlugin(for: type)
}
.controlSize(.small)
}
}
}
}
} else if PluginManager.shared.connectionMode(for: type) == .fileBased {
Section(String(localized: "Database File")) {
HStack {
TextField(
Expand Down Expand Up @@ -920,7 +962,7 @@ struct ConnectionFormView: View { // swiftlint:disable:this type_body_length
Text("Test Connection")
}
}
.disabled(isTesting || !isValid)
.disabled(isTesting || isInstallingPlugin || !isValid)

Spacer()

Expand Down Expand Up @@ -952,7 +994,7 @@ struct ConnectionFormView: View { // swiftlint:disable:this type_body_length
}
.keyboardShortcut(.return)
.buttonStyle(.borderedProminent)
.disabled(!isValid)
.disabled(isInstallingPlugin || !isValid)
}
.padding(.horizontal, 16)
.padding(.vertical, 12)
Expand Down Expand Up @@ -1454,6 +1496,25 @@ struct ConnectionFormView: View { // swiftlint:disable:this type_body_length
}
}

private func installPlugin(for databaseType: DatabaseType) {
isInstallingPlugin = true
Task {
do {
try await PluginManager.shared.installMissingPlugin(for: databaseType) { _ in }
if type == databaseType {
for field in PluginManager.shared.additionalConnectionFields(for: databaseType) {
if additionalFieldValues[field.id] == nil, let defaultValue = field.defaultValue {
additionalFieldValues[field.id] = defaultValue
}
}
}
} catch {
pluginInstallError = error.localizedDescription
}
isInstallingPlugin = false
}
}

private func cleanupTestSecrets(for testId: UUID) {
ConnectionStorage.shared.deletePassword(for: testId)
ConnectionStorage.shared.deleteSSHPassword(for: testId)
Expand Down
Loading