diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5512607a6..9c1f253e7 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -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
diff --git a/Plugins/CSVExportPlugin/CSVExportPlugin.swift b/Plugins/CSVExportPlugin/CSVExportPlugin.swift
index 75db39b12..332a9825c 100644
--- a/Plugins/CSVExportPlugin/CSVExportPlugin.swift
+++ b/Plugins/CSVExportPlugin/CSVExportPlugin.swift
@@ -33,6 +33,10 @@ final class CSVExportPlugin: ExportFormatPlugin, SettablePlugin {
AnyView(CSVExportOptionsView(plugin: self))
}
+ func resetSettingsToDefaults() {
+ settings = CSVExportOptions()
+ }
+
func export(
tables: [PluginExportTable],
dataSource: any PluginExportDataSource,
diff --git a/Plugins/JSONExportPlugin/JSONExportPlugin.swift b/Plugins/JSONExportPlugin/JSONExportPlugin.swift
index 2868e2890..5dc96422e 100644
--- a/Plugins/JSONExportPlugin/JSONExportPlugin.swift
+++ b/Plugins/JSONExportPlugin/JSONExportPlugin.swift
@@ -30,6 +30,10 @@ final class JSONExportPlugin: ExportFormatPlugin, SettablePlugin {
AnyView(JSONExportOptionsView(plugin: self))
}
+ func resetSettingsToDefaults() {
+ settings = JSONExportOptions()
+ }
+
func export(
tables: [PluginExportTable],
dataSource: any PluginExportDataSource,
diff --git a/Plugins/JSONImportPlugin/JSONImportPlugin.swift b/Plugins/JSONImportPlugin/JSONImportPlugin.swift
index 47705f3ab..d5a5cb923 100644
--- a/Plugins/JSONImportPlugin/JSONImportPlugin.swift
+++ b/Plugins/JSONImportPlugin/JSONImportPlugin.swift
@@ -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,
diff --git a/Plugins/MQLExportPlugin/MQLExportPlugin.swift b/Plugins/MQLExportPlugin/MQLExportPlugin.swift
index f4f7a686f..90e19d6d8 100644
--- a/Plugins/MQLExportPlugin/MQLExportPlugin.swift
+++ b/Plugins/MQLExportPlugin/MQLExportPlugin.swift
@@ -45,6 +45,10 @@ final class MQLExportPlugin: ExportFormatPlugin, SettablePlugin {
AnyView(MQLExportOptionsView(plugin: self))
}
+ func resetSettingsToDefaults() {
+ settings = MQLExportOptions()
+ }
+
func export(
tables: [PluginExportTable],
dataSource: any PluginExportDataSource,
diff --git a/Plugins/SQLExportPlugin/SQLExportPlugin.swift b/Plugins/SQLExportPlugin/SQLExportPlugin.swift
index 578c31e6d..eaa26b45c 100644
--- a/Plugins/SQLExportPlugin/SQLExportPlugin.swift
+++ b/Plugins/SQLExportPlugin/SQLExportPlugin.swift
@@ -55,6 +55,10 @@ final class SQLExportPlugin: ExportFormatPlugin, SettablePlugin {
AnyView(SQLExportOptionsView(plugin: self))
}
+ func resetSettingsToDefaults() {
+ settings = SQLExportOptions()
+ }
+
func export(
tables: [PluginExportTable],
dataSource: any PluginExportDataSource,
diff --git a/Plugins/SQLImportPlugin/SQLImportPlugin.swift b/Plugins/SQLImportPlugin/SQLImportPlugin.swift
index 453a3405e..9a2d4a464 100644
--- a/Plugins/SQLImportPlugin/SQLImportPlugin.swift
+++ b/Plugins/SQLImportPlugin/SQLImportPlugin.swift
@@ -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,
diff --git a/Plugins/TableProPluginKit/SettablePlugin.swift b/Plugins/TableProPluginKit/SettablePlugin.swift
index 2a2f1725c..021a05d48 100644
--- a/Plugins/TableProPluginKit/SettablePlugin.swift
+++ b/Plugins/TableProPluginKit/SettablePlugin.swift
@@ -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()
+}
+
+public extension SettablePluginDiscoverable {
+ func snapshotSettingsData() -> Data? { nil }
+
+ func restoreSettingsData(_ data: Data) {}
+
+ func resetSettingsToDefaults() {}
}
/// Opt-in protocol for plugins with user-configurable settings.
@@ -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) {
diff --git a/Plugins/XLSXExportPlugin/XLSXExportPlugin.swift b/Plugins/XLSXExportPlugin/XLSXExportPlugin.swift
index 68e37cae1..ed6030fec 100644
--- a/Plugins/XLSXExportPlugin/XLSXExportPlugin.swift
+++ b/Plugins/XLSXExportPlugin/XLSXExportPlugin.swift
@@ -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(
diff --git a/TablePro/Core/Plugins/PluginSettingsSnapshot.swift b/TablePro/Core/Plugins/PluginSettingsSnapshot.swift
new file mode 100644
index 000000000..a228bcd1e
--- /dev/null
+++ b/TablePro/Core/Plugins/PluginSettingsSnapshot.swift
@@ -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
+ }
+}
diff --git a/TablePro/Core/Storage/TransferDialogStorage.swift b/TablePro/Core/Storage/TransferDialogStorage.swift
new file mode 100644
index 000000000..386e87194
--- /dev/null
+++ b/TablePro/Core/Storage/TransferDialogStorage.swift
@@ -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)
+ }
+}
diff --git a/TablePro/Views/Export/ExportDialog.swift b/TablePro/Views/Export/ExportDialog.swift
index d826ec29f..192a1cd11 100644
--- a/TablePro/Views/Export/ExportDialog.swift
+++ b/TablePro/Views/Export/ExportDialog.swift
@@ -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
@@ -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) {
@@ -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)
@@ -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 }
@@ -803,6 +846,7 @@ struct ExportDialog: View {
showProgressDialog = false
isExporting = false
+ recordSuccessfulExport()
if hideSuccessDialog {
isPresented = false
@@ -847,6 +891,7 @@ struct ExportDialog: View {
showProgressDialog = false
isExporting = false
+ recordSuccessfulExport()
if hideSuccessDialog {
isPresented = false
diff --git a/TablePro/Views/Import/ImportDialog.swift b/TablePro/Views/Import/ImportDialog.swift
index fea4fcb77..a919a7758 100644
--- a/TablePro/Views/Import/ImportDialog.swift
+++ b/TablePro/Views/Import/ImportDialog.swift
@@ -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
@@ -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
@@ -83,6 +86,7 @@ struct ImportDialog: View {
selectedFormatId = type(of: first).formatId
}
}
+ captureSettingsSnapshot()
}
.onExitCommand {
if !(importService?.state.isImporting ?? false) {
@@ -99,6 +103,9 @@ struct ImportDialog: View {
countStatementsTask?.cancel()
importTask?.cancel()
cleanupTempFiles()
+ if !importSucceeded {
+ restoreSettingsSnapshot()
+ }
}
.sheet(isPresented: $showProgressDialog) {
if let service = importService {
@@ -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)
@@ -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 }
@@ -432,6 +473,7 @@ struct ImportDialog: View {
await MainActor.run {
showProgressDialog = false
importResult = result
+ recordSuccessfulImport()
showSuccessDialog = true
}
} catch is PluginImportCancellationError {
diff --git a/TableProTests/Core/Plugins/PluginSettingsSnapshotTests.swift b/TableProTests/Core/Plugins/PluginSettingsSnapshotTests.swift
new file mode 100644
index 000000000..389ce75a1
--- /dev/null
+++ b/TableProTests/Core/Plugins/PluginSettingsSnapshotTests.swift
@@ -0,0 +1,93 @@
+//
+// PluginSettingsSnapshotTests.swift
+// TableProTests
+//
+
+import Foundation
+import SwiftUI
+import TableProPluginKit
+import Testing
+@testable import TablePro
+
+@Suite("PluginSettingsSnapshot", .serialized)
+struct PluginSettingsSnapshotTests {
+ private struct TestOptions: Codable, Equatable {
+ var flag = false
+ var count = 0
+ }
+
+ private final class TestPlugin: SettablePlugin {
+ static let settingsStorageId = "test.snapshot.holder"
+
+ var settings = TestOptions() {
+ didSet { saveSettings() }
+ }
+
+ func resetSettingsToDefaults() {
+ settings = TestOptions()
+ }
+ }
+
+ private final class BareDiscoverablePlugin: SettablePluginDiscoverable {
+ func settingsView() -> AnyView? { nil }
+ }
+
+ private func cleanup() {
+ PluginSettingsStorage(pluginId: TestPlugin.settingsStorageId).removeAll()
+ }
+
+ @Test("restore reverts captured plugins")
+ func restoreReverts() {
+ defer { cleanup() }
+ let plugin = TestPlugin()
+ plugin.settings = TestOptions(flag: true, count: 1)
+ let snapshot = PluginSettingsSnapshot(plugins: [plugin])
+
+ plugin.settings = TestOptions(flag: false, count: 9)
+ snapshot.restore()
+
+ #expect(plugin.settings == TestOptions(flag: true, count: 1))
+ }
+
+ @Test("recapture updates the rollback baseline")
+ func recaptureUpdatesBaseline() {
+ defer { cleanup() }
+ let plugin = TestPlugin()
+ plugin.settings = TestOptions(flag: true, count: 1)
+ var snapshot = PluginSettingsSnapshot(plugins: [plugin])
+
+ plugin.resetSettingsToDefaults()
+ snapshot.recapture(plugin)
+ plugin.settings = TestOptions(flag: true, count: 5)
+ snapshot.restore()
+
+ #expect(plugin.settings == TestOptions())
+ }
+
+ @Test("plugins without snapshot data are skipped")
+ func skipsPluginsWithoutData() {
+ defer { cleanup() }
+ let bare = BareDiscoverablePlugin()
+ let plugin = TestPlugin()
+ plugin.settings = TestOptions(flag: true, count: 2)
+ let snapshot = PluginSettingsSnapshot(plugins: [bare, plugin])
+
+ plugin.settings = TestOptions(flag: false, count: 0)
+ snapshot.restore()
+
+ #expect(plugin.settings == TestOptions(flag: true, count: 2))
+ }
+
+ @Test("recapture ignores plugins that were not captured")
+ func recaptureIgnoresUncaptured() {
+ defer { cleanup() }
+ let plugin = TestPlugin()
+ plugin.settings = TestOptions(flag: true, count: 3)
+ var snapshot = PluginSettingsSnapshot(plugins: [])
+
+ snapshot.recapture(plugin)
+ snapshot.restore()
+
+ #expect(plugin.settings == TestOptions(flag: true, count: 3))
+ }
+}
diff --git a/TableProTests/Core/Plugins/PluginSettingsTests.swift b/TableProTests/Core/Plugins/PluginSettingsTests.swift
index 21d49b61b..fedc5ba45 100644
--- a/TableProTests/Core/Plugins/PluginSettingsTests.swift
+++ b/TableProTests/Core/Plugins/PluginSettingsTests.swift
@@ -130,6 +130,93 @@ struct PluginSettingsStorageTests {
}
}
+@Suite("SettablePlugin snapshot and restore", .serialized)
+struct SettablePluginSnapshotTests {
+ private struct TestOptions: Codable, Equatable {
+ var flag = false
+ var count = 0
+ }
+
+ private final class TestSettablePlugin: SettablePlugin {
+ static let settingsStorageId = "test.settable.snapshot"
+
+ var settings = TestOptions() {
+ didSet { saveSettings() }
+ }
+
+ func resetSettingsToDefaults() {
+ settings = TestOptions()
+ }
+ }
+
+ private func cleanup() {
+ PluginSettingsStorage(pluginId: TestSettablePlugin.settingsStorageId).removeAll()
+ }
+
+ @Test("snapshotSettingsData encodes current settings")
+ func snapshotEncodesCurrentSettings() throws {
+ defer { cleanup() }
+ let plugin = TestSettablePlugin()
+ plugin.settings = TestOptions(flag: true, count: 7)
+
+ let data = try #require(plugin.snapshotSettingsData())
+ let decoded = try JSONDecoder().decode(TestOptions.self, from: data)
+
+ #expect(decoded == plugin.settings)
+ }
+
+ @Test("restoreSettingsData restores settings and persists the restored value")
+ func restoreRevertsSettingsAndStorage() throws {
+ defer { cleanup() }
+ let plugin = TestSettablePlugin()
+ plugin.settings = TestOptions(flag: true, count: 1)
+ let snapshot = try #require(plugin.snapshotSettingsData())
+
+ plugin.settings = TestOptions(flag: false, count: 99)
+ plugin.restoreSettingsData(snapshot)
+
+ #expect(plugin.settings == TestOptions(flag: true, count: 1))
+ let storage = PluginSettingsStorage(pluginId: TestSettablePlugin.settingsStorageId)
+ #expect(storage.load(TestOptions.self) == TestOptions(flag: true, count: 1))
+ }
+
+ @Test("restoreSettingsData ignores invalid data")
+ func restoreIgnoresInvalidData() {
+ defer { cleanup() }
+ let plugin = TestSettablePlugin()
+ plugin.settings = TestOptions(flag: true, count: 5)
+
+ plugin.restoreSettingsData(Data("not json".utf8))
+
+ #expect(plugin.settings == TestOptions(flag: true, count: 5))
+ }
+
+ @Test("resetSettingsToDefaults restores default values")
+ func resetRestoresDefaults() {
+ defer { cleanup() }
+ let plugin = TestSettablePlugin()
+ plugin.settings = TestOptions(flag: true, count: 42)
+
+ plugin.resetSettingsToDefaults()
+
+ #expect(plugin.settings == TestOptions())
+ }
+
+ @Test("snapshot and restore dispatch through the type-erased protocol")
+ func typeErasedDispatch() throws {
+ defer { cleanup() }
+ let plugin = TestSettablePlugin()
+ plugin.settings = TestOptions(flag: true, count: 3)
+ let discoverable: any SettablePluginDiscoverable = plugin
+
+ let snapshot = try #require(discoverable.snapshotSettingsData())
+ plugin.settings = TestOptions(flag: false, count: 0)
+ discoverable.restoreSettingsData(snapshot)
+
+ #expect(plugin.settings == TestOptions(flag: true, count: 3))
+ }
+}
+
@Suite("PluginCapability")
struct PluginCapabilityTests {
diff --git a/TableProTests/Core/Storage/TransferDialogStorageTests.swift b/TableProTests/Core/Storage/TransferDialogStorageTests.swift
new file mode 100644
index 000000000..2fa0152a5
--- /dev/null
+++ b/TableProTests/Core/Storage/TransferDialogStorageTests.swift
@@ -0,0 +1,80 @@
+//
+// TransferDialogStorageTests.swift
+// TableProTests
+//
+
+import Foundation
+import Testing
+@testable import TablePro
+
+@Suite("TransferDialogStorage")
+struct TransferDialogStorageTests {
+ private let suiteName = "com.TablePro.tests.exportDialog.\(UUID().uuidString)"
+
+ private func makeDefaults() throws -> UserDefaults {
+ try #require(UserDefaults(suiteName: suiteName))
+ }
+
+ @Test("loadLastExportFormatId returns nil when nothing stored")
+ func formatIdNilWhenEmpty() throws {
+ let defaults = try makeDefaults()
+ defer { defaults.removePersistentDomain(forName: suiteName) }
+
+ let storage = TransferDialogStorage(userDefaults: defaults)
+ #expect(storage.loadLastExportFormatId() == nil)
+ }
+
+ @Test("saveLastExportFormatId round-trips")
+ func formatIdRoundTrip() throws {
+ let defaults = try makeDefaults()
+ defer { defaults.removePersistentDomain(forName: suiteName) }
+
+ let storage = TransferDialogStorage(userDefaults: defaults)
+ storage.saveLastExportFormatId("sql")
+
+ #expect(storage.loadLastExportFormatId() == "sql")
+ }
+
+ @Test("saveLastExportFormatId overwrites previous value")
+ func formatIdOverwrites() throws {
+ let defaults = try makeDefaults()
+ defer { defaults.removePersistentDomain(forName: suiteName) }
+
+ let storage = TransferDialogStorage(userDefaults: defaults)
+ storage.saveLastExportFormatId("csv")
+ storage.saveLastExportFormatId("xlsx")
+
+ #expect(storage.loadLastExportFormatId() == "xlsx")
+ }
+
+ @Test("loadLastImportEncoding returns utf8 when nothing stored")
+ func encodingDefaultsToUTF8() throws {
+ let defaults = try makeDefaults()
+ defer { defaults.removePersistentDomain(forName: suiteName) }
+
+ let storage = TransferDialogStorage(userDefaults: defaults)
+ #expect(storage.loadLastImportEncoding() == .utf8)
+ }
+
+ @Test("saveLastImportEncoding round-trips all cases", arguments: ImportEncoding.allCases)
+ func encodingRoundTrip(encoding: ImportEncoding) throws {
+ let defaults = try makeDefaults()
+ defer { defaults.removePersistentDomain(forName: suiteName) }
+
+ let storage = TransferDialogStorage(userDefaults: defaults)
+ storage.saveLastImportEncoding(encoding)
+
+ #expect(storage.loadLastImportEncoding() == encoding)
+ }
+
+ @Test("loadLastImportEncoding falls back to utf8 for unknown stored value")
+ func encodingFallsBackOnUnknownValue() throws {
+ let defaults = try makeDefaults()
+ defer { defaults.removePersistentDomain(forName: suiteName) }
+
+ defaults.set("Shift-JIS", forKey: "com.TablePro.import.dialog.lastEncoding")
+ let storage = TransferDialogStorage(userDefaults: defaults)
+
+ #expect(storage.loadLastImportEncoding() == .utf8)
+ }
+}
diff --git a/docs/features/import-export.mdx b/docs/features/import-export.mdx
index 0c7cf3076..e4c761eb9 100644
--- a/docs/features/import-export.mdx
+++ b/docs/features/import-export.mdx
@@ -14,6 +14,8 @@ Export data in five formats (CSV, JSON, SQL, MQL, XLSX), import SQL files with g
3. Choose a format, select tables, configure options
4. Click **Export**
+TablePro remembers the last format and options you exported with, so the dialog opens ready for a repeat export. Option changes stick only after a successful export; cancelling the dialog discards them. **Reset to Defaults** under the options restores the stock settings for the current format.
+
**MongoDB**: SQL export is not available. Use CSV, JSON, MQL, or XLSX. MQL generates `db.collection.insertMany([...])` scripts for `mongosh`.
@@ -211,7 +213,7 @@ Import `.sql` and `.sql.gz` files (statements execute directly against your data
Click **File** > **Import** > **From SQL** (`Cmd+Shift+I`), or drag and drop a `.sql` / `.sql.gz` file onto the app.
- Set encoding, transaction wrapping, and foreign key check options.
+ Set encoding, transaction wrapping, and foreign key check options. TablePro remembers the encoding and options from your last successful import; **Reset to Defaults** next to the Options header restores them.
Review the SQL preview, statement count, and file size. Click **Import** to execute.