fix(editor): execute manually selected sql from the toolbar
This commit is contained in:
1 parent
28206f2963
commit
1137cab7f4
9 files changed
+310
-33
No files matched your search
@@ -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)"
|
||||
|
||||
@@ -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<typeof EditorGroup> | 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<typeof EditorGroup> | null {
|
||||
const groupElement = element?.closest<HTMLElement>("[data-group-id]");
|
||||
const groupId = groupElement?.dataset.groupId;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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<HTMLElement>(".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 = [
|
||||
|
||||
+52
@@ -21,6 +21,7 @@ const groupHandleModRCalls = vi.hoisted(() => [] as Element[]);
|
||||
const groupFocusSearchCalls = vi.hoisted(() => [] as Array<Element | null>);
|
||||
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: `<div data-test="editor-group" :data-group-id="groupId" :data-tab-ids="tabIds.join(',')" :data-active-tab-id="activeTabId"><button data-test="emit-execute" @click="$emit('execute', { fullSql: 'SELECT 1', selectedSql: 'SELECT 1', cursorPos: 0, selectionFrom: 0, selectionTo: 8 })">execute</button></div>`,
|
||||
},
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in new issue
Block a user