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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Redis connects to Amazon ElastiCache with IAM auth (access key, profile, or SSO). TablePro generates the IAM token and uses it as the password; set the AWS region and cache name and enable TLS. (#1567)
- AWS SSO connections can sign in from TablePro: when the SSO session has expired, a prompt opens the AWS sign-in page in your browser and refreshes the cached token. (#1567)
- Cassandra connects to Amazon Keyspaces with AWS IAM (SigV4) auth, using access keys, a profile, or SSO. Set Authentication to an AWS IAM mode and the region, and enable TLS. (#1567)
- Export and import dialogs reopen with the last-used format, options, and encoding. Option changes are kept only when the export or import completes, so cancelling no longer overwrites your saved settings, and a Reset to Defaults button restores the stock options. (#1591)

### Changed

Expand Down
4 changes: 4 additions & 0 deletions Plugins/CSVExportPlugin/CSVExportPlugin.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@ final class CSVExportPlugin: ExportFormatPlugin, SettablePlugin {
AnyView(CSVExportOptionsView(plugin: self))
}

func resetSettingsToDefaults() {
settings = CSVExportOptions()
}

func export(
tables: [PluginExportTable],
dataSource: any PluginExportDataSource,
Expand Down
4 changes: 4 additions & 0 deletions Plugins/JSONExportPlugin/JSONExportPlugin.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ final class JSONExportPlugin: ExportFormatPlugin, SettablePlugin {
AnyView(JSONExportOptionsView(plugin: self))
}

func resetSettingsToDefaults() {
settings = JSONExportOptions()
}

func export(
tables: [PluginExportTable],
dataSource: any PluginExportDataSource,
Expand Down
4 changes: 4 additions & 0 deletions Plugins/JSONImportPlugin/JSONImportPlugin.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ final class JSONImportPlugin: ImportFormatPlugin, SettablePlugin {
AnyView(JSONImportOptionsView(plugin: self))
}

func resetSettingsToDefaults() {
settings = JSONImportOptions()
}

func performImport(
source: any PluginImportSource,
sink: any PluginImportDataSink,
Expand Down
4 changes: 4 additions & 0 deletions Plugins/MQLExportPlugin/MQLExportPlugin.swift
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,10 @@ final class MQLExportPlugin: ExportFormatPlugin, SettablePlugin {
AnyView(MQLExportOptionsView(plugin: self))
}

func resetSettingsToDefaults() {
settings = MQLExportOptions()
}

func export(
tables: [PluginExportTable],
dataSource: any PluginExportDataSource,
Expand Down
4 changes: 4 additions & 0 deletions Plugins/SQLExportPlugin/SQLExportPlugin.swift
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,10 @@
AnyView(SQLExportOptionsView(plugin: self))
}

func resetSettingsToDefaults() {
settings = SQLExportOptions()
}

func export(
tables: [PluginExportTable],
dataSource: any PluginExportDataSource,
Expand Down Expand Up @@ -456,7 +460,7 @@
switch element {
case .header(let header):
columns = header.columns
columnTypeNames = header.columnTypeNames ?? []

Check warning on line 463 in Plugins/SQLExportPlugin/SQLExportPlugin.swift

View workflow job for this annotation

GitHub Actions / macOS App Tests

left side of nil coalescing operator '??' has non-optional type '[String]', so the right side is never used
case .rows(let rows):
for row in rows {
rowBatch.append(row)
Expand Down
4 changes: 4 additions & 0 deletions Plugins/SQLImportPlugin/SQLImportPlugin.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@ final class SQLImportPlugin: ImportFormatPlugin, SettablePlugin {
AnyView(SQLImportOptionsView(plugin: self))
}

func resetSettingsToDefaults() {
settings = SQLImportOptions()
}

func performImport(
source: any PluginImportSource,
sink: any PluginImportDataSink,
Expand Down
20 changes: 20 additions & 0 deletions Plugins/TableProPluginKit/SettablePlugin.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,17 @@ import SwiftUI
/// Type-erased witness for runtime discovery (needed because SettablePlugin has associated type).
public protocol SettablePluginDiscoverable: AnyObject {
func settingsView() -> AnyView?
func snapshotSettingsData() -> Data?
func restoreSettingsData(_ data: Data)
func resetSettingsToDefaults()
Comment on lines +12 to +14

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Bump the plugin kit ABI version

Adding these requirements changes the SettablePluginDiscoverable witness table that export/import plugins compiled against kit 18 provide, but PluginManager.currentPluginKitVersion and the bundled Info.plist kit declarations still remain at 18. In an installation with an existing third-party settable export/import plugin built against the previous kit 18, version validation will still accept and load it, and the new dialog paths call snapshotSettingsData/resetSettingsToDefaults through this existential, which can dispatch against a stale witness table instead of forcing the plugin to rebuild against the new protocol. Please bump the plugin kit version and bundled plugin declarations so incompatible plugins are rejected or updated.

Useful? React with 👍 / 👎.

}

public extension SettablePluginDiscoverable {
func snapshotSettingsData() -> Data? { nil }

func restoreSettingsData(_ data: Data) {}

func resetSettingsToDefaults() {}
}

/// Opt-in protocol for plugins with user-configurable settings.
Expand All @@ -25,6 +36,15 @@ public protocol SettablePlugin: SettablePluginDiscoverable {
public extension SettablePlugin {
func settingsView() -> AnyView? { nil }

func snapshotSettingsData() -> Data? {
try? JSONEncoder().encode(settings)
}

func restoreSettingsData(_ data: Data) {
guard let restored = try? JSONDecoder().decode(Settings.self, from: data) else { return }
settings = restored
}

func loadSettings() {
let storage = PluginSettingsStorage(pluginId: Self.settingsStorageId)
if let saved = storage.load(Settings.self) {
Expand Down
4 changes: 4 additions & 0 deletions Plugins/XLSXExportPlugin/XLSXExportPlugin.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ final class XLSXExportPlugin: ExportFormatPlugin, SettablePlugin {
AnyView(XLSXExportOptionsView(plugin: self))
}

func resetSettingsToDefaults() {
settings = XLSXExportOptions()
}

private static let maxRowsPerSheet = 1_048_576

func export(
Expand Down
30 changes: 30 additions & 0 deletions TablePro/Core/Plugins/PluginSettingsSnapshot.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
//
// PluginSettingsSnapshot.swift
// TablePro
//

import Foundation
import TableProPluginKit

struct PluginSettingsSnapshot {
private var entries: [(plugin: any SettablePluginDiscoverable, data: Data)]

init(plugins: [any SettablePluginDiscoverable]) {
entries = plugins.compactMap { plugin in
guard let data = plugin.snapshotSettingsData() else { return nil }
return (plugin, data)
}
}

func restore() {
for entry in entries {
entry.plugin.restoreSettingsData(entry.data)
}
}

mutating func recapture(_ plugin: any SettablePluginDiscoverable) {
guard let index = entries.firstIndex(where: { $0.plugin === plugin }),
let data = plugin.snapshotSettingsData() else { return }
entries[index].data = data
}
}
41 changes: 41 additions & 0 deletions TablePro/Core/Storage/TransferDialogStorage.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
//
// TransferDialogStorage.swift
// TablePro
//

import Foundation

final class TransferDialogStorage {
static let shared = TransferDialogStorage()

private let defaults: UserDefaults

private enum Keys {
static let lastExportFormatId = "com.TablePro.export.dialog.lastFormatId"
static let lastImportEncoding = "com.TablePro.import.dialog.lastEncoding"
}

init(userDefaults: UserDefaults = .standard) {
self.defaults = userDefaults
}

func loadLastExportFormatId() -> String? {
defaults.string(forKey: Keys.lastExportFormatId)
}

func saveLastExportFormatId(_ formatId: String) {
defaults.set(formatId, forKey: Keys.lastExportFormatId)
}

func loadLastImportEncoding() -> ImportEncoding {
guard let rawValue = defaults.string(forKey: Keys.lastImportEncoding),
let encoding = ImportEncoding(rawValue: rawValue) else {
return .utf8
}
return encoding
}

func saveLastImportEncoding(_ encoding: ImportEncoding) {
defaults.set(encoding.rawValue, forKey: Keys.lastImportEncoding)
}
}
53 changes: 49 additions & 4 deletions TablePro/Views/Export/ExportDialog.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ struct ExportDialog: View {
@State private var showProgressDialog = false
@State private var showSuccessDialog = false
@State private var exportedFileURL: URL?
@State private var settingsSnapshot: PluginSettingsSnapshot?
@State private var exportSucceeded = false

// MARK: - User Preferences

Expand Down Expand Up @@ -95,10 +97,18 @@ struct ExportDialog: View {
.background(Color(nsColor: .windowBackgroundColor))
.onAppear {
let available = availableFormats
if !available.contains(where: { type(of: $0).formatId == config.formatId }) {
if let first = available.first {
config.formatId = type(of: first).formatId
}
if let lastFormatId = TransferDialogStorage.shared.loadLastExportFormatId(),
available.contains(where: { type(of: $0).formatId == lastFormatId }) {
config.formatId = lastFormatId
} else if !available.contains(where: { type(of: $0).formatId == config.formatId }),
let first = available.first {
config.formatId = type(of: first).formatId
}
captureSettingsSnapshot()
}
.onDisappear {
if !exportSucceeded {
restoreSettingsSnapshot()
}
}
.onChange(of: config.formatId) {
Expand Down Expand Up @@ -329,6 +339,16 @@ struct ExportDialog: View {
if let settable = currentPlugin as? any SettablePluginDiscoverable,
let optionsView = settable.settingsView() {
optionsView

HStack {
Spacer()
Button("Reset to Defaults") {
resetCurrentFormatSettings()
}
.buttonStyle(.link)
.font(.callout)
}
.padding(.top, 8)
}
}
.padding(.horizontal, 16)
Expand Down Expand Up @@ -516,6 +536,29 @@ struct ExportDialog: View {

// MARK: - Actions

private func captureSettingsSnapshot() {
settingsSnapshot = PluginSettingsSnapshot(
plugins: availableFormats.compactMap { $0 as? any SettablePluginDiscoverable }
)
}

private func restoreSettingsSnapshot() {
settingsSnapshot?.restore()
settingsSnapshot = nil
}

private func resetCurrentFormatSettings() {
guard let settable = currentPlugin as? any SettablePluginDiscoverable else { return }
settable.resetSettingsToDefaults()
settingsSnapshot?.recapture(settable)
}

private func recordSuccessfulExport() {
exportSucceeded = true
TransferDialogStorage.shared.saveLastExportFormatId(config.formatId)
settingsSnapshot = nil
}

/// Instantly populate the current database from sidebar tables (no network).
private func populateFromSidebarTables() {
guard !sidebarTables.isEmpty else { return }
Expand Down Expand Up @@ -803,6 +846,7 @@ struct ExportDialog: View {

showProgressDialog = false
isExporting = false
recordSuccessfulExport()

if hideSuccessDialog {
isPresented = false
Expand Down Expand Up @@ -847,6 +891,7 @@ struct ExportDialog: View {

showProgressDialog = false
isExporting = false
recordSuccessfulExport()

if hideSuccessDialog {
isPresented = false
Expand Down
50 changes: 46 additions & 4 deletions TablePro/Views/Import/ImportDialog.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ struct ImportDialog: View {
self.connection = connection
self.initialFileURL = initialFileURL
self._selectedFormatId = State(initialValue: initialFormatId)
self._selectedEncoding = State(initialValue: TransferDialogStorage.shared.loadLastImportEncoding())
}

// MARK: - State
Expand All @@ -32,8 +33,10 @@ struct ImportDialog: View {
@State private var fileSize: Int64 = 0
@State private var statementCount: Int = 0
@State private var isCountingStatements = false
@State private var selectedEncoding: ImportEncoding = .utf8
@State private var selectedEncoding: ImportEncoding
@State private var selectedFormatId: String = "sql"
@State private var settingsSnapshot: PluginSettingsSnapshot?
@State private var importSucceeded = false
@State private var showProgressDialog = false
@State private var showSuccessDialog = false
@State private var showErrorDialog = false
Expand Down Expand Up @@ -83,6 +86,7 @@ struct ImportDialog: View {
selectedFormatId = type(of: first).formatId
}
}
captureSettingsSnapshot()
}
.onExitCommand {
if !(importService?.state.isImporting ?? false) {
Expand All @@ -99,6 +103,9 @@ struct ImportDialog: View {
countStatementsTask?.cancel()
importTask?.cancel()
cleanupTempFiles()
if !importSucceeded {
restoreSettingsSnapshot()
}
}
.sheet(isPresented: $showProgressDialog) {
if let service = importService {
Expand Down Expand Up @@ -237,9 +244,19 @@ struct ImportDialog: View {

private var optionsView: some View {
VStack(alignment: .leading, spacing: 12) {
Text("Options")
.font(.callout.weight(.semibold))
.foregroundStyle(.primary)
HStack {
Text("Options")
.font(.callout.weight(.semibold))
.foregroundStyle(.primary)

Spacer()

Button("Reset to Defaults") {
resetOptionsToDefaults()
}
.buttonStyle(.link)
.font(.callout)
}

VStack(alignment: .leading, spacing: 12) {
// Encoding picker (always shown, independent of plugin)
Expand Down Expand Up @@ -298,6 +315,30 @@ struct ImportDialog: View {

// MARK: - Actions

private func captureSettingsSnapshot() {
settingsSnapshot = PluginSettingsSnapshot(
plugins: availableFormats.compactMap { $0 as? any SettablePluginDiscoverable }
)
}

private func restoreSettingsSnapshot() {
settingsSnapshot?.restore()
settingsSnapshot = nil
}

private func resetOptionsToDefaults() {
selectedEncoding = .utf8
guard let settable = currentPlugin as? any SettablePluginDiscoverable else { return }
settable.resetSettingsToDefaults()
settingsSnapshot?.recapture(settable)
}

private func recordSuccessfulImport() {
importSucceeded = true
TransferDialogStorage.shared.saveLastImportEncoding(selectedEncoding)
settingsSnapshot = nil
}

@MainActor
private func selectFile() async {
guard let window = NSApp.keyWindow ?? NSApp.mainWindow else { return }
Expand Down Expand Up @@ -432,6 +473,7 @@ struct ImportDialog: View {
await MainActor.run {
showProgressDialog = false
importResult = result
recordSuccessfulImport()
showSuccessDialog = true
}
} catch is PluginImportCancellationError {
Expand Down
Loading
Loading