fix(sidebar): keep table search results on Enter
This commit is contained in:
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;
|
||||
|
||||
+48
@@ -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();
|
||||
});
|
||||
});
|
||||
+10
-1
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { createFlatTreeIndex, flattenTree, mutateFlatTreeExpansion, replaceFlatTreeChildren, type FlatTreeNode } from "@/composables/useFlatTree";
|
||||
import { createFlatTreeIndex, flatTreeRowsChanged, flattenTree, mutateFlatTreeExpansion, replaceFlatTreeChildren, type FlatTreeNode } from "@/composables/useFlatTree";
|
||||
import type { TreeNode, TreeNodeType } from "@/types/database";
|
||||
|
||||
function item(id: string, type: TreeNodeType, depth: number, children?: TreeNode[]): FlatTreeNode {
|
||||
@@ -145,6 +145,15 @@ describe("flat-tree range mutations", () => {
|
||||
});
|
||||
|
||||
describe("flat-tree render identities", () => {
|
||||
it("detects same-length table-search row replacements for #6951", () => {
|
||||
const previous = [item("table-a", "table", 1), item("table-b", "table", 1)];
|
||||
const unchanged = previous.map((entry) => ({ ...entry }));
|
||||
const next = [item("table-c", "table", 1), item("table-b", "table", 1)];
|
||||
|
||||
expect(flatTreeRowsChanged(unchanged, previous)).toBe(false);
|
||||
expect(flatTreeRowsChanged(next, previous)).toBe(true);
|
||||
});
|
||||
|
||||
it("isolates equal business ids that belong to different lineages", () => {
|
||||
const collidingId = "connection:a:b";
|
||||
const collidingSchemaId = "connection:a:b:c";
|
||||
|
||||
@@ -98,6 +98,15 @@ export function mutateFlatTreeExpansion(nodes: readonly FlatTreeNode[], parentIn
|
||||
return replaceFlatTreeChildren(nodes, parentIndex, parent);
|
||||
}
|
||||
|
||||
/** Whether the visible row identities changed enough to refresh recycled views. */
|
||||
export function flatTreeRowsChanged(nodes: readonly FlatTreeNode[], previousNodes: readonly FlatTreeNode[] | undefined): boolean {
|
||||
if (!previousNodes || nodes.length !== previousNodes.length) return true;
|
||||
return nodes.some((item, index) => {
|
||||
const previous = previousNodes[index];
|
||||
return !previous || item.renderKey !== previous.renderKey || item.poolType !== previous.poolType || item.depth !== previous.depth;
|
||||
});
|
||||
}
|
||||
|
||||
export function shouldVirtualizeFlatTree(count: number): boolean {
|
||||
return count > 0;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user