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 @@ -32,6 +32,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Internal: result-grid sortable header drops the custom resize cursor handling that duplicated AppKit's built-in column-edge resize, and consolidates three sort delegate methods into one that carries the full sort state. No user-facing change; multi-column sort, shift-click cycle, and the column resize cursor still work the same.
- Internal: drop four 1-to-1 PassthroughSubject buses (`saveAsFavoriteRequested`, `focusConnectionFormWindowRequested`, `openSampleDatabaseRequested`, `resetSampleDatabaseRequested`) for direct ownership. The favorite-edit dialog query is now observable state on `MainContentCoordinator`, the connection-form focus lookup is inlined in `WelcomeViewModel`, and the sample-database menu items call `SampleDatabaseLauncher` directly. `AppDelegate` no longer needs `commandCancellables`.
- Internal: tighten dependency injection in the four files that already accept `services: AppServices`. `ConnectionFormCoordinator`, `MainContentCoordinator`, `WelcomeViewModel`, and `ERDiagramViewModel` now read `services.appEvents.*` instead of mixing in raw `AppEvents.shared.*` for events they were already supposed to thread through the container.
- Internal: extend `AppServices` with `appSettingsStorage`, `schemaProviderRegistry`, `aiKeyStorage`, `groupStorage`, `favoritesExpansionState`, and `linkedFolderWatcher`. Convert the raw `.shared` reads of those types in `AIChatViewModel`, `FavoritesSidebarViewModel`, `WelcomeViewModel`, and `MainContentCoordinator` to `services.*`.
- Internal: Redis sidebar key tree uses SwiftUI `OutlineGroup` instead of recursive `DisclosureGroup` + `ForEach` wrapped in `AnyView`. Expansion state is now managed natively per branch identifier; the explicit `expandedPrefixes` set is gone.
- Result-grid cells render via direct `draw(_:)` on a layer-backed `NSView` instead of an `NSTableCellView` wrapping an `NSTextField` plus an `NSButton` accessory. Per cell during scroll there is no Auto Layout solving, no `NSTextField` re-layout, and no `NSButton` tracking-area work. Editing for plain-text columns now opens the overlay editor (the same surface previously used for multi-line cells) rather than an inline text field.

