mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-21 05:20:02 +00:00
fix(objects): preserve category filter per tab
This commit is contained in:
@@ -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>
|
||||
|
||||
+2
-1
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user