From 43918458b732bc6b72bb614762f2157519cbb88d Mon Sep 17 00:00:00 2001 From: Ngo Quoc Dat Date: Fri, 22 May 2026 20:49:34 +0700 Subject: [PATCH 1/2] fix(datagrid): restore applied filters when reopening a table (#1347) --- CHANGELOG.md | 1 + .../Core/Coordinators/FilterCoordinator.swift | 47 +++++-- .../Core/Storage/FilterSettingsStorage.swift | 131 +++++++++++++---- .../MainContentCoordinator+TabSwitch.swift | 5 +- .../Extensions/MainContentView+Setup.swift | 34 ++--- .../Storage/FilterSettingsStorageTests.swift | 132 ++++++++++++++++++ .../Views/Main/FilterRestoreTests.swift | 66 +++++++++ docs/features/filtering.mdx | 4 +- 8 files changed, 356 insertions(+), 64 deletions(-) create mode 100644 TableProTests/Core/Storage/FilterSettingsStorageTests.swift create mode 100644 TableProTests/Views/Main/FilterRestoreTests.swift diff --git a/CHANGELOG.md b/CHANGELOG.md index c132178f5..7e835ab01 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +- Reopening a table now restores the filter you had applied, instead of clearing it. Filters are remembered per connection. (#1347) - Raw SQL filter now suggests columns and keywords at every position in the expression, including after AND and OR, instead of only the first column. (#1346) - Plugins left incompatible after a TablePro update now update quietly in the background instead of showing a premature "could not be loaded" alert. You are only notified when no compatible version exists yet, and the message tells you what to do. (#1322) - A plugin you download and install by hand is no longer blocked by macOS Gatekeeper once its signature is verified. (#1322) diff --git a/TablePro/Core/Coordinators/FilterCoordinator.swift b/TablePro/Core/Coordinators/FilterCoordinator.swift index 3effcb76f..8384536db 100644 --- a/TablePro/Core/Coordinators/FilterCoordinator.swift +++ b/TablePro/Core/Coordinators/FilterCoordinator.swift @@ -323,7 +323,10 @@ final class FilterCoordinator { let tableName = tab.tableContext.tableName else { return } FilterSettingsStorage.shared.saveLastFilters( tab.filterState.appliedFilters, - for: tableName + for: tableName, + connectionId: parent.connectionId, + databaseName: tab.tableContext.databaseName, + schemaName: tab.tableContext.schemaName ) } @@ -331,24 +334,44 @@ final class FilterCoordinator { guard let tab = parent.tabManager.selectedTab else { return } FilterSettingsStorage.shared.saveLastFilters( tab.filterState.appliedFilters, - for: tableName + for: tableName, + connectionId: parent.connectionId, + databaseName: tab.tableContext.databaseName, + schemaName: tab.tableContext.schemaName ) } func restoreLastFilters(for tableName: String) { let settings = FilterSettingsStorage.shared.loadSettings() + guard settings.panelState != .alwaysHide, + let tab = parent.tabManager.selectedTab else { return } + + let restored = FilterSettingsStorage.shared.loadLastFilters( + for: tableName, + connectionId: parent.connectionId, + databaseName: tab.tableContext.databaseName, + schemaName: tab.tableContext.schemaName + ) mutateSelectedTabFilterState { state in - if settings.panelState == .restoreLast { - let restored = FilterSettingsStorage.shared.loadLastFilters(for: tableName) - if !restored.isEmpty { - state.filters = restored - state.appliedFilters = restored - } - } - if settings.panelState == .alwaysShow { - state.isVisible = true - } + state = Self.resolvedRestoredState(panelState: settings.panelState, saved: restored, current: state) + } + } + + static func resolvedRestoredState( + panelState: FilterPanelDefaultState, + saved: [TableFilter], + current: TabFilterState + ) -> TabFilterState { + guard panelState != .alwaysHide else { return current } + var state = current + if !saved.isEmpty { + state.filters = saved + state.appliedFilters = saved + state.isVisible = true + } else if panelState == .alwaysShow { + state.isVisible = true } + return state } func clearFilterState() { diff --git a/TablePro/Core/Storage/FilterSettingsStorage.swift b/TablePro/Core/Storage/FilterSettingsStorage.swift index 513c1233c..59af594ca 100644 --- a/TablePro/Core/Storage/FilterSettingsStorage.swift +++ b/TablePro/Core/Storage/FilterSettingsStorage.swift @@ -66,7 +66,7 @@ struct FilterSettings: Codable, Equatable { init( defaultColumn: FilterDefaultColumn = .rawSQL, defaultOperator: FilterDefaultOperator = .equal, - panelState: FilterPanelDefaultState = .alwaysHide + panelState: FilterPanelDefaultState = .restoreLast ) { self.defaultColumn = defaultColumn self.defaultOperator = defaultOperator @@ -82,9 +82,10 @@ final class FilterSettingsStorage { private static let legacyLastFiltersKeyPrefix = "com.TablePro.filter.lastFilters." private static let legacyKnownFilterKeysKey = "com.TablePro.filter.knownFilterKeys" private static let migrationCompleteKey = "com.TablePro.filterStateMigrationComplete" + private static let compositeKeyMigrationKey = "com.TablePro.filterStateCompositeKeyMigrationComplete" + private static let settingsKey = "com.TablePro.filter.settings" - private let settingsKey = "com.TablePro.filter.settings" - private let defaults = UserDefaults.standard + private let defaults: UserDefaults private let filterStateDirectory: URL private let encoder = JSONEncoder() @@ -93,8 +94,13 @@ final class FilterSettingsStorage { private var cachedSettings: FilterSettings? private var lastFiltersCache: [String: [TableFilter]] = [:] - private init() { - filterStateDirectory = Self.resolvedFilterStateDirectory() + private convenience init() { + self.init(filterStateDirectory: Self.resolvedFilterStateDirectory(), defaults: .standard) + } + + init(filterStateDirectory: URL, defaults: UserDefaults) { + self.filterStateDirectory = filterStateDirectory + self.defaults = defaults do { try FileManager.default.createDirectory( @@ -105,13 +111,14 @@ final class FilterSettingsStorage { Self.logger.error("Failed to create filter state directory: \(error.localizedDescription)") } - Self.performMigrationIfNeeded(filterStateDirectory: filterStateDirectory) + Self.performMigrationIfNeeded(filterStateDirectory: filterStateDirectory, defaults: defaults) + Self.performCompositeKeyMigrationIfNeeded(filterStateDirectory: filterStateDirectory, defaults: defaults) } func loadSettings() -> FilterSettings { if let cached = cachedSettings { return cached } - guard let data = defaults.data(forKey: settingsKey) else { + guard let data = defaults.data(forKey: Self.settingsKey) else { let defaultSettings = FilterSettings() cachedSettings = defaultSettings return defaultSettings @@ -133,58 +140,89 @@ final class FilterSettingsStorage { cachedSettings = settings do { let data = try encoder.encode(settings) - defaults.set(data, forKey: settingsKey) + defaults.set(data, forKey: Self.settingsKey) } catch { Self.logger.error("Failed to encode filter settings: \(error)") } } - func loadLastFilters(for tableName: String) -> [TableFilter] { - let sanitized = sanitizeTableName(tableName) - if let cached = lastFiltersCache[sanitized] { return cached } - - let fileURL = fileURL(forSanitizedName: sanitized) + func loadLastFilters( + for tableName: String, + connectionId: UUID, + databaseName: String, + schemaName: String? + ) -> [TableFilter] { + let key = compositeKey( + tableName: tableName, + connectionId: connectionId, + databaseName: databaseName, + schemaName: schemaName + ) + if let cached = lastFiltersCache[key] { return cached } + + let fileURL = fileURL(forKey: key) guard FileManager.default.fileExists(atPath: fileURL.path) else { - lastFiltersCache[sanitized] = [] + lastFiltersCache[key] = [] return [] } do { let data = try Data(contentsOf: fileURL) let filters = try decoder.decode([TableFilter].self, from: data) - lastFiltersCache[sanitized] = filters + lastFiltersCache[key] = filters return filters } catch { Self.logger.error("Failed to load last filters for \(tableName): \(error)") - lastFiltersCache[sanitized] = [] + lastFiltersCache[key] = [] return [] } } - func saveLastFilters(_ filters: [TableFilter], for tableName: String) { - let sanitized = sanitizeTableName(tableName) - let fileURL = fileURL(forSanitizedName: sanitized) + func saveLastFilters( + _ filters: [TableFilter], + for tableName: String, + connectionId: UUID, + databaseName: String, + schemaName: String? + ) { + let key = compositeKey( + tableName: tableName, + connectionId: connectionId, + databaseName: databaseName, + schemaName: schemaName + ) + let fileURL = fileURL(forKey: key) guard !filters.isEmpty else { removeFile(at: fileURL, label: tableName) - lastFiltersCache.removeValue(forKey: sanitized) + lastFiltersCache.removeValue(forKey: key) return } do { let data = try encoder.encode(filters) try data.write(to: fileURL, options: .atomic) - lastFiltersCache[sanitized] = filters + lastFiltersCache[key] = filters } catch { Self.logger.error("Failed to save last filters for \(tableName): \(error)") } } - func clearLastFilters(for tableName: String) { - let sanitized = sanitizeTableName(tableName) - let fileURL = fileURL(forSanitizedName: sanitized) + func clearLastFilters( + for tableName: String, + connectionId: UUID, + databaseName: String, + schemaName: String? + ) { + let key = compositeKey( + tableName: tableName, + connectionId: connectionId, + databaseName: databaseName, + schemaName: schemaName + ) + let fileURL = fileURL(forKey: key) removeFile(at: fileURL, label: tableName) - lastFiltersCache.removeValue(forKey: sanitized) + lastFiltersCache.removeValue(forKey: key) } func clearAllLastFilters() { @@ -200,8 +238,8 @@ final class FilterSettingsStorage { lastFiltersCache.removeAll() } - private func fileURL(forSanitizedName sanitized: String) -> URL { - filterStateDirectory.appendingPathComponent("\(sanitized).json") + private func fileURL(forKey key: String) -> URL { + filterStateDirectory.appendingPathComponent("\(key).json") } private func removeFile(at fileURL: URL, label: String) { @@ -213,8 +251,15 @@ final class FilterSettingsStorage { } } - private func sanitizeTableName(_ tableName: String) -> String { - tableName.addingPercentEncoding(withAllowedCharacters: .alphanumerics) ?? tableName + private func compositeKey( + tableName: String, + connectionId: UUID, + databaseName: String, + schemaName: String? + ) -> String { + [connectionId.uuidString, databaseName, schemaName ?? "", tableName] + .map { $0.addingPercentEncoding(withAllowedCharacters: .alphanumerics) ?? $0 } + .joined(separator: ".") } private static func resolvedFilterStateDirectory() -> URL { @@ -227,8 +272,7 @@ final class FilterSettingsStorage { .appendingPathComponent("FilterState", isDirectory: true) } - private static func performMigrationIfNeeded(filterStateDirectory: URL) { - let defaults = UserDefaults.standard + private static func performMigrationIfNeeded(filterStateDirectory: URL, defaults: UserDefaults) { guard !defaults.bool(forKey: migrationCompleteKey) else { return } let allKeys = defaults.dictionaryRepresentation().keys @@ -260,4 +304,29 @@ final class FilterSettingsStorage { logger.trace("Migrated \(migrated) per-table filter entries to file storage") } } + + private static func performCompositeKeyMigrationIfNeeded(filterStateDirectory: URL, defaults: UserDefaults) { + guard !defaults.bool(forKey: compositeKeyMigrationKey) else { return } + + let fileManager = FileManager.default + if let files = try? fileManager.contentsOfDirectory( + at: filterStateDirectory, + includingPropertiesForKeys: nil + ) { + for file in files where file.pathExtension == "json" { + try? fileManager.removeItem(at: file) + } + } + + if let data = defaults.data(forKey: settingsKey), + var settings = try? JSONDecoder().decode(FilterSettings.self, from: data), + settings.panelState == .alwaysHide { + settings.panelState = .restoreLast + if let upgraded = try? JSONEncoder().encode(settings) { + defaults.set(upgraded, forKey: settingsKey) + } + } + + defaults.set(true, forKey: compositeKeyMigrationKey) + } } diff --git a/TablePro/Views/Main/Extensions/MainContentCoordinator+TabSwitch.swift b/TablePro/Views/Main/Extensions/MainContentCoordinator+TabSwitch.swift index cc9a957d8..9d59ba75f 100644 --- a/TablePro/Views/Main/Extensions/MainContentCoordinator+TabSwitch.swift +++ b/TablePro/Views/Main/Extensions/MainContentCoordinator+TabSwitch.swift @@ -38,7 +38,10 @@ extension MainContentCoordinator { if let tableName = tabManager.tabs[oldIndex].tableContext.tableName { FilterSettingsStorage.shared.saveLastFilters( tabManager.tabs[oldIndex].filterState.appliedFilters, - for: tableName + for: tableName, + connectionId: connectionId, + databaseName: tabManager.tabs[oldIndex].tableContext.databaseName, + schemaName: tabManager.tabs[oldIndex].tableContext.schemaName ) } persistOutgoingTabHiddenColumns(oldIndex: oldIndex) diff --git a/TablePro/Views/Main/Extensions/MainContentView+Setup.swift b/TablePro/Views/Main/Extensions/MainContentView+Setup.swift index 67fd82edb..25fc30a5d 100644 --- a/TablePro/Views/Main/Extensions/MainContentView+Setup.swift +++ b/TablePro/Views/Main/Extensions/MainContentView+Setup.swift @@ -40,6 +40,17 @@ extension MainContentView { switch payload.intent { case .openContent: + if let selectedTab = tabManager.selectedTab, + selectedTab.tabType == .table, + let tableName = selectedTab.tableContext.tableName + { + coordinator.restoreLastHiddenColumnsForTable(tableName) + if selectedTab.filterState.appliedFilters.isEmpty { + coordinator.restoreFiltersForTable(tableName) + } else if let tabIndex = tabManager.selectedTabIndex { + coordinator.rebuildTableQuery(at: tabIndex) + } + } if payload.skipAutoExecute { _ = await schemaLoad return @@ -56,22 +67,6 @@ extension MainContentView { { await coordinator.switchDatabase(to: selectedTab.tableContext.databaseName) } else { - if !selectedTab.filterState.appliedFilters.isEmpty, - let tableName = selectedTab.tableContext.tableName, - let tabIndex = tabManager.selectedTabIndex - { - let filteredQuery = coordinator.queryBuilder.buildFilteredQuery( - tableName: tableName, - filters: selectedTab.filterState.appliedFilters, - columns: [], - limit: selectedTab.pagination.pageSize, - offset: selectedTab.pagination.currentOffset - ) - tabManager.mutate(at: tabIndex) { $0.content.query = filteredQuery } - } - if let tableName = selectedTab.tableContext.tableName { - coordinator.restoreLastHiddenColumnsForTable(tableName) - } coordinator.executeTableTabQueryDirectly() } } else { @@ -150,6 +145,10 @@ extension MainContentView { if firstTab.tabType == .table, !firstTab.content.query.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { + if let tableName = firstTab.tableContext.tableName { + coordinator.restoreLastHiddenColumnsForTable(tableName) + coordinator.restoreFiltersForTable(tableName) + } if let session = DatabaseManager.shared.activeSessions[connection.id], session.isConnected { @@ -158,9 +157,6 @@ extension MainContentView { { Task { await coordinator.switchDatabase(to: firstTab.tableContext.databaseName) } } else { - if let tableName = firstTab.tableContext.tableName { - coordinator.restoreLastHiddenColumnsForTable(tableName) - } coordinator.executeTableTabQueryDirectly() } } else { diff --git a/TableProTests/Core/Storage/FilterSettingsStorageTests.swift b/TableProTests/Core/Storage/FilterSettingsStorageTests.swift new file mode 100644 index 000000000..d2f334158 --- /dev/null +++ b/TableProTests/Core/Storage/FilterSettingsStorageTests.swift @@ -0,0 +1,132 @@ +// +// FilterSettingsStorageTests.swift +// TableProTests +// + +import Foundation +@testable import TablePro +import Testing + +@Suite("FilterSettingsStorage") +@MainActor +struct FilterSettingsStorageTests { + private func makeStorage() -> (storage: FilterSettingsStorage, directory: URL) { + let suiteName = "FilterSettingsStorageTests-\(UUID().uuidString)" + guard let defaults = UserDefaults(suiteName: suiteName) else { + fatalError("Failed to create UserDefaults suite for tests") + } + let directory = FileManager.default.temporaryDirectory + .appendingPathComponent("FilterSettingsStorageTests-\(UUID().uuidString)", isDirectory: true) + return (FilterSettingsStorage(filterStateDirectory: directory, defaults: defaults), directory) + } + + @Test("Saving then loading round-trips the filters") + func roundTripsSaveAndLoad() { + let (storage, directory) = makeStorage() + defer { try? FileManager.default.removeItem(at: directory) } + let connectionId = UUID() + let filters = [TestFixtures.makeTableFilter(column: "email", value: "a@b.com")] + + storage.saveLastFilters(filters, for: "users", connectionId: connectionId, databaseName: "db", schemaName: nil) + + #expect( + storage.loadLastFilters(for: "users", connectionId: connectionId, databaseName: "db", schemaName: nil) == filters + ) + } + + @Test("Loading an unsaved table returns no filters") + func loadReturnsEmptyForMissing() { + let (storage, directory) = makeStorage() + defer { try? FileManager.default.removeItem(at: directory) } + #expect( + storage.loadLastFilters(for: "users", connectionId: UUID(), databaseName: "db", schemaName: nil).isEmpty + ) + } + + @Test("The same table name in different connections stays isolated") + func connectionsAreIsolated() { + let (storage, directory) = makeStorage() + defer { try? FileManager.default.removeItem(at: directory) } + let connectionA = UUID() + let connectionB = UUID() + let filtersA = [TestFixtures.makeTableFilter(column: "a")] + + storage.saveLastFilters(filtersA, for: "users", connectionId: connectionA, databaseName: "db", schemaName: nil) + + #expect( + storage.loadLastFilters(for: "users", connectionId: connectionB, databaseName: "db", schemaName: nil).isEmpty + ) + #expect( + storage.loadLastFilters(for: "users", connectionId: connectionA, databaseName: "db", schemaName: nil) == filtersA + ) + } + + @Test("The same table name in different databases stays isolated") + func databasesAreIsolated() { + let (storage, directory) = makeStorage() + defer { try? FileManager.default.removeItem(at: directory) } + let connectionId = UUID() + let filters = [TestFixtures.makeTableFilter(column: "a")] + + storage.saveLastFilters(filters, for: "users", connectionId: connectionId, databaseName: "db_a", schemaName: nil) + + #expect( + storage.loadLastFilters(for: "users", connectionId: connectionId, databaseName: "db_b", schemaName: nil).isEmpty + ) + } + + @Test("The same table name in different schemas stays isolated") + func schemasAreIsolated() { + let (storage, directory) = makeStorage() + defer { try? FileManager.default.removeItem(at: directory) } + let connectionId = UUID() + let filters = [TestFixtures.makeTableFilter(column: "a")] + + storage.saveLastFilters(filters, for: "users", connectionId: connectionId, databaseName: "db", schemaName: "public") + + #expect( + storage.loadLastFilters(for: "users", connectionId: connectionId, databaseName: "db", schemaName: "app").isEmpty + ) + #expect( + storage.loadLastFilters(for: "users", connectionId: connectionId, databaseName: "db", schemaName: "public") == filters + ) + } + + @Test("Saving an empty filter set clears the stored filters") + func savingEmptyClearsState() { + let (storage, directory) = makeStorage() + defer { try? FileManager.default.removeItem(at: directory) } + let connectionId = UUID() + storage.saveLastFilters( + [TestFixtures.makeTableFilter()], for: "users", connectionId: connectionId, databaseName: "db", schemaName: nil + ) + storage.saveLastFilters([], for: "users", connectionId: connectionId, databaseName: "db", schemaName: nil) + + #expect( + storage.loadLastFilters(for: "users", connectionId: connectionId, databaseName: "db", schemaName: nil).isEmpty + ) + } + + @Test("New installs default to restoring the last filter") + func defaultPanelStateRestoresLast() { + let (storage, directory) = makeStorage() + defer { try? FileManager.default.removeItem(at: directory) } + #expect(storage.loadSettings().panelState == .restoreLast) + } + + @Test("Migration upgrades a stored Always Hide setting to Restore Last") + func migrationUpgradesAlwaysHide() throws { + let suiteName = "FilterSettingsStorageTests-\(UUID().uuidString)" + let defaults = try #require(UserDefaults(suiteName: suiteName)) + let directory = FileManager.default.temporaryDirectory + .appendingPathComponent("FilterSettingsStorageTests-\(UUID().uuidString)", isDirectory: true) + defer { try? FileManager.default.removeItem(at: directory) } + + let stored = FilterSettings(panelState: .alwaysHide) + defaults.set(try JSONEncoder().encode(stored), forKey: "com.TablePro.filter.settings") + + let storage = FilterSettingsStorage(filterStateDirectory: directory, defaults: defaults) + + #expect(storage.loadSettings().panelState == .restoreLast) + } +} diff --git a/TableProTests/Views/Main/FilterRestoreTests.swift b/TableProTests/Views/Main/FilterRestoreTests.swift new file mode 100644 index 000000000..f2fc0efb3 --- /dev/null +++ b/TableProTests/Views/Main/FilterRestoreTests.swift @@ -0,0 +1,66 @@ +// +// FilterRestoreTests.swift +// TableProTests +// + +import Foundation +@testable import TablePro +import Testing + +@Suite("FilterRestore") +@MainActor +struct FilterRestoreTests { + @Test("Restore Last applies saved filters and shows the bar") + func restoreLastAppliesSavedFiltersAndShowsBar() { + let saved = [TestFixtures.makeTableFilter(column: "email", value: "a@b.com")] + + let result = FilterCoordinator.resolvedRestoredState( + panelState: .restoreLast, + saved: saved, + current: TabFilterState() + ) + + #expect(result.filters == saved) + #expect(result.appliedFilters == saved) + #expect(result.isVisible) + } + + @Test("Restore Last with no saved filters keeps the bar hidden") + func restoreLastWithNoFiltersKeepsBarHidden() { + let result = FilterCoordinator.resolvedRestoredState( + panelState: .restoreLast, + saved: [], + current: TabFilterState() + ) + + #expect(result.appliedFilters.isEmpty) + #expect(!result.isVisible) + } + + @Test("Always Show reveals the bar even without saved filters") + func alwaysShowRevealsBarWithoutFilters() { + let result = FilterCoordinator.resolvedRestoredState( + panelState: .alwaysShow, + saved: [], + current: TabFilterState() + ) + + #expect(result.appliedFilters.isEmpty) + #expect(result.isVisible) + } + + @Test("Always Hide never restores filters or shows the bar") + func alwaysHideRestoresNothing() { + let saved = [TestFixtures.makeTableFilter(column: "email")] + + let result = FilterCoordinator.resolvedRestoredState( + panelState: .alwaysHide, + saved: saved, + current: TabFilterState() + ) + + #expect(result.filters.isEmpty) + #expect(result.appliedFilters.isEmpty) + #expect(!result.isVisible) + } +} diff --git a/docs/features/filtering.mdx b/docs/features/filtering.mdx index 46b513587..2f5731039 100644 --- a/docs/features/filtering.mdx +++ b/docs/features/filtering.mdx @@ -9,7 +9,7 @@ Press `Cmd+F` while viewing a table to open the filter panel. Type a raw SQL WHE Each row has a column picker, operator, value field, and **+**/**−** buttons. Multiple rows combine with **AND** or **OR** (toggle in the header). Click **Apply** or press `Enter` to activate. Click **Unset** to clear all. -Filters are per-tab. Tables with active filters open in a new tab when you click another table. +When you reopen a table, TablePro restores the filter you last applied to it, including after you quit and relaunch. Filters are remembered per connection. Tables with active filters open in a new tab when you click another table. ## Operators @@ -60,3 +60,5 @@ Save and load filter configurations via the **⋯** menu in the header. | Default Column | Raw SQL, Primary Key, Any Column | | Default Operator | Equal, Contains | | Panel State | Always Hide, Always Show, Restore Last Filter | + +**Panel State** controls what happens when you reopen a table. **Restore Last Filter** (the default) brings back the filter you last applied. **Always Hide** reopens tables unfiltered with the panel closed. **Always Show** keeps the filter panel visible even when no filter is set. From 7da873e49784e44e85f3085beef4c514a76950ea Mon Sep 17 00:00:00 2001 From: Ngo Quoc Dat Date: Fri, 22 May 2026 21:02:07 +0700 Subject: [PATCH 2/2] test(datagrid): cover on-disk filter persistence across storage instances (#1347) --- .../Storage/FilterSettingsStorageTests.swift | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/TableProTests/Core/Storage/FilterSettingsStorageTests.swift b/TableProTests/Core/Storage/FilterSettingsStorageTests.swift index d2f334158..888e419b0 100644 --- a/TableProTests/Core/Storage/FilterSettingsStorageTests.swift +++ b/TableProTests/Core/Storage/FilterSettingsStorageTests.swift @@ -129,4 +129,25 @@ struct FilterSettingsStorageTests { #expect(storage.loadSettings().panelState == .restoreLast) } + + @Test("Saved filters decode from disk in a fresh storage instance") + func persistsAcrossInstances() { + let suiteName = "FilterSettingsStorageTests-\(UUID().uuidString)" + guard let defaults = UserDefaults(suiteName: suiteName) else { + fatalError("Failed to create UserDefaults suite for tests") + } + let directory = FileManager.default.temporaryDirectory + .appendingPathComponent("FilterSettingsStorageTests-\(UUID().uuidString)", isDirectory: true) + defer { try? FileManager.default.removeItem(at: directory) } + let connectionId = UUID() + let filters = [TestFixtures.makeTableFilter(column: "email", value: "a@b.com")] + + let writer = FilterSettingsStorage(filterStateDirectory: directory, defaults: defaults) + writer.saveLastFilters(filters, for: "users", connectionId: connectionId, databaseName: "db", schemaName: nil) + + let reader = FilterSettingsStorage(filterStateDirectory: directory, defaults: defaults) + #expect( + reader.loadLastFilters(for: "users", connectionId: connectionId, databaseName: "db", schemaName: nil) == filters + ) + } }