fix(objects): preserve category filter per tab

This commit is contained in:
Eve
2026-09-10 15:57:15 +08:00
committed by GitHub
parent bd828076ab
commit 5b5d58cd2a
11 changed files with 47 additions and 32 deletions
+2 -1
View File
@@ -69,7 +69,7 @@ import { isTauriRuntime } from "@/lib/backend/tauriRuntime";
import { openQueryResultArchiveFile } from "@/lib/query/queryResultArchiveFile";
import { rememberExternalSqlFileTarget, resolveExternalSqlFileTarget, unassociatedExternalSqlFileTarget } from "@/lib/sql/externalSqlFileTarget";
import { externalSqlFileOpenErrorMessage, externalSqlEditorMaxBytes, isSqlFilePath, readBrowserSqlFile, sqlFileTitleFromPath } from "@/lib/sql/sqlFileOpen";
import type { ConnectionConfig, DatabaseType, ObjectSourceKind, QueryTab, TreeNode } from "@/types/database";
import type { ConnectionConfig, DatabaseType, ObjectBrowserFilter, ObjectSourceKind, QueryTab, TreeNode } from "@/types/database";
import { parseConnectionDeepLink, type ConnectionDeepLinkDraft } from "@/lib/connection/connectionDeepLink";
import { parseAiConfigDeepLink, type AiConfigDeepLinkDraft } from "@/lib/ai/aiConfigDeepLink";
import { activeDesktopAiRuns, blockingDesktopAiRunsForQuit } from "@/lib/ai/desktopAiRunRegistry";
@@ -3706,6 +3706,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)"
@object-browser-filter-change="(tabId: string, filter: ObjectBrowserFilter) => queryStore.updateObjectBrowserFilter(tabId, filter)"
@add-object-table-to-ai="
(tabId: string, tables: Array<{ name: string; schema?: string }>) => {
const tab = queryStore.tabs.find((candidate) => candidate.id === tabId) ?? activeTab;
@@ -2504,12 +2504,14 @@ defineExpose({
:initial-event-open-request-id="activeTab.objectBrowser?.eventOpenRequestId"
:initial-event-create-request-id="activeTab.objectBrowser?.eventCreateRequestId"
:initial-object-filter="activeTab.objectBrowser?.initialObjectFilter"
:selected-object-filter="activeTab.objectBrowser?.filter"
: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)"
@filter-change="emit('objectBrowserFilterChange', activeTab.id, $event)"
@add-to-ai="emit('addObjectTableToAi', activeTab.id, $event)"
/>
</div>
@@ -1,4 +1,4 @@
import type { ConnectionConfig, ObjectBrowserViewport, QueryTab } from "@/types/database";
import type { ConnectionConfig, ObjectBrowserFilter, ObjectBrowserViewport, QueryTab } from "@/types/database";
import type { DataGridReloadIntent } from "@/lib/dataGrid/dataGridToolbar";
import type { DataGridSortMode } from "@/lib/dataGrid/dataGridSort";
import type { SqlObjectNavigationTarget } from "@/lib/sql/sqlNavigation";
@@ -90,6 +90,7 @@ export interface ContentAreaSurfaceEmits {
objectSchemaChange: [tabId: string, schema: string | undefined];
objectBrowserViewportChange: [tabId: string, viewport: ObjectBrowserViewport];
objectBrowserSearchChange: [tabId: string, query: string];
objectBrowserFilterChange: [tabId: string, filter: ObjectBrowserFilter];
addObjectTableToAi: [tabId: string, tables: Array<{ name: string; schema?: string }>];
structureEditorSaved: [tabId: string, commentChanged: boolean];
structureEditorClose: [tabId: string];
@@ -180,6 +180,7 @@ const props = defineProps<{
/** 显式"新建事件"请求号:每次菜单点击递增,用于打开/重新进入 CREATE 编辑器 */
initialEventCreateRequestId?: number;
initialObjectFilter?: "tables" | "events";
selectedObjectFilter?: ObjectFilter;
initialSearchQuery?: string;
viewport?: ObjectBrowserViewport;
}>();
@@ -189,6 +190,7 @@ const emit = defineEmits<{
schemaChange: [schema: string | undefined];
viewportChange: [viewport: ObjectBrowserViewport];
searchChange: [query: string];
filterChange: [filter: ObjectFilter];
addToAi: [tables: Array<{ name: string; schema?: string }>];
}>();
@@ -2805,7 +2807,7 @@ function openInitialEventIfNeeded() {
function finishObjectBrowserRowsLoad() {
loadingObjects.value = false;
const preferredFilter = props.initialObjectFilter ?? (props.initialEventName || props.initialEventCreateRequestId !== undefined ? "events" : "tables");
const preferredFilter = props.initialEventName || props.initialEventCreateRequestId !== undefined ? "events" : (props.selectedObjectFilter ?? props.initialObjectFilter ?? "tables");
if (!userHasSelectedFilter.value && objectCounts.value[preferredFilter] > 0) {
// The default table filter is a presentation choice, not a user query
// change, so preserve the tab's saved scroll offset across remounts.
@@ -2817,7 +2819,10 @@ function finishObjectBrowserRowsLoad() {
}
watch([() => props.initialEventName, () => props.initialEventOpenRequestId, () => props.initialEventCreateRequestId], ([name, requestId, createRequestId], [previousName, previousRequestId, previousCreateRequestId]) => {
if (name !== previousName || requestId !== previousRequestId || createRequestId !== previousCreateRequestId) openedInitialEvent.value = "";
if (name !== previousName || requestId !== previousRequestId || createRequestId !== previousCreateRequestId) {
openedInitialEvent.value = "";
if (name || createRequestId !== undefined) objectFilter.value = "events";
}
openInitialEventIfNeeded();
});
@@ -3010,6 +3015,12 @@ function filterLabel(filter: ObjectFilter) {
return `${t(key)} ${filterCount(filter)}`;
}
function selectObjectFilter(filter: ObjectFilter) {
userHasSelectedFilter.value = true;
objectFilter.value = filter;
emit("filterChange", filter);
}
function getSearchInput(): HTMLInputElement | null {
return rootRef.value?.querySelector<HTMLInputElement>("[data-object-search-input]") ?? null;
}
@@ -3323,17 +3334,7 @@ function getObjectBrowserMenuItems(item: ObjectBrowserRow): ContextMenuItem[] {
</button>
</div>
<div v-if="showObjectFilter && showInlineObjectFilter" class="flex h-7 shrink-0 items-center rounded border bg-muted/20 p-0.5">
<button
v-for="filter in objectFilters"
:key="filter"
type="button"
class="h-6 rounded-sm px-2 text-xs text-muted-foreground transition-colors hover:text-foreground"
:class="{ 'bg-background text-foreground shadow-sm': objectFilter === filter }"
@click="
userHasSelectedFilter = true;
objectFilter = filter;
"
>
<button v-for="filter in objectFilters" :key="filter" type="button" class="h-6 rounded-sm px-2 text-xs text-muted-foreground transition-colors hover:text-foreground" :class="{ 'bg-background text-foreground shadow-sm': objectFilter === filter }" @click="selectObjectFilter(filter)">
{{ filterLabel(filter) }}
</button>
</div>
@@ -3419,17 +3420,7 @@ function getObjectBrowserMenuItems(item: ObjectBrowserRow): ContextMenuItem[] {
<DropdownMenuCheckboxItem :model-value="settingsStore.editorSettings.objectBrowserShowCheckbox" @select.prevent @update:model-value="toggleCheckboxColumn()">{{ t("objects.toggleCheckbox") }}</DropdownMenuCheckboxItem>
<template v-if="showObjectFilter && toolbarTier >= 2">
<DropdownMenuSeparator />
<DropdownMenuCheckboxItem
v-for="filter in objectFilters"
:key="filter"
:model-value="objectFilter === filter"
@select.prevent
@update:model-value="
userHasSelectedFilter = true;
objectFilter = filter;
"
>{{ filterLabel(filter) }}</DropdownMenuCheckboxItem
>
<DropdownMenuCheckboxItem v-for="filter in objectFilters" :key="filter" :model-value="objectFilter === filter" @select.prevent @update:model-value="selectObjectFilter(filter)">{{ filterLabel(filter) }}</DropdownMenuCheckboxItem>
</template>
</ToolbarOverflowMenu>
</div>
@@ -55,10 +55,11 @@ describe("ObjectBrowser MySQL Event CREATE navigation", () => {
it("re-triggers a create request when the request id changes on a reused tab", () => {
expect(objectBrowserSource).toContain("() => props.initialEventCreateRequestId");
expect(objectBrowserSource).toMatch(/watch\(\[\(\) => props\.initialEventName, \(\) => props\.initialEventOpenRequestId, \(\) => props\.initialEventCreateRequestId\][\s\S]*?openedInitialEvent\.value = "";[\s\S]*?openInitialEventIfNeeded\(\);/);
expect(objectBrowserSource).toContain('if (name || createRequestId !== undefined) objectFilter.value = "events";');
});
it("lists the events filter as the preferred view for a create request", () => {
expect(objectBrowserSource).toMatch(/const preferredFilter = props\.initialObjectFilter \?\? \(props\.initialEventName \|\| props\.initialEventCreateRequestId !== undefined \? "events" : "tables"\);/);
expect(objectBrowserSource).toMatch(/const preferredFilter = props\.initialEventName \|\| props\.initialEventCreateRequestId !== undefined \? "events" : \(props\.selectedObjectFilter \?\? props\.initialObjectFilter \?\? "tables"\);/);
});
it("ContentArea passes the create request id into ObjectBrowser", () => {
@@ -1,4 +1,5 @@
import type { MongoCollectionKind, ObjectInfo, TreeNode, TreeNodeType } from "@/types/database";
import type { MongoCollectionKind, ObjectBrowserFilter, ObjectInfo, TreeNode, TreeNodeType } from "@/types/database";
export type { ObjectBrowserFilter } from "@/types/database";
import { pinnedTreeNodeIdentityMatches, type PinnedTreeNodeIdentity } from "@/lib/app/pinnedItems";
import { toMongoCollectionKind } from "@/lib/sidebar/mongoCollectionMutation";
import { buildGroupedObjectTreeNodes, buildSimpleObjectTreeNodes, buildTableTreeNodes, compareDatabaseObjectNames, normalizeDatabaseObjectName } from "@/lib/table/tableTree";
@@ -26,7 +27,6 @@ export type ObjectBrowserRow = {
export type ObjectBrowserSortKey = "name" | "type" | "estimatedRows" | "totalBytes" | "created_at" | "updated_at" | "comment";
export type ObjectBrowserSortDirection = "asc" | "desc";
export type ObjectBrowserFilter = "all" | "tables" | "views" | "materializedViews" | "procedures" | "functions" | "triggers" | "events" | "sequences" | "packages" | "types";
export type ObjectBrowserFilterCounts = Record<ObjectBrowserFilter, number>;
export type ObjectBrowserPinnedTreeNodeContext = {
@@ -35,6 +35,7 @@ export const contentSurfaceEventNames = [
"objectSchemaChange",
"objectBrowserViewportChange",
"objectBrowserSearchChange",
"objectBrowserFilterChange",
"addObjectTableToAi",
"structureEditorSaved",
"structureEditorClose",
@@ -80,21 +80,24 @@ describe("queryStore database open state", () => {
expect(store.tabs.filter((tab) => tab.mode === "dolt-version-control")).toHaveLength(2);
});
it("keeps object browser search and viewport per tab", async () => {
it("keeps object browser filter, search, and viewport per tab", async () => {
const { useQueryStore } = await import("@/stores/queryStore");
const store = useQueryStore();
const tabId = store.openObjectBrowser("pg-1", "app", "public");
store.updateObjectBrowserFilter(tabId, "views");
store.updateObjectBrowserSearch(tabId, "orders");
store.updateObjectBrowserViewport(tabId, { scrollTop: 340, viewMode: "list" });
const tab = store.tabs.find((item) => item.id === tabId);
expect(tab?.objectBrowser?.filter).toBe("views");
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?.filter).toBe("views");
expect(store.tabs.find((item) => item.id === tabId)?.objectBrowser?.searchQuery).toBe("orders");
store.updateSchema(tabId, "archive");
@@ -102,6 +105,7 @@ describe("queryStore database open state", () => {
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?.filter).toBeUndefined();
expect(tab?.objectBrowser?.viewport).toBeUndefined();
});
@@ -77,6 +77,10 @@ describe("queryStore MySQL Event create navigation", () => {
expect(tab.objectBrowser?.eventCreateRequestId).toBeUndefined();
expect(tab.objectBrowser?.initialObjectFilter).toBe("events");
store.updateObjectBrowserFilter(tabId, "tables");
expect(tab.objectBrowser?.filter).toBe("tables");
expect(tab.objectBrowser?.initialObjectFilter).toBe("events");
// Re-opening the same existing event on the reused tab increments edit request id
store.openObjectBrowser("mysql-1", "shop", undefined, undefined, "foo_event", true, "events");
expect(tab.objectBrowser?.eventOpenRequestId).toBe(2);
+9 -2
View File
@@ -3,7 +3,7 @@ import { defineStore } from "pinia";
import { uuid } from "@/lib/common/utils";
import { computed, markRaw, nextTick, onScopeDispose, reactive, ref, watch } from "vue";
import { useI18n } from "vue-i18n";
import type { BatchSqlExecution, ConnectionConfig, DatabaseType, IndexInfo, NacosConfigEditorViewport, ObjectBrowserViewport, QueryResult, QueryResultSourceColumnRef, QueryTab, TableInfoTab, TableStructureEditorTarget } from "@/types/database";
import type { BatchSqlExecution, ConnectionConfig, DatabaseType, IndexInfo, NacosConfigEditorViewport, ObjectBrowserFilter, ObjectBrowserViewport, QueryResult, QueryResultSourceColumnRef, QueryTab, TableInfoTab, TableStructureEditorTarget } from "@/types/database";
import { orderPinnedFirst } from "@/lib/app/pinnedItems";
import { canCancelQueryExecution } from "@/lib/sql/queryExecutionState";
import { buildExplainSql, parseExplainResult, parseDamengExplainText, parseOracleExplainText, sqlServerExplainResult, type BuildExplainSqlResult, type ExplainPlanDatabaseType } from "@/lib/diagram/explainPlan";
@@ -4193,6 +4193,12 @@ export const useQueryStore = defineStore("query", () => {
tab.objectBrowser = { ...tab.objectBrowser, searchQuery: query };
}
function updateObjectBrowserFilter(id: string, filter: ObjectBrowserFilter) {
const tab = tabs.value.find((t) => t.id === id);
if (!tab || tab.mode !== "objects" || tab.objectBrowser?.filter === filter) return;
tab.objectBrowser = { ...tab.objectBrowser, filter };
}
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);
@@ -4437,7 +4443,7 @@ export const useQueryStore = defineStore("query", () => {
clearExplain(tab);
}
tab.schema = schema;
if (tab.mode === "objects") tab.objectBrowser = { ...tab.objectBrowser, schema, viewport: undefined };
if (tab.mode === "objects") tab.objectBrowser = { ...tab.objectBrowser, schema, filter: undefined, viewport: undefined };
persistSavedSqlExecutionTarget(tab, options);
}
@@ -7903,6 +7909,7 @@ export const useQueryStore = defineStore("query", () => {
flushEditorState,
updateObjectBrowserViewport,
updateObjectBrowserSearch,
updateObjectBrowserFilter,
updateNacosConfigEditorViewport,
setAutoCommit,
markManualTransactionDirty,
+3
View File
@@ -1124,6 +1124,8 @@ export interface TableStructureEditorViewport {
export type ObjectBrowserViewMode = "list" | "grid";
export type ObjectBrowserFilter = "all" | "tables" | "views" | "materializedViews" | "procedures" | "functions" | "triggers" | "events" | "sequences" | "packages" | "types";
export interface ObjectBrowserViewport {
scrollTop: number;
viewMode: ObjectBrowserViewMode;
@@ -1305,6 +1307,7 @@ export interface QueryTab {
/** 显式的"新建事件"请求:单调递增,用于让已复用 tab 也能重复进入 CREATE 编辑器 */
eventCreateRequestId?: number;
initialObjectFilter?: "tables" | "events";
filter?: ObjectBrowserFilter;
searchQuery?: string;
viewport?: ObjectBrowserViewport;
};