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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- Foreign key preview: press Cmd+Enter on a FK cell to see the referenced row in a popover

## [0.27.2] - 2026-04-02

### Added
Expand Down
23 changes: 22 additions & 1 deletion TablePro/Resources/Localizable.xcstrings
Original file line number Diff line number Diff line change
Expand Up @@ -540,6 +540,16 @@
}
}
},
"%@ → %@.%@" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "new",
"value" : "%1$@ → %2$@.%3$@"
}
}
}
},
"%@ cannot be empty" : {
"localizations" : {
"tr" : {
Expand Down Expand Up @@ -13194,6 +13204,9 @@
}
}
}
},
"Failed to load referenced row" : {

},
"Failed to load schemas" : {
"localizations" : {
Expand Down Expand Up @@ -19763,7 +19776,6 @@
}
},
"No database connection" : {
"extractionState" : "stale",
"localizations" : {
"tr" : {
"stringUnit" : {
Expand Down Expand Up @@ -21140,6 +21152,9 @@
}
}
}
},
"NULL — no referenced row" : {

},
"NULL display cannot be empty" : {
"localizations" : {
Expand Down Expand Up @@ -21434,6 +21449,9 @@
}
}
}
},
"Open %@" : {

},
"Open %@ Editor" : {
"localizations" : {
Expand Down Expand Up @@ -24703,6 +24721,9 @@
}
}
}
},
"Referenced row not found" : {

},
"Referenced table" : {
"extractionState" : "stale",
Expand Down
29 changes: 29 additions & 0 deletions TablePro/Views/Results/Extensions/DataGridView+Popovers.swift
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,35 @@ extension TableViewCoordinator {
}
}

func showForeignKeyPreview(tableView: NSTableView, row: Int, column: Int, columnIndex: Int) {
guard columnIndex >= 0, columnIndex < rowProvider.columns.count else { return }
let columnName = rowProvider.columns[columnIndex]
guard let fkInfo = rowProvider.columnForeignKeys[columnName] else { return }
let cellValue = rowProvider.value(atRow: row, column: columnIndex)
guard let databaseType, let connectionId else { return }
guard tableView.view(atColumn: column, row: row, makeIfNecessary: false) != nil else { return }

let cellRect = tableView.rect(ofRow: row).intersection(tableView.rect(ofColumn: column))
PopoverPresenter.show(
relativeTo: cellRect,
of: tableView,
contentSize: NSSize(width: 380, height: 400)
) { [weak self] dismiss in
ForeignKeyPreviewView(
cellValue: cellValue,
fkInfo: fkInfo,
connectionId: connectionId,
databaseType: databaseType,
onNavigate: {
dismiss()
guard let value = cellValue else { return }
self?.onNavigateFK?(value, fkInfo)
},
onDismiss: dismiss
)
}
}

