fix(sidebar): keep table search results on Enter

This commit is contained in:
jearhe authored and GitHub committed 2026-08-23 01:39:53 +08:00
1 parent acc2137cde
commit 0908a944e8
4 files changed
+77 -11

No files matched your search

@@ -46,7 +46,7 @@ import {
type SidebarNodeScrollAlign,
} from "@/lib/sidebar/sidebarActiveTabTarget";
import { findLoadedTableTargetForCandidate, queryContextTargetFromCandidate, queryCursorTableCandidate, type QueryCursorTableCandidate } from "@/lib/sql/queryCursorTableTarget";
import { createFlatTreeIndex, SIDEBAR_TREE_ROW_HEIGHT, SIDEBAR_TREE_PRERENDER_COUNT, SIDEBAR_TREE_SCROLL_BUFFER, flattenTree, shouldVirtualizeFlatTree, type FlatTreeNode } from "@/composables/useFlatTree";
import { createFlatTreeIndex, flatTreeRowsChanged, SIDEBAR_TREE_ROW_HEIGHT, SIDEBAR_TREE_PRERENDER_COUNT, SIDEBAR_TREE_SCROLL_BUFFER, flattenTree, shouldVirtualizeFlatTree, type FlatTreeNode } from "@/composables/useFlatTree";
import { sidebarTreeContextKey } from "@/lib/sidebar/sidebarTreeContext";
import { createSidebarTreeRuntime, sidebarTreeRuntimeKey, type SidebarTreeRuntimeHostInstance } from "@/lib/sidebar/sidebarTreeRuntime";
import { createSidebarPasteHandlerRegistry } from "@/lib/sidebar/sidebarPasteHandlerRegistry";
@@ -980,15 +980,15 @@ watch(flatNodes, (nodes, previousNodes) => {
stickyScrollTop.value = 0;
void nextTick(scheduleSidebarScrollMetricsUpdate);
// After a structural change (list grew/shrunk, e.g. a Dameng connection
// expands or collapses) reconcile the virtual scroller with the browser's
// scroll position. The recycle pool is rebuilt from the live scrollTop, but
// scrollTop is only clamped on the next layout, so right after a shrink the
// pool can still target a window beyond the new content — the viewport then
// lands inside the end spacer and shows a blank region until the next scroll
// event. Reading scrollHeight forces that layout (applying the clamp), then
// one explicit pool refresh keeps the rendered window aligned. Only needed
// when the item count changed; reorders are already rebuilt by the library.
if (nodes.length === (previousNodes?.length ?? -1)) return;
// expands or collapses) or a same-length search projection replacement,
// reconcile the virtual scroller with the browser's scroll position. The
// recycle pool is rebuilt from the live scrollTop, but scrollTop is only
// clamped on the next layout, so right after a shrink the pool can still
// target a window beyond the new content — the viewport then lands inside
// the end spacer and shows a blank region until the next scroll event.
// Reading scrollHeight forces that layout (applying the clamp), then one
// explicit pool refresh keeps the rendered window aligned.
if (!flatTreeRowsChanged(nodes, previousNodes)) return;
void nextTick(() => {
const scroller = treeScrollerRef.value;
if (!scroller || !useVirtualTree.value) return;
@@ -4,6 +4,7 @@ import { createApp, defineComponent, h, nextTick, type App } from "vue";
import { createI18n } from "vue-i18n";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import TreeItem from "@/components/sidebar/TreeItem.vue";
import { sidebarTreeContextKey } from "@/lib/sidebar/sidebarTreeContext";
import { createSidebarTreeRuntime, sidebarTreeRuntimeKey, type SidebarTreeRuntimeHost } from "@/lib/sidebar/sidebarTreeRuntime";
import type { ConnectionConfig, SidebarLayout, TreeNode } from "@/types/database";
@@ -59,6 +60,7 @@ vi.mock("@/stores/settingsStore", () => ({
sidebarAllowHorizontalScroll: false,
sidebarHiddenTablePrefixes: [],
sidebarObjectInfoMode: "none",
sidebarTableSearchLocal: false,
},
}),
}));
@@ -189,4 +191,50 @@ describe("TreeItem connection quick rename", () => {
await vi.waitFor(() => expect(toast).toHaveBeenCalledWith(expect.stringContaining("disk full"), 5000));
});
it("keeps Enter scoped to the table-search input without triggering a tree row action", async () => {
const container = document.createElement("div");
document.body.append(container);
let bubbledKeydowns = 0;
container.addEventListener("keydown", () => {
bubbledKeydowns += 1;
});
const host = runtimeHost();
const runtime = createSidebarTreeRuntime();
runtime.bindHost(host);
const setTableSearchQuery = vi.fn();
const node: TreeNode = {
id: "connection-1:app:__table_search",
label: "sidebar.searchTablesInCurrentScope",
type: "table-search-control",
connectionId: connection.id,
database: "app",
tableSearchParentId: "connection-1:app",
};
const app = createApp(
defineComponent({
setup: () => () => h(TreeItem, { node, depth: 2 }),
}),
);
app.use(i18n);
app.provide(sidebarTreeRuntimeKey, runtime);
app.provide(sidebarTreeContextKey, {
getVisibleNodes: () => [],
getVisibleNodeIndex: () => -1,
setTableSearchQuery,
});
app.mount(container);
mountedApps.push(app);
await nextTick();
const input = container.querySelector<HTMLInputElement>("[data-sidebar-table-search-parent-id]");
expect(input).toBeTruthy();
const event = new KeyboardEvent("keydown", { key: "Enter", bubbles: true, cancelable: true });
input!.dispatchEvent(event);
expect(event.defaultPrevented).toBe(false);
expect(bubbledKeydowns).toBe(0);
expect(host.handleRowKeydown).not.toHaveBeenCalled();
expect(setTableSearchQuery).not.toHaveBeenCalled();
});
});