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 @@ -29,6 +29,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Toolbar button tooltips now show each action's real keyboard shortcut and follow your custom bindings, instead of a fixed value. The Switch Connection tooltip showed the wrong shortcut. (#1694)
- The row detail panel no longer stays blank when a table is opened in a second tab. The panel now shows the selected row right away instead of only after the inspector is toggled.
- The sidebar filter now stays put when you open another tab. Before, opening a second table tab cleared the filter text and reset the table list to show everything.
- Fixed a crash when browsing the database tree on servers with many schemas, such as PostgreSQL. The sidebar tree is now a native outline view that loads each database and schema on demand.

## [0.51.1] - 2026-06-16

Expand Down
49 changes: 49 additions & 0 deletions TablePro/Views/Sidebar/DatabaseTreeCellView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
//
// DatabaseTreeCellView.swift
// TablePro
//

import AppKit
import SwiftUI

final class DatabaseTreeCellView: NSTableCellView {
private var hosting: NSHostingView<DatabaseTreeRowView>?
private var node: DatabaseTreeNode?
private var rowContext: DatabaseTreeRowContext?
private var actions: DatabaseTreeRowActions?

override var backgroundStyle: NSView.BackgroundStyle {
didSet { rebuild() }
}

func configure(node: DatabaseTreeNode, context: DatabaseTreeRowContext, actions: DatabaseTreeRowActions) {
self.node = node
self.rowContext = context
self.actions = actions
rebuild()
}

private func rebuild() {
guard let node, let rowContext, let actions else { return }
let rootView = DatabaseTreeRowView(
node: node,
isEmphasized: backgroundStyle == .emphasized,
context: rowContext,
actions: actions
)
if let hosting {
hosting.rootView = rootView
return
}
let view = NSHostingView(rootView: rootView)
view.translatesAutoresizingMaskIntoConstraints = false
addSubview(view)
NSLayoutConstraint.activate([
view.leadingAnchor.constraint(equalTo: leadingAnchor),
view.trailingAnchor.constraint(equalTo: trailingAnchor),
view.topAnchor.constraint(equalTo: topAnchor, constant: 2),
view.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -2)
])
hosting = view
}
}
44 changes: 44 additions & 0 deletions TablePro/Views/Sidebar/DatabaseTreeFilter.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
//
// DatabaseTreeFilter.swift
// TablePro
//

import Foundation
import TableProPluginKit

enum DatabaseTreeFilter {
static func matches(_ query: String, _ candidate: String) -> Bool {
FuzzyMatcher.matches(query: query, candidate: candidate)
}

static func filteredTables(_ tables: [TableInfo], searchText: String) -> [TableInfo] {
let matched = searchText.isEmpty ? tables : tables.filter { matches(searchText, $0.name) }
return deduplicated(matched, by: \.id)
}

static func filteredRoutines(_ routines: [RoutineInfo], searchText: String) -> [RoutineInfo] {
let matched = searchText.isEmpty ? routines : routines.filter { matches(searchText, $0.name) }
return deduplicated(matched, by: \.id)
}

static func visibleSchemas(
_ schemas: [String],
systemSchemas: Set<String>,
searchText: String,
contentMatches: (String) -> Bool
) -> [String] {
let nonSystem = schemas.filter { !systemSchemas.contains($0) }
let matched = searchText.isEmpty
? nonSystem
: nonSystem.filter { matches(searchText, $0) || contentMatches($0) }
return deduplicated(matched, by: { $0 })
}

private static func deduplicated<Element, Key: Hashable>(
_ items: [Element],
by key: (Element) -> Key
) -> [Element] {
var seen = Set<Key>()
return items.filter { seen.insert(key($0)).inserted }
}
}
55 changes: 55 additions & 0 deletions TablePro/Views/Sidebar/DatabaseTreeNode.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
//
// DatabaseTreeNode.swift
// TablePro
//

import Foundation
import TableProPluginKit

final class DatabaseTreeNode {
enum Status: Equatable {
case loading
case empty
case error(String)
}

enum Kind {
case database(DatabaseMetadata)
case schema(database: String, schema: String)
case table(DatabaseTreeTableRef)
case routine(DatabaseTreeRoutineRef)
case status(Status)
}

let id: String
var kind: Kind

init(id: String, kind: Kind) {
self.id = id
self.kind = kind
}

var isExpandable: Bool {
switch kind {
case .database, .schema: return true
case .table, .routine, .status: return false
}
}

var tableRef: DatabaseTreeTableRef? {
if case .table(let ref) = kind { return ref }
return nil
}

static func databaseId(_ database: String) -> String { "db\u{1}\(database)" }
static func schemaId(database: String, schema: String) -> String { "schema\u{1}\(database)\u{1}\(schema)" }
static func tableId(_ ref: DatabaseTreeTableRef) -> String { "table\u{1}\(ref.id)" }
static func routineId(_ ref: DatabaseTreeRoutineRef) -> String { "routine\u{1}\(ref.id)" }
static func statusId(parentId: String, status: Status) -> String {
switch status {
case .loading: return "\(parentId)\u{1}status.loading"
case .empty: return "\(parentId)\u{1}status.empty"
case .error: return "\(parentId)\u{1}status.error"
}
}
}
Loading
Loading