From 80737dcafbf00723c960c1a11629ab441c399f3d Mon Sep 17 00:00:00 2001 From: Ngo Quoc Dat Date: Thu, 28 May 2026 18:45:01 +0700 Subject: [PATCH 1/2] fix(toolbar): persist customizations across window opens (#1455) --- CHANGELOG.md | 1 + .../Infrastructure/MainWindowToolbar.swift | 8 ++--- .../MainWindowToolbarValidationTests.swift | 35 +++++++++++++++++++ 3 files changed, 40 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f11f305fd..a2de1ee36 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +- Toolbar customizations now persist after closing and reopening a session window. (#1455) - Pasting copied rows no longer misplaces values when a cell contains a comma (such as a user agent string); each value stays in its own column, and a real NULL is kept distinct from the literal text "NULL". - BigQuery: switching to another table now loads its data right away, instead of leaving the grid empty until you close and reopen the tab. - Custom and OpenAI-compatible AI providers now work when the base URL already ends in `/v1`, instead of building a doubled `/v1/v1/` path that failed. (#1400) diff --git a/TablePro/Core/Services/Infrastructure/MainWindowToolbar.swift b/TablePro/Core/Services/Infrastructure/MainWindowToolbar.swift index 0749c9bc9..1ab57a5f1 100644 --- a/TablePro/Core/Services/Infrastructure/MainWindowToolbar.swift +++ b/TablePro/Core/Services/Infrastructure/MainWindowToolbar.swift @@ -13,6 +13,8 @@ import TableProPluginKit internal final class MainWindowToolbar: NSObject, NSToolbarDelegate { private static let lifecycleLogger = Logger(subsystem: "com.TablePro", category: "NativeTabLifecycle") + internal static let toolbarIdentifier = NSToolbar.Identifier("com.TablePro.main.toolbar") + weak var coordinator: MainContentCoordinator? internal let managedToolbar: NSToolbar @@ -26,14 +28,12 @@ internal final class MainWindowToolbar: NSObject, NSToolbarDelegate { internal init(coordinator: MainContentCoordinator) { self.coordinator = coordinator - // Unique identifier per toolbar instance prevents tab-group merging that would collapse - // all tabs into one toolbar and leave subsequent windows blank. - self.managedToolbar = NSToolbar(identifier: "com.TablePro.main.toolbar.\(UUID().uuidString)") + self.managedToolbar = NSToolbar(identifier: Self.toolbarIdentifier) super.init() self.managedToolbar.delegate = self self.managedToolbar.displayMode = .iconOnly self.managedToolbar.allowsUserCustomization = true - self.managedToolbar.autosavesConfiguration = false + self.managedToolbar.autosavesConfiguration = true } func invalidate() { diff --git a/TableProTests/Services/MainWindowToolbarValidationTests.swift b/TableProTests/Services/MainWindowToolbarValidationTests.swift index 6605be4f9..0f5916f47 100644 --- a/TableProTests/Services/MainWindowToolbarValidationTests.swift +++ b/TableProTests/Services/MainWindowToolbarValidationTests.swift @@ -142,4 +142,39 @@ struct MainWindowToolbarValidationTests { let unknown = NSToolbarItem.Identifier("com.test.unknown") #expect(MainWindowToolbar.isEnabled(itemIdentifier: unknown, context: context) == true) } + + @Test("Toolbar identifier is stable across instances so AppKit autosave can persist customisations") + func toolbarIdentifierIsStable() { + #expect(MainWindowToolbar.toolbarIdentifier.rawValue == "com.TablePro.main.toolbar") + #expect(MainWindowToolbar.toolbarIdentifier.rawValue.contains("-") == false) + } + + @Test("Toolbar is configured for user customisation and autosave") + func toolbarConfigurationEnablesAutosave() { + let owner = makeToolbarOwner() + defer { owner.coordinator?.teardown() } + #expect(owner.managedToolbar.identifier == MainWindowToolbar.toolbarIdentifier) + #expect(owner.managedToolbar.allowsUserCustomization == true) + #expect(owner.managedToolbar.autosavesConfiguration == true) + } + + @Test("Allowed item identifiers are a superset of defaults so restored items survive autosave") + func allowedItemIdentifiersAreSupersetOfDefaults() { + let owner = makeToolbarOwner() + defer { owner.coordinator?.teardown() } + let toolbar = owner.managedToolbar + let defaults = Set(owner.toolbarDefaultItemIdentifiers(toolbar)) + let allowed = Set(owner.toolbarAllowedItemIdentifiers(toolbar)) + #expect(defaults.isSubset(of: allowed)) + } + + private func makeToolbarOwner() -> MainWindowToolbar { + let coordinator = MainContentCoordinator( + connection: TestFixtures.makeConnection(database: "db_a"), + tabManager: QueryTabManager(), + changeManager: DataChangeManager(), + toolbarState: ConnectionToolbarState() + ) + return MainWindowToolbar(coordinator: coordinator) + } } From 0daa623f3e61fa965d767897c878e1d0eed36445 Mon Sep 17 00:00:00 2001 From: Ngo Quoc Dat Date: Thu, 28 May 2026 18:51:51 +0700 Subject: [PATCH 2/2] test(toolbar): hold coordinator strongly so teardown actually runs --- .../MainWindowToolbarValidationTests.swift | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/TableProTests/Services/MainWindowToolbarValidationTests.swift b/TableProTests/Services/MainWindowToolbarValidationTests.swift index 0f5916f47..a8c408e74 100644 --- a/TableProTests/Services/MainWindowToolbarValidationTests.swift +++ b/TableProTests/Services/MainWindowToolbarValidationTests.swift @@ -143,16 +143,16 @@ struct MainWindowToolbarValidationTests { #expect(MainWindowToolbar.isEnabled(itemIdentifier: unknown, context: context) == true) } - @Test("Toolbar identifier is stable across instances so AppKit autosave can persist customisations") + @Test("Toolbar identifier is stable across instances so AppKit autosave can persist customizations") func toolbarIdentifierIsStable() { #expect(MainWindowToolbar.toolbarIdentifier.rawValue == "com.TablePro.main.toolbar") - #expect(MainWindowToolbar.toolbarIdentifier.rawValue.contains("-") == false) } - @Test("Toolbar is configured for user customisation and autosave") + @Test("Toolbar is configured for user customization and autosave") func toolbarConfigurationEnablesAutosave() { - let owner = makeToolbarOwner() - defer { owner.coordinator?.teardown() } + let coordinator = makeCoordinator() + defer { coordinator.teardown() } + let owner = MainWindowToolbar(coordinator: coordinator) #expect(owner.managedToolbar.identifier == MainWindowToolbar.toolbarIdentifier) #expect(owner.managedToolbar.allowsUserCustomization == true) #expect(owner.managedToolbar.autosavesConfiguration == true) @@ -160,21 +160,21 @@ struct MainWindowToolbarValidationTests { @Test("Allowed item identifiers are a superset of defaults so restored items survive autosave") func allowedItemIdentifiersAreSupersetOfDefaults() { - let owner = makeToolbarOwner() - defer { owner.coordinator?.teardown() } + let coordinator = makeCoordinator() + defer { coordinator.teardown() } + let owner = MainWindowToolbar(coordinator: coordinator) let toolbar = owner.managedToolbar let defaults = Set(owner.toolbarDefaultItemIdentifiers(toolbar)) let allowed = Set(owner.toolbarAllowedItemIdentifiers(toolbar)) #expect(defaults.isSubset(of: allowed)) } - private func makeToolbarOwner() -> MainWindowToolbar { - let coordinator = MainContentCoordinator( + private func makeCoordinator() -> MainContentCoordinator { + MainContentCoordinator( connection: TestFixtures.makeConnection(database: "db_a"), tabManager: QueryTabManager(), changeManager: DataChangeManager(), toolbarState: ConnectionToolbarState() ) - return MainWindowToolbar(coordinator: coordinator) } }