Skip to content

Commit a881dc1

Browse files
authored
feat(sidebar): scroll to workflow/folder (#2302)
* feat(sidebar): scroll to workflow/folder * improvement: sidebar scrolling optimizations
1 parent da36c45 commit a881dc1

File tree

2 files changed

+42
-62
lines changed

2 files changed

+42
-62
lines changed

apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/workflow-list.tsx

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
'use client'
22

3-
import { useCallback, useEffect, useMemo } from 'react'
3+
import { useCallback, useEffect, useMemo, useRef } from 'react'
44
import clsx from 'clsx'
55
import { useParams, usePathname } from 'next/navigation'
66
import { FolderItem } from '@/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/folder-item/folder-item'
@@ -144,25 +144,45 @@ export function WorkflowList({
144144
[pathname, workspaceId]
145145
)
146146

147+
// Track last scrolled workflow to avoid redundant scroll checks
148+
const lastScrolledWorkflowRef = useRef<string | null>(null)
149+
147150
/**
148-
* Auto-expand folders and select the active workflow
151+
* Auto-expand folders, select active workflow, and scroll into view if needed.
149152
*/
150153
useEffect(() => {
151154
if (!workflowId || isLoading || foldersLoading) return
152155

153-
// Expand folder path
156+
// Expand folder path to reveal workflow
154157
if (activeWorkflowFolderId) {
155158
const folderPath = getFolderPath(activeWorkflowFolderId)
156-
for (const folder of folderPath) {
157-
setExpanded(folder.id, true)
158-
}
159+
folderPath.forEach((folder) => setExpanded(folder.id, true))
159160
}
160161

161-
// Auto-select active workflow if not already selected
162+
// Select workflow if not already selected
162163
const { selectedWorkflows, selectOnly } = useFolderStore.getState()
163164
if (!selectedWorkflows.has(workflowId)) {
164165
selectOnly(workflowId)
165166
}
167+
168+
// Skip scroll check if already handled for this workflow
169+
if (lastScrolledWorkflowRef.current === workflowId) return
170+
lastScrolledWorkflowRef.current = workflowId
171+
172+
// Scroll after render only if element is completely off-screen
173+
requestAnimationFrame(() => {
174+
const element = document.querySelector(`[data-item-id="${workflowId}"]`)
175+
const container = scrollContainerRef.current
176+
if (!element || !container) return
177+
178+
const { top: elTop, bottom: elBottom } = element.getBoundingClientRect()
179+
const { top: ctTop, bottom: ctBottom } = container.getBoundingClientRect()
180+
181+
// Only scroll if completely above or below the visible area
182+
if (elBottom <= ctTop || elTop >= ctBottom) {
183+
element.scrollIntoView({ behavior: 'smooth', block: 'center' })
184+
}
185+
})
166186
}, [workflowId, activeWorkflowFolderId, isLoading, foldersLoading, getFolderPath, setExpanded])
167187

168188
const renderWorkflowItem = useCallback(

apps/sim/app/workspace/[workspaceId]/w/components/sidebar/sidebar.tsx

Lines changed: 15 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -212,64 +212,24 @@ export function Sidebar() {
212212
// Combined loading state
213213
const isLoading = workflowsLoading || sessionLoading
214214

215-
// Ref to track active timeout IDs for cleanup
216-
const scrollTimeoutRef = useRef<number | null>(null)
217-
218215
/**
219-
* Scrolls an element into view if it's not already visible in the scroll container.
220-
* Uses a retry mechanism with cleanup to wait for the element to be rendered in the DOM.
221-
*
222-
* @param elementId - The ID of the element to scroll to
223-
* @param maxRetries - Maximum number of retry attempts (default: 10)
216+
* Scrolls a newly created element into view if completely off-screen.
217+
* Uses requestAnimationFrame to sync with render, then scrolls.
224218
*/
225-
const scrollToElement = useCallback(
226-
(elementId: string, maxRetries = 10) => {
227-
// Clear any existing timeout
228-
if (scrollTimeoutRef.current !== null) {
229-
clearTimeout(scrollTimeoutRef.current)
230-
scrollTimeoutRef.current = null
231-
}
232-
233-
let attempts = 0
234-
235-
const tryScroll = () => {
236-
attempts++
237-
const element = document.querySelector(`[data-item-id="${elementId}"]`)
238-
const container = scrollContainerRef.current
239-
240-
if (element && container) {
241-
const elementRect = element.getBoundingClientRect()
242-
const containerRect = container.getBoundingClientRect()
243-
244-
// Check if element is not fully visible in the container
245-
const isAboveView = elementRect.top < containerRect.top
246-
const isBelowView = elementRect.bottom > containerRect.bottom
247-
248-
if (isAboveView || isBelowView) {
249-
element.scrollIntoView({ behavior: 'smooth', block: 'nearest' })
250-
}
251-
scrollTimeoutRef.current = null
252-
} else if (attempts < maxRetries) {
253-
// Element not in DOM yet, retry after a short delay
254-
scrollTimeoutRef.current = window.setTimeout(tryScroll, 50)
255-
} else {
256-
scrollTimeoutRef.current = null
257-
}
258-
}
259-
260-
// Start the scroll attempt after a small delay to ensure rendering.
261-
scrollTimeoutRef.current = window.setTimeout(tryScroll, 50)
262-
},
263-
[scrollContainerRef]
264-
)
265-
266-
// Cleanup timeouts on unmount
267-
useEffect(() => {
268-
return () => {
269-
if (scrollTimeoutRef.current !== null) {
270-
clearTimeout(scrollTimeoutRef.current)
219+
const scrollToElement = useCallback((elementId: string) => {
220+
requestAnimationFrame(() => {
221+
const element = document.querySelector(`[data-item-id="${elementId}"]`)
222+
const container = scrollContainerRef.current
223+
if (!element || !container) return
224+
225+
const { top: elTop, bottom: elBottom } = element.getBoundingClientRect()
226+
const { top: ctTop, bottom: ctBottom } = container.getBoundingClientRect()
227+
228+
// Only scroll if element is completely off-screen
229+
if (elBottom <= ctTop || elTop >= ctBottom) {
230+
element.scrollIntoView({ behavior: 'smooth', block: 'center' })
271231
}
272-
}
232+
})
273233
}, [])
274234

275235
/**

0 commit comments

Comments
 (0)