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
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- The `postgres` database shows in the database list again. It was marked as a system database, which hid it from the sidebar, Cmd+K, the database filter, and the Backup and Restore Dump pickers. PostgreSQL creates it for users and applications, so nothing about it is internal. CockroachDB's `defaultdb` and Redshift's `dev` were hidden the same way and now show too. (#1967)
- The database a connection is using always shows in the sidebar and the database switchers, even when it is a system database or the database filter excludes it. (#1967)
- The license activation sheet now opens when you click **Activate License**. It was built and then failed to appear, and once that happened further clicks did nothing at all.
- File > Import from Other App..., Open Project Folder..., Import Connections... and Export Connections... now work when no welcome window is open. They used to do nothing.
- File > New Connection... and ⌘N now open the connection chooser once you have a connection open. They did nothing at all, because the welcome window closes as soon as you connect and the command was addressed to it. Import from Other App..., Open Project Folder..., Import Connections..., Import from URL... and Export Connections... had the same fault and are fixed with it. (#1975)
- The tooltip on the welcome screen's **+** button shows the shortcut you actually have bound for New Connection instead of always claiming ⌘N.
- The AI chat panel stays inside the right panel when you drag it narrow. The model name, tool names, and code block headers now truncate, long chat messages and code wrap, wide tables scroll inside their own box, and the composer text follows the panel width instead of running under the editor. (#1956)
- BigQuery `REPEATED` columns (including repeated `STRUCT`) no longer show every element as `null`. (#1963)
Expand Down
9 changes: 0 additions & 9 deletions TablePro/Core/Events/AppCommands.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,16 +18,7 @@ final class AppCommands {
// MARK: - File / Connection Import-Export

let openSQLFiles = PassthroughSubject<[URL], Never>()
let exportConnections = PassthroughSubject<Void, Never>()
let importConnections = PassthroughSubject<Void, Never>()
let importConnectionsFromApp = PassthroughSubject<Void, Never>()
let openProjectFolder = PassthroughSubject<Void, Never>()
let importConnectionFromURL = PassthroughSubject<Void, Never>()
let exportQueryResults = PassthroughSubject<Void, Never>()

// MARK: - Window / Sheet Commands

let presentDatabaseTypeChooser = PassthroughSubject<DatabaseTypeChooserPayload, Never>()

private init() {}
}
4 changes: 3 additions & 1 deletion TablePro/Core/Services/AppServices.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ struct AppServices {
let mcpServerManager: MCPServerManager
let syncTracker: SyncChangeTracker
let themeEngine: ThemeEngine
let welcomeRouter: WelcomeRouter

static let live = AppServices(
appEvents: .shared,
Expand Down Expand Up @@ -62,7 +63,8 @@ struct AppServices {
copilotService: .shared,
mcpServerManager: .shared,
syncTracker: .shared,
themeEngine: .shared
themeEngine: .shared,
welcomeRouter: .shared
)
}

Expand Down
40 changes: 36 additions & 4 deletions TablePro/Core/Services/Infrastructure/WelcomeRouter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,32 @@ internal struct PendingConnectionError {
let error: Error
}

internal final class DatabaseTypeChooserPayload: Identifiable {
internal let id = UUID()
internal let initialType: DatabaseType?
internal let onSelected: (DatabaseType) -> Void

internal init(initialType: DatabaseType?, onSelected: @escaping (DatabaseType) -> Void) {
self.initialType = initialType
self.onSelected = onSelected
}
}

internal enum WelcomeRequest {
case chooseDatabaseType(DatabaseTypeChooserPayload)
case exportConnections
case importConnections
case importFromApp
case importFromURL
case openProjectFolder
}

@MainActor
@Observable
internal final class WelcomeRouter {
internal static let shared = WelcomeRouter()

private(set) var pendingRequest: WelcomeRequest?
private(set) var pendingImport: ExportableConnection?
private(set) var pendingConnectionShare: URL?
private(set) var pendingSQLFiles: [URL] = []
Expand All @@ -27,11 +48,11 @@ internal final class WelcomeRouter {

@ObservationIgnored private var databaseDidConnectCancellable: AnyCancellable?

private init() {
databaseDidConnectCancellable = AppEvents.shared.databaseDidConnect
internal init(appEvents: AppEvents = .shared) {
databaseDidConnectCancellable = appEvents.databaseDidConnect
.receive(on: RunLoop.main)
.sink { _ in
WelcomeRouter.shared.drainPendingSQLFiles()
.sink { [weak self] _ in
self?.drainPendingSQLFiles()
}
}

Expand All @@ -41,6 +62,17 @@ internal final class WelcomeRouter {
AppCommands.shared.openSQLFiles.send(urls)
}

internal func route(_ request: WelcomeRequest) {
pendingRequest = request
showWelcomeWindow()
}

internal func consumePendingRequest() -> WelcomeRequest? {
let value = pendingRequest
pendingRequest = nil
return value
}

internal func routeImport(_ exportable: ExportableConnection) {
pendingImport = exportable
showWelcomeWindow()
Expand Down
15 changes: 5 additions & 10 deletions TablePro/Core/Services/Infrastructure/WindowOpener.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,6 @@ internal final class WindowOpener {
@ObservationIgnored private var openConnectionFormAction: ((UUID?) -> Void)?
@ObservationIgnored private var openIntegrationsActivityAction: (() -> Void)?
@ObservationIgnored private var openSettingsAction: (() -> Void)?
@ObservationIgnored
private var presentTypeChooserAction: ((DatabaseType?, @escaping (DatabaseType) -> Void) -> Void)?
@ObservationIgnored private var pendingCalls: [() -> Void] = []
@ObservationIgnored private var isWired = false

Expand Down Expand Up @@ -53,10 +51,8 @@ internal final class WindowOpener {
run { $0.openConnectionFormAction?(connectionId) }
return
}
run { opener in
opener.presentTypeChooser(initialType: nil) { selected in
opener.openConnectionForm(editing: nil, withType: selected)
}
presentTypeChooser(initialType: nil) { selected in
WindowOpener.shared.openConnectionForm(editing: nil, withType: selected)
}
}

Expand All @@ -74,7 +70,8 @@ internal final class WindowOpener {
initialType: DatabaseType?,
onSelected: @escaping (DatabaseType) -> Void
) {
run { $0.presentTypeChooserAction?(initialType, onSelected) }
let payload = DatabaseTypeChooserPayload(initialType: initialType, onSelected: onSelected)
WelcomeRouter.shared.route(.chooseDatabaseType(payload))
}

internal func openIntegrationsActivity() {
Expand All @@ -85,14 +82,12 @@ internal final class WindowOpener {
openWelcome: @escaping () -> Void,
openConnectionForm: @escaping (UUID?) -> Void,
openIntegrationsActivity: @escaping () -> Void,
openSettings: @escaping () -> Void,
presentTypeChooser: @escaping (DatabaseType?, @escaping (DatabaseType) -> Void) -> Void
openSettings: @escaping () -> Void
) {
openWelcomeAction = openWelcome
openConnectionFormAction = openConnectionForm
openIntegrationsActivityAction = openIntegrationsActivity
openSettingsAction = openSettings
presentTypeChooserAction = presentTypeChooser
isWired = true
let drained = pendingCalls
pendingCalls.removeAll()
Expand Down
15 changes: 5 additions & 10 deletions TablePro/TableProApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -355,28 +355,23 @@ struct AppMenuCommands: Commands {
Divider()

Button(String(localized: "Export Connections...")) {
WindowOpener.shared.openWelcome()
AppCommands.shared.exportConnections.send(())
WelcomeRouter.shared.route(.exportConnections)
}

Button(String(localized: "Import Connections...")) {
WindowOpener.shared.openWelcome()
AppCommands.shared.importConnections.send(())
WelcomeRouter.shared.route(.importConnections)
}

Button(String(localized: "Import from URL...")) {
WindowOpener.shared.openWelcome()
AppCommands.shared.importConnectionFromURL.send(())
WelcomeRouter.shared.route(.importFromURL)
}

Button(String(localized: "Import from Other App...")) {
WindowOpener.shared.openWelcome()
AppCommands.shared.importConnectionsFromApp.send(())
WelcomeRouter.shared.route(.importFromApp)
}

Button(String(localized: "Open Project Folder...")) {
WindowOpener.shared.openWelcome()
AppCommands.shared.openProjectFolder.send(())
WelcomeRouter.shared.route(.openProjectFolder)
}

Divider()
Expand Down
52 changes: 52 additions & 0 deletions TablePro/ViewModels/WelcomeViewModel+ConnectionCreation.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
//
// WelcomeViewModel+ConnectionCreation.swift
// TablePro
//

import Foundation

extension WelcomeViewModel {
func handle(_ request: WelcomeRequest) {
switch request {
case .chooseDatabaseType(let payload):
databaseTypeChooser = payload
case .exportConnections:
guard !connections.isEmpty else { return }
exportConnections(connections)
case .importConnections:
importConnectionsFromFile()
case .importFromApp:
importConnectionsFromApp()
case .importFromURL:
urlImportPresented = true
case .openProjectFolder:
openProjectFolder()
}
}

func selectDatabaseType(_ type: DatabaseType, for payload: DatabaseTypeChooserPayload) {
databaseTypeChooser = nil
guard services.pluginManager.isDriverInstalled(for: type) else {
pendingInstallPayload = payload
pendingInstallType = type
return
}
applySelectedDatabaseType(type, payload: payload)
}

func completePendingInstall(for type: DatabaseType) {
guard let payload = pendingInstallPayload else { return }
pendingInstallPayload = nil
applySelectedDatabaseType(type, payload: payload)
}

func presentURLImport() {
databaseTypeChooser = nil
urlImportPresented = true
}

private func applySelectedDatabaseType(_ type: DatabaseType, payload: DatabaseTypeChooserPayload) {
PendingNewConnectionType.shared.set(type)
payload.onSelected(type)
}
}
9 changes: 0 additions & 9 deletions TablePro/ViewModels/WelcomeViewModel+ProjectFolder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
//

import AppKit
import Combine
import os

extension WelcomeViewModel {
Expand All @@ -13,14 +12,6 @@ extension WelcomeViewModel {
category: "WelcomeProjectFolder"
)

func setUpProjectFolderCommand() {
openProjectFolderCancellable = AppCommands.shared.openProjectFolder
.receive(on: RunLoop.main)
.sink { [weak self] _ in
self?.openProjectFolder()
}
}

func openProjectFolder() {
Task { @MainActor in
await presentProjectFolderPicker()
Expand Down
58 changes: 22 additions & 36 deletions TablePro/ViewModels/WelcomeViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,11 @@ final class WelcomeViewModel {
var activeSheet: WelcomeActiveSheet?
var pluginInstallConnection: DatabaseConnection?

var databaseTypeChooser: DatabaseTypeChooserPayload?
var urlImportPresented = false
var pendingInstallType: DatabaseType?
@ObservationIgnored var pendingInstallPayload: DatabaseTypeChooserPayload?

var renameGroupTarget: ConnectionGroup?
var renameGroupName = ""
var showRenameGroupAlert = false
Expand Down Expand Up @@ -94,10 +99,6 @@ final class WelcomeViewModel {
@ObservationIgnored private var connectionUpdatedCancellable: AnyCancellable?
@ObservationIgnored private var linkedFoldersCancellable: AnyCancellable?
@ObservationIgnored private var teamLibraryCancellable: AnyCancellable?
@ObservationIgnored private var exportConnectionsCancellable: AnyCancellable?
@ObservationIgnored private var importConnectionsCancellable: AnyCancellable?
@ObservationIgnored private var importFromAppCancellable: AnyCancellable?
@ObservationIgnored var openProjectFolderCancellable: AnyCancellable?
@ObservationIgnored private var welcomeRouterTask: Task<Void, Never>?
@ObservationIgnored private var searchDebounceTask: Task<Void, Never>?
private static let searchDebounceNanoseconds: UInt64 = 150_000_000
Expand Down Expand Up @@ -195,27 +196,6 @@ final class WelcomeViewModel {
self?.loadConnections()
}

exportConnectionsCancellable = AppCommands.shared.exportConnections
.receive(on: RunLoop.main)
.sink { [weak self] _ in
guard let self, !self.connections.isEmpty else { return }
self.activeSheet = .exportConnections(self.connections)
}

importConnectionsCancellable = AppCommands.shared.importConnections
.receive(on: RunLoop.main)
.sink { [weak self] _ in
self?.importConnectionsFromFile()
}

importFromAppCancellable = AppCommands.shared.importConnectionsFromApp
.receive(on: RunLoop.main)
.sink { [weak self] _ in
self?.activeSheet = .importFromApp
}

setUpProjectFolderCommand()

linkedFoldersCancellable = services.appEvents.linkedFoldersDidUpdate
.receive(on: RunLoop.main)
.sink { [weak self] _ in
Expand All @@ -238,44 +218,50 @@ final class WelcomeViewModel {
}

private func consumePendingRouterActions() {
if let pendingURL = WelcomeRouter.shared.consumePendingShare() {
let router = services.welcomeRouter
if let request = router.consumePendingRequest() {
handle(request)
return
}
if let pendingURL = router.consumePendingShare() {
activeSheet = .importFile(pendingURL)
return
}
if let pendingImport = WelcomeRouter.shared.consumePendingImport() {
if let pendingImport = router.consumePendingImport() {
activeSheet = .deeplinkImport(pendingImport)
return
}
if let pendingInstall = WelcomeRouter.shared.consumePendingPluginInstall() {
if let pendingInstall = router.consumePendingPluginInstall() {
pluginInstallConnection = pendingInstall
return
}
if let pendingError = WelcomeRouter.shared.consumePendingError() {
if let pendingError = router.consumePendingError() {
presentConnectionFailure(pendingError.error, connection: pendingError.connection)
}
}

private func startWelcomeRouterObservation() {
welcomeRouterTask?.cancel()
let router = services.welcomeRouter
welcomeRouterTask = Task { @MainActor [weak self] in
while !Task.isCancelled {
let didChange = await Self.awaitWelcomeRouterChange()
guard didChange else { return }
self?.consumePendingRouterActions()
guard await Self.awaitWelcomeRouterChange(router: router) else { return }
}
}
}

private static func awaitWelcomeRouterChange() async -> Bool {
private static func awaitWelcomeRouterChange(router: WelcomeRouter) async -> Bool {
let box = ContinuationBox()
return await withTaskCancellationHandler {
await withCheckedContinuation { continuation in
box.set(continuation)
withObservationTracking({
_ = WelcomeRouter.shared.pendingImport
_ = WelcomeRouter.shared.pendingConnectionShare
_ = WelcomeRouter.shared.pendingError
_ = WelcomeRouter.shared.pendingPluginInstall
_ = router.pendingRequest
_ = router.pendingImport
_ = router.pendingConnectionShare
_ = router.pendingError
_ = router.pendingPluginInstall
}, onChange: {
box.resume(with: true)
})
Expand Down
Loading
Loading