Merge branch 'main' into tp

This commit is contained in:
JzoNg 2026-05-09 09:49:36 +08:00
commit e9070daaaa
6 changed files with 148 additions and 49 deletions

View File

@ -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()
}
}

View File

@ -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()

View File

@ -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()
}
}

View File

@ -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()

View File

@ -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

View File

@ -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 })),
})