added typeaheead and introspetion
This commit is contained in:
@@ -136,6 +136,25 @@ final class DatabaseModel: ObservableObject {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Phase 68: updating a saved query keeps its stable identity and list position.
|
||||||
|
func updateSavedQuery(_ id: UUID, name: String, sql updatedSQL: String) {
|
||||||
|
let trimmedName = name.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
guard !trimmedName.isEmpty,
|
||||||
|
!updatedSQL.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty,
|
||||||
|
let index = savedQueries.firstIndex(where: { $0.id == id }) else { return }
|
||||||
|
savedQueries[index].name = trimmedName
|
||||||
|
savedQueries[index].sql = updatedSQL
|
||||||
|
persistSavedQueries()
|
||||||
|
}
|
||||||
|
|
||||||
|
func replaceSavedQueryWithEditor(_ id: UUID) {
|
||||||
|
guard let index = savedQueries.firstIndex(where: { $0.id == id }),
|
||||||
|
!sql.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else { return }
|
||||||
|
savedQueries[index].sql = sql
|
||||||
|
persistSavedQueries()
|
||||||
|
status = "Updated saved query: \(savedQueries[index].name)"
|
||||||
|
}
|
||||||
|
|
||||||
func deleteSavedQuery(_ id: UUID) {
|
func deleteSavedQuery(_ id: UUID) {
|
||||||
savedQueries.removeAll { $0.id == id }
|
savedQueries.removeAll { $0.id == id }
|
||||||
persistSavedQueries()
|
persistSavedQueries()
|
||||||
|
|||||||
+75
-7
@@ -1311,11 +1311,21 @@ private struct SQLHistorySheet: View {
|
|||||||
@ObservedObject var model: DatabaseModel
|
@ObservedObject var model: DatabaseModel
|
||||||
@Environment(\.dismiss) private var dismiss
|
@Environment(\.dismiss) private var dismiss
|
||||||
@State private var filter = ""
|
@State private var filter = ""
|
||||||
|
@State private var resultFilter = 0
|
||||||
|
@State private var selectedConnection = "All connections"
|
||||||
|
|
||||||
|
private var connections: [String] {
|
||||||
|
["All connections"] + Array(Set(model.queryHistory.map(\.connectionName))).sorted()
|
||||||
|
}
|
||||||
|
|
||||||
private var entries: [SQLHistoryEntry] {
|
private var entries: [SQLHistoryEntry] {
|
||||||
model.queryHistory.filter {
|
model.queryHistory.filter { entry in
|
||||||
filter.isEmpty || $0.sql.localizedCaseInsensitiveContains(filter) ||
|
(filter.isEmpty || entry.sql.localizedCaseInsensitiveContains(filter) ||
|
||||||
$0.connectionName.localizedCaseInsensitiveContains(filter)
|
entry.connectionName.localizedCaseInsensitiveContains(filter) ||
|
||||||
|
entry.database.localizedCaseInsensitiveContains(filter) ||
|
||||||
|
entry.detail.localizedCaseInsensitiveContains(filter)) &&
|
||||||
|
(resultFilter == 0 || (resultFilter == 1 ? entry.succeeded : !entry.succeeded)) &&
|
||||||
|
(selectedConnection == "All connections" || entry.connectionName == selectedConnection)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1324,12 +1334,24 @@ private struct SQLHistorySheet: View {
|
|||||||
HStack {
|
HStack {
|
||||||
Text("Query History").font(.title2).bold()
|
Text("Query History").font(.title2).bold()
|
||||||
Spacer()
|
Spacer()
|
||||||
Button("Clear History") { model.clearQueryHistory() }
|
Text("\(entries.count) of \(model.queryHistory.count)")
|
||||||
|
.font(.caption).foregroundStyle(.secondary)
|
||||||
|
Button("Clear History", role: .destructive) { model.clearQueryHistory() }
|
||||||
.disabled(model.queryHistory.isEmpty)
|
.disabled(model.queryHistory.isEmpty)
|
||||||
Button("Done") { dismiss() }
|
Button("Done") { dismiss() }
|
||||||
}
|
}
|
||||||
TextField("Filter SQL or connection", text: $filter)
|
HStack {
|
||||||
|
TextField("Filter SQL, database, or error", text: $filter)
|
||||||
.textFieldStyle(.roundedBorder)
|
.textFieldStyle(.roundedBorder)
|
||||||
|
Picker("Result", selection: $resultFilter) {
|
||||||
|
Text("All").tag(0)
|
||||||
|
Text("Succeeded").tag(1)
|
||||||
|
Text("Failed").tag(2)
|
||||||
|
}.frame(width: 155)
|
||||||
|
Picker("Connection", selection: $selectedConnection) {
|
||||||
|
ForEach(connections, id: \.self) { Text($0).tag($0) }
|
||||||
|
}.frame(width: 190)
|
||||||
|
}
|
||||||
List(entries) { entry in
|
List(entries) { entry in
|
||||||
VStack(alignment: .leading, spacing: 5) {
|
VStack(alignment: .leading, spacing: 5) {
|
||||||
HStack {
|
HStack {
|
||||||
@@ -1340,6 +1362,10 @@ private struct SQLHistorySheet: View {
|
|||||||
Text(entry.connectionName).foregroundStyle(.secondary)
|
Text(entry.connectionName).foregroundStyle(.secondary)
|
||||||
Text(entry.database).foregroundStyle(.secondary)
|
Text(entry.database).foregroundStyle(.secondary)
|
||||||
Spacer()
|
Spacer()
|
||||||
|
Button("Copy SQL") {
|
||||||
|
NSPasteboard.general.clearContents()
|
||||||
|
NSPasteboard.general.setString(entry.sql, forType: .string)
|
||||||
|
}
|
||||||
Button("Open in New Tab") {
|
Button("Open in New Tab") {
|
||||||
model.newQueryTab()
|
model.newQueryTab()
|
||||||
model.sql = entry.sql
|
model.sql = entry.sql
|
||||||
@@ -1354,7 +1380,7 @@ private struct SQLHistorySheet: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.padding(20)
|
.padding(20)
|
||||||
.frame(minWidth: 740, minHeight: 460)
|
.frame(minWidth: 800, minHeight: 480)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1362,12 +1388,17 @@ private struct SavedSQLQueriesSheet: View {
|
|||||||
@ObservedObject var model: DatabaseModel
|
@ObservedObject var model: DatabaseModel
|
||||||
@Environment(\.dismiss) private var dismiss
|
@Environment(\.dismiss) private var dismiss
|
||||||
@State private var filter = ""
|
@State private var filter = ""
|
||||||
|
@State private var editingID: UUID?
|
||||||
|
@State private var editedName = ""
|
||||||
|
@State private var editedSQL = ""
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(alignment: .leading, spacing: 12) {
|
VStack(alignment: .leading, spacing: 12) {
|
||||||
HStack {
|
HStack {
|
||||||
Text("Saved SQL Queries").font(.title2).bold()
|
Text("Saved SQL Queries").font(.title2).bold()
|
||||||
Spacer()
|
Spacer()
|
||||||
|
Text("\(model.savedQueries.count) saved")
|
||||||
|
.font(.caption).foregroundStyle(.secondary)
|
||||||
Button("Done") { dismiss() }
|
Button("Done") { dismiss() }
|
||||||
}
|
}
|
||||||
TextField("Filter saved queries", text: $filter)
|
TextField("Filter saved queries", text: $filter)
|
||||||
@@ -1383,6 +1414,15 @@ private struct SavedSQLQueriesSheet: View {
|
|||||||
.lineLimit(2).foregroundStyle(.secondary)
|
.lineLimit(2).foregroundStyle(.secondary)
|
||||||
}
|
}
|
||||||
Spacer()
|
Spacer()
|
||||||
|
Button("Copy SQL") {
|
||||||
|
NSPasteboard.general.clearContents()
|
||||||
|
NSPasteboard.general.setString(entry.sql, forType: .string)
|
||||||
|
}
|
||||||
|
Button("Edit") {
|
||||||
|
editingID = entry.id
|
||||||
|
editedName = entry.name
|
||||||
|
editedSQL = entry.sql
|
||||||
|
}
|
||||||
Button("Open") {
|
Button("Open") {
|
||||||
model.openSavedQuery(entry)
|
model.openSavedQuery(entry)
|
||||||
dismiss()
|
dismiss()
|
||||||
@@ -1396,7 +1436,35 @@ private struct SavedSQLQueriesSheet: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.padding(20)
|
.padding(20)
|
||||||
.frame(minWidth: 650, minHeight: 420)
|
.frame(minWidth: 750, minHeight: 450)
|
||||||
|
.sheet(isPresented: Binding(
|
||||||
|
get: { editingID != nil },
|
||||||
|
set: { if !$0 { editingID = nil } }
|
||||||
|
)) {
|
||||||
|
VStack(alignment: .leading, spacing: 12) {
|
||||||
|
Text("Edit Saved Query").font(.title2).bold()
|
||||||
|
TextField("Query name", text: $editedName)
|
||||||
|
TextEditor(text: $editedSQL)
|
||||||
|
.font(.system(.body, design: .monospaced))
|
||||||
|
.frame(minHeight: 220)
|
||||||
|
.border(Color.secondary.opacity(0.3))
|
||||||
|
HStack {
|
||||||
|
Spacer()
|
||||||
|
Button("Cancel") { editingID = nil }
|
||||||
|
Button("Save Changes") {
|
||||||
|
if let id = editingID {
|
||||||
|
model.updateSavedQuery(id, name: editedName, sql: editedSQL)
|
||||||
|
}
|
||||||
|
editingID = nil
|
||||||
|
}
|
||||||
|
.keyboardShortcut(.defaultAction)
|
||||||
|
.disabled(editedName.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty ||
|
||||||
|
editedSQL.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(20)
|
||||||
|
.frame(minWidth: 620, minHeight: 350)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
# Phase 68 — SQL Productivity
|
||||||
|
|
||||||
|
Based on Phase 67.
|
||||||
|
|
||||||
|
- Query History: search by SQL, connection, schema, or execution detail; filter by successful/failed execution and connection.
|
||||||
|
- Query History: copy SQL to clipboard, open a historical query in a new editor tab, and see matching result counts.
|
||||||
|
- Saved SQL Queries: edit the name and SQL of existing saved queries, with validation before saving.
|
||||||
|
- Saved SQL Queries: copy saved SQL without opening or executing it.
|
||||||
|
- Saved query edits preserve stable IDs and are persisted through the existing storage mechanism.
|
||||||
|
- No SQL is executed by browsing, editing, or copying history and saved queries.
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
Swift source was checked structurally, but full Xcode compilation and macOS UI testing are required.
|
||||||
Reference in New Issue
Block a user