diff --git a/apps/desktop/src/App.vue b/apps/desktop/src/App.vue index c4bcea55c0..c9729974cd 100644 --- a/apps/desktop/src/App.vue +++ b/apps/desktop/src/App.vue @@ -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; diff --git a/apps/desktop/src/components/layout/ContentArea.vue b/apps/desktop/src/components/layout/ContentArea.vue index 670144bcc7..4d961fb692 100644 --- a/apps/desktop/src/components/layout/ContentArea.vue +++ b/apps/desktop/src/components/layout/ContentArea.vue @@ -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)" /> diff --git a/apps/desktop/src/components/layout/querySurfaces.ts b/apps/desktop/src/components/layout/querySurfaces.ts index 316a4b8e08..72cc4d9f63 100644 --- a/apps/desktop/src/components/layout/querySurfaces.ts +++ b/apps/desktop/src/components/layout/querySurfaces.ts @@ -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]; diff --git a/apps/desktop/src/components/objects/ObjectBrowser.vue b/apps/desktop/src/components/objects/ObjectBrowser.vue index 31f27f1317..0a0465c734 100644 --- a/apps/desktop/src/components/objects/ObjectBrowser.vue +++ b/apps/desktop/src/components/objects/ObjectBrowser.vue @@ -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("[data-object-search-input]") ?? null; } @@ -3323,17 +3334,7 @@ function getObjectBrowserMenuItems(item: ObjectBrowserRow): ContextMenuItem[] {
-
@@ -3419,17 +3420,7 @@ function getObjectBrowserMenuItems(item: ObjectBrowserRow): ContextMenuItem[] { {{ t("objects.toggleCheckbox") }} diff --git a/apps/desktop/src/components/objects/__tests__/ObjectBrowser.eventCreate.spec.ts b/apps/desktop/src/components/objects/__tests__/ObjectBrowser.eventCreate.spec.ts index ac4be0e186..da78461743 100644 --- a/apps/desktop/src/components/objects/__tests__/ObjectBrowser.eventCreate.spec.ts +++ b/apps/desktop/src/components/objects/__tests__/ObjectBrowser.eventCreate.spec.ts @@ -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", () => { diff --git a/apps/desktop/src/lib/table/objectBrowserRows.ts b/apps/desktop/src/lib/table/objectBrowserRows.ts index 65c0bdc9b8..eb8bb24560 100644 --- a/apps/desktop/src/lib/table/objectBrowserRows.ts +++ b/apps/desktop/src/lib/table/objectBrowserRows.ts @@ -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; export type ObjectBrowserPinnedTreeNodeContext = { diff --git a/apps/desktop/src/lib/tabs/contentSurfaceEvents.ts b/apps/desktop/src/lib/tabs/contentSurfaceEvents.ts index 9adcb242c9..e88e7fb156 100644 --- a/apps/desktop/src/lib/tabs/contentSurfaceEvents.ts +++ b/apps/desktop/src/lib/tabs/contentSurfaceEvents.ts @@ -35,6 +35,7 @@ export const contentSurfaceEventNames = [ "objectSchemaChange", "objectBrowserViewportChange", "objectBrowserSearchChange", + "objectBrowserFilterChange", "addObjectTableToAi", "structureEditorSaved", "structureEditorClose", 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 7e16b8bb27..8b109ab965 100644 --- a/apps/desktop/src/stores/__tests__/queryStore.database-open.spec.ts +++ b/apps/desktop/src/stores/__tests__/queryStore.database-open.spec.ts @@ -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(); }); diff --git a/apps/desktop/src/stores/__tests__/queryStore.eventCreate.spec.ts b/apps/desktop/src/stores/__tests__/queryStore.eventCreate.spec.ts index 8b259bbde3..7bfa8740d6 100644 --- a/apps/desktop/src/stores/__tests__/queryStore.eventCreate.spec.ts +++ b/apps/desktop/src/stores/__tests__/queryStore.eventCreate.spec.ts @@ -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); diff --git a/apps/desktop/src/stores/queryStore.ts b/apps/desktop/src/stores/queryStore.ts index a81976ba66..1bb3a9f918 100644 --- a/apps/desktop/src/stores/queryStore.ts +++ b/apps/desktop/src/stores/queryStore.ts @@ -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, diff --git a/apps/desktop/src/types/database.ts b/apps/desktop/src/types/database.ts index 195a2185e4..af55650539 100644 --- a/apps/desktop/src/types/database.ts +++ b/apps/desktop/src/types/database.ts @@ -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; };