func showJSONEditorPopover(tableView: NSTableView, row: Int, column: Int, columnIndex: Int) {
let currentValue = rowProvider.value(atRow: row, column: columnIndex)

Expand Down
175 changes: 175 additions & 0 deletions TablePro/Views/Results/ForeignKeyPreviewView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
//
// ForeignKeyPreviewView.swift
// TablePro
//
// Read-only popover showing the referenced row for a foreign key cell.
//

import os
import SwiftUI
import TableProPluginKit

struct ForeignKeyPreviewView: View {
let cellValue: String?
let fkInfo: ForeignKeyInfo
let connectionId: UUID
let databaseType: DatabaseType
let onNavigate: () -> Void
let onDismiss: () -> Void

@State private var columns: [String] = []
@State private var values: [String?] = []
@State private var isLoading = true
@State private var errorMessage: String?

private static let logger = Logger(subsystem: "com.TablePro", category: "FKPreview")

var body: some View {
VStack(spacing: 0) {
header
Divider()
content
Divider()
footer
}
.frame(width: 380)
.fixedSize(horizontal: false, vertical: true)
.task { await fetchReferencedRow() }
}

// MARK: - Header

private var header: some View {
HStack {
Text("\(fkInfo.column) → \(fkInfo.referencedTable).\(fkInfo.referencedColumn)")
.font(.system(size: 11, design: .monospaced))
.foregroundStyle(.secondary)
Spacer()
}
.padding(.horizontal, 10)
.padding(.vertical, 8)
}

// MARK: - Content

@ViewBuilder
private var content: some View {
if cellValue == nil {
Text("NULL — no referenced row")
.foregroundStyle(.secondary)
.font(.system(size: 12))
.frame(maxWidth: .infinity, alignment: .center)
.frame(height: 60)
} else if isLoading {
ProgressView()
.frame(maxWidth: .infinity, alignment: .center)
.frame(height: 60)
} else if let errorMessage {
Text(errorMessage)
.foregroundStyle(.red)
.font(.system(size: 12))
.padding(10)
} else if values.isEmpty {
Text("Referenced row not found")
.foregroundStyle(.secondary)
.font(.system(size: 12))
.frame(maxWidth: .infinity, alignment: .center)
.frame(height: 60)
} else {
ScrollView {
VStack(alignment: .leading, spacing: 0) {
ForEach(Array(zip(columns, values).enumerated()), id: \.offset) { _, pair in
let (col, value) = pair
HStack(alignment: .top, spacing: 8) {
Text(col)
.font(.system(size: 11, design: .monospaced))
.foregroundStyle(.secondary)
.frame(width: 120, alignment: .trailing)
.lineLimit(1)

if let val = value {
Text(val)
.font(.system(size: 12, design: .monospaced))
.foregroundStyle(.primary)
.lineLimit(3)
.textSelection(.enabled)
} else {
Text("NULL")
.font(.system(size: 12, design: .monospaced))
.foregroundStyle(.tertiary)
.italic()
}
}
.padding(.horizontal, 10)
.padding(.vertical, 4)
}
}
}
.frame(maxHeight: 300)
}
}

// MARK: - Footer

private var footer: some View {
HStack {
Spacer()
Button {
onNavigate()
} label: {
Label(
String(format: String(localized: "Open %@"), fkInfo.referencedTable),
systemImage: "arrow.right"
)
}
.buttonStyle(.borderedProminent)
.controlSize(.small)
.disabled(cellValue == nil || isLoading || values.isEmpty)
}
.padding(.horizontal, 10)
.padding(.vertical, 8)
}

// MARK: - Data Fetching

private func fetchReferencedRow() async {
guard let value = cellValue else {
isLoading = false
return
}

guard let driver = DatabaseManager.shared.driver(for: connectionId) else {
Self.logger.error("No active driver for FK preview")
errorMessage = String(localized: "No database connection")
isLoading = false
return
}

let quotedTable = driver.quoteIdentifier(fkInfo.referencedTable)
let quotedColumn = driver.quoteIdentifier(fkInfo.referencedColumn)
let escapedValue = driver.escapeStringLiteral(value)

let limitClause: String
switch PluginManager.shared.paginationStyle(for: databaseType) {
case .offsetFetch:
limitClause = "OFFSET 0 ROWS FETCH NEXT 1 ROWS ONLY"
case .limit:
limitClause = "LIMIT 1"
}

let query = "SELECT * FROM \(quotedTable) WHERE \(quotedColumn) = '\(escapedValue)' \(limitClause)"

do {
let result = try await driver.execute(query: query)
if let firstRow = result.rows.first {
columns = result.columns
values = firstRow
}
} catch {
Self.logger.error("FK preview query failed: \(error.localizedDescription)")
errorMessage = String(localized: "Failed to load referenced row")
}

isLoading = false
}
}
8 changes: 8 additions & 0 deletions TablePro/Views/Results/KeyHandlingTableView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,14 @@ final class KeyHandlingTableView: NSTableView {
break
}

// Cmd+Return: preview referenced FK row
if key == .return && modifiers.contains(.command) && selectedRow >= 0 && focusedColumn >= 1 {
coordinator?.showForeignKeyPreview(
tableView: self, row: selectedRow, column: focusedColumn, columnIndex: focusedColumn - 1
)
return
}

// For all other keys, use interpretKeyEvents to map to standard selectors
// This handles Return → insertNewline(_:), Delete → deleteBackward(_:), ESC → cancelOperation(_:)
interpretKeyEvents([event])
Expand Down
1 change: 1 addition & 0 deletions docs/features/keyboard-shortcuts.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ TablePro is keyboard-driven. Most actions have shortcuts, and most menu shortcut
| Action | Shortcut |
|--------|----------|
| Edit cell | `Enter` or `F2` |
| Preview FK reference | `Cmd+Enter` |
| Cancel edit | `Escape` |
| Delete row | `Delete` or `Backspace` |
| Commit changes | `Cmd+S` |
Expand Down
Loading