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

### Added

- JSON results view mode with Data/Structure/JSON toggle in status bar
- Import URL: dynamic placeholder, parsed preview, clipboard auto-paste, libSQL/D1 support, URL schemes for Oracle/ClickHouse/etcd/D1/libSQL
- In-app feedback form for bug reports and feature requests via Help > Report an Issue
- Per-connection "Local only" option to exclude individual connections from iCloud sync
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ enum SessionStateFactory {
tabMgr.tabs[index].isEditable = !payload.isView
tabMgr.tabs[index].schemaName = payload.schemaName
if payload.showStructure {
tabMgr.tabs[index].showStructure = true
tabMgr.tabs[index].resultsViewMode = .structure
}
if let initialFilter = payload.initialFilterState {
tabMgr.tabs[index].filterState = initialFilter
Expand Down
2 changes: 1 addition & 1 deletion TablePro/Models/Query/EditorTabPayload.swift
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ internal struct EditorTabPayload: Codable, Hashable {
self.schemaName = tab.schemaName
self.initialQuery = tab.query
self.isView = tab.isView
self.showStructure = tab.showStructure
self.showStructure = tab.resultsViewMode == .structure
self.skipAutoExecute = skipAutoExecute
self.isPreview = false
self.initialFilterState = nil
Expand Down
14 changes: 10 additions & 4 deletions TablePro/Models/Query/QueryTab.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,12 @@ import Observation
import os
import TableProPluginKit

enum ResultsViewMode: String, Equatable {
case data
case structure
case json
}

/// Represents a single tab (query or table)
struct QueryTab: Identifiable, Equatable {
let id: UUID
Expand Down Expand Up @@ -73,7 +79,7 @@ struct QueryTab: Identifiable, Equatable {
var isView: Bool // True for database views (read-only)
var databaseName: String // Database this tab was opened in (for multi-database restore)
var schemaName: String? // Schema this tab was opened in (for multi-schema restore, e.g. PostgreSQL)
var showStructure: Bool // Toggle to show structure view instead of data
var resultsViewMode: ResultsViewMode = .data
var erDiagramSchemaKey: String?
var explainText: String?
var explainExecutionTime: TimeInterval?
Expand Down Expand Up @@ -165,7 +171,7 @@ struct QueryTab: Identifiable, Equatable {
self.isView = false
self.databaseName = ""
self.schemaName = nil
self.showStructure = false
self.resultsViewMode = .data
self.pendingChanges = TabPendingChanges()
self.selectedRowIndices = []
self.sortState = SortState()
Expand Down Expand Up @@ -201,7 +207,7 @@ struct QueryTab: Identifiable, Equatable {
self.isView = persisted.isView
self.databaseName = persisted.databaseName
self.schemaName = persisted.schemaName
self.showStructure = false
self.resultsViewMode = .data
self.erDiagramSchemaKey = persisted.erDiagramSchemaKey
self.pendingChanges = TabPendingChanges()
self.selectedRowIndices = []
Expand Down Expand Up @@ -297,7 +303,7 @@ struct QueryTab: Identifiable, Equatable {
&& lhs.paginationVersion == rhs.paginationVersion
&& lhs.pagination == rhs.pagination
&& lhs.sortState == rhs.sortState
&& lhs.showStructure == rhs.showStructure
&& lhs.resultsViewMode == rhs.resultsViewMode
&& lhs.isEditable == rhs.isEditable
&& lhs.isView == rhs.isView
&& lhs.tabType == rhs.tabType
Expand Down
2 changes: 1 addition & 1 deletion TablePro/Models/Query/QueryTabManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,7 @@ final class QueryTabManager {
tab.statusMessage = nil
tab.errorMessage = nil
tab.lastExecutedAt = nil
tab.showStructure = false
tab.resultsViewMode = .data
tab.sortState = SortState()
tab.selectedRowIndices = []
tab.pendingChanges = TabPendingChanges()
Expand Down
139 changes: 76 additions & 63 deletions TablePro/Views/Main/Child/MainEditorContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -367,74 +367,87 @@ struct MainEditorContentView: View {
@ViewBuilder
private func resultsSection(tab: QueryTab) -> some View {
VStack(spacing: 0) {
if tab.showStructure, let tableName = tab.tableName {
TableStructureView(
tableName: tableName, connection: connection,
toolbarState: coordinator.toolbarState, coordinator: coordinator
)
.id(tableName)
.frame(maxHeight: .infinity)
} else if let explainText = tab.explainText {
ExplainResultView(text: explainText, executionTime: tab.explainExecutionTime, plan: tab.explainPlan)
.frame(maxWidth: .infinity, maxHeight: .infinity)
} else {
// Result tab bar (when multiple result sets)
if tab.resultSets.count > 1 {
resultTabBar(tab: tab)
Divider()
}

// Inline error banner (when active result set has error)
if let error = tab.activeResultSet?.errorMessage {
InlineErrorBanner(
message: error,
onDismiss: { tab.activeResultSet?.errorMessage = nil }
switch tab.resultsViewMode {
case .structure:
if let tableName = tab.tableName {
TableStructureView(
tableName: tableName, connection: connection,
toolbarState: coordinator.toolbarState, coordinator: coordinator
)
Divider()
.id(tableName)
.frame(maxHeight: .infinity)
}

// Content: success view OR filter+grid
if let rs = tab.activeResultSet, rs.resultColumns.isEmpty,
rs.errorMessage == nil, tab.lastExecutedAt != nil, !tab.isExecuting
{
ResultSuccessView(
rowsAffected: rs.rowsAffected,
executionTime: rs.executionTime,
statusMessage: rs.statusMessage
)
} else if tab.resultColumns.isEmpty && tab.errorMessage == nil
&& tab.lastExecutedAt != nil && !tab.isExecuting
{
if tab.resultSets.isEmpty {
Spacer()
} else {
ResultSuccessView(
rowsAffected: tab.rowsAffected,
executionTime: tab.executionTime,
statusMessage: tab.statusMessage
)
}
case .json:
ResultsJsonView(
columns: tab.resultColumns,
columnTypes: tab.columnTypes,
rows: tab.resultRows,
selectedRowIndices: selectedRowIndices
)
.frame(maxWidth: .infinity, maxHeight: .infinity)
case .data:
if let explainText = tab.explainText {
ExplainResultView(text: explainText, executionTime: tab.explainExecutionTime, plan: tab.explainPlan)
.frame(maxWidth: .infinity, maxHeight: .infinity)
} else {
// Filter panel (collapsible, above data grid)
if filterStateManager.isVisible && tab.tabType == .table {
FilterPanelView(
filterState: filterStateManager,
columns: tab.resultColumns,
primaryKeyColumn: changeManager.primaryKeyColumn,
databaseType: connection.type,
onApply: onApplyFilters,
onUnset: onClearFilters
// Result tab bar (when multiple result sets)
if tab.resultSets.count > 1 {
resultTabBar(tab: tab)
Divider()
}

// Inline error banner (when active result set has error)
if let error = tab.activeResultSet?.errorMessage {
InlineErrorBanner(
message: error,
onDismiss: { tab.activeResultSet?.errorMessage = nil }
)
Divider()
}

if tab.tabType == .query && !tab.resultColumns.isEmpty
&& tab.resultRows.isEmpty && tab.lastExecutedAt != nil
&& !tab.isExecuting && !filterStateManager.hasAppliedFilters
// Content: success view OR filter+grid
if let rs = tab.activeResultSet, rs.resultColumns.isEmpty,
rs.errorMessage == nil, tab.lastExecutedAt != nil, !tab.isExecuting
{
ResultSuccessView(
rowsAffected: rs.rowsAffected,
executionTime: rs.executionTime,
statusMessage: rs.statusMessage
)
} else if tab.resultColumns.isEmpty && tab.errorMessage == nil
&& tab.lastExecutedAt != nil && !tab.isExecuting
{
emptyResultView(executionTime: tab.activeResultSet?.executionTime ?? tab.executionTime)
if tab.resultSets.isEmpty {
Spacer()
} else {
ResultSuccessView(
rowsAffected: tab.rowsAffected,
executionTime: tab.executionTime,
statusMessage: tab.statusMessage
)
}
} else {
dataGridView(tab: tab)
// Filter panel (collapsible, above data grid)
if filterStateManager.isVisible && tab.tabType == .table {
FilterPanelView(
filterState: filterStateManager,
columns: tab.resultColumns,
primaryKeyColumn: changeManager.primaryKeyColumn,
databaseType: connection.type,
onApply: onApplyFilters,
onUnset: onClearFilters
)
Divider()
}

if tab.tabType == .query && !tab.resultColumns.isEmpty
&& tab.resultRows.isEmpty && tab.lastExecutedAt != nil
&& !tab.isExecuting && !filterStateManager.hasAppliedFilters
{
emptyResultView(executionTime: tab.activeResultSet?.executionTime ?? tab.executionTime)
} else {
dataGridView(tab: tab)
}
}
}
}
Expand Down Expand Up @@ -768,7 +781,7 @@ struct MainEditorContentView: View {
columnVisibilityManager: columnVisibilityManager,
allColumns: tab.resultColumns,
selectedRowIndices: selectedRowIndices,
showStructure: showStructureBinding(for: tab),
viewMode: resultsViewModeBinding(for: tab),
onFirstPage: onFirstPage,
onPreviousPage: onPreviousPage,
onNextPage: onNextPage,
Expand All @@ -781,13 +794,13 @@ struct MainEditorContentView: View {
)
}

private func showStructureBinding(for tab: QueryTab) -> Binding<Bool> {
private func resultsViewModeBinding(for tab: QueryTab) -> Binding<ResultsViewMode> {
Binding(
get: { tab.showStructure },
get: { tab.resultsViewMode },
set: { newValue in
Task { @MainActor in
if let index = tabManager.selectedTabIndex {
tabManager.tabs[index].showStructure = newValue
tabManager.tabs[index].resultsViewMode = newValue
}
}
}
Expand Down
34 changes: 23 additions & 11 deletions TablePro/Views/Main/Child/MainStatusBarView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ struct MainStatusBarView: View {
let columnVisibilityManager: ColumnVisibilityManager
let allColumns: [String]
let selectedRowIndices: Set<Int>
@Binding var showStructure: Bool
@Binding var viewMode: ResultsViewMode

@State private var showColumnPopover = false

Expand All @@ -33,16 +33,28 @@ struct MainStatusBarView: View {

var body: some View {
HStack {
// Left: Data/Structure toggle for table tabs
if let tab = tab, tab.tabType == .table, tab.tableName != nil {
Picker(String(localized: "View Mode"), selection: $showStructure) {
Label("Data", systemImage: "tablecells").tag(false)
Label("Structure", systemImage: "list.bullet.rectangle").tag(true)
// Left: View mode toggle
if let tab = tab {
if tab.tabType == .table, tab.tableName != nil {
Picker(String(localized: "View Mode"), selection: $viewMode) {
Label("Data", systemImage: "tablecells").tag(ResultsViewMode.data)
Label("Structure", systemImage: "list.bullet.rectangle").tag(ResultsViewMode.structure)
Label("JSON", systemImage: "curlybraces").tag(ResultsViewMode.json)
}
.labelsHidden()
.pickerStyle(.segmented)
.frame(width: 260)
.controlSize(.small)
} else if !tab.resultColumns.isEmpty {
Picker(String(localized: "View Mode"), selection: $viewMode) {
Label("Data", systemImage: "tablecells").tag(ResultsViewMode.data)
Label("JSON", systemImage: "curlybraces").tag(ResultsViewMode.json)
}
.labelsHidden()
.pickerStyle(.segmented)
.frame(width: 140)
.controlSize(.small)
}
.labelsHidden()
.pickerStyle(.segmented)
.frame(width: 180)
.controlSize(.small)
}

Spacer()
Expand Down Expand Up @@ -166,7 +178,7 @@ struct MainStatusBarView: View {
}
}
}
.padding(.horizontal, 12)
.padding(.horizontal, 0)
.padding(.vertical, 4)
.background(Color(nsColor: .controlBackgroundColor))
.onChange(of: tab?.id) { _, _ in
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ extension MainContentCoordinator {
current.tableName == tableName,
current.databaseName == currentDatabase {
if showStructure, let idx = tabManager.selectedTabIndex {
tabManager.tabs[idx].showStructure = true
tabManager.tabs[idx].resultsViewMode = .structure
}
return
}
Expand Down Expand Up @@ -204,7 +204,7 @@ extension MainContentCoordinator {
)
previewCoordinator.filterStateManager.clearAll()
if let tabIndex = previewCoordinator.tabManager.selectedTabIndex {
previewCoordinator.tabManager.tabs[tabIndex].showStructure = showStructure
previewCoordinator.tabManager.tabs[tabIndex].resultsViewMode = showStructure ? .structure : .data
previewCoordinator.tabManager.tabs[tabIndex].pagination.reset()
previewCoordinator.toolbarState.isTableTab = true
}
Expand Down Expand Up @@ -274,7 +274,7 @@ extension MainContentCoordinator {
)
filterStateManager.clearAll()
if let tabIndex = tabManager.selectedTabIndex {
tabManager.tabs[tabIndex].showStructure = showStructure
tabManager.tabs[tabIndex].resultsViewMode = showStructure ? .structure : .data
tabManager.tabs[tabIndex].pagination.reset()
toolbarState.isTableTab = true
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ extension MainContentCoordinator {
) {
// If showing structure view, let it handle refresh notifications
if let tabIndex = tabManager.selectedTabIndex,
tabManager.tabs[tabIndex].showStructure {
tabManager.tabs[tabIndex].resultsViewMode == .structure {
return
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ extension MainContentCoordinator {
pendingDeletes: Set<String>,
tableOperationOptions: [String: TableOperationOptions]
) {
if tabManager.selectedTab?.showStructure == true {
if tabManager.selectedTab?.resultsViewMode == .structure {
// Structure view handles its own preview via direct call
structureActions?.previewSQL?()
} else {
Expand Down
10 changes: 5 additions & 5 deletions TablePro/Views/Main/Extensions/MainContentView+Bindings.swift
Original file line number Diff line number Diff line change
Expand Up @@ -81,15 +81,15 @@ extension MainContentView {
)
}

// MARK: - Show Structure Binding
// MARK: - Results View Mode Binding

/// Binding for showStructure state
var showStructureBinding: Binding<Bool> {
/// Binding for resultsViewMode state
var resultsViewModeBinding: Binding<ResultsViewMode> {
Binding(
get: { coordinator.tabManager.selectedTab?.showStructure ?? false },
get: { coordinator.tabManager.selectedTab?.resultsViewMode ?? .data },
set: { newValue in
if let index = coordinator.tabManager.selectedTabIndex {
coordinator.tabManager.tabs[index].showStructure = newValue
coordinator.tabManager.tabs[index].resultsViewMode = newValue
}
}
)
Expand Down
Loading
Loading