fix(web): snippet graph view port

This commit is contained in:
JzoNg 2026-04-20 16:39:38 +08:00
parent bcd87ddc58
commit f5112928b3
3 changed files with 94 additions and 41 deletions

View File

@ -1,6 +1,7 @@
import type { WorkflowProps } from '@/app/components/workflow'
import type { SnippetDetailPayload, SnippetInputField } from '@/models/snippet'
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
import { fireEvent, screen, waitFor } from '@testing-library/react'
import { renderWorkflowComponent } from '@/app/components/workflow/__tests__/workflow-test-env'
import { PipelineInputVarType } from '@/models/pipeline'
import SnippetMain from '../snippet-main'
@ -191,7 +192,7 @@ const payload: SnippetDetailPayload = {
}
const renderSnippetMain = () => {
return render(
return renderWorkflowComponent(
<SnippetMain
payload={payload}
snippetId="snippet-1"
@ -206,7 +207,7 @@ describe('SnippetMain', () => {
beforeEach(() => {
vi.clearAllMocks()
mockSyncInputFieldsDraft.mockResolvedValue(undefined)
mockPublishSnippetMutateAsync.mockResolvedValue(undefined)
mockPublishSnippetMutateAsync.mockResolvedValue({ created_at: 1_744_000_000 })
capturedHooksStore = undefined
snippetDetailStoreState = {
editingField: null,

View File

@ -300,7 +300,14 @@ describe('SelectionContextmenu', () => {
createEdge({ id: 'e2', source: 'n2', target: 'n3' }),
]
const { store } = renderSelectionMenu({ nodes, edges })
const { store } = renderSelectionMenu({
nodes,
edges,
initialStoreState: {
workflowCanvasWidth: 800,
workflowCanvasHeight: 600,
},
})
act(() => {
store.setState({ selectionMenu: { clientX: 120, clientY: 120 } })
@ -348,7 +355,7 @@ describe('SelectionContextmenu', () => {
selected: false,
}),
],
viewport: { x: 0, y: 0, zoom: 1 },
viewport: { x: 300, y: 255, zoom: 1 },
},
},
})

View File

@ -18,7 +18,7 @@ import {
useState,
} from 'react'
import { useTranslation } from 'react-i18next'
import { useStore as useReactFlowStore, useStoreApi } from 'reactflow'
import { getNodesBounds, useStore as useReactFlowStore, useStoreApi } from 'reactflow'
import { useCollaborativeWorkflow } from '@/app/components/workflow/hooks/use-collaborative-workflow'
import { useSnippetAndEvaluationPlanAccess } from '@/hooks/use-snippet-and-evaluation-plan-access'
import { useRouter } from '@/next/navigation'
@ -67,6 +67,7 @@ type ActionMenuItem = {
}
const DEFAULT_SNIPPET_VIEWPORT: SnippetCanvasData['viewport'] = { x: 0, y: 0, zoom: 1 }
const SNIPPET_VIEWPORT_PADDING = 100
const alignMenuItems: AlignMenuItem[] = [
{ alignType: AlignType.Left, icon: 'i-ri-align-item-left-line', translationKey: 'operator.alignLeft' },
@ -228,6 +229,10 @@ const getSelectedSnippetGraph = (
nodes: Node[],
edges: Edge[],
selectedNodes: Node[],
canvasSize?: {
width?: number
height?: number
},
): SnippetCanvasData => {
const includedNodeIds = new Set(selectedNodes.map(node => node.id))
@ -257,41 +262,74 @@ const getSelectedSnippetGraph = (
const minRootX = rootNodes.length ? Math.min(...rootNodes.map(node => node.position.x)) : 0
const minRootY = rootNodes.length ? Math.min(...rootNodes.map(node => node.position.y)) : 0
return {
nodes: nodes
.filter(node => includedNodeIds.has(node.id))
.map((node) => {
const isRootNode = !node.parentId || !includedNodeIds.has(node.parentId)
const nextPosition = isRootNode
? { x: node.position.x - minRootX, y: node.position.y - minRootY }
: node.position
const snippetNodes = nodes
.filter(node => includedNodeIds.has(node.id))
.map((node) => {
const isRootNode = !node.parentId || !includedNodeIds.has(node.parentId)
const nextPosition = isRootNode
? { x: node.position.x - minRootX, y: node.position.y - minRootY }
: node.position
return {
...node,
position: nextPosition,
positionAbsolute: node.positionAbsolute
? (isRootNode
? {
x: node.positionAbsolute.x - minRootX,
y: node.positionAbsolute.y - minRootY,
}
: node.positionAbsolute)
: undefined,
selected: false,
data: {
...node.data,
selected: false,
_children: node.data._children?.filter(child => includedNodeIds.has(child.nodeId)),
},
}
}),
edges: edges
.filter(edge => includedNodeIds.has(edge.source) && includedNodeIds.has(edge.target))
.map(edge => ({
...edge,
return {
...node,
position: nextPosition,
positionAbsolute: node.positionAbsolute
? (isRootNode
? {
x: node.positionAbsolute.x - minRootX,
y: node.positionAbsolute.y - minRootY,
}
: node.positionAbsolute)
: undefined,
selected: false,
})),
viewport: DEFAULT_SNIPPET_VIEWPORT,
data: {
...node.data,
selected: false,
_children: node.data._children?.filter(child => includedNodeIds.has(child.nodeId)),
},
}
})
const snippetEdges = edges
.filter(edge => includedNodeIds.has(edge.source) && includedNodeIds.has(edge.target))
.map(edge => ({
...edge,
selected: false,
}))
const viewportWidth = canvasSize?.width
const viewportHeight = canvasSize?.height
const hasCanvasSize = !!viewportWidth && !!viewportHeight
const viewport = (() => {
if (!hasCanvasSize || !snippetNodes.length)
return DEFAULT_SNIPPET_VIEWPORT
const bounds = getNodesBounds(snippetNodes)
const paddedWidth = bounds.width + SNIPPET_VIEWPORT_PADDING
const paddedHeight = bounds.height + SNIPPET_VIEWPORT_PADDING
const zoom = Math.min(
viewportWidth / paddedWidth,
viewportHeight / paddedHeight,
1,
)
if (!Number.isFinite(zoom) || zoom <= 0)
return DEFAULT_SNIPPET_VIEWPORT
const centerX = bounds.x + bounds.width / 2
const centerY = bounds.y + bounds.height / 2
return {
x: viewportWidth / 2 - centerX * zoom,
y: viewportHeight / 2 - centerY * zoom,
zoom,
}
})()
return {
nodes: snippetNodes,
edges: snippetEdges,
viewport,
}
}
@ -356,11 +394,18 @@ const SelectionContextmenu = () => {
const nodes = store.getState().getNodes()
const { edges } = store.getState()
const {
workflowCanvasWidth,
workflowCanvasHeight,
} = workflowStore.getState()
setSelectedGraphSnapshot(getSelectedSnippetGraph(nodes, edges, selectedNodes))
setSelectedGraphSnapshot(getSelectedSnippetGraph(nodes, edges, selectedNodes, {
width: workflowCanvasWidth,
height: workflowCanvasHeight,
}))
setIsCreateSnippetDialogOpen(true)
handleSelectionContextmenuCancel()
}, [canAccessSnippetsAndEvaluation, handleSelectionContextmenuCancel, isAddToSnippetDisabled, selectedNodes, store])
}, [canAccessSnippetsAndEvaluation, handleSelectionContextmenuCancel, isAddToSnippetDisabled, selectedNodes, store, workflowStore])
const handleCloseCreateSnippetDialog = useCallback(() => {
setIsCreateSnippetDialogOpen(false)