fix(sidebar): focus object search after opening a database

This commit is contained in:
zipg authored and GitHub committed 2026-09-17 14:32:19 +08:00
1 parent 8567e50887
commit 90ad4e62e6
6 files changed
+117 -10

No files matched your search

@@ -1,5 +1,5 @@
<script setup lang="ts">
import { computed, onBeforeUnmount, ref, watch } from "vue";
import { computed, nextTick, onBeforeUnmount, ref, watch } from "vue";
import { ArrowDown, ArrowUp, Check, Database, GripVertical, LayoutGrid, List, Loader2, RefreshCw, Search, X } from "@lucide/vue";
import { useI18n } from "vue-i18n";
import { Button } from "@/components/ui/button";
@@ -10,6 +10,7 @@ import { useToolbarOverflow } from "@/composables/useToolbarOverflow";
import * as api from "@/lib/backend/api";
import { filterDatabaseNamesForConnection } from "@/lib/database/visibleDatabases";
import { formatObjectBrowserBytes, formatObjectBrowserTimestamp } from "@/lib/table/objectBrowserRows";
import { requestObjectBrowserSearchFocus } from "@/lib/tabs/objectBrowserSearchFocus";
import { useTabUiState } from "@/lib/tabs/tabUiState";
import { useQueryStore } from "@/stores/queryStore";
import { useSettingsStore } from "@/stores/settingsStore";
@@ -112,7 +113,8 @@ const visibleRows = computed(() => {
});
function openDatabase(database: string) {
queryStore.openObjectBrowser(props.connection.id, database);
const tabId = queryStore.openObjectBrowser(props.connection.id, database);
void nextTick(() => requestObjectBrowserSearchFocus(tabId));
}
function columnLabel(key: DatabaseBrowserColumnKey): string {
@@ -86,6 +86,7 @@ import { supportsSidebarObjectNameFilter } from "@/lib/sidebar/sidebarObjectName
import { connectionGroupDestinationRows } from "@/lib/sidebar/sidebarLayout";
import { objectTypesForGroupNode } from "@/lib/table/tableTree";
import { loadSidebarObjectGroup } from "@/lib/sidebar/sidebarObjectGroupRouting";
import { requestObjectBrowserSearchFocus } from "@/lib/tabs/objectBrowserSearchFocus";
import { isXuguTypeMemberContainer } from "@/lib/sidebar/xuguTypeMembers";
import { isXuguSyntheticTreeNode } from "@/lib/sidebar/xuguPublicSynonyms";
import { buildXuguSchedulerJobSql, type XuguSchedulerJobAction } from "@/lib/database/xuguSchedulerJobSql";
@@ -1112,9 +1113,9 @@ function runRowClickAction(clickDetail: number, requestId: number) {
if (action === "open-data") {
scheduleOpenData(node);
} else if (action === "open-object-browser") {
void openObjectBrowser();
void openObjectBrowser(false, false, true);
} else if (action === "open-object-browser-and-expand") {
void openObjectBrowser();
void openObjectBrowser(false, false, true);
if (!node.isExpanded) void toggle();
} else if (action === "open-source") {
openObjectSourceDialog(false);
@@ -1500,9 +1501,9 @@ function onDoubleClick(event: MouseEvent) {
if (action === "open-database-browser") {
void openDatabaseBrowser();
} else if (action === "open-object-browser") {
void openObjectBrowser();
void openObjectBrowser(false, false, true);
} else if (action === "open-object-browser-and-expand") {
void openObjectBrowser();
void openObjectBrowser(false, false, true);
if (!activeNode.value.isExpanded) void toggle();
} else if (action === "open-data") {
openDataImmediately(activeNode.value);
@@ -1649,7 +1650,7 @@ async function confirmDeleteSavedSqlFile() {
releaseActiveNodeReference([node.id]);
}
async function openObjectBrowser(eventReadOnly = false, openEventEditor: boolean | "create" = false) {
async function openObjectBrowser(eventReadOnly = false, openEventEditor: boolean | "create" = false, focusSearch = false) {
const node = activeNode.value;
if (!node.connectionId) return;
try {
@@ -1667,13 +1668,15 @@ async function openObjectBrowser(eventReadOnly = false, openEventEditor: boolean
return;
}
if (hasTreeNodeDatabaseContext(node)) {
queryStore.openObjectBrowser(node.connectionId, node.database, node.schema, node.catalog, eventName, eventReadOnly, objectFilter, eventCreateRequestId);
const tabId = queryStore.openObjectBrowser(node.connectionId, node.database, node.schema, node.catalog, eventName, eventReadOnly, objectFilter, eventCreateRequestId);
if (focusSearch) await nextTick(() => requestObjectBrowserSearchFocus(tabId));
return;
}
const options = await getDatabaseOptions(node.connectionId);
const database = resolveDefaultDatabase(connection, options);
if (database) {
queryStore.openObjectBrowser(node.connectionId, database, undefined, undefined, eventName, eventReadOnly, objectFilter, eventCreateRequestId);
const tabId = queryStore.openObjectBrowser(node.connectionId, database, undefined, undefined, eventName, eventReadOnly, objectFilter, eventCreateRequestId);
if (focusSearch) await nextTick(() => requestObjectBrowserSearchFocus(tabId));
} else {
await toggle();
}
@@ -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 { OBJECT_BROWSER_SEARCH_FOCUS_EVENT } from "@/lib/tabs/objectBrowserSearchFocus";
import SidebarTreeRuntimeHost from "@/components/sidebar/SidebarTreeRuntimeHost.vue";
const connectionStore = {
@@ -36,6 +37,15 @@ const queryStore = {
updateSql: vi.fn(),
setTableMeta: vi.fn(),
openNacosAdmin: vi.fn(),
openObjectBrowser: vi.fn(() => "object-browser-tab"),
};
const settingsStore = {
editorSettings: {
sidebarActivation: "single" as "single" | "double",
sidebarBrowseObjectsOnDatabaseActivation: false,
shortcuts: { openDataInNewTab: "" },
},
};
vi.mock("@/stores/connectionStore", () => ({
@@ -44,7 +54,7 @@ vi.mock("@/stores/connectionStore", () => ({
}));
vi.mock("@/stores/queryStore", () => ({ useQueryStore: () => queryStore }));
vi.mock("@/stores/settingsStore", () => ({ useSettingsStore: () => ({ editorSettings: { sidebarActivation: "single" } }) }));
vi.mock("@/stores/settingsStore", () => ({ useSettingsStore: () => settingsStore }));
vi.mock("@/stores/savedSqlStore", () => ({ useSavedSqlStore: () => ({}) }));
vi.mock("@/composables/useToast", () => ({ useToast: () => ({ toast: vi.fn() }) }));
vi.mock("@/composables/useSqlHighlighter", () => ({ useSqlHighlighter: () => ({ highlight: vi.fn() }) }));
@@ -66,6 +76,8 @@ afterEach(() => {
connectionStore.activeConnectionId = null;
connectionStore.connectedIds.clear();
queryStore.tabs = [];
settingsStore.editorSettings.sidebarActivation = "single";
settingsStore.editorSettings.sidebarBrowseObjectsOnDatabaseActivation = false;
vi.clearAllMocks();
connectionStore.canUseLoadedTreeNodeToggle.mockReturnValue(true);
connectionStore.getConfig.mockReturnValue({ db_type: "mysql", name: "connection" });
@@ -73,6 +85,42 @@ afterEach(() => {
});
describe("SidebarTreeRuntimeHost expansion", () => {
it.each(["single", "double"] as const)("focuses object search when %s-click activation opens a database", async (activation) => {
const database: TreeNode = {
id: "mysql:dbx_test",
label: "dbx_test",
type: "database",
connectionId: "mysql",
database: "dbx_test",
isExpanded: true,
children: [],
};
settingsStore.editorSettings.sidebarActivation = activation;
settingsStore.editorSettings.sidebarBrowseObjectsOnDatabaseActivation = true;
const focusRequested = vi.fn();
window.addEventListener(OBJECT_BROWSER_SEARCH_FOCUS_EVENT, focusRequested);
const host = ref<InstanceType<typeof SidebarTreeRuntimeHost> | null>(null);
const app = createApp(defineComponent({ setup: () => () => h(SidebarTreeRuntimeHost, { ref: host, node: database, depth: 0 }) }));
mountedApps.push(app);
const container = document.createElement("div");
document.body.append(container);
app.use(i18n);
app.mount(container);
if (activation === "single") {
host.value?.handleRowClick(database, 1);
} else {
host.value?.handleRowDoubleClick(database, new MouseEvent("dblclick"));
}
await vi.waitFor(() => expect(queryStore.openObjectBrowser).toHaveBeenCalledWith("mysql", "dbx_test", undefined, undefined, undefined, false, undefined, undefined));
await vi.waitFor(() => expect(focusRequested).toHaveBeenCalledOnce());
expect((focusRequested.mock.calls[0]?.[0] as CustomEvent<{ tabId: string }>).detail.tabId).toBe("object-browser-tab");
window.removeEventListener(OBJECT_BROWSER_SEARCH_FOCUS_EVENT, focusRequested);
});
it("reuses a Mongo collection tab by identity without replacing its state", async () => {
const collection: TreeNode = {
id: "mongo:app:orders",