feat(sidebar): scroll to workflow/folder (#2302)

* feat(sidebar): scroll to workflow/folder

* improvement: sidebar scrolling optimizations
This commit is contained in:
Emir Karabeg
2025-12-10 22:08:10 -08:00
committed by GitHub
parent da36c453b5
commit a881dc1877
2 changed files with 42 additions and 62 deletions

View File

@@ -1,6 +1,6 @@
'use client'
import { useCallback, useEffect, useMemo } from 'react'
import { useCallback, useEffect, useMemo, useRef } from 'react'
import clsx from 'clsx'
import { useParams, usePathname } from 'next/navigation'
import { FolderItem } from '@/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/folder-item/folder-item'
@@ -144,25 +144,45 @@ export function WorkflowList({
[pathname, workspaceId]
)
// Track last scrolled workflow to avoid redundant scroll checks
const lastScrolledWorkflowRef = useRef<string | null>(null)
/**
* Auto-expand folders and select the active workflow
* Auto-expand folders, select active workflow, and scroll into view if needed.
*/
useEffect(() => {
if (!workflowId || isLoading || foldersLoading) return
// Expand folder path
// Expand folder path to reveal workflow
if (activeWorkflowFolderId) {
const folderPath = getFolderPath(activeWorkflowFolderId)
for (const folder of folderPath) {
setExpanded(folder.id, true)
}
folderPath.forEach((folder) => setExpanded(folder.id, true))
}
// Auto-select active workflow if not already selected
// Select workflow if not already selected
const { selectedWorkflows, selectOnly } = useFolderStore.getState()
if (!selectedWorkflows.has(workflowId)) {
selectOnly(workflowId)
}
// Skip scroll check if already handled for this workflow
if (lastScrolledWorkflowRef.current === workflowId) return
lastScrolledWorkflowRef.current = workflowId
// Scroll after render only if element is completely off-screen
requestAnimationFrame(() => {
const element = document.querySelector(`[data-item-id="${workflowId}"]`)
const container = scrollContainerRef.current
if (!element || !container) return
const { top: elTop, bottom: elBottom } = element.getBoundingClientRect()
const { top: ctTop, bottom: ctBottom } = container.getBoundingClientRect()
// Only scroll if completely above or below the visible area
if (elBottom <= ctTop || elTop >= ctBottom) {
element.scrollIntoView({ behavior: 'smooth', block: 'center' })
}
})
}, [workflowId, activeWorkflowFolderId, isLoading, foldersLoading, getFolderPath, setExpanded])
const renderWorkflowItem = useCallback(

View File

@@ -212,64 +212,24 @@ export function Sidebar() {
// Combined loading state
const isLoading = workflowsLoading || sessionLoading
// Ref to track active timeout IDs for cleanup
const scrollTimeoutRef = useRef<number | null>(null)
/**
* Scrolls an element into view if it's not already visible in the scroll container.
* Uses a retry mechanism with cleanup to wait for the element to be rendered in the DOM.
*
* @param elementId - The ID of the element to scroll to
* @param maxRetries - Maximum number of retry attempts (default: 10)
* Scrolls a newly created element into view if completely off-screen.
* Uses requestAnimationFrame to sync with render, then scrolls.
*/
const scrollToElement = useCallback(
(elementId: string, maxRetries = 10) => {
// Clear any existing timeout
if (scrollTimeoutRef.current !== null) {
clearTimeout(scrollTimeoutRef.current)
scrollTimeoutRef.current = null
const scrollToElement = useCallback((elementId: string) => {
requestAnimationFrame(() => {
const element = document.querySelector(`[data-item-id="${elementId}"]`)
const container = scrollContainerRef.current
if (!element || !container) return
const { top: elTop, bottom: elBottom } = element.getBoundingClientRect()
const { top: ctTop, bottom: ctBottom } = container.getBoundingClientRect()
// Only scroll if element is completely off-screen
if (elBottom <= ctTop || elTop >= ctBottom) {
element.scrollIntoView({ behavior: 'smooth', block: 'center' })
}
let attempts = 0
const tryScroll = () => {
attempts++
const element = document.querySelector(`[data-item-id="${elementId}"]`)
const container = scrollContainerRef.current
if (element && container) {
const elementRect = element.getBoundingClientRect()
const containerRect = container.getBoundingClientRect()
// Check if element is not fully visible in the container
const isAboveView = elementRect.top < containerRect.top
const isBelowView = elementRect.bottom > containerRect.bottom
if (isAboveView || isBelowView) {
element.scrollIntoView({ behavior: 'smooth', block: 'nearest' })
}
scrollTimeoutRef.current = null
} else if (attempts < maxRetries) {
// Element not in DOM yet, retry after a short delay
scrollTimeoutRef.current = window.setTimeout(tryScroll, 50)
} else {
scrollTimeoutRef.current = null
}
}
// Start the scroll attempt after a small delay to ensure rendering.
scrollTimeoutRef.current = window.setTimeout(tryScroll, 50)
},
[scrollContainerRef]
)
// Cleanup timeouts on unmount
useEffect(() => {
return () => {
if (scrollTimeoutRef.current !== null) {
clearTimeout(scrollTimeoutRef.current)
}
}
})
}, [])
/**