diff --git a/CHANGELOG.md b/CHANGELOG.md index 3d106907d..0dfb44617 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,7 +19,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Connecting to a PostgreSQL-compatible engine that doesn't implement the pg_matviews catalog (such as db9.ai) no longer fails to load tables. (#1383) - Filtering a table now updates the row count and page count in the bottom-right to match the filtered result, instead of showing the whole-table totals. -- Reopening a table now restores the filter you had applied, instead of clearing it. Filters are remembered per connection. (#1347) +- Reopening a table now restores the filter you had applied. Removing or clearing a filter takes effect right away and is remembered, so a table you unfiltered reopens with no filter. Filters are remembered per connection. (#1347) - Quick switcher panel height now fits its results instead of leaving a large empty area below short lists. (#1349) - Importing connections from TablePlus brings over saved passwords again. A recent release looked under the wrong keychain name, so connections imported with no passwords and no warning. - Importing an SSH connection from TablePlus no longer fills in a fake private key path such as `~/.ssh/Import a private key...` when no key was selected. Empty TLS certificate paths are skipped too. diff --git a/TablePro/Core/Coordinators/FilterCoordinator.swift b/TablePro/Core/Coordinators/FilterCoordinator.swift index 5a98cecb0..7c66c5e1d 100644 --- a/TablePro/Core/Coordinators/FilterCoordinator.swift +++ b/TablePro/Core/Coordinators/FilterCoordinator.swift @@ -47,11 +47,7 @@ final class FilterCoordinator { ) parent.tabManager.mutate(at: capturedTabIndex) { $0.content.query = newQuery } - - if !capturedFilters.isEmpty { - saveLastFilters(for: capturedTableName) - } - + saveLastFilters(for: capturedTableName) parent.runQuery() } } @@ -81,6 +77,7 @@ final class FilterCoordinator { ) parent.tabManager.mutate(at: capturedTabIndex) { $0.content.query = newQuery } + clearLastFilters(for: capturedTableName) parent.runQuery() } } @@ -215,6 +212,37 @@ final class FilterCoordinator { } } + enum RemoveFilterOutcome: Equatable { + case noChange + case clear + case reapply([TableFilter]) + } + + static func removeFilterOutcome( + removing filter: TableFilter, + from appliedFilters: [TableFilter] + ) -> RemoveFilterOutcome { + guard appliedFilters.contains(where: { $0.id == filter.id }) else { return .noChange } + let remaining = appliedFilters.filter { $0.id != filter.id } + return remaining.isEmpty ? .clear : .reapply(remaining) + } + + func removeFilterAndReload(_ filter: TableFilter) { + let outcome = Self.removeFilterOutcome( + removing: filter, + from: selectedTabFilterState.appliedFilters + ) + removeFilter(filter) + switch outcome { + case .noChange: + break + case .clear: + clearFiltersAndReload() + case .reapply(let remaining): + applyFilters(remaining) + } + } + func updateFilter(_ filter: TableFilter) { mutateSelectedTabFilterState { state in if let index = state.filters.firstIndex(where: { $0.id == filter.id }) { @@ -343,6 +371,16 @@ final class FilterCoordinator { ) } + func clearLastFilters(for tableName: String) { + guard let tab = parent.tabManager.selectedTab else { return } + FilterSettingsStorage.shared.clearLastFilters( + 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, diff --git a/TablePro/Views/Filter/FilterPanelView.swift b/TablePro/Views/Filter/FilterPanelView.swift index 9558b13e4..9719718bc 100644 --- a/TablePro/Views/Filter/FilterPanelView.swift +++ b/TablePro/Views/Filter/FilterPanelView.swift @@ -199,15 +199,9 @@ struct FilterPanelView: View { focusedFilterId = filterState.filters.last?.id }, onRemove: { - let hadAppliedFilters = filterState.hasAppliedFilters - coordinator.removeFilter(filter) + coordinator.removeFilterAndReload(filter) if filterState.filters.isEmpty { - if hadAppliedFilters { - coordinator.clearFilterState() - onUnset() - } else { - coordinator.closeFilterPanel() - } + coordinator.closeFilterPanel() } }, onSubmit: { applyAllValidFilters() }, diff --git a/TablePro/Views/Main/Extensions/MainContentCoordinator+FilterState.swift b/TablePro/Views/Main/Extensions/MainContentCoordinator+FilterState.swift index 77c5660ee..31287e568 100644 --- a/TablePro/Views/Main/Extensions/MainContentCoordinator+FilterState.swift +++ b/TablePro/Views/Main/Extensions/MainContentCoordinator+FilterState.swift @@ -36,6 +36,10 @@ extension MainContentCoordinator { filterCoordinator.removeFilter(filter) } + func removeFilterAndReload(_ filter: TableFilter) { + filterCoordinator.removeFilterAndReload(filter) + } + func updateFilter(_ filter: TableFilter) { filterCoordinator.updateFilter(filter) } diff --git a/TableProTests/Core/Storage/FilterSettingsStorageTests.swift b/TableProTests/Core/Storage/FilterSettingsStorageTests.swift index 888e419b0..ae7973378 100644 --- a/TableProTests/Core/Storage/FilterSettingsStorageTests.swift +++ b/TableProTests/Core/Storage/FilterSettingsStorageTests.swift @@ -150,4 +150,19 @@ struct FilterSettingsStorageTests { reader.loadLastFilters(for: "users", connectionId: connectionId, databaseName: "db", schemaName: nil) == filters ) } + + @Test("Clearing removes the stored filters so a reopen restores nothing") + func clearRemovesStoredFilters() { + 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) + storage.clearLastFilters(for: "users", connectionId: connectionId, databaseName: "db", schemaName: nil) + + #expect( + storage.loadLastFilters(for: "users", connectionId: connectionId, databaseName: "db", schemaName: nil).isEmpty + ) + } } diff --git a/TableProTests/Views/Main/FilterRestoreTests.swift b/TableProTests/Views/Main/FilterRestoreTests.swift index f2fc0efb3..30e237983 100644 --- a/TableProTests/Views/Main/FilterRestoreTests.swift +++ b/TableProTests/Views/Main/FilterRestoreTests.swift @@ -63,4 +63,26 @@ struct FilterRestoreTests { #expect(result.appliedFilters.isEmpty) #expect(!result.isVisible) } + + @Test("Removing a filter that isn't applied changes nothing") + func removeUnappliedFilterIsNoChange() { + let applied = TestFixtures.makeTableFilter(column: "email") + let other = TestFixtures.makeTableFilter(column: "name") + #expect(FilterCoordinator.removeFilterOutcome(removing: other, from: [applied]) == .noChange) + } + + @Test("Removing the only applied filter clears") + func removeOnlyAppliedFilterClears() { + let only = TestFixtures.makeTableFilter(column: "email") + #expect(FilterCoordinator.removeFilterOutcome(removing: only, from: [only]) == .clear) + } + + @Test("Removing one of several applied filters reapplies the rest") + func removeOneOfSeveralReappliesRemainder() { + let first = TestFixtures.makeTableFilter(column: "email") + let second = TestFixtures.makeTableFilter(column: "name") + #expect( + FilterCoordinator.removeFilterOutcome(removing: first, from: [first, second]) == .reapply([second]) + ) + } } diff --git a/docs/features/filtering.mdx b/docs/features/filtering.mdx index 2f5731039..7679d39a8 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. -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. +When you reopen a table, TablePro restores the filter you last applied to it, including after you quit and relaunch. If you clear the filter with **Unset** or remove its conditions with **−**, reopening the table shows it unfiltered. Filters are remembered per connection. Tables with active filters open in a new tab when you click another table. ## Operators