From cd6f99fbc57d276bcfff7d1a6b3037c57330a123 Mon Sep 17 00:00:00 2001 From: Melody Ruch <396921921@qq.com> Date: Sat, 15 Aug 2026 14:41:29 +0800 Subject: [PATCH] fix(grid): enable copy as SQL for selected cells --- .../__tests__/useDataGridExport.spec.ts | 96 +++++++++++++++++-- .../src/composables/useDataGridExtractor.ts | 17 +++- 2 files changed, 101 insertions(+), 12 deletions(-) diff --git a/apps/desktop/src/composables/__tests__/useDataGridExport.spec.ts b/apps/desktop/src/composables/__tests__/useDataGridExport.spec.ts index 0f6741e341..6b2a5accb7 100644 --- a/apps/desktop/src/composables/__tests__/useDataGridExport.spec.ts +++ b/apps/desktop/src/composables/__tests__/useDataGridExport.spec.ts @@ -197,7 +197,7 @@ describe("useDataGridExport prepared row statements", () => { expect(state.canCopyRow.value).toBe(true); }); - it("builds SQL UPDATE from only selected writable columns while retaining a hidden primary key", async () => { + it("builds SQL UPDATE using the full row when right-clicking a selected cell", async () => { const item = row([7, "Ada", true]); const matrix: CellSelectionMatrix = { rowIndexes: [0], @@ -242,11 +242,11 @@ describe("useDataGridExport prepared row statements", () => { hasRowSelection: computed(() => false), }; vi.mocked(extractDataGridSelection).mockResolvedValueOnce({ - text: "UPDATE users SET active = TRUE WHERE id = 7;", + text: "UPDATE users SET `name` = 'Ada', `active` = TRUE WHERE `id` = 7;", mimeType: "application/sql", fileExtension: "sql", rowCount: 1, - columnCount: 1, + columnCount: 2, }); const state = useDataGridExport(options); @@ -256,12 +256,12 @@ describe("useDataGridExport prepared row statements", () => { expect(extractDataGridSelection).toHaveBeenCalledWith( expect.objectContaining({ extractor: "sql-updates", - selectedColumnIndexes: [0], - rows: [[true, 7]], - selectionKind: "cells", + selectedColumnIndexes: [0, 1], + rows: [["Ada", true, 7]], + selectionKind: "rows", }), ); - expect(copyToClipboard).toHaveBeenCalledWith("UPDATE users SET active = TRUE WHERE id = 7;"); + expect(copyToClipboard).toHaveBeenCalledWith("UPDATE users SET `name` = 'Ada', `active` = TRUE WHERE `id` = 7;"); }); it("builds a SELECT request for exactly one explicitly selected cell", async () => { @@ -364,6 +364,88 @@ describe("useDataGridExport prepared row statements", () => { expect(createExportState({ ...editableTable, tableName: "" }, ["id", "name"], { ...matrix, columnIndexes: [1], columns: ["name"], rows: [["Ada"]] }, [7, "Ada"]).canCopyWithExtractor("sql-select")).toBe(false); }); + // Regression tests for https://github.com/t8y2/dbx/issues/6272 + it("enables SELECT copy when sourceColumns is undefined by falling back to display names", () => { + const matrix: CellSelectionMatrix = { + rowIndexes: [0], + columnIndexes: [0], + columns: ["id"], + rows: [[7]], + }; + const item = row([7, "Ada"]); + const state = useDataGridExport({ + columns: computed(() => ["id", "name"]), + displayItems: computed(() => [item]), + sql: computed(() => undefined), + tableMeta: computed(() => editableTable), + databaseType: computed(() => "mysql"), + connectionId: computed(() => "connection-1"), + database: computed(() => "dbx"), + context: computed(() => "table-data"), + sourceColumns: computed(() => ["id", "name"]), + allColumns: computed(() => ["id", "name"]), + allDisplayItems: computed(() => [item]), + allSourceColumns: computed(() => undefined), + visibleColumnIndexes: computed(() => [0, 1]), + columnTypes: computed(() => ["int", "varchar"]), + whereInput: computed(() => undefined), + orderBy: computed(() => undefined), + exportBatchSize: computed(() => 1000), + hasCellSelection: computed(() => true), + selectedCells: computed(() => matrix), + selectedCellMatrix: computed(() => matrix), + selectedRange: computed(() => ({ startRow: 0, endRow: 0, startCol: 0, endCol: 0 })), + contextCell: ref(null), + contextSelectionIsSynthetic: ref(false), + getRowItem: (rowId) => (rowId === item.id ? item : undefined), + selectedRowIds: ref(new Set()), + hasRowSelection: computed(() => false), + }); + + expect(state.canCopyWithExtractor("sql-select")).toBe(true); + }); + + it("enables INSERT/UPDATE when right-clicking a previously selected primary-key cell", () => { + const item = row([7, "Ada"]); + const matrix: CellSelectionMatrix = { + rowIndexes: [0], + columnIndexes: [0], + columns: ["id"], + rows: [[7]], + }; + const state = useDataGridExport({ + columns: computed(() => ["id", "name"]), + displayItems: computed(() => [item]), + sql: computed(() => undefined), + tableMeta: computed(() => editableTable), + databaseType: computed(() => "mysql"), + connectionId: computed(() => "connection-1"), + database: computed(() => "dbx"), + context: computed(() => "table-data"), + sourceColumns: computed(() => ["id", "name"]), + allColumns: computed(() => ["id", "name"]), + allDisplayItems: computed(() => [item]), + allSourceColumns: computed(() => ["id", "name"]), + visibleColumnIndexes: computed(() => [0, 1]), + columnTypes: computed(() => ["int", "varchar"]), + whereInput: computed(() => undefined), + orderBy: computed(() => undefined), + exportBatchSize: computed(() => 1000), + hasCellSelection: computed(() => true), + selectedCells: computed(() => matrix), + selectedCellMatrix: computed(() => matrix), + selectedRange: computed(() => ({ startRow: 0, endRow: 0, startCol: 0, endCol: 0 })), + contextCell: ref({ rowId: item.id, rowIndex: 0, col: 0 }), + contextSelectionIsSynthetic: ref(false), + getRowItem: (rowId) => (rowId === item.id ? item : undefined), + selectedRowIds: ref(new Set()), + hasRowSelection: computed(() => false), + }); + + expect(state.canCopyWithExtractor("sql-updates")).toBe(true); + expect(state.canCopyWithExtractor("sql-inserts")).toBe(true); + }); + it("preserves drag-reordered visible column order when copying", async () => { vi.mocked(extractDataGridSelection).mockResolvedValue({ text: "x", mimeType: "text/csv", fileExtension: "csv", rowCount: 1, columnCount: 3 }); const matrix: CellSelectionMatrix = { diff --git a/apps/desktop/src/composables/useDataGridExtractor.ts b/apps/desktop/src/composables/useDataGridExtractor.ts index 809d080ccb..aa542c55de 100644 --- a/apps/desktop/src/composables/useDataGridExtractor.ts +++ b/apps/desktop/src/composables/useDataGridExtractor.ts @@ -92,9 +92,15 @@ export function useDataGridExtractor(options: UseDataGridExtractorOptions) { const matrix = options.selectedCellMatrix.value; const isMultiCellSelection = !!matrix && (matrix.rowIndexes.length > 1 || matrix.columnIndexes.length > 1); // A right-click sets contextCell.col to the clicked column (≥ 0); the test - // harness and non-right-click paths leave it at -1. Only treat a 1×1 matrix - // as synthetic when there's a real right-click context. + // harness and non-right-click paths leave it at -1. const hasRightClickContext = !!options.contextCell.value && options.contextSelectionIsSynthetic.value; + // When the user already has a single-cell selection and right-clicks the same + // cell, contextSelectionIsSynthetic is false but we still have a valid context + // cell. For INSERT/UPDATE extractors, we include all visible non-PK columns + // so the "has writable column" check doesn't fail when the selected cell + // happens to be the primary key. sql-select intentionally keeps using only + // the selected cell via the matrix branch below. + const hasNonSyntheticContextCell = !!options.contextCell.value && options.contextCell.value.col >= 0 && !options.contextSelectionIsSynthetic.value && !!matrix && !isMultiCellSelection; if (options.hasRowSelection.value && options.selectedRowIds.value.size > 0) { const items = options.displayItems.value.filter((item) => options.selectedRowIds.value.has(item.id) && !item.isDraft); @@ -108,8 +114,9 @@ export function useDataGridExtractor(options: UseDataGridExtractorOptions) { sourceRowIds = items.map((item) => item.id); selectedSourceIndexes = dedupeColumnIndexes(matrix.columnIndexes.map((index) => visibleIndexes[index] ?? index)).filter((index) => index < fullColumns.length); if (options.hasColumnSelection.value) selectionKind = "columns"; - } else if (hasRightClickContext && options.contextCell.value) { - // A SELECT targets the clicked cell; existing extractors keep their full-row context behavior. + } else if ((hasRightClickContext || (hasNonSyntheticContextCell && extractor !== "sql-select")) && options.contextCell.value) { + // A SELECT targets the clicked cell; other extractors use the full row + // so that right-clicking a selected PK cell still enables INSERT/UPDATE. const item = fullItemsById.get(options.contextCell.value.rowId); if (item && !item.isDraft) { sourceRows = [item.data]; @@ -248,7 +255,7 @@ export function useDataGridExtractor(options: UseDataGridExtractorOptions) { if (!request?.tableMeta?.tableName.trim() || request.rows.length !== 1) return false; if (request.selectionKind === "columns") return false; if (request.selectionKind === "cells" && request.selectedColumnIndexes.length !== 1) return false; - return request.columns.length > 0 && request.columns.every((column) => !!column.sourceName?.trim()); + return request.columns.length > 0 && request.columns.every((column) => !!(column.sourceName ?? column.displayName)?.trim()); } if (extractor === "raw") { const request = buildRequest(extractor, extractorOptions);