fix(sidebar): focus object search after opening a database
This commit is contained in:
6 files changed
+117
-10
No files matched your search
@@ -53,6 +53,7 @@ import { translateBackendError } from "@/i18n/backend-errors";
|
||||
import * as api from "@/lib/backend/api";
|
||||
import { connectionRedactedNameLabel } from "@/lib/connection/connectionPresentation";
|
||||
import { quickConnectionOpenTarget } from "@/lib/connection/connectionOpenTarget";
|
||||
import { OBJECT_BROWSER_SEARCH_FOCUS_EVENT, objectBrowserSearchFocusTabId } from "@/lib/tabs/objectBrowserSearchFocus";
|
||||
import { parseRecentConnectionIds, rankRecentConnections, RECENT_CONNECTION_IDS_STORAGE_KEY, recordRecentConnection } from "@/lib/connection/recentConnections";
|
||||
import { resolveDefaultDatabase } from "@/lib/database/defaultDatabase";
|
||||
import { normalizeSqliteNamespace } from "@/lib/database/sqliteNamespace";
|
||||
@@ -1068,6 +1069,19 @@ function activateQuerySurface() {
|
||||
activateMainContentSurface("query");
|
||||
}
|
||||
|
||||
async function focusRequestedObjectBrowserSearch(event: Event) {
|
||||
const tabId = objectBrowserSearchFocusTabId(event);
|
||||
if (!tabId) return;
|
||||
await nextTick();
|
||||
let remainingFrames = 8;
|
||||
const focusWhenReady = () => {
|
||||
if (queryStore.activeTabId !== tabId || contentAreaRef.value?.focusSearch()) return;
|
||||
remainingFrames -= 1;
|
||||
if (remainingFrames > 0) window.requestAnimationFrame(focusWhenReady);
|
||||
};
|
||||
focusWhenReady();
|
||||
}
|
||||
|
||||
function activateOpenSpecialPageFallback() {
|
||||
if (settingsPageTabOpen.value) {
|
||||
activateMainContentSurface("settings");
|
||||
@@ -3625,6 +3639,7 @@ onMounted(async () => {
|
||||
document.addEventListener("visibilitychange", handleTabSwitcherVisibilityChange);
|
||||
window.addEventListener("dbx-open-driver-store", openDriverStoreFromEvent);
|
||||
window.addEventListener("dbx:activate-query-surface", activateQuerySurface);
|
||||
window.addEventListener(OBJECT_BROWSER_SEARCH_FOCUS_EVENT, focusRequestedObjectBrowserSearch);
|
||||
window.addEventListener("dbx-mcp-status-changed", handleMcpStatusChanged);
|
||||
window.addEventListener("dbx:ai-run-notify", handleAiRunNotify);
|
||||
if (isDesktop) {
|
||||
@@ -3708,6 +3723,7 @@ onUnmounted(() => {
|
||||
tabSwitcherKeyboard.reset();
|
||||
window.removeEventListener("dbx-open-driver-store", openDriverStoreFromEvent);
|
||||
window.removeEventListener("dbx:activate-query-surface", activateQuerySurface);
|
||||
window.removeEventListener(OBJECT_BROWSER_SEARCH_FOCUS_EVENT, focusRequestedObjectBrowserSearch);
|
||||
window.removeEventListener("dbx-mcp-status-changed", handleMcpStatusChanged);
|
||||
window.removeEventListener("dbx:ai-run-notify", handleAiRunNotify);
|
||||
document.removeEventListener("contextmenu", handleContextMenu);
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
+49
-1
@@ -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",
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
// @vitest-environment happy-dom
|
||||
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { OBJECT_BROWSER_SEARCH_FOCUS_EVENT, objectBrowserSearchFocusTabId, requestObjectBrowserSearchFocus } from "@/lib/tabs/objectBrowserSearchFocus";
|
||||
|
||||
describe("object browser search focus requests", () => {
|
||||
it("dispatches the target tab id", () => {
|
||||
const listener = vi.fn();
|
||||
window.addEventListener(OBJECT_BROWSER_SEARCH_FOCUS_EVENT, listener);
|
||||
|
||||
requestObjectBrowserSearchFocus("objects-1");
|
||||
|
||||
expect(listener).toHaveBeenCalledOnce();
|
||||
expect(objectBrowserSearchFocusTabId(listener.mock.calls[0][0])).toBe("objects-1");
|
||||
window.removeEventListener(OBJECT_BROWSER_SEARCH_FOCUS_EVENT, listener);
|
||||
});
|
||||
|
||||
it("ignores unrelated or malformed events", () => {
|
||||
expect(objectBrowserSearchFocusTabId(new Event(OBJECT_BROWSER_SEARCH_FOCUS_EVENT))).toBeUndefined();
|
||||
expect(objectBrowserSearchFocusTabId(new CustomEvent(OBJECT_BROWSER_SEARCH_FOCUS_EVENT, { detail: {} }))).toBeUndefined();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,16 @@
|
||||
export const OBJECT_BROWSER_SEARCH_FOCUS_EVENT = "dbx:focus-object-browser-search";
|
||||
|
||||
type ObjectBrowserSearchFocusDetail = {
|
||||
tabId: string;
|
||||
};
|
||||
|
||||
export function requestObjectBrowserSearchFocus(tabId: string) {
|
||||
if (typeof window === "undefined") return;
|
||||
window.dispatchEvent(new CustomEvent<ObjectBrowserSearchFocusDetail>(OBJECT_BROWSER_SEARCH_FOCUS_EVENT, { detail: { tabId } }));
|
||||
}
|
||||
|
||||
export function objectBrowserSearchFocusTabId(event: Event): string | undefined {
|
||||
if (!(event instanceof CustomEvent)) return undefined;
|
||||
const tabId = (event.detail as Partial<ObjectBrowserSearchFocusDetail> | null)?.tabId;
|
||||
return typeof tabId === "string" && tabId.length > 0 ? tabId : undefined;
|
||||
}
|
||||
Reference in new issue
Block a user