Expand Down
12 changes: 12 additions & 0 deletions TablePro/Core/Services/AppServices.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,27 +9,39 @@ import SwiftUI
struct AppServices {
let appEvents: AppEvents
let appSettings: AppSettingsManager
let appSettingsStorage: AppSettingsStorage
let connectionStorage: ConnectionStorage
let databaseManager: DatabaseManager
let pluginManager: PluginManager
let schemaService: SchemaService
let schemaProviderRegistry: SchemaProviderRegistry
let queryHistoryStorage: QueryHistoryStorage
let sqlFavoriteManager: SQLFavoriteManager
let aiChatStorage: AIChatStorage
let aiKeyStorage: AIKeyStorage
let groupStorage: GroupStorage
let favoritesExpansionState: FavoritesExpansionState
let linkedFolderWatcher: LinkedFolderWatcher
let syncTracker: SyncChangeTracker
let themeEngine: ThemeEngine
let feedbackAPIClient: FeedbackAPIClient

static let live = AppServices(
appEvents: .shared,
appSettings: .shared,
appSettingsStorage: .shared,
connectionStorage: .shared,
databaseManager: .shared,
pluginManager: .shared,
schemaService: .shared,
schemaProviderRegistry: .shared,
queryHistoryStorage: .shared,
sqlFavoriteManager: .shared,
aiChatStorage: .shared,
aiKeyStorage: .shared,
groupStorage: .shared,
favoritesExpansionState: .shared,
linkedFolderWatcher: .shared,
syncTracker: .shared,
themeEngine: .shared,
feedbackAPIClient: .shared
Expand Down
2 changes: 1 addition & 1 deletion TablePro/ViewModels/AIChatViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -235,7 +235,7 @@ final class AIChatViewModel {
let apiKey: String?
switch config.type.authStyle {
case .apiKey:
apiKey = AIKeyStorage.shared.loadAPIKey(for: config.id)
apiKey = services.aiKeyStorage.loadAPIKey(for: config.id)
case .oauth, .none:
apiKey = nil
}
Expand Down
6 changes: 3 additions & 3 deletions TablePro/ViewModels/FavoritesSidebarViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -223,7 +223,7 @@ internal final class FavoritesSidebarViewModel {

func createFavorite(query: String? = nil, folderId: UUID? = nil) {
if let folderId {
FavoritesExpansionState.shared.setFolderExpanded(folderId, expanded: true, for: connectionId)
services.favoritesExpansionState.setFolderExpanded(folderId, expanded: true, for: connectionId)
}
editDialogItem = FavoriteEditItem(favorite: nil, query: query, folderId: folderId)
}
Expand Down Expand Up @@ -262,7 +262,7 @@ internal final class FavoritesSidebarViewModel {

func createFolder(parentId: UUID? = nil) {
if let parentId {
FavoritesExpansionState.shared.setFolderExpanded(parentId, expanded: true, for: connectionId)
services.favoritesExpansionState.setFolderExpanded(parentId, expanded: true, for: connectionId)
}
Task {
let folder = SQLFavoriteFolder(
Expand All @@ -272,7 +272,7 @@ internal final class FavoritesSidebarViewModel {
)
let success = await manager.addFolder(folder)
if success {
FavoritesExpansionState.shared.setFolderExpanded(folder.id, expanded: true, for: connectionId)
services.favoritesExpansionState.setFolderExpanded(folder.id, expanded: true, for: connectionId)
try? await Task.sleep(for: .milliseconds(100))
startRenameFolder(folder)
}
Expand Down
9 changes: 5 additions & 4 deletions TablePro/ViewModels/WelcomeViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ final class WelcomeViewModel {

@ObservationIgnored let services: AppServices
private var storage: ConnectionStorage { services.connectionStorage }
private let groupStorage = GroupStorage.shared
private var groupStorage: GroupStorage { services.groupStorage }

// MARK: - State

Expand All @@ -43,7 +43,7 @@ final class WelcomeViewModel {
var selectedConnectionIds: Set<UUID> = []
var groups: [ConnectionGroup] = []
var linkedConnections: [LinkedConnection] = []
var showOnboarding = !AppSettingsStorage.shared.hasCompletedOnboarding()
var showOnboarding: Bool
var connectionsToDelete: [DatabaseConnection] = []
var showDeleteConfirmation = false
var showDeleteGroupConfirmation = false
Expand Down Expand Up @@ -146,6 +146,7 @@ final class WelcomeViewModel {

init(services: AppServices = .live) {
self.services = services
self.showOnboarding = !services.appSettingsStorage.hasCompletedOnboarding()
}

// MARK: - Setup & Teardown
Expand Down Expand Up @@ -188,11 +189,11 @@ final class WelcomeViewModel {
linkedFoldersCancellable = services.appEvents.linkedFoldersDidUpdate
.receive(on: RunLoop.main)
.sink { [weak self] _ in
self?.linkedConnections = LinkedFolderWatcher.shared.linkedConnections
self?.linkedConnections = services.linkedFolderWatcher.linkedConnections
}

loadConnections()
linkedConnections = LinkedFolderWatcher.shared.linkedConnections
linkedConnections = services.linkedFolderWatcher.linkedConnections

consumePendingRouterActions()
startWelcomeRouterObservation()
Expand Down
18 changes: 9 additions & 9 deletions TablePro/Views/Main/MainContentCoordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -359,8 +359,8 @@ final class MainContentCoordinator {
)
self.persistence = TabPersistenceCoordinator(connectionId: connection.id)

_ = SchemaProviderRegistry.shared.getOrCreate(for: connection.id)
SchemaProviderRegistry.shared.retain(for: connection.id)
_ = services.schemaProviderRegistry.getOrCreate(for: connection.id)
services.schemaProviderRegistry.retain(for: connection.id)
ConnectionDataCache.shared(for: connection.id).ensureLoaded()
changeManager.undoManagerProvider = { [weak self] in self?.contentWindow?.undoManager }
changeManager.onUndoApplied = { [weak self] result in self?.handleUndoResult(result) }
Expand Down Expand Up @@ -512,7 +512,7 @@ final class MainContentCoordinator {
private func reconcilePostSchemaLoad() async {
guard case .loaded(let tables) = services.schemaService.state(for: connectionId) else { return }
if let driver = services.databaseManager.driver(for: connectionId),
let provider = SchemaProviderRegistry.shared.provider(for: connectionId) {
let provider = services.schemaProviderRegistry.provider(for: connectionId) {
let currentDb = services.databaseManager.session(for: connectionId)?.activeDatabase
await provider.resetForDatabase(currentDb, tables: tables, driver: driver)
}
Expand Down Expand Up @@ -585,8 +585,8 @@ final class MainContentCoordinator {
// Release metadata
tableMetadata = nil

SchemaProviderRegistry.shared.release(for: connection.id)
SchemaProviderRegistry.shared.purgeUnused()
services.schemaProviderRegistry.release(for: connection.id)
services.schemaProviderRegistry.purgeUnused()
Self.lifecycleLogger.info(
"[close] MainContentCoordinator.teardown done connId=\(self.connection.id, privacy: .public) elapsedMs=\(Int(Date().timeIntervalSince(start) * 1_000))"
)
Expand All @@ -608,8 +608,8 @@ final class MainContentCoordinator {
}
if !alreadyHandled {
Task { @MainActor in
SchemaProviderRegistry.shared.release(for: connectionId)
SchemaProviderRegistry.shared.purgeUnused()
services.schemaProviderRegistry.release(for: connectionId)
services.schemaProviderRegistry.purgeUnused()
}
}
return
Expand All @@ -622,8 +622,8 @@ final class MainContentCoordinator {

if !alreadyHandled {
Task { @MainActor in
SchemaProviderRegistry.shared.release(for: connectionId)
SchemaProviderRegistry.shared.purgeUnused()
services.schemaProviderRegistry.release(for: connectionId)
services.schemaProviderRegistry.purgeUnused()
}
}
}
Expand Down
Loading