Skip to content
Closed
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
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

- Switcher, menus, and alerts now use each database's own container name: Dataset for BigQuery, Keyspace for Cassandra and ScyllaDB. (#509)

### Changed

- Filter panel header "Unset" renamed to "Clear": it now keeps filter rows in place and only removes the applied state, returning the table to unfiltered results. Use "Remove All Filters" in the filter options menu to discard all filter rows at once.
- Per-row Apply and Applied buttons removed from the filter panel; "Apply Only This Filter" is now in each row's right-click context menu.
- A tri-state checkbox in the filter panel header toggles all filter rows enabled or disabled at once.

### Fixed

- Delete key now respects cell-range selection in the data grid, removing all rows covered by the selection instead of ignoring it.
- Right-clicking a row inside a multi-row selection no longer collapses the selection before the context menu appears.
- Pressing Cmd+R to refresh while a query is executing now cancels the running query and starts a fresh one instead of silently doing nothing.
- iCloud Sync between the iPhone and Mac apps: the iOS app now uses the Production CloudKit environment, so a development build no longer syncs into a separate database the Mac never reads.
- Exports no longer fail mid-table on servers that enforce a statement time limit; the export session disables the limit and restores it afterwards, the same way mysqldump does. (#1633)

Expand Down
53 changes: 49 additions & 4 deletions TablePro/Views/Filter/FilterPanelView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -67,8 +67,43 @@ struct FilterPanelView: View {
.onPreferenceChange(FilterRowsHeightKey.self) { filterRowsHeight = $0 }
}

private var allFiltersCheckboxImage: String {
switch allFiltersEnabledState {
case true: return "checkmark.square.fill"
case false: return "square"
case .none: return "minus.square.fill"
}
}

private var allFiltersEnabledState: Bool? {
guard !filterState.filters.isEmpty else { return false }
let enabledCount = filterState.filters.count { $0.isEnabled }
if enabledCount == filterState.filters.count { return true }
if enabledCount == 0 { return false }
return nil
}

private func toggleAllFiltersEnabled() {
let allEnabled = filterState.filters.allSatisfy { $0.isEnabled }
let newState = !allEnabled
for filter in filterState.filters {
var updated = filter
updated.isEnabled = newState
coordinator.updateFilter(updated)
}
}

private var filterHeader: some View {
HStack(spacing: 8) {
if !filterState.filters.isEmpty {
Button(action: toggleAllFiltersEnabled) {
Image(systemName: allFiltersCheckboxImage)
.foregroundStyle(.primary)
}
.buttonStyle(.plain)
.help(String(localized: "Enable or disable all filters"))
}

Text("Filters")
.font(.callout.weight(.medium))

Expand All @@ -88,15 +123,15 @@ struct FilterPanelView: View {

filterOptionsMenu

Button("Unset") {
coordinator.clearFilterState()
Button("Clear") {
coordinator.clearAppliedFilters()
onUnset()
coordinator.focusActiveGrid()
}
.buttonStyle(.bordered)
.controlSize(.small)
.disabled(!filterState.hasAppliedFilters)
.help(String(localized: "Remove all filters and reload"))
.help(String(localized: "Clear applied filters without removing filter rows"))

Button("Apply") {
applyAllValidFilters()
Expand Down Expand Up @@ -172,6 +207,17 @@ struct FilterPanelView: View {

Divider()

Button(role: .destructive) {
coordinator.clearFilterState()
onUnset()
coordinator.focusActiveGrid()
} label: {
Label(String(localized: "Remove All Filters"), systemImage: "xmark.circle")
}
.disabled(filterState.filters.isEmpty)

Divider()

Button {
showSettingsPopover.toggle()
} label: {
Expand All @@ -198,7 +244,6 @@ struct FilterPanelView: View {
completions: completionItems(),
enumValuesByColumn: enumValuesByColumn,
rawSQLCompletionProvider: rawSQLCompletionProvider,
isApplied: filterState.commit == .solo(filter.id),
onAdd: {
coordinator.addFilter(columns: columns, primaryKeyColumn: primaryKeyColumn)
focusedFilterId = filterState.filters.last?.id
Expand Down
30 changes: 9 additions & 21 deletions TablePro/Views/Filter/FilterRowView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ struct FilterRowView: View {
let completions: [String]
var enumValuesByColumn: [String: [String]] = [:]
var rawSQLCompletionProvider: RawSQLFilterCompletionProvider?
let isApplied: Bool
let onAdd: () -> Void
let onDuplicate: () -> Void
let onRemove: () -> Void
Expand Down Expand Up @@ -160,28 +159,8 @@ struct FilterRowView: View {
}
}

@ViewBuilder
private var soloApplyButton: some View {
if isApplied {
Button(String(localized: "Applied"), action: onApply)
.buttonStyle(.borderedProminent)
} else {
Button(String(localized: "Apply"), action: onApply)
.buttonStyle(.bordered)
}
}

private var rowButtons: some View {
HStack(spacing: 4) {
soloApplyButton
.controlSize(.small)
.disabled(!filter.isValid)
.accessibilityLabel(String(localized: "Apply only this filter"))
.accessibilityValue(isApplied ? String(localized: "Applied") : "")
.help(isApplied
? String(localized: "Filtering by only this row")
: String(localized: "Filter by only this row"))

Button(action: onAdd) {
Image(systemName: "plus")
.frame(width: rowButtonGlyphSize, height: rowButtonGlyphSize)
Expand All @@ -204,6 +183,15 @@ struct FilterRowView: View {

@ViewBuilder
private var rowContextMenu: some View {
Button {
onApply()
} label: {
Label(String(localized: "Apply Only This Filter"), systemImage: "checkmark.circle")
}
.disabled(!filter.isValid)

Divider()

Button {
onAdd()
} label: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ extension MainContentCoordinator {
if let (tab, tabIndex) = tabManager.selectedTabAndIndex,
tab.tabType == .table {
currentQueryTask?.cancel()
currentQueryTask = nil
tabManager.mutate(at: tabIndex) { $0.execution.isExecuting = false }
rebuildTableQuery(at: tabIndex)
runQuery()
}
Expand All @@ -43,6 +45,8 @@ extension MainContentCoordinator {
if let (tab, tabIndex) = tabManager.selectedTabAndIndex,
tab.tabType == .table {
currentQueryTask?.cancel()
currentQueryTask = nil
tabManager.mutate(at: tabIndex) { $0.execution.isExecuting = false }
Comment on lines 47 to +49

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Avoid clearing the running task before restarting

When Cmd+R is pressed while a table query is still running, these lines discard the only currentQueryTask reference and mark the tab idle before runQuery() starts the replacement. The canceled task can still be inside queryExecutor.executeQuery; when it later observes cancellation, executeQueryInternal calls resetExecutionState(...), which unconditionally sets isExecuting = false and currentQueryTask = nil for the tab, thereby clearing the newly-started refresh task's UI/task state. Because the task reference is nil before executeQueryInternal runs, its existing cancelQuery() path is also skipped for this refresh case.

Useful? React with 👍 / 👎.

rebuildTableQuery(at: tabIndex)
runQuery()
}
Expand Down
24 changes: 22 additions & 2 deletions TablePro/Views/Results/KeyHandlingTableView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,12 @@ final class KeyHandlingTableView: NSTableView {

@objc func delete(_ sender: Any?) {
guard coordinator?.isEditable == true else { return }
if let controller = gridSelection, !controller.isEmpty {
let rows = controller.selection.affectedRows
guard !rows.isEmpty else { return }
coordinator?.delegate?.dataGridDeleteRows(Set(rows))
return
}
guard !selectedRowIndexes.isEmpty else { return }
coordinator?.delegate?.dataGridDeleteRows(Set(selectedRowIndexes))
}
Expand Down Expand Up @@ -279,7 +285,8 @@ final class KeyHandlingTableView: NSTableView {
override func validateUserInterfaceItem(_ item: NSValidatedUserInterfaceItem) -> Bool {
switch item.action {
case #selector(delete(_:)), #selector(deleteBackward(_:)):
return coordinator?.isEditable == true && !selectedRowIndexes.isEmpty
let hasGridSelection = gridSelection?.isEmpty == false
return coordinator?.isEditable == true && (hasGridSelection || !selectedRowIndexes.isEmpty)
case #selector(copy(_:)):
let hasGridSelection = gridSelection?.isEmpty == false
return hasGridSelection || !selectedRowIndexes.isEmpty
Expand Down Expand Up @@ -407,7 +414,7 @@ final class KeyHandlingTableView: NSTableView {

private func deleteSelectedRowsIfPossible() {
guard coordinator?.isEditable == true else { return }
guard !selectedRowIndexes.isEmpty else { return }
guard gridSelection?.isEmpty == false || !selectedRowIndexes.isEmpty else { return }
delete(nil)
}

Expand Down Expand Up @@ -521,6 +528,19 @@ final class KeyHandlingTableView: NSTableView {
scrollColumnToVisible(prevColumn)
}

override func rightMouseDown(with event: NSEvent) {
let point = convert(event.locationInWindow, from: nil)
let clickedRow = row(at: point)
if clickedRow >= 0, selectedRowIndexes.contains(clickedRow) {
window?.makeFirstResponder(self)
if let menu = menu(for: event) {
NSMenu.popUpContextMenu(menu, with: event, for: self)
}
return
}
super.rightMouseDown(with: event)
}

override func menu(for event: NSEvent) -> NSMenu? {
let point = convert(event.locationInWindow, from: nil)
let clickedRow = row(at: point)
Expand Down
Loading