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
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
48 changes: 43 additions & 5 deletions TablePro/Core/Coordinators/FilterCoordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
}
Expand Down Expand Up @@ -81,6 +77,7 @@ final class FilterCoordinator {
)

parent.tabManager.mutate(at: capturedTabIndex) { $0.content.query = newQuery }
clearLastFilters(for: capturedTableName)
parent.runQuery()
}
}
Expand Down Expand Up @@ -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 }) {
Expand Down Expand Up @@ -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,
Expand Down
10 changes: 2 additions & 8 deletions TablePro/Views/Filter/FilterPanelView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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() },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@ extension MainContentCoordinator {
filterCoordinator.removeFilter(filter)
}

func removeFilterAndReload(_ filter: TableFilter) {
filterCoordinator.removeFilterAndReload(filter)
}

func updateFilter(_ filter: TableFilter) {
filterCoordinator.updateFilter(filter)
}
Expand Down
15 changes: 15 additions & 0 deletions TableProTests/Core/Storage/FilterSettingsStorageTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
}
}
22 changes: 22 additions & 0 deletions TableProTests/Views/Main/FilterRestoreTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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])
)
}
}
2 changes: 1 addition & 1 deletion docs/features/filtering.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Loading