fix(grid): enable copy as SQL for selected cells

This commit is contained in:
Melody Ruch authored and GitHub committed 2026-08-15 14:41:29 +08:00
1 parent 84f1498a4b
commit cd6f99fbc5
2 files changed
+101 -12

No files matched your search

@@ -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<number>()),
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<number>()),
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 = {
@@ -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);