diff --git a/apps/desktop/src/App.vue b/apps/desktop/src/App.vue index bd05087207..737fdd1c02 100644 --- a/apps/desktop/src/App.vue +++ b/apps/desktop/src/App.vue @@ -3600,6 +3600,7 @@ onUnmounted(() => { " @object-schema-change="(tabId: string, schema: string | undefined) => queryStore.updateSchema(tabId, schema)" @object-browser-viewport-change="(tabId: string, viewport: any) => queryStore.updateObjectBrowserViewport(tabId, viewport)" + @object-browser-search-change="(tabId: string, query: string) => queryStore.updateObjectBrowserSearch(tabId, query)" @structure-editor-saved=" (tabId: string, commentChanged: boolean) => { const tab = queryStore.tabs.find((candidate) => candidate.id === tabId); diff --git a/apps/desktop/src/components/layout/ContentArea.vue b/apps/desktop/src/components/layout/ContentArea.vue index 70d583317e..e2dc033f8b 100644 --- a/apps/desktop/src/components/layout/ContentArea.vue +++ b/apps/desktop/src/components/layout/ContentArea.vue @@ -2502,10 +2502,12 @@ defineExpose({ :initial-event-open-request-id="activeTab.objectBrowser?.eventOpenRequestId" :initial-event-create-request-id="activeTab.objectBrowser?.eventCreateRequestId" :initial-object-filter="activeTab.objectBrowser?.initialObjectFilter" + :initial-search-query="activeTab.objectBrowser?.searchQuery" :viewport="activeTab.objectBrowser?.viewport" @open-table="emit('openObjectTable', activeTab.id, $event)" @schema-change="emit('objectSchemaChange', activeTab.id, $event)" @viewport-change="emit('objectBrowserViewportChange', activeTab.id, $event)" + @search-change="emit('objectBrowserSearchChange', activeTab.id, $event)" /> diff --git a/apps/desktop/src/components/layout/querySurfaces.ts b/apps/desktop/src/components/layout/querySurfaces.ts index aa260991fd..70b3bf31cb 100644 --- a/apps/desktop/src/components/layout/querySurfaces.ts +++ b/apps/desktop/src/components/layout/querySurfaces.ts @@ -88,6 +88,7 @@ export interface ContentAreaSurfaceEmits { openObjectTable: [tabId: string, target: { tableName: string; schema?: string; tableType?: string; catalog?: string }]; objectSchemaChange: [tabId: string, schema: string | undefined]; objectBrowserViewportChange: [tabId: string, viewport: ObjectBrowserViewport]; + objectBrowserSearchChange: [tabId: string, query: string]; structureEditorSaved: [tabId: string, commentChanged: boolean]; structureEditorClose: [tabId: string]; previewStatement: [tabId: string, range: StatementRange | null]; diff --git a/apps/desktop/src/components/objects/ObjectBrowser.vue b/apps/desktop/src/components/objects/ObjectBrowser.vue index 24e8c039b5..6d95cde1d7 100644 --- a/apps/desktop/src/components/objects/ObjectBrowser.vue +++ b/apps/desktop/src/components/objects/ObjectBrowser.vue @@ -176,6 +176,7 @@ const props = defineProps<{ /** 显式"新建事件"请求号:每次菜单点击递增,用于打开/重新进入 CREATE 编辑器 */ initialEventCreateRequestId?: number; initialObjectFilter?: "tables" | "events"; + initialSearchQuery?: string; viewport?: ObjectBrowserViewport; }>(); @@ -183,6 +184,7 @@ const emit = defineEmits<{ openTable: [target: { tableName: string; schema?: string; tableType?: string; catalog?: string }]; schemaChange: [schema: string | undefined]; viewportChange: [viewport: ObjectBrowserViewport]; + searchChange: [query: string]; }>(); const { t } = useI18n(); @@ -200,7 +202,7 @@ const schemas = ref([]); const selectedSchema = ref(props.schema); const rows = ref([]); const rootRef = ref(); -const search = ref(""); +const search = ref(props.initialSearchQuery ?? ""); const objectFilter = ref("all"); const userHasSelectedFilter = ref(false); const sortKey = ref("name"); @@ -534,7 +536,10 @@ watch([sortKey, sortDirection], () => scrollObjectsToTop()); // Also jump to the top when the search query or object-type filter changes — // filtered results bear no relation to the previous scroll position. -watch(search, () => scrollObjectsToTop()); +watch(search, (value) => { + scrollObjectsToTop(); + emit("searchChange", value); +}); watch(objectFilter, () => { if (preserveObjectFilterScrollOnce) { preserveObjectFilterScrollOnce = false; diff --git a/apps/desktop/src/components/objects/__tests__/ObjectBrowserSearchPersistence.spec.ts b/apps/desktop/src/components/objects/__tests__/ObjectBrowserSearchPersistence.spec.ts new file mode 100644 index 0000000000..170fa413e7 --- /dev/null +++ b/apps/desktop/src/components/objects/__tests__/ObjectBrowserSearchPersistence.spec.ts @@ -0,0 +1,48 @@ +import { readFileSync } from "node:fs"; +import { describe, expect, it } from "vitest"; + +// Issue #8236: switching away from the "Browse Objects" tab and back reset +// the keyword search filter, because ObjectBrowser has no around +// it (ContentArea renders it inside a plain v-else-if chain) and its `search` +// ref was always seeded to "". The fix threads the value through +// activeTab.objectBrowser.searchQuery, mirroring the existing viewport +// persistence path. + +const objectBrowserSource = readFileSync(new URL("../ObjectBrowser.vue", import.meta.url), "utf8"); +const contentAreaSource = readFileSync(new URL("../../layout/ContentArea.vue", import.meta.url), "utf8"); +const querySurfacesSource = readFileSync(new URL("../../layout/querySurfaces.ts", import.meta.url), "utf8"); +const contentSurfaceEventsSource = readFileSync(new URL("../../../lib/tabs/contentSurfaceEvents.ts", import.meta.url), "utf8"); +const queryStoreSource = readFileSync(new URL("../../../stores/queryStore.ts", import.meta.url), "utf8"); +const databaseTypesSource = readFileSync(new URL("../../../types/database.ts", import.meta.url), "utf8"); + +describe("ObjectBrowser keyword search survives tab switches", () => { + it("seeds the local search ref from a persisted initial value instead of always starting empty", () => { + expect(objectBrowserSource).toContain("initialSearchQuery?: string;"); + expect(objectBrowserSource).toContain('const search = ref(props.initialSearchQuery ?? "");'); + }); + + it("emits searchChange whenever the keyword changes, so the parent can persist it", () => { + expect(objectBrowserSource).toContain("searchChange: [query: string];"); + expect(objectBrowserSource).toMatch(/watch\(search, \(value\) => \{\s*scrollObjectsToTop\(\);\s*emit\("searchChange", value\);\s*\}\);/); + }); + + it("ContentArea binds the persisted search query as a prop and forwards the change event", () => { + expect(contentAreaSource).toContain(':initial-search-query="activeTab.objectBrowser?.searchQuery"'); + expect(contentAreaSource).toContain("@search-change=\"emit('objectBrowserSearchChange', activeTab.id, $event)\""); + }); + + it("the surface event contract and forwarding list both know about objectBrowserSearchChange", () => { + expect(querySurfacesSource).toContain("objectBrowserSearchChange: [tabId: string, query: string];"); + expect(contentSurfaceEventsSource).toContain('"objectBrowserSearchChange",'); + }); + + it("queryStore persists the search query onto the tab's objectBrowser state", () => { + expect(queryStoreSource).toContain("function updateObjectBrowserSearch(id: string, query: string)"); + expect(queryStoreSource).toContain("tab.objectBrowser = { ...tab.objectBrowser, searchQuery: query };"); + expect(queryStoreSource).toContain("updateObjectBrowserSearch,"); + }); + + it("the tab type declares searchQuery on objectBrowser state", () => { + expect(databaseTypesSource).toMatch(/objectBrowser\?:\s*\{[^}]*searchQuery\?:\s*string;/s); + }); +}); diff --git a/apps/desktop/src/lib/tabs/contentSurfaceEvents.ts b/apps/desktop/src/lib/tabs/contentSurfaceEvents.ts index cb7272a430..a8e47693d6 100644 --- a/apps/desktop/src/lib/tabs/contentSurfaceEvents.ts +++ b/apps/desktop/src/lib/tabs/contentSurfaceEvents.ts @@ -34,6 +34,7 @@ export const contentSurfaceEventNames = [ "openObjectTable", "objectSchemaChange", "objectBrowserViewportChange", + "objectBrowserSearchChange", "structureEditorSaved", "structureEditorClose", "previewStatement", diff --git a/apps/desktop/src/stores/__tests__/queryStore.database-open.spec.ts b/apps/desktop/src/stores/__tests__/queryStore.database-open.spec.ts index 259715863f..7e16b8bb27 100644 --- a/apps/desktop/src/stores/__tests__/queryStore.database-open.spec.ts +++ b/apps/desktop/src/stores/__tests__/queryStore.database-open.spec.ts @@ -80,19 +80,28 @@ describe("queryStore database open state", () => { expect(store.tabs.filter((tab) => tab.mode === "dolt-version-control")).toHaveLength(2); }); - it("keeps object browser viewport per tab and clears it on schema change", async () => { + it("keeps object browser search and viewport per tab", async () => { const { useQueryStore } = await import("@/stores/queryStore"); const store = useQueryStore(); const tabId = store.openObjectBrowser("pg-1", "app", "public"); + store.updateObjectBrowserSearch(tabId, "orders"); store.updateObjectBrowserViewport(tabId, { scrollTop: 340, viewMode: "list" }); const tab = store.tabs.find((item) => item.id === tabId); + expect(tab?.objectBrowser?.searchQuery).toBe("orders"); expect(tab?.objectBrowser?.viewport).toEqual({ scrollTop: 340, viewMode: "list" }); + const otherTabId = store.createTab("pg-1", "app", "query"); + store.switchTab(otherTabId); + store.switchTab(tabId); + expect(store.tabs.find((item) => item.id === tabId)?.objectBrowser?.searchQuery).toBe("orders"); + store.updateSchema(tabId, "archive"); expect(tab?.objectBrowser?.schema).toBe("archive"); + // The keyword intentionally survives a schema switch: the mounted browser keeps its local search too. + expect(tab?.objectBrowser?.searchQuery).toBe("orders"); expect(tab?.objectBrowser?.viewport).toBeUndefined(); }); diff --git a/apps/desktop/src/stores/queryStore.ts b/apps/desktop/src/stores/queryStore.ts index 8d2bd48f0a..d3c00829c7 100644 --- a/apps/desktop/src/stores/queryStore.ts +++ b/apps/desktop/src/stores/queryStore.ts @@ -4076,6 +4076,13 @@ export const useQueryStore = defineStore("query", () => { tab.objectBrowser = { ...tab.objectBrowser, viewport }; } + function updateObjectBrowserSearch(id: string, query: string) { + const tab = tabs.value.find((t) => t.id === id); + if (!tab || tab.mode !== "objects") return; + if (tab.objectBrowser?.searchQuery === query) return; + tab.objectBrowser = { ...tab.objectBrowser, searchQuery: query }; + } + function updateNacosConfigEditorViewport(connectionId: string, namespace: string, viewport: NacosConfigEditorViewport) { if (!Number.isFinite(viewport.scrollTop) || !Number.isFinite(viewport.scrollLeft)) return; const tab = tabs.value.find((candidate) => candidate.mode === "nacos" && candidate.connectionId === connectionId && (candidate.nacosNamespace || "") === namespace); @@ -7704,6 +7711,7 @@ export const useQueryStore = defineStore("query", () => { updateEditorViewport, updateEditorSelection, updateObjectBrowserViewport, + updateObjectBrowserSearch, updateNacosConfigEditorViewport, setAutoCommit, markManualTransactionDirty, diff --git a/apps/desktop/src/types/database.ts b/apps/desktop/src/types/database.ts index f9a62e030d..aad2138371 100644 --- a/apps/desktop/src/types/database.ts +++ b/apps/desktop/src/types/database.ts @@ -1291,6 +1291,7 @@ export interface QueryTab { /** 显式的"新建事件"请求:单调递增,用于让已复用 tab 也能重复进入 CREATE 编辑器 */ eventCreateRequestId?: number; initialObjectFilter?: "tables" | "events"; + searchQuery?: string; viewport?: ObjectBrowserViewport; }; objectSource?: {