Changes
* Replaced the nested export form with a scrollable layout. * Made the table selection checkboxes directly visible. * Preserved the Select All and Select None buttons. * Kept the view selection controls. * Cleared stale table selections when switching databases. * Added automatic database loading when the application opens. * Preserved the existing SQL editor and administration functionality.
This commit is contained in:
@@ -68,6 +68,11 @@ struct MainView: View {
|
||||
) == "dark" ? .dark : .light
|
||||
)
|
||||
.background(Color(nsColor: .windowBackgroundColor))
|
||||
.task {
|
||||
if model.databases.isEmpty, model.selected != nil {
|
||||
model.loadDatabases()
|
||||
}
|
||||
}
|
||||
.sheet(isPresented: $showingDatabaseTransfer) {
|
||||
WorkbenchDatabaseTransferView(model: model)
|
||||
}
|
||||
@@ -1572,6 +1577,9 @@ struct WorkbenchDatabaseTransferView: View {
|
||||
@State private var exportStructure = true
|
||||
@State private var exportData = true
|
||||
@State private var includeDrop = false
|
||||
@State private var includeViews = true
|
||||
@State private var availableViews: [String] = []
|
||||
@State private var selectedViews: Set<String> = []
|
||||
@State private var loadingTables = false
|
||||
@State private var availableDatabases: [String] = []
|
||||
@State private var loadingDatabases = false
|
||||
@@ -1593,8 +1601,10 @@ struct WorkbenchDatabaseTransferView: View {
|
||||
.pickerStyle(.segmented)
|
||||
.padding(20)
|
||||
|
||||
Form {
|
||||
Section("MySQL Connection") {
|
||||
ScrollView {
|
||||
VStack(alignment: .leading, spacing: 16) {
|
||||
GroupBox("MySQL Connection") {
|
||||
VStack(alignment: .leading, spacing: 10) {
|
||||
LabeledContent("Connection") {
|
||||
Text(selectedProfile?.name ?? "Not selected")
|
||||
}
|
||||
@@ -1613,14 +1623,18 @@ struct WorkbenchDatabaseTransferView: View {
|
||||
}
|
||||
.disabled(loadingDatabases || selectedProfile == nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if selectedMode == 0 {
|
||||
Section("Export Options") {
|
||||
GroupBox("Export Options") {
|
||||
VStack(alignment: .leading, spacing: 10) {
|
||||
Toggle("Table structure", isOn: $exportStructure)
|
||||
Toggle("Table data", isOn: $exportData)
|
||||
Toggle("Add DROP TABLE IF EXISTS", isOn: $includeDrop)
|
||||
.disabled(!exportStructure)
|
||||
Toggle("Include views (definitions)", isOn: $includeViews)
|
||||
.disabled(!exportStructure)
|
||||
HStack {
|
||||
Text("Tables (\(selectedTables.count) selected)")
|
||||
Spacer()
|
||||
@@ -1630,25 +1644,43 @@ struct WorkbenchDatabaseTransferView: View {
|
||||
.disabled(loadingTables)
|
||||
}
|
||||
if loadingTables { ProgressView("Loading tables…") }
|
||||
ScrollView {
|
||||
LazyVStack(alignment: .leading) {
|
||||
ForEach(availableTables, id: \.self) { table in
|
||||
Toggle(table, isOn: Binding(
|
||||
get: { selectedTables.contains(table) },
|
||||
set: { checked in
|
||||
if checked { selectedTables.insert(table) }
|
||||
else { selectedTables.remove(table) }
|
||||
}
|
||||
))
|
||||
if availableTables.isEmpty && !loadingTables {
|
||||
Text("No tables found. Select a database or refresh the list.")
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
ForEach(availableTables, id: \.self) { table in
|
||||
Toggle(table, isOn: Binding(
|
||||
get: { selectedTables.contains(table) },
|
||||
set: { checked in
|
||||
if checked { selectedTables.insert(table) }
|
||||
else { selectedTables.remove(table) }
|
||||
}
|
||||
))
|
||||
}
|
||||
if includeViews && exportStructure {
|
||||
HStack {
|
||||
Text("Views (\(selectedViews.count) selected)")
|
||||
Spacer()
|
||||
Button("All") { selectedViews = Set(availableViews) }
|
||||
Button("None") { selectedViews.removeAll() }
|
||||
}
|
||||
ForEach(availableViews, id: \.self) { view in
|
||||
Toggle(view, isOn: Binding(
|
||||
get: { selectedViews.contains(view) },
|
||||
set: { checked in
|
||||
if checked { selectedViews.insert(view) }
|
||||
else { selectedViews.remove(view) }
|
||||
}
|
||||
))
|
||||
}
|
||||
}
|
||||
.frame(maxHeight: 180)
|
||||
Text("Exports base tables with batched INSERT statements. Views, triggers, routines and transaction-consistent snapshots are not included.")
|
||||
Text("Exports base tables and selected views. Triggers, routines, consistent snapshots and guaranteed row ordering are not yet supported.")
|
||||
.font(.caption).foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Section("Import Options") {
|
||||
GroupBox("Import Options") {
|
||||
VStack(alignment: .leading, spacing: 10) {
|
||||
Text("Import SQL statements into the selected database.")
|
||||
.foregroundStyle(.secondary)
|
||||
|
||||
@@ -1656,9 +1688,11 @@ struct WorkbenchDatabaseTransferView: View {
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Section("Progress") {
|
||||
GroupBox("Progress") {
|
||||
VStack(alignment: .leading, spacing: 10) {
|
||||
ProgressView(value: progress)
|
||||
.frame(maxWidth: .infinity)
|
||||
|
||||
@@ -1666,9 +1700,12 @@ struct WorkbenchDatabaseTransferView: View {
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
.textSelection(.enabled)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.formStyle(.grouped)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.padding(20)
|
||||
|
||||
Divider()
|
||||
|
||||
@@ -1701,7 +1738,7 @@ struct WorkbenchDatabaseTransferView: View {
|
||||
.disabled(
|
||||
selectedProfile == nil ||
|
||||
database.isEmpty ||
|
||||
(selectedMode == 0 && (selectedTables.isEmpty || (!exportStructure && !exportData)))
|
||||
(selectedMode == 0 && ((!exportStructure && !exportData) || (selectedTables.isEmpty && !(exportStructure && includeViews && !selectedViews.isEmpty))))
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1719,6 +1756,10 @@ struct WorkbenchDatabaseTransferView: View {
|
||||
await loadTransferDatabases()
|
||||
}
|
||||
.task(id: database) {
|
||||
availableTables = []
|
||||
selectedTables = []
|
||||
availableViews = []
|
||||
selectedViews = []
|
||||
if !database.isEmpty { await loadTables() }
|
||||
}
|
||||
.confirmationDialog(
|
||||
@@ -1846,7 +1887,11 @@ struct WorkbenchDatabaseTransferView: View {
|
||||
|
||||
@MainActor
|
||||
private func loadTables() async {
|
||||
guard !database.isEmpty else { availableTables = []; selectedTables = []; return }
|
||||
guard !database.isEmpty else {
|
||||
availableTables = []; selectedTables = []
|
||||
availableViews = []; selectedViews = []
|
||||
return
|
||||
}
|
||||
loadingTables = true
|
||||
defer { loadingTables = false }
|
||||
do {
|
||||
@@ -1855,10 +1900,16 @@ struct WorkbenchDatabaseTransferView: View {
|
||||
row.count > 1 && row[1].uppercased() == "BASE TABLE" ? row[0] : nil
|
||||
}
|
||||
selectedTables = Set(availableTables)
|
||||
availableViews = rows.rows.compactMap { row in
|
||||
row.count > 1 && row[1].uppercased() == "VIEW" ? row[0] : nil
|
||||
}
|
||||
selectedViews = Set(availableViews)
|
||||
} catch {
|
||||
status = "Cannot list tables: \(error.localizedDescription)"
|
||||
availableTables = []
|
||||
selectedTables = []
|
||||
availableViews = []
|
||||
selectedViews = []
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1866,7 +1917,11 @@ struct WorkbenchDatabaseTransferView: View {
|
||||
// preserved rather than reconstructed from QueryResult's String values.
|
||||
@MainActor
|
||||
private func exportSQL(to url: URL) async {
|
||||
guard !selectedTables.isEmpty, exportStructure || exportData else { return }
|
||||
guard exportStructure || exportData else { return }
|
||||
let views = exportStructure && includeViews
|
||||
? availableViews.filter { selectedViews.contains($0) }
|
||||
: []
|
||||
guard !selectedTables.isEmpty || !views.isEmpty else { return }
|
||||
isWorking = true
|
||||
progress = 0
|
||||
let tables = availableTables.filter { selectedTables.contains($0) }
|
||||
@@ -1937,7 +1992,23 @@ struct WorkbenchDatabaseTransferView: View {
|
||||
if batch.rows.count < 250 { break }
|
||||
}
|
||||
}
|
||||
progress = Double(index + 1) / Double(tables.count)
|
||||
progress = Double(index + 1) / Double(max(tables.count + views.count, 1))
|
||||
}
|
||||
// Views are exported after base tables so dependencies exist on restore.
|
||||
// The server's SHOW CREATE VIEW response may contain a DEFINER clause;
|
||||
// importing under a different MySQL account can require extra privileges.
|
||||
for (index, view) in views.enumerated() {
|
||||
try Task.checkCancellation()
|
||||
let qualified = "\(db).\(quoteIdentifier(view))"
|
||||
status = "Exporting view \(view)"
|
||||
let definition = try await execute("SHOW CREATE VIEW \(qualified)")
|
||||
guard let row = definition.rows.first, row.count > 1 else {
|
||||
throw WorkbenchTransferError.message("Missing CREATE VIEW for \(view)")
|
||||
}
|
||||
try write("\n-- View \(qualified)\n")
|
||||
if includeDrop { try write("DROP VIEW IF EXISTS \(qualified);\n") }
|
||||
try write(row[1] + ";\n")
|
||||
progress = Double(tables.count + index + 1) / Double(max(tables.count + views.count, 1))
|
||||
}
|
||||
try Task.checkCancellation()
|
||||
try handle?.close()
|
||||
@@ -1948,7 +2019,7 @@ struct WorkbenchDatabaseTransferView: View {
|
||||
try FileManager.default.moveItem(at: tempURL, to: url)
|
||||
}
|
||||
progress = 1
|
||||
status = "Export complete: \(tables.count) tables"
|
||||
status = "Export complete: \(tables.count) tables, \(views.count) views"
|
||||
} catch is CancellationError {
|
||||
status = "Export cancelled; no final backup was written"
|
||||
} catch {
|
||||
|
||||
Reference in New Issue
Block a user