From 82a54e79052af1bf3f5a6e4d96cd4514b91b362a Mon Sep 17 00:00:00 2001 From: Mark Gregory Date: Mon, 19 Oct 2026 10:53:12 +0100 Subject: [PATCH] new tests --- DBSyn/App.swift | 189 ++++++++++++++++++++++++------- DBSyn/LaravelMigrationView.swift | 8 +- DBSyn/MainView.swift | 3 + DBSyn/SchemaDiagramView.swift | 3 +- 4 files changed, 163 insertions(+), 40 deletions(-) diff --git a/DBSyn/App.swift b/DBSyn/App.swift index f8e04f4..42e2b2c 100644 --- a/DBSyn/App.swift +++ b/DBSyn/App.swift @@ -2,6 +2,7 @@ import SwiftUI import AppKit extension Notification.Name { + static let dbsynOpenConnectionManager = Notification.Name("DBSyn.OpenConnectionManager") static let workbenchRevealSQLEditor = Notification.Name("DBSyn.RevealSQLEditor") } @@ -33,7 +34,7 @@ struct DBSynApp: App { let icon = NSImage(contentsOfFile: Bundle.main.path(forResource: "DBSynBrand", ofType: "png") ?? "") NSApplication.shared.orderFrontStandardAboutPanel(options: [ .applicationName: "DBSyn", - .applicationIcon: icon ?? NSApplication.shared.applicationIconImage, + .applicationIcon: icon ?? NSApplication.shared.applicationIconImage ?? NSImage(), .version: Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String ?? "1.0", .credits: NSAttributedString(string: "Native MySQL client for macOS") ]) @@ -125,48 +126,153 @@ struct WorkbenchSettingsView: View { @AppStorage("preferences.migrationFilenamePrefix") private var migrationFilenamePrefix = "create" - var body: some View { - TabView { + @State private var selectedSection: SettingsSection? = .general - editorSettings - .tabItem { - Label( - "SQL Editor", - systemImage: "chevron.left.forwardslash.chevron.right" - ) - } - - executionSettings - .tabItem { - Label( - "Query Execution", - systemImage: "bolt" - ) - } - - resultSettings - .tabItem { - Label( - "Result Grid", - systemImage: "tablecells" - ) - } - - migrationSettings - .tabItem { Label("Laravel", systemImage: "curlybraces") } - - generalSettings - .tabItem { - Label( - "General", - systemImage: "gearshape" - ) - } + private enum SettingsSection: String, CaseIterable, Identifiable { + case general = "General" + case connections = "Connections" + case security = "Security" + case editor = "SQL Editor" + case execution = "Query Execution" + case results = "Result Grid" + case database = "Database" + case diagrams = "Schema Designer" + case laravel = "Laravel" + case administration = "Administration" + case exports = "Export & Backup" + case advanced = "Advanced" + var id: String { rawValue } + var icon: String { + switch self { + case .general: return "gearshape" + case .connections: return "server.rack" + case .security: return "lock.shield" + case .editor: return "chevron.left.forwardslash.chevron.right" + case .execution: return "bolt" + case .results: return "tablecells" + case .database: return "externaldrive" + case .diagrams: return "point.3.connected.trianglepath.dotted" + case .laravel: return "curlybraces" + case .administration: return "gauge.with.dots.needle.67percent" + case .exports: return "square.and.arrow.up" + case .advanced: return "wrench.and.screwdriver" + } } - .frame(width: 760, height: 480) + } + + @AppStorage("preferences.diagramPalette") private var diagramPalette = "Ocean" + @AppStorage("preferences.diagramShowAllColumns") private var diagramShowAllColumns = false + @AppStorage("preferences.migrationOutputDirectory") private var migrationOutputDirectory = "" + @AppStorage("preferences.migrationUseTimestamps") private var migrationUseTimestamps = true + @AppStorage("preferences.confirmDestructiveSQL") private var confirmDestructiveSQL = true + + var body: some View { + NavigationSplitView { + List(SettingsSection.allCases, selection: $selectedSection) { section in + Label(section.rawValue, systemImage: section.icon) + .tag(section) + .padding(.vertical, 3) + } + .listStyle(.sidebar) + .navigationTitle("Settings") + .frame(minWidth: 205) + } detail: { + Group { + switch selectedSection ?? .general { + case .general: generalSettings + case .connections: connectionsSettings + case .security: securitySettings + case .editor: editorSettings + case .execution: executionSettings + case .results: resultSettings + case .database: databaseSettings + case .diagrams: diagramSettings + case .laravel: migrationSettings + case .administration: administrationSettings + case .exports: exportSettings + case .advanced: advancedSettings + } + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + } + .navigationSplitViewStyle(.balanced) + .frame(minWidth: 940, idealWidth: 1050, minHeight: 610, idealHeight: 710) .preferredColorScheme(appearance == "dark" ? .dark : appearance == "light" ? .light : nil) } + private func information(_ message: String) -> some View { + Text(message).font(.callout).foregroundStyle(.secondary) + .frame(maxWidth: .infinity, alignment: .leading) + .fixedSize(horizontal: false, vertical: true) + } + + private var connectionsSettings: some View { + settingsPage("Connections") { + information("Manage and switch saved connections from the main window's Connections menu. Select a profile to connect, or edit its MySQL host, port and default schema.") + information("Connection profiles and credentials are stored separately. Passwords are held in macOS Keychain.") + Button("Open Connection Manager") { + NotificationCenter.default.post(name: .dbsynOpenConnectionManager, object: nil) + // Leave window management to the Settings scene. + } + .buttonStyle(.borderedProminent) + } + } + + private var securitySettings: some View { + settingsPage("Security & Transport") { + information("Each connection has independent TLS and SSH transport options. Edit a connection to configure certificate-verified TLS, or an SSH tunnel using macOS OpenSSH.") + information("Do not disable certificate verification to work around connection failures. Native TLS handshake compatibility must be validated against your server before relying on it.") + information("Passwords use the DBSyn.MySQL Keychain service. Legacy WorkbenchReplacement.MySQL credentials are read for compatibility.") + } + } + + private var databaseSettings: some View { + settingsPage("Database Navigator") { + settingsRow("Expand tables") { + Toggle("Automatically expand table folders", isOn: $autoExpandTables) + } + information("Select the active database in the schema navigator. Refresh schema metadata from the main window after switching connections.") + } + } + + private var diagramSettings: some View { + settingsPage("Schema Designer") { + settingsRow("Default palette") { + Picker("Palette", selection: $diagramPalette) { + ForEach(["Ocean", "Slate", "Forest", "Plum"], id: \.self) { Text($0) } + }.labelsHidden().frame(width: 200) + } + settingsRow("Columns") { + Toggle("Show all columns by default", isOn: $diagramShowAllColumns) + } + information("Diagram-specific layouts and table selections are saved from the Schema Designer. You can override the palette and column visibility there.") + } + } + + private var administrationSettings: some View { + settingsPage("Administration") { + information("Server status, connections, users, variables, performance and logs are available in the Administration navigator.") + information("Server start/stop and configuration-file editing require server-level operating-system privileges and are not available through ordinary MySQL credentials.") + } + } + + private var exportSettings: some View { + settingsPage("Export & Backup") { + information("SQL and CSV export, backup and restore actions are available from the main window. Choose a destination when exporting.") + information("Backups and restores can be destructive. Review the target server and schema before starting a restore.") + } + } + + private var advancedSettings: some View { + settingsPage("Advanced") { + settingsRow("Safety") { + Toggle("Confirm destructive SQL operations", isOn: $confirmDestructiveSQL) + } + information("The safety preference is reserved for a future SQL confirmation integration; it does not yet intercept SQL execution.") + information("For troubleshooting, use the macOS Console and the DBSyn test runner. TLS negotiation must be tested on a macOS host.") + } + } + private var migrationSettings: some View { settingsPage("Laravel & Developer Tools") { Text("Generate Laravel migrations from a connected MySQL table or an imported CREATE TABLE SQL file.") @@ -175,6 +281,13 @@ struct WorkbenchSettingsView: View { TextField("create", text: $migrationFilenamePrefix) .textFieldStyle(.roundedBorder) } + settingsRow("Output folder") { + TextField("Choose folder when saving", text: $migrationOutputDirectory) + .textFieldStyle(.roundedBorder) + } + settingsRow("Filename") { + Toggle("Use timestamp prefix", isOn: $migrationUseTimestamps) + } Text("Open Tools → Generate Laravel Migration to preview, edit and save the generated PHP file.") .font(.callout).foregroundStyle(.secondary) Text("Generated migrations must be reviewed before execution. DBSyn never executes generated PHP.") diff --git a/DBSyn/LaravelMigrationView.swift b/DBSyn/LaravelMigrationView.swift index a12f1d8..f5cc682 100644 --- a/DBSyn/LaravelMigrationView.swift +++ b/DBSyn/LaravelMigrationView.swift @@ -276,7 +276,13 @@ struct LaravelMigrationView: View { private func save() { let panel = NSSavePanel(); panel.allowedContentTypes = [UTType(filenameExtension: "php") ?? .plainText] let name = tableName.isEmpty ? "table" : tableName.lowercased() - panel.nameFieldStringValue = "\(prefix)_\(name)_table.php" + let timestamp = DateFormatter() + timestamp.dateFormat = "yyyy_MM_dd_HHmmss" + let useTimestamps = UserDefaults.standard.object(forKey: "preferences.migrationUseTimestamps") as? Bool ?? true + let datePrefix = useTimestamps ? timestamp.string(from: Date()) + "_" : "" + panel.nameFieldStringValue = "\(datePrefix)\(prefix)_\(name)_table.php" + let folder = UserDefaults.standard.string(forKey: "preferences.migrationOutputDirectory") ?? "" + if !folder.isEmpty { panel.directoryURL = URL(fileURLWithPath: (folder as NSString).expandingTildeInPath, isDirectory: true) } if panel.runModal() == .OK, let url = panel.url { do { try generated.write(to: url, atomically: true, encoding: .utf8); errorText = ""; statusText = "Saved: \(url.lastPathComponent)" } catch { errorText = error.localizedDescription } diff --git a/DBSyn/MainView.swift b/DBSyn/MainView.swift index 2b1ace5..6051022 100644 --- a/DBSyn/MainView.swift +++ b/DBSyn/MainView.swift @@ -85,6 +85,9 @@ struct MainView: View { } .frame(minWidth: 800, minHeight: 520) .background(Color(nsColor: .windowBackgroundColor)) + .onReceive(NotificationCenter.default.publisher(for: .dbsynOpenConnectionManager)) { _ in + showingConnectionManager = true + } .onReceive(NotificationCenter.default.publisher(for: .workbenchRevealSQLEditor)) { _ in administrationPage = nil navigatorMode = 1 diff --git a/DBSyn/SchemaDiagramView.swift b/DBSyn/SchemaDiagramView.swift index 60d9347..10874c6 100644 --- a/DBSyn/SchemaDiagramView.swift +++ b/DBSyn/SchemaDiagramView.swift @@ -98,7 +98,8 @@ struct DBSynSchemaDiagramView: View { if let stored = UserDefaults.standard.stringArray(forKey: selectionKey) { includedObjects = Set(stored) } else { includedObjects = Set(allObjects) } - theme = UserDefaults.standard.string(forKey: themeKey) ?? "Ocean" + theme = UserDefaults.standard.string(forKey: themeKey) ?? UserDefaults.standard.string(forKey: "preferences.diagramPalette") ?? "Ocean" + showAllColumns = UserDefaults.standard.bool(forKey: "preferences.diagramShowAllColumns") } private func saveOptions() { UserDefaults.standard.set(Array(includedObjects).sorted(), forKey: selectionKey)