diff --git a/apps/desktop/src/components/objects/ObjectBrowser.vue b/apps/desktop/src/components/objects/ObjectBrowser.vue index fe56730348..64d97aa2e6 100644 --- a/apps/desktop/src/components/objects/ObjectBrowser.vue +++ b/apps/desktop/src/components/objects/ObjectBrowser.vue @@ -433,6 +433,7 @@ type ObjectBrowserScroller = // type loose because vue-virtual-scroller does not ship complete ref typings. const listScrollerRef = ref(null); const gridScrollerRef = ref(null); +const objectListHeaderRef = ref(null); let viewportFrame = 0; let restoreViewportFrame = 0; @@ -463,6 +464,7 @@ function emitViewportChange(scrollTop: number) { } function onObjectsScroll() { + syncObjectListHeaderScroll(); if (viewportFrame) return; viewportFrame = window.requestAnimationFrame(() => { viewportFrame = 0; @@ -472,6 +474,17 @@ function onObjectsScroll() { }); } +// The list header sits outside the row scroller and is clipped (overflow: +// hidden), so keep its programmatic scrollLeft aligned with the scroller's +// horizontal position on every scroll, resize, and (re)attach. +function syncObjectListHeaderScroll() { + if (!isListView) return; + const header = objectListHeaderRef.value; + const el = scrollerElement(listScrollerRef.value); + if (!header || !el) return; + if (header.scrollLeft !== el.scrollLeft) header.scrollLeft = el.scrollLeft; +} + function flushObjectBrowserViewport() { if (viewportFrame) { window.cancelAnimationFrame(viewportFrame); @@ -522,6 +535,7 @@ watch( if (!el) return; el.addEventListener("scroll", onObjectsScroll, { passive: true }); restoreObjectBrowserViewport(); + nextTick(() => syncObjectListHeaderScroll()); onCleanup(() => el.removeEventListener("scroll", onObjectsScroll)); }, { flush: "post" }, @@ -697,6 +711,14 @@ const selectedTableRows = computed(() => { const selectedTableCount = computed(() => selectedTableRows.value.length); const canBatchDropCascade = computed(() => selectedTableCount.value > 0 && supportsDropTableCascade(effectiveDatabaseType.value)); const canBatchTruncateCascade = computed(() => selectedTableCount.value > 0 && supportsTruncateTableCascade(effectiveDatabaseType.value)); + +// Column resizes change the scrollable content width, so the header's clamped +// scrollLeft has to be re-aligned right after the DOM updates. Registered here +// (after every computed it transitively reads) because watch sources are +// evaluated eagerly at registration time. +watch([objectGridMinWidth, objectColumnWidths], () => { + nextTick(() => syncObjectListHeaderScroll()); +}); const allVisibleTablesSelected = computed(() => visibleSelectableRows.value.length > 0 && visibleSelectableRows.value.every((row) => selectedTableIds.value.has(row.id))); const batchDropProgressPercent = computed(() => (batchDropProgress.value.total > 0 ? Math.round((batchDropProgress.value.completed / batchDropProgress.value.total) * 100) : 0)); @@ -3470,102 +3492,104 @@ function getObjectBrowserMenuItems(item: ObjectBrowserRow): ContextMenuItem[] {
-
-
-
- -
- +
+
+
+
+ +
+ +
-
-
- -
- +
+ +
+ +
-
-
- -
- +
+ +
+ +
-
-
- -
- +
+ +
+ +
-
-
- -
- +
+ +
+ +
-
-
- -
- +
+ +
+ +
-
-
- -
- +
+ +
+ +
-
-
- -
- +
+ +
+ +
- +