mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-21 06:00:39 +00:00
fix(sidebar): preserve group state after search
This commit is contained in:
@@ -21,6 +21,7 @@ import {
|
||||
} from "@/lib/sidebar/sidebarSearchTree";
|
||||
import { createSidebarLabelMatcher, matchSidebarLabel } from "@/lib/sidebar/sidebarSearch";
|
||||
import { collectSidebarRegexIndexScopes, resolveSidebarRemoteSearchQuery, resolveSidebarSearchDispatchMode } from "@/lib/sidebar/sidebarRegexSearchIndex";
|
||||
import { createSidebarSearchExpansionState } from "@/lib/sidebar/sidebarSearchExpansionState";
|
||||
import { buildTableTreeNodes } from "@/lib/table/tableTree";
|
||||
import { isCancelSearchShortcut, isCopySidebarSelectionShortcut, isEditSidebarConnectionShortcut, isPasteSidebarSelectionShortcut, isViewTableDdlShortcut } from "@/lib/editor/keyboardShortcuts";
|
||||
import { sidebarNodeSupportsDdlView } from "@/lib/sidebar/sidebarTreeDdlShortcut";
|
||||
@@ -136,7 +137,13 @@ const sidebarObjectSourceFormatDialect = computed(() => sqlFormatDialectForDbTyp
|
||||
type SearchScope = "connection" | "database" | "schema" | "table" | "view";
|
||||
const selectedSearchScopes = ref<SearchScope[]>([]);
|
||||
const searchCollapsedIds = ref<Set<string>>(new Set());
|
||||
const searchRefreshedNodeIds = new Set<string>();
|
||||
const searchExpansionState = createSidebarSearchExpansionState();
|
||||
const searchRefreshedNodeIds = searchExpansionState.filteredNodeIds;
|
||||
// Group nodes that search force-expanded on the user's behalf (they were
|
||||
// collapsed before the query started). Cleared alongside searchRefreshedNodeIds
|
||||
// so clearing the search box collapses them back instead of leaving every
|
||||
// touched group open and reloading it again.
|
||||
const searchAutoExpandedNodeIds = searchExpansionState.autoExpandedNodeIds;
|
||||
let searchTimer: number | undefined;
|
||||
const tableSearchTimers = new Map<string, number>();
|
||||
const tableSearchFocusRestoreTokens = new Map<string, number>();
|
||||
@@ -253,11 +260,21 @@ watch([deferredSearchQuery, regexMode], ([newQuery, isRegexMode], [oldQuery, was
|
||||
if (dispatchMode === "regex") {
|
||||
// Regex search is a read-only projection over live nodes and the local
|
||||
// table index. It must never trigger ensureConnected/listTables.
|
||||
void loadRegexTableSearchIndexes();
|
||||
const restoreTasks = restoreTrackedSearchTargets();
|
||||
void Promise.all([loadRegexTableSearchIndexes(), ...restoreTasks])
|
||||
.then(() => {
|
||||
if (restoreTasks.length > 0) refreshActiveSidebarTableSearches();
|
||||
})
|
||||
.catch(() => {});
|
||||
return;
|
||||
}
|
||||
if (dispatchMode === "none") {
|
||||
if (!wasRegexMode && !newQuery && oldQuery) searchRefreshedNodeIds.clear();
|
||||
const restoreTasks = restoreTrackedSearchTargets();
|
||||
if (restoreTasks.length > 0) {
|
||||
void Promise.all(restoreTasks)
|
||||
.then(() => refreshActiveSidebarTableSearches())
|
||||
.catch(() => {});
|
||||
}
|
||||
return;
|
||||
}
|
||||
const tasks: Promise<void>[] = [];
|
||||
@@ -266,7 +283,7 @@ watch([deferredSearchQuery, regexMode], ([newQuery, isRegexMode], [oldQuery, was
|
||||
collectExpandedObjectSearchTargets(root, tasks, newQuery ? searchRefreshedNodeIds : undefined, preservesSearchSubtree);
|
||||
}
|
||||
if (!newQuery && oldQuery) {
|
||||
searchRefreshedNodeIds.clear();
|
||||
searchExpansionState.clear();
|
||||
}
|
||||
Promise.all(tasks)
|
||||
.then(() => {
|
||||
@@ -306,18 +323,25 @@ function collectExpandedObjectSearchTargets(node: TreeNode, tasks: Promise<void>
|
||||
for (const child of node.children) {
|
||||
if (child.connectionId && searchableObjectGroupTypes.has(child.type)) {
|
||||
if (preservesSearchSubtree) {
|
||||
if (refreshedNodeIds.delete(child.id)) {
|
||||
if (searchExpansionState.markUnfiltered(child.id)) {
|
||||
tasks.push(store.loadObjectGroupChildren(child, { force: true, searchFilter: "", allowGlobalSearchMismatch: true, expectedSidebarSearchQuery: store.sidebarSearchQuery }));
|
||||
}
|
||||
} else {
|
||||
refreshedNodeIds.add(child.id);
|
||||
searchExpansionState.markFiltered(child.id, !child.isExpanded);
|
||||
tasks.push(store.loadObjectGroupChildren(child, { force: true }));
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (!refreshedNodeIds && searchRefreshedNodeIds.has(node.id)) {
|
||||
} else if (!refreshedNodeIds && searchExpansionState.shouldRestore(node.id)) {
|
||||
if (searchableObjectGroupTypes.has(node.type)) {
|
||||
tasks.push(store.loadObjectGroupChildren(node, { force: true }));
|
||||
if (searchAutoExpandedNodeIds.has(node.id)) {
|
||||
// Search opened this group on the user's behalf to check for matches;
|
||||
// once the query is gone there is nothing to show, so collapse it back
|
||||
// instead of reloading and leaving it open.
|
||||
node.isExpanded = false;
|
||||
} else {
|
||||
tasks.push(store.loadObjectGroupChildren(node, { force: true }));
|
||||
}
|
||||
} else if (simpleObjectParentTypes.has(node.type)) {
|
||||
tasks.push(store.refreshTreeNode(node));
|
||||
}
|
||||
@@ -329,6 +353,16 @@ function collectExpandedObjectSearchTargets(node: TreeNode, tasks: Promise<void>
|
||||
}
|
||||
}
|
||||
|
||||
function restoreTrackedSearchTargets(): Promise<void>[] {
|
||||
if (!searchExpansionState.hasTrackedNodes()) return [];
|
||||
const tasks: Promise<void>[] = [];
|
||||
for (const root of store.treeNodes) {
|
||||
collectExpandedObjectSearchTargets(root, tasks);
|
||||
}
|
||||
searchExpansionState.clear();
|
||||
return tasks;
|
||||
}
|
||||
|
||||
const sidebarFilterGuards = computed(() => resolveSidebarFilterGuards(showConnectedConnectionsOnly.value, searchQuery.value, hasSearchScopeFilter.value));
|
||||
// Connected-only filtering changes only root visibility, so descendant-local
|
||||
// features stay available while operations requiring the full root list pause.
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { createSidebarSearchExpansionState } from "@/lib/sidebar/sidebarSearchExpansionState";
|
||||
|
||||
describe("sidebar search expansion state", () => {
|
||||
it("keeps auto-expanded ownership after a filtered group becomes an unfiltered ancestor match", () => {
|
||||
const state = createSidebarSearchExpansionState();
|
||||
|
||||
state.markFiltered("group-tables", true);
|
||||
expect(state.markUnfiltered("group-tables")).toBe(true);
|
||||
|
||||
expect(state.filteredNodeIds.has("group-tables")).toBe(false);
|
||||
expect(state.autoExpandedNodeIds.has("group-tables")).toBe(true);
|
||||
expect(state.shouldRestore("group-tables")).toBe(true);
|
||||
});
|
||||
|
||||
it("clears tracked ownership when ordinary search exits through another mode", () => {
|
||||
const state = createSidebarSearchExpansionState();
|
||||
state.markFiltered("group-tables", true);
|
||||
|
||||
state.clear();
|
||||
|
||||
expect(state.hasTrackedNodes()).toBe(false);
|
||||
expect(state.shouldRestore("group-tables")).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,36 @@
|
||||
export interface SidebarSearchExpansionState {
|
||||
readonly filteredNodeIds: Set<string>;
|
||||
readonly autoExpandedNodeIds: Set<string>;
|
||||
markFiltered(nodeId: string, autoExpanded: boolean): void;
|
||||
markUnfiltered(nodeId: string): boolean;
|
||||
shouldRestore(nodeId: string): boolean;
|
||||
hasTrackedNodes(): boolean;
|
||||
clear(): void;
|
||||
}
|
||||
|
||||
export function createSidebarSearchExpansionState(): SidebarSearchExpansionState {
|
||||
const filteredNodeIds = new Set<string>();
|
||||
const autoExpandedNodeIds = new Set<string>();
|
||||
|
||||
return {
|
||||
filteredNodeIds,
|
||||
autoExpandedNodeIds,
|
||||
markFiltered(nodeId, autoExpanded) {
|
||||
filteredNodeIds.add(nodeId);
|
||||
if (autoExpanded) autoExpandedNodeIds.add(nodeId);
|
||||
},
|
||||
markUnfiltered(nodeId) {
|
||||
return filteredNodeIds.delete(nodeId);
|
||||
},
|
||||
shouldRestore(nodeId) {
|
||||
return filteredNodeIds.has(nodeId) || autoExpandedNodeIds.has(nodeId);
|
||||
},
|
||||
hasTrackedNodes() {
|
||||
return filteredNodeIds.size > 0 || autoExpandedNodeIds.size > 0;
|
||||
},
|
||||
clear() {
|
||||
filteredNodeIds.clear();
|
||||
autoExpandedNodeIds.clear();
|
||||
},
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user