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
5 changes: 5 additions & 0 deletions TablePro/Views/Components/PaginationControlsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,11 @@ struct PaginationControlsView: View {
.foregroundStyle(.secondary)
.frame(minWidth: 60)

if pagination.isLoading {
ProgressView()
.controlSize(.small)
}

// Next page button
Button(action: onNext) {
Image(systemName: "chevron.right")
Expand Down
9 changes: 6 additions & 3 deletions TablePro/Views/Connection/ConnectionFormView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -973,11 +973,14 @@ struct ConnectionFormView: View { // swiftlint:disable:this type_body_length
if isTesting {
ProgressView()
.controlSize(.small)
} else if testSucceeded {
Image(systemName: "checkmark.circle.fill")
.foregroundStyle(.green)
} else {
Image(systemName: testSucceeded ? "checkmark.circle.fill" : "bolt.horizontal")
.foregroundStyle(testSucceeded ? .green : .secondary)
Image(systemName: "bolt.horizontal")
.foregroundStyle(.secondary)
}
Text("Test Connection")
Text(testSucceeded ? String(localized: "Connected") : String(localized: "Test Connection"))
}
}
.disabled(isTesting || isInstallingPlugin || !isValid)
Expand Down
20 changes: 10 additions & 10 deletions TablePro/Views/Connection/OnboardingContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -110,28 +110,28 @@ struct OnboardingContentView: View {
VStack(alignment: .leading, spacing: 16) {
featureRow(
icon: "cylinder.split.1x2",
title: "MySQL, PostgreSQL & SQLite",
description: "Connect to popular databases with full feature support"
title: String(localized: "MySQL, PostgreSQL & SQLite"),
description: String(localized: "Connect to popular databases with full feature support")
)
featureRow(
icon: "chevron.left.forwardslash.chevron.right",
title: "Smart SQL Editor",
description: "Syntax highlighting, autocomplete, and multi-tab editing"
title: String(localized: "Smart SQL Editor"),
description: String(localized: "Syntax highlighting, autocomplete, and multi-tab editing")
)
featureRow(
icon: "tablecells",
title: "Interactive Data Grid",
description: "Browse, edit, and manage your data with ease"
title: String(localized: "Interactive Data Grid"),
description: String(localized: "Browse, edit, and manage your data with ease")
)
featureRow(
icon: "lock.shield",
title: "Secure Connections",
description: "SSH tunneling and SSL/TLS encryption support"
title: String(localized: "Secure Connections"),
description: String(localized: "SSH tunneling and SSL/TLS encryption support")
)
featureRow(
icon: "brain",
title: "AI-Powered Assistant",
description: "Get intelligent SQL suggestions and query assistance"
title: String(localized: "AI-Powered Assistant"),
description: String(localized: "Get intelligent SQL suggestions and query assistance")
)
}
.padding(.horizontal, 20)
Expand Down
2 changes: 1 addition & 1 deletion TablePro/Views/Connection/WelcomeLeftPanel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ struct WelcomeLeftPanel: View {
Image(nsImage: NSApp.applicationIconImage)
.resizable()
.frame(width: 80, height: 80)
.shadow(color: Color(red: 1.0, green: 0.576, blue: 0.0).opacity(0.4), radius: 20, x: 0, y: 0)
.shadow(color: Color.accentColor.opacity(0.4), radius: 20, x: 0, y: 0)

VStack(spacing: 6) {
Text("TablePro")
Expand Down
17 changes: 17 additions & 0 deletions TablePro/Views/Editor/AIEditorContextMenu.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ final class AIEditorContextMenu: NSMenu, NSMenuDelegate {
var onExplainWithAI: ((String) -> Void)?
var onOptimizeWithAI: ((String) -> Void)?
var onSaveAsFavorite: ((String) -> Void)?
var onFormatSQL: (() -> Void)?

override init(title: String) {
super.init(title: title)
Expand Down Expand Up @@ -49,6 +50,18 @@ final class AIEditorContextMenu: NSMenu, NSMenuDelegate {

menu.addItem(.separator())

let formatItem = NSMenuItem(
title: String(localized: "Format SQL"),
action: #selector(handleFormatSQL),
keyEquivalent: ""
)
formatItem.target = self
formatItem.image = NSImage(systemSymbolName: "text.alignleft", accessibilityDescription: nil)
formatItem.isEnabled = (fullText?()?.isEmpty == false) && (onFormatSQL != nil)
menu.addItem(formatItem)

menu.addItem(.separator())

let saveAsFavItem = NSMenuItem(
title: String(localized: "Save as Favorite..."),
action: #selector(handleSaveAsFavorite),
Expand Down Expand Up @@ -95,6 +108,10 @@ final class AIEditorContextMenu: NSMenu, NSMenuDelegate {
onOptimizeWithAI?(text)
}

@objc private func handleFormatSQL() {
onFormatSQL?()
}

@objc private func handleSaveAsFavorite() {
if let text = selectedText?(), !text.isEmpty {
onSaveAsFavorite?(text)
Expand Down
3 changes: 2 additions & 1 deletion TablePro/Views/Editor/QueryEditorView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,8 @@ struct QueryEditorView: View {
onExecuteQuery: onExecuteQuery,
onAIExplain: onAIExplain,
onAIOptimize: onAIOptimize,
onSaveAsFavorite: onSaveAsFavorite
onSaveAsFavorite: onSaveAsFavorite,
onFormatSQL: formatQuery
)
.frame(minHeight: 100)
.clipped()
Expand Down
2 changes: 2 additions & 0 deletions TablePro/Views/Editor/SQLEditorCoordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ final class SQLEditorCoordinator: TextViewCoordinator {
@ObservationIgnored var onAIExplain: ((String) -> Void)?
@ObservationIgnored var onAIOptimize: ((String) -> Void)?
@ObservationIgnored var onSaveAsFavorite: ((String) -> Void)?
@ObservationIgnored var onFormatSQL: (() -> Void)?

/// Whether the editor text view is currently the first responder.
/// Used to guard cursor propagation — when the find panel highlights
Expand Down Expand Up @@ -207,6 +208,7 @@ final class SQLEditorCoordinator: TextViewCoordinator {
menu.onExplainWithAI = { [weak self] text in self?.onAIExplain?(text) }
menu.onOptimizeWithAI = { [weak self] text in self?.onAIOptimize?(text) }
menu.onSaveAsFavorite = { [weak self] text in self?.onSaveAsFavorite?(text) }
menu.onFormatSQL = { [weak self] in self?.onFormatSQL?() }
contextMenu = menu
}

Expand Down
3 changes: 3 additions & 0 deletions TablePro/Views/Editor/SQLEditorView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ struct SQLEditorView: View {
var onAIExplain: ((String) -> Void)?
var onAIOptimize: ((String) -> Void)?
var onSaveAsFavorite: ((String) -> Void)?
var onFormatSQL: (() -> Void)?

@State private var editorState = SourceEditorState()
@State private var completionAdapter: SQLCompletionAdapter?
Expand Down Expand Up @@ -104,6 +105,7 @@ struct SQLEditorView: View {
coordinator.onAIExplain = onAIExplain
coordinator.onAIOptimize = onAIOptimize
coordinator.onSaveAsFavorite = onSaveAsFavorite
coordinator.onFormatSQL = onFormatSQL
setupFavoritesObserver()
}
} else {
Expand All @@ -118,6 +120,7 @@ struct SQLEditorView: View {
coordinator.onAIExplain = onAIExplain
coordinator.onAIOptimize = onAIOptimize
coordinator.onSaveAsFavorite = onSaveAsFavorite
coordinator.onFormatSQL = onFormatSQL
setupFavoritesObserver()
editorReady = true
}
Expand Down
18 changes: 18 additions & 0 deletions TablePro/Views/Export/ExportDialog.swift
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,13 @@ struct ExportDialog: View {

Spacer()
}

let description = formatDescription(for: config.formatId)
if !description.isEmpty {
Text(description)
.font(.system(size: ThemeEngine.shared.activeTheme.typography.small))
.foregroundStyle(.secondary)
}
}

// Selection count or Pro gate message
Expand Down Expand Up @@ -430,6 +437,17 @@ struct ExportDialog: View {
private static let formatDisplayOrder = ["csv", "json", "sql", "xlsx", "mql"]
private static let proFormatIds: Set<String> = ["xlsx"]

private func formatDescription(for formatId: String) -> String {
switch formatId {
case "csv": return String(localized: "Comma-separated values. Compatible with Excel and most tools.")
case "json": return String(localized: "Structured data format. Ideal for APIs and web applications.")
case "sql": return String(localized: "SQL INSERT statements. Use to recreate data in another database.")
case "xlsx": return String(localized: "Excel spreadsheet with formatting support.")
case "mql": return String(localized: "MongoDB query language. Use to import into MongoDB.")
default: return ""
}
}

private func isProGatedFormat(_ formatId: String) -> Bool {
Self.proFormatIds.contains(formatId) && !LicenseManager.shared.isFeatureAvailable(.xlsxExport)
}
Expand Down
14 changes: 11 additions & 3 deletions TablePro/Views/Filter/FilterPanelView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -98,10 +98,18 @@ struct FilterPanelView: View {
TextField("Preset Name", text: $newPresetName)
Button("Cancel", role: .cancel) {}
Button("Save") {
if !newPresetName.isEmpty {
filterState.saveAsPreset(name: newPresetName)
loadPresets()
guard !newPresetName.isEmpty else { return }
var finalName = newPresetName
let existingNames = Set(savedPresets.map(\.name))
if existingNames.contains(finalName) {
var counter = 2
while existingNames.contains("\(newPresetName) (\(counter))") {
counter += 1
}
finalName = "\(newPresetName) (\(counter))"
}
filterState.saveAsPreset(name: finalName)
loadPresets()
}
} message: {
Text("Enter a name for this filter preset")
Expand Down
15 changes: 11 additions & 4 deletions TablePro/Views/Import/ImportDialog.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ struct ImportDialog: View {
@State private var importResult: PluginImportResult?
@State private var importError: (any Error)?

@State private var hasPreviewError = false
@State private var tempPreviewURL: URL?
@State private var loadFileTask: Task<Void, Never>?

Expand Down Expand Up @@ -276,7 +277,7 @@ struct ImportDialog: View {
performImport()
}
.buttonStyle(.borderedProminent)
.disabled(fileURL == nil || importServiceState.isImporting || availableFormats.isEmpty)
.disabled(fileURL == nil || importServiceState.isImporting || availableFormats.isEmpty || hasPreviewError)
.keyboardShortcut(.return, modifiers: [])
}
.padding(16)
Expand Down Expand Up @@ -305,12 +306,14 @@ struct ImportDialog: View {
@MainActor
private func loadFile(_ url: URL) async {
cleanupTempFiles()
hasPreviewError = false

var isDirectory: ObjCBool = false
guard FileManager.default.fileExists(atPath: url.path(percentEncoded: false), isDirectory: &isDirectory),
!isDirectory.boolValue
else {
filePreview = String(localized: "Error: Selected path is not a regular file")
hasPreviewError = true
return
}

Expand All @@ -331,7 +334,8 @@ struct ImportDialog: View {
tempPreviewURL = urlToRead
}
} catch {
filePreview = String(localized: "Failed to decompress file: \(error.localizedDescription)")
filePreview = "Failed to decompress file: \(error.localizedDescription)"
hasPreviewError = true
return
}

Expand All @@ -350,11 +354,14 @@ struct ImportDialog: View {

if let preview = String(data: previewData, encoding: selectedEncoding.encoding) {
filePreview = preview
hasPreviewError = false
} else {
filePreview = String(localized: "Failed to load preview using encoding: \(selectedEncoding.rawValue). Try selecting a different text encoding.")
filePreview = "Failed to load preview using encoding: \(selectedEncoding.rawValue). Try selecting a different text encoding."
hasPreviewError = true
}
} catch {
filePreview = String(localized: "Failed to load preview: \(error.localizedDescription)")
filePreview = "Failed to load preview: \(error.localizedDescription)"
hasPreviewError = true
}

Task {
Expand Down
27 changes: 26 additions & 1 deletion TablePro/Views/Main/Child/MainEditorContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -355,7 +355,14 @@ struct MainEditorContentView: View {
Divider()
}

dataGridView(tab: tab)
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 @@ -388,6 +395,24 @@ struct MainEditorContentView: View {
)
}

private func emptyResultView(executionTime: TimeInterval?) -> some View {
VStack(spacing: 12) {
Spacer()
Image(systemName: "tray")
.font(.system(size: 36))
.foregroundStyle(.secondary)
Text("No rows returned")
.font(.system(size: ThemeEngine.shared.activeTheme.typography.body, weight: .medium))
if let time = executionTime {
Text(String(format: "%.3fs", time))
.font(.system(size: ThemeEngine.shared.activeTheme.typography.small))
.foregroundStyle(.secondary)
}
Spacer()
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
}

@ViewBuilder
private func dataGridView(tab: QueryTab) -> some View {
DataGridView(
Expand Down
15 changes: 14 additions & 1 deletion TablePro/Views/Results/DataGridView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -121,10 +121,13 @@ struct DataGridView: NSViewRepresentable {
for (index, columnName) in rowProvider.columns.enumerated() {
let column = NSTableColumn(identifier: NSUserInterfaceItemIdentifier("col_\(index)"))
column.title = columnName
if index < rowProvider.columnTypes.count {
let typeName = rowProvider.columnTypes[index].rawType ?? rowProvider.columnTypes[index].displayName
column.headerToolTip = "\(columnName) (\(typeName))"
}
column.headerCell.setAccessibilityLabel(
String(localized: "Column: \(columnName)")
)
// Use optimal width calculation based on both header and cell content
column.width = context.coordinator.cellFactory.calculateOptimalColumnWidth(
for: columnName,
columnIndex: index,
Expand Down Expand Up @@ -373,6 +376,11 @@ struct DataGridView: NSViewRepresentable {
for (index, columnName) in rowProvider.columns.enumerated() {
let column = NSTableColumn(identifier: NSUserInterfaceItemIdentifier("col_\(index)"))
column.title = columnName
if index < rowProvider.columnTypes.count {
let typeName = rowProvider.columnTypes[index].rawType
?? rowProvider.columnTypes[index].displayName
column.headerToolTip = "\(columnName) (\(typeName))"
}
column.headerCell.setAccessibilityLabel(
String(localized: "Column: \(columnName)")
)
Expand All @@ -394,6 +402,11 @@ struct DataGridView: NSViewRepresentable {
colIndex < rowProvider.columns.count else { continue }
let columnName = rowProvider.columns[colIndex]
column.title = columnName
if colIndex < rowProvider.columnTypes.count {
let typeName = rowProvider.columnTypes[colIndex].rawType
?? rowProvider.columnTypes[colIndex].displayName
column.headerToolTip = "\(columnName) (\(typeName))"
}
column.width = coordinator.cellFactory.calculateOptimalColumnWidth(
for: columnName,
columnIndex: colIndex,
Expand Down
10 changes: 10 additions & 0 deletions TablePro/Views/Results/InlineErrorBanner.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
// Dismissable red error banner for query errors, displayed inline above results.
//

import AppKit
import SwiftUI

struct InlineErrorBanner: View {
Expand All @@ -20,6 +21,15 @@ struct InlineErrorBanner: View {
.lineLimit(3)
.textSelection(.enabled)
Spacer()
Button {
NSPasteboard.general.clearContents()
NSPasteboard.general.setString(message, forType: .string)
} label: {
Image(systemName: "doc.on.doc")
.foregroundStyle(.secondary)
}
.buttonStyle(.plain)
.help(String(localized: "Copy error message"))
if let onDismiss {
Button { onDismiss() } label: {
Image(systemName: "xmark")
Expand Down
Loading
Loading