feat(editor): drag result grid columns into the SQL editor
This commit is contained in:
1 parent
9e4fe8ce56
commit
ab4e747bf9
17 files changed
+911
-25
No files matched your search
@@ -97,14 +97,18 @@ import { analyzeMysqlRoutineSyntax, supportsMysqlRoutineSyntaxDiagnostics } from
|
||||
import {
|
||||
DBX_TABLE_REFERENCE_MIME,
|
||||
DBX_TABLE_REFERENCE_DROP_EVENT,
|
||||
DBX_TABLE_REFERENCE_HOVER_EVENT,
|
||||
DBX_TABLE_REFERENCE_DRAG_END_EVENT,
|
||||
activeTableReferencePayloadValue,
|
||||
clearActiveTableReferencePayload,
|
||||
hasTableReferencePayloadType,
|
||||
parseTableReferencePayload,
|
||||
tableReferenceInsertText,
|
||||
type QueryEditorTableReferenceDropDetail,
|
||||
type QueryEditorTableReferenceHoverDetail,
|
||||
type QueryEditorTableReferencePayload,
|
||||
} from "@/lib/editor/queryEditorTableDrop";
|
||||
import { isPointOverElementRoot } from "@/lib/editor/tableReferenceDragFeedback";
|
||||
import type { SqlHighlighter } from "@/lib/sql/sqlHighlighter";
|
||||
import { EDITOR_FONT_FAMILY_CSS_VAR, EDITOR_FONT_SIZE_CSS_VAR, editorDiagnosticColors, editorThemeAppearanceFor, loadEditorTheme, editorFontTheme, shellLineCommentTheme, sqlCompletionTheme, sqlSemanticHighlightTheme } from "@/lib/editor/editorThemes";
|
||||
import { createStatementGutterMarkerDom, shouldShowStatementGutter } from "@/lib/editor/codemirrorStatementGutter";
|
||||
@@ -3295,6 +3299,7 @@ function insertTableReferencePayload(currentView: EditorViewType, payload: Query
|
||||
userEvent: "input.drop",
|
||||
});
|
||||
clearActiveTableReferencePayload(payload);
|
||||
hideQueryEditorDropCaret();
|
||||
currentView.focus();
|
||||
return true;
|
||||
}
|
||||
@@ -3316,21 +3321,77 @@ function onTableReferenceDropEvent(event: Event) {
|
||||
if (!currentView || props.readOnly || !(event instanceof CustomEvent)) return;
|
||||
const detail = event.detail as QueryEditorTableReferenceDropDetail | undefined;
|
||||
if (!detail?.payload) return;
|
||||
const target = document.elementFromPoint(detail.clientX, detail.clientY);
|
||||
if (target instanceof Element && editorRef.value?.contains(target)) {
|
||||
// elementFromPoint 被透明覆盖层拦截时回退为编辑器根节点包围盒判定(见 isPointOverElementRoot)。
|
||||
if (isPointOverElementRoot(detail.clientX, detail.clientY, editorRef.value)) {
|
||||
insertTableReferencePayload(currentView, detail.payload, detail);
|
||||
}
|
||||
}
|
||||
|
||||
// --- 表引用拖拽悬停时的插入光标线(指针模拟拖拽经 window 事件驱动) ---
|
||||
const queryEditorDropCaret = ref<{ left: number; top: number; height: number } | null>(null);
|
||||
const queryEditorDropCaretStyle = computed(() => {
|
||||
const caret = queryEditorDropCaret.value;
|
||||
return caret ? { left: `${caret.left}px`, top: `${caret.top}px`, height: `${caret.height}px` } : {};
|
||||
});
|
||||
|
||||
function showQueryEditorDropCaretAt(clientX: number, clientY: number) {
|
||||
const currentView = view.value;
|
||||
if (!currentView || props.readOnly || !editorRef.value) {
|
||||
hideQueryEditorDropCaret();
|
||||
return;
|
||||
}
|
||||
let dropPos: number | null = null;
|
||||
try {
|
||||
dropPos = currentView.posAtCoords({ x: clientX, y: clientY });
|
||||
} catch {
|
||||
dropPos = null;
|
||||
}
|
||||
if (dropPos == null) {
|
||||
hideQueryEditorDropCaret();
|
||||
return;
|
||||
}
|
||||
const coords = currentView.coordsAtPos(dropPos);
|
||||
if (!coords) {
|
||||
hideQueryEditorDropCaret();
|
||||
return;
|
||||
}
|
||||
const rect = editorRef.value.getBoundingClientRect();
|
||||
queryEditorDropCaret.value = { left: coords.left - rect.left, top: coords.top - rect.top, height: Math.max(coords.bottom - coords.top, 0) };
|
||||
}
|
||||
|
||||
function hideQueryEditorDropCaret() {
|
||||
queryEditorDropCaret.value = null;
|
||||
}
|
||||
|
||||
function onTableReferenceHoverEvent(event: Event) {
|
||||
if (!(event instanceof CustomEvent)) return;
|
||||
const detail = event.detail as QueryEditorTableReferenceHoverDetail | undefined;
|
||||
if (!detail) return;
|
||||
// elementFromPoint 被透明覆盖层拦截时回退为编辑器根节点包围盒判定(见 isPointOverElementRoot)。
|
||||
if (!isPointOverElementRoot(detail.clientX, detail.clientY, editorRef.value)) {
|
||||
hideQueryEditorDropCaret();
|
||||
return;
|
||||
}
|
||||
showQueryEditorDropCaretAt(detail.clientX, detail.clientY);
|
||||
}
|
||||
|
||||
function onTableReferenceDragEndEvent() {
|
||||
hideQueryEditorDropCaret();
|
||||
}
|
||||
|
||||
function registerTableReferenceDropListener() {
|
||||
if (tableReferenceDropListenerRegistered) return;
|
||||
window.addEventListener(DBX_TABLE_REFERENCE_DROP_EVENT, onTableReferenceDropEvent);
|
||||
window.addEventListener(DBX_TABLE_REFERENCE_HOVER_EVENT, onTableReferenceHoverEvent);
|
||||
window.addEventListener(DBX_TABLE_REFERENCE_DRAG_END_EVENT, onTableReferenceDragEndEvent);
|
||||
tableReferenceDropListenerRegistered = true;
|
||||
}
|
||||
|
||||
function unregisterTableReferenceDropListener() {
|
||||
if (!tableReferenceDropListenerRegistered) return;
|
||||
window.removeEventListener(DBX_TABLE_REFERENCE_DROP_EVENT, onTableReferenceDropEvent);
|
||||
window.removeEventListener(DBX_TABLE_REFERENCE_HOVER_EVENT, onTableReferenceHoverEvent);
|
||||
window.removeEventListener(DBX_TABLE_REFERENCE_DRAG_END_EVENT, onTableReferenceDragEndEvent);
|
||||
tableReferenceDropListenerRegistered = false;
|
||||
}
|
||||
|
||||
@@ -5374,12 +5435,21 @@ onMounted(async () => {
|
||||
return recoverLargeTauriPaste(event, currentView);
|
||||
},
|
||||
dragover(event) {
|
||||
if (props.readOnly || !hasDroppedTableReference(event)) return false;
|
||||
if (props.readOnly || !hasDroppedTableReference(event)) {
|
||||
hideQueryEditorDropCaret();
|
||||
return false;
|
||||
}
|
||||
event.preventDefault();
|
||||
if (event.dataTransfer) event.dataTransfer.dropEffect = "copy";
|
||||
showQueryEditorDropCaretAt(event.clientX, event.clientY);
|
||||
return true;
|
||||
},
|
||||
dragleave() {
|
||||
hideQueryEditorDropCaret();
|
||||
return false;
|
||||
},
|
||||
drop(event, currentView) {
|
||||
hideQueryEditorDropCaret();
|
||||
return insertDroppedTableReference(currentView, event);
|
||||
},
|
||||
blur(_event, currentView) {
|
||||
@@ -6187,6 +6257,7 @@ defineExpose({
|
||||
"
|
||||
/>
|
||||
</CustomContextMenu>
|
||||
<div v-show="queryEditorDropCaret" data-query-editor-drop-caret class="pointer-events-none absolute z-20 w-0.5 rounded-full bg-primary/70" :style="queryEditorDropCaretStyle" />
|
||||
<EditorSearchPanel ref="searchPanelRef" :view="view" />
|
||||
<SqlExecutionTargetPicker v-if="pickerVisible" :candidates="pickerCandidates" :active-index="pickerActiveIndex" :anchor="pickerAnchor" @update:active-index="onPickerActiveIndexChange" @confirm="onPickerConfirm" @cancel="closePicker" />
|
||||
<DelimitedListDialog v-model:open="delimitedListOpen" :selected-text="delimitedListSelectedText" @confirm="applyDelimitedListResult" />
|
||||
|
||||
@@ -303,7 +303,9 @@ import { DATA_GRID_COPY_EXTRACTOR_DESCRIPTORS, DATA_GRID_COPY_EXTRACTOR_IDS, DAT
|
||||
import { columnNamesForCopy } from "@/lib/dataGrid/dataGridColumnNameCopy";
|
||||
import { DATA_GRID_ROW_NUM_WIDTH, dataGridRowNumberColumnWidth, resolveDataGridMaxRowNumber, useDataGridColumnResize } from "@/composables/useDataGridColumnResize";
|
||||
import { createDataGridColumnStructureSignature } from "@/lib/dataGrid/dataGridColumnWidthState";
|
||||
import { useDataGridColumnLayout, useDataGridColumnLayoutState } from "@/composables/useDataGridColumnLayout";
|
||||
import { useDataGridColumnLayout, useDataGridColumnLayoutState, type ColumnHeaderReferenceDragController } from "@/composables/useDataGridColumnLayout";
|
||||
import { createColumnReferencePayload, createTableReferenceDragEndEvent, createTableReferenceDropEvent, createTableReferenceHoverEvent } from "@/lib/editor/queryEditorTableDrop";
|
||||
import { beginTableReferenceDragFeedback, isOverSqlEditorTarget, type TableReferenceDragFeedback } from "@/lib/editor/tableReferenceDragFeedback";
|
||||
import { dataGridCanvasDevicePixelSize, useDataGridCanvasRuntime, type DataGridCanvasRuntime } from "@/composables/useDataGridCanvasRuntime";
|
||||
import { useDataGridScrollbars, type DataGridScrollbarsRuntime } from "@/composables/useDataGridScrollbars";
|
||||
import { useDataGridSelection } from "@/composables/useDataGridSelection";
|
||||
@@ -2867,6 +2869,62 @@ function unfreezeAllColumns() {
|
||||
applyColumnOrderChange(unfreezeAllColumnsInLayout);
|
||||
}
|
||||
|
||||
// --- 表头拖拽进 SQL 编辑器:目标导向模式切换的控制器 ---
|
||||
// 按选择顺序(Set 插入序)取列名;被拖列未选中时仅拖该列。
|
||||
function columnReferenceDragNames(draggedVisibleColIdx: number): string[] | null {
|
||||
const selected = [...selection.selectedColumnIndexes.value];
|
||||
const ordered = selected.includes(draggedVisibleColIdx) ? selected : [draggedVisibleColIdx];
|
||||
const names = ordered.map((index) => visibleColumns.value[index]).filter((name): name is string => !!name);
|
||||
return names.length > 0 ? names : null;
|
||||
}
|
||||
|
||||
function buildColumnReferencePayload(draggedVisibleColIdx: number) {
|
||||
const columnNames = columnReferenceDragNames(draggedVisibleColIdx);
|
||||
if (!columnNames) return null;
|
||||
return createColumnReferencePayload({
|
||||
connectionId: props.connectionId,
|
||||
database: props.database,
|
||||
schema: props.schema,
|
||||
columnNames,
|
||||
databaseType: resolvedDatabaseType.value,
|
||||
});
|
||||
}
|
||||
|
||||
let referenceDragFeedback: TableReferenceDragFeedback | null = null;
|
||||
|
||||
// 拖拽 chip 文案:≤3 个列名直接平铺,更多时走 i18n 摘要模板。
|
||||
function columnReferenceDragLabel(names: string[]): string {
|
||||
if (names.length <= 3) return names.join(", ");
|
||||
return t("grid.columnDragChipMany", { names: names.slice(0, 2).join(", "), count: names.length });
|
||||
}
|
||||
|
||||
const columnHeaderReferenceDragController: ColumnHeaderReferenceDragController = {
|
||||
isOverEditorTarget: (clientX, clientY) => isOverSqlEditorTarget(clientX, clientY),
|
||||
onEnter(sourceVisibleIndex) {
|
||||
const names = columnReferenceDragNames(sourceVisibleIndex);
|
||||
if (!names) return null;
|
||||
const label = columnReferenceDragLabel(names);
|
||||
referenceDragFeedback?.end();
|
||||
referenceDragFeedback = beginTableReferenceDragFeedback(label);
|
||||
return label;
|
||||
},
|
||||
onMove(_sourceVisibleIndex, clientX, clientY) {
|
||||
referenceDragFeedback?.update(clientX, clientY);
|
||||
window.dispatchEvent(createTableReferenceHoverEvent({ clientX, clientY }));
|
||||
},
|
||||
onDrop(sourceVisibleIndex, clientX, clientY) {
|
||||
const payload = buildColumnReferencePayload(sourceVisibleIndex);
|
||||
if (!payload) return false;
|
||||
window.dispatchEvent(createTableReferenceDropEvent({ payload, clientX, clientY }));
|
||||
return true;
|
||||
},
|
||||
onCancel() {
|
||||
referenceDragFeedback?.end();
|
||||
referenceDragFeedback = null;
|
||||
window.dispatchEvent(createTableReferenceDragEndEvent());
|
||||
},
|
||||
};
|
||||
|
||||
const {
|
||||
renderedColumnOffsets,
|
||||
horizontalColumnWindow,
|
||||
@@ -2905,6 +2963,7 @@ const {
|
||||
onRefreshMetrics: scheduleColumnLayoutRefresh,
|
||||
onPersistColumnOrder: persistDraggedColumnOrder,
|
||||
frozenColumnCount,
|
||||
columnReferenceDrag: columnHeaderReferenceDragController,
|
||||
});
|
||||
|
||||
function onHeaderClickCapture(event: MouseEvent) {
|
||||
|
||||
@@ -53,8 +53,9 @@ import { Switch } from "@/components/ui/switch";
|
||||
import LightTooltip from "@/components/ui/LightTooltip.vue";
|
||||
import type { ColumnInfo, ConnectionConfig, CustomTypeTreeMemberMeta, DatabaseType, TreeNode, TriggerInfo } from "@/types/database";
|
||||
import { alignedCommentLeadingWidth, canTreeNodePin, canTreeNodeShowExpander, sidebarTreeNodeComment, trailingCommentAvailableWidth, trailingCommentGapPx, treeItemPaddingLeft, treeLabelWidthClass, usesFullWidthTreeLabel } from "@/lib/sidebar/sidebarTreeItemLayout";
|
||||
import { clearActiveTableReferencePayload, createTableReferencePayload, createTableReferenceDropEvent, setActiveTableReferencePayload, type QueryEditorTableReferencePayload } from "@/lib/editor/queryEditorTableDrop";
|
||||
import { clearActiveTableReferencePayload, createTableReferenceDragEndEvent, createTableReferenceDropEvent, createTableReferenceHoverEvent, createTableReferencePayload, setActiveTableReferencePayload, type QueryEditorTableReferencePayload } from "@/lib/editor/queryEditorTableDrop";
|
||||
import { AI_ASSISTANT_TABLE_DROP_ROOT_SELECTOR } from "@/lib/ai/aiTableReferenceDrop";
|
||||
import { beginTableReferenceDragFeedback, isOverSqlEditorTarget, type TableReferenceDragFeedback } from "@/lib/editor/tableReferenceDragFeedback";
|
||||
import { formatSidebarObjectStorage } from "@/lib/sidebar/sidebarDatabaseStorage";
|
||||
import { dataTabOpenModeFromTreeClick } from "@/lib/sidebar/dataTabOpenPolicy";
|
||||
import { effectiveDatabaseTypeForConnection } from "@/lib/database/jdbcDialect";
|
||||
@@ -1126,8 +1127,6 @@ function clearTreeDragTarget() {
|
||||
|
||||
const TABLE_REFERENCE_DRAG_THRESHOLD = 5;
|
||||
|
||||
const TABLE_REFERENCE_DRAGGING_CLASS = "dbx-table-reference-dragging";
|
||||
|
||||
const canDragTableReference = computed(() => {
|
||||
if (props.referenceDragDisabled || !activeNode.value.connectionId) return false;
|
||||
if (activeNode.value.type === "database") return typeof activeNode.value.database === "string" && activeNode.value.database.trim().length > 0;
|
||||
@@ -1144,8 +1143,15 @@ let pendingTableReferenceDrag: {
|
||||
|
||||
let draggingTableReferencePayload: QueryEditorTableReferencePayload | null = null;
|
||||
|
||||
let referenceDragFeedback: TableReferenceDragFeedback | null = null;
|
||||
|
||||
let suppressNextTableReferenceClick = false;
|
||||
|
||||
function tableReferenceDragLabel(payload: QueryEditorTableReferencePayload): string {
|
||||
if (payload.referenceType === "column" && payload.columnName) return payload.columnName;
|
||||
return payload.tableName || payload.database;
|
||||
}
|
||||
|
||||
function tableReferenceDragPayload(): QueryEditorTableReferencePayload | null {
|
||||
if (!canDragTableReference.value) return null;
|
||||
if (activeNode.value.type === "database") {
|
||||
@@ -1191,15 +1197,16 @@ function startTableReferenceDrag(payload: QueryEditorTableReferencePayload) {
|
||||
draggingTableReferencePayload = payload;
|
||||
setActiveTableReferencePayload(payload);
|
||||
document.getSelection()?.removeAllRanges();
|
||||
document.body.style.cursor = "copy";
|
||||
referenceDragFeedback = beginTableReferenceDragFeedback(tableReferenceDragLabel(payload));
|
||||
}
|
||||
|
||||
function finishTableReferenceDrag() {
|
||||
clearActiveTableReferencePayload(draggingTableReferencePayload);
|
||||
pendingTableReferenceDrag = null;
|
||||
draggingTableReferencePayload = null;
|
||||
document.body.classList.remove(TABLE_REFERENCE_DRAGGING_CLASS);
|
||||
document.body.style.cursor = "";
|
||||
referenceDragFeedback?.end();
|
||||
referenceDragFeedback = null;
|
||||
window.dispatchEvent(createTableReferenceDragEndEvent());
|
||||
document.removeEventListener("mousemove", onTableReferenceMouseMove, true);
|
||||
document.removeEventListener("mouseup", onTableReferenceMouseUp, true);
|
||||
}
|
||||
@@ -1215,6 +1222,11 @@ function onTableReferenceMouseMove(event: MouseEvent) {
|
||||
if (draggingTableReferencePayload) {
|
||||
event.preventDefault();
|
||||
document.getSelection()?.removeAllRanges();
|
||||
referenceDragFeedback?.update(event.clientX, event.clientY);
|
||||
// 仅查询编辑器消费 hover 光标线事件;AI 面板不监听。命中判定含覆盖层拦截时的几何回退。
|
||||
if (isOverSqlEditorTarget(event.clientX, event.clientY)) {
|
||||
window.dispatchEvent(createTableReferenceHoverEvent({ clientX: event.clientX, clientY: event.clientY }));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1242,7 +1254,6 @@ function startTableReferenceMouseDrag(event: MouseEvent) {
|
||||
if (!payload) return;
|
||||
event.preventDefault();
|
||||
document.getSelection()?.removeAllRanges();
|
||||
document.body.classList.add(TABLE_REFERENCE_DRAGGING_CLASS);
|
||||
pendingTableReferenceDrag = { payload, startX: event.clientX, startY: event.clientY };
|
||||
document.addEventListener("mousemove", onTableReferenceMouseMove, true);
|
||||
document.addEventListener("mouseup", onTableReferenceMouseUp, true);
|
||||
|
||||
Reference in new issue
Block a user