diff --git a/CHANGELOG.md b/CHANGELOG.md index cca74e938..e90493bc7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Structure tab context menu with Copy Name, Copy Definition (SQL), Duplicate, and Delete for columns, indexes, and foreign keys - Foreign key preview: press Cmd+Enter on a FK cell to see the referenced row in a popover +- Column header: sort ascending/descending and show all hidden columns in context menu +- Data grid: preview and navigate FK references from right-click context menu +- Data grid: add row from right-click on empty space ## [0.27.2] - 2026-04-02 diff --git a/TablePro/Resources/Localizable.xcstrings b/TablePro/Resources/Localizable.xcstrings index 4b2f46fbd..67e456064 100644 --- a/TablePro/Resources/Localizable.xcstrings +++ b/TablePro/Resources/Localizable.xcstrings @@ -23334,6 +23334,9 @@ }, "Preview Query" : { + }, + "Preview Referenced Row" : { + }, "Preview Schema Changes" : { "localizations" : { @@ -27762,6 +27765,9 @@ } } } + }, + "Show All Columns" : { + }, "Show All Databases" : { "extractionState" : "stale", @@ -28350,6 +28356,12 @@ } } } + }, + "Sort Ascending" : { + + }, + "Sort Descending" : { + }, "Sorting will reload data and discard all unsaved changes." : { "localizations" : { diff --git a/TablePro/Views/Main/Child/MainEditorContentView.swift b/TablePro/Views/Main/Child/MainEditorContentView.swift index 61a024aa6..2bddc25b2 100644 --- a/TablePro/Views/Main/Child/MainEditorContentView.swift +++ b/TablePro/Views/Main/Child/MainEditorContentView.swift @@ -446,6 +446,23 @@ struct MainEditorContentView: View { onHideColumn: { [coordinator] columnName in coordinator.hideColumn(columnName) }, + onShowAllColumns: { [columnVisibilityManager, coordinator] in + columnVisibilityManager.showAll() + coordinator.saveColumnVisibilityToTab() + }, + emptySpaceMenu: (tab.isEditable && !tab.isView && tab.tableName != nil) ? { [onAddRow] in + let menu = NSMenu() + let target = StructureMenuTarget { onAddRow() } + let item = NSMenuItem( + title: String(localized: "Add Row"), + action: #selector(StructureMenuTarget.addNewItem), + keyEquivalent: "" + ) + item.target = target + item.representedObject = target + menu.addItem(item) + return menu + } : nil, selectedRowIndices: $selectedRowIndices, sortState: sortStateBinding(for: tab), editingCell: $editingCell, diff --git a/TablePro/Views/Results/DataGridCoordinator.swift b/TablePro/Views/Results/DataGridCoordinator.swift index cd647c99f..ba09b7ae7 100644 --- a/TablePro/Views/Results/DataGridCoordinator.swift +++ b/TablePro/Views/Results/DataGridCoordinator.swift @@ -30,6 +30,7 @@ final class TableViewCoordinator: NSObject, NSTableViewDelegate, NSTableViewData var onUndoInsert: ((Int) -> Void)? var onFilterColumn: ((String) -> Void)? var onHideColumn: ((String) -> Void)? + var onShowAllColumns: (() -> Void)? var onMoveRow: ((Int, Int) -> Void)? var rowViewProvider: ((NSTableView, Int, TableViewCoordinator) -> NSTableRowView)? var emptySpaceMenu: (() -> NSMenu?)? @@ -244,6 +245,7 @@ final class TableViewCoordinator: NSObject, NSTableViewDelegate, NSTableViewData onUndoInsert = nil onFilterColumn = nil onHideColumn = nil + onShowAllColumns = nil onNavigateFK = nil rowViewProvider = nil emptySpaceMenu = nil diff --git a/TablePro/Views/Results/DataGridView.swift b/TablePro/Views/Results/DataGridView.swift index 2aee26be8..572cda7c6 100644 --- a/TablePro/Views/Results/DataGridView.swift +++ b/TablePro/Views/Results/DataGridView.swift @@ -65,6 +65,7 @@ struct DataGridView: NSViewRepresentable { var showRowNumbers: Bool = true var hiddenColumns: Set = [] var onHideColumn: ((String) -> Void)? + var onShowAllColumns: (() -> Void)? var onMoveRow: ((Int, Int) -> Void)? var rowViewProvider: ((NSTableView, Int, TableViewCoordinator) -> NSTableRowView)? var emptySpaceMenu: (() -> NSMenu?)? @@ -181,6 +182,7 @@ struct DataGridView: NSViewRepresentable { context.coordinator.onMoveRow = onMoveRow context.coordinator.rowViewProvider = rowViewProvider context.coordinator.emptySpaceMenu = emptySpaceMenu + context.coordinator.onShowAllColumns = onShowAllColumns context.coordinator.rebuildColumnMetadataCache() if let connectionId { context.coordinator.observeTeardown(connectionId: connectionId) @@ -239,6 +241,7 @@ struct DataGridView: NSViewRepresentable { coordinator.onUndoInsert = onUndoInsert coordinator.onFilterColumn = onFilterColumn coordinator.onHideColumn = onHideColumn + coordinator.onShowAllColumns = onShowAllColumns coordinator.onMoveRow = onMoveRow coordinator.onRefresh = onRefresh coordinator.onDeleteRows = onDeleteRows @@ -310,6 +313,7 @@ struct DataGridView: NSViewRepresentable { coordinator.onUndoInsert = onUndoInsert coordinator.onFilterColumn = onFilterColumn coordinator.onHideColumn = onHideColumn + coordinator.onShowAllColumns = onShowAllColumns coordinator.onMoveRow = onMoveRow coordinator.getVisualState = getVisualState coordinator.onNavigateFK = onNavigateFK diff --git a/TablePro/Views/Results/Extensions/DataGridView+Sort.swift b/TablePro/Views/Results/Extensions/DataGridView+Sort.swift index ba9b28487..b234232ca 100644 --- a/TablePro/Views/Results/Extensions/DataGridView+Sort.swift +++ b/TablePro/Views/Results/Extensions/DataGridView+Sort.swift @@ -71,6 +71,28 @@ extension TableViewCoordinator { return column.title }() + if let dataColumnIndex = DataGridView.columnIndex(from: column.identifier) { + let sortAscItem = NSMenuItem( + title: String(localized: "Sort Ascending"), + action: #selector(sortAscending(_:)), + keyEquivalent: "" + ) + sortAscItem.representedObject = dataColumnIndex + sortAscItem.target = self + menu.addItem(sortAscItem) + + let sortDescItem = NSMenuItem( + title: String(localized: "Sort Descending"), + action: #selector(sortDescending(_:)), + keyEquivalent: "" + ) + sortDescItem.representedObject = dataColumnIndex + sortDescItem.target = self + menu.addItem(sortDescItem) + + menu.addItem(NSMenuItem.separator()) + } + let copyItem = NSMenuItem(title: String(localized: "Copy Column Name"), action: #selector(copyColumnName(_:)), keyEquivalent: "") copyItem.representedObject = baseName copyItem.target = self @@ -98,6 +120,31 @@ extension TableViewCoordinator { hideItem.representedObject = baseName hideItem.target = self menu.addItem(hideItem) + + if onShowAllColumns != nil, + tableView.tableColumns.contains(where: { $0.isHidden && $0.identifier.rawValue != "__rowNumber__" }) { + let showAllItem = NSMenuItem( + title: String(localized: "Show All Columns"), + action: #selector(showAllColumns), + keyEquivalent: "" + ) + showAllItem.target = self + menu.addItem(showAllItem) + } + } + + @objc func sortAscending(_ sender: NSMenuItem) { + guard let columnIndex = sender.representedObject as? Int else { return } + onSort?(columnIndex, true, false) + } + + @objc func sortDescending(_ sender: NSMenuItem) { + guard let columnIndex = sender.representedObject as? Int else { return } + onSort?(columnIndex, false, false) + } + + @objc func showAllColumns() { + onShowAllColumns?() } @objc func copyColumnName(_ sender: NSMenuItem) { diff --git a/TablePro/Views/Results/TableRowViewWithMenu.swift b/TablePro/Views/Results/TableRowViewWithMenu.swift index 5d17e1c71..4cf99c065 100644 --- a/TablePro/Views/Results/TableRowViewWithMenu.swift +++ b/TablePro/Views/Results/TableRowViewWithMenu.swift @@ -101,6 +101,34 @@ final class TableRowViewWithMenu: NSTableRowView { menu.addItem(pasteItem) } + // FK actions (only for FK columns with non-empty values) + if dataColumnIndex >= 0, dataColumnIndex < coordinator.rowProvider.columns.count { + let columnName = coordinator.rowProvider.columns[dataColumnIndex] + if let fkInfo = coordinator.rowProvider.columnForeignKeys[columnName], + let cellValue = coordinator.rowProvider.value(atRow: rowIndex, column: dataColumnIndex), + !cellValue.isEmpty { + menu.addItem(NSMenuItem.separator()) + + let previewItem = NSMenuItem( + title: String(localized: "Preview Referenced Row"), + action: #selector(previewForeignKey(_:)), + keyEquivalent: "" + ) + previewItem.representedObject = dataColumnIndex + previewItem.target = self + menu.addItem(previewItem) + + let navItem = NSMenuItem( + title: String(format: String(localized: "Open %@"), fkInfo.referencedTable), + action: #selector(navigateToForeignKey(_:)), + keyEquivalent: "" + ) + navItem.representedObject = dataColumnIndex + navItem.target = self + menu.addItem(navItem) + } + } + if coordinator.isEditable { menu.addItem(NSMenuItem.separator()) } @@ -270,4 +298,21 @@ final class TableRowViewWithMenu: NSTableRowView { : [rowIndex] coordinator.copyRowsAsJson(at: indices) } + + @objc private func previewForeignKey(_ sender: NSMenuItem) { + guard let columnIndex = sender.representedObject as? Int, + let coordinator, let tableView = coordinator.tableView else { return } + coordinator.showForeignKeyPreview( + tableView: tableView, row: rowIndex, column: columnIndex + 1, columnIndex: columnIndex + ) + } + + @objc private func navigateToForeignKey(_ sender: NSMenuItem) { + guard let columnIndex = sender.representedObject as? Int, + let coordinator else { return } + let columnName = coordinator.rowProvider.columns[columnIndex] + guard let fkInfo = coordinator.rowProvider.columnForeignKeys[columnName], + let value = coordinator.rowProvider.value(atRow: rowIndex, column: columnIndex) else { return } + coordinator.onNavigateFK?(value, fkInfo) + } }