From 1137cab7f4ced01dbc8d8724e80db1a9f6fffc2c Mon Sep 17 00:00:00 2001 From: jearhe <0verme7753@gmail.com> Date: Sun, 13 Sep 2026 13:27:12 +0800 Subject: [PATCH] fix(editor): execute manually selected sql from the toolbar --- apps/desktop/src/App.vue | 41 ++--- .../src/components/layout/EditorGroup.vue | 4 +- .../components/layout/SqlEditorWorkspace.vue | 11 +- .../__tests__/EditorGroup.mount.spec.ts | 6 + .../__tests__/EditorToolbar.mount.spec.ts | 168 +++++++++++++++++- .../SqlEditorWorkspace.mount.spec.ts | 52 ++++++ .../components/layout/editorToolbarActions.ts | 4 +- .../__tests__/useSqlExecution.spec.ts | 44 +++++ .../editor/queryEditorExecution.spec.ts | 13 +- 9 files changed, 310 insertions(+), 33 deletions(-) diff --git a/apps/desktop/src/App.vue b/apps/desktop/src/App.vue index 88fd39651f..bab9a33721 100644 --- a/apps/desktop/src/App.vue +++ b/apps/desktop/src/App.vue @@ -776,15 +776,17 @@ const executableSql = computed(() => { : ""; }); -async function resolveActiveExecutableSql(snapshot?: SqlExecutionSnapshot) { - const tab = activeTab.value; - return tab - ? await resolveExecutableSqlWithBackend(snapshot?.fullSql ?? tab.sql, snapshot?.selectedSql ?? selectedSql.value, { - mode: settingsStore.editorSettings.executeMode, - cursorPos: snapshot?.cursorPos ?? cursorPos.value, - databaseType: activeConnection.value?.db_type, - }) - : ""; +async function resolveActiveExecutableSql(snapshot?: SqlExecutionSnapshot, executionTab?: QueryTab) { + const tab = executionTab ?? activeTab.value; + if (!tab) return ""; + const connection = connectionStore.getConfig(tab.connectionId) ?? activeConnection.value; + const selection = tab.editorSelection; + const fallbackSelectedSql = tab.id === activeTab.value?.id ? selectedSql.value : selection && selection.anchor !== selection.head ? tab.sql.slice(Math.min(selection.anchor, selection.head), Math.max(selection.anchor, selection.head)) : ""; + return await resolveExecutableSqlWithBackend(snapshot?.fullSql ?? tab.sql, snapshot?.selectedSql ?? fallbackSelectedSql, { + mode: settingsStore.editorSettings.executeMode, + cursorPos: snapshot?.cursorPos ?? (tab.id === activeTab.value?.id ? cursorPos.value : (selection?.head ?? 0)), + databaseType: connection?.db_type, + }); } const blockDangerousRedisCommands = ref(true); @@ -835,22 +837,21 @@ const { onExecutionCancelled: (editorViewportRequestId) => contentAreaRef.value?.cancelQueryEditorExecutionViewport(editorViewportRequestId), }); -function captureActiveEditorExecutionSnapshot() { - const snapshot = contentAreaRef.value?.captureQueryEditorExecutionSnapshot?.(); - pendingToolbarExecutionSnapshot.value = snapshot ? { ...snapshot, tabId: activeTab.value?.id } : undefined; +function captureActiveEditorExecutionSnapshot(tabId: string) { + const snapshot = contentAreaRef.value?.captureQueryEditorExecutionSnapshot?.(tabId); + pendingToolbarExecutionSnapshot.value = snapshot ? { ...snapshot, tabId } : undefined; } -function requestActiveEditorExecute(source?: "pointer" | "keyboard") { +function requestActiveEditorExecute(source?: "pointer" | "keyboard", tabId?: string) { const snapshot = pendingToolbarExecutionSnapshot.value; pendingToolbarExecutionSnapshot.value = undefined; - if (source === "pointer") { - if (snapshot) { - void tryExecute(snapshot, { tabId: snapshot.tabId ?? activeTab.value?.id }); - return; - } + const targetTabId = tabId ?? (source === "pointer" ? snapshot?.tabId : undefined); + if (source === "pointer" && snapshot && snapshot.tabId === targetTabId) { + void tryExecute(snapshot, { tabId: targetTabId }); + return; } - if (contentAreaRef.value?.requestQueryEditorExecute?.()) return; - void tryExecute(); + if (contentAreaRef.value?.requestQueryEditorExecute?.(targetTabId)) return; + void tryExecute(undefined, targetTabId ? { tabId: targetTabId } : undefined); } function requestActiveEditorExecuteInNewResultTab() { diff --git a/apps/desktop/src/components/layout/EditorGroup.vue b/apps/desktop/src/components/layout/EditorGroup.vue index ceff63bc45..3fc00dc032 100644 --- a/apps/desktop/src/components/layout/EditorGroup.vue +++ b/apps/desktop/src/components/layout/EditorGroup.vue @@ -192,8 +192,8 @@ const groupExecutableSql = computed(() => { @commit="activeTab && queryStore.commitTransaction(activeTab.id)" @rollback="activeTab && queryStore.rollbackTransaction(activeTab.id)" @dismiss-txn-rolled-back="activeTab && (activeTab.txnAutoRolledBack = false)" - @execute-pointer-down="toolbar.captureExecutionSnapshot()" - @toolbar-execute="toolbar.toolbarExecute($event)" + @execute-pointer-down="toolbar.captureExecutionSnapshot(activeTab.id)" + @toolbar-execute="toolbar.toolbarExecute($event, activeTab.id)" @multi-execute="toolbar.multiExecute()" @preview-changes="activeTab && toolbar.previewChanges(activeTab.id)" @cancel="activeTab && toolbar.cancelExecution(activeTab.id)" diff --git a/apps/desktop/src/components/layout/SqlEditorWorkspace.vue b/apps/desktop/src/components/layout/SqlEditorWorkspace.vue index 3e6fcdb751..aba9300794 100644 --- a/apps/desktop/src/components/layout/SqlEditorWorkspace.vue +++ b/apps/desktop/src/components/layout/SqlEditorWorkspace.vue @@ -92,9 +92,9 @@ defineExpose({ const group = groupForElement(target) ?? activeEditorGroup(); return group?.handleModRTarget(target) ?? false; }, - requestQueryEditorExecute: () => activeEditorGroup()?.requestQueryEditorExecute() ?? false, - captureQueryEditorExecutionSnapshot: () => activeEditorGroup()?.captureQueryEditorExecutionSnapshot(), - requestQueryEditorExecuteInNewResultTab: () => activeEditorGroup()?.requestQueryEditorExecuteInNewResultTab() ?? false, + requestQueryEditorExecute: (tabId?: string) => editorGroupForTab(tabId)?.requestQueryEditorExecute() ?? false, + captureQueryEditorExecutionSnapshot: (tabId?: string) => editorGroupForTab(tabId)?.captureQueryEditorExecutionSnapshot(), + requestQueryEditorExecuteInNewResultTab: (tabId?: string) => editorGroupForTab(tabId)?.requestQueryEditorExecuteInNewResultTab() ?? false, requestQueryEditorPreviewChanges: (stackSql?: string) => activeEditorGroup()?.requestQueryEditorPreviewChanges(stackSql) ?? false, shouldBlockQueryEditorExecutionShortcut: (event: KeyboardEvent) => activeEditorGroup()?.shouldBlockQueryEditorExecutionShortcut(event) ?? false, cancelQueryEditorExecutionViewport: (requestId: number) => activeEditorGroup()?.cancelQueryEditorExecutionViewport(requestId) ?? false, @@ -214,6 +214,11 @@ function activeEditorGroup() { const group = queryStore.groups.find((item) => item.id === queryStore.focusedGroupId) ?? queryStore.groups[0]; return group ? (groupRefs.get(group.id) ?? null) : null; } +function editorGroupForTab(tabId?: string): InstanceType | null { + if (!tabId) return activeEditorGroup(); + const group = queryStore.groups.find((item) => item.activeTabId === tabId); + return group ? (groupRefs.get(group.id) ?? null) : null; +} function groupForElement(element: Element | null): InstanceType | null { const groupElement = element?.closest("[data-group-id]"); const groupId = groupElement?.dataset.groupId; diff --git a/apps/desktop/src/components/layout/__tests__/EditorGroup.mount.spec.ts b/apps/desktop/src/components/layout/__tests__/EditorGroup.mount.spec.ts index 74e7217f8e..9fe4e5c0ea 100644 --- a/apps/desktop/src/components/layout/__tests__/EditorGroup.mount.spec.ts +++ b/apps/desktop/src/components/layout/__tests__/EditorGroup.mount.spec.ts @@ -198,6 +198,8 @@ describe("EditorGroup mount contract", () => { const actions: EditorToolbarActions = { ...createNoopEditorToolbarActions(), + captureExecutionSnapshot: vi.fn(), + toolbarExecute: vi.fn(), cancelExecution: vi.fn(), explain: vi.fn(), saveSql: vi.fn(), @@ -229,10 +231,14 @@ describe("EditorGroup mount contract", () => { expect(toolbar?.textContent).toBe(queryId); // 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("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.cancelExecution).toHaveBeenCalledWith(queryId); expect(actions.explain).toHaveBeenCalledWith(queryId); expect(actions.saveSql).toHaveBeenCalledWith(queryId); diff --git a/apps/desktop/src/components/layout/__tests__/EditorToolbar.mount.spec.ts b/apps/desktop/src/components/layout/__tests__/EditorToolbar.mount.spec.ts index 3a5b7f9f77..d03590825a 100644 --- a/apps/desktop/src/components/layout/__tests__/EditorToolbar.mount.spec.ts +++ b/apps/desktop/src/components/layout/__tests__/EditorToolbar.mount.spec.ts @@ -1,5 +1,5 @@ // @vitest-environment happy-dom -import { createApp, nextTick } from "vue"; +import { createApp, h, nextTick, reactive, ref } from "vue"; import { createPinia, setActivePinia } from "pinia"; import { createI18n } from "vue-i18n"; import { beforeEach, describe, expect, it, vi } from "vitest"; @@ -63,6 +63,9 @@ vi.mock("@/components/common/ProductionContextBadge.vue", () => ({ import EditorToolbar from "../EditorToolbar.vue"; import { useConnectionStore } from "@/stores/connectionStore"; +import { resolveExecutableSql, type SqlExecutionSnapshot } from "@/lib/sql/sqlExecutionTarget"; +import QueryEditor from "@/components/editor/QueryEditor.vue"; +import { EditorView } from "@codemirror/view"; function createHost(): HTMLDivElement { const host = document.createElement("div"); @@ -141,6 +144,169 @@ describe("EditorToolbar mount contract", () => { host.remove(); }); + it("keeps the manually selected SQL from pointer-down through toolbar execution", async () => { + const connectionStore = useConnectionStore(); + connectionStore.connections = [ + { + id: "conn-1", + name: "conn", + db_type: "postgres", + color: "", + } as never, + ]; + + const fullSql = "SELECT first_value;\nSELECT selected_value;"; + const selectedSql = "SELECT selected_value;"; + const snapshot = { + fullSql, + selectedSql, + cursorPos: fullSql.length, + selectionFrom: fullSql.indexOf(selectedSql), + selectionTo: fullSql.length, + }; + let pendingSnapshot: typeof snapshot | undefined; + let executedSql = ""; + const onExecutePointerDown = vi.fn(() => { + pendingSnapshot = snapshot; + }); + const onToolbarExecute = vi.fn((source: "pointer" | "keyboard") => { + executedSql = source === "pointer" && pendingSnapshot ? resolveExecutableSql(pendingSnapshot.fullSql, pendingSnapshot.selectedSql, { mode: "current", cursorPos: pendingSnapshot.cursorPos }) : fullSql; + }); + + const host = createHost(); + const app = createApp(EditorToolbar, { + activeTab: { + id: "tab-1", + title: "SQL", + connectionId: "conn-1", + database: "db", + sql: fullSql, + mode: "query", + isExecuting: false, + isCancelling: false, + isExplaining: false, + }, + activeConnection: connectionStore.getConfig("conn-1"), + executableSql: selectedSql, + explainMode: "explain", + blockDangerousRedisCommands: false, + sqlKeywordCase: "preserve", + databaseRequiredSignal: 0, + autoCommit: true, + txnSessionId: undefined, + txnAutoRolledBack: false, + oracleTxnPossiblyDirty: false, + isOracleManualTransaction: false, + onExecutePointerDown, + onToolbarExecute, + }); + app.use(pinia); + app.use(i18n); + app.mount(host); + await nextTick(); + + const runButton = host.querySelectorAll("button")[0]; + runButton?.dispatchEvent(new MouseEvent("mousedown", { button: 0, bubbles: true, cancelable: true })); + runButton?.dispatchEvent(new MouseEvent("click", { detail: 1, bubbles: true })); + await nextTick(); + + expect(onExecutePointerDown).toHaveBeenCalledTimes(1); + expect(onToolbarExecute).toHaveBeenCalledWith("pointer"); + expect(onExecutePointerDown.mock.invocationCallOrder[0]).toBeLessThan(onToolbarExecute.mock.invocationCallOrder[0]!); + expect(executedSql).toBe(selectedSql); + + app.unmount(); + host.remove(); + }); + + it("executes the exact CodeMirror selection after the toolbar click moves focus", async () => { + const connectionStore = useConnectionStore(); + const connection = { + id: "conn-1", + name: "conn", + db_type: "postgres", + color: "", + } as never; + connectionStore.connections = [connection]; + + const fullSql = "SELECT first_value;\nSELECT selected_value;"; + const selectedSql = "SELECT selected_value;"; + const selectionFrom = fullSql.indexOf(selectedSql); + const editorRef = ref<{ captureExecutionSnapshot?: () => SqlExecutionSnapshot | undefined } | null>(null); + const state = reactive({ sql: fullSql }); + let pendingSnapshot: SqlExecutionSnapshot | undefined; + let executedSql = ""; + + const host = createHost(); + const app = createApp({ + setup() { + return () => + h("div", [ + h(QueryEditor, { + ref: editorRef, + modelValue: state.sql, + tabId: "tab-1", + connectionId: "conn-1", + database: "db", + databaseType: "postgres", + dialect: "postgres", + autoFocus: false, + "onUpdate:modelValue": (value: string) => (state.sql = value), + }), + h(EditorToolbar, { + activeTab: { + id: "tab-1", + title: "SQL", + connectionId: "conn-1", + database: "db", + sql: fullSql, + mode: "query", + isExecuting: false, + isCancelling: false, + isExplaining: false, + }, + activeConnection: connection, + executableSql: fullSql, + explainMode: "explain", + blockDangerousRedisCommands: false, + sqlKeywordCase: "preserve", + databaseRequiredSignal: 0, + autoCommit: true, + txnSessionId: undefined, + txnAutoRolledBack: false, + oracleTxnPossiblyDirty: false, + isOracleManualTransaction: false, + onExecutePointerDown: () => { + pendingSnapshot = editorRef.value?.captureExecutionSnapshot?.(); + }, + onToolbarExecute: (source: "pointer" | "keyboard") => { + executedSql = source === "pointer" && pendingSnapshot ? resolveExecutableSql(pendingSnapshot.fullSql, pendingSnapshot.selectedSql, { mode: "current", cursorPos: pendingSnapshot.cursorPos }) : fullSql; + }, + }), + ]); + }, + }); + app.use(pinia); + app.use(i18n); + app.mount(host); + await vi.waitFor(() => expect(host.querySelector(".cm-editor")).not.toBeNull(), { timeout: 5000 }); + + const view = EditorView.findFromDOM(host.querySelector(".cm-editor") as HTMLElement)!; + view.dispatch({ selection: { anchor: selectionFrom, head: fullSql.length } }); + expect(view.state.sliceDoc(view.state.selection.main.from, view.state.selection.main.to)).toBe(selectedSql); + + const runButton = host.querySelector(".app-editor-toolbar button"); + runButton?.dispatchEvent(new MouseEvent("mousedown", { button: 0, bubbles: true, cancelable: true })); + runButton?.dispatchEvent(new MouseEvent("click", { detail: 1, bubbles: true })); + await nextTick(); + + expect(pendingSnapshot?.selectedSql).toBe(selectedSql); + expect(executedSql).toBe(selectedSql); + + app.unmount(); + host.remove(); + }); + it("does not expose SQL formatting for Redis connections", async () => { const connectionStore = useConnectionStore(); connectionStore.connections = [ diff --git a/apps/desktop/src/components/layout/__tests__/SqlEditorWorkspace.mount.spec.ts b/apps/desktop/src/components/layout/__tests__/SqlEditorWorkspace.mount.spec.ts index 47c2fac06b..3f91fd7570 100644 --- a/apps/desktop/src/components/layout/__tests__/SqlEditorWorkspace.mount.spec.ts +++ b/apps/desktop/src/components/layout/__tests__/SqlEditorWorkspace.mount.spec.ts @@ -21,6 +21,7 @@ const groupHandleModRCalls = vi.hoisted(() => [] as Element[]); const groupFocusSearchCalls = vi.hoisted(() => [] as Array); const resultHandleModRCalls = vi.hoisted(() => [] as Element[]); const resultFocusSearchCalls = vi.hoisted(() => [] as boolean[]); +const groupExecutionCalls = vi.hoisted(() => ({ capture: [] as string[], request: [] as string[] })); vi.mock("@/components/layout/EditorGroup.vue", () => ({ default: { @@ -44,6 +45,14 @@ vi.mock("@/components/layout/EditorGroup.vue", () => ({ groupFocusSearchCalls.push(target ?? null); return true; }, + captureQueryEditorExecutionSnapshot(this: { groupId: string }) { + groupExecutionCalls.capture.push(this.groupId); + return { fullSql: `SELECT ${this.groupId}`, selectedSql: `SELECT ${this.groupId}`, cursorPos: 0, selectionFrom: 0, selectionTo: 13 }; + }, + requestQueryEditorExecute(this: { groupId: string }) { + groupExecutionCalls.request.push(this.groupId); + return true; + }, }, template: `
`, }, @@ -149,6 +158,8 @@ describe("SqlEditorWorkspace mount contract", () => { groupFocusSearchCalls.length = 0; resultHandleModRCalls.length = 0; resultFocusSearchCalls.length = 0; + groupExecutionCalls.capture.length = 0; + groupExecutionCalls.request.length = 0; pinia = createPinia(); setActivePinia(pinia); i18n = createI18n({ @@ -158,6 +169,47 @@ describe("SqlEditorWorkspace mount contract", () => { }); }); + it("routes toolbar execution to the requested tab's editor group", async () => { + const store = useQueryStore(); + const tabs = [tab("tab-a"), tab("tab-b")]; + store.tabs = tabs; + store.activeTabId = "tab-a"; + store.groups = [ + { id: "g1", tabIds: ["tab-a"], activeTabId: "tab-a" }, + { id: "g2", tabIds: ["tab-b"], activeTabId: "tab-b" }, + ]; + store.focusedGroupId = "g1"; + store.sizes = [50, 50]; + const host = createHost(); + const app = createApp(SqlEditorWorkspace, { + activeTab: tabs[0]!, + activeConnection: undefined, + executableSql: "SELECT 1", + activeOutputView: "result", + formatSqlRequest: null, + compressSqlRequest: null, + selectedSql: "", + cursorPos: 0, + blockDangerousRedisCommands: false, + }); + app.use(pinia); + app.use(i18n); + const vm = app.mount(host) as unknown as { + captureQueryEditorExecutionSnapshot: (tabId?: string) => { selectedSql: string } | undefined; + requestQueryEditorExecute: (tabId?: string) => boolean; + }; + await nextTick(); + + const snapshot = vm.captureQueryEditorExecutionSnapshot("tab-b"); + expect(snapshot?.selectedSql).toBe("SELECT g2"); + expect(groupExecutionCalls.capture).toEqual(["g2"]); + expect(vm.requestQueryEditorExecute("tab-b")).toBe(true); + expect(groupExecutionCalls.request).toEqual(["g2"]); + + app.unmount(); + host.remove(); + }); + it("gives an idle query the full editor pane without a result surface or re-show button", async () => { const { host, vm, cleanup } = mountOutputWorkspace([{ ...tab("idle"), result: undefined }]); await nextTick(); diff --git a/apps/desktop/src/components/layout/editorToolbarActions.ts b/apps/desktop/src/components/layout/editorToolbarActions.ts index d29e7c1c85..f883444bcd 100644 --- a/apps/desktop/src/components/layout/editorToolbarActions.ts +++ b/apps/desktop/src/components/layout/editorToolbarActions.ts @@ -26,8 +26,8 @@ export interface EditorToolbarActions { /** Highlights the database selector of the tab that needs a database choice. */ databaseRequiredSignalFor(tabId: string): number; /** Captures the acting editor's execution snapshot before a toolbar click. */ - captureExecutionSnapshot(): void; - toolbarExecute(source: "pointer" | "keyboard"): void; + captureExecutionSnapshot(tabId: string): void; + toolbarExecute(source: "pointer" | "keyboard", tabId: string): void; cancelExecution(tabId: string): void; explain(tabId: string): void; formatSql(tabId: string): void; diff --git a/apps/desktop/src/composables/__tests__/useSqlExecution.spec.ts b/apps/desktop/src/composables/__tests__/useSqlExecution.spec.ts index 87ce8f136c..d08cb2807c 100644 --- a/apps/desktop/src/composables/__tests__/useSqlExecution.spec.ts +++ b/apps/desktop/src/composables/__tests__/useSqlExecution.spec.ts @@ -10,6 +10,7 @@ import { useProductionSafetyStore } from "@/stores/productionSafetyStore"; import { createQueryEditorExecutionViewportOwnership } from "@/lib/editor/queryEditorExecutionViewport"; import * as objectMetadataCache from "@/lib/metadata/objectMetadataCache"; import type { ConnectionConfig, QueryTab } from "@/types/database"; +import type { SqlExecutionSnapshot } from "@/lib/sql/sqlExecutionTarget"; vi.mock("vue-i18n", () => ({ createI18n: () => ({ global: { locale: { value: "en" }, setLocaleMessage: vi.fn() } }), @@ -248,6 +249,49 @@ describe("useSqlExecution", () => { expect(executeCurrentSql).toHaveBeenCalledWith(selectedSql, { tabId: "tab-1", sourceOffset: selectionFrom }); }); + it("keeps a toolbar snapshot bound to its owning tab when focus changes", async () => { + const fullSql = "SELECT active_value;\nSELECT selected_value;"; + const selectedSql = "SELECT selected_value;"; + const selectionFrom = fullSql.indexOf(selectedSql); + const activeTabValue = { ...queryTab("app"), id: "tab-a", sql: "SELECT active_value;" }; + const executionTabValue = { ...queryTab("app"), id: "tab-b", sql: fullSql }; + const activeTab = ref(activeTabValue); + const activeConnection = ref(connection("mysql")); + const activeOutputView = ref<"result" | "summary" | "explain" | "chart">("result"); + const queryStore = useQueryStore(); + const connectionStore = useConnectionStore(); + connectionStore.connections = [connection("mysql")]; + queryStore.tabs = [activeTabValue, executionTabValue]; + queryStore.activeTabId = activeTabValue.id; + const executeCurrentSql = vi.spyOn(queryStore, "executeCurrentSql").mockImplementation(async () => undefined); + vi.spyOn(useHistoryStore(), "add").mockResolvedValue(undefined); + const resolveExecutableSql = vi.fn(async (snapshot: SqlExecutionSnapshot | undefined, tab?: QueryTab) => { + expect(tab?.id).toBe("tab-b"); + return snapshot?.selectedSql ?? ""; + }); + const execution = useSqlExecution({ + activeTab: computed(() => activeTab.value), + activeConnection: computed(() => activeConnection.value), + executableSql: computed(() => activeTab.value?.sql ?? ""), + resolveExecutableSql, + activeOutputView, + }); + + await execution.tryExecute( + { + fullSql, + selectedSql, + cursorPos: selectionFrom, + selectionFrom, + selectionTo: fullSql.length, + }, + { tabId: executionTabValue.id }, + ); + + expect(resolveExecutableSql).toHaveBeenCalledWith(expect.objectContaining({ selectedSql }), executionTabValue); + expect(executeCurrentSql).toHaveBeenCalledWith(selectedSql, { tabId: executionTabValue.id, sourceOffset: selectionFrom }); + }); + it("expands preceding @set values in a selected shell-style statement", async () => { const selectedSql = "SELECT * FROM patrol WHERE post_id = ${postid};"; const fullSql = ["-- saved query", "@set postid='224';", selectedSql, "@set postid = 'future';"].join("\n"); diff --git a/apps/desktop/src/lib/__tests__/editor/queryEditorExecution.spec.ts b/apps/desktop/src/lib/__tests__/editor/queryEditorExecution.spec.ts index 6af431aa14..d9d7da9f2a 100644 --- a/apps/desktop/src/lib/__tests__/editor/queryEditorExecution.spec.ts +++ b/apps/desktop/src/lib/__tests__/editor/queryEditorExecution.spec.ts @@ -58,17 +58,20 @@ describe("QueryEditor execution routing", () => { expect(contentAreaSource).toContain("function captureQueryEditorExecutionSnapshot()"); expect(contentAreaSource).toContain("queryEditorRef.value?.captureExecutionSnapshot();"); expect(appSource).toContain("const pendingToolbarExecutionSnapshot = ref();"); - expect(appSource).toContain("pendingToolbarExecutionSnapshot.value = snapshot ? { ...snapshot, tabId: activeTab.value?.id } : undefined;"); - expect(appSource).toContain('if (source === "pointer")'); + expect(appSource).toContain("contentAreaRef.value?.captureQueryEditorExecutionSnapshot?.(tabId)"); + expect(appSource).toContain("pendingToolbarExecutionSnapshot.value = snapshot ? { ...snapshot, tabId } : undefined;"); + expect(appSource).toContain('if (source === "pointer" && snapshot && snapshot.tabId === targetTabId)'); expect(appSource).toContain("pendingToolbarExecutionSnapshot.value = undefined;"); - expect(appSource).toContain("void tryExecute(snapshot, { tabId: snapshot.tabId ?? activeTab.value?.id });"); + expect(appSource).toContain("void tryExecute(snapshot, { tabId: targetTabId });"); expect(appSource).toContain('@execute="(tabId: string, override?: SqlExecutionOverride) => tryExecute(override, { tabId })"'); + expect(appSource).toContain("async function resolveActiveExecutableSql(snapshot?: SqlExecutionSnapshot, executionTab?: QueryTab)"); + expect(appSource).toContain("const connection = connectionStore.getConfig(tab.connectionId) ?? activeConnection.value;"); // Per-group toolbars call back into App-owned orchestration via injection. expect(appSource).toContain("provide(EDITOR_TOOLBAR_ACTIONS, {"); expect(appSource).toContain("captureExecutionSnapshot: captureActiveEditorExecutionSnapshot,"); expect(appSource).toContain("toolbarExecute: requestActiveEditorExecute,"); - expect(editorGroupSource).toContain('@execute-pointer-down="toolbar.captureExecutionSnapshot()"'); - expect(editorGroupSource).toContain('@toolbar-execute="toolbar.toolbarExecute($event)"'); + expect(editorGroupSource).toContain('@execute-pointer-down="toolbar.captureExecutionSnapshot(activeTab.id)"'); + expect(editorGroupSource).toContain('@toolbar-execute="toolbar.toolbarExecute($event, activeTab.id)"'); expect(editorToolbarSource).toContain("function onExecutePointerDown(event: MouseEvent)"); expect(editorToolbarSource).toContain('emit("toolbarExecute", event.detail > 0 ? "pointer" : "keyboard")'); expect(editorToolbarSource).not.toContain('emit("execute", event.detail > 0 ? "pointer" : "keyboard")');