mirror of
https://github.com/langgenius/dify.git
synced 2026-05-09 12:59:18 +08:00
fix: prevent workflow preview resize observer loop (#35936)
This commit is contained in:
parent
19bf36a716
commit
271019006e
@ -213,6 +213,8 @@ export const Workflow: FC<WorkflowProps> = memo(({
|
||||
const bottomPanelHeight = useStore(s => s.bottomPanelHeight)
|
||||
const setWorkflowCanvasWidth = useStore(s => s.setWorkflowCanvasWidth)
|
||||
const setWorkflowCanvasHeight = useStore(s => s.setWorkflowCanvasHeight)
|
||||
const workflowCanvasSizeRef = useRef<{ width?: number, height?: number }>({})
|
||||
const workflowCanvasResizeFrameRef = useRef<number | undefined>(undefined)
|
||||
const controlHeight = useMemo(() => {
|
||||
if (!workflowCanvasHeight)
|
||||
return '100%'
|
||||
@ -222,15 +224,33 @@ export const Workflow: FC<WorkflowProps> = memo(({
|
||||
// update workflow Canvas width and height
|
||||
useEffect(() => {
|
||||
if (workflowContainerRef.current) {
|
||||
const updateWorkflowCanvasSize = (width: number, height: number) => {
|
||||
if (workflowCanvasSizeRef.current.width === width && workflowCanvasSizeRef.current.height === height)
|
||||
return
|
||||
|
||||
workflowCanvasSizeRef.current = { width, height }
|
||||
if (workflowCanvasResizeFrameRef.current)
|
||||
cancelAnimationFrame(workflowCanvasResizeFrameRef.current)
|
||||
|
||||
workflowCanvasResizeFrameRef.current = requestAnimationFrame(() => {
|
||||
workflowCanvasResizeFrameRef.current = undefined
|
||||
setWorkflowCanvasWidth(width)
|
||||
setWorkflowCanvasHeight(height)
|
||||
})
|
||||
}
|
||||
|
||||
const resizeContainerObserver = new ResizeObserver((entries) => {
|
||||
for (const entry of entries) {
|
||||
const { inlineSize, blockSize } = entry.borderBoxSize[0]!
|
||||
setWorkflowCanvasWidth(inlineSize)
|
||||
setWorkflowCanvasHeight(blockSize)
|
||||
updateWorkflowCanvasSize(inlineSize, blockSize)
|
||||
}
|
||||
})
|
||||
resizeContainerObserver.observe(workflowContainerRef.current)
|
||||
return () => {
|
||||
if (workflowCanvasResizeFrameRef.current) {
|
||||
cancelAnimationFrame(workflowCanvasResizeFrameRef.current)
|
||||
workflowCanvasResizeFrameRef.current = undefined
|
||||
}
|
||||
resizeContainerObserver.disconnect()
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { act, screen } from '@testing-library/react'
|
||||
import { act, screen, waitFor } from '@testing-library/react'
|
||||
import { createNode } from '../../__tests__/fixtures'
|
||||
import { renderWorkflowFlowComponent } from '../../__tests__/workflow-test-env'
|
||||
import { BlockEnum } from '../../types'
|
||||
@ -110,7 +110,7 @@ describe('Operator', () => {
|
||||
expect(container.querySelector('div[style*="width: auto"]')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should sync the observed panel size back into the workflow store and disconnect on unmount', () => {
|
||||
it('should sync the observed panel size back into the workflow store and disconnect on unmount', async () => {
|
||||
const { store, unmount } = renderOperator({
|
||||
workflowCanvasWidth: 900,
|
||||
rightPanelWidth: 260,
|
||||
@ -126,8 +126,10 @@ describe('Operator', () => {
|
||||
], {} as ResizeObserver)
|
||||
})
|
||||
|
||||
expect(store.getState().bottomPanelWidth).toBe(512)
|
||||
expect(store.getState().bottomPanelHeight).toBe(188)
|
||||
await waitFor(() => {
|
||||
expect(store.getState().bottomPanelWidth).toBe(512)
|
||||
expect(store.getState().bottomPanelHeight).toBe(188)
|
||||
})
|
||||
|
||||
unmount()
|
||||
|
||||
|
||||
@ -15,6 +15,8 @@ type OperatorProps = {
|
||||
|
||||
const Operator = ({ handleUndo, handleRedo }: OperatorProps) => {
|
||||
const bottomPanelRef = useRef<HTMLDivElement>(null)
|
||||
const bottomPanelSizeRef = useRef<{ width?: number, height?: number }>({})
|
||||
const bottomPanelResizeFrameRef = useRef<number | undefined>(undefined)
|
||||
const [showMiniMap, setShowMiniMap] = useState(true)
|
||||
const showUserCursors = useStore(s => s.showUserCursors)
|
||||
const setShowUserCursors = useStore(s => s.setShowUserCursors)
|
||||
@ -55,15 +57,33 @@ const Operator = ({ handleUndo, handleRedo }: OperatorProps) => {
|
||||
// update bottom panel height
|
||||
useEffect(() => {
|
||||
if (bottomPanelRef.current) {
|
||||
const updateBottomPanelSize = (width: number, height: number) => {
|
||||
if (bottomPanelSizeRef.current.width === width && bottomPanelSizeRef.current.height === height)
|
||||
return
|
||||
|
||||
bottomPanelSizeRef.current = { width, height }
|
||||
if (bottomPanelResizeFrameRef.current)
|
||||
cancelAnimationFrame(bottomPanelResizeFrameRef.current)
|
||||
|
||||
bottomPanelResizeFrameRef.current = requestAnimationFrame(() => {
|
||||
bottomPanelResizeFrameRef.current = undefined
|
||||
setBottomPanelWidth(width)
|
||||
setBottomPanelHeight(height)
|
||||
})
|
||||
}
|
||||
|
||||
const resizeContainerObserver = new ResizeObserver((entries) => {
|
||||
for (const entry of entries) {
|
||||
const { inlineSize, blockSize } = entry.borderBoxSize[0]!
|
||||
setBottomPanelWidth(inlineSize)
|
||||
setBottomPanelHeight(blockSize)
|
||||
updateBottomPanelSize(inlineSize, blockSize)
|
||||
}
|
||||
})
|
||||
resizeContainerObserver.observe(bottomPanelRef.current)
|
||||
return () => {
|
||||
if (bottomPanelResizeFrameRef.current) {
|
||||
cancelAnimationFrame(bottomPanelResizeFrameRef.current)
|
||||
bottomPanelResizeFrameRef.current = undefined
|
||||
}
|
||||
resizeContainerObserver.disconnect()
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import { act, render, screen, waitFor } from '@testing-library/react'
|
||||
import * as React from 'react'
|
||||
import Panel from '../index'
|
||||
|
||||
@ -232,17 +232,55 @@ describe('Panel', () => {
|
||||
expect(mockPanelStoreState.setPreviewPanelWidth).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should derive observer widths from border-box, content-rect, and fallback values and disconnect on unmount', () => {
|
||||
mockResizeModes = ['borderBox', 'contentRect', 'fallback']
|
||||
|
||||
it('should derive observer widths from border-box, content-rect, and fallback values and disconnect on unmount', async () => {
|
||||
const { unmount } = render(<Panel />)
|
||||
|
||||
expect(mockPanelStoreState.setRightPanelWidth).toHaveBeenCalledWith(720)
|
||||
expect(mockPanelStoreState.setRightPanelWidth).toHaveBeenCalledWith(530)
|
||||
expect(mockPanelStoreState.setRightPanelWidth).toHaveBeenCalledWith(640)
|
||||
expect(mockPanelStoreState.setOtherPanelWidth).toHaveBeenCalledWith(720)
|
||||
expect(mockPanelStoreState.setOtherPanelWidth).toHaveBeenCalledWith(530)
|
||||
expect(mockPanelStoreState.setOtherPanelWidth).toHaveBeenCalledWith(640)
|
||||
await waitFor(() => {
|
||||
expect(mockPanelStoreState.setRightPanelWidth).toHaveBeenCalledWith(640)
|
||||
expect(mockPanelStoreState.setOtherPanelWidth).toHaveBeenCalledWith(640)
|
||||
})
|
||||
|
||||
vi.mocked(mockPanelStoreState.setRightPanelWidth).mockClear()
|
||||
vi.mocked(mockPanelStoreState.setOtherPanelWidth).mockClear()
|
||||
|
||||
act(() => {
|
||||
mockResizeObservers.forEach((observer) => {
|
||||
observer.callback([createResizeEntry('borderBox')], observer as unknown as ResizeObserver)
|
||||
})
|
||||
})
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockPanelStoreState.setRightPanelWidth).toHaveBeenCalledWith(720)
|
||||
expect(mockPanelStoreState.setOtherPanelWidth).toHaveBeenCalledWith(720)
|
||||
})
|
||||
|
||||
vi.mocked(mockPanelStoreState.setRightPanelWidth).mockClear()
|
||||
vi.mocked(mockPanelStoreState.setOtherPanelWidth).mockClear()
|
||||
|
||||
act(() => {
|
||||
mockResizeObservers.forEach((observer) => {
|
||||
observer.callback([createResizeEntry('contentRect')], observer as unknown as ResizeObserver)
|
||||
})
|
||||
})
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockPanelStoreState.setRightPanelWidth).toHaveBeenCalledWith(530)
|
||||
expect(mockPanelStoreState.setOtherPanelWidth).toHaveBeenCalledWith(530)
|
||||
})
|
||||
|
||||
vi.mocked(mockPanelStoreState.setRightPanelWidth).mockClear()
|
||||
vi.mocked(mockPanelStoreState.setOtherPanelWidth).mockClear()
|
||||
|
||||
act(() => {
|
||||
mockResizeObservers.forEach((observer) => {
|
||||
observer.callback([createResizeEntry('fallback')], observer as unknown as ResizeObserver)
|
||||
})
|
||||
})
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockPanelStoreState.setRightPanelWidth).toHaveBeenCalledWith(640)
|
||||
expect(mockPanelStoreState.setOtherPanelWidth).toHaveBeenCalledWith(640)
|
||||
})
|
||||
|
||||
unmount()
|
||||
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import type { FC } from 'react'
|
||||
import type { VersionHistoryPanelProps } from '@/app/components/workflow/panel/version-history-panel'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { memo, useCallback, useEffect, useRef } from 'react'
|
||||
import { memo, useEffect, useRef } from 'react'
|
||||
import { useStore as useReactflow } from 'reactflow'
|
||||
import { useShallow } from 'zustand/react/shallow'
|
||||
import dynamic from '@/next/dynamic'
|
||||
@ -34,35 +34,50 @@ const getEntryWidth = (entry: ResizeObserverEntry, element: HTMLElement): number
|
||||
return element.getBoundingClientRect().width
|
||||
}
|
||||
|
||||
const useResizeObserver = (
|
||||
callback: (width: number) => void,
|
||||
dependencies: React.DependencyList = [],
|
||||
) => {
|
||||
const useResizeObserver = (callback: (width: number) => void) => {
|
||||
const elementRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
const stableCallback = useCallback(callback, [callback])
|
||||
const widthRef = useRef<number | undefined>(undefined)
|
||||
const animationFrameRef = useRef<number | undefined>(undefined)
|
||||
|
||||
useEffect(() => {
|
||||
const element = elementRef.current
|
||||
if (!element)
|
||||
return
|
||||
|
||||
widthRef.current = undefined
|
||||
|
||||
const updateWidth = (width: number) => {
|
||||
if (widthRef.current === width)
|
||||
return
|
||||
|
||||
widthRef.current = width
|
||||
if (animationFrameRef.current)
|
||||
cancelAnimationFrame(animationFrameRef.current)
|
||||
|
||||
animationFrameRef.current = requestAnimationFrame(() => {
|
||||
animationFrameRef.current = undefined
|
||||
callback(width)
|
||||
})
|
||||
}
|
||||
|
||||
const resizeObserver = new ResizeObserver((entries) => {
|
||||
for (const entry of entries) {
|
||||
const width = getEntryWidth(entry, element)
|
||||
stableCallback(width)
|
||||
}
|
||||
for (const entry of entries)
|
||||
updateWidth(getEntryWidth(entry, element))
|
||||
})
|
||||
|
||||
resizeObserver.observe(element)
|
||||
|
||||
const initialWidth = element.getBoundingClientRect().width
|
||||
stableCallback(initialWidth)
|
||||
updateWidth(initialWidth)
|
||||
|
||||
return () => {
|
||||
if (animationFrameRef.current) {
|
||||
cancelAnimationFrame(animationFrameRef.current)
|
||||
animationFrameRef.current = undefined
|
||||
}
|
||||
resizeObserver.disconnect()
|
||||
}
|
||||
}, [stableCallback, ...dependencies])
|
||||
}, [callback])
|
||||
return elementRef
|
||||
}
|
||||
|
||||
@ -113,15 +128,9 @@ const Panel: FC<PanelProps> = ({
|
||||
const setRightPanelWidth = useStore(s => s.setRightPanelWidth)
|
||||
const setOtherPanelWidth = useStore(s => s.setOtherPanelWidth)
|
||||
|
||||
const rightPanelRef = useResizeObserver(
|
||||
setRightPanelWidth,
|
||||
[setRightPanelWidth, selectedNode, showEnvPanel, showWorkflowVersionHistoryPanel],
|
||||
)
|
||||
const rightPanelRef = useResizeObserver(setRightPanelWidth)
|
||||
|
||||
const otherPanelRef = useResizeObserver(
|
||||
setOtherPanelWidth,
|
||||
[setOtherPanelWidth, showEnvPanel, showWorkflowVersionHistoryPanel],
|
||||
)
|
||||
const otherPanelRef = useResizeObserver(setOtherPanelWidth)
|
||||
|
||||
return (
|
||||
<div
|
||||
|
||||
@ -27,22 +27,32 @@ export type LayoutSliceShape = {
|
||||
export const createLayoutSlice: StateCreator<LayoutSliceShape> = set => ({
|
||||
workflowCanvasWidth: undefined,
|
||||
workflowCanvasHeight: undefined,
|
||||
setWorkflowCanvasWidth: width => set(() => ({ workflowCanvasWidth: width })),
|
||||
setWorkflowCanvasHeight: height => set(() => ({ workflowCanvasHeight: height })),
|
||||
setWorkflowCanvasWidth: width => set(state =>
|
||||
state.workflowCanvasWidth === width ? state : ({ workflowCanvasWidth: width })),
|
||||
setWorkflowCanvasHeight: height => set(state =>
|
||||
state.workflowCanvasHeight === height ? state : ({ workflowCanvasHeight: height })),
|
||||
rightPanelWidth: undefined,
|
||||
setRightPanelWidth: width => set(() => ({ rightPanelWidth: width })),
|
||||
setRightPanelWidth: width => set(state =>
|
||||
state.rightPanelWidth === width ? state : ({ rightPanelWidth: width })),
|
||||
nodePanelWidth: localStorage.getItem('workflow-node-panel-width') ? Number.parseFloat(localStorage.getItem('workflow-node-panel-width')!) : 400,
|
||||
setNodePanelWidth: width => set(() => ({ nodePanelWidth: width })),
|
||||
setNodePanelWidth: width => set(state =>
|
||||
state.nodePanelWidth === width ? state : ({ nodePanelWidth: width })),
|
||||
previewPanelWidth: localStorage.getItem('debug-and-preview-panel-width') ? Number.parseFloat(localStorage.getItem('debug-and-preview-panel-width')!) : 400,
|
||||
setPreviewPanelWidth: width => set(() => ({ previewPanelWidth: width })),
|
||||
setPreviewPanelWidth: width => set(state =>
|
||||
state.previewPanelWidth === width ? state : ({ previewPanelWidth: width })),
|
||||
otherPanelWidth: 400,
|
||||
setOtherPanelWidth: width => set(() => ({ otherPanelWidth: width })),
|
||||
setOtherPanelWidth: width => set(state =>
|
||||
state.otherPanelWidth === width ? state : ({ otherPanelWidth: width })),
|
||||
bottomPanelWidth: 480,
|
||||
setBottomPanelWidth: width => set(() => ({ bottomPanelWidth: width })),
|
||||
setBottomPanelWidth: width => set(state =>
|
||||
state.bottomPanelWidth === width ? state : ({ bottomPanelWidth: width })),
|
||||
bottomPanelHeight: 324,
|
||||
setBottomPanelHeight: height => set(() => ({ bottomPanelHeight: height })),
|
||||
setBottomPanelHeight: height => set(state =>
|
||||
state.bottomPanelHeight === height ? state : ({ bottomPanelHeight: height })),
|
||||
variableInspectPanelHeight: localStorage.getItem('workflow-variable-inpsect-panel-height') ? Number.parseFloat(localStorage.getItem('workflow-variable-inpsect-panel-height')!) : 320,
|
||||
setVariableInspectPanelHeight: height => set(() => ({ variableInspectPanelHeight: height })),
|
||||
setVariableInspectPanelHeight: height => set(state =>
|
||||
state.variableInspectPanelHeight === height ? state : ({ variableInspectPanelHeight: height })),
|
||||
maximizeCanvas: localStorage.getItem('workflow-canvas-maximize') === 'true',
|
||||
setMaximizeCanvas: maximize => set(() => ({ maximizeCanvas: maximize })),
|
||||
setMaximizeCanvas: maximize => set(state =>
|
||||
state.maximizeCanvas === maximize ? state : ({ maximizeCanvas: maximize })),
|
||||
})
|
||||
|
||||
Loading…
Reference in New Issue
Block a user