From c77948b3c06fa30e5998910e1ba9637a54e504f8 Mon Sep 17 00:00:00 2001 From: t8y2 <1156263951@qq.com> Date: Tue, 16 Jun 2026 17:12:37 +0800 Subject: [PATCH] feat(editor): drag columns into SQL editor --- .../src/components/sidebar/TreeItem.vue | 24 ++++++++- apps/desktop/src/lib/queryEditorTableDrop.ts | 23 +++++++-- .../app-tests/queryEditorTableDrop.test.ts | 50 +++++++++++++++++++ 3 files changed, 93 insertions(+), 4 deletions(-) diff --git a/apps/desktop/src/components/sidebar/TreeItem.vue b/apps/desktop/src/components/sidebar/TreeItem.vue index 3c59f86867..ca90876828 100644 --- a/apps/desktop/src/components/sidebar/TreeItem.vue +++ b/apps/desktop/src/components/sidebar/TreeItem.vue @@ -2773,7 +2773,11 @@ const showDropInside = computed(() => dragState.active && dragState.targetId === const isDragging = computed(() => dragState.active && dragState.draggedId === props.node.id); const TABLE_REFERENCE_DRAG_THRESHOLD = 5; const TABLE_REFERENCE_DRAGGING_CLASS = "dbx-table-reference-dragging"; -const canDragTableReference = computed(() => !props.dragDisabled && (props.node.type === "table" || props.node.type === "view") && !!props.node.connectionId && props.node.database != null); +const canDragTableReference = computed(() => { + if (props.dragDisabled || !props.node.connectionId || props.node.database == null) return false; + if (props.node.type === "table" || props.node.type === "view") return true; + return props.node.type === "column" && !!props.node.tableName; +}); let pendingTableReferenceDrag: { payload: QueryEditorTableReferencePayload; @@ -2785,6 +2789,18 @@ let suppressNextTableReferenceClick = false; function tableReferenceDragPayload(): QueryEditorTableReferencePayload | null { if (!canDragTableReference.value) return null; + if (props.node.type === "column") { + const columnName = columnNameForDrag(props.node); + if (!props.node.tableName || !columnName) return null; + return createTableReferencePayload({ + connectionId: props.node.connectionId, + database: props.node.database, + schema: props.node.schema, + tableName: props.node.tableName, + columnName, + databaseType: currentDatabaseType(), + }); + } const payload = createTableReferencePayload({ connectionId: props.node.connectionId, database: props.node.database, @@ -2795,6 +2811,12 @@ function tableReferenceDragPayload(): QueryEditorTableReferencePayload | null { return payload; } +function columnNameForDrag(node: TreeNode): string { + const column = node.meta as Partial | undefined; + if (typeof column?.name === "string" && column.name) return column.name; + return node.label.replace(/\s+\([^()]*\)$/, ""); +} + function startTableReferenceDrag(payload: QueryEditorTableReferencePayload) { draggingTableReferencePayload = payload; setActiveTableReferencePayload(payload); diff --git a/apps/desktop/src/lib/queryEditorTableDrop.ts b/apps/desktop/src/lib/queryEditorTableDrop.ts index 23e4e22915..5f601e50f8 100644 --- a/apps/desktop/src/lib/queryEditorTableDrop.ts +++ b/apps/desktop/src/lib/queryEditorTableDrop.ts @@ -1,5 +1,5 @@ import type { DatabaseType } from "@/types/database"; -import { qualifiedTableName } from "@/lib/tableSelectSql"; +import { qualifiedTableName, quoteTableIdentifier } from "@/lib/tableSelectSql"; export const DBX_TABLE_REFERENCE_MIME = "application/x-dbx-table-reference"; export const DBX_TABLE_REFERENCE_DROP_EVENT = "dbx-table-reference-drop"; @@ -10,6 +10,8 @@ export interface QueryEditorTableReferencePayload { database: string; schema?: string; tableName: string; + columnName?: string; + referenceType?: "table" | "column"; databaseType?: DatabaseType; } @@ -21,7 +23,7 @@ export interface QueryEditorTableReferenceDropDetail { let activeTableReferencePayload: QueryEditorTableReferencePayload | null = null; -export function createTableReferencePayload(options: { connectionId?: string; database?: string; schema?: string; tableName?: string; databaseType?: DatabaseType }): QueryEditorTableReferencePayload | null { +export function createTableReferencePayload(options: { connectionId?: string; database?: string; schema?: string; tableName?: string; columnName?: string; databaseType?: DatabaseType }): QueryEditorTableReferencePayload | null { if (!options.connectionId || options.database == null || !options.tableName) return null; const payload: QueryEditorTableReferencePayload = { kind: "dbx-table-reference", @@ -29,6 +31,10 @@ export function createTableReferencePayload(options: { connectionId?: string; da database: options.database, tableName: options.tableName, }; + if (options.columnName) { + payload.columnName = options.columnName; + payload.referenceType = "column"; + } if (options.schema) payload.schema = options.schema; if (options.databaseType) payload.databaseType = options.databaseType; return payload; @@ -45,12 +51,19 @@ export function parseTableReferencePayload(value: string | undefined | null): Qu if (parsed.kind !== "dbx-table-reference" || typeof parsed.connectionId !== "string" || typeof parsed.database !== "string" || typeof parsed.tableName !== "string" || !parsed.connectionId || !parsed.tableName) { return null; } + const columnName = typeof parsed.columnName === "string" && parsed.columnName ? parsed.columnName : undefined; + const referenceType = parsed.referenceType === "column" || columnName ? "column" : "table"; + if (referenceType === "column" && !columnName) return null; const payload: QueryEditorTableReferencePayload = { kind: "dbx-table-reference", connectionId: parsed.connectionId, database: parsed.database, tableName: parsed.tableName, }; + if (referenceType === "column" && columnName) { + payload.columnName = columnName; + payload.referenceType = "column"; + } if (typeof parsed.schema === "string" && parsed.schema) payload.schema = parsed.schema; if (parsed.databaseType) payload.databaseType = parsed.databaseType; return payload; @@ -86,8 +99,12 @@ export function createTableReferenceDropEvent(detail: QueryEditorTableReferenceD } export function tableReferenceInsertText(payload: QueryEditorTableReferencePayload, fallbackDatabaseType?: DatabaseType): string { + const databaseType = payload.databaseType ?? fallbackDatabaseType; + if (payload.referenceType === "column" && payload.columnName) { + return quoteTableIdentifier(databaseType, payload.columnName); + } return qualifiedTableName({ - databaseType: payload.databaseType ?? fallbackDatabaseType, + databaseType, schema: payload.schema, tableName: payload.tableName, }); diff --git a/packages/app-tests/queryEditorTableDrop.test.ts b/packages/app-tests/queryEditorTableDrop.test.ts index 01c8833679..3bed891b9d 100644 --- a/packages/app-tests/queryEditorTableDrop.test.ts +++ b/packages/app-tests/queryEditorTableDrop.test.ts @@ -48,6 +48,29 @@ test("creates table drag payload only when table context is complete", () => { ); }); +test("creates column drag payload when column context is complete", () => { + assert.deepEqual( + createTableReferencePayload({ + connectionId: "c1", + database: "db", + schema: "public", + tableName: "users", + columnName: "user_id", + databaseType: "postgres", + }), + { + kind: "dbx-table-reference", + connectionId: "c1", + database: "db", + schema: "public", + tableName: "users", + columnName: "user_id", + referenceType: "column", + databaseType: "postgres", + }, + ); +}); + test("round trips table drag payload and rejects unrelated data", () => { const payload = createTableReferencePayload({ connectionId: "c1", @@ -75,6 +98,18 @@ test("round trips table drag payload and rejects unrelated data", () => { ); assert.equal(parseTableReferencePayload("not json"), null); assert.equal(parseTableReferencePayload(JSON.stringify({ kind: "dbx-table-reference", tableName: "orders" })), null); + assert.equal( + parseTableReferencePayload( + JSON.stringify({ + kind: "dbx-table-reference", + connectionId: "c1", + database: "db", + tableName: "orders", + referenceType: "column", + }), + ), + null, + ); }); test("tracks the active in-app table drag payload without dataTransfer reads", () => { @@ -132,3 +167,18 @@ test("formats dropped table reference for the source database type", () => { "`order-detail`", ); }); + +test("formats dropped column references for the source database type", () => { + assert.equal( + tableReferenceInsertText({ + kind: "dbx-table-reference", + connectionId: "c1", + database: "db", + tableName: "users", + columnName: "display name", + referenceType: "column", + databaseType: "postgres", + }), + '"display name"', + ); +});