diff --git a/CHANGELOG.md b/CHANGELOG.md index e54b256a9..7247f4d13 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +- Import now detects the Setapp edition of TablePlus and reads connections from its data folder. It was reported as not installed before. (#1528) - Favorite keyword suggestions now show in the editor autocomplete when you type the keyword. They were being dropped before reaching the popup. ## [0.47.0] - 2026-06-01 diff --git a/TablePro/Core/Services/Export/ForeignApp/TablePlusImporter.swift b/TablePro/Core/Services/Export/ForeignApp/TablePlusImporter.swift index 6675ada0f..3d803b49f 100644 --- a/TablePro/Core/Services/Export/ForeignApp/TablePlusImporter.swift +++ b/TablePro/Core/Services/Export/ForeignApp/TablePlusImporter.swift @@ -3,6 +3,7 @@ // TablePro // +import AppKit import Foundation import os import TableProPluginKit @@ -18,14 +19,48 @@ struct TablePlusImporter: ForeignAppImporter { static let keychainService = "com.tableplus.TablePlus" + private static let knownBundleIdentifiers = [ + "com.tinyapp.TablePlus", + "com.tinyapp.TablePlus-setapp" + ] + var readKeychain: ForeignKeychainRead = ForeignKeychainReader.readPassword var keyFileExists: (_ path: String) -> Bool = { FileManager.default.fileExists(atPath: $0) } + var resolveAppURL: (_ bundleIdentifier: String) -> URL? = { + NSWorkspace.shared.urlForApplication(withBundleIdentifier: $0) + } + + var dataDirectoryOverride: URL? + + var connectionsFileURL: URL { + dataDirectory.appendingPathComponent("Connections.plist") + } + + var groupsFileURL: URL { + dataDirectory.appendingPathComponent("ConnectionGroups.plist") + } - var connectionsFileURL: URL = FileManager.default.homeDirectoryForCurrentUser - .appendingPathComponent("Library/Application Support/com.tinyapp.TablePlus/Data/Connections.plist") + func installedAppURL() -> URL? { + installedBundleIdentifier.flatMap { resolveAppURL($0) } + } - var groupsFileURL: URL = FileManager.default.homeDirectoryForCurrentUser - .appendingPathComponent("Library/Application Support/com.tinyapp.TablePlus/Data/ConnectionGroups.plist") + private var installedBundleIdentifier: String? { + Self.knownBundleIdentifiers.first { resolveAppURL($0) != nil } + } + + private var dataDirectory: URL { + if let dataDirectoryOverride { + return dataDirectoryOverride + } + return Self.dataDirectory( + forBundleIdentifier: installedBundleIdentifier ?? appBundleIdentifier, + home: FileManager.default.homeDirectoryForCurrentUser + ) + } + + static func dataDirectory(forBundleIdentifier bundleIdentifier: String, home: URL) -> URL { + home.appendingPathComponent("Library/Application Support/\(bundleIdentifier)/Data") + } func connectionCount() -> Int { guard let data = try? Data(contentsOf: connectionsFileURL), @@ -35,13 +70,14 @@ struct TablePlusImporter: ForeignAppImporter { } func importConnections(includePasswords: Bool) throws -> ForeignAppImportResult { - guard FileManager.default.fileExists(atPath: connectionsFileURL.path) else { + let connectionsURL = connectionsFileURL + guard FileManager.default.fileExists(atPath: connectionsURL.path) else { throw ForeignAppImportError.fileNotFound(displayName) } let data: Data do { - data = try Data(contentsOf: connectionsFileURL) + data = try Data(contentsOf: connectionsURL) } catch { throw ForeignAppImportError.parseError(error.localizedDescription) } diff --git a/TableProTests/Core/Services/ForeignApp/TablePlusImporterTests.swift b/TableProTests/Core/Services/ForeignApp/TablePlusImporterTests.swift index 93235ad07..030662aca 100644 --- a/TableProTests/Core/Services/ForeignApp/TablePlusImporterTests.swift +++ b/TableProTests/Core/Services/ForeignApp/TablePlusImporterTests.swift @@ -19,8 +19,7 @@ struct TablePlusImporterTests { try FileManager.default.createDirectory(at: tempDir, withIntermediateDirectories: true) var imp = TablePlusImporter() - imp.connectionsFileURL = tempDir.appendingPathComponent("Connections.plist") - imp.groupsFileURL = tempDir.appendingPathComponent("ConnectionGroups.plist") + imp.dataDirectoryOverride = tempDir importer = imp } @@ -91,17 +90,74 @@ struct TablePlusImporterTests { return entry } - // MARK: - isAvailable + // MARK: - Edition detection - @Test("isAvailable returns true when file exists") - func testIsAvailable_whenFileExists_returnsTrue() throws { - try writeConnections([makeConnection()]) - #expect(importer.isAvailable() == true) + @Test("isAvailable returns true when the standalone app is installed") + func testIsAvailable_whenStandaloneInstalled_returnsTrue() { + var imp = TablePlusImporter() + imp.resolveAppURL = { $0 == "com.tinyapp.TablePlus" ? URL(fileURLWithPath: "/Applications/TablePlus.app") : nil } + #expect(imp.isAvailable() == true) + } + + @Test("isAvailable returns true when only the Setapp edition is installed") + func testIsAvailable_whenSetappInstalled_returnsTrue() { + var imp = TablePlusImporter() + imp.resolveAppURL = { + $0 == "com.tinyapp.TablePlus-setapp" + ? URL(fileURLWithPath: "/Applications/Setapp/TablePlus.app") + : nil + } + #expect(imp.isAvailable() == true) + #expect(imp.installedAppURL() == URL(fileURLWithPath: "/Applications/Setapp/TablePlus.app")) } - @Test("isAvailable returns false when file is missing") - func testIsAvailable_whenFileMissing_returnsFalse() { - #expect(importer.isAvailable() == false) + @Test("isAvailable returns false when no edition is installed") + func testIsAvailable_whenNoEditionInstalled_returnsFalse() { + var imp = TablePlusImporter() + imp.resolveAppURL = { _ in nil } + #expect(imp.isAvailable() == false) + #expect(imp.installedAppURL() == nil) + } + + @Test("installedAppURL prefers the standalone edition when both are installed") + func testInstalledAppURL_prefersStandaloneWhenBothInstalled() { + let standalone = URL(fileURLWithPath: "/Applications/TablePlus.app") + let setapp = URL(fileURLWithPath: "/Applications/Setapp/TablePlus.app") + var imp = TablePlusImporter() + imp.resolveAppURL = { $0 == "com.tinyapp.TablePlus" ? standalone : setapp } + #expect(imp.installedAppURL() == standalone) + } + + @Test("dataDirectory derives from the Setapp bundle identifier") + func testDataDirectory_forSetappEdition() { + let home = URL(fileURLWithPath: "/Users/test") + let dir = TablePlusImporter.dataDirectory(forBundleIdentifier: "com.tinyapp.TablePlus-setapp", home: home) + #expect(dir.path == "/Users/test/Library/Application Support/com.tinyapp.TablePlus-setapp/Data") + } + + @Test("connectionsFileURL follows the installed Setapp edition") + func testConnectionsFileURL_followsSetappEdition() { + var imp = TablePlusImporter() + imp.resolveAppURL = { + $0 == "com.tinyapp.TablePlus-setapp" + ? URL(fileURLWithPath: "/Applications/Setapp/TablePlus.app") + : nil + } + #expect(imp.connectionsFileURL.path.hasSuffix( + "Library/Application Support/com.tinyapp.TablePlus-setapp/Data/Connections.plist" + )) + #expect(imp.groupsFileURL.path.hasSuffix( + "Library/Application Support/com.tinyapp.TablePlus-setapp/Data/ConnectionGroups.plist" + )) + } + + @Test("connectionsFileURL falls back to the standalone edition when none is installed") + func testConnectionsFileURL_fallsBackToStandalone() { + var imp = TablePlusImporter() + imp.resolveAppURL = { _ in nil } + #expect(imp.connectionsFileURL.path.hasSuffix( + "Library/Application Support/com.tinyapp.TablePlus/Data/Connections.plist" + )) } // MARK: - connectionCount