From a4ec4ade3dfd4591c07665065f886b587c84c65b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ng=C3=B4=20Qu=E1=BB=91c=20=C4=90=E1=BA=A1t?= Date: Fri, 3 Apr 2026 09:24:36 +0700 Subject: [PATCH 1/5] feat: add context menu for Structure tab (columns, indexes, foreign keys) --- CHANGELOG.md | 1 + .../MySQLDriverPlugin/MySQLPluginDriver.swift | 14 +++ .../PostgreSQLPluginDriver.swift | 14 +++ .../PluginDatabaseDriver.swift | 9 ++ TablePro/Core/Database/DatabaseDriver.swift | 9 ++ .../Core/Plugins/PluginDriverAdapter.swift | 14 +++ TablePro/Resources/Localizable.xcstrings | 3 + .../Views/Results/DataGridCoordinator.swift | 2 + TablePro/Views/Results/DataGridView.swift | 4 + .../Extensions/DataGridView+Columns.swift | 3 + .../Structure/StructureRowViewWithMenu.swift | 112 +++++++++++++++++ .../Views/Structure/TableStructureView.swift | 115 ++++++++++++++++++ docs/features/table-structure.mdx | 14 +++ 13 files changed, 314 insertions(+) create mode 100644 TablePro/Views/Structure/StructureRowViewWithMenu.swift diff --git a/CHANGELOG.md b/CHANGELOG.md index 03c159841..cca74e938 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- 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 ## [0.27.2] - 2026-04-02 diff --git a/Plugins/MySQLDriverPlugin/MySQLPluginDriver.swift b/Plugins/MySQLDriverPlugin/MySQLPluginDriver.swift index bcffd5f61..9dcba5138 100644 --- a/Plugins/MySQLDriverPlugin/MySQLPluginDriver.swift +++ b/Plugins/MySQLDriverPlugin/MySQLPluginDriver.swift @@ -740,6 +740,20 @@ final class MySQLPluginDriver: PluginDatabaseDriver, @unchecked Sendable { return def } + // MARK: - Definition SQL (clipboard copy) + + func generateColumnDefinitionSQL(column: PluginColumnDefinition) -> String? { + buildColumnDefinitionSQL(column) + } + + func generateIndexDefinitionSQL(index: PluginIndexDefinition) -> String? { + buildIndexDefinitionSQL(index) + } + + func generateForeignKeyDefinitionSQL(fk: PluginForeignKeyDefinition) -> String? { + buildForeignKeyDefinitionSQL(fk) + } + // MARK: - Column Reorder DDL func generateMoveColumnSQL(table: String, column: PluginColumnDefinition, afterColumn: String?) -> String? { diff --git a/Plugins/PostgreSQLDriverPlugin/PostgreSQLPluginDriver.swift b/Plugins/PostgreSQLDriverPlugin/PostgreSQLPluginDriver.swift index 972e984fa..f82b37cf9 100644 --- a/Plugins/PostgreSQLDriverPlugin/PostgreSQLPluginDriver.swift +++ b/Plugins/PostgreSQLDriverPlugin/PostgreSQLPluginDriver.swift @@ -868,6 +868,20 @@ final class PostgreSQLPluginDriver: PluginDatabaseDriver, @unchecked Sendable { return def } + // MARK: - Definition SQL (clipboard copy) + + func generateColumnDefinitionSQL(column: PluginColumnDefinition) -> String? { + pgColumnDefinition(column, inlinePK: false) + } + + func generateIndexDefinitionSQL(index: PluginIndexDefinition) -> String? { + pgIndexDefinition(index, qualifiedTable: "\"table\"") + } + + func generateForeignKeyDefinitionSQL(fk: PluginForeignKeyDefinition) -> String? { + pgForeignKeyDefinition(fk) + } + // MARK: - Helpers private func stripLimitOffset(from query: String) -> String { diff --git a/Plugins/TableProPluginKit/PluginDatabaseDriver.swift b/Plugins/TableProPluginKit/PluginDatabaseDriver.swift index 39aeb3fed..a209d5b97 100644 --- a/Plugins/TableProPluginKit/PluginDatabaseDriver.swift +++ b/Plugins/TableProPluginKit/PluginDatabaseDriver.swift @@ -101,6 +101,11 @@ public protocol PluginDatabaseDriver: AnyObject, Sendable { func generateMoveColumnSQL(table: String, column: PluginColumnDefinition, afterColumn: String?) -> String? func generateCreateTableSQL(definition: PluginCreateTableDefinition) -> String? + // Definition SQL for clipboard copy (optional — return nil if not supported) + func generateColumnDefinitionSQL(column: PluginColumnDefinition) -> String? + func generateIndexDefinitionSQL(index: PluginIndexDefinition) -> String? + func generateForeignKeyDefinitionSQL(fk: PluginForeignKeyDefinition) -> String? + // Table operations (optional — return nil to use app-level fallback) func truncateTableStatements(table: String, schema: String?, cascade: Bool) -> [String]? func dropObjectStatement(name: String, objectType: String, schema: String?, cascade: Bool) -> String? @@ -230,6 +235,10 @@ public extension PluginDatabaseDriver { func generateMoveColumnSQL(table: String, column: PluginColumnDefinition, afterColumn: String?) -> String? { nil } func generateCreateTableSQL(definition: PluginCreateTableDefinition) -> String? { nil } + func generateColumnDefinitionSQL(column: PluginColumnDefinition) -> String? { nil } + func generateIndexDefinitionSQL(index: PluginIndexDefinition) -> String? { nil } + func generateForeignKeyDefinitionSQL(fk: PluginForeignKeyDefinition) -> String? { nil } + func truncateTableStatements(table: String, schema: String?, cascade: Bool) -> [String]? { nil } func dropObjectStatement(name: String, objectType: String, schema: String?, cascade: Bool) -> String? { nil } func foreignKeyDisableStatements() -> [String]? { nil } diff --git a/TablePro/Core/Database/DatabaseDriver.swift b/TablePro/Core/Database/DatabaseDriver.swift index 8e7ff51af..62ff8b613 100644 --- a/TablePro/Core/Database/DatabaseDriver.swift +++ b/TablePro/Core/Database/DatabaseDriver.swift @@ -153,6 +153,11 @@ protocol DatabaseDriver: AnyObject { func foreignKeyDisableStatements() -> [String]? func foreignKeyEnableStatements() -> [String]? + + // Definition SQL for clipboard copy + func generateColumnDefinitionSQL(column: PluginColumnDefinition) -> String? + func generateIndexDefinitionSQL(index: PluginIndexDefinition) -> String? + func generateForeignKeyDefinitionSQL(fk: PluginForeignKeyDefinition) -> String? } // MARK: - Schema Switching @@ -193,6 +198,10 @@ extension DatabaseDriver { func foreignKeyDisableStatements() -> [String]? { nil } func foreignKeyEnableStatements() -> [String]? { nil } + func generateColumnDefinitionSQL(column: PluginColumnDefinition) -> String? { nil } + func generateIndexDefinitionSQL(index: PluginIndexDefinition) -> String? { nil } + func generateForeignKeyDefinitionSQL(fk: PluginForeignKeyDefinition) -> String? { nil } + func testConnection() async throws -> Bool { try await connect() disconnect() diff --git a/TablePro/Core/Plugins/PluginDriverAdapter.swift b/TablePro/Core/Plugins/PluginDriverAdapter.swift index 09df73218..ba3290324 100644 --- a/TablePro/Core/Plugins/PluginDriverAdapter.swift +++ b/TablePro/Core/Plugins/PluginDriverAdapter.swift @@ -349,6 +349,20 @@ final class PluginDriverAdapter: DatabaseDriver, SchemaSwitchable { pluginDriver.generateCreateTableSQL(definition: definition) } + // MARK: - Definition SQL (clipboard copy) + + func generateColumnDefinitionSQL(column: PluginColumnDefinition) -> String? { + pluginDriver.generateColumnDefinitionSQL(column: column) + } + + func generateIndexDefinitionSQL(index: PluginIndexDefinition) -> String? { + pluginDriver.generateIndexDefinitionSQL(index: index) + } + + func generateForeignKeyDefinitionSQL(fk: PluginForeignKeyDefinition) -> String? { + pluginDriver.generateForeignKeyDefinitionSQL(fk: fk) + } + // MARK: - Table Operations func truncateTableStatements(table: String, schema: String?, cascade: Bool) -> [String] { diff --git a/TablePro/Resources/Localizable.xcstrings b/TablePro/Resources/Localizable.xcstrings index e24a43872..4b2f46fbd 100644 --- a/TablePro/Resources/Localizable.xcstrings +++ b/TablePro/Resources/Localizable.xcstrings @@ -7809,6 +7809,9 @@ } } } + }, + "Copy Definition" : { + }, "Copy error message" : { diff --git a/TablePro/Views/Results/DataGridCoordinator.swift b/TablePro/Views/Results/DataGridCoordinator.swift index 920610f77..3137205c7 100644 --- a/TablePro/Views/Results/DataGridCoordinator.swift +++ b/TablePro/Views/Results/DataGridCoordinator.swift @@ -31,6 +31,7 @@ final class TableViewCoordinator: NSObject, NSTableViewDelegate, NSTableViewData var onFilterColumn: ((String) -> Void)? var onHideColumn: ((String) -> Void)? var onMoveRow: ((Int, Int) -> Void)? + var rowViewProvider: ((NSTableView, Int, TableViewCoordinator) -> NSTableRowView)? var onNavigateFK: ((String, ForeignKeyInfo) -> Void)? var getVisualState: ((Int) -> RowVisualState)? var dropdownColumns: Set? @@ -243,6 +244,7 @@ final class TableViewCoordinator: NSObject, NSTableViewDelegate, NSTableViewData onFilterColumn = nil onHideColumn = nil onNavigateFK = nil + rowViewProvider = nil getVisualState = nil } diff --git a/TablePro/Views/Results/DataGridView.swift b/TablePro/Views/Results/DataGridView.swift index 0888fbada..285bdc705 100644 --- a/TablePro/Views/Results/DataGridView.swift +++ b/TablePro/Views/Results/DataGridView.swift @@ -66,6 +66,7 @@ struct DataGridView: NSViewRepresentable { var hiddenColumns: Set = [] var onHideColumn: ((String) -> Void)? var onMoveRow: ((Int, Int) -> Void)? + var rowViewProvider: ((NSTableView, Int, TableViewCoordinator) -> NSTableRowView)? @Binding var selectedRowIndices: Set @Binding var sortState: SortState @@ -177,6 +178,7 @@ struct DataGridView: NSViewRepresentable { scrollView.documentView = tableView context.coordinator.tableView = tableView context.coordinator.onMoveRow = onMoveRow + context.coordinator.rowViewProvider = rowViewProvider context.coordinator.rebuildColumnMetadataCache() if let connectionId { context.coordinator.observeTeardown(connectionId: connectionId) @@ -240,6 +242,7 @@ struct DataGridView: NSViewRepresentable { coordinator.onDeleteRows = onDeleteRows coordinator.getVisualState = getVisualState coordinator.onNavigateFK = onNavigateFK + coordinator.rowViewProvider = rowViewProvider return } let previousIdentity = coordinator.lastIdentity @@ -307,6 +310,7 @@ struct DataGridView: NSViewRepresentable { coordinator.onMoveRow = onMoveRow coordinator.getVisualState = getVisualState coordinator.onNavigateFK = onNavigateFK + coordinator.rowViewProvider = rowViewProvider coordinator.dropdownColumns = dropdownColumns coordinator.typePickerColumns = typePickerColumns coordinator.connectionId = connectionId diff --git a/TablePro/Views/Results/Extensions/DataGridView+Columns.swift b/TablePro/Views/Results/Extensions/DataGridView+Columns.swift index 7a6b6851c..315e81587 100644 --- a/TablePro/Views/Results/Extensions/DataGridView+Columns.swift +++ b/TablePro/Views/Results/Extensions/DataGridView+Columns.swift @@ -65,6 +65,9 @@ extension TableViewCoordinator { } func tableView(_ tableView: NSTableView, rowViewForRow row: Int) -> NSTableRowView? { + if let provider = rowViewProvider { + return provider(tableView, row, self) + } let rowView = (tableView.makeView(withIdentifier: Self.rowViewIdentifier, owner: nil) as? TableRowViewWithMenu) ?? TableRowViewWithMenu() rowView.identifier = Self.rowViewIdentifier diff --git a/TablePro/Views/Structure/StructureRowViewWithMenu.swift b/TablePro/Views/Structure/StructureRowViewWithMenu.swift new file mode 100644 index 000000000..83f49ff50 --- /dev/null +++ b/TablePro/Views/Structure/StructureRowViewWithMenu.swift @@ -0,0 +1,112 @@ +// +// StructureRowViewWithMenu.swift +// TablePro +// +// Custom row view with structure-specific context menu. +// Provides Copy Name, Copy Definition, Duplicate, Delete for structure items. +// + +import AppKit + +/// Row view providing a context menu tailored to the Structure tab +final class StructureRowViewWithMenu: NSTableRowView { + weak var coordinator: TableViewCoordinator? + var rowIndex: Int = 0 + var structureTab: StructureTab = .columns + var isStructureEditable: Bool = true + var isRowDeleted: Bool = false + var referencedTableName: String? + + var onCopyName: ((Set) -> Void)? + var onCopyDefinition: ((Set) -> Void)? + var onNavigateFK: ((Int) -> Void)? + var onDuplicate: ((Set) -> Void)? + var onDelete: ((Set) -> Void)? + var onUndoDelete: ((Int) -> Void)? + + override func menu(for event: NSEvent) -> NSMenu? { + guard structureTab != .ddl, structureTab != .parts else { return nil } + + let menu = NSMenu() + + if isRowDeleted { + let undoItem = NSMenuItem( + title: String(localized: "Undo Delete"), + action: #selector(handleUndoDelete), + keyEquivalent: "" + ) + undoItem.target = self + menu.addItem(undoItem) + return menu + } + + let copyNameItem = NSMenuItem( + title: String(localized: "Copy Name"), + action: #selector(handleCopyName), + keyEquivalent: "c" + ) + copyNameItem.keyEquivalentModifierMask = .command + copyNameItem.target = self + menu.addItem(copyNameItem) + + let copyDefItem = NSMenuItem( + title: String(localized: "Copy Definition"), + action: #selector(handleCopyDefinition), + keyEquivalent: "" + ) + copyDefItem.target = self + menu.addItem(copyDefItem) + + if structureTab == .foreignKeys, + let tableName = referencedTableName, !tableName.isEmpty { + menu.addItem(NSMenuItem.separator()) + let navItem = NSMenuItem( + title: String(format: String(localized: "Open %@"), tableName), + action: #selector(handleNavigateFK), + keyEquivalent: "" + ) + navItem.target = self + menu.addItem(navItem) + } + + if isStructureEditable { + menu.addItem(NSMenuItem.separator()) + + let dupItem = NSMenuItem( + title: String(localized: "Duplicate"), + action: #selector(handleDuplicate), + keyEquivalent: "d" + ) + dupItem.keyEquivalentModifierMask = .command + dupItem.target = self + menu.addItem(dupItem) + + let delItem = NSMenuItem( + title: String(localized: "Delete"), + action: #selector(handleDelete), + keyEquivalent: String( + UnicodeScalar(NSBackspaceCharacter).map { Character($0) } ?? "\u{8}" + ) + ) + delItem.keyEquivalentModifierMask = [] + delItem.target = self + menu.addItem(delItem) + } + + return menu + } + + private func effectiveIndices() -> Set { + if let selected = coordinator?.selectedRowIndices, !selected.isEmpty { + return selected + } + return [rowIndex] + } + + @objc private func handleCopyName() { onCopyName?(effectiveIndices()) } + @objc private func handleCopyDefinition() { onCopyDefinition?(effectiveIndices()) } + @objc private func handleNavigateFK() { onNavigateFK?(rowIndex) } + @objc private func handleDuplicate() { onDuplicate?(effectiveIndices()) } + @objc private func handleDelete() { onDelete?(effectiveIndices()) } + @objc private func handleUndoDelete() { onUndoDelete?(rowIndex) } +} diff --git a/TablePro/Views/Structure/TableStructureView.swift b/TablePro/Views/Structure/TableStructureView.swift index 51916b002..34763bacd 100644 --- a/TablePro/Views/Structure/TableStructureView.swift +++ b/TablePro/Views/Structure/TableStructureView.swift @@ -227,6 +227,7 @@ struct TableStructureView: View { connectionId: connection.id, databaseType: getDatabaseType(), onMoveRow: moveRowHandler, + rowViewProvider: makeStructureRowView, selectedRowIndices: $selectedRows, sortState: $sortState, editingCell: $editingCell, @@ -561,6 +562,120 @@ struct TableStructureView: View { } } + // MARK: - Structure Context Menu + + private func makeStructureRowView( + _ tableView: NSTableView, _ row: Int, _ coordinator: TableViewCoordinator + ) -> NSTableRowView { + let rowView = StructureRowViewWithMenu() + rowView.coordinator = coordinator + rowView.rowIndex = row + rowView.structureTab = selectedTab + rowView.isStructureEditable = connection.type.supportsSchemaEditing + rowView.isRowDeleted = structureChangeManager.getVisualState(for: row, tab: selectedTab).isDeleted + + if selectedTab == .foreignKeys, row < structureChangeManager.workingForeignKeys.count { + rowView.referencedTableName = structureChangeManager.workingForeignKeys[row].referencedTable + } + + rowView.onCopyName = { [self] indices in handleCopyName(indices) } + rowView.onCopyDefinition = { [self] indices in handleCopyDefinition(indices) } + rowView.onNavigateFK = { [self] idx in handleNavigateToFK(idx) } + rowView.onDuplicate = { [self] indices in handleCopyRows(indices); handlePaste() } + rowView.onDelete = { [self] indices in handleDeleteRows(indices) } + rowView.onUndoDelete = { [self] _ in handleUndo() } + return rowView + } + + private func handleCopyName(_ indices: Set) { + let provider = StructureRowProvider( + changeManager: structureChangeManager, tab: selectedTab, databaseType: connection.type + ) + let names = indices.sorted().compactMap { provider.row(at: $0)?.first ?? nil } + guard !names.isEmpty else { return } + NSPasteboard.general.clearContents() + NSPasteboard.general.setString(names.joined(separator: "\n"), forType: .string) + } + + private func handleCopyDefinition(_ indices: Set) { + guard let driver = DatabaseManager.shared.driver(for: connection.id) else { return } + var definitions: [String] = [] + + for row in indices.sorted() { + switch selectedTab { + case .columns: + guard row < structureChangeManager.workingColumns.count else { continue } + let col = structureChangeManager.workingColumns[row] + let pluginCol = toPluginColumnDefinition(col) + if let sql = driver.generateColumnDefinitionSQL(column: pluginCol) { + definitions.append(sql) + } + case .indexes: + guard row < structureChangeManager.workingIndexes.count else { continue } + let idx = structureChangeManager.workingIndexes[row] + let pluginIdx = toPluginIndexDefinition(idx) + if let sql = driver.generateIndexDefinitionSQL(index: pluginIdx) { + definitions.append(sql) + } + case .foreignKeys: + guard row < structureChangeManager.workingForeignKeys.count else { continue } + let fk = structureChangeManager.workingForeignKeys[row] + let pluginFK = toPluginForeignKeyDefinition(fk) + if let sql = driver.generateForeignKeyDefinitionSQL(fk: pluginFK) { + definitions.append(sql) + } + case .ddl, .parts: + break + } + } + + guard !definitions.isEmpty else { return } + NSPasteboard.general.clearContents() + NSPasteboard.general.setString(definitions.joined(separator: "\n"), forType: .string) + } + + private func handleNavigateToFK(_ row: Int) { + guard row < structureChangeManager.workingForeignKeys.count else { return } + let fk = structureChangeManager.workingForeignKeys[row] + coordinator?.openTableTab(fk.referencedTable, showStructure: false, isView: false) + } + + // MARK: - Plugin Type Converters (for context menu) + + private func toPluginColumnDefinition(_ col: EditableColumnDefinition) -> PluginColumnDefinition { + PluginColumnDefinition( + name: col.name, + dataType: col.dataType, + isNullable: col.isNullable, + defaultValue: col.defaultValue, + isPrimaryKey: col.isPrimaryKey, + autoIncrement: col.autoIncrement, + comment: col.comment, + unsigned: col.unsigned, + onUpdate: col.onUpdate + ) + } + + private func toPluginIndexDefinition(_ index: EditableIndexDefinition) -> PluginIndexDefinition { + PluginIndexDefinition( + name: index.name, + columns: index.columns, + isUnique: index.isUnique, + indexType: index.type.rawValue + ) + } + + private func toPluginForeignKeyDefinition(_ fk: EditableForeignKeyDefinition) -> PluginForeignKeyDefinition { + PluginForeignKeyDefinition( + name: fk.name, + columns: fk.columns, + referencedTable: fk.referencedTable, + referencedColumns: fk.referencedColumns, + onDelete: fk.onDelete.rawValue, + onUpdate: fk.onUpdate.rawValue + ) + } + // MARK: - Schema Operations private func generateStructurePreviewSQL() { diff --git a/docs/features/table-structure.mdx b/docs/features/table-structure.mdx index 4d796ad3c..d29493ede 100644 --- a/docs/features/table-structure.mdx +++ b/docs/features/table-structure.mdx @@ -181,6 +181,20 @@ Select the column, click **-** or press Delete, confirm, then apply. /> +### Context Menu + +Right-click any row in the Columns, Indexes, or Foreign Keys tabs: + +| Action | Description | +|--------|-------------| +| **Copy Name** | Copy the item name to clipboard | +| **Copy Definition** | Copy the SQL definition (e.g., column definition, index creation clause) | +| **Open [table]** | (Foreign Keys tab only) Navigate to the referenced table | +| **Duplicate** | Copy and paste the selected items | +| **Delete** | Mark items for deletion (apply to execute) | + +Multi-select rows first to copy or delete multiple items at once. + ### Schema Change Preview Before applying, TablePro shows the generated ALTER TABLE SQL for review. From 37f6d1f1beb76e79ed9a19ee4eee36a5ee8f24d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ng=C3=B4=20Qu=E1=BB=91c=20=C4=90=E1=BA=A1t?= Date: Fri, 3 Apr 2026 09:37:38 +0700 Subject: [PATCH 2/5] fix: address code review issues for structure context menu --- .../MySQLDriverPlugin/MySQLPluginDriver.swift | 2 +- .../PostgreSQLPluginDriver.swift | 5 +- .../PluginDatabaseDriver.swift | 4 +- TablePro/Core/Database/DatabaseDriver.swift | 4 +- .../Core/Plugins/PluginDriverAdapter.swift | 4 +- .../SchemaStatementGenerator.swift | 49 ++-------- TablePro/Models/Schema/ColumnDefinition.swift | 9 ++ .../Models/Schema/ForeignKeyDefinition.swift | 8 ++ TablePro/Models/Schema/IndexDefinition.swift | 5 ++ .../Views/Structure/TableStructureView.swift | 89 ++++++++++--------- 10 files changed, 84 insertions(+), 95 deletions(-) diff --git a/Plugins/MySQLDriverPlugin/MySQLPluginDriver.swift b/Plugins/MySQLDriverPlugin/MySQLPluginDriver.swift index 9dcba5138..e618604d0 100644 --- a/Plugins/MySQLDriverPlugin/MySQLPluginDriver.swift +++ b/Plugins/MySQLDriverPlugin/MySQLPluginDriver.swift @@ -746,7 +746,7 @@ final class MySQLPluginDriver: PluginDatabaseDriver, @unchecked Sendable { buildColumnDefinitionSQL(column) } - func generateIndexDefinitionSQL(index: PluginIndexDefinition) -> String? { + func generateIndexDefinitionSQL(index: PluginIndexDefinition, tableName: String?) -> String? { buildIndexDefinitionSQL(index) } diff --git a/Plugins/PostgreSQLDriverPlugin/PostgreSQLPluginDriver.swift b/Plugins/PostgreSQLDriverPlugin/PostgreSQLPluginDriver.swift index f82b37cf9..6d0a893e1 100644 --- a/Plugins/PostgreSQLDriverPlugin/PostgreSQLPluginDriver.swift +++ b/Plugins/PostgreSQLDriverPlugin/PostgreSQLPluginDriver.swift @@ -874,8 +874,9 @@ final class PostgreSQLPluginDriver: PluginDatabaseDriver, @unchecked Sendable { pgColumnDefinition(column, inlinePK: false) } - func generateIndexDefinitionSQL(index: PluginIndexDefinition) -> String? { - pgIndexDefinition(index, qualifiedTable: "\"table\"") + func generateIndexDefinitionSQL(index: PluginIndexDefinition, tableName: String?) -> String? { + let qualifiedTable = tableName.map { quoteIdentifier($0) } ?? "\"table\"" + return pgIndexDefinition(index, qualifiedTable: qualifiedTable) } func generateForeignKeyDefinitionSQL(fk: PluginForeignKeyDefinition) -> String? { diff --git a/Plugins/TableProPluginKit/PluginDatabaseDriver.swift b/Plugins/TableProPluginKit/PluginDatabaseDriver.swift index a209d5b97..ff907be02 100644 --- a/Plugins/TableProPluginKit/PluginDatabaseDriver.swift +++ b/Plugins/TableProPluginKit/PluginDatabaseDriver.swift @@ -103,7 +103,7 @@ public protocol PluginDatabaseDriver: AnyObject, Sendable { // Definition SQL for clipboard copy (optional — return nil if not supported) func generateColumnDefinitionSQL(column: PluginColumnDefinition) -> String? - func generateIndexDefinitionSQL(index: PluginIndexDefinition) -> String? + func generateIndexDefinitionSQL(index: PluginIndexDefinition, tableName: String?) -> String? func generateForeignKeyDefinitionSQL(fk: PluginForeignKeyDefinition) -> String? // Table operations (optional — return nil to use app-level fallback) @@ -236,7 +236,7 @@ public extension PluginDatabaseDriver { func generateCreateTableSQL(definition: PluginCreateTableDefinition) -> String? { nil } func generateColumnDefinitionSQL(column: PluginColumnDefinition) -> String? { nil } - func generateIndexDefinitionSQL(index: PluginIndexDefinition) -> String? { nil } + func generateIndexDefinitionSQL(index: PluginIndexDefinition, tableName: String?) -> String? { nil } func generateForeignKeyDefinitionSQL(fk: PluginForeignKeyDefinition) -> String? { nil } func truncateTableStatements(table: String, schema: String?, cascade: Bool) -> [String]? { nil } diff --git a/TablePro/Core/Database/DatabaseDriver.swift b/TablePro/Core/Database/DatabaseDriver.swift index 62ff8b613..2c8dbbff1 100644 --- a/TablePro/Core/Database/DatabaseDriver.swift +++ b/TablePro/Core/Database/DatabaseDriver.swift @@ -156,7 +156,7 @@ protocol DatabaseDriver: AnyObject { // Definition SQL for clipboard copy func generateColumnDefinitionSQL(column: PluginColumnDefinition) -> String? - func generateIndexDefinitionSQL(index: PluginIndexDefinition) -> String? + func generateIndexDefinitionSQL(index: PluginIndexDefinition, tableName: String?) -> String? func generateForeignKeyDefinitionSQL(fk: PluginForeignKeyDefinition) -> String? } @@ -199,7 +199,7 @@ extension DatabaseDriver { func foreignKeyEnableStatements() -> [String]? { nil } func generateColumnDefinitionSQL(column: PluginColumnDefinition) -> String? { nil } - func generateIndexDefinitionSQL(index: PluginIndexDefinition) -> String? { nil } + func generateIndexDefinitionSQL(index: PluginIndexDefinition, tableName: String?) -> String? { nil } func generateForeignKeyDefinitionSQL(fk: PluginForeignKeyDefinition) -> String? { nil } func testConnection() async throws -> Bool { diff --git a/TablePro/Core/Plugins/PluginDriverAdapter.swift b/TablePro/Core/Plugins/PluginDriverAdapter.swift index ba3290324..86ac48d2f 100644 --- a/TablePro/Core/Plugins/PluginDriverAdapter.swift +++ b/TablePro/Core/Plugins/PluginDriverAdapter.swift @@ -355,8 +355,8 @@ final class PluginDriverAdapter: DatabaseDriver, SchemaSwitchable { pluginDriver.generateColumnDefinitionSQL(column: column) } - func generateIndexDefinitionSQL(index: PluginIndexDefinition) -> String? { - pluginDriver.generateIndexDefinitionSQL(index: index) + func generateIndexDefinitionSQL(index: PluginIndexDefinition, tableName: String?) -> String? { + pluginDriver.generateIndexDefinitionSQL(index: index, tableName: tableName) } func generateForeignKeyDefinitionSQL(fk: PluginForeignKeyDefinition) -> String? { diff --git a/TablePro/Core/SchemaTracking/SchemaStatementGenerator.swift b/TablePro/Core/SchemaTracking/SchemaStatementGenerator.swift index 3ceae1cc4..991e0ba91 100644 --- a/TablePro/Core/SchemaTracking/SchemaStatementGenerator.swift +++ b/TablePro/Core/SchemaTracking/SchemaStatementGenerator.swift @@ -133,7 +133,7 @@ struct SchemaStatementGenerator { // MARK: - Column Operations private func generateAddColumn(_ column: EditableColumnDefinition) -> SchemaStatement? { - guard let sql = pluginDriver.generateAddColumnSQL(table: tableName, column: toPluginColumnDefinition(column)) else { + guard let sql = pluginDriver.generateAddColumnSQL(table: tableName, column: column.toPlugin()) else { return nil } return SchemaStatement(sql: sql, description: "Add column '\(column.name)'", isDestructive: false) @@ -142,8 +142,8 @@ struct SchemaStatementGenerator { private func generateModifyColumn(old: EditableColumnDefinition, new: EditableColumnDefinition) -> SchemaStatement? { guard let sql = pluginDriver.generateModifyColumnSQL( table: tableName, - oldColumn: toPluginColumnDefinition(old), - newColumn: toPluginColumnDefinition(new) + oldColumn: old.toPlugin(), + newColumn: new.toPlugin() ) else { return nil } @@ -164,7 +164,7 @@ struct SchemaStatementGenerator { // MARK: - Index Operations private func generateAddIndex(_ index: EditableIndexDefinition) -> SchemaStatement? { - guard let sql = pluginDriver.generateAddIndexSQL(table: tableName, index: toPluginIndexDefinition(index)) else { + guard let sql = pluginDriver.generateAddIndexSQL(table: tableName, index: index.toPlugin()) else { return nil } return SchemaStatement(sql: sql, description: "Add index '\(index.name)'", isDestructive: false) @@ -172,7 +172,7 @@ struct SchemaStatementGenerator { private func generateModifyIndex(old: EditableIndexDefinition, new: EditableIndexDefinition) -> SchemaStatement? { guard let dropSql = pluginDriver.generateDropIndexSQL(table: tableName, indexName: old.name), - let addSql = pluginDriver.generateAddIndexSQL(table: tableName, index: toPluginIndexDefinition(new)) else { + let addSql = pluginDriver.generateAddIndexSQL(table: tableName, index: new.toPlugin()) else { return nil } let sql = "\(dropSql);\n\(addSql);" @@ -195,7 +195,7 @@ struct SchemaStatementGenerator { private func generateAddForeignKey(_ fk: EditableForeignKeyDefinition) -> SchemaStatement? { guard let sql = pluginDriver.generateAddForeignKeySQL( table: tableName, - fk: toPluginForeignKeyDefinition(fk) + fk: fk.toPlugin() ) else { return nil } @@ -204,7 +204,7 @@ struct SchemaStatementGenerator { private func generateModifyForeignKey(old: EditableForeignKeyDefinition, new: EditableForeignKeyDefinition) -> SchemaStatement? { guard let dropSql = pluginDriver.generateDropForeignKeySQL(table: tableName, constraintName: old.name), - let addSql = pluginDriver.generateAddForeignKeySQL(table: tableName, fk: toPluginForeignKeyDefinition(new)) else { + let addSql = pluginDriver.generateAddForeignKeySQL(table: tableName, fk: new.toPlugin()) else { return nil } let sql = "\(dropSql);\n\(addSql);" @@ -238,39 +238,4 @@ struct SchemaStatementGenerator { ) } - // MARK: - Plugin Type Converters - - private func toPluginColumnDefinition(_ col: EditableColumnDefinition) -> PluginColumnDefinition { - PluginColumnDefinition( - name: col.name, - dataType: col.dataType, - isNullable: col.isNullable, - defaultValue: col.defaultValue, - isPrimaryKey: col.isPrimaryKey, - autoIncrement: col.autoIncrement, - comment: col.comment, - unsigned: col.unsigned, - onUpdate: col.onUpdate - ) - } - - private func toPluginIndexDefinition(_ index: EditableIndexDefinition) -> PluginIndexDefinition { - PluginIndexDefinition( - name: index.name, - columns: index.columns, - isUnique: index.isUnique, - indexType: index.type.rawValue - ) - } - - private func toPluginForeignKeyDefinition(_ fk: EditableForeignKeyDefinition) -> PluginForeignKeyDefinition { - PluginForeignKeyDefinition( - name: fk.name, - columns: fk.columns, - referencedTable: fk.referencedTable, - referencedColumns: fk.referencedColumns, - onDelete: fk.onDelete.rawValue, - onUpdate: fk.onUpdate.rawValue - ) - } } diff --git a/TablePro/Models/Schema/ColumnDefinition.swift b/TablePro/Models/Schema/ColumnDefinition.swift index 7ee0cbc88..a3a3b66f2 100644 --- a/TablePro/Models/Schema/ColumnDefinition.swift +++ b/TablePro/Models/Schema/ColumnDefinition.swift @@ -6,6 +6,7 @@ // import Foundation +import TableProPluginKit /// Column definition for schema modification (editable structure tab) struct EditableColumnDefinition: Hashable, Codable, Identifiable { @@ -69,6 +70,14 @@ struct EditableColumnDefinition: Hashable, Codable, Identifiable { ) } + func toPlugin() -> PluginColumnDefinition { + PluginColumnDefinition( + name: name, dataType: dataType, isNullable: isNullable, defaultValue: defaultValue, + isPrimaryKey: isPrimaryKey, autoIncrement: autoIncrement, comment: comment, + unsigned: unsigned, onUpdate: onUpdate + ) + } + /// Convert back to ColumnInfo func toColumnInfo() -> ColumnInfo { ColumnInfo( diff --git a/TablePro/Models/Schema/ForeignKeyDefinition.swift b/TablePro/Models/Schema/ForeignKeyDefinition.swift index 5aa9d3a49..844d821b3 100644 --- a/TablePro/Models/Schema/ForeignKeyDefinition.swift +++ b/TablePro/Models/Schema/ForeignKeyDefinition.swift @@ -6,6 +6,7 @@ // import Foundation +import TableProPluginKit /// Foreign key definition for schema modification (editable structure tab) struct EditableForeignKeyDefinition: Hashable, Codable, Identifiable { @@ -59,6 +60,13 @@ struct EditableForeignKeyDefinition: Hashable, Codable, Identifiable { ) } + func toPlugin() -> PluginForeignKeyDefinition { + PluginForeignKeyDefinition( + name: name, columns: columns, referencedTable: referencedTable, + referencedColumns: referencedColumns, onDelete: onDelete.rawValue, onUpdate: onUpdate.rawValue + ) + } + /// Convert back to ForeignKeyInfo (single column only) func toForeignKeyInfo() -> ForeignKeyInfo? { guard let column = columns.first, diff --git a/TablePro/Models/Schema/IndexDefinition.swift b/TablePro/Models/Schema/IndexDefinition.swift index 6d1cf7ab2..a11db551d 100644 --- a/TablePro/Models/Schema/IndexDefinition.swift +++ b/TablePro/Models/Schema/IndexDefinition.swift @@ -6,6 +6,7 @@ // import Foundation +import TableProPluginKit /// Index definition for schema modification (editable structure tab) struct EditableIndexDefinition: Hashable, Codable, Identifiable { @@ -59,6 +60,10 @@ struct EditableIndexDefinition: Hashable, Codable, Identifiable { ) } + func toPlugin() -> PluginIndexDefinition { + PluginIndexDefinition(name: name, columns: columns, isUnique: isUnique, indexType: type.rawValue) + } + /// Convert back to IndexInfo func toIndexInfo() -> IndexInfo { IndexInfo( diff --git a/TablePro/Views/Structure/TableStructureView.swift b/TablePro/Views/Structure/TableStructureView.swift index 34763bacd..4fbe43a18 100644 --- a/TablePro/Views/Structure/TableStructureView.swift +++ b/TablePro/Views/Structure/TableStructureView.swift @@ -564,10 +564,14 @@ struct TableStructureView: View { // MARK: - Structure Context Menu + private static let structureRowViewId = NSUserInterfaceItemIdentifier("StructureRowView") + private func makeStructureRowView( _ tableView: NSTableView, _ row: Int, _ coordinator: TableViewCoordinator ) -> NSTableRowView { - let rowView = StructureRowViewWithMenu() + let rowView = (tableView.makeView(withIdentifier: Self.structureRowViewId, owner: nil) + as? StructureRowViewWithMenu) ?? StructureRowViewWithMenu() + rowView.identifier = Self.structureRowViewId rowView.coordinator = coordinator rowView.rowIndex = row rowView.structureTab = selectedTab @@ -581,7 +585,7 @@ struct TableStructureView: View { rowView.onCopyName = { [self] indices in handleCopyName(indices) } rowView.onCopyDefinition = { [self] indices in handleCopyDefinition(indices) } rowView.onNavigateFK = { [self] idx in handleNavigateToFK(idx) } - rowView.onDuplicate = { [self] indices in handleCopyRows(indices); handlePaste() } + rowView.onDuplicate = { [self] indices in handleDuplicateItems(indices) } rowView.onDelete = { [self] indices in handleDeleteRows(indices) } rowView.onUndoDelete = { [self] _ in handleUndo() } return rowView @@ -606,22 +610,19 @@ struct TableStructureView: View { case .columns: guard row < structureChangeManager.workingColumns.count else { continue } let col = structureChangeManager.workingColumns[row] - let pluginCol = toPluginColumnDefinition(col) - if let sql = driver.generateColumnDefinitionSQL(column: pluginCol) { + if let sql = driver.generateColumnDefinitionSQL(column: col.toPlugin()) { definitions.append(sql) } case .indexes: guard row < structureChangeManager.workingIndexes.count else { continue } let idx = structureChangeManager.workingIndexes[row] - let pluginIdx = toPluginIndexDefinition(idx) - if let sql = driver.generateIndexDefinitionSQL(index: pluginIdx) { + if let sql = driver.generateIndexDefinitionSQL(index: idx.toPlugin(), tableName: tableName) { definitions.append(sql) } case .foreignKeys: guard row < structureChangeManager.workingForeignKeys.count else { continue } let fk = structureChangeManager.workingForeignKeys[row] - let pluginFK = toPluginForeignKeyDefinition(fk) - if let sql = driver.generateForeignKeyDefinitionSQL(fk: pluginFK) { + if let sql = driver.generateForeignKeyDefinitionSQL(fk: fk.toPlugin()) { definitions.append(sql) } case .ddl, .parts: @@ -634,48 +635,48 @@ struct TableStructureView: View { NSPasteboard.general.setString(definitions.joined(separator: "\n"), forType: .string) } + private func handleDuplicateItems(_ indices: Set) { + for row in indices.sorted() { + switch selectedTab { + case .columns: + guard row < structureChangeManager.workingColumns.count else { continue } + var copy = structureChangeManager.workingColumns[row] + copy = EditableColumnDefinition( + id: UUID(), name: copy.name, dataType: copy.dataType, isNullable: copy.isNullable, + defaultValue: copy.defaultValue, autoIncrement: copy.autoIncrement, unsigned: copy.unsigned, + comment: copy.comment, collation: copy.collation, onUpdate: copy.onUpdate, + charset: copy.charset, extra: copy.extra, isPrimaryKey: copy.isPrimaryKey + ) + structureChangeManager.addColumn(copy) + case .indexes: + guard row < structureChangeManager.workingIndexes.count else { continue } + var copy = structureChangeManager.workingIndexes[row] + copy = EditableIndexDefinition( + id: UUID(), name: copy.name, columns: copy.columns, + type: copy.type, isUnique: copy.isUnique, isPrimary: false, comment: copy.comment + ) + structureChangeManager.addIndex(copy) + case .foreignKeys: + guard row < structureChangeManager.workingForeignKeys.count else { continue } + var copy = structureChangeManager.workingForeignKeys[row] + copy = EditableForeignKeyDefinition( + id: UUID(), name: copy.name, columns: copy.columns, + referencedTable: copy.referencedTable, referencedColumns: copy.referencedColumns, + onDelete: copy.onDelete, onUpdate: copy.onUpdate + ) + structureChangeManager.addForeignKey(copy) + case .ddl, .parts: + break + } + } + } + private func handleNavigateToFK(_ row: Int) { guard row < structureChangeManager.workingForeignKeys.count else { return } let fk = structureChangeManager.workingForeignKeys[row] coordinator?.openTableTab(fk.referencedTable, showStructure: false, isView: false) } - // MARK: - Plugin Type Converters (for context menu) - - private func toPluginColumnDefinition(_ col: EditableColumnDefinition) -> PluginColumnDefinition { - PluginColumnDefinition( - name: col.name, - dataType: col.dataType, - isNullable: col.isNullable, - defaultValue: col.defaultValue, - isPrimaryKey: col.isPrimaryKey, - autoIncrement: col.autoIncrement, - comment: col.comment, - unsigned: col.unsigned, - onUpdate: col.onUpdate - ) - } - - private func toPluginIndexDefinition(_ index: EditableIndexDefinition) -> PluginIndexDefinition { - PluginIndexDefinition( - name: index.name, - columns: index.columns, - isUnique: index.isUnique, - indexType: index.type.rawValue - ) - } - - private func toPluginForeignKeyDefinition(_ fk: EditableForeignKeyDefinition) -> PluginForeignKeyDefinition { - PluginForeignKeyDefinition( - name: fk.name, - columns: fk.columns, - referencedTable: fk.referencedTable, - referencedColumns: fk.referencedColumns, - onDelete: fk.onDelete.rawValue, - onUpdate: fk.onUpdate.rawValue - ) - } - // MARK: - Schema Operations private func generateStructurePreviewSQL() { From cee5d8f808378d2629e549c95591142a41aaddae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ng=C3=B4=20Qu=E1=BB=91c=20=C4=90=E1=BA=A1t?= Date: Fri, 3 Apr 2026 09:51:52 +0700 Subject: [PATCH 3/5] fix: allow any NSTableRowView subclass to provide context menu --- TablePro/Views/Results/KeyHandlingTableView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/TablePro/Views/Results/KeyHandlingTableView.swift b/TablePro/Views/Results/KeyHandlingTableView.swift index 31dec487c..86e4db620 100644 --- a/TablePro/Views/Results/KeyHandlingTableView.swift +++ b/TablePro/Views/Results/KeyHandlingTableView.swift @@ -420,7 +420,7 @@ final class KeyHandlingTableView: NSTableView { let clickedRow = row(at: point) if clickedRow >= 0, - let rowView = rowView(atRow: clickedRow, makeIfNecessary: false) as? TableRowViewWithMenu { + let rowView = rowView(atRow: clickedRow, makeIfNecessary: false) { if !selectedRowIndexes.contains(clickedRow) { selectRowIndexes(IndexSet(integer: clickedRow), byExtendingSelection: false) } From 662cea2d8cb1301ce14a28d4b3b17d40b7f32c5e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ng=C3=B4=20Qu=E1=BB=91c=20=C4=90=E1=BA=A1t?= Date: Fri, 3 Apr 2026 09:57:10 +0700 Subject: [PATCH 4/5] feat: add context menu on empty space in Structure tab --- .../Views/Results/DataGridCoordinator.swift | 2 ++ TablePro/Views/Results/DataGridView.swift | 4 +++ .../Views/Results/KeyHandlingTableView.swift | 5 ++++ .../Structure/StructureRowViewWithMenu.swift | 14 +++++++++++ .../Views/Structure/TableStructureView.swift | 25 +++++++++++++++++++ 5 files changed, 50 insertions(+) diff --git a/TablePro/Views/Results/DataGridCoordinator.swift b/TablePro/Views/Results/DataGridCoordinator.swift index 3137205c7..cd647c99f 100644 --- a/TablePro/Views/Results/DataGridCoordinator.swift +++ b/TablePro/Views/Results/DataGridCoordinator.swift @@ -32,6 +32,7 @@ final class TableViewCoordinator: NSObject, NSTableViewDelegate, NSTableViewData var onHideColumn: ((String) -> Void)? var onMoveRow: ((Int, Int) -> Void)? var rowViewProvider: ((NSTableView, Int, TableViewCoordinator) -> NSTableRowView)? + var emptySpaceMenu: (() -> NSMenu?)? var onNavigateFK: ((String, ForeignKeyInfo) -> Void)? var getVisualState: ((Int) -> RowVisualState)? var dropdownColumns: Set? @@ -245,6 +246,7 @@ final class TableViewCoordinator: NSObject, NSTableViewDelegate, NSTableViewData onHideColumn = nil onNavigateFK = nil rowViewProvider = nil + emptySpaceMenu = nil getVisualState = nil } diff --git a/TablePro/Views/Results/DataGridView.swift b/TablePro/Views/Results/DataGridView.swift index 285bdc705..2aee26be8 100644 --- a/TablePro/Views/Results/DataGridView.swift +++ b/TablePro/Views/Results/DataGridView.swift @@ -67,6 +67,7 @@ struct DataGridView: NSViewRepresentable { var onHideColumn: ((String) -> Void)? var onMoveRow: ((Int, Int) -> Void)? var rowViewProvider: ((NSTableView, Int, TableViewCoordinator) -> NSTableRowView)? + var emptySpaceMenu: (() -> NSMenu?)? @Binding var selectedRowIndices: Set @Binding var sortState: SortState @@ -179,6 +180,7 @@ struct DataGridView: NSViewRepresentable { context.coordinator.tableView = tableView context.coordinator.onMoveRow = onMoveRow context.coordinator.rowViewProvider = rowViewProvider + context.coordinator.emptySpaceMenu = emptySpaceMenu context.coordinator.rebuildColumnMetadataCache() if let connectionId { context.coordinator.observeTeardown(connectionId: connectionId) @@ -243,6 +245,7 @@ struct DataGridView: NSViewRepresentable { coordinator.getVisualState = getVisualState coordinator.onNavigateFK = onNavigateFK coordinator.rowViewProvider = rowViewProvider + coordinator.emptySpaceMenu = emptySpaceMenu return } let previousIdentity = coordinator.lastIdentity @@ -311,6 +314,7 @@ struct DataGridView: NSViewRepresentable { coordinator.getVisualState = getVisualState coordinator.onNavigateFK = onNavigateFK coordinator.rowViewProvider = rowViewProvider + coordinator.emptySpaceMenu = emptySpaceMenu coordinator.dropdownColumns = dropdownColumns coordinator.typePickerColumns = typePickerColumns coordinator.connectionId = connectionId diff --git a/TablePro/Views/Results/KeyHandlingTableView.swift b/TablePro/Views/Results/KeyHandlingTableView.swift index 86e4db620..6d71a94d5 100644 --- a/TablePro/Views/Results/KeyHandlingTableView.swift +++ b/TablePro/Views/Results/KeyHandlingTableView.swift @@ -427,6 +427,11 @@ final class KeyHandlingTableView: NSTableView { return rowView.menu(for: event) } + // Empty space: ask coordinator for a fallback menu (e.g., Structure tab "Add" actions) + if let menu = coordinator?.emptySpaceMenu?() { + return menu + } + return super.menu(for: event) } } diff --git a/TablePro/Views/Structure/StructureRowViewWithMenu.swift b/TablePro/Views/Structure/StructureRowViewWithMenu.swift index 83f49ff50..51a91c040 100644 --- a/TablePro/Views/Structure/StructureRowViewWithMenu.swift +++ b/TablePro/Views/Structure/StructureRowViewWithMenu.swift @@ -110,3 +110,17 @@ final class StructureRowViewWithMenu: NSTableRowView { @objc private func handleDelete() { onDelete?(effectiveIndices()) } @objc private func handleUndoDelete() { onUndoDelete?(rowIndex) } } + +/// Menu action target for empty-space context menu. +/// Stored as `representedObject` on the menu item to keep it alive while the menu is shown. +final class StructureMenuTarget: NSObject { + private let action: () -> Void + + init(action: @escaping () -> Void) { + self.action = action + } + + @objc func addNewItem() { + action() + } +} diff --git a/TablePro/Views/Structure/TableStructureView.swift b/TablePro/Views/Structure/TableStructureView.swift index 4fbe43a18..4786c466d 100644 --- a/TablePro/Views/Structure/TableStructureView.swift +++ b/TablePro/Views/Structure/TableStructureView.swift @@ -228,6 +228,7 @@ struct TableStructureView: View { databaseType: getDatabaseType(), onMoveRow: moveRowHandler, rowViewProvider: makeStructureRowView, + emptySpaceMenu: makeEmptySpaceMenu, selectedRowIndices: $selectedRows, sortState: $sortState, editingCell: $editingCell, @@ -564,6 +565,30 @@ struct TableStructureView: View { // MARK: - Structure Context Menu + private func makeEmptySpaceMenu() -> NSMenu? { + guard selectedTab != .ddl, selectedTab != .parts else { return nil } + guard connection.type.supportsSchemaEditing else { return nil } + + let menu = NSMenu() + let label: String + switch selectedTab { + case .columns: label = String(localized: "Add Column") + case .indexes: label = String(localized: "Add Index") + case .foreignKeys: label = String(localized: "Add Foreign Key") + case .ddl, .parts: return nil + } + + let item = NSMenuItem(title: label, action: nil, keyEquivalent: "") + item.target = nil + menu.addItem(item) + menu.items.first?.action = #selector(StructureMenuTarget.addNewItem) + + let target = StructureMenuTarget { [self] in addNewRow() } + item.target = target + item.representedObject = target + return menu + } + private static let structureRowViewId = NSUserInterfaceItemIdentifier("StructureRowView") private func makeStructureRowView( From 46c4f6dd192139087b8fd7adaee1aed1cfef6bb5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ng=C3=B4=20Qu=E1=BB=91c=20=C4=90=E1=BA=A1t?= Date: Fri, 3 Apr 2026 10:01:08 +0700 Subject: [PATCH 5/5] fix: simplify empty space menu and remove last duplicated converters --- .../Views/Structure/CreateTableView.swift | 40 ++----------------- .../Views/Structure/TableStructureView.swift | 7 +--- 2 files changed, 5 insertions(+), 42 deletions(-) diff --git a/TablePro/Views/Structure/CreateTableView.swift b/TablePro/Views/Structure/CreateTableView.swift index d9c7f2f22..4336286f1 100644 --- a/TablePro/Views/Structure/CreateTableView.swift +++ b/TablePro/Views/Structure/CreateTableView.swift @@ -439,13 +439,13 @@ struct CreateTableView: View { let definition = PluginCreateTableDefinition( tableName: tableName.isEmpty ? "untitled" : tableName, - columns: columns.map { toPluginColumnDefinition($0) }, + columns: columns.map { $0.toPlugin() }, indexes: structureChangeManager.workingIndexes .filter { !$0.name.isEmpty && !$0.columns.isEmpty } - .map { toPluginIndexDefinition($0) }, + .map { $0.toPlugin() }, foreignKeys: structureChangeManager.workingForeignKeys .filter { !$0.name.isEmpty && !$0.columns.isEmpty && !$0.referencedTable.isEmpty } - .map { toPluginForeignKeyDefinition($0) }, + .map { $0.toPlugin() }, primaryKeyColumns: pkColumns, engine: showMySQLOptions ? tableOptions.engine : nil, charset: showMySQLOptions ? tableOptions.charset : nil, @@ -457,40 +457,6 @@ struct CreateTableView: View { return pluginDriver?.generateCreateTableSQL(definition: definition) } - private func toPluginColumnDefinition(_ col: EditableColumnDefinition) -> PluginColumnDefinition { - PluginColumnDefinition( - name: col.name, - dataType: col.dataType, - isNullable: col.isNullable, - defaultValue: col.defaultValue, - isPrimaryKey: col.isPrimaryKey, - autoIncrement: col.autoIncrement, - comment: col.comment, - unsigned: col.unsigned, - onUpdate: col.onUpdate - ) - } - - private func toPluginIndexDefinition(_ index: EditableIndexDefinition) -> PluginIndexDefinition { - PluginIndexDefinition( - name: index.name, - columns: index.columns, - isUnique: index.isUnique, - indexType: index.type.rawValue - ) - } - - private func toPluginForeignKeyDefinition(_ fk: EditableForeignKeyDefinition) -> PluginForeignKeyDefinition { - PluginForeignKeyDefinition( - name: fk.name, - columns: fk.columns, - referencedTable: fk.referencedTable, - referencedColumns: fk.referencedColumns, - onDelete: fk.onDelete.rawValue, - onUpdate: fk.onUpdate.rawValue - ) - } - // MARK: - Create Table private func createTable() { diff --git a/TablePro/Views/Structure/TableStructureView.swift b/TablePro/Views/Structure/TableStructureView.swift index 4786c466d..31f9dc1dd 100644 --- a/TablePro/Views/Structure/TableStructureView.swift +++ b/TablePro/Views/Structure/TableStructureView.swift @@ -578,14 +578,11 @@ struct TableStructureView: View { case .ddl, .parts: return nil } - let item = NSMenuItem(title: label, action: nil, keyEquivalent: "") - item.target = nil - menu.addItem(item) - menu.items.first?.action = #selector(StructureMenuTarget.addNewItem) - let target = StructureMenuTarget { [self] in addNewRow() } + let item = NSMenuItem(title: label, action: #selector(StructureMenuTarget.addNewItem), keyEquivalent: "") item.target = target item.representedObject = target + menu.addItem(item) return menu }