Skip to content

Commit 70d36f9

Browse files
authored
fix: resolve all codebase audit issues (critical through low) (#871)
* fix: add scroll support for connection form and group delete confirmation * fix: resolve plugin bundle thread-safety race in principalClass access * fix: resolve 3 critical code issues from codebase audit * fix: export preference persistence, connection error feedback, accessible fonts * fix: persist window frame and toolbar customization * fix: remove fixed settings window frame, allow content-sized tabs * fix: use standard About panel, consolidate double-click detection * fix: eliminate AnyView erasure, add task cancellation, async plugin loading * fix: split frame modifiers to fix SwiftUI overload resolution * fix: restore fixed settings window frame * fix: export format gating, flexible layouts for welcome and SSH views * fix: improve accessibility labels, interaction feedback, and HIG typography * fix: pre-compute group depths, weak self capture, log cancel errors, shared codecs * fix: improve UX feedback across settings, export, and connection views * fix: replace notification coupling and private API dark mode detection * fix: add Edit > Find menu item and evaluate toolbar HIG compliance * fix: drain pending libpq results before streaming to prevent concurrent query error * fix: constrain database switcher segmented picker width * fix: native file importer, remove iOS gestures, improve accessibility labels * fix: increase onboarding page dot tap targets * fix: let native List handle click and arrow key selection in dialogs * fix: use standalone NSOpenPanel for SSH key file picker * docs: simplify changelog entries
1 parent dd82839 commit 70d36f9

40 files changed

Lines changed: 405 additions & 522 deletions

CHANGELOG.md

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,29 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### Added
11+
12+
- Edit > Find menu item (Cmd+F)
13+
14+
### Fixed
15+
16+
- Export "Don't show again" preference lost when clicking "Open Folder"
17+
- Connection failure error not shown on welcome screen
18+
- Window position not restored between launches
19+
- Export multi-table race condition on PostgreSQL
20+
- List selection requiring two clicks in Quick Switcher and Database Switcher
21+
- AI provider and connection test states not clearing on field changes
22+
- Export stop button missing confirmation dialog
23+
- SSH profile test showing modal alert instead of inline error
24+
- DDL results showing misleading "0 row(s) affected"
25+
- Export dialog missing empty state when no tables found
26+
1027
### Changed
1128

12-
- iOS: TabView navigation with ConnectionCoordinator extraction and coordinator caching
29+
- About window uses standard macOS panel
30+
- Consolidated double-click detection into single shared component
31+
- Accessibility improvements: VoiceOver labels, semantic fonts, larger tap targets
32+
- iOS: TabView navigation with ConnectionCoordinator extraction
1333

1434
## [0.35.0] - 2026-04-25
1535

LocalPackages/CodeEditSourceEditor/Sources/CodeEditSourceEditor/Controller/TextViewController+FindPanelTarget.swift

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,3 +29,10 @@ extension TextViewController: FindPanelTarget {
2929
textView?.emphasisManager
3030
}
3131
}
32+
33+
public extension TextViewController {
34+
func showFindPanel() {
35+
_ = textView.resignFirstResponder()
36+
findViewController?.showFindPanel()
37+
}
38+
}

Plugins/PostgreSQLDriverPlugin/LibPQPluginConnection.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -485,6 +485,8 @@ final class LibPQPluginConnection: @unchecked Sendable {
485485
return
486486
}
487487

488+
while let res = PQgetResult(conn) { PQclear(res) }
489+
488490
let sendOk = queryToRun.withCString { queryPtr in
489491
PQsendQuery(conn, queryPtr)
490492
}

TablePro/Core/Database/DatabaseDriver.swift

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -420,21 +420,6 @@ enum DatabaseDriverFactory {
420420
return try createDriverFromPlugin(for: connection, passwordOverride: passwordOverride)
421421
}
422422

