feat(editor): support dragging tables into queries

This commit is contained in:
t8y2 committed 2026-05-30 09:31:07 +08:00
1 parent 13d9fd14b4
commit e324f35f28
6 files changed
+229 -1

No files matched your search

@@ -24,6 +24,11 @@ import {
} from "@/lib/sqlCompletion";
import { extractIdentifierAt, isSqlKeyword, matchTable } from "@/lib/sqlNavigation";
import { lineColumnToOffset, parseSqlErrorLocation } from "@/lib/sqlDiagnostics";
import {
DBX_TABLE_REFERENCE_MIME,
parseTableReferencePayload,
tableReferenceInsertText,
} from "@/lib/queryEditorTableDrop";
import {
EDITOR_FONT_FAMILY_CSS_VAR,
EDITOR_FONT_SIZE_CSS_VAR,
@@ -47,13 +52,14 @@ import {
type SqlSemanticDiagnostic,
} from "@/lib/sqlSemanticDiagnostics";
import type { SqlCompletionColumn } from "@/lib/sqlCompletion";
import type { SqlReferenceAnalysis, SqlTableReference, SqlTextSpan } from "@/types/database";
import type { DatabaseType, SqlReferenceAnalysis, SqlTableReference, SqlTextSpan } from "@/types/database";
import { vscodeSelectionLayer } from "@/lib/codemirrorVscodeSelectionLayer";
const props = defineProps<{
modelValue: string;
connectionId?: string;
database?: string;
databaseType?: DatabaseType;
dialect?: "mysql" | "postgres" | "sqlserver";
formatDialect?: SqlFormatDialect;
formatRequestId?: number;
@@ -715,6 +721,33 @@ async function formatCurrentSql() {
}
}
function droppedTableReference(event: DragEvent) {
return parseTableReferencePayload(event.dataTransfer?.getData(DBX_TABLE_REFERENCE_MIME));
}
function insertDroppedTableReference(currentView: EditorViewType, event: DragEvent): boolean {
if (props.readOnly) return false;
const payload = droppedTableReference(event);
if (!payload) return false;
event.preventDefault();
event.stopPropagation();
const insertText = tableReferenceInsertText(payload, props.databaseType);
const dropPos = currentView.posAtCoords({ x: event.clientX, y: event.clientY });
const selection = currentView.state.selection.main;
const from = dropPos ?? selection.from;
const to = dropPos == null && !selection.empty ? selection.to : from;
currentView.dispatch({
changes: { from, to, insert: insertText },
selection: { anchor: from + insertText.length },
scrollIntoView: true,
userEvent: "input.drop",
});
currentView.focus();
return true;
}
let completionEpoch = 0;
let completionDebounceTimer: ReturnType<typeof setTimeout> | null = null;
@@ -1249,6 +1282,15 @@ onMounted(async () => {
}),
),
EditorView.domEventHandlers({
dragover(event) {
if (props.readOnly || !droppedTableReference(event)) return false;
event.preventDefault();
if (event.dataTransfer) event.dataTransfer.dropEffect = "copy";
return true;
},
drop(event, currentView) {
return insertDroppedTableReference(currentView, event);
},
wheel(event) {
if (!event.metaKey && !event.ctrlKey) return false;
event.preventDefault();
@@ -293,6 +293,7 @@ defineExpose({ focusSearch, refreshData, handleModRTarget });
:model-value="activeTab.sql"
:connection-id="activeTab.connectionId"
:database="activeTab.database"
:database-type="activeConnection?.db_type"
:dialect="editorDialect"
:format-dialect="activeSqlFormatDialect"
:format-request-id="formatSqlRequestId"
@@ -1391,6 +1391,7 @@ function getObjectBrowserMenuItems(item: ObjectBrowserRow): ContextMenuItem[] {
class="min-h-0 flex-1"
:connection-id="props.connection.id"
:database="props.database"
:database-type="props.connection.db_type"
:dialect="sourceDialect"
:format-dialect="sourceFormatDialect"
force-word-wrap
@@ -1407,6 +1408,7 @@ function getObjectBrowserMenuItems(item: ObjectBrowserRow): ContextMenuItem[] {
class="min-h-0 flex-1"
:connection-id="props.connection.id"
:database="props.database"
:database-type="props.connection.db_type"
:dialect="sourceDialect"
:format-dialect="sourceFormatDialect"
force-word-wrap
@@ -58,6 +58,12 @@ import { uuid } from "@/lib/utils";
import { resolveDefaultDatabase } from "@/lib/defaultDatabase";
import { canTreeNodeShowExpander, treeItemPaddingLeft } from "@/lib/sidebarTreeItemLayout";
import { buildTableSelectSql } from "@/lib/tableSelectSql";
import {
DBX_TABLE_REFERENCE_MIME,
createTableReferencePayload,
serializeTableReferencePayload,
tableReferenceInsertText,
} from "@/lib/queryEditorTableDrop";
import { editablePrimaryKeys, usesSyntheticRowIdKey } from "@/lib/tableEditing";
import {
supportsDatabaseCreation,
@@ -1792,6 +1798,29 @@ const showDropInside = computed(
() => dragState.active && dragState.targetId === props.node.id && dragState.dropPosition === "inside",
);
const isDragging = computed(() => dragState.active && dragState.draggedId === props.node.id);
const canDragTableReference = computed(
() =>
!props.dragDisabled &&
(props.node.type === "table" || props.node.type === "view") &&
!!props.node.connectionId &&
!!props.node.database,
);
function onTableReferenceDragStart(event: DragEvent) {
if (!canDragTableReference.value) return;
const payload = createTableReferencePayload({
connectionId: props.node.connectionId,
database: props.node.database,
schema: props.node.schema,
tableName: props.node.label,
databaseType: currentDatabaseType(),
});
if (!payload || !event.dataTransfer) return;
const insertText = tableReferenceInsertText(payload, currentDatabaseType());
event.dataTransfer.effectAllowed = "copy";
event.dataTransfer.setData(DBX_TABLE_REFERENCE_MIME, serializeTableReferencePayload(payload));
event.dataTransfer.setData("text/plain", insertText);
}
// ---- CustomContextMenu ----
@@ -2110,12 +2139,14 @@ function treeItemMenuItems(): ContextMenuItem[] {
}"
:tabindex="isSelected ? 0 : -1"
:style="rowStyle"
:draggable="canDragTableReference"
@click="onClick"
@dblclick="onDoubleClick"
@keydown="onKeydown"
@mousedown="isDraggable ? startDrag($event, node.id, node.type) : undefined"
@mousemove="isDropTarget ? updateTarget($event, node.id, node.type) : undefined"
@mouseleave="clearTarget(node.id)"
@dragstart="onTableReferenceDragStart"
>
<div
v-if="showDropBefore"