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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
6 changes: 5 additions & 1 deletion TablePro/Views/Editor/QueryEditorView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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)
}
Expand Down
8 changes: 8 additions & 0 deletions TablePro/Views/Main/Child/DataTabGridDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
3 changes: 2 additions & 1 deletion TablePro/Views/Main/Child/MainEditorContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
14 changes: 14 additions & 0 deletions TablePro/Views/Results/DataGridRowView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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: "")
Expand Down Expand Up @@ -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))
Expand Down
4 changes: 4 additions & 0 deletions TablePro/Views/Results/DataGridViewDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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() {}
Expand Down
90 changes: 90 additions & 0 deletions TableProTests/Views/Main/ClearQueryResultsTests.swift
Original file line number Diff line number Diff line change
@@ -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()
)
}
}
4 changes: 4 additions & 0 deletions docs/features/sql-editor.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Loading