mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-21 05:20:02 +00:00
@@ -1126,27 +1126,32 @@ async function toggle(requestId = beginNavigationRequest()) {
|
||||
}
|
||||
}
|
||||
|
||||
function runRowClickAction(clickDetail: number, requestId: number) {
|
||||
function runRowClickAction(clickDetail: number) {
|
||||
const node = activeNode.value;
|
||||
if (node.type === "load-more") {
|
||||
if (clickDetail > 1) return;
|
||||
beginNavigationRequest();
|
||||
void loadMoreObjectGroupChildren();
|
||||
return;
|
||||
}
|
||||
if (node.type === "object-browser") {
|
||||
if (clickDetail > 1) return;
|
||||
beginNavigationRequest();
|
||||
void openObjectBrowser();
|
||||
return;
|
||||
}
|
||||
if (node.type === "mongo-gridfs") {
|
||||
beginNavigationRequest();
|
||||
openMongoTreeData(node);
|
||||
return;
|
||||
}
|
||||
if (node.type === "event") {
|
||||
beginNavigationRequest();
|
||||
void openObjectBrowser(false, true);
|
||||
return;
|
||||
}
|
||||
if (node.type === "group-events") {
|
||||
beginNavigationRequest();
|
||||
void openObjectBrowser();
|
||||
return;
|
||||
}
|
||||
@@ -1155,13 +1160,17 @@ function runRowClickAction(clickDetail: number, requestId: number) {
|
||||
// between adjacent rows. Nacos entries are idempotent navigation targets, so
|
||||
// do not mistake that rapid one-click switching for a double-click toggle.
|
||||
if (!shouldRunTreeNodeRowAction(action, clickDetail, isGroupLabel(node) || isRepeatableNavigationTreeNode(node.type))) return;
|
||||
// A double click emits click(detail=2) before dblclick. Claim ownership only
|
||||
// after that follow-up click has resolved to a real action, otherwise it
|
||||
// makes the first click's pending connection/expansion request look stale.
|
||||
const requestId = beginNavigationRequest();
|
||||
if (action === "open-data") {
|
||||
scheduleOpenData(node);
|
||||
} else if (action === "open-object-browser") {
|
||||
void openObjectBrowser(false, false, true);
|
||||
} else if (action === "open-object-browser-and-expand") {
|
||||
void openObjectBrowser(false, false, true);
|
||||
if (!node.isExpanded) void toggle();
|
||||
if (!node.isExpanded) void toggle(requestId);
|
||||
} else if (action === "open-source") {
|
||||
openObjectSourceDialog(false);
|
||||
} else if (action === "open-extension-details") {
|
||||
@@ -1551,21 +1560,27 @@ function requestDeleteSelectedNode(): boolean {
|
||||
function onDoubleClick(event: MouseEvent) {
|
||||
if (dataTabOpenModeFromTreeClick(activeNode.value.type, event, settingsStore.editorSettings.shortcuts.openDataInNewTab) === "new-tab") return;
|
||||
if (activeNode.value.type === "event") {
|
||||
beginNavigationRequest();
|
||||
void openObjectBrowser(false, true);
|
||||
return;
|
||||
}
|
||||
if (activeNode.value.type === "group-events") {
|
||||
beginNavigationRequest();
|
||||
void openObjectBrowser();
|
||||
return;
|
||||
}
|
||||
const action = treeNodeRowDoubleClickAction(activeNode.value.type, canOpenObjectBrowser.value, settingsStore.editorSettings.sidebarActivation, canExpand.value, currentDatabaseType(), canOpenConnectionDatabaseBrowser.value, settingsStore.editorSettings.sidebarBrowseObjectsOnDatabaseActivation);
|
||||
// In single-click mode the trailing dblclick normally has no action and must
|
||||
// leave the first click's request ownership intact.
|
||||
if (action === "none") return;
|
||||
const requestId = beginNavigationRequest();
|
||||
if (action === "open-database-browser") {
|
||||
void openDatabaseBrowser();
|
||||
} else if (action === "open-object-browser") {
|
||||
void openObjectBrowser(false, false, true);
|
||||
} else if (action === "open-object-browser-and-expand") {
|
||||
void openObjectBrowser(false, false, true);
|
||||
if (!activeNode.value.isExpanded) void toggle();
|
||||
if (!activeNode.value.isExpanded) void toggle(requestId);
|
||||
} else if (action === "open-data") {
|
||||
openDataImmediately(activeNode.value);
|
||||
} else if (action === "activate-data") {
|
||||
@@ -1581,7 +1596,7 @@ function onDoubleClick(event: MouseEvent) {
|
||||
} else if (action === "toggle" && (activeNode.value.type === "mongo-gridfs" || isDocumentBrowserTreeNode(activeNode.value.type))) {
|
||||
openMongoTreeData(activeNode.value);
|
||||
} else if (action === "toggle") {
|
||||
toggle();
|
||||
void toggle(requestId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6891,9 +6906,8 @@ function buildContextMenu(node: TreeNode): ContextMenuItem[] {
|
||||
}
|
||||
|
||||
function handleRowClick(node: TreeNode, clickDetail: number) {
|
||||
const requestId = beginNavigationRequest();
|
||||
activateRuntimeNode(node);
|
||||
runRowClickAction(clickDetail, requestId);
|
||||
runRowClickAction(clickDetail);
|
||||
}
|
||||
|
||||
function handleRowDoubleClick(node: TreeNode, event: MouseEvent) {
|
||||
@@ -6905,7 +6919,6 @@ function handleRowDoubleClick(node: TreeNode, event: MouseEvent) {
|
||||
activateRuntimeNode(node);
|
||||
return;
|
||||
}
|
||||
beginNavigationRequest();
|
||||
activateRuntimeNode(node);
|
||||
onDoubleClick(event);
|
||||
}
|
||||
|
||||
+77
-2
@@ -6,6 +6,7 @@ import i18n from "@/i18n";
|
||||
import type { TreeNode } from "@/types/database";
|
||||
import { syncSidebarTreeNodeExpansion } from "@/lib/sidebar/sidebarTreeExpansion";
|
||||
import { sidebarTreeContextKey } from "@/lib/sidebar/sidebarTreeContext";
|
||||
import { filterSidebarTree } from "@/lib/sidebar/sidebarSearchTree";
|
||||
import { OBJECT_BROWSER_SEARCH_FOCUS_EVENT } from "@/lib/tabs/objectBrowserSearchFocus";
|
||||
import SidebarTreeRuntimeHost from "@/components/sidebar/SidebarTreeRuntimeHost.vue";
|
||||
|
||||
@@ -25,6 +26,7 @@ const connectionStore = {
|
||||
loadObjectGroupChildren: vi.fn(async (node: TreeNode) => {
|
||||
node.isExpanded = true;
|
||||
}),
|
||||
loadTables: vi.fn(async () => undefined),
|
||||
loadXuguTablespaces: vi.fn(async (node: TreeNode) => {
|
||||
node.isExpanded = true;
|
||||
}),
|
||||
@@ -40,6 +42,8 @@ const queryStore = {
|
||||
openObjectBrowser: vi.fn(() => "object-browser-tab"),
|
||||
};
|
||||
|
||||
const toast = vi.fn();
|
||||
|
||||
const settingsStore = {
|
||||
editorSettings: {
|
||||
sidebarActivation: "single" as "single" | "double",
|
||||
@@ -49,14 +53,14 @@ const settingsStore = {
|
||||
};
|
||||
|
||||
vi.mock("@/stores/connectionStore", () => ({
|
||||
CONNECTION_ATTEMPT_CANCELLED_MESSAGE: "connection attempt cancelled",
|
||||
CONNECTION_ATTEMPT_CANCELLED_MESSAGE: "Connection attempt was cancelled",
|
||||
useConnectionStore: () => connectionStore,
|
||||
}));
|
||||
|
||||
vi.mock("@/stores/queryStore", () => ({ useQueryStore: () => queryStore }));
|
||||
vi.mock("@/stores/settingsStore", () => ({ useSettingsStore: () => settingsStore }));
|
||||
vi.mock("@/stores/savedSqlStore", () => ({ useSavedSqlStore: () => ({}) }));
|
||||
vi.mock("@/composables/useToast", () => ({ useToast: () => ({ toast: vi.fn() }) }));
|
||||
vi.mock("@/composables/useToast", () => ({ useToast: () => ({ toast }) }));
|
||||
vi.mock("@/composables/useSqlHighlighter", () => ({ useSqlHighlighter: () => ({ highlight: vi.fn() }) }));
|
||||
vi.mock("@/composables/useSidebarDataOpenRuntime", () => ({ useSidebarDataOpenRuntime: () => ({ openData: vi.fn() }) }));
|
||||
vi.mock("@/composables/useDatabaseOptions", () => ({ useDatabaseOptions: () => ({ getDatabaseOptions: vi.fn() }) }));
|
||||
@@ -82,6 +86,7 @@ afterEach(() => {
|
||||
connectionStore.canUseLoadedTreeNodeToggle.mockReturnValue(true);
|
||||
connectionStore.getConfig.mockReturnValue({ db_type: "mysql", name: "connection" });
|
||||
connectionStore.getEtcdAccessCapabilities.mockReturnValue({ admin: true, writable: true, writePermissions: null });
|
||||
connectionStore.loadTables.mockResolvedValue(undefined);
|
||||
});
|
||||
|
||||
describe("SidebarTreeRuntimeHost expansion", () => {
|
||||
@@ -151,6 +156,76 @@ describe("SidebarTreeRuntimeHost expansion", () => {
|
||||
expect(queryStore.setTableMeta).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("keeps a searched database browse-and-expand activation current through the complete double-click sequence", async () => {
|
||||
let resolveConnection!: () => void;
|
||||
const connection = new Promise<void>((resolve) => {
|
||||
resolveConnection = resolve;
|
||||
});
|
||||
connectionStore.ensureConnected.mockReturnValue(connection);
|
||||
settingsStore.editorSettings.sidebarBrowseObjectsOnDatabaseActivation = true;
|
||||
|
||||
const database: TreeNode = {
|
||||
id: "mysql:inventory",
|
||||
label: "inventory",
|
||||
type: "database",
|
||||
connectionId: "mysql",
|
||||
database: "inventory",
|
||||
isExpanded: false,
|
||||
children: [],
|
||||
};
|
||||
const root: TreeNode = {
|
||||
id: "mysql",
|
||||
label: "local-mysql",
|
||||
type: "connection",
|
||||
connectionId: "mysql",
|
||||
isExpanded: true,
|
||||
children: [database],
|
||||
};
|
||||
connectionStore.treeNodes = [root];
|
||||
const searchedDatabase = filterSidebarTree([root], "inventory", new Set())[0]?.children?.[0];
|
||||
expect(searchedDatabase).toBeDefined();
|
||||
expect(searchedDatabase).not.toBe(database);
|
||||
|
||||
const cancellationHandled = vi.fn();
|
||||
const cancellation = {
|
||||
get message() {
|
||||
cancellationHandled();
|
||||
return "Connection attempt was cancelled";
|
||||
},
|
||||
};
|
||||
connectionStore.loadTables.mockImplementationOnce(async () => {
|
||||
await connectionStore.ensureConnected("mysql");
|
||||
throw cancellation;
|
||||
});
|
||||
|
||||
const host = ref<InstanceType<typeof SidebarTreeRuntimeHost> | null>(null);
|
||||
const app = createApp(defineComponent({ setup: () => () => h(SidebarTreeRuntimeHost, { ref: host, node: searchedDatabase!, depth: 1 }) }));
|
||||
mountedApps.push(app);
|
||||
const container = document.createElement("div");
|
||||
document.body.append(container);
|
||||
app.use(i18n);
|
||||
app.mount(container);
|
||||
await nextTick();
|
||||
|
||||
// A browser double click emits click(1), click(2), then dblclick. Only the
|
||||
// first event activates a database in single-click mode.
|
||||
host.value!.handleRowClick(searchedDatabase!, 1);
|
||||
await vi.waitFor(() => expect(connectionStore.ensureConnected).toHaveBeenCalledOnce());
|
||||
host.value!.handleRowClick(searchedDatabase!, 2);
|
||||
host.value!.handleRowDoubleClick(searchedDatabase!, new MouseEvent("dblclick", { detail: 2 }));
|
||||
|
||||
expect(connectionStore.loadTables).toHaveBeenCalledOnce();
|
||||
expect(connectionStore.ensureConnected).toHaveBeenCalledTimes(2);
|
||||
|
||||
resolveConnection();
|
||||
// Reading the cancellation proves the original request still owns its
|
||||
// completion; a stale request returns before classifying the failure.
|
||||
await vi.waitFor(() => expect(cancellationHandled).toHaveBeenCalledOnce());
|
||||
await vi.waitFor(() => expect(queryStore.openObjectBrowser).toHaveBeenCalledOnce());
|
||||
expect(queryStore.openObjectBrowser).toHaveBeenCalledWith("mysql", "inventory", undefined, undefined, undefined, false, undefined, undefined);
|
||||
expect(toast).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("activates the owning connection when a cached node toggles locally", async () => {
|
||||
const group: TreeNode = {
|
||||
id: "mysql:basic:__tables",
|
||||
|
||||
Reference in New Issue
Block a user