Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

- When saving the connections file fails (disk full, sandbox denied, encoding error), TablePro now aborts the dependent steps instead of continuing as if the save had succeeded. Previously a failed delete could remove the keychain password and queue a CloudKit tombstone for a record that was still on disk; on next sync the connection would be nuked from iCloud as well. Now: delete, add, update, duplicate, batch-delete, sync-pull, group-cleanup, and the plugin secure field migration all check the persistence result and skip the downstream side effects on failure. The connection form surfaces a localized error and keeps the form open instead of dismissing.
- iCloud sync no longer silently substitutes empty defaults when an SSH config, SSL config, jump-host list, or driver-specific field stored in a synced record fails to decode. A device on an older app build that pulled a record written by a newer build would previously end up with a connection missing its SSH/SSL config, then push that empty config back to iCloud and overwrite the authoritative copy. Decode failures now skip the record entirely and log which field failed; the cloud copy stays intact until the device is updated.
- iCloud sync of app settings (general, appearance, editor, data grid, history, tabs, keyboard, AI) no longer silently does nothing when a category's payload fails to decode. Each of the eight category branches previously wrapped the decode in `try?`, so a record written by a newer schema version would fall through with no log, no error, and no UI signal: the user would think their settings synced when they hadn't. Decode failures now skip the category and log which one failed and why.
- Result-grid cells on rows marked for deletion keep their dropdown / date / JSON / blob chevron visible at reduced opacity instead of hiding it, so the cell type is still legible while clearly inactive. Click on the dimmed chevron is a no-op; FK arrow navigation is unchanged. Matches the macOS HIG "disabled appearance" guideline.
- Foreign key navigation from a table with unsaved edits opens the referenced table in a new window tab to preserve the edit buffer. Closing that new tab no longer wipes the original tab's data grid. Previously the new tab's teardown broadcast a connection-scoped event that other coordinators on the same connection received, causing them to release their cell data.
- Tables sidebar refreshes automatically after a successful SQL import; the refresh notification now fires after the success sheet's dismissal animation, so the main window is key when the observer runs (#1114)
Expand Down
55 changes: 19 additions & 36 deletions TablePro/Core/Sync/SyncCoordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -572,7 +572,11 @@ final class SyncCoordinator {
guard let category = SyncRecordMapper.settingsCategory(from: record),
let data = SyncRecordMapper.settingsData(from: record)
else { return }
applySettingsData(data, for: category)
do {
try applySettingsData(data, for: category)
} catch {
Self.logger.error("Skipping remote settings \(record.recordID.recordName, privacy: .public) (\(category, privacy: .public)): \(error.localizedDescription, privacy: .public)")
}
}

// MARK: - Observers
Expand Down Expand Up @@ -721,45 +725,24 @@ final class SyncCoordinator {
}
}

private func applySettingsData(_ data: Data, for category: String) {
private func applySettingsData(_ data: Data, for category: String) throws {
let manager = AppSettingsManager.shared
let decoder = JSONDecoder()

switch category {
case "general":
if let settings = try? decoder.decode(GeneralSettings.self, from: data) {
manager.general = settings
}
case "appearance":
if let settings = try? decoder.decode(AppearanceSettings.self, from: data) {
manager.appearance = settings
}
case "editor":
if let settings = try? decoder.decode(EditorSettings.self, from: data) {
manager.editor = settings
}
case "dataGrid":
if let settings = try? decoder.decode(DataGridSettings.self, from: data) {
manager.dataGrid = settings
}
case "history":
if let settings = try? decoder.decode(HistorySettings.self, from: data) {
manager.history = settings
}
case "tabs":
if let settings = try? decoder.decode(TabSettings.self, from: data) {
manager.tabs = settings
}
case "keyboard":
if let settings = try? decoder.decode(KeyboardSettings.self, from: data) {
manager.keyboard = settings
}
case "ai":
if let settings = try? decoder.decode(AISettings.self, from: data) {
manager.ai = settings
do {
switch category {
case "general": manager.general = try decoder.decode(GeneralSettings.self, from: data)
case "appearance": manager.appearance = try decoder.decode(AppearanceSettings.self, from: data)
case "editor": manager.editor = try decoder.decode(EditorSettings.self, from: data)
case "dataGrid": manager.dataGrid = try decoder.decode(DataGridSettings.self, from: data)
case "history": manager.history = try decoder.decode(HistorySettings.self, from: data)
case "tabs": manager.tabs = try decoder.decode(TabSettings.self, from: data)
case "keyboard": manager.keyboard = try decoder.decode(KeyboardSettings.self, from: data)
case "ai": manager.ai = try decoder.decode(AISettings.self, from: data)
default: return
}
default:
break
} catch {
throw SyncDecodeError.decodeFailure(field: category, underlying: error)
}
}

Expand Down
Loading