Fix: keep simple sidebar table pagination bounded (#2092)

This commit is contained in:
zipg 2026-06-30 00:08:41 +08:00 committed by GitHub
parent b951d3a50a
commit 267768ecbd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 53 additions and 7 deletions

View File

@ -208,10 +208,12 @@ const activeTab = computed(() => queryStore.tabs.find((tab) => tab.id === queryS
// ancestor. The overlay reuses <TreeItem>, so collapse/expand comes for free.
const stickyScrollTop = ref(0);
const sidebarScrollMetrics = ref({ scrollTop: 0, clientHeight: 0, scrollHeight: 0 });
const isScrollingSidebar = ref(false);
const isDraggingSidebarScrollbar = ref(false);
let sidebarScrollbarResizeObserver: ResizeObserver | null = null;
let sidebarScrollbarAnimationFrame = 0;
let sidebarScrollbarDragOffset = 0;
let sidebarScrollingTimer = 0;
function updateSidebarScrollMetrics() {
const scroller = currentTreeScroller();
@ -234,6 +236,11 @@ function scheduleSidebarScrollMetricsUpdate() {
}
function onTreeScroll() {
isScrollingSidebar.value = true;
window.clearTimeout(sidebarScrollingTimer);
sidebarScrollingTimer = window.setTimeout(() => {
isScrollingSidebar.value = false;
}, 700);
scheduleSidebarScrollMetricsUpdate();
}
@ -780,6 +787,7 @@ onUnmounted(() => {
stopSidebarScrollbarDrag();
sidebarScrollbarResizeObserver?.disconnect();
window.cancelAnimationFrame(sidebarScrollbarAnimationFrame);
window.clearTimeout(sidebarScrollingTimer);
});
defineExpose({ focusSearch, createNewGroup });
@ -853,7 +861,7 @@ defineExpose({ focusSearch, createNewGroup });
<div v-if="stickyNode" class="sticky-database-header pointer-events-auto absolute inset-x-0 top-0 z-[5] border-b border-border/60" :style="stickyHeaderStyle">
<TreeItem :node="stickyNode.node" :depth="stickyNode.depth" :drag-disabled="true" @search-toggle="onSearchToggle" />
</div>
<div v-if="hasSidebarVerticalOverflow" ref="sidebarScrollbarTrackRef" class="sidebar-tree-scrollbar" :class="{ 'sidebar-tree-scrollbar--dragging': isDraggingSidebarScrollbar }" @pointerdown="onSidebarScrollbarTrackPointerDown">
<div v-if="hasSidebarVerticalOverflow" ref="sidebarScrollbarTrackRef" class="sidebar-tree-scrollbar" :class="{ 'sidebar-tree-scrollbar--scrolling': isScrollingSidebar, 'sidebar-tree-scrollbar--dragging': isDraggingSidebarScrollbar }" @pointerdown="onSidebarScrollbarTrackPointerDown">
<div class="sidebar-tree-scrollbar__thumb" :style="sidebarScrollbarThumbStyle" @pointerdown.stop="onSidebarScrollbarThumbPointerDown" />
</div>
</div>
@ -871,7 +879,7 @@ defineExpose({ focusSearch, createNewGroup });
@rename-started="pendingRenameGroupId = null"
/>
</div>
<div v-if="hasSidebarVerticalOverflow" ref="sidebarScrollbarTrackRef" class="sidebar-tree-scrollbar" :class="{ 'sidebar-tree-scrollbar--dragging': isDraggingSidebarScrollbar }" @pointerdown="onSidebarScrollbarTrackPointerDown">
<div v-if="hasSidebarVerticalOverflow" ref="sidebarScrollbarTrackRef" class="sidebar-tree-scrollbar" :class="{ 'sidebar-tree-scrollbar--scrolling': isScrollingSidebar, 'sidebar-tree-scrollbar--dragging': isDraggingSidebarScrollbar }" @pointerdown="onSidebarScrollbarTrackPointerDown">
<div class="sidebar-tree-scrollbar__thumb" :style="sidebarScrollbarThumbStyle" @pointerdown.stop="onSidebarScrollbarThumbPointerDown" />
</div>
</div>
@ -889,10 +897,13 @@ defineExpose({ focusSearch, createNewGroup });
.connection-tree-scroller {
will-change: scroll-position;
contain: content;
scrollbar-width: none;
-ms-overflow-style: none;
}
.connection-tree-scroller::-webkit-scrollbar:vertical {
.connection-tree-scroller::-webkit-scrollbar {
width: 0;
height: 0;
}
.connection-tree-scroller :deep(.vue-recycle-scroller__item-view) {
@ -906,9 +917,9 @@ defineExpose({ focusSearch, createNewGroup });
.sidebar-tree-scrollbar {
position: absolute;
top: 4px;
top: 0;
right: 0;
bottom: 4px;
bottom: 0;
z-index: 10;
width: 12px;
cursor: default;
@ -916,6 +927,7 @@ defineExpose({ focusSearch, createNewGroup });
transition: opacity 120ms ease;
}
.sidebar-tree-scrollbar--scrolling,
.sidebar-tree-scrollbar:hover,
.sidebar-tree-scrollbar--dragging {
opacity: 1;

View File

@ -254,6 +254,13 @@ export function mergeTableInfosIntoObjects(objects: readonly ObjectInfo[], table
return merged;
}
export function filterSimpleSidebarSupplementalObjects(objects: readonly ObjectInfo[]): ObjectInfo[] {
return objects.filter((object) => {
const objectType = normalizeObjectType(object.object_type);
return objectType !== "TABLE" && objectType !== "VIEW" && objectType !== "MATERIALIZED_VIEW";
});
}
function buildPartitionTree(entries: TableTreeEntry[], connectionId: string, database: string): TreeNode[] {
const orderedEntries = sortDatabaseObjectsByName(entries, (entry) => entry.node.label);
const byKey = new Map<string, TableTreeEntry>();

View File

@ -35,6 +35,7 @@ import {
buildSimpleObjectTreeNodes,
buildTableTreeNodes,
expandCachedObjectBrowserNodes,
filterSimpleSidebarSupplementalObjects,
mergeTableInfosIntoObjects,
mergeTableTreePageChildren,
objectGroupRefreshParentId,
@ -770,7 +771,7 @@ export const useConnectionStore = defineStore("connection", () => {
connectionId: options.connectionId,
database: options.database,
schema: options.effectiveSchema,
objects: mergeTableInfosIntoObjects(objects, pageTables, options.effectiveSchema),
objects: mergeTableInfosIntoObjects(filterSimpleSidebarSupplementalObjects(objects), pageTables, options.effectiveSchema),
});
return {
children,

View File

@ -1,6 +1,6 @@
import { test } from "vitest";
import assert from "node:assert/strict";
import { buildGroupedObjectTreeNodes, buildObjectGroupPlaceholderNodes, buildSimpleObjectTreeNodes, buildTableTreeNodes, mergeTableInfosIntoObjects, mergeTableTreePageChildren } from "../../apps/desktop/src/lib/tableTree.ts";
import { buildGroupedObjectTreeNodes, buildObjectGroupPlaceholderNodes, buildSimpleObjectTreeNodes, buildTableTreeNodes, filterSimpleSidebarSupplementalObjects, mergeTableInfosIntoObjects, mergeTableTreePageChildren } from "../../apps/desktop/src/lib/tableTree.ts";
import type { ObjectInfo, TableInfo, TreeNode } from "../../apps/desktop/src/types/database.ts";
function table(name: string, parent?: string): TableInfo {
@ -296,6 +296,32 @@ test("mergeTableInfosIntoObjects restores views missing from object metadata", (
);
});
test("filterSimpleSidebarSupplementalObjects leaves paged tables and views to listTables", () => {
const supplemental = filterSimpleSidebarSupplementalObjects([
{ name: "orders", object_type: "TABLE", schema: "public" },
{ name: "active_orders", object_type: "VIEW", schema: "public" },
{ name: "order_summary", object_type: "MATERIALIZED VIEW", schema: "public" },
{ name: "refresh_stats", object_type: "PROCEDURE", schema: "public" },
{ name: "total_due", object_type: "FUNCTION", schema: "public" },
]);
const nodes = buildSimpleObjectTreeNodes({
nodeId: "conn:app:public",
connectionId: "conn",
database: "app",
schema: "public",
objects: mergeTableInfosIntoObjects(supplemental, [table("orders")], "public"),
});
assert.deepEqual(
nodes.map((node) => ({ label: node.label, type: node.type })),
[
{ label: "orders", type: "table" },
{ label: "refresh_stats", type: "procedure" },
{ label: "total_due", type: "function" },
],
);
});
test("mergeTableInfosIntoObjects dedupes MySQL tables when object metadata carries database as schema", () => {
const merged = mergeTableInfosIntoObjects(
[