fix(sidebar): restore table list after remote search
This commit is contained in:
1 parent
b6beefc89e
commit
299c342d6d
6 files changed
+137
-2
No files matched your search
@@ -429,9 +429,10 @@ function collectExpandedObjectSearchTargets(node: TreeNode, tasks: SidebarSearch
|
||||
if (searchableObjectGroupTypes.has(node.type)) {
|
||||
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.
|
||||
// once the query is gone, drop the filtered projection and collapse it
|
||||
// back. Its next explicit expansion will load the ordinary first page.
|
||||
node.isExpanded = false;
|
||||
store.discardFilteredTreeNodeChildren(node.id);
|
||||
} else {
|
||||
tasks.push(() => store.loadObjectGroupChildren(node, { force: true }));
|
||||
}
|
||||
|
||||
@@ -715,6 +715,12 @@ async function openDirectNavigationNode(node: TreeNode, requestId: number) {
|
||||
|
||||
async function toggle(requestId = beginNavigationRequest()) {
|
||||
const node = activeNode.value;
|
||||
// Remote sidebar search intentionally descends into one active connection.
|
||||
// Keep that scope aligned even when this toggle is satisfied entirely from
|
||||
// already-loaded children and therefore performs no connection request.
|
||||
if (node.connectionId && connectionStore.connectedIds.has(node.connectionId)) {
|
||||
connectionStore.activeConnectionId = node.connectionId;
|
||||
}
|
||||
const treeLoadSearchOptions = sidebarTreeContext?.getTreeLoadSearchOptions?.(node);
|
||||
if (isDirectNavigationTreeNode(node.type)) {
|
||||
try {
|
||||
|
||||
+1
@@ -18,6 +18,7 @@ describe("ConnectionTree global search loading", () => {
|
||||
expect(source).toContain("while (deferredSearchQuery.value === query && store.sidebarSearchQuery === query)");
|
||||
expect(source).toContain("const restoreTasks = !newQuery && oldQuery ? restoreTrackedSearchTargets() : [];");
|
||||
expect(source).toContain("if (shouldCollapse) node.isExpanded = false;");
|
||||
expect(source).toContain("store.discardFilteredTreeNodeChildren(node.id);");
|
||||
});
|
||||
|
||||
it("limits concurrent metadata loads without dropping a task", async () => {
|
||||
|
||||
+37
@@ -11,6 +11,8 @@ import SidebarTreeRuntimeHost from "@/components/sidebar/SidebarTreeRuntimeHost.
|
||||
const connectionStore = {
|
||||
treeNodes: [] as TreeNode[],
|
||||
sidebarSearchQuery: "",
|
||||
activeConnectionId: null as string | null,
|
||||
connectedIds: new Set<string>(),
|
||||
canUseLoadedTreeNodeToggle: vi.fn(() => true),
|
||||
releaseCollapsedTreeNodeChildren: vi.fn(),
|
||||
getConfig: vi.fn(() => ({ db_type: "mysql", name: "connection" })),
|
||||
@@ -56,12 +58,47 @@ afterEach(() => {
|
||||
document.body.innerHTML = "";
|
||||
connectionStore.treeNodes = [];
|
||||
connectionStore.sidebarSearchQuery = "";
|
||||
connectionStore.activeConnectionId = null;
|
||||
connectionStore.connectedIds.clear();
|
||||
vi.clearAllMocks();
|
||||
connectionStore.canUseLoadedTreeNodeToggle.mockReturnValue(true);
|
||||
connectionStore.getConfig.mockReturnValue({ db_type: "mysql", name: "connection" });
|
||||
});
|
||||
|
||||
describe("SidebarTreeRuntimeHost expansion", () => {
|
||||
it("activates the owning connection when a cached node toggles locally", async () => {
|
||||
const group: TreeNode = {
|
||||
id: "mysql:basic:__tables",
|
||||
label: "tree.tables",
|
||||
type: "group-tables",
|
||||
connectionId: "mysql",
|
||||
database: "basic",
|
||||
isExpanded: false,
|
||||
children: [{ id: "mysql:basic:orders", label: "orders", type: "table", connectionId: "mysql", database: "basic" }],
|
||||
};
|
||||
connectionStore.connectedIds.add("mysql");
|
||||
connectionStore.activeConnectionId = "another-connection";
|
||||
|
||||
const host = ref<InstanceType<typeof SidebarTreeRuntimeHost> | null>(null);
|
||||
const app = createApp(
|
||||
defineComponent({
|
||||
setup: () => () => h(SidebarTreeRuntimeHost, { ref: host, node: group, depth: 0 }),
|
||||
}),
|
||||
);
|
||||
mountedApps.push(app);
|
||||
const container = document.createElement("div");
|
||||
document.body.append(container);
|
||||
app.use(i18n);
|
||||
app.mount(container);
|
||||
|
||||
host.value?.toggleNode(group);
|
||||
await nextTick();
|
||||
|
||||
expect(connectionStore.activeConnectionId).toBe("mysql");
|
||||
expect(group.isExpanded).toBe(true);
|
||||
expect(connectionStore.loadObjectGroupChildren).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("publishes a rendered group collapse and synchronizes the live tree", async () => {
|
||||
const liveGroup: TreeNode = {
|
||||
id: "connection:database:__tables",
|
||||
|
||||
@@ -3326,6 +3326,76 @@ describe("connectionStore metadata loading", () => {
|
||||
expect(listTables.mock.calls[1].slice(3, 6)).toEqual(["t", 3, 2]);
|
||||
});
|
||||
|
||||
it("discards a collapsed remote-search projection so the next expansion reloads the ordinary page", async () => {
|
||||
const listTables = vi.fn((_connectionId: string, _database: string, _schema: string, searchFilter?: string, limit?: number) => {
|
||||
if (searchFilter) {
|
||||
return Promise.resolve([{ name: "zz_late_probe", table_type: "TABLE" as const, comment: null }]);
|
||||
}
|
||||
return Promise.resolve(
|
||||
Array.from({ length: Math.min(limit ?? 1001, 1001) }, (_, index) => ({
|
||||
name: `t_${String(index + 1).padStart(4, "0")}`,
|
||||
table_type: "TABLE" as const,
|
||||
comment: null,
|
||||
})),
|
||||
);
|
||||
});
|
||||
const deleteSchemaCachePrefix = vi.fn().mockResolvedValue(undefined);
|
||||
|
||||
vi.doMock("@/lib/backend/tauriRuntime", () => ({ isTauriRuntime: () => false }));
|
||||
vi.doMock("@/lib/backend/api", () => ({
|
||||
checkConnectionHealth: vi.fn().mockResolvedValue(undefined),
|
||||
deleteSchemaCachePrefix,
|
||||
listTables,
|
||||
loadSchemaCache: vi.fn().mockResolvedValue(null),
|
||||
saveSchemaCache: vi.fn().mockResolvedValue(undefined),
|
||||
saveConnections: vi.fn().mockResolvedValue(undefined),
|
||||
saveSidebarLayout: vi.fn().mockResolvedValue(undefined),
|
||||
}));
|
||||
|
||||
const { useConnectionStore } = await import("@/stores/connectionStore");
|
||||
const { useSettingsStore } = await import("@/stores/settingsStore");
|
||||
const store = useConnectionStore();
|
||||
const settingsStore = useSettingsStore();
|
||||
settingsStore.editorSettings.sidebarObjectDisplay = "grouped";
|
||||
settingsStore.desktopSettings.sidebar_table_page_size = 1000;
|
||||
|
||||
const connection = mysqlConnection();
|
||||
const tablesGroup: TreeNode = {
|
||||
id: "mysql-1:app:__tables",
|
||||
label: "tree.tables",
|
||||
type: "group-tables",
|
||||
connectionId: connection.id,
|
||||
database: "app",
|
||||
isExpanded: false,
|
||||
children: [],
|
||||
};
|
||||
store.connections = [connection];
|
||||
store.connectedIds.add(connection.id);
|
||||
store.treeNodes = [{ id: connection.id, label: connection.name, type: "connection", connectionId: connection.id, children: [tablesGroup] }];
|
||||
|
||||
store.sidebarSearchQuery = "zz_late_probe";
|
||||
await store.loadObjectGroupChildren(tablesGroup, { force: true });
|
||||
|
||||
expect(listTables).toHaveBeenLastCalledWith(connection.id, "app", "app", "zz_late_probe", SIDEBAR_SEARCH_RESULT_BUDGET, undefined, ["TABLE"]);
|
||||
expect(tablesGroup.children?.map((node) => node.label)).toEqual(["zz_late_probe"]);
|
||||
expect(tablesGroup.objectCount).toBe(1);
|
||||
expect(store.canUseLoadedTreeNodeToggle(tablesGroup)).toBe(true);
|
||||
|
||||
tablesGroup.isExpanded = false;
|
||||
expect(store.discardFilteredTreeNodeChildren(tablesGroup.id)).toBe(true);
|
||||
expect(tablesGroup.children).toEqual([]);
|
||||
expect(tablesGroup.objectCount).toBeUndefined();
|
||||
expect(store.canUseLoadedTreeNodeToggle(tablesGroup)).toBe(false);
|
||||
expect(deleteSchemaCachePrefix).not.toHaveBeenCalled();
|
||||
|
||||
store.sidebarSearchQuery = "";
|
||||
await store.loadObjectGroupChildren(tablesGroup);
|
||||
|
||||
expect(listTables).toHaveBeenLastCalledWith(connection.id, "app", "app", undefined, 1001, 0, ["TABLE"]);
|
||||
expect(tablesGroup.children).toHaveLength(1001);
|
||||
expect(tablesGroup.children?.at(-1)?.type).toBe("load-more");
|
||||
});
|
||||
|
||||
it("keeps connection-name matches out of object metadata filters", async () => {
|
||||
const listTables = vi.fn((_connectionId: string, _database: string, _schema: string, searchFilter?: string) => Promise.resolve(searchFilter === "orders" || !searchFilter ? [{ name: "orders", table_type: "TABLE" as const, comment: null }] : []));
|
||||
|
||||
|
||||
@@ -2913,6 +2913,25 @@ export const useConnectionStore = defineStore("connection", () => {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Remote sidebar search temporarily replaces an object group's ordinary
|
||||
// children with a filtered projection. When search expanded a previously
|
||||
// collapsed group, discard that projection on clear instead of letting the
|
||||
// loaded marker make the next user expansion reuse incomplete children.
|
||||
function discardFilteredTreeNodeChildren(nodeId: string): boolean {
|
||||
if (!filteredObjectGroupChildrenIds.has(nodeId)) return false;
|
||||
const node = findNode(treeNodes.value, nodeId);
|
||||
if (!node) {
|
||||
filteredObjectGroupChildrenIds.delete(nodeId);
|
||||
return false;
|
||||
}
|
||||
clearLoadedChildrenCache(nodeId, { deletePersisted: false });
|
||||
treeNodeLoads.invalidatePrefix(nodeId);
|
||||
node.children = [];
|
||||
node.objectCount = undefined;
|
||||
node.isLoading = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
function treeNodeInSidebarTree(node: TreeNode): TreeNode | null {
|
||||
return findNode(treeNodes.value, node.id);
|
||||
}
|
||||
@@ -8928,6 +8947,7 @@ export const useConnectionStore = defineStore("connection", () => {
|
||||
isTreeNodeChildrenLoaded,
|
||||
canUseLoadedTreeNodeToggle,
|
||||
releaseCollapsedTreeNodeChildren,
|
||||
discardFilteredTreeNodeChildren,
|
||||
cancelTreeNodeLoad,
|
||||
setBeforeConnectHandler,
|
||||
initFromDisk,
|
||||
|
||||
Reference in new issue
Block a user