From 2fa3e5703d313ea7f52534a9206b713adfbfbb7d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nguy=E1=BB=85n=20Nam=20Long?= Date: Wed, 10 Jun 2026 22:16:53 +0700 Subject: [PATCH] fix(datagrid): D12 Delete/right-click multi-select, D13 filter panel redesign, D14 refresh fix - Delete key now reads gridSelection.affectedRows first, mirroring the copy() pattern - rightMouseDown override preserves multi-row selection when right-clicking inside it - Filter panel: "Unset" -> "Clear" (clearAppliedFilters, keeps rows); per-row Apply buttons removed; "Apply Only This Filter" added to row context menu; tri-state checkbox in header toggles all rows enabled/disabled; "Remove All Filters" added to options menu (clearFilterState) - Refresh: cancel + nil currentQueryTask + reset isExecuting before runQuery so Cmd+R while a query runs always starts a fresh query --- CHANGELOG.md | 9 ++++ TablePro/Views/Filter/FilterPanelView.swift | 53 +++++++++++++++++-- TablePro/Views/Filter/FilterRowView.swift | 30 ++++------- .../MainContentCoordinator+Refresh.swift | 4 ++ .../Views/Results/KeyHandlingTableView.swift | 24 ++++++++- 5 files changed, 93 insertions(+), 27 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 32625cb83..9c20fd8ea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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) diff --git a/TablePro/Views/Filter/FilterPanelView.swift b/TablePro/Views/Filter/FilterPanelView.swift index 7c413aa74..f594f20e3 100644 --- a/TablePro/Views/Filter/FilterPanelView.swift +++ b/TablePro/Views/Filter/FilterPanelView.swift @@ -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)) @@ -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() @@ -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: { @@ -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 diff --git a/TablePro/Views/Filter/FilterRowView.swift b/TablePro/Views/Filter/FilterRowView.swift index c9ebcc9a9..b0491e613 100644 --- a/TablePro/Views/Filter/FilterRowView.swift +++ b/TablePro/Views/Filter/FilterRowView.swift @@ -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 @@ -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) @@ -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: { diff --git a/TablePro/Views/Main/Extensions/MainContentCoordinator+Refresh.swift b/TablePro/Views/Main/Extensions/MainContentCoordinator+Refresh.swift index 4abdbcd70..146a83ce4 100644 --- a/TablePro/Views/Main/Extensions/MainContentCoordinator+Refresh.swift +++ b/TablePro/Views/Main/Extensions/MainContentCoordinator+Refresh.swift @@ -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() } @@ -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 } rebuildTableQuery(at: tabIndex) runQuery() } diff --git a/TablePro/Views/Results/KeyHandlingTableView.swift b/TablePro/Views/Results/KeyHandlingTableView.swift index 0b39899f7..ed6b6d3ec 100644 --- a/TablePro/Views/Results/KeyHandlingTableView.swift +++ b/TablePro/Views/Results/KeyHandlingTableView.swift @@ -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)) } @@ -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 @@ -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) } @@ -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)