diff --git a/CHANGELOG.md b/CHANGELOG.md index c973fcbd4..413265fa2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - OpenCode Zen as an AI provider. Add it from the provider list and paste an OpenCode key, or leave the key blank to use the free models; the model list loads automatically, covering the Claude, GPT, Gemini, and open models Zen serves. (#1400) +### Changed + +- Clearing a query with the trash button now also clears its results, and a new Clear Results item on the results right-click menu clears results on their own. (#1256) + ### Fixed - Custom and OpenAI-compatible AI providers now work when the base URL already ends in `/v1`, instead of building a doubled `/v1/v1/` path that failed. (#1400) diff --git a/TablePro/Views/Editor/QueryEditorView.swift b/TablePro/Views/Editor/QueryEditorView.swift index f4e4892c3..211d45a4b 100644 --- a/TablePro/Views/Editor/QueryEditorView.swift +++ b/TablePro/Views/Editor/QueryEditorView.swift @@ -32,6 +32,7 @@ struct QueryEditorView: View { var onAIExplain: ((String) -> Void)? var onAIOptimize: ((String) -> Void)? var onSaveAsFavorite: ((String) -> Void)? + var onClearResults: (() -> Void)? @State private var vimMode: VimMode = .normal @@ -90,7 +91,10 @@ struct QueryEditorView: View { Spacer() // Clear button - Button(action: { queryText = "" }) { + Button(action: { + queryText = "" + onClearResults?() + }) { Image(systemName: "trash") .frame(width: 24, height: 24) } diff --git a/TablePro/Views/Main/Child/DataTabGridDelegate.swift b/TablePro/Views/Main/Child/DataTabGridDelegate.swift index bcd2d48c8..68ba9774c 100644 --- a/TablePro/Views/Main/Child/DataTabGridDelegate.swift +++ b/TablePro/Views/Main/Child/DataTabGridDelegate.swift @@ -69,6 +69,14 @@ final class DataTabGridDelegate: DataGridViewDelegate { AppCommands.shared.exportQueryResults.send(()) } + func dataGridClearResults() { + coordinator?.clearActiveQueryResults() + } + + func dataGridCanClearResults() -> Bool { + coordinator?.canClearActiveQueryResults ?? false + } + func dataGridNavigateFK(value: String, fkInfo: ForeignKeyInfo) { coordinator?.navigateToFKReference(value: value, fkInfo: fkInfo) } diff --git a/TablePro/Views/Main/Child/MainEditorContentView.swift b/TablePro/Views/Main/Child/MainEditorContentView.swift index b02ccfed0..0d4ef29a6 100644 --- a/TablePro/Views/Main/Child/MainEditorContentView.swift +++ b/TablePro/Views/Main/Child/MainEditorContentView.swift @@ -320,7 +320,8 @@ struct MainEditorContentView: View { onSaveAsFavorite: { text in guard !text.isEmpty else { return } coordinator.favoriteDialogQuery = FavoriteDialogQuery(query: text) - } + }, + onClearResults: { coordinator.clearActiveQueryResults() } ) } .frame(maxWidth: .infinity, maxHeight: .infinity) diff --git a/TablePro/Views/Main/Extensions/MainContentCoordinator+SidebarActions.swift b/TablePro/Views/Main/Extensions/MainContentCoordinator+SidebarActions.swift index e375d907f..4c3818f6a 100644 --- a/TablePro/Views/Main/Extensions/MainContentCoordinator+SidebarActions.swift +++ b/TablePro/Views/Main/Extensions/MainContentCoordinator+SidebarActions.swift @@ -37,6 +37,29 @@ extension MainContentCoordinator { } } + var canClearActiveQueryResults: Bool { + guard let tab = tabManager.selectedTab, tab.tabType == .query else { return false } + return !tabSessionRegistry.tableRows(for: tab.id).rows.isEmpty || tab.execution.lastExecutedAt != nil + } + + func clearActiveQueryResults() { + guard let tabIdx = tabManager.selectedTabIndex else { return } + let tabId = tabManager.tabs[tabIdx].id + setActiveTableRows(TableRows(), for: tabId) + tabManager.mutate(at: tabIdx) { tab in + tab.display.resultSets = [] + tab.display.activeResultSetId = nil + tab.execution.errorMessage = nil + tab.execution.rowsAffected = 0 + tab.execution.executionTime = nil + tab.execution.statusMessage = nil + tab.execution.lastExecutedAt = nil + tab.schemaVersion += 1 + tab.display.isResultsCollapsed = true + } + toolbarState.isResultsCollapsed = true + } + // MARK: - Table Operations func createNewTable() { diff --git a/TablePro/Views/Results/DataGridRowView.swift b/TablePro/Views/Results/DataGridRowView.swift index 9f49797bf..b1173e669 100644 --- a/TablePro/Views/Results/DataGridRowView.swift +++ b/TablePro/Views/Results/DataGridRowView.swift @@ -259,6 +259,16 @@ class DataGridRowView: NSTableRowView { exportItem.target = self menu.addItem(exportItem) + if coordinator.delegate?.dataGridCanClearResults() == true { + let clearResultsItem = NSMenuItem( + title: String(localized: "Clear Results"), + action: #selector(clearResults), + keyEquivalent: "" + ) + clearResultsItem.target = self + menu.addItem(clearResultsItem) + } + if coordinator.isEditable { let duplicateItem = NSMenuItem( title: String(localized: "Duplicate"), action: #selector(duplicateRow), keyEquivalent: "") @@ -412,6 +422,10 @@ class DataGridRowView: NSTableRowView { AppCommands.shared.exportQueryResults.send(()) } + @objc private func clearResults() { + coordinator?.delegate?.dataGridClearResults() + } + @objc private func copyAsJson() { guard let coordinator else { return } coordinator.copyRowsAsJson(at: selectedOrCurrentIndices(in: coordinator)) diff --git a/TablePro/Views/Results/DataGridViewDelegate.swift b/TablePro/Views/Results/DataGridViewDelegate.swift index a7f315c1b..0b6b49b07 100644 --- a/TablePro/Views/Results/DataGridViewDelegate.swift +++ b/TablePro/Views/Results/DataGridViewDelegate.swift @@ -23,6 +23,8 @@ protocol DataGridViewDelegate: AnyObject { func dataGridNavigateFK(value: String, fkInfo: ForeignKeyInfo) func dataGridDuplicateRow() func dataGridExportResults() + func dataGridClearResults() + func dataGridCanClearResults() -> Bool func dataGridHideColumn(_ columnName: String) func dataGridShowAllColumns() func dataGridRefresh() @@ -50,6 +52,8 @@ extension DataGridViewDelegate { func dataGridNavigateFK(value: String, fkInfo: ForeignKeyInfo) {} func dataGridDuplicateRow() {} func dataGridExportResults() {} + func dataGridClearResults() {} + func dataGridCanClearResults() -> Bool { false } func dataGridHideColumn(_ columnName: String) {} func dataGridShowAllColumns() {} func dataGridRefresh() {} diff --git a/TableProTests/Views/Main/ClearQueryResultsTests.swift b/TableProTests/Views/Main/ClearQueryResultsTests.swift new file mode 100644 index 000000000..867200f8a --- /dev/null +++ b/TableProTests/Views/Main/ClearQueryResultsTests.swift @@ -0,0 +1,90 @@ +import Foundation +import TableProPluginKit +import Testing + +@testable import TablePro + +@Suite("ClearQueryResults") +struct ClearQueryResultsTests { + @Test("Clearing results empties rows, result sets, and execution state") + @MainActor + func clearEmptiesResultsAndState() throws { + let coordinator = Self.makeCoordinator() + defer { coordinator.teardown() } + + coordinator.tabManager.addTab(databaseName: "db") + let tabId = try #require(coordinator.tabManager.selectedTab?.id) + let index = try #require(coordinator.tabManager.selectedTabIndex) + + coordinator.setActiveTableRows(TestFixtures.makeTableRows(rowCount: 3), for: tabId) + coordinator.tabManager.mutate(at: index) { tab in + tab.execution.lastExecutedAt = Date() + tab.execution.rowsAffected = 3 + tab.execution.executionTime = 0.12 + tab.display.activeResultSetId = UUID() + } + + coordinator.clearActiveQueryResults() + + #expect(coordinator.tabSessionRegistry.tableRows(for: tabId).rows.isEmpty) + let tab = try #require(coordinator.tabManager.selectedTab) + #expect(tab.display.resultSets.isEmpty) + #expect(tab.display.activeResultSetId == nil) + #expect(tab.execution.lastExecutedAt == nil) + #expect(tab.execution.rowsAffected == 0) + #expect(tab.execution.executionTime == nil) + #expect(tab.display.isResultsCollapsed) + } + + @Test("Clearing results leaves the query text intact") + @MainActor + func clearKeepsQueryText() throws { + let coordinator = Self.makeCoordinator() + defer { coordinator.teardown() } + + coordinator.tabManager.addTab(initialQuery: "SELECT 1", databaseName: "db") + let tabId = try #require(coordinator.tabManager.selectedTab?.id) + coordinator.setActiveTableRows(TestFixtures.makeTableRows(rowCount: 2), for: tabId) + + coordinator.clearActiveQueryResults() + + #expect(coordinator.tabManager.selectedTab?.content.query == "SELECT 1") + } + + @Test("Can clear only when a query tab has results") + @MainActor + func canClearGating() throws { + let coordinator = Self.makeCoordinator() + defer { coordinator.teardown() } + + coordinator.tabManager.addTab(databaseName: "db") + #expect(coordinator.canClearActiveQueryResults == false) + + let tabId = try #require(coordinator.tabManager.selectedTab?.id) + coordinator.setActiveTableRows(TestFixtures.makeTableRows(rowCount: 1), for: tabId) + #expect(coordinator.canClearActiveQueryResults == true) + } + + @Test("Cannot clear results on a table tab") + @MainActor + func cannotClearOnTableTab() throws { + let coordinator = Self.makeCoordinator() + defer { coordinator.teardown() } + + try coordinator.tabManager.addTableTab(tableName: "users", databaseType: .mysql, databaseName: "db") + let tabId = try #require(coordinator.tabManager.selectedTab?.id) + coordinator.setActiveTableRows(TestFixtures.makeTableRows(rowCount: 3), for: tabId) + + #expect(coordinator.canClearActiveQueryResults == false) + } + + @MainActor + private static func makeCoordinator() -> MainContentCoordinator { + MainContentCoordinator( + connection: TestFixtures.makeConnection(database: "db"), + tabManager: QueryTabManager(), + changeManager: DataChangeManager(), + toolbarState: ConnectionToolbarState() + ) + } +} diff --git a/docs/features/sql-editor.mdx b/docs/features/sql-editor.mdx index d5c8938d8..885b84f69 100644 --- a/docs/features/sql-editor.mdx +++ b/docs/features/sql-editor.mdx @@ -129,6 +129,10 @@ See [Query Parameters](/features/query-parameters) for details. Results appear in the data grid below the editor with row count and execution time. Large result sets are paginated. +#### Clearing Results + +The trash button in the editor toolbar clears the query and its results together. To clear results but keep the query, right-click the results and choose **Clear Results**. Running a query again repopulates the panel. + #### Collapsible Results Panel Toggle the results panel with `Cmd+Opt+R` or the toolbar button to give the editor full height. The panel auto-expands when a new query executes.