Skip to content

Commit 83de4e2

Browse files
authored
Merge pull request #276 from datlechin/feat/settable-plugin-protocol
feat: add SettablePlugin protocol to unify plugin settings
2 parents e3d9fc2 + 7610df3 commit 83de4e2

21 files changed

+138
-146
lines changed

CHANGELOG.md

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

1010
### Added
1111

12+
- `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
1213
- 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
1314
- Driver plugin settings view support: `DriverPlugin.settingsView()` allows plugins to provide custom settings UI in the Installed Plugins panel
1415
- 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

Plugins/CSVExportPlugin/CSVExportOptionsView.swift

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,16 +11,16 @@ struct CSVExportOptionsView: View {
1111
var body: some View {
1212
VStack(alignment: .leading, spacing: 10) {
1313
VStack(alignment: .leading, spacing: 8) {
14-
Toggle("Convert NULL to EMPTY", isOn: $plugin.options.convertNullToEmpty)
14+
Toggle("Convert NULL to EMPTY", isOn: $plugin.settings.convertNullToEmpty)
1515
.toggleStyle(.checkbox)
1616

17-
Toggle("Convert line break to space", isOn: $plugin.options.convertLineBreakToSpace)
17+
Toggle("Convert line break to space", isOn: $plugin.settings.convertLineBreakToSpace)
1818
.toggleStyle(.checkbox)
1919

20-
Toggle("Put field names in the first row", isOn: $plugin.options.includeFieldNames)
20+
Toggle("Put field names in the first row", isOn: $plugin.settings.includeFieldNames)
2121
.toggleStyle(.checkbox)
2222

23-
Toggle("Sanitize formula-like values", isOn: $plugin.options.sanitizeFormulas)
23+
Toggle("Sanitize formula-like values", isOn: $plugin.settings.sanitizeFormulas)
2424
.toggleStyle(.checkbox)
2525
.help("Prevent CSV formula injection by prefixing values starting with =, +, -, @ with a single quote")
2626
}
@@ -30,7 +30,7 @@ struct CSVExportOptionsView: View {
3030

3131
VStack(alignment: .leading, spacing: 10) {
3232
optionRow(String(localized: "Delimiter", bundle: .main)) {
33-
Picker("", selection: $plugin.options.delimiter) {
33+
Picker("", selection: $plugin.settings.delimiter) {
3434
ForEach(CSVDelimiter.allCases) { delimiter in
3535
Text(delimiter.displayName).tag(delimiter)
3636
}
@@ -41,7 +41,7 @@ struct CSVExportOptionsView: View {
4141
}
4242

4343
optionRow(String(localized: "Quote", bundle: .main)) {
44-
Picker("", selection: $plugin.options.quoteHandling) {
44+
Picker("", selection: $plugin.settings.quoteHandling) {
4545
ForEach(CSVQuoteHandling.allCases) { handling in
4646
Text(handling.rawValue).tag(handling)
4747
}
@@ -52,7 +52,7 @@ struct CSVExportOptionsView: View {
5252
}
5353

5454
optionRow(String(localized: "Line break", bundle: .main)) {
55-
Picker("", selection: $plugin.options.lineBreak) {
55+
Picker("", selection: $plugin.settings.lineBreak) {
5656
ForEach(CSVLineBreak.allCases) { lineBreak in
5757
Text(lineBreak.rawValue).tag(lineBreak)
5858
}
@@ -63,7 +63,7 @@ struct CSVExportOptionsView: View {
6363
}
6464

6565
optionRow(String(localized: "Decimal", bundle: .main)) {
66-
Picker("", selection: $plugin.options.decimalFormat) {
66+
Picker("", selection: $plugin.settings.decimalFormat) {
6767
ForEach(CSVDecimalFormat.allCases) { format in
6868
Text(format.rawValue).tag(format)
6969
}

Plugins/CSVExportPlugin/CSVExportPlugin.swift

Lines changed: 11 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import SwiftUI
88
import TableProPluginKit
99

1010
@Observable
11-
final class CSVExportPlugin: ExportFormatPlugin {
11+
final class CSVExportPlugin: ExportFormatPlugin, SettablePlugin {
1212
static let pluginName = "CSV Export"
1313
static let pluginVersion = "1.0.0"
1414
static let pluginDescription = "Export data to CSV format"
@@ -20,19 +20,16 @@ final class CSVExportPlugin: ExportFormatPlugin {
2020
// swiftlint:disable:next force_try
2121
static let decimalFormatRegex = try! NSRegularExpression(pattern: #"^[+-]?\d+\.\d+$"#)
2222

23-
private let storage = PluginSettingsStorage(pluginId: "csv")
23+
typealias Settings = CSVExportOptions
24+
static let settingsStorageId = "csv"
2425

25-
var options = CSVExportOptions() {
26-
didSet { storage.save(options) }
26+
var settings = CSVExportOptions() {
27+
didSet { saveSettings() }
2728
}
2829

29-
required init() {
30-
if let saved = storage.load(CSVExportOptions.self) {
31-
options = saved
32-
}
33-
}
30+
required init() { loadSettings() }
3431

35-
func optionsView() -> AnyView? {
32+
func settingsView() -> AnyView? {
3633
AnyView(CSVExportOptionsView(plugin: self))
3734
}
3835

@@ -45,7 +42,7 @@ final class CSVExportPlugin: ExportFormatPlugin {
4542
let fileHandle = try PluginExportUtilities.createFileHandle(at: destination)
4643
defer { try? fileHandle.close() }
4744

48-
let lineBreak = options.lineBreak.value
45+
let lineBreak = settings.lineBreak.value
4946

5047
for (index, table) in tables.enumerated() {
5148
try progress.checkCancellation()
@@ -73,15 +70,15 @@ final class CSVExportPlugin: ExportFormatPlugin {
7370

7471
if result.rows.isEmpty { break }
7572

76-
var batchOptions = options
73+
var batchSettings = settings
7774
if !isFirstBatch {
78-
batchOptions.includeFieldNames = false
75+
batchSettings.includeFieldNames = false
7976
}
8077

8178
try writeCSVContent(
8279
columns: result.columns,
8380
rows: result.rows,
84-
options: batchOptions,
81+
options: batchSettings,
8582
to: fileHandle,
8683
progress: progress
8784
)
@@ -180,5 +177,4 @@ final class CSVExportPlugin: ExportFormatPlugin {
180177
return processed
181178
}
182179
}
183-
184180
}

Plugins/JSONExportPlugin/JSONExportOptionsView.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,13 @@ struct JSONExportOptionsView: View {
1010

1111
var body: some View {
1212
VStack(alignment: .leading, spacing: 8) {
13-
Toggle("Pretty print (formatted output)", isOn: $plugin.options.prettyPrint)
13+
Toggle("Pretty print (formatted output)", isOn: $plugin.settings.prettyPrint)
1414
.toggleStyle(.checkbox)
1515

16-
Toggle("Include NULL values", isOn: $plugin.options.includeNullValues)
16+
Toggle("Include NULL values", isOn: $plugin.settings.includeNullValues)
1717
.toggleStyle(.checkbox)
1818

19-
Toggle("Preserve all values as strings", isOn: $plugin.options.preserveAllAsStrings)
19+
Toggle("Preserve all values as strings", isOn: $plugin.settings.preserveAllAsStrings)
2020
.toggleStyle(.checkbox)
2121
.help("Keep leading zeros in ZIP codes, phone numbers, and IDs by outputting all values as strings")
2222
}

Plugins/JSONExportPlugin/JSONExportPlugin.swift

Lines changed: 10 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import SwiftUI
88
import TableProPluginKit
99

1010
@Observable
11-
final class JSONExportPlugin: ExportFormatPlugin {
11+
final class JSONExportPlugin: ExportFormatPlugin, SettablePlugin {
1212
static let pluginName = "JSON Export"
1313
static let pluginVersion = "1.0.0"
1414
static let pluginDescription = "Export data to JSON format"
@@ -17,19 +17,16 @@ final class JSONExportPlugin: ExportFormatPlugin {
1717
static let defaultFileExtension = "json"
1818
static let iconName = "curlybraces"
1919

20-
private let storage = PluginSettingsStorage(pluginId: "json")
20+
typealias Settings = JSONExportOptions
21+
static let settingsStorageId = "json"
2122

22-
var options = JSONExportOptions() {
23-
didSet { storage.save(options) }
23+
var settings = JSONExportOptions() {
24+
didSet { saveSettings() }
2425
}
2526

26-
required init() {
27-
if let saved = storage.load(JSONExportOptions.self) {
28-
options = saved
29-
}
30-
}
27+
required init() { loadSettings() }
3128

32-
func optionsView() -> AnyView? {
29+
func settingsView() -> AnyView? {
3330
AnyView(JSONExportOptionsView(plugin: self))
3431
}
3532

@@ -42,7 +39,7 @@ final class JSONExportPlugin: ExportFormatPlugin {
4239
let fileHandle = try PluginExportUtilities.createFileHandle(at: destination)
4340
defer { try? fileHandle.close() }
4441

45-
let prettyPrint = options.prettyPrint
42+
let prettyPrint = settings.prettyPrint
4643
let indent = prettyPrint ? " " : ""
4744
let newline = prettyPrint ? "\n" : ""
4845

@@ -95,7 +92,7 @@ final class JSONExportPlugin: ExportFormatPlugin {
9592
for (colIndex, column) in columns.enumerated() {
9693
if colIndex < row.count {
9794
let value = row[colIndex]
98-
if options.includeNullValues || value != nil {
95+
if settings.includeNullValues || value != nil {
9996
if !isFirstField {
10097
rowString += ", "
10198
}
@@ -104,7 +101,7 @@ final class JSONExportPlugin: ExportFormatPlugin {
104101
let escapedKey = PluginExportUtilities.escapeJSONString(column)
105102
let jsonValue = formatJSONValue(
106103
value,
107-
preserveAsString: options.preserveAllAsStrings
104+
preserveAsString: settings.preserveAllAsStrings
108105
)
109106
rowString += "\"\(escapedKey)\": \(jsonValue)"
110107
}
@@ -167,5 +164,4 @@ final class JSONExportPlugin: ExportFormatPlugin {
167164

168165
return "\"\(PluginExportUtilities.escapeJSONString(val))\""
169166
}
170-
171167
}

Plugins/MQLExportPlugin/MQLExportOptionsView.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ struct MQLExportOptionsView: View {
2626

2727
Spacer()
2828

29-
Picker("", selection: $plugin.options.batchSize) {
29+
Picker("", selection: $plugin.settings.batchSize) {
3030
ForEach(Self.batchSizeOptions, id: \.self) { size in
3131
Text("\(size)")
3232
.tag(size)

Plugins/MQLExportPlugin/MQLExportPlugin.swift

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import SwiftUI
88
import TableProPluginKit
99

1010
@Observable
11-
final class MQLExportPlugin: ExportFormatPlugin {
11+
final class MQLExportPlugin: ExportFormatPlugin, SettablePlugin {
1212
static let pluginName = "MQL Export"
1313
static let pluginVersion = "1.0.0"
1414
static let pluginDescription = "Export data to MongoDB Query Language format"
@@ -24,17 +24,14 @@ final class MQLExportPlugin: ExportFormatPlugin {
2424
PluginExportOptionColumn(id: "data", label: "Data", width: 44)
2525
]
2626

27-
private let storage = PluginSettingsStorage(pluginId: "mql")
27+
typealias Settings = MQLExportOptions
28+
static let settingsStorageId = "mql"
2829

29-
var options = MQLExportOptions() {
30-
didSet { storage.save(options) }
30+
var settings = MQLExportOptions() {
31+
didSet { saveSettings() }
3132
}
3233

33-
required init() {
34-
if let saved = storage.load(MQLExportOptions.self) {
35-
options = saved
36-
}
37-
}
34+
required init() { loadSettings() }
3835

3936
func defaultTableOptionValues() -> [Bool] {
4037
[true, true, true]
@@ -44,7 +41,7 @@ final class MQLExportPlugin: ExportFormatPlugin {
4441
optionValues.contains(true)
4542
}
4643

47-
func optionsView() -> AnyView? {
44+
func settingsView() -> AnyView? {
4845
AnyView(MQLExportOptionsView(plugin: self))
4946
}
5047

@@ -67,7 +64,7 @@ final class MQLExportPlugin: ExportFormatPlugin {
6764
}
6865
try fileHandle.write(contentsOf: "\n".toUTF8Data())
6966

70-
let batchSize = options.batchSize
67+
let batchSize = settings.batchSize
7168

7269
for (index, table) in tables.enumerated() {
7370
try progress.checkCancellation()
@@ -219,5 +216,4 @@ final class MQLExportPlugin: ExportFormatPlugin {
219216
try fileHandle.write(contentsOf: "\(indexContent)\n".toUTF8Data())
220217
}
221218
}
222-
223219
}

Plugins/SQLExportPlugin/SQLExportOptionsView.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ struct SQLExportOptionsView: View {
2626

2727
Spacer()
2828

29-
Picker("", selection: $plugin.options.batchSize) {
29+
Picker("", selection: $plugin.settings.batchSize) {
3030
ForEach(Self.batchSizeOptions, id: \.self) { size in
3131
Text(size == 1 ? String(localized: "1 (no batching)", bundle: .main) : "\(size)")
3232
.tag(size)
@@ -38,7 +38,7 @@ struct SQLExportOptionsView: View {
3838
}
3939
.help("Higher values create fewer INSERT statements, resulting in smaller files and faster imports")
4040

41-
Toggle("Compress the file using Gzip", isOn: $plugin.options.compressWithGzip)
41+
Toggle("Compress the file using Gzip", isOn: $plugin.settings.compressWithGzip)
4242
.toggleStyle(.checkbox)
4343
.font(.system(size: 13))
4444
}

Plugins/SQLExportPlugin/SQLExportPlugin.swift

Lines changed: 11 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import SwiftUI
99
import TableProPluginKit
1010

1111
@Observable
12-
final class SQLExportPlugin: ExportFormatPlugin {
12+
final class SQLExportPlugin: ExportFormatPlugin, SettablePlugin {
1313
static let pluginName = "SQL Export"
1414
static let pluginVersion = "1.0.0"
1515
static let pluginDescription = "Export data to SQL format"
@@ -25,21 +25,18 @@ final class SQLExportPlugin: ExportFormatPlugin {
2525
PluginExportOptionColumn(id: "data", label: "Data", width: 44)
2626
]
2727

28-
private let storage = PluginSettingsStorage(pluginId: "sql")
28+
typealias Settings = SQLExportOptions
29+
static let settingsStorageId = "sql"
2930

30-
var options = SQLExportOptions() {
31-
didSet { storage.save(options) }
31+
var settings = SQLExportOptions() {
32+
didSet { saveSettings() }
3233
}
3334

3435
var ddlFailures: [String] = []
3536

3637
private static let logger = Logger(subsystem: "com.TablePro", category: "SQLExportPlugin")
3738

38-
required init() {
39-
if let saved = storage.load(SQLExportOptions.self) {
40-
options = saved
41-
}
42-
}
39+
required init() { loadSettings() }
4340

4441
func defaultTableOptionValues() -> [Bool] {
4542
[true, true, true]
@@ -50,7 +47,7 @@ final class SQLExportPlugin: ExportFormatPlugin {
5047
}
5148

5249
var currentFileExtension: String {
53-
options.compressWithGzip ? "sql.gz" : "sql"
50+
settings.compressWithGzip ? "sql.gz" : "sql"
5451
}
5552

5653
var warnings: [String] {
@@ -59,7 +56,7 @@ final class SQLExportPlugin: ExportFormatPlugin {
5956
return ["Could not fetch table structure for: \(failedTables)"]
6057
}
6158

62-
func optionsView() -> AnyView? {
59+
func settingsView() -> AnyView? {
6360
AnyView(SQLExportOptionsView(plugin: self))
6461
}
6562

@@ -75,7 +72,7 @@ final class SQLExportPlugin: ExportFormatPlugin {
7572
let targetURL: URL
7673
let tempFileURL: URL?
7774

78-
if options.compressWithGzip {
75+
if settings.compressWithGzip {
7976
let tempURL = FileManager.default.temporaryDirectory
8077
.appendingPathComponent(UUID().uuidString + ".sql")
8178
tempFileURL = tempURL
@@ -171,7 +168,7 @@ final class SQLExportPlugin: ExportFormatPlugin {
171168
}
172169

173170
if includeData {
174-
let batchSize = options.batchSize
171+
let batchSize = settings.batchSize
175172
var offset = 0
176173
var wroteAnyRows = false
177174

@@ -218,7 +215,7 @@ final class SQLExportPlugin: ExportFormatPlugin {
218215
}
219216

220217
// Handle gzip compression
221-
if options.compressWithGzip, let tempURL = tempFileURL {
218+
if settings.compressWithGzip, let tempURL = tempFileURL {
222219
progress.setStatus("Compressing...")
223220

224221
do {

Plugins/SQLImportPlugin/SQLImportOptionsView.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,13 @@ struct SQLImportOptionsView: View {
1010

1111
var body: some View {
1212
VStack(alignment: .leading, spacing: 12) {
13-
Toggle("Wrap in transaction (BEGIN/COMMIT)", isOn: Bindable(plugin).options.wrapInTransaction)
13+
Toggle("Wrap in transaction (BEGIN/COMMIT)", isOn: Bindable(plugin).settings.wrapInTransaction)
1414
.font(.system(size: 13))
1515
.help(
1616
"Execute all statements in a single transaction. If any statement fails, all changes are rolled back."
1717
)
1818

19-
Toggle("Disable foreign key checks", isOn: Bindable(plugin).options.disableForeignKeyChecks)
19+
Toggle("Disable foreign key checks", isOn: Bindable(plugin).settings.disableForeignKeyChecks)
2020
.font(.system(size: 13))
2121
.help(
2222
"Temporarily disable foreign key constraints during import. Useful for importing data with circular dependencies."

0 commit comments

Comments
 (0)