From 33ec9f7f1439ff483f8a6f3db2f506ade41fcf5f Mon Sep 17 00:00:00 2001 From: Mark Gregory Date: Sun, 18 Oct 2026 14:19:52 +0100 Subject: [PATCH] Improved progress display Previously, the progress bar updated only after completing an entire table. For large tables such as LIBARC, this could make the application appear to have stopped responding. Phase 10 updates the interface while rows are being exported. --- WorkbenchReplacement/App.swift | 20 ++++++++++++ WorkbenchReplacement/DatabaseModel.swift | 38 +++++++++++++++++++++- WorkbenchReplacement/MainView.swift | 41 ++++++++++++++++++++++-- docs/PHASE58-SQL-EDITOR.md | 9 ++++++ 4 files changed, 105 insertions(+), 3 deletions(-) create mode 100644 docs/PHASE58-SQL-EDITOR.md diff --git a/WorkbenchReplacement/App.swift b/WorkbenchReplacement/App.swift index 4dce51d..7066cd1 100644 --- a/WorkbenchReplacement/App.swift +++ b/WorkbenchReplacement/App.swift @@ -1,6 +1,10 @@ import SwiftUI import AppKit +extension Notification.Name { + static let workbenchRevealSQLEditor = Notification.Name("WorkbenchReplacement.RevealSQLEditor") +} + @main struct WorkbenchReplacementApp: App { @StateObject private var model = DatabaseModel() @@ -410,6 +414,7 @@ struct WorkbenchFileCommands: Commands { CommandGroup(replacing: .newItem) { Button("New Query Tab") { model.newQueryTab() + NotificationCenter.default.post(name: .workbenchRevealSQLEditor, object: nil) } .keyboardShortcut("t", modifiers: .command) } @@ -420,6 +425,21 @@ struct WorkbenchFileCommands: Commands { } .keyboardShortcut("o", modifiers: .command) + Menu("Open Recent SQL Script") { + if model.recentSQLScripts.isEmpty { + Text("No Recent Scripts") + } else { + ForEach(model.recentSQLScripts, id: \.self) { path in + Button(URL(fileURLWithPath: path).lastPathComponent) { + model.openRecentSQLScript(at: path) + } + .help(path) + } + Divider() + Button("Clear Recent Scripts") { model.clearRecentSQLScripts() } + } + } + Divider() Button("Save SQL Script") { diff --git a/WorkbenchReplacement/DatabaseModel.swift b/WorkbenchReplacement/DatabaseModel.swift index 7592da7..3bb589f 100644 --- a/WorkbenchReplacement/DatabaseModel.swift +++ b/WorkbenchReplacement/DatabaseModel.swift @@ -87,6 +87,38 @@ final class DatabaseModel: ObservableObject { private let historyKey = "WorkbenchReplacement.queryHistory.v1" private let savedQueriesKey = "WorkbenchReplacement.savedQueries.v1" private let maxHistoryEntries = 200 + private let recentScriptsKey = "WorkbenchReplacement.recentSQLScripts.v1" + @Published private(set) var recentSQLScripts: [String] = [] + + private func recordRecentSQLScript(_ url: URL) { + let path = url.standardizedFileURL.path + recentSQLScripts.removeAll { $0 == path } + recentSQLScripts.insert(path, at: 0) + recentSQLScripts = Array(recentSQLScripts.prefix(12)) + UserDefaults.standard.set(recentSQLScripts, forKey: recentScriptsKey) + } + + func clearRecentSQLScripts() { + recentSQLScripts = [] + UserDefaults.standard.removeObject(forKey: recentScriptsKey) + } + + func openRecentSQLScript(at path: String) { + let url = URL(fileURLWithPath: path) + do { + let contents = try String(contentsOf: url, encoding: .utf8) + saveActiveQuery() + let tab = WorkbenchQueryTab(title: url.lastPathComponent, sql: contents, fileURL: url) + queryTabs.append(tab) + activateQuery(tab.id) + recordRecentSQLScript(url) + status = "Opened \(url.lastPathComponent)" + NotificationCenter.default.post(name: .workbenchRevealSQLEditor, object: nil) + } catch { + status = "Could not open \(url.lastPathComponent): \(error.localizedDescription)" + } + } + func saveCurrentQuery(named name: String) { let clean = name.trimmingCharacters(in: .whitespacesAndNewlines) @@ -153,6 +185,7 @@ final class DatabaseModel: ObservableObject { private let storageKey = "WorkbenchReplacement.connectionProfiles.native.v1" init() { + recentSQLScripts = UserDefaults.standard.stringArray(forKey: recentScriptsKey) ?? [] if let data = UserDefaults.standard.data(forKey: historyKey), let entries = try? JSONDecoder().decode([SQLHistoryEntry].self, from: data) { queryHistory = Array(entries.prefix(maxHistoryEntries)) @@ -171,7 +204,7 @@ final class DatabaseModel: ObservableObject { func newQueryTab() { saveActiveQuery() let tab = WorkbenchQueryTab( - title: "Query \\(queryTabs.count + 1)" + title: "Query \(queryTabs.count + 1)" ) queryTabs.append(tab) activeQueryID = tab.id @@ -235,6 +268,8 @@ final class DatabaseModel: ObservableObject { queryTabs.append(tab) activateQuery(tab.id) + recordRecentSQLScript(url) + NotificationCenter.default.post(name: .workbenchRevealSQLEditor, object: nil) status = "Opened \\(url.lastPathComponent)" } @@ -262,6 +297,7 @@ final class DatabaseModel: ObservableObject { try sql.write(to: url, atomically: true, encoding: .utf8) queryTabs[index].fileURL = url queryTabs[index].title = url.lastPathComponent + recordRecentSQLScript(url) status = "Saved \\(url.lastPathComponent)" } catch { status = "Save failed: \\(error.localizedDescription)" diff --git a/WorkbenchReplacement/MainView.swift b/WorkbenchReplacement/MainView.swift index 5700696..bfe81dd 100644 --- a/WorkbenchReplacement/MainView.swift +++ b/WorkbenchReplacement/MainView.swift @@ -89,6 +89,11 @@ struct MainView: View { ) == "dark" ? .dark : .light ) .background(Color(nsColor: .windowBackgroundColor)) + .onReceive(NotificationCenter.default.publisher(for: .workbenchRevealSQLEditor)) { _ in + administrationPage = nil + navigatorMode = 1 + DispatchQueue.main.async { sqlFocusRequest += 1 } + } .task { if model.databases.isEmpty, model.selected != nil { model.loadDatabases() @@ -842,8 +847,40 @@ struct MainView: View { } .help("Format SQL") - Divider() - .frame(height: 17) + Divider().frame(height: 17) + + Button { + model.openSQLScript() + } label: { + Image(systemName: "folder") + } + .help("Open SQL Script…") + + Button { + model.saveSQLScript() + } label: { + Image(systemName: "square.and.arrow.down") + } + .help("Save SQL Script") + + Menu { + if model.recentSQLScripts.isEmpty { + Text("No Recent Scripts") + } else { + ForEach(model.recentSQLScripts, id: \.self) { path in + Button(URL(fileURLWithPath: path).lastPathComponent) { + model.openRecentSQLScript(at: path) + } + } + Divider() + Button("Clear Recent Scripts") { model.clearRecentSQLScripts() } + } + } label: { + Image(systemName: "clock.arrow.circlepath") + } + .help("Recent SQL Scripts") + + Divider().frame(height: 17) Button { showingQueryHistory = true diff --git a/docs/PHASE58-SQL-EDITOR.md b/docs/PHASE58-SQL-EDITOR.md new file mode 100644 index 0000000..4c3a913 --- /dev/null +++ b/docs/PHASE58-SQL-EDITOR.md @@ -0,0 +1,9 @@ +# Phase 58 — SQL Editor Workflow + +Based on Phase 57. Adds persistent recent SQL script history (12 files), Open Recent and Clear Recent menus, SQL editor toolbar buttons for opening/saving scripts, and a shared navigation notification so opening a script or creating a query tab from the macOS File menu reveals and focuses the SQL editor. Corrects query tab numbering interpolation. Existing query tabs, history, saved queries, formatting, schema completion and shortcuts remain intact. + +## Testing +Swift sources passed `swiftc -frontend -parse` on Linux. This is not a macOS Xcode compile or live MySQL test. Build with `scripts/release-workbench.sh 58 SQL-Editor-Workflow` on a Mac. Verify opening scripts via File menu and toolbar, Open Recent, Save/Save As, new query tab, and dashboard-to-editor navigation. + +## Remaining roadmap +Full configurable execution settings and advanced completion preferences remain for Phase 65 (Preferences System). This release does not claim to implement them.