mirror of
https://github.com/simstudioai/sim.git
synced 2026-01-09 15:07:55 -05:00
feat(sidebar): scroll to workflow/folder (#2302)
* feat(sidebar): scroll to workflow/folder * improvement: sidebar scrolling optimizations
This commit is contained in:
@@ -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(
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
})
|
||||
}, [])
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user