diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/workflow-list.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/workflow-list.tsx index 91a38ad1a7..b2c70f7044 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/workflow-list.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/workflow-list.tsx @@ -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(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( diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/sidebar.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/sidebar.tsx index cdbcd75830..8549437792 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/sidebar.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/sidebar.tsx @@ -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(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 - } - - 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) + 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' }) } - } + }) }, []) /**