Skip to content

Commit 6be25e5

Browse files
committed
fix(datagrid): persist column layout per table, keep hidden columns hidden, and show errors inline
1 parent 2d66407 commit 6be25e5

37 files changed

Lines changed: 563 additions & 392 deletions

TablePro/Core/Coordinators/QueryExecutionCoordinator+Helpers.swift

Lines changed: 1 addition & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -488,8 +488,7 @@ extension QueryExecutionCoordinator {
488488
_ error: Error,
489489
sql: String,
490490
tabId: UUID,
491-
connection conn: DatabaseConnection,
492-
trigger: TableLoadTrigger = .userInitiated
491+
connection conn: DatabaseConnection
493492
) {
494493
parent.currentQueryTask = nil
495494
parent.tabManager.mutate(tabId: tabId) { tab in
@@ -508,31 +507,6 @@ extension QueryExecutionCoordinator {
508507
wasSuccessful: false,
509508
errorMessage: error.localizedDescription
510509
)
511-
512-
guard !trigger.suppressesFailureModal else { return }
513-
514-
let errorMessage = error.localizedDescription
515-
let queryCopy = sql
516-
Task { [weak self, parent] in
517-
guard let self else { return }
518-
if AppSettingsManager.shared.ai.enabled {
519-
let wantsAIFix = await AlertHelper.showQueryErrorWithAIOption(
520-
title: String(localized: "Query Execution Failed"),
521-
message: errorMessage,
522-
window: parent.contentWindow
523-
)
524-
if wantsAIFix {
525-
parent.showAIChatPanel()
526-
parent.aiViewModel?.handleFixError(query: queryCopy, error: errorMessage)
527-
}
528-
} else {
529-
AlertHelper.showErrorSheet(
530-
title: String(localized: "Query Execution Failed"),
531-
message: errorMessage,
532-
window: parent.contentWindow
533-
)
534-
}
535-
}
536510
}
537511

538512
func restoreSchemaAndRunQuery(_ schema: String, trigger: TableLoadTrigger = .userInitiated) async {

TablePro/Core/Coordinators/QueryExecutionCoordinator+Parameters.swift

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -462,12 +462,6 @@ extension QueryExecutionCoordinator {
462462
wasSuccessful: false,
463463
errorMessage: error.localizedDescription
464464
)
465-
466-
AlertHelper.showErrorSheet(
467-
title: String(localized: "Query Execution Failed"),
468-
message: contextMsg,
469-
window: parent.contentWindow
470-
)
471465
}
472466
}
473467
}

TablePro/Core/Storage/ColumnLayoutPersister.swift

Lines changed: 36 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,16 @@ import os
88

