Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- `SettablePlugin` protocol in TableProPluginKit SDK: unified settings pattern for all plugins with automatic persistence via `loadSettings()`/`saveSettings()`, replacing duplicated boilerplate across export/import/driver plugins
- Plugin UI/capability metadata: each driver plugin now self-declares brand color, connection mode, supported features, column types, URL schemes, and grouping strategy via the `DriverPlugin` protocol
- Driver plugin settings view support: `DriverPlugin.settingsView()` allows plugins to provide custom settings UI in the Installed Plugins panel
- Dynamic connection fields: connection form Advanced tab now renders fields from `DriverPlugin.additionalConnectionFields` instead of hardcoded per-database sections, with support for text, secure, and dropdown field types
- Dynamic connection fields: connection form Advanced tab now renders fields from `DriverPlugin.additionalConnectionFields` instead of hardcoded per-database sections, with support for text, secure, dropdown, number, toggle, and stepper field types
- Configurable plugin registry URL via `defaults write com.TablePro com.TablePro.customRegistryURL <url>` for enterprise/private registries
- SQL import options (wrap in transaction, disable FK checks) now persist across launches
- `needsRestart` banner persists across app quit/relaunch after plugin uninstall
Expand Down
9 changes: 8 additions & 1 deletion Plugins/RedisDriverPlugin/RedisPlugin.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,14 @@ final class RedisPlugin: NSObject, TableProPlugin, DriverPlugin {
static let databaseDisplayName = "Redis"
static let iconName = "cylinder.fill"
static let defaultPort = 6379
static let additionalConnectionFields: [ConnectionField] = []
static let additionalConnectionFields: [ConnectionField] = [
ConnectionField(
id: "redisDatabase",
label: String(localized: "Database Index"),
defaultValue: "0",
fieldType: .stepper(range: ConnectionField.IntRange(0...15))
),
]
static let additionalDatabaseTypeIds: [String] = []

// MARK: - UI/Capability Metadata
Expand Down
20 changes: 20 additions & 0 deletions Plugins/TableProPluginKit/ConnectionField.swift
Original file line number Diff line number Diff line change
@@ -1,10 +1,30 @@
import Foundation

public struct ConnectionField: Codable, Sendable {
public struct IntRange: Codable, Sendable, Equatable {
public let lowerBound: Int
public let upperBound: Int

public init(_ range: ClosedRange<Int>) {
self.lowerBound = range.lowerBound
self.upperBound = range.upperBound
}

public init(lowerBound: Int, upperBound: Int) {
self.lowerBound = lowerBound
self.upperBound = upperBound
}

public var closedRange: ClosedRange<Int> { lowerBound...upperBound }
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

public enum FieldType: Codable, Sendable, Equatable {
case text
case secure
case dropdown(options: [DropdownOption])
case number
case toggle
case stepper(range: IntRange)
}

public struct DropdownOption: Codable, Sendable, Equatable {
Expand Down
2 changes: 0 additions & 2 deletions TablePro/Core/Database/DatabaseDriver.swift
Original file line number Diff line number Diff line change
Expand Up @@ -359,8 +359,6 @@ enum DatabaseDriverFactory {
switch connection.type {
case .mongodb:
fields["sslCACertPath"] = ssl.caCertificatePath
case .redis:
fields["redisDatabase"] = String(connection.redisDatabase ?? 0)
default:
break
}
Expand Down
24 changes: 24 additions & 0 deletions TablePro/Views/Connection/ConnectionFieldRow.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,30 @@ struct ConnectionFieldRow: View {
Text(option.label).tag(option.value)
}
}
case .number:
TextField(
field.label,
text: $value,
prompt: field.placeholder.isEmpty ? nil : Text(field.placeholder)
)
Comment thread
coderabbitai[bot] marked this conversation as resolved.
case .toggle:
Toggle(
field.label,
isOn: Binding(
get: { value == "true" },
set: { value = $0 ? "true" : "false" }
)
)
case .stepper(let range):
Stepper(
value: Binding(
get: { Int(value) ?? range.lowerBound },
set: { value = String($0) }
),
in: range.closedRange
) {
Text("\(field.label): \(Int(value) ?? range.lowerBound)")
}
}
}
}
30 changes: 11 additions & 19 deletions TablePro/Views/Connection/ConnectionFormView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -650,20 +650,6 @@ struct ConnectionFormView: View {
}
}

if type == .redis {
Section("Redis") {
Stepper(
value: Binding(
get: { Int(database) ?? 0 },
set: { database = String($0) }
),
in: 0...15
) {
Text(String(localized: "Database Index: \(Int(database) ?? 0)"))
}
}
}

Section(String(localized: "Startup Commands")) {
StartupCommandsEditor(text: $startupCommands)
.frame(height: 80)
Expand Down Expand Up @@ -883,9 +869,11 @@ struct ConnectionFormView: View {
}
}

