mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-20 00:17:44 +00:00
@@ -943,9 +943,16 @@ function requestActiveEditorExecute(source?: "pointer" | "keyboard", tabId?: str
|
||||
void tryExecute(undefined, targetTabId ? { tabId: targetTabId } : undefined);
|
||||
}
|
||||
|
||||
function requestActiveEditorExecuteInNewResultTab() {
|
||||
if (contentAreaRef.value?.requestQueryEditorExecuteInNewResultTab?.()) return;
|
||||
void tryExecuteInNewResultTab();
|
||||
function requestActiveEditorExecuteInNewResultTab(source?: "pointer" | "keyboard", tabId?: string) {
|
||||
const snapshot = pendingToolbarExecutionSnapshot.value;
|
||||
pendingToolbarExecutionSnapshot.value = undefined;
|
||||
const targetTabId = tabId ?? (source === "pointer" ? snapshot?.tabId : undefined);
|
||||
if (source === "pointer" && snapshot && snapshot.tabId === targetTabId) {
|
||||
void tryExecuteInNewResultTab(snapshot, { tabId: targetTabId });
|
||||
return;
|
||||
}
|
||||
if (contentAreaRef.value?.requestQueryEditorExecuteInNewResultTab?.(targetTabId)) return;
|
||||
void tryExecuteInNewResultTab(undefined, targetTabId ? { tabId: targetTabId } : undefined);
|
||||
}
|
||||
|
||||
const toolbarAgentDriverUpdateCount = computed(() => Math.max(agentDriverUpdateCount.value, componentUpdates.driverUpdateCount.value));
|
||||
@@ -989,6 +996,7 @@ provide(EDITOR_TOOLBAR_ACTIONS, {
|
||||
databaseRequiredSignalFor: (tabId: string) => (databaseRequiredTabId.value === tabId ? databaseRequiredSignal.value : 0),
|
||||
captureExecutionSnapshot: captureActiveEditorExecutionSnapshot,
|
||||
toolbarExecute: requestActiveEditorExecute,
|
||||
toolbarExecuteInNewResultTab: requestActiveEditorExecuteInNewResultTab,
|
||||
cancelExecution: (tabId: string) => cancelActiveExecution(tabId),
|
||||
explain: (tabId: string) => tryExplain(undefined, { tabId }),
|
||||
formatSql: formatActiveSql,
|
||||
|
||||
@@ -208,6 +208,7 @@ const groupExecutableSql = computed(() => {
|
||||
@dismiss-auto-commit-session-txn-rolled-back="activeTab && (activeTab.autoCommitSessionTxnRolledBack = false)"
|
||||
@execute-pointer-down="toolbar.captureExecutionSnapshot(activeTab.id)"
|
||||
@toolbar-execute="toolbar.toolbarExecute($event, activeTab.id)"
|
||||
@toolbar-execute-in-new-result-tab="toolbar.toolbarExecuteInNewResultTab($event, activeTab.id)"
|
||||
@multi-execute="toolbar.multiExecute()"
|
||||
@preview-changes="activeTab && toolbar.previewChanges(activeTab.id)"
|
||||
@cancel="activeTab && toolbar.cancelExecution(activeTab.id)"
|
||||
|
||||
@@ -1,7 +1,35 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, nextTick, onUnmounted, ref, watch, watchEffect } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { Play, CirclePlay, Loader2, Square, Database, Check, Table2, AlignLeft, GitBranch, Save, FolderOpen, X, Shield, Download, RotateCcw, AlertTriangle, ClipboardPaste, Minimize2, SpellCheck2, Layers, MoreHorizontal, BetweenVerticalStart, Eye, WrapText, RefreshCw, UserRound } from "@lucide/vue";
|
||||
import {
|
||||
Play,
|
||||
SquarePlay,
|
||||
CirclePlay,
|
||||
Loader2,
|
||||
Square,
|
||||
Database,
|
||||
Check,
|
||||
Table2,
|
||||
AlignLeft,
|
||||
GitBranch,
|
||||
Save,
|
||||
FolderOpen,
|
||||
X,
|
||||
Shield,
|
||||
Download,
|
||||
RotateCcw,
|
||||
AlertTriangle,
|
||||
ClipboardPaste,
|
||||
Minimize2,
|
||||
SpellCheck2,
|
||||
Layers,
|
||||
MoreHorizontal,
|
||||
BetweenVerticalStart,
|
||||
Eye,
|
||||
WrapText,
|
||||
RefreshCw,
|
||||
UserRound,
|
||||
} from "@lucide/vue";
|
||||
import { supportsInsertValueHints } from "@/lib/editor/codemirrorInsertValueHints";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { SearchableSelect } from "@/components/ui/searchable-select";
|
||||
@@ -63,6 +91,7 @@ const props = defineProps<{
|
||||
|
||||
const emit = defineEmits<{
|
||||
toolbarExecute: [source: "pointer" | "keyboard"];
|
||||
toolbarExecuteInNewResultTab: [source: "pointer" | "keyboard"];
|
||||
executePointerDown: [];
|
||||
cancel: [];
|
||||
previewChanges: [];
|
||||
@@ -289,6 +318,11 @@ function refreshObjectSource() {
|
||||
}
|
||||
const executeShortcutDisplay = computed(() => formatShortcutDisplay(settingsStore.editorSettings.shortcuts.executeSql));
|
||||
const executeShortcutTooltip = computed(() => t("toolbar.executeShortcut", { shortcut: executeShortcutDisplay.value }));
|
||||
const executeInNewResultTabShortcutDisplay = computed(() => formatShortcutDisplay(settingsStore.editorSettings.shortcuts.executeSqlInNewResultTab));
|
||||
const executeInNewResultTabTooltip = computed(() => {
|
||||
const label = t("settings.shortcutExecuteSqlInNewResultTab");
|
||||
return executeInNewResultTabShortcutDisplay.value ? `${label} (${executeInNewResultTabShortcutDisplay.value})` : label;
|
||||
});
|
||||
// executableSql 在无选区时可能是整篇文档;只要有 DML 语句出现就显示预览按钮,
|
||||
// 具体"当前语句"由编辑器(QueryEditor)按执行模式解析。
|
||||
const DML_KEYWORD_RE = /(^|\s)(update|insert|delete)\s/i;
|
||||
@@ -469,6 +503,10 @@ function onExecuteClick(event: MouseEvent) {
|
||||
emit("toolbarExecute", event.detail > 0 ? "pointer" : "keyboard");
|
||||
}
|
||||
|
||||
function onExecuteInNewResultTabClick(event: MouseEvent) {
|
||||
emit("toolbarExecuteInNewResultTab", event.detail > 0 ? "pointer" : "keyboard");
|
||||
}
|
||||
|
||||
async function changeCatalog(selectedCatalog: string) {
|
||||
const connection = props.activeConnection;
|
||||
if (!connection) return;
|
||||
@@ -504,6 +542,22 @@ async function changeCatalog(selectedCatalog: string) {
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>{{ activeTab.isExecuting ? t("toolbar.stopQuery") : executeShortcutTooltip }}</TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip>
|
||||
<TooltipTrigger as-child>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-6 w-6 text-violet-600 hover:bg-violet-500/10 hover:text-violet-700 dark:text-violet-300 dark:hover:text-violet-200"
|
||||
:disabled="activeTab.isExecuting || activeTab.isCancelling || activeTab.isExplaining || !executableSql.trim()"
|
||||
:aria-label="t('settings.shortcutExecuteSqlInNewResultTab')"
|
||||
@mousedown.prevent="onExecutePointerDown"
|
||||
@click="onExecuteInNewResultTabClick"
|
||||
>
|
||||
<SquarePlay class="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>{{ executeInNewResultTabTooltip }}</TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip v-if="showPreviewButton">
|
||||
<TooltipTrigger as-child>
|
||||
<Button variant="ghost" size="icon" class="h-6 w-6 text-sky-600 hover:bg-sky-500/10 hover:text-sky-700" :disabled="activeTab.isExecuting || activeTab.isCancelling" :aria-label="t('editor.previewChanges')" @mousedown.prevent @click="emit('previewChanges')">
|
||||
|
||||
@@ -200,6 +200,7 @@ describe("EditorGroup mount contract", () => {
|
||||
...createNoopEditorToolbarActions(),
|
||||
captureExecutionSnapshot: vi.fn(),
|
||||
toolbarExecute: vi.fn(),
|
||||
toolbarExecuteInNewResultTab: vi.fn(),
|
||||
cancelExecution: vi.fn(),
|
||||
explain: vi.fn(),
|
||||
saveSql: vi.fn(),
|
||||
@@ -233,12 +234,14 @@ describe("EditorGroup mount contract", () => {
|
||||
// Toolbar events flow to the injected actions with the group's tab id.
|
||||
(toolbar as any).__vueParentComponent.emit("executePointerDown");
|
||||
(toolbar as any).__vueParentComponent.emit("toolbarExecute", "pointer");
|
||||
(toolbar as any).__vueParentComponent.emit("toolbarExecuteInNewResultTab", "pointer");
|
||||
(toolbar as any).__vueParentComponent.emit("cancel");
|
||||
(toolbar as any).__vueParentComponent.emit("explain");
|
||||
(toolbar as any).__vueParentComponent.emit("saveSql", queryId);
|
||||
await nextTick();
|
||||
expect(actions.captureExecutionSnapshot).toHaveBeenCalledWith(queryId);
|
||||
expect(actions.toolbarExecute).toHaveBeenCalledWith("pointer", queryId);
|
||||
expect(actions.toolbarExecuteInNewResultTab).toHaveBeenCalledWith("pointer", queryId);
|
||||
expect(actions.cancelExecution).toHaveBeenCalledWith(queryId);
|
||||
expect(actions.explain).toHaveBeenCalledWith(queryId);
|
||||
expect(actions.saveSql).toHaveBeenCalledWith(queryId);
|
||||
|
||||
@@ -220,7 +220,7 @@ describe("EditorToolbar mount contract", () => {
|
||||
host.remove();
|
||||
});
|
||||
|
||||
it("executes the exact CodeMirror selection after the toolbar click moves focus", async () => {
|
||||
it("keeps the exact CodeMirror selection for regular and new-result toolbar execution", async () => {
|
||||
const connectionStore = useConnectionStore();
|
||||
const connection = {
|
||||
id: "conn-1",
|
||||
@@ -237,6 +237,7 @@ describe("EditorToolbar mount contract", () => {
|
||||
const state = reactive({ sql: fullSql });
|
||||
let pendingSnapshot: SqlExecutionSnapshot | undefined;
|
||||
let executedSql = "";
|
||||
let executedInNewResultSql = "";
|
||||
|
||||
const host = createHost();
|
||||
const app = createApp({
|
||||
@@ -283,6 +284,9 @@ describe("EditorToolbar mount contract", () => {
|
||||
onToolbarExecute: (source: "pointer" | "keyboard") => {
|
||||
executedSql = source === "pointer" && pendingSnapshot ? resolveExecutableSql(pendingSnapshot.fullSql, pendingSnapshot.selectedSql, { mode: "current", cursorPos: pendingSnapshot.cursorPos }) : fullSql;
|
||||
},
|
||||
onToolbarExecuteInNewResultTab: (source: "pointer" | "keyboard") => {
|
||||
executedInNewResultSql = source === "pointer" && pendingSnapshot ? resolveExecutableSql(pendingSnapshot.fullSql, pendingSnapshot.selectedSql, { mode: "current", cursorPos: pendingSnapshot.cursorPos }) : fullSql;
|
||||
},
|
||||
}),
|
||||
]);
|
||||
},
|
||||
@@ -304,6 +308,69 @@ describe("EditorToolbar mount contract", () => {
|
||||
expect(pendingSnapshot?.selectedSql).toBe(selectedSql);
|
||||
expect(executedSql).toBe(selectedSql);
|
||||
|
||||
const newResultButton = host.querySelector<HTMLButtonElement>('button[aria-label="settings.shortcutExecuteSqlInNewResultTab"]');
|
||||
expect(newResultButton).not.toBeNull();
|
||||
expect(newResultButton?.disabled).toBe(false);
|
||||
newResultButton?.dispatchEvent(new MouseEvent("mousedown", { button: 0, bubbles: true, cancelable: true }));
|
||||
newResultButton?.dispatchEvent(new MouseEvent("click", { detail: 1, bubbles: true }));
|
||||
await nextTick();
|
||||
|
||||
expect(pendingSnapshot?.selectedSql).toBe(selectedSql);
|
||||
expect(executedInNewResultSql).toBe(selectedSql);
|
||||
|
||||
app.unmount();
|
||||
host.remove();
|
||||
});
|
||||
|
||||
it("disables new-result execution when there is no executable SQL", async () => {
|
||||
const connectionStore = useConnectionStore();
|
||||
connectionStore.connections = [
|
||||
{
|
||||
id: "conn-empty",
|
||||
name: "empty",
|
||||
db_type: "mysql",
|
||||
color: "",
|
||||
} as never,
|
||||
];
|
||||
|
||||
const host = createHost();
|
||||
const onToolbarExecuteInNewResultTab = vi.fn();
|
||||
const app = createApp(EditorToolbar, {
|
||||
activeTab: {
|
||||
id: "tab-empty",
|
||||
title: "SQL",
|
||||
connectionId: "conn-empty",
|
||||
database: "db",
|
||||
sql: "",
|
||||
mode: "query",
|
||||
isExecuting: false,
|
||||
isCancelling: false,
|
||||
isExplaining: false,
|
||||
},
|
||||
activeConnection: connectionStore.getConfig("conn-empty"),
|
||||
executableSql: "",
|
||||
explainMode: "explain",
|
||||
blockDangerousRedisCommands: false,
|
||||
sqlKeywordCase: "preserve",
|
||||
databaseRequiredSignal: 0,
|
||||
autoCommit: true,
|
||||
txnSessionId: undefined,
|
||||
txnAutoRolledBack: false,
|
||||
txnPossiblyDirty: false,
|
||||
stickyProvenReadOnlyState: false,
|
||||
onToolbarExecuteInNewResultTab,
|
||||
});
|
||||
app.use(pinia);
|
||||
app.use(i18n);
|
||||
app.mount(host);
|
||||
await nextTick();
|
||||
|
||||
const newResultButton = host.querySelector<HTMLButtonElement>('button[aria-label="settings.shortcutExecuteSqlInNewResultTab"]');
|
||||
expect(newResultButton).not.toBeNull();
|
||||
expect(newResultButton?.disabled).toBe(true);
|
||||
newResultButton?.click();
|
||||
expect(onToolbarExecuteInNewResultTab).not.toHaveBeenCalled();
|
||||
|
||||
app.unmount();
|
||||
host.remove();
|
||||
});
|
||||
|
||||
@@ -28,6 +28,7 @@ export interface EditorToolbarActions {
|
||||
/** Captures the acting editor's execution snapshot before a toolbar click. */
|
||||
captureExecutionSnapshot(tabId: string): void;
|
||||
toolbarExecute(source: "pointer" | "keyboard", tabId: string): void;
|
||||
toolbarExecuteInNewResultTab(source: "pointer" | "keyboard", tabId: string): void;
|
||||
cancelExecution(tabId: string): void;
|
||||
explain(tabId: string): void;
|
||||
formatSql(tabId: string): void;
|
||||
@@ -75,6 +76,7 @@ export function createNoopEditorToolbarActions(): EditorToolbarActions {
|
||||
databaseRequiredSignalFor: () => 0,
|
||||
captureExecutionSnapshot: noop,
|
||||
toolbarExecute: noop,
|
||||
toolbarExecuteInNewResultTab: noop,
|
||||
cancelExecution: noop,
|
||||
explain: noop,
|
||||
formatSql: noop,
|
||||
|
||||
Reference in New Issue
Block a user