Skip to content

Commit ee95461

Browse files
authored
fix: UI/UX polish (#527)
* fix: show loading spinner in pagination controls * fix: UI polish — localize onboarding, empty result state, pagination spinner * fix: UI polish — import validation, preset dedup, AI retry button * fix: UI polish — execution feedback, test label, settings reset * fix: UI polish — license error, column tooltips, copy error, format menu * fix: UI polish — accent shadow, Pro badge, export descriptions * fix: address review — reset preview error flag, fix activation display, remove invalid String(localized:) interpolation
1 parent 589bea6 commit ee95461

19 files changed

Lines changed: 178 additions & 31 deletions

TablePro/Views/Components/PaginationControlsView.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,11 @@ struct PaginationControlsView: View {
7272
.foregroundStyle(.secondary)
7373
.frame(minWidth: 60)
7474

75+
if pagination.isLoading {
76+
ProgressView()
77+
.controlSize(.small)
78+
}
79+
7580
// Next page button
7681
Button(action: onNext) {
7782
Image(systemName: "chevron.right")

TablePro/Views/Connection/ConnectionFormView.swift

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -973,11 +973,14 @@ struct ConnectionFormView: View { // swiftlint:disable:this type_body_length
973973
if isTesting {
974974
ProgressView()
975975
.controlSize(.small)
976+
} else if testSucceeded {
977+
Image(systemName: "checkmark.circle.fill")
978+
.foregroundStyle(.green)
976979
} else {
977-
Image(systemName: testSucceeded ? "checkmark.circle.fill" : "bolt.horizontal")
978-
.foregroundStyle(testSucceeded ? .green : .secondary)
980+
Image(systemName: "bolt.horizontal")
981+
.foregroundStyle(.secondary)
979982
}
980-
Text("Test Connection")
983+
Text(testSucceeded ? String(localized: "Connected") : String(localized: "Test Connection"))
981984
}
982985
}
983986
.disabled(isTesting || isInstallingPlugin || !isValid)

TablePro/Views/Connection/OnboardingContentView.swift

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -110,28 +110,28 @@ struct OnboardingContentView: View {
110110
VStack(alignment: .leading, spacing: 16) {
111111
featureRow(
112112
icon: "cylinder.split.1x2",
113-
title: "MySQL, PostgreSQL & SQLite",
114-
description: "Connect to popular databases with full feature support"
113+
title: String(localized: "MySQL, PostgreSQL & SQLite"),
114+
description: String(localized: "Connect to popular databases with full feature support")
115115
)
116116
featureRow(
117117
icon: "chevron.left.forwardslash.chevron.right",
118-
title: "Smart SQL Editor",
119-
description: "Syntax highlighting, autocomplete, and multi-tab editing"
118+
title: String(localized: "Smart SQL Editor"),
119+
description: String(localized: "Syntax highlighting, autocomplete, and multi-tab editing")
120120
)
121121
featureRow(
122122
icon: "tablecells",
123-
title: "Interactive Data Grid",
124-
description: "Browse, edit, and manage your data with ease"
123+
title: String(localized: "Interactive Data Grid"),
124+
description: String(localized: "Browse, edit, and manage your data with ease")
125125
)
126126
featureRow(
127127
icon: "lock.shield",
128-
title: "Secure Connections",
129-
description: "SSH tunneling and SSL/TLS encryption support"
128+
title: String(localized: "Secure Connections"),
129+
description: String(localized: "SSH tunneling and SSL/TLS encryption support")
130130
)
131131
featureRow(
132132
icon: "brain",
133-
title: "AI-Powered Assistant",
134-
description: "Get intelligent SQL suggestions and query assistance"
133+
title: String(localized: "AI-Powered Assistant"),
134+
description: String(localized: "Get intelligent SQL suggestions and query assistance")
135135
)
136136
}
137137
.padding(.horizontal, 20)

TablePro/Views/Connection/WelcomeLeftPanel.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ struct WelcomeLeftPanel: View {
1717
Image(nsImage: NSApp.applicationIconImage)
1818
.resizable()
1919
.frame(width: 80, height: 80)
20-
.shadow(color: Color(red: 1.0, green: 0.576, blue: 0.0).opacity(0.4), radius: 20, x: 0, y: 0)
20+
.shadow(color: Color.accentColor.opacity(0.4), radius: 20, x: 0, y: 0)
2121

2222
VStack(spacing: 6) {
2323
Text("TablePro")

TablePro/Views/Editor/AIEditorContextMenu.swift

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ final class AIEditorContextMenu: NSMenu, NSMenuDelegate {
1616
var onExplainWithAI: ((String) -> Void)?
1717
var onOptimizeWithAI: ((String) -> Void)?
1818
var onSaveAsFavorite: ((String) -> Void)?
19+
var onFormatSQL: (() -> Void)?
1920

2021
override init(title: String) {
2122
super.init(title: title)
@@ -49,6 +50,18 @@ final class AIEditorContextMenu: NSMenu, NSMenuDelegate {
4950

5051
menu.addItem(.separator())
5152

53+
let formatItem = NSMenuItem(
54+
title: String(localized: "Format SQL"),
55+
action: #selector(handleFormatSQL),
56+
keyEquivalent: ""
57+
)
58+
formatItem.target = self
59+
formatItem.image = NSImage(systemSymbolName: "text.alignleft", accessibilityDescription: nil)
60+
formatItem.isEnabled = (fullText?()?.isEmpty == false) && (onFormatSQL != nil)
61+
menu.addItem(formatItem)
62+
63+
menu.addItem(.separator())
64+
5265
let saveAsFavItem = NSMenuItem(
5366
title: String(localized: "Save as Favorite..."),
5467
action: #selector(handleSaveAsFavorite),
@@ -95,6 +108,10 @@ final class AIEditorContextMenu: NSMenu, NSMenuDelegate {
95108
onOptimizeWithAI?(text)
96109
}
97110

111+
@objc private func handleFormatSQL() {
112+
onFormatSQL?()
113+
}
114+
98115
@objc private func handleSaveAsFavorite() {
99116
if let text = selectedText?(), !text.isEmpty {
100117
onSaveAsFavorite?(text)

TablePro/Views/Editor/QueryEditorView.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,8 @@ struct QueryEditorView: View {
5454
onExecuteQuery: onExecuteQuery,
5555
onAIExplain: onAIExplain,
5656
onAIOptimize: onAIOptimize,
57-
onSaveAsFavorite: onSaveAsFavorite
57+
onSaveAsFavorite: onSaveAsFavorite,
58+
onFormatSQL: formatQuery
5859
)
5960
.frame(minHeight: 100)
6061
.clipped()

TablePro/Views/Editor/SQLEditorCoordinator.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ final class SQLEditorCoordinator: TextViewCoordinator {
4747
@ObservationIgnored var onAIExplain: ((String) -> Void)?
4848
@ObservationIgnored var onAIOptimize: ((String) -> Void)?
4949
@ObservationIgnored var onSaveAsFavorite: ((String) -> Void)?
50+
@ObservationIgnored var onFormatSQL: (() -> Void)?
5051

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

TablePro/Views/Editor/SQLEditorView.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ struct SQLEditorView: View {
2626
var onAIExplain: ((String) -> Void)?
2727
var onAIOptimize: ((String) -> Void)?
2828
var onSaveAsFavorite: ((String) -> Void)?
29+
var onFormatSQL: (() -> Void)?
2930

3031
@State private var editorState = SourceEditorState()
3132
@State private var completionAdapter: SQLCompletionAdapter?
@@ -104,6 +105,7 @@ struct SQLEditorView: View {
104105
coordinator.onAIExplain = onAIExplain
105106
coordinator.onAIOptimize = onAIOptimize
106107
coordinator.onSaveAsFavorite = onSaveAsFavorite
108+
coordinator.onFormatSQL = onFormatSQL
107109
setupFavoritesObserver()
108110
}
109111
} else {
@@ -118,6 +120,7 @@ struct SQLEditorView: View {
118120
coordinator.onAIExplain = onAIExplain
119121
coordinator.onAIOptimize = onAIOptimize
120122
coordinator.onSaveAsFavorite = onSaveAsFavorite
123+
coordinator.onFormatSQL = onFormatSQL
121124
setupFavoritesObserver()
122125
editorReady = true
123126
}

TablePro/Views/Export/ExportDialog.swift

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -274,6 +274,13 @@ struct ExportDialog: View {
274274

275275
Spacer()
276276
}
277+
278+
let description = formatDescription(for: config.formatId)
279+
if !description.isEmpty {
280+
Text(description)
281+
.font(.system(size: ThemeEngine.shared.activeTheme.typography.small))
282+
.foregroundStyle(.secondary)
283+
}
277284
}
278285

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

440+
private func formatDescription(for formatId: String) -> String {
441+
switch formatId {
442+
case "csv": return String(localized: "Comma-separated values. Compatible with Excel and most tools.")
443+
case "json": return String(localized: "Structured data format. Ideal for APIs and web applications.")
444+
case "sql": return String(localized: "SQL INSERT statements. Use to recreate data in another database.")
445+
case "xlsx": return String(localized: "Excel spreadsheet with formatting support.")
446+
case "mql": return String(localized: "MongoDB query language. Use to import into MongoDB.")
447+
default: return ""
448+
}
449+
}
450+
433451
private func isProGatedFormat(_ formatId: String) -> Bool {
434452
Self.proFormatIds.contains(formatId) && !LicenseManager.shared.isFeatureAvailable(.xlsxExport)
435453
}

TablePro/Views/Filter/FilterPanelView.swift

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -98,10 +98,18 @@ struct FilterPanelView: View {
9898
TextField("Preset Name", text: $newPresetName)
9999
Button("Cancel", role: .cancel) {}
100100
Button("Save") {
101-
if !newPresetName.isEmpty {
102-
filterState.saveAsPreset(name: newPresetName)
103-
loadPresets()
101+
guard !newPresetName.isEmpty else { return }
102+
var finalName = newPresetName
103+
let existingNames = Set(savedPresets.map(\.name))
104+
if existingNames.contains(finalName) {
105+
var counter = 2
106+
while existingNames.contains("\(newPresetName) (\(counter))") {
107+
counter += 1
108+
}
109+
finalName = "\(newPresetName) (\(counter))"
104110
}
111+
filterState.saveAsPreset(name: finalName)
112+
loadPresets()
105113
}
106114
} message: {
107115
Text("Enter a name for this filter preset")

0 commit comments

Comments
 (0)