fix(grid): improve scrolling and sidebar responsiveness

This commit is contained in:
juve authored and GitHub committed 2026-07-13 16:12:54 +08:00
1 parent 67541232a2
commit 29b351691a
8 files changed
+142 -41

No files matched your search

@@ -19,6 +19,7 @@ import { activeTabSidebarTarget, findSidebarNodeForActiveTab, findSidebarNodeFor
import { findLoadedTableTargetForCandidate, queryContextTargetFromCandidate, queryCursorTableCandidate, type QueryCursorTableCandidate } from "@/lib/sql/queryCursorTableTarget";
import { 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 { createSidebarPasteHandlerRegistry } from "@/lib/sidebar/sidebarPasteHandlerRegistry";
import { insertSidebarTableSearchControls, isSidebarTableSearchControlNode } from "@/lib/sidebar/sidebarTableSearchControl";
import TreeItem from "./TreeItem.vue";
import { RecycleScroller } from "vue-virtual-scroller";
@@ -520,6 +521,8 @@ function onSidebarScrollbarThumbPointerDown(event: PointerEvent) {
window.addEventListener("pointercancel", stopSidebarScrollbarDrag);
}
const pasteHandlerRegistry = createSidebarPasteHandlerRegistry();
provide(sidebarTreeContextKey, {
getVisibleNodes: () => selectableVisibleNodes.value,
getVisibleNodeIndex: (id: string) => selectableVisibleNodeIndexById.value.get(id) ?? -1,
@@ -528,6 +531,7 @@ provide(sidebarTreeContextKey, {
store.setSidebarTableSearchQuery(parentNodeId, query);
scheduleSidebarTableSearchRefresh(parentNodeId, { restoreFocus: true });
},
registerPasteHandler: pasteHandlerRegistry.register,
});
const pendingRenameGroupId = ref<string | null>(null);
@@ -1032,8 +1036,8 @@ function requestSelectedSidebarPaste(): boolean {
return true;
}
if (clipboard?.kind !== "table-copy" || clipboard.tables.length === 0 || !selectedNodeId) return false;
window.dispatchEvent(new CustomEvent("dbx:sidebar-request-paste-table", { detail: { nodeId: selectedNodeId } }));
return true;
return pasteHandlerRegistry.request(selectedNodeId);
}
onMounted(() => {
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { ref, computed, nextTick, watch, onMounted, onBeforeUnmount, inject, type Component } from "vue";
import { ref, computed, nextTick, watch, onBeforeUnmount, inject, type Component } from "vue";
import { useSqlHighlighter } from "@/composables/useSqlHighlighter";
import { useI18n } from "vue-i18n";
import { translateBackendError } from "@/i18n/backend-errors";
@@ -227,18 +227,22 @@ function scheduleLabelOverflowMeasure() {
});
}
function observeLabelOverflow() {
labelResizeObserver?.disconnect();
labelResizeObserver = null;
function handleMouseEnter() {
if (!shouldMeasureLabelOverflow()) {
labelOverflowing.value = false;
return;
}
if (typeof ResizeObserver !== "undefined" && labelRef.value) {
updateLabelOverflow();
if (typeof ResizeObserver !== "undefined" && labelRef.value && !labelResizeObserver) {
labelResizeObserver = new ResizeObserver(scheduleLabelOverflowMeasure);
labelResizeObserver.observe(labelRef.value);
}
scheduleLabelOverflowMeasure();
}
function handleMouseLeave() {
labelResizeObserver?.disconnect();
labelResizeObserver = null;
cancelLabelOverflowMeasure();
}
const connectionStore = useConnectionStore();
const queryStore = useQueryStore();
@@ -992,12 +996,6 @@ function requestPasteTreeClipboard(): boolean {
return true;
}
function onSidebarRequestPasteTable(event: Event) {
const nodeId = (event as CustomEvent<{ nodeId?: string }>).detail?.nodeId;
if (nodeId !== props.node.id) return;
requestPasteTreeClipboard();
}
function requestRefreshSelectedNode(): boolean {
if (!canRefreshTreeNodeShortcut()) return false;
void refresh();
@@ -1299,6 +1297,12 @@ async function openData() {
queryStore.setExecutingWithId(tabId, openDataId);
logPhase("state-prepared", { tabId });
// Yield to Vue's scheduler so the new tab becomes visible in the UI (tab
// bar activates, content area switches) before the first blocking network
// call. Without this the entire openData flow runs synchronously before the
// browser paints, making the UI feel frozen on each table click.
await nextTick();
// Helper to check if this openData call is still active (not superseded by a newer click)
const isActive = () => queryStore.tabs.find((t) => t.id === tabId)?.executionId === openDataId;
const isCurrentDataTab = () => {
@@ -4180,7 +4184,7 @@ const isConnected = computed(() => props.node.type === "connection" && !!props.n
const isConnecting = computed(() => props.node.type === "connection" && !!props.node.connectionId && connectionStore.connectingIds.has(props.node.connectionId));
const isConnectionReadonly = computed(() => props.node.type === "connection" && !!props.node.connectionId && (connectionStore.getConfig(props.node.connectionId)?.read_only ?? false));
const isOpenedDatabase = computed(() => isSidebarDatabaseOpened(props.node, connectionStore.isTreeNodeChildrenLoaded));
const showsDatabaseOpenIndicator = computed(() => props.node.type === "database" && (isOpenedDatabase.value || (!!props.node.connectionId && props.node.database != null && queryStore.isDatabaseOpen(props.node.connectionId, props.node.database))));
const showsDatabaseOpenIndicator = computed(() => props.node.type === "database" && (isOpenedDatabase.value || (!!props.node.connectionId && props.node.database != null && queryStore.openDatabaseKeys.has(`${props.node.connectionId}\x00${props.node.database}`))));
const canCloseDatabaseConnection = computed(() => canCloseSidebarDatabaseConnection(props.node, connectionStore.isTreeNodeChildrenLoaded));
const nodeIconClass = computed(() => {
const infoClass = getIconInfo(props.node)?.colorClass;
@@ -4222,7 +4226,7 @@ const connectionColor = computed(() => {
});
const isActiveConnectionScope = computed(() => !!props.node.connectionId && connectionStore.activeConnectionId === props.node.connectionId);
const isSelected = computed(() => connectionStore.selectedTreeNodeId === props.node.id);
const isMultiSelected = computed(() => connectionStore.selectedTreeNodeIds.includes(props.node.id));
const isMultiSelected = computed(() => connectionStore.selectedTreeNodeIdsSet.has(props.node.id));
const isTreeRowSelected = computed(() => isSelected.value || isMultiSelected.value);
const usesSelectionSetHighlight = computed(() => connectionStore.connectionMultiSelectActive || connectionStore.selectedTreeNodeIds.length > 1);
const rowStyle = computed(() => {
@@ -4307,14 +4311,6 @@ watch(
{ immediate: true },
);
watch(
[() => props.node.id, () => visibleLabel(props.node), () => usesFullWidthLabel.value, () => detailTooltip.value?.rows.length ?? 0, isRenamingGroup],
() => {
nextTick(observeLabelOverflow);
},
{ flush: "post", immediate: true },
);
function finishRenameGroup() {
// Guard against double invocation: pressing Enter sets isRenamingGroup=false
// and unmounts the input, which then fires @blur -> finishRenameGroup again.
@@ -4545,21 +4541,23 @@ function onRowMouseDown(event: MouseEvent) {
}
}
onMounted(() => {
observeLabelOverflow();
window.addEventListener("dbx:sidebar-request-paste-table", onSidebarRequestPasteTable);
});
// RecycleScroller reuses mounted TreeItem instances for different nodes, so the
// handler must follow the reactive node id rather than component mount lifetime.
const stopPasteHandlerRegistration = watch(
() => props.node.id,
(nodeId, _previousNodeId, onCleanup) => {
const unregister = sidebarTreeContext?.registerPasteHandler?.(nodeId, requestPasteTreeClipboard);
if (unregister) onCleanup(unregister);
},
{ immediate: true },
);
onBeforeUnmount(() => {
labelResizeObserver?.disconnect();
labelResizeObserver = null;
cancelLabelOverflowMeasure();
window.removeEventListener("dbx:sidebar-request-paste-table", onSidebarRequestPasteTable);
handleMouseLeave();
stopPasteHandlerRegistration();
finishTableReferenceDrag();
});
// ---- CustomContextMenu ----
const shortcutCopyName = computed(() => settingsStore.editorSettings.shortcuts.copySidebarSelection);
const shortcutEditConnection = computed(() => settingsStore.editorSettings.shortcuts.editSidebarConnection);
const shortcutRename = "F2";
@@ -5322,7 +5320,11 @@ function treeItemMenuItems(): ContextMenuItem[] {
@keydown="onKeydown"
@mousedown="onRowMouseDown"
@mousemove="isDropTarget ? updateTarget($event, node.id, node.type) : undefined"
@mouseleave="clearTarget(node.id)"
@mouseenter="handleMouseEnter"
@mouseleave="
clearTarget(node.id);
handleMouseLeave();
"
>
<div v-if="showDropBefore" class="absolute right-2 top-0 h-0.5 bg-primary rounded-full pointer-events-none" :style="{ left: paddingLeft }" />
<div v-if="showDropAfter" class="absolute right-2 bottom-0 h-0.5 bg-primary rounded-full pointer-events-none" :style="{ left: paddingLeft }" />