// Load Redis settings (special case)
if existing.type == .redis, let rdb = existing.redisDatabase {
database = String(rdb)
// Migrate legacy Redis database index into additionalFieldValues
if existing.type == .redis,
additionalFieldValues["redisDatabase"] == nil,
let rdb = existing.redisDatabase {
additionalFieldValues["redisDatabase"] = String(rdb)
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
}

// Load startup commands
Expand Down Expand Up @@ -965,7 +953,9 @@ struct ConnectionFormView: View {
groupId: selectedGroupId,
safeModeLevel: safeModeLevel,
aiPolicy: aiPolicy,
redisDatabase: type == .redis ? (Int(database) ?? 0) : nil,
redisDatabase: type == .redis
? Int(additionalFieldValues["redisDatabase"] ?? "0")
: nil,
Comment thread
coderabbitai[bot] marked this conversation as resolved.
startupCommands: startupCommands.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
? nil : startupCommands,
additionalFields: finalAdditionalFields.isEmpty ? nil : finalAdditionalFields
Expand Down Expand Up @@ -1108,7 +1098,9 @@ struct ConnectionFormView: View {
color: connectionColor,
tagId: selectedTagId,
groupId: selectedGroupId,
redisDatabase: type == .redis ? (Int(database) ?? 0) : nil,
redisDatabase: type == .redis
? Int(additionalFieldValues["redisDatabase"] ?? "0")
: nil,
startupCommands: startupCommands.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
? nil : startupCommands,
additionalFields: finalAdditionalFields.isEmpty ? nil : finalAdditionalFields
Expand Down
103 changes: 103 additions & 0 deletions TableProTests/Core/Plugins/ConnectionFieldTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -153,4 +153,107 @@ struct ConnectionFieldTests {
#expect(decoded.id == field.id)
#expect(decoded.fieldType == .text)
}

// MARK: - IntRange

@Test("IntRange init from ClosedRange")
func intRangeFromClosedRange() {
let range = ConnectionField.IntRange(0...15)
#expect(range.lowerBound == 0)
#expect(range.upperBound == 15)
}

@Test("IntRange closedRange round-trip")
func intRangeClosedRangeRoundTrip() {
let range = ConnectionField.IntRange(3...42)
#expect(range.closedRange == 3...42)
}

@Test("IntRange init from bounds")
func intRangeFromBounds() {
let range = ConnectionField.IntRange(lowerBound: 1, upperBound: 100)
#expect(range.lowerBound == 1)
#expect(range.upperBound == 100)
#expect(range.closedRange == 1...100)
}

// MARK: - isSecure for new types

@Test("isSecure is false for .number")
func isSecureForNumber() {
let field = ConnectionField(id: "port", label: "Port", fieldType: .number)
#expect(field.isSecure == false)
}

@Test("isSecure is false for .toggle")
func isSecureForToggle() {
let field = ConnectionField(id: "flag", label: "Flag", fieldType: .toggle)
#expect(field.isSecure == false)
}

@Test("isSecure is false for .stepper")
func isSecureForStepper() {
let range = ConnectionField.IntRange(0...15)
let field = ConnectionField(id: "db", label: "DB", fieldType: .stepper(range: range))
#expect(field.isSecure == false)
}

// MARK: - Codable round-trips for new types

@Test("Codable round-trip for .number field")
func codableNumber() throws {
let field = ConnectionField(
id: "port",
label: "Port",
placeholder: "3306",
defaultValue: "3306",
fieldType: .number
)

let data = try JSONEncoder().encode(field)
let decoded = try JSONDecoder().decode(ConnectionField.self, from: data)

#expect(decoded.id == field.id)
#expect(decoded.label == field.label)
#expect(decoded.placeholder == field.placeholder)
#expect(decoded.defaultValue == field.defaultValue)
#expect(decoded.fieldType == .number)
}

@Test("Codable round-trip for .toggle field")
func codableToggle() throws {
let field = ConnectionField(
id: "compress",
label: "Compress",
defaultValue: "false",
fieldType: .toggle
)

let data = try JSONEncoder().encode(field)
let decoded = try JSONDecoder().decode(ConnectionField.self, from: data)

#expect(decoded.id == field.id)
#expect(decoded.label == field.label)
#expect(decoded.defaultValue == "false")
#expect(decoded.fieldType == .toggle)
}

@Test("Codable round-trip for .stepper field with IntRange")
func codableStepper() throws {
let range = ConnectionField.IntRange(0...15)
let field = ConnectionField(
id: "redisDatabase",
label: "Database Index",
defaultValue: "0",
fieldType: .stepper(range: range)
)

let data = try JSONEncoder().encode(field)
let decoded = try JSONDecoder().decode(ConnectionField.self, from: data)

#expect(decoded.id == field.id)
#expect(decoded.label == field.label)
#expect(decoded.defaultValue == "0")
#expect(decoded.fieldType == .stepper(range: range))
}
}
Loading