diff --git a/apps/desktop/src/components/editor/QueryEditor.vue b/apps/desktop/src/components/editor/QueryEditor.vue index 292d0037a..fdcad01e4 100644 --- a/apps/desktop/src/components/editor/QueryEditor.vue +++ b/apps/desktop/src/components/editor/QueryEditor.vue @@ -26,6 +26,8 @@ import { extractIdentifierAt, isSqlKeyword, matchTable } from "@/lib/sqlNavigati import { lineColumnToOffset, parseSqlErrorLocation } from "@/lib/sqlDiagnostics"; import { DBX_TABLE_REFERENCE_MIME, + activeTableReferencePayloadValue, + clearActiveTableReferencePayload, hasTableReferencePayloadType, parseTableReferencePayload, tableReferenceInsertText, @@ -723,11 +725,14 @@ async function formatCurrentSql() { } function droppedTableReference(event: DragEvent) { - return parseTableReferencePayload(event.dataTransfer?.getData(DBX_TABLE_REFERENCE_MIME)); + return ( + activeTableReferencePayloadValue() ?? + parseTableReferencePayload(event.dataTransfer?.getData(DBX_TABLE_REFERENCE_MIME)) + ); } function hasDroppedTableReference(event: DragEvent) { - return hasTableReferencePayloadType(event.dataTransfer?.types); + return !!activeTableReferencePayloadValue() || hasTableReferencePayloadType(event.dataTransfer?.types); } function insertDroppedTableReference(currentView: EditorViewType, event: DragEvent): boolean { @@ -749,6 +754,7 @@ function insertDroppedTableReference(currentView: EditorViewType, event: DragEve scrollIntoView: true, userEvent: "input.drop", }); + clearActiveTableReferencePayload(payload); currentView.focus(); return true; } diff --git a/apps/desktop/src/components/sidebar/TreeItem.vue b/apps/desktop/src/components/sidebar/TreeItem.vue index 9fb9fe888..2465752e6 100644 --- a/apps/desktop/src/components/sidebar/TreeItem.vue +++ b/apps/desktop/src/components/sidebar/TreeItem.vue @@ -60,8 +60,10 @@ import { canTreeNodeShowExpander, treeItemPaddingLeft } from "@/lib/sidebarTreeI import { buildTableSelectSql } from "@/lib/tableSelectSql"; import { DBX_TABLE_REFERENCE_MIME, + clearActiveTableReferencePayload, createTableReferencePayload, serializeTableReferencePayload, + setActiveTableReferencePayload, tableReferenceInsertText, } from "@/lib/queryEditorTableDrop"; import { editablePrimaryKeys, usesSyntheticRowIdKey } from "@/lib/tableEditing"; @@ -1803,9 +1805,11 @@ const canDragTableReference = computed( !props.dragDisabled && (props.node.type === "table" || props.node.type === "view") && !!props.node.connectionId && - !!props.node.database, + props.node.database != null, ); +let draggingTableReferencePayload: ReturnType = null; + function onTableReferenceDragStart(event: DragEvent) { if (!canDragTableReference.value) return; const payload = createTableReferencePayload({ @@ -1815,11 +1819,20 @@ function onTableReferenceDragStart(event: DragEvent) { tableName: props.node.label, databaseType: currentDatabaseType(), }); - if (!payload || !event.dataTransfer) return; + if (!payload) return; + draggingTableReferencePayload = payload; + setActiveTableReferencePayload(payload); const insertText = tableReferenceInsertText(payload, currentDatabaseType()); - event.dataTransfer.effectAllowed = "copy"; - event.dataTransfer.setData(DBX_TABLE_REFERENCE_MIME, serializeTableReferencePayload(payload)); - event.dataTransfer.setData("text/plain", insertText); + if (event.dataTransfer) { + event.dataTransfer.effectAllowed = "copy"; + event.dataTransfer.setData(DBX_TABLE_REFERENCE_MIME, serializeTableReferencePayload(payload)); + event.dataTransfer.setData("text/plain", insertText); + } +} + +function onTableReferenceDragEnd() { + clearActiveTableReferencePayload(draggingTableReferencePayload); + draggingTableReferencePayload = null; } // ---- CustomContextMenu ---- @@ -2147,6 +2160,7 @@ function treeItemMenuItems(): ContextMenuItem[] { @mousemove="isDropTarget ? updateTarget($event, node.id, node.type) : undefined" @mouseleave="clearTarget(node.id)" @dragstart="onTableReferenceDragStart" + @dragend="onTableReferenceDragEnd" >
| undefined return false; } +export function setActiveTableReferencePayload(payload: QueryEditorTableReferencePayload | null) { + activeTableReferencePayload = payload; +} + +export function activeTableReferencePayloadValue(): QueryEditorTableReferencePayload | null { + return activeTableReferencePayload; +} + +export function clearActiveTableReferencePayload(payload?: QueryEditorTableReferencePayload | null) { + if (!payload || activeTableReferencePayload === payload) { + activeTableReferencePayload = null; + } +} + export function tableReferenceInsertText( payload: QueryEditorTableReferencePayload, fallbackDatabaseType?: DatabaseType, diff --git a/packages/app-tests/queryEditorTableDrop.test.ts b/packages/app-tests/queryEditorTableDrop.test.ts index d0cab9154..49bbb4593 100644 --- a/packages/app-tests/queryEditorTableDrop.test.ts +++ b/packages/app-tests/queryEditorTableDrop.test.ts @@ -2,15 +2,33 @@ import { strict as assert } from "node:assert"; import test from "node:test"; import { DBX_TABLE_REFERENCE_MIME, + activeTableReferencePayloadValue, + clearActiveTableReferencePayload, createTableReferencePayload, hasTableReferencePayloadType, parseTableReferencePayload, serializeTableReferencePayload, + setActiveTableReferencePayload, tableReferenceInsertText, } from "../../apps/desktop/src/lib/queryEditorTableDrop.ts"; test("creates table drag payload only when table context is complete", () => { assert.equal(createTableReferencePayload({ connectionId: "c1", database: "db" }), null); + assert.deepEqual( + createTableReferencePayload({ + connectionId: "c1", + database: "", + tableName: "catalogless_table", + databaseType: "sqlite", + }), + { + kind: "dbx-table-reference", + connectionId: "c1", + database: "", + tableName: "catalogless_table", + databaseType: "sqlite", + }, + ); assert.deepEqual( createTableReferencePayload({ connectionId: "c1", @@ -39,10 +57,40 @@ test("round trips table drag payload and rejects unrelated data", () => { }); assert.ok(payload); assert.deepEqual(parseTableReferencePayload(serializeTableReferencePayload(payload)), payload); + assert.deepEqual( + parseTableReferencePayload( + JSON.stringify({ + kind: "dbx-table-reference", + connectionId: "c1", + database: "", + tableName: "orders", + }), + ), + { + kind: "dbx-table-reference", + connectionId: "c1", + database: "", + tableName: "orders", + }, + ); assert.equal(parseTableReferencePayload("not json"), null); assert.equal(parseTableReferencePayload(JSON.stringify({ kind: "dbx-table-reference", tableName: "orders" })), null); }); +test("tracks the active in-app table drag payload without dataTransfer reads", () => { + const payload = createTableReferencePayload({ + connectionId: "c1", + database: "", + tableName: "orders", + databaseType: "sqlite", + }); + assert.ok(payload); + setActiveTableReferencePayload(payload); + assert.equal(activeTableReferencePayloadValue(), payload); + clearActiveTableReferencePayload(payload); + assert.equal(activeTableReferencePayloadValue(), null); +}); + test("detects table drag payload type without reading drag data", () => { assert.equal(hasTableReferencePayloadType(undefined), false); assert.equal(hasTableReferencePayloadType(["text/plain"]), false);