feat(editor): add ExPaste paste-as-IN-condition helper (#2746)
* feat: add ExPaste SQL IN condition helper * fix: restrict ExPaste to SQL-like editors --------- Co-authored-by: zipg <4047349+zipg@users.noreply.github.com>
This commit is contained in:
1 parent
29495a7220
commit
705bacba04
17 files changed
+491
-8
No files matched your search
@@ -8,12 +8,13 @@ import { search as cmSearch } from "@codemirror/search";
|
||||
import EditorSearchPanel from "./EditorSearchPanel.vue";
|
||||
import SqlExecutionTargetPicker from "./SqlExecutionTargetPicker.vue";
|
||||
import CustomContextMenu, { type ContextMenuItem } from "@/components/ui/CustomContextMenu.vue";
|
||||
import { copyToClipboard } from "@/lib/common/clipboard";
|
||||
import { copyToClipboard, readTextFromClipboard } from "@/lib/common/clipboard";
|
||||
import { resolveExecutableSql, type SqlExecutionSnapshot, type SqlExecutionOverride, type SqlExecutionCandidate } from "@/lib/sql/sqlExecutionTarget";
|
||||
import { buildExecutionCandidates, hasMultipleExecutionTargets, supportsExecutionTargetPicker, type SqlTextRange } from "@/lib/sql/sqlStatementRanges";
|
||||
import { executableStatementRangeAtCursor, executableStatementRangeCacheForDoc, executableStatementRangeStartingAt as executableStatementRangeStartingAtLine, type ExecutableStatementRangeCache } from "@/lib/sql/executableStatementRangeCache";
|
||||
import { currentStatementFrameRangeTo, visualSqlColumns } from "@/lib/sql/currentStatementFrame";
|
||||
import { formatSqlText, type SqlFormatDialect } from "@/lib/sql/sqlFormatter";
|
||||
import { buildSqlInConditionFromPasteSource, insertTextForSqlInCondition } from "@/lib/sql/sqlInListPaste";
|
||||
import { formatMongoShellText } from "@/lib/mongo/mongoFormatter";
|
||||
import { useConnectionStore, COMPLETION_METADATA_CONCURRENCY } from "@/stores/connectionStore";
|
||||
import { useSettingsStore } from "@/stores/settingsStore";
|
||||
@@ -54,7 +55,7 @@ import { normalizeShortcutSettings, shortcutToCodeMirrorKey } from "@/lib/editor
|
||||
import { trimmedSelectionLayer } from "@/lib/editor/codemirrorTrimmedSelectionLayer";
|
||||
import { selectionMatchOccurrences } from "@/lib/editor/codemirrorSelectionMatches";
|
||||
import { createDbxCodeMirrorSqlDialect } from "@/lib/editor/codemirrorSqlDialect";
|
||||
import { isSchemaAware, isSingleDatabase } from "@/lib/database/databaseFeatureSupport";
|
||||
import { isSchemaAware, isSingleDatabase, supportsSqlInListPaste } from "@/lib/database/databaseFeatureSupport";
|
||||
import { usesLocalOnlyEditorCompletionMetadata, usesOnDemandOnlyEditorColumnMetadata } from "@/lib/metadata/completionMetadataPolicy";
|
||||
import { qualifiedTableNameAtSqlPosition } from "@/lib/sql/queryCursorTableTarget";
|
||||
import * as api from "@/lib/backend/api";
|
||||
@@ -738,6 +739,50 @@ function convertSelectedSqlCase(mode: SelectionCaseMode): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
async function pasteClipboardAsSqlInCondition(): Promise<boolean> {
|
||||
if (!supportsSqlInListPaste(props.databaseType)) return false;
|
||||
if (props.readOnly) return false;
|
||||
const currentView = view.value;
|
||||
if (!currentView) return false;
|
||||
|
||||
const selection = currentView.state.selection.main;
|
||||
const selectedSource = selection.empty ? "" : currentView.state.sliceDoc(selection.from, selection.to);
|
||||
let source = selectedSource;
|
||||
if (!source) {
|
||||
try {
|
||||
source = await readTextFromClipboard();
|
||||
} catch (e: any) {
|
||||
toast(t("editor.exPasteClipboardReadFailed", { message: e?.message || String(e) }), 5000);
|
||||
focusEditor();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
const result = buildSqlInConditionFromPasteSource(source);
|
||||
if (!result.ok) {
|
||||
const key = result.reason === "too-large" ? "editor.exPasteTooLarge" : result.reason === "too-many-values" ? "editor.exPasteTooManyValues" : result.reason === "not-list" ? "editor.exPasteNotList" : "editor.exPasteNoValues";
|
||||
toast(t(key, { limit: result.limit ?? 0 }), 5000);
|
||||
focusEditor();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (view.value !== currentView || props.readOnly) return false;
|
||||
const state = currentView.state;
|
||||
const line = state.doc.lineAt(selection.from);
|
||||
const prefix = state.sliceDoc(line.from, selection.from);
|
||||
const insertText = insertTextForSqlInCondition(result.sql, prefix);
|
||||
|
||||
currentView.dispatch({
|
||||
changes: { from: selection.from, to: selection.to, insert: insertText },
|
||||
selection: { anchor: selection.from + insertText.length },
|
||||
scrollIntoView: true,
|
||||
userEvent: "input.paste",
|
||||
});
|
||||
currentView.focus();
|
||||
toast(t("editor.exPastePasted", { count: result.valueCount }), 2000);
|
||||
return true;
|
||||
}
|
||||
|
||||
function openTableFromContextMenu() {
|
||||
if (!contextTableName.value) return;
|
||||
emit("viewTableData", contextTableName.value);
|
||||
@@ -887,6 +932,11 @@ function runKeymapExtension(codeMirrorKeymap: (typeof import("@codemirror/view")
|
||||
...binding(shortcuts.selectAll, (view) => codeMirrorSelectAll?.(view) ?? false),
|
||||
...binding(shortcuts.uppercaseSelection, () => convertSelectedSqlCase("upper")),
|
||||
...binding(shortcuts.lowercaseSelection, () => convertSelectedSqlCase("lower")),
|
||||
...binding(shortcuts.exPasteSqlInCondition, () => {
|
||||
if (!supportsSqlInListPaste(props.databaseType)) return false;
|
||||
void pasteClipboardAsSqlInCondition();
|
||||
return true;
|
||||
}),
|
||||
]),
|
||||
) ?? [],
|
||||
codeMirrorKeymap.of(
|
||||
@@ -3254,7 +3304,7 @@ function scrollCursorIntoView() {
|
||||
});
|
||||
}
|
||||
|
||||
defineExpose({ openSearch, openReplace, scrollCursorIntoView, requestExecute });
|
||||
defineExpose({ openSearch, openReplace, scrollCursorIntoView, requestExecute, pasteClipboardAsSqlInCondition });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -727,7 +727,11 @@ function requestQueryEditorExecute() {
|
||||
return queryEditorRef.value?.requestExecute();
|
||||
}
|
||||
|
||||
defineExpose({ focusSearch, refreshData, handleModRTarget, requestQueryEditorExecute });
|
||||
function pasteClipboardAsSqlInCondition() {
|
||||
return queryEditorRef.value?.pasteClipboardAsSqlInCondition();
|
||||
}
|
||||
|
||||
defineExpose({ focusSearch, refreshData, handleModRTarget, requestQueryEditorExecute, pasteClipboardAsSqlInCondition });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch, watchEffect } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { Play, Loader2, Square, Database, Check, Table2, AlignLeft, GitBranch, Save, FolderOpen, Layers, X, Shield, Upload, RotateCcw, AlertTriangle } from "@lucide/vue";
|
||||
import { Play, Loader2, Square, Database, Check, Table2, AlignLeft, GitBranch, Save, FolderOpen, Layers, X, Shield, Upload, RotateCcw, AlertTriangle, ClipboardPaste } from "@lucide/vue";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { SearchableSelect } from "@/components/ui/searchable-select";
|
||||
import { Tooltip, TooltipTrigger, TooltipContent } from "@/components/ui/tooltip";
|
||||
@@ -13,7 +13,7 @@ import { useSchemaOptions } from "@/composables/useSchemaOptions";
|
||||
import { connectionIconType } from "@/lib/connection/connectionPresentation";
|
||||
import { formatDatabaseLabel, isDefaultDatabase } from "@/lib/database/defaultDatabase";
|
||||
import { connectionDisplayName } from "@/lib/tabs/tabPresentation";
|
||||
import { isSingleDatabase, supportsTransaction as supportsTransactionFeature } from "@/lib/database/databaseCapabilities";
|
||||
import { isSingleDatabase, supportsSqlInListPaste, supportsTransaction as supportsTransactionFeature } from "@/lib/database/databaseCapabilities";
|
||||
import { hexToRgba } from "@/lib/common/color";
|
||||
import type { QueryTab, ConnectionConfig } from "@/types/database";
|
||||
|
||||
@@ -40,6 +40,7 @@ const emit = defineEmits<{
|
||||
saveSql: [];
|
||||
openSql: [];
|
||||
importResultArchive: [];
|
||||
pasteSqlInCondition: [];
|
||||
changeConnection: [connectionId: string];
|
||||
changeDatabase: [database: string];
|
||||
changeSchema: [schema: string | undefined];
|
||||
@@ -71,6 +72,7 @@ const supportsExplain = computed(() => {
|
||||
return dbType !== "redis" && dbType !== "mongodb" && dbType !== "elasticsearch" && dbType !== "qdrant" && dbType !== "milvus" && dbType !== "weaviate" && dbType !== "chromadb" && dbType !== "etcd" && dbType !== "zookeeper" && dbType !== "mq" && dbType !== "nacos";
|
||||
});
|
||||
const isSingleDb = computed(() => isSingleDatabase(props.activeConnection?.db_type));
|
||||
const supportsExPaste = computed(() => supportsSqlInListPaste(props.activeConnection?.db_type));
|
||||
const supportsTransaction = computed(() => supportsTransactionFeature(props.activeConnection?.db_type));
|
||||
const hasDefaultDatabaseOption = computed(() => activeDatabaseOptions.value.includes(""));
|
||||
const schemaDatabaseKey = computed(() => props.activeTab.database || (isSingleDb.value ? "_" : ""));
|
||||
@@ -288,6 +290,14 @@ function connectionById(connectionId: string): ConnectionConfig | undefined {
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>{{ t("tabs.importResultArchive") }}</TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip v-if="supportsExPaste">
|
||||
<TooltipTrigger as-child>
|
||||
<Button variant="ghost" size="icon" class="h-6 w-6 text-teal-600 hover:bg-teal-500/10 hover:text-teal-700 dark:text-teal-300 dark:hover:text-teal-200" @click="emit('pasteSqlInCondition')">
|
||||
<ClipboardPaste class="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>{{ t("toolbar.exPasteSqlInCondition") }}</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<span class="flex-1 min-w-0" />
|
||||
<div class="flex items-center gap-2 shrink-0">
|
||||
|
||||
Reference in new issue
Block a user