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 @@ -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
Expand Down
48 changes: 42 additions & 6 deletions TablePro/Core/Services/Export/ForeignApp/TablePlusImporter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
// TablePro
//

import AppKit
import Foundation
import os
import TableProPluginKit
Expand All @@ -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),
Expand All @@ -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)
}
Expand Down
76 changes: 66 additions & 10 deletions TableProTests/Core/Services/ForeignApp/TablePlusImporterTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down Expand Up @@ -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
Expand Down
Loading