From 81e1667d7de1eb2de3d1c7bdefafcd2d3a6955f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ng=C3=B4=20Qu=E1=BB=91c=20=C4=90=E1=BA=A1t?= Date: Tue, 7 Apr 2026 09:56:19 +0700 Subject: [PATCH 1/4] merge: resolve Localizable.xcstrings conflict (accept remote) --- TablePro/Resources/Localizable.xcstrings | 33 +++++++++++++++++++++--- 1 file changed, 30 insertions(+), 3 deletions(-) diff --git a/TablePro/Resources/Localizable.xcstrings b/TablePro/Resources/Localizable.xcstrings index 82f024f4..1050aeb5 100644 --- a/TablePro/Resources/Localizable.xcstrings +++ b/TablePro/Resources/Localizable.xcstrings @@ -5964,6 +5964,9 @@ } } } + }, + "Change Primary Key" : { + }, "Character Set" : { "localizations" : { @@ -10610,7 +10613,6 @@ } }, "Delete Column" : { - "extractionState" : "stale", "localizations" : { "tr" : { "stringUnit" : { @@ -10699,7 +10701,6 @@ } }, "Delete Foreign Key" : { - "extractionState" : "stale", "localizations" : { "tr" : { "stringUnit" : { @@ -10744,7 +10745,6 @@ } }, "Delete Index" : { - "extractionState" : "stale", "localizations" : { "tr" : { "stringUnit" : { @@ -10815,6 +10815,12 @@ } } } + }, + "Delete Row" : { + + }, + "Delete Rows" : { + }, "Delete Selected" : { "localizations" : { @@ -11689,6 +11695,12 @@ } } } + }, + "Edit Cell" : { + + }, + "Edit Column" : { + }, "Edit Connection" : { "localizations" : { @@ -11734,6 +11746,12 @@ } } } + }, + "Edit Foreign Key" : { + + }, + "Edit Index" : { + }, "Edit Profile..." : { "localizations" : { @@ -17115,6 +17133,12 @@ } } } + }, + "Insert Row" : { + + }, + "Insert Rows" : { + }, "INSERT Statement(s)" : { "localizations" : { @@ -28525,6 +28549,7 @@ } }, "Search for field..." : { + "extractionState" : "stale", "localizations" : { "tr" : { "stringUnit" : { @@ -28636,6 +28661,7 @@ } }, "Search shortcuts..." : { + "extractionState" : "stale", "localizations" : { "tr" : { "stringUnit" : { @@ -28658,6 +28684,7 @@ } }, "Search tables, views, databases..." : { + "extractionState" : "stale", "localizations" : { "tr" : { "stringUnit" : { From a97d98790b0314a73f91bc7be244812fc3df20c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ng=C3=B4=20Qu=E1=BB=91c=20=C4=90=E1=BA=A1t?= Date: Mon, 6 Apr 2026 16:56:13 +0700 Subject: [PATCH 2/4] fix: prevent hang on coordinator dealloc and concurrent WindowOpener waits - Resume saveCompletionContinuation in MainContentCoordinator.deinit to prevent permanent hang if coordinator deallocates during save - Support multiple concurrent waitUntilReady() callers in WindowOpener by using an array of continuations instead of a single optional --- CHANGELOG.md | 5 +++++ .../Core/Services/Infrastructure/WindowOpener.swift | 10 ++++++---- TablePro/Views/Main/MainContentCoordinator.swift | 3 +++ 3 files changed, 14 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f00305f4..e010ad88 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Fixed + +- Fix potential hang when coordinator deallocates during save +- Fix Cmd+W save not persisting data grid changes (sidebar edits intercepted save path) + ## [0.27.5] - 2026-04-06 ### Added diff --git a/TablePro/Core/Services/Infrastructure/WindowOpener.swift b/TablePro/Core/Services/Infrastructure/WindowOpener.swift index 35fc553f..c75ca71f 100644 --- a/TablePro/Core/Services/Infrastructure/WindowOpener.swift +++ b/TablePro/Core/Services/Infrastructure/WindowOpener.swift @@ -15,15 +15,17 @@ internal final class WindowOpener { internal static let shared = WindowOpener() - private var readyContinuation: CheckedContinuation? + private var readyContinuations: [CheckedContinuation] = [] /// Set on appear by ContentView, WelcomeViewModel, or ConnectionFormView. /// Safe to store — OpenWindowAction is app-scoped, not view-scoped. internal var openWindow: OpenWindowAction? { didSet { if openWindow != nil { - readyContinuation?.resume() - readyContinuation = nil + for continuation in readyContinuations { + continuation.resume() + } + readyContinuations.removeAll() } } } @@ -35,7 +37,7 @@ internal final class WindowOpener { if openWindow != nil { continuation.resume() } else { - readyContinuation = continuation + readyContinuations.append(continuation) } } } diff --git a/TablePro/Views/Main/MainContentCoordinator.swift b/TablePro/Views/Main/MainContentCoordinator.swift index 903af2bc..70aa8178 100644 --- a/TablePro/Views/Main/MainContentCoordinator.swift +++ b/TablePro/Views/Main/MainContentCoordinator.swift @@ -410,6 +410,9 @@ final class MainContentCoordinator { } deinit { + saveCompletionContinuation?.resume(returning: false) + saveCompletionContinuation = nil + let connectionId = connection.id let alreadyHandled = _didTeardown.withLock { $0 } || _teardownScheduled.withLock { $0 } From 414fd675a11581859ea3d402d7c73b2f3886ffde Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ng=C3=B4=20Qu=E1=BB=91c=20=C4=90=E1=BA=A1t?= Date: Tue, 7 Apr 2026 09:24:26 +0700 Subject: [PATCH 3/4] fix: prioritize data grid save over sidebar edits in close-with-save flow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit saveAndClose() checked rightPanelState.editState.hasEdits before changeManager.hasChanges, causing it to take the sidebar-only save path (which doesn't execute SQL) when the inspector panel reflected data grid edits. This matched the user's report: Cmd+W → Save behaved identically to Cmd+W → Don't Save. The regular saveChanges() already had the correct priority order (data grid first, sidebar second). This aligns saveAndClose() with that logic. --- .../Main/MainContentCommandActions.swift | 25 +++++++++++-------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/TablePro/Views/Main/MainContentCommandActions.swift b/TablePro/Views/Main/MainContentCommandActions.swift index bc00dd86..b5d1da27 100644 --- a/TablePro/Views/Main/MainContentCommandActions.swift +++ b/TablePro/Views/Main/MainContentCommandActions.swift @@ -358,22 +358,27 @@ final class MainContentCommandActions { return } - // Sidebar edits + // Data grid changes take priority (synced to sidebar editState, + // and the data grid path uses the correct plugin driver for SQL generation) + if coordinator.changeManager.hasChanges { + let saved = await withCheckedContinuation { continuation in + coordinator.saveCompletionContinuation = continuation + saveChanges() + } + if saved { + performClose() + } + return + } + + // Sidebar-only edits (made directly in the inspector panel) if rightPanelState.editState.hasEdits { rightPanelState.onSave?() performClose() return } - // Data grid changes: await the async save via continuation - let saved = await withCheckedContinuation { continuation in - coordinator.saveCompletionContinuation = continuation - saveChanges() - } - - if saved { - performClose() - } + performClose() } private func saveFileToSourceURL() { From 52dccf55d3e4117f87fe6d3d59a2c7c7074cb27f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ng=C3=B4=20Qu=E1=BB=91c=20=C4=90=E1=BA=A1t?= Date: Tue, 7 Apr 2026 09:28:09 +0700 Subject: [PATCH 4/4] fix: handle pending table ops and file dirty in saveAndClose flow --- .../Views/Main/MainContentCommandActions.swift | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/TablePro/Views/Main/MainContentCommandActions.swift b/TablePro/Views/Main/MainContentCommandActions.swift index b5d1da27..82c6561b 100644 --- a/TablePro/Views/Main/MainContentCommandActions.swift +++ b/TablePro/Views/Main/MainContentCommandActions.swift @@ -358,9 +358,11 @@ final class MainContentCommandActions { return } - // Data grid changes take priority (synced to sidebar editState, - // and the data grid path uses the correct plugin driver for SQL generation) - if coordinator.changeManager.hasChanges { + // Data grid changes or pending table operations take priority + let hasDataChanges = coordinator.changeManager.hasChanges + || !pendingTruncates.wrappedValue.isEmpty + || !pendingDeletes.wrappedValue.isEmpty + if hasDataChanges { let saved = await withCheckedContinuation { continuation in coordinator.saveCompletionContinuation = continuation saveChanges() @@ -378,6 +380,13 @@ final class MainContentCommandActions { return } + // File save (query editor with source file) + if coordinator.tabManager.selectedTab?.isFileDirty == true { + saveFileToSourceURL() + performClose() + return + } + performClose() }