fix(editor): refresh context actions on first right-click

Closes #6811
This commit is contained in:
t8y2 committed 2026-08-20 14:38:00 +00:00
1 parent 5767a0fd59
commit 90a18a7b23
2 files changed
+105 -1

No files matched your search

@@ -1853,6 +1853,10 @@ const contextMenuItems = computed<ContextMenuItem[]>(() => {
]; ];
}); });
function currentContextMenuItems(): ContextMenuItem[] {
return contextMenuItems.value;
}
function handleSqlIntentionActions(currentView: EditorViewType): boolean { function handleSqlIntentionActions(currentView: EditorViewType): boolean {
if (props.readOnly) return false; if (props.readOnly) return false;
try { try {
@@ -5838,7 +5842,7 @@ defineExpose({
<template> <template>
<div class="h-full w-full overflow-hidden relative" @gesturestart="onEditorGestureStart" @gesturechange="onEditorGestureChange" @gestureend="onEditorGestureEnd"> <div class="h-full w-full overflow-hidden relative" @gesturestart="onEditorGestureStart" @gesturechange="onEditorGestureChange" @gestureend="onEditorGestureEnd">
<CustomContextMenu :items="contextMenuItems" @close="contextMenuOpen = false" v-slot="{ onContextMenu }"> <CustomContextMenu :items="currentContextMenuItems" @close="contextMenuOpen = false" v-slot="{ onContextMenu }">
<div <div
ref="editorRef" ref="editorRef"
data-query-editor-root data-query-editor-root
@@ -0,0 +1,100 @@
// @vitest-environment happy-dom
import { readFileSync } from "node:fs";
import path from "node:path";
import { computed, createApp, defineComponent, h, nextTick, ref } from "vue";
import { describe, expect, it } from "vitest";
import CustomContextMenu, { type ContextMenuItem } from "@/components/ui/CustomContextMenu.vue";
const source = readFileSync(path.resolve(process.cwd(), "apps/desktop/src/components/editor/QueryEditor.vue"), "utf8");
describe("QueryEditor context menu lifecycle", () => {
it("resolves menu items after synchronizing the right-click target", () => {
const syncStart = source.indexOf("function syncContextMenuStateAtEvent");
const syncEnd = source.indexOf("\n}", syncStart);
const syncSource = source.slice(syncStart, syncEnd);
const syncIndex = source.indexOf("syncContextMenuStateAtEvent(view, e);");
const openIndex = source.indexOf("onContextMenu(e);", syncIndex);
const getterStart = source.indexOf("function currentContextMenuItems()");
const getterEnd = source.indexOf("\n}", getterStart);
const getterSource = source.slice(getterStart, getterEnd);
expect(syncStart).toBeGreaterThanOrEqual(0);
expect(syncEnd).toBeGreaterThan(syncStart);
expect(syncSource).toContain("if (pos == null)");
expect(syncSource).toContain("contextObjectTarget.value = null;");
expect(syncIndex).toBeGreaterThanOrEqual(0);
expect(openIndex).toBeGreaterThan(syncIndex);
expect(getterStart).toBeGreaterThanOrEqual(0);
expect(getterEnd).toBeGreaterThan(getterStart);
expect(getterSource).toContain("return contextMenuItems.value;");
expect(getterSource).not.toContain("nextTick");
expect(getterSource).not.toContain("setTimeout");
expect(source).toContain(':items="currentContextMenuItems"');
expect(source).not.toContain('<CustomContextMenu :items="contextMenuItems"');
});
it("uses the target synchronized in the current context-menu event", async () => {
const target = ref<string | null>(null);
const openedTargets: string[] = [];
const items = computed<ContextMenuItem[]>(() => [
{
label: target.value ? `Inspect ${target.value}` : "Inspect",
disabled: !target.value,
action: () => {
if (target.value) openedTargets.push(target.value);
},
},
]);
const root = defineComponent({
setup() {
const currentItems = () => items.value;
const contextTarget = (id: string, value: string | null) =>
h(
"div",
{
id,
onContextmenu: (event: MouseEvent) => {
target.value = value;
onContextMenu(event);
},
},
id,
);
let onContextMenu = (_event: MouseEvent) => {};
return () =>
h(
CustomContextMenu,
{ items: currentItems },
{
default: (slot: { onContextMenu: (event: MouseEvent) => void }) => {
onContextMenu = slot.onContextMenu;
return [contextTarget("table-a", "table_a"), contextTarget("table-b", "table_b"), contextTarget("empty", null)];
},
},
);
},
});
const container = document.createElement("div");
document.body.append(container);
const app = createApp(root);
app.mount(container);
const open = async (id: string) => {
container.querySelector(`#${id}`)?.dispatchEvent(new MouseEvent("contextmenu", { bubbles: true, cancelable: true }));
await nextTick();
return document.querySelector<HTMLButtonElement>("[data-dbx-context-menu] button");
};
expect(await open("table-a")).toMatchObject({ disabled: false, textContent: "Inspect table_a" });
expect(await open("table-a")).toMatchObject({ disabled: false, textContent: "Inspect table_a" });
const tableBItem = await open("table-b");
expect(tableBItem).toMatchObject({ disabled: false, textContent: "Inspect table_b" });
tableBItem?.click();
expect(openedTargets).toEqual(["table_b"]);
expect(await open("empty")).toMatchObject({ disabled: true, textContent: "Inspect" });
app.unmount();
container.remove();
});
});