99
@MainActor
1010
final class FileColumnLayoutPersister: ColumnLayoutPersisting {
11+
static let shared: FileColumnLayoutPersister = {
12+
let persister = FileColumnLayoutPersister()
13+
persister.performScopeMigration()
14+
return persister
15+
}()
16+
1117
private static let logger = Logger(subsystem: "com.TablePro", category: "ColumnLayoutPersister")
12-
private static let legacyKeyPrefix = "com.TablePro.columns.layout."
13-
private static let migrationCompleteKey = "com.TablePro.columnLayoutMigrationComplete"
18+
private static let legacyUserDefaultsPrefix = "com.TablePro.columns.layout."
19+
private static let legacyVisibilityPrefix = "com.TablePro.columns.hiddenColumns."
20+
private static let scopeMigrationKey = "com.TablePro.columnLayoutSchemaScopeMigrationComplete"
1421

1522
private struct PersistedColumnLayout: Codable {
1623
var columnWidths: [String: CGFloat]
@@ -34,44 +41,42 @@ final class FileColumnLayoutPersister: ColumnLayoutPersisting {
3441
} catch {
3542
Self.logger.error("Failed to create storage directory: \(error.localizedDescription)")
3643
}
37-
38-
Self.performMigrationIfNeeded(storageDirectory: self.storageDirectory)
3944
}
4045

41-
func save(_ layout: ColumnLayoutState, for tableName: String, connectionId: UUID) {
46+
func save(_ layout: ColumnLayoutState, for key: ColumnLayoutTableKey) {
4247
guard !layout.columnWidths.isEmpty else { return }
4348

4449
let persisted = PersistedColumnLayout(
4550
columnWidths: layout.columnWidths,
4651
columnOrder: layout.columnOrder
4752
)
4853

49-
var entries = loadEntries(for: connectionId)
50-
entries[tableName] = persisted
51-
cache[connectionId] = entries
52-
writeEntries(entries, for: connectionId)
54+
var entries = loadEntries(for: key.connectionId)
55+
entries[key.storageKey] = persisted
56+
cache[key.connectionId] = entries
57+
writeEntries(entries, for: key.connectionId)
5358
}
5459

55-
func load(for tableName: String, connectionId: UUID) -> ColumnLayoutState? {
56-
let entries = loadEntries(for: connectionId)
57-
guard let persisted = entries[tableName] else { return nil }
60+
func load(for key: ColumnLayoutTableKey) -> ColumnLayoutState? {
61+
let entries = loadEntries(for: key.connectionId)
62+
guard let persisted = entries[key.storageKey] else { return nil }
5863

5964
var state = ColumnLayoutState()
6065
state.columnWidths = persisted.columnWidths
6166
state.columnOrder = persisted.columnOrder
6267
return state
6368
}
6469

65-
func clear(for tableName: String, connectionId: UUID) {
66-
var entries = loadEntries(for: connectionId)
67-
guard entries.removeValue(forKey: tableName) != nil else { return }
70+
func clear(for key: ColumnLayoutTableKey) {
71+
var entries = loadEntries(for: key.connectionId)
72+
guard entries.removeValue(forKey: key.storageKey) != nil else { return }
6873

6974
if entries.isEmpty {
70-
cache[connectionId] = [:]
71-
removeFile(for: connectionId)
75+
cache[key.connectionId] = [:]
76+
removeFile(for: key.connectionId)
7277
} else {
73-
cache[connectionId] = entries
74-
writeEntries(entries, for: connectionId)
78+
cache[key.connectionId] = entries
79+
writeEntries(entries, for: key.connectionId)
7580
}
7681
}
7782

@@ -136,53 +141,26 @@ final class FileColumnLayoutPersister: ColumnLayoutPersisting {
136141
.appendingPathComponent("ColumnLayout", isDirectory: true)
137142
}
138143

139-
private static func performMigrationIfNeeded(storageDirectory: URL) {
144+
private func performScopeMigration() {
140145
let defaults = UserDefaults.standard
141-
guard !defaults.bool(forKey: migrationCompleteKey) else { return }
142-
143-
let allKeys = defaults.dictionaryRepresentation().keys
144-
let legacyKeys = allKeys.filter { $0.hasPrefix(legacyKeyPrefix) }
145-
146-
var grouped: [UUID: [String: PersistedColumnLayout]] = [:]
147-
let decoder = JSONDecoder()
148-
149-
for key in legacyKeys {
150-
let suffix = String(key.dropFirst(legacyKeyPrefix.count))
151-
guard let dotIndex = suffix.firstIndex(of: ".") else { continue }
152-
153-
let uuidString = String(suffix[..<dotIndex])
154-
let tableName = String(suffix[suffix.index(after: dotIndex)...])
155-
156-
guard let connectionId = UUID(uuidString: uuidString),
157-
let data = defaults.data(forKey: key),
158-
let persisted = try? decoder.decode(PersistedColumnLayout.self, from: data) else {
159-
defaults.removeObject(forKey: key)
160-
continue
146+
guard !defaults.bool(forKey: Self.scopeMigrationKey) else { return }
147+
148+
if let files = try? FileManager.default.contentsOfDirectory(
149+
at: storageDirectory,
150+
includingPropertiesForKeys: nil
151+
) {
152+
for file in files where file.pathExtension == "json" {
153+
try? FileManager.default.removeItem(at: file)
161154
}
162-
163-
grouped[connectionId, default: [:]][tableName] = persisted
164155
}
165156

166-
let encoder = JSONEncoder()
167-
for (connectionId, entries) in grouped {
168-
let fileURL = storageDirectory.appendingPathComponent("\(connectionId.uuidString).json")
169-
do {
170-
let data = try encoder.encode(entries)
171-
try data.write(to: fileURL, options: .atomic)
172-
} catch {
173-
logger.error(
174-
"Migration failed for \(connectionId): \(error.localizedDescription)"
175-
)
176-
}
157+
let legacyKeys = defaults.dictionaryRepresentation().keys.filter {
158+
$0.hasPrefix(Self.legacyUserDefaultsPrefix) || $0.hasPrefix(Self.legacyVisibilityPrefix)
177159
}
178-
179160
for key in legacyKeys {
180161
defaults.removeObject(forKey: key)
181162
}
182-
defaults.set(true, forKey: migrationCompleteKey)
183163

184-
if !grouped.isEmpty {
185-
logger.trace("Migrated \(grouped.count) connection(s) of column layouts to file storage")
186-
}
164+
defaults.set(true, forKey: Self.scopeMigrationKey)
187165
}
188166
}

TablePro/Core/Storage/ColumnLayoutPersisting.swift

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,22 @@
55

66
import Foundation
77

8+
struct ColumnLayoutTableKey: Hashable {
9+
let connectionId: UUID
10+
let databaseName: String
11+
let schemaName: String?
12+
let tableName: String
13+
14+
var storageKey: String {
15+
[connectionId.uuidString, databaseName, schemaName ?? "", tableName]
16+
.map { $0.addingPercentEncoding(withAllowedCharacters: .alphanumerics) ?? $0 }
17+
.joined(separator: ".")
18+
}
19+
}
20+
821
@MainActor
922
protocol ColumnLayoutPersisting: AnyObject {
10-
func load(for tableName: String, connectionId: UUID) -> ColumnLayoutState?
11-
func save(_ layout: ColumnLayoutState, for tableName: String, connectionId: UUID)
12-
func clear(for tableName: String, connectionId: UUID)
23+
func load(for key: ColumnLayoutTableKey) -> ColumnLayoutState?
24+
func save(_ layout: ColumnLayoutState, for key: ColumnLayoutTableKey)
25+
func clear(for key: ColumnLayoutTableKey)
1326
}

TablePro/Core/Storage/ColumnVisibilityPersistence.swift

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,27 +6,30 @@
66
import Foundation
77

88
enum ColumnVisibilityPersistence {
9-
static func key(tableName: String, connectionId: UUID) -> String {
10-
"com.TablePro.columns.hiddenColumns.\(connectionId.uuidString).\(tableName)"
9+
private static let keyPrefix = "com.TablePro.columns.hiddenColumns."
10+
11+
static func key(for tableKey: ColumnLayoutTableKey) -> String {
12+
keyPrefix + tableKey.storageKey
1113
}
1214

1315
static func loadHiddenColumns(
14-
for tableName: String,
15-
connectionId: UUID,
16+
for tableKey: ColumnLayoutTableKey,
1617
defaults: UserDefaults = .standard
1718
) -> Set<String> {
18-
let storageKey = key(tableName: tableName, connectionId: connectionId)
19-
guard let array = defaults.stringArray(forKey: storageKey) else { return [] }
19+
guard let array = defaults.stringArray(forKey: key(for: tableKey)) else { return [] }
2020
return Set(array)
2121
}
2222

2323
static func saveHiddenColumns(
2424
_ hiddenColumns: Set<String>,
25-
for tableName: String,
26-
connectionId: UUID,
25+
for tableKey: ColumnLayoutTableKey,
2726
defaults: UserDefaults = .standard
2827
) {
29-
let storageKey = key(tableName: tableName, connectionId: connectionId)
30-
defaults.set(Array(hiddenColumns), forKey: storageKey)
28+
let storageKey = key(for: tableKey)
29+
if hiddenColumns.isEmpty {
30+
defaults.removeObject(forKey: storageKey)
31+
} else {
32+
defaults.set(Array(hiddenColumns), forKey: storageKey)
33+
}
3134
}
3235
}

TablePro/Core/Utilities/UI/AlertHelper.swift

Lines changed: 0 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -238,28 +238,4 @@ final class AlertHelper {
238238
alert.runModal()
239239
}
240240
}
241-
242-
// MARK: - Query Error with AI Option
243-
244-
static func showQueryErrorWithAIOption(
245-
title: String,
246-
message: String,
247-
window: NSWindow?
248-
) async -> Bool {
249-
let alert = NSAlert()
250-
alert.messageText = title
251-
alert.informativeText = message
252-
alert.alertStyle = .critical
253-
alert.addButton(withTitle: String(localized: "OK"))
254-
alert.addButton(withTitle: String(localized: "Ask AI to Fix"))
255-
256-
if let window = resolveWindow(window) {
257-
return await withCheckedContinuation { continuation in
258-
alert.beginSheetModal(for: window) { response in
259-
continuation.resume(returning: response == .alertSecondButtonReturn)
260-
}
261-
}
262-
}
263-
return alert.runModal() == .alertSecondButtonReturn
264-
}
265241
}

TablePro/Models/Query/QueryTabState.swift

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -321,6 +321,17 @@ struct ColumnLayoutState: Equatable {
321321
var columnWidths: [String: CGFloat] = [:]
322322
var columnOrder: [String]?
323323
var hiddenColumns: Set<String> = []
324+
325+
mutating func applyGeometry(from other: ColumnLayoutState) {
326+
columnWidths = other.columnWidths
327+
columnOrder = other.columnOrder
328+
}
329+
330+
func mergingWidths(_ liveWidths: [String: CGFloat]) -> ColumnLayoutState {
331+
var result = self
332+
result.columnWidths.merge(liveWidths) { _, live in live }
333+
return result
334+
}
324335
}
325336

326337
struct TabExecutionState: Equatable {

TablePro/Models/Query/TableLoadTrigger.swift

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,4 @@ import Foundation
88
internal enum TableLoadTrigger {
99
case userInitiated
1010
case restore
11-
12-
var suppressesFailureModal: Bool {
13-
self == .restore
14-
}
1511
}

TablePro/Models/UI/DataGridConfiguration.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ struct DataGridConfiguration: Equatable {
1414
var connectionId: UUID?
1515
var databaseType: DatabaseType?
1616
var tableName: String?
17+
var databaseName: String?
18+
var schemaName: String?
1719
var primaryKeyColumns: [String] = []
1820
var tabType: TabType?
1921
var showRowNumbers: Bool = true

0 commit comments

Comments
 (0)