fix(objects): keep browse search filter per tab

This commit is contained in:
Melody Ruch authored and GitHub committed 2026-09-05 22:28:14 +08:00
1 parent 38df8e3097
commit 5244e57dca
9 files changed
+79 -3

No files matched your search

+1
View File
@@ -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);
@@ -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)"
/>
</div>
</template>
@@ -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];
@@ -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<string[]>([]);
const selectedSchema = ref<string | undefined>(props.schema);
const rows = ref<ObjectBrowserRow[]>([]);
const rootRef = ref<HTMLElement>();
const search = ref("");
const search = ref(props.initialSearchQuery ?? "");
const objectFilter = ref<ObjectFilter>("all");
const userHasSelectedFilter = ref(false);
const sortKey = ref<ObjectBrowserSortKey>("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;
@@ -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 <KeepAlive> 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);
});
});
@@ -34,6 +34,7 @@ export const contentSurfaceEventNames = [
"openObjectTable",
"objectSchemaChange",
"objectBrowserViewportChange",
"objectBrowserSearchChange",
"structureEditorSaved",
"structureEditorClose",
"previewStatement",
@@ -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();
});
+8
View File
@@ -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,
+1
View File
@@ -1291,6 +1291,7 @@ export interface QueryTab {
/** 显式的"新建事件"请求:单调递增,用于让已复用 tab 也能重复进入 CREATE 编辑器 */
eventCreateRequestId?: number;
initialObjectFilter?: "tables" | "events";
searchQuery?: string;
viewport?: ObjectBrowserViewport;
};
objectSource?: {