fix(objects): keep browse search filter per tab
This commit is contained in:
1 parent
38df8e3097
commit
5244e57dca
9 files changed
+79
-3
No files matched your search
@@ -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;
|
||||
|
||||
+48
@@ -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();
|
||||
});
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -1291,6 +1291,7 @@ export interface QueryTab {
|
||||
/** 显式的"新建事件"请求:单调递增,用于让已复用 tab 也能重复进入 CREATE 编辑器 */
|
||||
eventCreateRequestId?: number;
|
||||
initialObjectFilter?: "tables" | "events";
|
||||
searchQuery?: string;
|
||||
viewport?: ObjectBrowserViewport;
|
||||
};
|
||||
objectSource?: {
|
||||
|
||||
Reference in new issue
Block a user