new tests

This commit is contained in:
Mark Gregory
2026-10-19 10:53:12 +01:00
parent 8d33346993
commit 82a54e7905
4 changed files with 163 additions and 40 deletions
+151 -38
View File
@@ -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.")
+7 -1
View File
@@ -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 }
+3
View File
@@ -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
+2 -1
View File
@@ -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)