Skip to content

Commit 4fc4a04

Browse files
committed
feat: persistent query history with timestamps, swipe-to-delete, clear all
1 parent d5c5dad commit 4fc4a04

File tree

3 files changed

+116
-16
lines changed

3 files changed

+116
-16
lines changed
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
//
2+
// QueryHistoryStorage.swift
3+
// TableProMobile
4+
//
5+
6+
import Foundation
7+
8+
struct QueryHistoryItem: Identifiable, Codable, Hashable {
9+
let id: UUID
10+
let query: String
11+
let timestamp: Date
12+
let connectionId: UUID
13+
14+
init(id: UUID = UUID(), query: String, timestamp: Date = Date(), connectionId: UUID) {
15+
self.id = id
16+
self.query = query
17+
self.timestamp = timestamp
18+
self.connectionId = connectionId
19+
}
20+
}
21+
22+
struct QueryHistoryStorage {
23+
private static let maxEntries = 200
24+
25+
private var fileURL: URL? {
26+
guard let dir = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first else {
27+
return nil
28+
}
29+
let appDir = dir.appendingPathComponent("TableProMobile", isDirectory: true)
30+
try? FileManager.default.createDirectory(at: appDir, withIntermediateDirectories: true)
31+
return appDir.appendingPathComponent("query-history.json")
32+
}
33+
34+
func save(_ item: QueryHistoryItem) {
35+
var items = loadAll()
36+
if items.last?.query == item.query && items.last?.connectionId == item.connectionId {
37+
return
38+
}
39+
items.append(item)
40+
if items.count > Self.maxEntries {
41+
items.removeFirst(items.count - Self.maxEntries)
42+
}
43+
writeAll(items)
44+
}
45+
46+
func loadAll() -> [QueryHistoryItem] {
47+
guard let fileURL, let data = try? Data(contentsOf: fileURL),
48+
let items = try? JSONDecoder().decode([QueryHistoryItem].self, from: data) else {
49+
return []
50+
}
51+
return items
52+
}
53+
54+
func load(for connectionId: UUID) -> [QueryHistoryItem] {
55+
loadAll().filter { $0.connectionId == connectionId }
56+
}
57+
58+
func delete(_ id: UUID) {
59+
var items = loadAll()
60+
items.removeAll { $0.id == id }
61+
writeAll(items)
62+
}
63+
64+
func clearAll(for connectionId: UUID) {
65+
var items = loadAll()
66+
items.removeAll { $0.connectionId == connectionId }
67+
writeAll(items)
68+
}
69+
70+
private func writeAll(_ items: [QueryHistoryItem]) {
71+
guard let fileURL, let data = try? JSONEncoder().encode(items) else { return }
72+
try? data.write(to: fileURL, options: [.atomic, .completeFileProtection])
73+
}
74+
}

TableProMobile/TableProMobile/Views/ConnectedView.swift

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@ struct ConnectedView: View {
2222
@State private var toastMessage: String?
2323
@State private var toastTask: Task<Void, Never>?
2424
@State private var selectedTab = ConnectedTab.tables
25-
@State private var queryHistory: [String] = []
25+
@State private var queryHistory: [QueryHistoryItem] = []
26+
private let historyStorage = QueryHistoryStorage()
2627
@State private var databases: [String] = []
2728
@State private var activeDatabase: String = ""
2829
@State private var schemas: [String] = []
@@ -149,7 +150,10 @@ struct ConnectedView: View {
149150
}
150151
}
151152
}
152-
.task { await connect() }
153+
.task {
154+
await connect()
155+
queryHistory = historyStorage.load(for: connection.id)
156+
}
153157
.onChange(of: scenePhase) { _, phase in
154158
if phase == .active, session != nil {
155159
Task { await reconnectIfNeeded() }
@@ -180,7 +184,9 @@ struct ConnectedView: View {
180184
QueryEditorView(
181185
session: session,
182186
tables: tables,
183-
queryHistory: $queryHistory
187+
queryHistory: $queryHistory,
188+
connectionId: connection.id,
189+
historyStorage: historyStorage
184190
)
185191
}
186192
}

TableProMobile/TableProMobile/Views/QueryEditorView.swift

Lines changed: 33 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,9 @@ struct QueryEditorView: View {
2121
@State private var isExecuting = false
2222
@State private var executionTime: TimeInterval?
2323
@State private var executeTask: Task<Void, Never>?
24-
@Binding var queryHistory: [String]
24+
@Binding var queryHistory: [QueryHistoryItem]
25+
let connectionId: UUID
26+
let historyStorage: QueryHistoryStorage
2527
@State private var showHistory = false
2628
@FocusState private var editorFocused: Bool
2729

@@ -234,21 +236,42 @@ struct QueryEditorView: View {
234236
private var historySheet: some View {
235237
NavigationStack {
236238
List {
237-
ForEach(queryHistory.reversed(), id: \.self) { historyQuery in
239+
ForEach(queryHistory.reversed()) { item in
238240
Button {
239-
query = historyQuery
241+
query = item.query
240242
showHistory = false
241243
} label: {
242-
Text(verbatim: historyQuery)
243-
.font(.system(.footnote, design: .monospaced))
244-
.lineLimit(3)
245-
.foregroundStyle(.primary)
244+
VStack(alignment: .leading, spacing: 4) {
245+
Text(verbatim: item.query)
246+
.font(.system(.footnote, design: .monospaced))
247+
.lineLimit(3)
248+
.foregroundStyle(.primary)
249+
Text(item.timestamp, style: .relative)
250+
.font(.caption2)
251+
.foregroundStyle(.tertiary)
252+
}
253+
}
254+
}
255+
.onDelete { indexSet in
256+
let reversed = queryHistory.reversed().map(\.id)
257+
for index in indexSet {
258+
historyStorage.delete(reversed[index])
246259
}
260+
queryHistory = historyStorage.load(for: connectionId)
247261
}
248262
}
263+
.listStyle(.insetGrouped)
249264
.navigationTitle("Query History")
250265
.navigationBarTitleDisplayMode(.inline)
251266
.toolbar {
267+
ToolbarItem(placement: .cancellationAction) {
268+
if !queryHistory.isEmpty {
269+
Button("Clear All", role: .destructive) {
270+
historyStorage.clearAll(for: connectionId)
271+
queryHistory = []
272+
}
273+
}
274+
}
252275
ToolbarItem(placement: .confirmationAction) {
253276
Button("Done") { showHistory = false }
254277
}
@@ -282,12 +305,9 @@ struct QueryEditorView: View {
282305
self.result = queryResult
283306
self.executionTime = queryResult.executionTime
284307

285-
if !queryHistory.contains(trimmed) {
286-
queryHistory.append(trimmed)
287-
if queryHistory.count > 50 {
288-
queryHistory.removeFirst()
289-
}
290-
}
308+
let item = QueryHistoryItem(query: trimmed, connectionId: connectionId)
309+
historyStorage.save(item)
310+
queryHistory = historyStorage.load(for: connectionId)
291311
} catch {
292312
let context = ErrorContext(operation: "executeQuery")
293313
self.appError = ErrorClassifier.classify(error, context: context)

0 commit comments

Comments
 (0)