From bf368bf888784d7452044d34c8e90a6dd886fa3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ng=C3=B4=20Qu=E1=BB=91c=20=C4=90=E1=BA=A1t?= Date: Tue, 31 Mar 2026 12:36:12 +0700 Subject: [PATCH 1/7] fix: show loading spinner in pagination controls --- TablePro/Views/Components/PaginationControlsView.swift | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/TablePro/Views/Components/PaginationControlsView.swift b/TablePro/Views/Components/PaginationControlsView.swift index be9c96a99..38ae40b6b 100644 --- a/TablePro/Views/Components/PaginationControlsView.swift +++ b/TablePro/Views/Components/PaginationControlsView.swift @@ -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") From 03a9250641b4830fd7e279d73f683339f5f17ea7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ng=C3=B4=20Qu=E1=BB=91c=20=C4=90=E1=BA=A1t?= Date: Tue, 31 Mar 2026 12:37:50 +0700 Subject: [PATCH 2/7] =?UTF-8?q?fix:=20UI=20polish=20=E2=80=94=20localize?= =?UTF-8?q?=20onboarding,=20empty=20result=20state,=20pagination=20spinner?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Connection/OnboardingContentView.swift | 20 +++++++------- .../Main/Child/MainEditorContentView.swift | 27 ++++++++++++++++++- 2 files changed, 36 insertions(+), 11 deletions(-) diff --git a/TablePro/Views/Connection/OnboardingContentView.swift b/TablePro/Views/Connection/OnboardingContentView.swift index 7ad88caa8..3752a30b4 100644 --- a/TablePro/Views/Connection/OnboardingContentView.swift +++ b/TablePro/Views/Connection/OnboardingContentView.swift @@ -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) diff --git a/TablePro/Views/Main/Child/MainEditorContentView.swift b/TablePro/Views/Main/Child/MainEditorContentView.swift index 0db40e6af..9e57e025b 100644 --- a/TablePro/Views/Main/Child/MainEditorContentView.swift +++ b/TablePro/Views/Main/Child/MainEditorContentView.swift @@ -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) + } } } @@ -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( From 264f617422ad1a7d3bf83f560fa981f61df00872 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ng=C3=B4=20Qu=E1=BB=91c=20=C4=90=E1=BA=A1t?= Date: Tue, 31 Mar 2026 12:41:09 +0700 Subject: [PATCH 3/7] =?UTF-8?q?fix:=20UI=20polish=20=E2=80=94=20import=20v?= =?UTF-8?q?alidation,=20preset=20dedup,=20AI=20retry=20button?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- TablePro/Views/Filter/FilterPanelView.swift | 14 +++++++++++--- TablePro/Views/Import/ImportDialog.swift | 8 +++++++- TablePro/Views/Settings/AISettingsView.swift | 15 ++++++++++++--- 3 files changed, 30 insertions(+), 7 deletions(-) diff --git a/TablePro/Views/Filter/FilterPanelView.swift b/TablePro/Views/Filter/FilterPanelView.swift index 7bb044946..9dcf0199a 100644 --- a/TablePro/Views/Filter/FilterPanelView.swift +++ b/TablePro/Views/Filter/FilterPanelView.swift @@ -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") diff --git a/TablePro/Views/Import/ImportDialog.swift b/TablePro/Views/Import/ImportDialog.swift index 7a0fbc111..da6041c6e 100644 --- a/TablePro/Views/Import/ImportDialog.swift +++ b/TablePro/Views/Import/ImportDialog.swift @@ -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? @@ -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) @@ -311,6 +312,7 @@ struct ImportDialog: View { !isDirectory.boolValue else { filePreview = String(localized: "Error: Selected path is not a regular file") + hasPreviewError = true return } @@ -332,6 +334,7 @@ struct ImportDialog: View { } } catch { filePreview = String(localized: "Failed to decompress file: \(error.localizedDescription)") + hasPreviewError = true return } @@ -350,11 +353,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.") + hasPreviewError = true } } catch { filePreview = String(localized: "Failed to load preview: \(error.localizedDescription)") + hasPreviewError = true } Task { diff --git a/TablePro/Views/Settings/AISettingsView.swift b/TablePro/Views/Settings/AISettingsView.swift index 90400d5a5..ad9e2f3c0 100644 --- a/TablePro/Views/Settings/AISettingsView.swift +++ b/TablePro/Views/Settings/AISettingsView.swift @@ -449,9 +449,18 @@ private struct AIProviderEditorSheet: View { } if let error = modelFetchError { - Text(error) - .font(.caption) - .foregroundStyle(.red) + HStack { + Text(error) + .font(.caption) + .foregroundStyle(.red) + Button { + fetchModels() + } label: { + Label("Retry", systemImage: "arrow.clockwise") + .font(.caption) + } + .buttonStyle(.borderless) + } } } } From fefa01eb414af7b82cafb3c5acc20d36694f52f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ng=C3=B4=20Qu=E1=BB=91c=20=C4=90=E1=BA=A1t?= Date: Tue, 31 Mar 2026 12:46:44 +0700 Subject: [PATCH 4/7] =?UTF-8?q?fix:=20UI=20polish=20=E2=80=94=20execution?= =?UTF-8?q?=20feedback,=20test=20label,=20settings=20reset?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Views/Connection/ConnectionFormView.swift | 9 ++++++--- TablePro/Views/Settings/GeneralSettingsView.swift | 15 +++++++++++++++ .../Views/Toolbar/ExecutionIndicatorView.swift | 5 ++++- 3 files changed, 25 insertions(+), 4 deletions(-) diff --git a/TablePro/Views/Connection/ConnectionFormView.swift b/TablePro/Views/Connection/ConnectionFormView.swift index 6f04c9ee6..80c48a7e0 100644 --- a/TablePro/Views/Connection/ConnectionFormView.swift +++ b/TablePro/Views/Connection/ConnectionFormView.swift @@ -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) diff --git a/TablePro/Views/Settings/GeneralSettingsView.swift b/TablePro/Views/Settings/GeneralSettingsView.swift index 414dd0abb..46d50ed56 100644 --- a/TablePro/Views/Settings/GeneralSettingsView.swift +++ b/TablePro/Views/Settings/GeneralSettingsView.swift @@ -13,6 +13,7 @@ struct GeneralSettingsView: View { var updaterBridge: UpdaterBridge @Bindable private var settingsManager = AppSettingsManager.shared @State private var initialLanguage: AppLanguage? + @State private var showResetConfirmation = false private static let standardTimeouts = [10, 20, 30, 40, 50, 60, 90, 120, 180, 300, 600] @@ -82,9 +83,23 @@ struct GeneralSettingsView: View { .font(.caption) .foregroundStyle(.secondary) } + + Section { + Button(String(localized: "Reset All Settings to Defaults"), role: .destructive) { + showResetConfirmation = true + } + } } .formStyle(.grouped) .scrollContentBackground(.hidden) + .alert(String(localized: "Reset All Settings"), isPresented: $showResetConfirmation) { + Button(String(localized: "Reset"), role: .destructive) { + settingsManager.resetToDefaults() + } + Button(String(localized: "Cancel"), role: .cancel) {} + } message: { + Text("This will reset all settings across every section to their default values.") + } .onAppear { if initialLanguage == nil { initialLanguage = settings.language diff --git a/TablePro/Views/Toolbar/ExecutionIndicatorView.swift b/TablePro/Views/Toolbar/ExecutionIndicatorView.swift index c0d799a6e..9438161c1 100644 --- a/TablePro/Views/Toolbar/ExecutionIndicatorView.swift +++ b/TablePro/Views/Toolbar/ExecutionIndicatorView.swift @@ -21,11 +21,14 @@ struct ExecutionIndicatorView: View { ProgressView() .controlSize(.small) .accessibilityLabel(String(localized: "Query executing")) - .help("Query executing...") if let progress = clickHouseProgress { Text(progress.formattedLive) .font(.system(size: ThemeEngine.shared.activeTheme.typography.small, weight: .regular, design: .monospaced)) .foregroundStyle(ThemeEngine.shared.colors.toolbar.tertiaryTextSwiftUI) + } else { + Text("Executing...") + .font(.system(size: ThemeEngine.shared.activeTheme.typography.small, weight: .regular, design: .monospaced)) + .foregroundStyle(ThemeEngine.shared.colors.toolbar.tertiaryTextSwiftUI) } } else if let chProgress = lastClickHouseProgress { Text(chProgress.formattedSummary) From 2533c84f51744bb5b12bf4573ea3fcd8873a3a7b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ng=C3=B4=20Qu=E1=BB=91c=20=C4=90=E1=BA=A1t?= Date: Tue, 31 Mar 2026 12:54:51 +0700 Subject: [PATCH 5/7] =?UTF-8?q?fix:=20UI=20polish=20=E2=80=94=20license=20?= =?UTF-8?q?error,=20column=20tooltips,=20copy=20error,=20format=20menu?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- TablePro/Views/Editor/AIEditorContextMenu.swift | 17 +++++++++++++++++ TablePro/Views/Editor/QueryEditorView.swift | 3 ++- .../Views/Editor/SQLEditorCoordinator.swift | 2 ++ TablePro/Views/Editor/SQLEditorView.swift | 3 +++ TablePro/Views/Results/DataGridView.swift | 15 ++++++++++++++- TablePro/Views/Results/InlineErrorBanner.swift | 10 ++++++++++ .../Views/Settings/LicenseSettingsView.swift | 10 +++++++++- 7 files changed, 57 insertions(+), 3 deletions(-) diff --git a/TablePro/Views/Editor/AIEditorContextMenu.swift b/TablePro/Views/Editor/AIEditorContextMenu.swift index 68a7a7ce3..c6b97cd86 100644 --- a/TablePro/Views/Editor/AIEditorContextMenu.swift +++ b/TablePro/Views/Editor/AIEditorContextMenu.swift @@ -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) @@ -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), @@ -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) diff --git a/TablePro/Views/Editor/QueryEditorView.swift b/TablePro/Views/Editor/QueryEditorView.swift index 2f1a67614..98be84613 100644 --- a/TablePro/Views/Editor/QueryEditorView.swift +++ b/TablePro/Views/Editor/QueryEditorView.swift @@ -54,7 +54,8 @@ struct QueryEditorView: View { onExecuteQuery: onExecuteQuery, onAIExplain: onAIExplain, onAIOptimize: onAIOptimize, - onSaveAsFavorite: onSaveAsFavorite + onSaveAsFavorite: onSaveAsFavorite, + onFormatSQL: formatQuery ) .frame(minHeight: 100) .clipped() diff --git a/TablePro/Views/Editor/SQLEditorCoordinator.swift b/TablePro/Views/Editor/SQLEditorCoordinator.swift index bbca2dce3..997c12aa4 100644 --- a/TablePro/Views/Editor/SQLEditorCoordinator.swift +++ b/TablePro/Views/Editor/SQLEditorCoordinator.swift @@ -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 @@ -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 } diff --git a/TablePro/Views/Editor/SQLEditorView.swift b/TablePro/Views/Editor/SQLEditorView.swift index c1e727931..7055ec8f1 100644 --- a/TablePro/Views/Editor/SQLEditorView.swift +++ b/TablePro/Views/Editor/SQLEditorView.swift @@ -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? @@ -104,6 +105,7 @@ struct SQLEditorView: View { coordinator.onAIExplain = onAIExplain coordinator.onAIOptimize = onAIOptimize coordinator.onSaveAsFavorite = onSaveAsFavorite + coordinator.onFormatSQL = onFormatSQL setupFavoritesObserver() } } else { @@ -118,6 +120,7 @@ struct SQLEditorView: View { coordinator.onAIExplain = onAIExplain coordinator.onAIOptimize = onAIOptimize coordinator.onSaveAsFavorite = onSaveAsFavorite + coordinator.onFormatSQL = onFormatSQL setupFavoritesObserver() editorReady = true } diff --git a/TablePro/Views/Results/DataGridView.swift b/TablePro/Views/Results/DataGridView.swift index 02d6d0e3c..52ee072fc 100644 --- a/TablePro/Views/Results/DataGridView.swift +++ b/TablePro/Views/Results/DataGridView.swift @@ -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, @@ -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)") ) @@ -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, diff --git a/TablePro/Views/Results/InlineErrorBanner.swift b/TablePro/Views/Results/InlineErrorBanner.swift index 96da638cc..4fa5a2d65 100644 --- a/TablePro/Views/Results/InlineErrorBanner.swift +++ b/TablePro/Views/Results/InlineErrorBanner.swift @@ -5,6 +5,7 @@ // Dismissable red error banner for query errors, displayed inline above results. // +import AppKit import SwiftUI struct InlineErrorBanner: View { @@ -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") diff --git a/TablePro/Views/Settings/LicenseSettingsView.swift b/TablePro/Views/Settings/LicenseSettingsView.swift index 65788869e..ae0d6d7c9 100644 --- a/TablePro/Views/Settings/LicenseSettingsView.swift +++ b/TablePro/Views/Settings/LicenseSettingsView.swift @@ -20,6 +20,7 @@ struct LicenseSettingsView: View { @State private var maxActivations = 0 @State private var isLoadingActivations = false @State private var hasLoadedActivations = false + @State private var activationLoadError: String? var body: some View { Form { @@ -92,7 +93,13 @@ struct LicenseSettingsView: View { } else if activations.isEmpty { Text("No activations found") .foregroundStyle(.secondary) - } else { + } + if let error = activationLoadError { + Text(error) + .font(.caption) + .foregroundStyle(.red) + } + if !activations.isEmpty { ForEach(activations) { activation in HStack { VStack(alignment: .leading, spacing: 2) { @@ -214,6 +221,7 @@ struct LicenseSettingsView: View { maxActivations = response.maxActivations } catch { Self.logger.debug("Failed to load activations: \(error.localizedDescription)") + activationLoadError = error.localizedDescription } } From c415808233e79afa4c7eb19a93971e353b460d71 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ng=C3=B4=20Qu=E1=BB=91c=20=C4=90=E1=BA=A1t?= Date: Tue, 31 Mar 2026 12:58:37 +0700 Subject: [PATCH 6/7] =?UTF-8?q?fix:=20UI=20polish=20=E2=80=94=20accent=20s?= =?UTF-8?q?hadow,=20Pro=20badge,=20export=20descriptions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Views/Connection/WelcomeLeftPanel.swift | 2 +- TablePro/Views/Export/ExportDialog.swift | 18 ++++++++++++++++++ TablePro/Views/Settings/SettingsView.swift | 2 +- 3 files changed, 20 insertions(+), 2 deletions(-) diff --git a/TablePro/Views/Connection/WelcomeLeftPanel.swift b/TablePro/Views/Connection/WelcomeLeftPanel.swift index 85b0853bb..da33b4279 100644 --- a/TablePro/Views/Connection/WelcomeLeftPanel.swift +++ b/TablePro/Views/Connection/WelcomeLeftPanel.swift @@ -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") diff --git a/TablePro/Views/Export/ExportDialog.swift b/TablePro/Views/Export/ExportDialog.swift index 806b1b9f8..2580488b9 100644 --- a/TablePro/Views/Export/ExportDialog.swift +++ b/TablePro/Views/Export/ExportDialog.swift @@ -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 @@ -430,6 +437,17 @@ struct ExportDialog: View { private static let formatDisplayOrder = ["csv", "json", "sql", "xlsx", "mql"] private static let proFormatIds: Set = ["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) } diff --git a/TablePro/Views/Settings/SettingsView.swift b/TablePro/Views/Settings/SettingsView.swift index 4bb4f4078..5eb680e69 100644 --- a/TablePro/Views/Settings/SettingsView.swift +++ b/TablePro/Views/Settings/SettingsView.swift @@ -70,7 +70,7 @@ struct SettingsView: View { SyncSettingsView() .tabItem { - Label("Sync", systemImage: "icloud") + Label("Sync (Pro)", systemImage: "icloud") } .tag(SettingsTab.sync.rawValue) .requiresPro(.iCloudSync) From 95c4ddab113080464cca867bda345393f4bd2e17 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ng=C3=B4=20Qu=E1=BB=91c=20=C4=90=E1=BA=A1t?= Date: Tue, 31 Mar 2026 13:10:47 +0700 Subject: [PATCH 7/7] =?UTF-8?q?fix:=20address=20review=20=E2=80=94=20reset?= =?UTF-8?q?=20preview=20error=20flag,=20fix=20activation=20display,=20remo?= =?UTF-8?q?ve=20invalid=20String(localized:)=20interpolation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- TablePro/Views/Import/ImportDialog.swift | 7 ++++--- TablePro/Views/Settings/LicenseSettingsView.swift | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/TablePro/Views/Import/ImportDialog.swift b/TablePro/Views/Import/ImportDialog.swift index da6041c6e..0b2b96b8a 100644 --- a/TablePro/Views/Import/ImportDialog.swift +++ b/TablePro/Views/Import/ImportDialog.swift @@ -306,6 +306,7 @@ 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), @@ -333,7 +334,7 @@ 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 } @@ -355,11 +356,11 @@ struct ImportDialog: View { 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 } diff --git a/TablePro/Views/Settings/LicenseSettingsView.swift b/TablePro/Views/Settings/LicenseSettingsView.swift index ae0d6d7c9..5ecc96e28 100644 --- a/TablePro/Views/Settings/LicenseSettingsView.swift +++ b/TablePro/Views/Settings/LicenseSettingsView.swift @@ -90,7 +90,7 @@ struct LicenseSettingsView: View { .controlSize(.small) Spacer() } - } else if activations.isEmpty { + } else if activations.isEmpty && activationLoadError == nil { Text("No activations found") .foregroundStyle(.secondary) }