Skip to content

Commit dd8bc55

Browse files
authored
feat: show all database types in connection form with install status badge (#418) (#428)
* feat: show all database types in connection form with install status badge (#418) * feat: show install prompt when selecting uninstalled database type * feat: add loading spinner during plugin install in connection form * fix: show error message when plugin install fails and add retry button * refactor: use ContentUnavailableView for uninstalled plugin state in connection form * refactor: use inline LabeledContent for plugin install state in connection form * fix: reset install state on type change and consistent casing * fix: disable form controls during plugin install and seed field defaults after install
1 parent 1ebf8bf commit dd8bc55

3 files changed

Lines changed: 76 additions & 6 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1111

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

1516
### Changed
1617

TablePro/Core/Plugins/PluginManager.swift

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -647,6 +647,14 @@ final class PluginManager {
647647
return types.sorted { $0.rawValue < $1.rawValue }
648648
}
649649

650+
var allAvailableDatabaseTypes: [DatabaseType] {
651+
var types = Set(availableDatabaseTypes)
652+
for type in DatabaseType.allKnownTypes {
653+
types.insert(type)
654+
}
655+
return types.sorted { $0.rawValue < $1.rawValue }
656+
}
657+
650658
// MARK: - Driver Availability
651659

652660
func isDriverAvailable(for databaseType: DatabaseType) -> Bool {

TablePro/Views/Connection/ConnectionFormView.swift

Lines changed: 67 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ struct ConnectionFormView: View { // swiftlint:disable:this type_body_length
2727
private var isNew: Bool { connectionId == nil }
2828

2929
private var availableDatabaseTypes: [DatabaseType] {
30-
PluginManager.shared.availableDatabaseTypes
30+
PluginManager.shared.allAvailableDatabaseTypes
3131
}
3232

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

128128
@State private var pluginInstallConnection: DatabaseConnection?
129+
@State private var isInstallingPlugin: Bool = false
130+
@State private var pluginInstallError: String?
129131

130132
// Tab selection
131133
@State private var selectedTab: FormTab = .general
@@ -185,6 +187,8 @@ struct ConnectionFormView: View { // swiftlint:disable:this type_body_length
185187
if !visibleTabs.contains(selectedTab) {
186188
selectedTab = .general
187189
}
190+
isInstallingPlugin = false
191+
pluginInstallError = nil
188192
}
189193
.pluginInstallPrompt(connection: $pluginInstallConnection) { connection in
190194
connectAfterInstall(connection)
@@ -237,9 +241,12 @@ struct ConnectionFormView: View { // swiftlint:disable:this type_body_length
237241
HStack {
238242
Text(t.rawValue)
239243
if t.isDownloadablePlugin && !PluginManager.shared.isDriverLoaded(for: t) {
240-
Image(systemName: "arrow.down.circle")
241-
.foregroundStyle(.secondary)
244+
Text("Not Installed")
242245
.font(.caption)
246+
.foregroundStyle(.secondary)
247+
.padding(.horizontal, 4)
248+
.padding(.vertical, 1)
249+
.background(.quaternary, in: RoundedRectangle(cornerRadius: 3))
243250
}
244251
}
245252
} icon: {
@@ -251,6 +258,7 @@ struct ConnectionFormView: View { // swiftlint:disable:this type_body_length
251258
.tag(t)
252259
}
253260
}
261+
.disabled(isInstallingPlugin)
254262
TextField(
255263
String(localized: "Name"),
256264
text: $name,
@@ -263,7 +271,41 @@ struct ConnectionFormView: View { // swiftlint:disable:this type_body_length
263271
}
264272
}
265273

266-
if PluginManager.shared.connectionMode(for: type) == .fileBased {
274+
if type.isDownloadablePlugin && !PluginManager.shared.isDriverLoaded(for: type) {
275+
Section {
276+
LabeledContent(String(localized: "Plugin")) {
277+
if isInstallingPlugin {
278+
HStack(spacing: 6) {
279+
ProgressView()
280+
.controlSize(.small)
281+
Text("Installing…")
282+
.foregroundStyle(.secondary)
283+
}
284+
} else if let error = pluginInstallError {
285+
HStack(spacing: 6) {
286+
Text(error)
287+
.foregroundStyle(.red)
288+
.font(.caption)
289+
.lineLimit(2)
290+
Button("Retry") {
291+
pluginInstallError = nil
292+
installPlugin(for: type)
293+
}
294+
.controlSize(.small)
295+
}
296+
} else {
297+
HStack(spacing: 6) {
298+
Text("Not Installed")
299+
.foregroundStyle(.secondary)
300+
Button("Install") {
301+
installPlugin(for: type)
302+
}
303+
.controlSize(.small)
304+
}
305+
}
306+
}
307+
}
308+
} else if PluginManager.shared.connectionMode(for: type) == .fileBased {
267309
Section(String(localized: "Database File")) {
268310
HStack {
269311
TextField(
@@ -920,7 +962,7 @@ struct ConnectionFormView: View { // swiftlint:disable:this type_body_length
920962
Text("Test Connection")
921963
}
922964
}
923-
.disabled(isTesting || !isValid)
965+
.disabled(isTesting || isInstallingPlugin || !isValid)
924966

925967
Spacer()
926968

@@ -952,7 +994,7 @@ struct ConnectionFormView: View { // swiftlint:disable:this type_body_length
952994
}
953995
.keyboardShortcut(.return)
954996
.buttonStyle(.borderedProminent)
955-
.disabled(!isValid)
997+
.disabled(isInstallingPlugin || !isValid)
956998
}
957999
.padding(.horizontal, 16)
9581000
.padding(.vertical, 12)
@@ -1454,6 +1496,25 @@ struct ConnectionFormView: View { // swiftlint:disable:this type_body_length
14541496
}
14551497
}
14561498

1499+
private func installPlugin(for databaseType: DatabaseType) {
1500+
isInstallingPlugin = true
1501+
Task {
1502+
do {
1503+
try await PluginManager.shared.installMissingPlugin(for: databaseType) { _ in }
1504+
if type == databaseType {
1505+
for field in PluginManager.shared.additionalConnectionFields(for: databaseType) {
1506+
if additionalFieldValues[field.id] == nil, let defaultValue = field.defaultValue {
1507+
additionalFieldValues[field.id] = defaultValue
1508+
}
1509+
}
1510+
}
1511+
} catch {
1512+
pluginInstallError = error.localizedDescription
1513+
}
1514+
isInstallingPlugin = false
1515+
}
1516+
}
1517+
14571518
private func cleanupTestSecrets(for testId: UUID) {
14581519
ConnectionStorage.shared.deletePassword(for: testId)
14591520
ConnectionStorage.shared.deleteSSHPassword(for: testId)

0 commit comments

Comments
 (0)