423-
/// Sync variant — falls back to synchronous plugin loading if needed.
424-
/// Only use when an async context is not available.
425-
static func createDriver(
426-
for connection: DatabaseConnection,
427-
passwordOverride: String? = nil
428-
) throws -> DatabaseDriver {
429-
let pluginId = connection.type.pluginTypeId
430-
if PluginManager.shared.driverPlugins[pluginId] == nil,
431-
!PluginManager.shared.hasFinishedInitialLoad {
432-
logger.warning("Plugin '\(pluginId)' not loaded yet — performing synchronous load")
433-
PluginManager.shared.loadPendingPlugins()
434-
}
435-
return try createDriverFromPlugin(for: connection, passwordOverride: passwordOverride)
436-
}
437-
438423
private static func createDriverFromPlugin(
439424
for connection: DatabaseConnection,
440425
passwordOverride: String? = nil

TablePro/Core/Plugins/PluginManager+Registration.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -421,7 +421,7 @@ extension PluginManager {
421421
}) {
422422
if !existingEntry.isEnabled {
423423
setEnabled(true, pluginId: existingEntry.id)
424-
loadPendingPlugins()
424+
await loadPendingPluginsAsync()
425425
}
426426
if driverPlugins[pluginTypeId] != nil {
427427
Self.logger.info("Re-enabled existing plugin '\(existingEntry.name)' for '\(databaseType.rawValue)'")

TablePro/Core/Plugins/PluginManager.swift

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -297,9 +297,21 @@ final class PluginManager {
297297
Self.logger.info("Discovered \(self.pendingPluginURLs.count) plugin(s), will load on first use")
298298
}
299299

300-
/// Load all discovered but not-yet-loaded plugin bundles synchronously on MainActor.
301-
/// Only used by install/uninstall paths that need immediate plugin availability.
302-
/// Normal startup uses `loadPlugins()` which loads bundles off the main thread.
300+
func loadPendingPluginsAsync(clearRestartFlag: Bool = false) async {
301+
if clearRestartFlag {
302+
needsRestartStorage = false
303+
}
304+
guard !pendingPluginURLs.isEmpty else { return }
305+
let pending = pendingPluginURLs
306+
pendingPluginURLs.removeAll()
307+
308+
let loaded = await Self.loadBundlesOffMain(pending)
309+
registerLoadedPlugins(loaded)
310+
hasFinishedInitialLoad = true
311+
validateDependencies()
312+
Self.logger.info("Loaded \(self.plugins.count) plugin(s): \(self.driverPlugins.count) driver(s), \(self.exportPlugins.count) export format(s), \(self.importPlugins.count) import format(s)")
313+
}
314+
303315
func loadPendingPlugins(clearRestartFlag: Bool = false) {
304316
if clearRestartFlag {
305317
needsRestartStorage = false

TablePro/Core/Services/Infrastructure/MainWindowToolbar.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,7 @@ internal final class MainWindowToolbar: NSObject, NSToolbarDelegate {
164164
return hostingItem(id: itemIdentifier, label: "",
165165
content: ToolbarPrincipalContent(
166166
state: coordinator.toolbarState,
167+
onSwitchDatabase: { [weak coordinator] in coordinator?.commandActions?.openDatabaseSwitcher() },
167168
onCancelQuery: { [weak coordinator] in coordinator?.cancelCurrentQuery() }
168169
))
169170
case Self.quickSwitcher:

TablePro/Core/Services/Infrastructure/TabWindowController.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ internal final class TabWindowController: NSWindowController, NSWindowDelegate {
6969
window.identifier = NSUserInterfaceItemIdentifier("main")
7070
window.minSize = NSSize(width: 720, height: 480)
7171
window.isRestorable = false
72+
window.setFrameAutosaveName("MainEditorWindow")
7273
window.toolbarStyle = .unified
7374
// Hide the window title ("Query 1 / TablePro") embedded in the unified
7475
// toolbar — otherwise it claims leading space and pushes our navigation

TablePro/Core/Services/Query/SchemaProviderRegistry.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,9 +49,9 @@ final class SchemaProviderRegistry {
4949
count -= 1
5050
if count <= 0 {
5151
refCounts.removeValue(forKey: connectionId)
52-
removalTasks[connectionId] = Task {
52+
removalTasks[connectionId] = Task { [weak self] in
5353
try? await Task.sleep(nanoseconds: 5_000_000_000)
54-
guard !Task.isCancelled else { return }
54+
guard let self, !Task.isCancelled else { return }
5555
self.providers.removeValue(forKey: connectionId)
5656
self.removalTasks.removeValue(forKey: connectionId)
5757
}

TablePro/Core/Storage/ConnectionStorage.swift

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -386,6 +386,9 @@ final class ConnectionStorage {
386386
// MARK: - Stored Connection (Codable wrapper)
387387

388388
private struct StoredConnection: Codable {
389+
static let sharedEncoder = JSONEncoder()
390+
static let sharedDecoder = JSONDecoder()
391+
389392
let id: UUID
390393
let name: String
391394
let host: String
@@ -524,7 +527,7 @@ private struct StoredConnection: Codable {
524527
self.localOnly = connection.localOnly
525528

526529
// SSH tunnel mode (v2 format preserving jump hosts, profiles, etc.)
527-
self.sshTunnelModeJson = try? JSONEncoder().encode(connection.sshTunnelMode)
530+
self.sshTunnelModeJson = try? Self.sharedEncoder.encode(connection.sshTunnelMode)
528531

529532
// Plugin-driven additional fields
530533
self.additionalFields = connection.additionalFields.isEmpty ? nil : connection.additionalFields
@@ -670,7 +673,7 @@ private struct StoredConnection: Codable {
670673
// Prefer sshTunnelModeJson (v2 format) over legacy flat fields
671674
let resolvedTunnelMode: SSHTunnelMode
672675
if let json = sshTunnelModeJson,
673-
let decoded = try? JSONDecoder().decode(SSHTunnelMode.self, from: json) {
676+
let decoded = try? Self.sharedDecoder.decode(SSHTunnelMode.self, from: json) {
674677
resolvedTunnelMode = decoded
675678
switch decoded {
676679
case .disabled:

0 commit comments

Comments
 (0)