mirror of
https://github.com/langgenius/dify.git
synced 2025-12-20 14:42:37 +00:00
Compare commits
1 Commits
deploy/end
...
feat/itera
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2e0a7857f0 |
@@ -13,7 +13,7 @@ import {
|
|||||||
useWorkflowStore,
|
useWorkflowStore,
|
||||||
} from './store'
|
} from './store'
|
||||||
import { WorkflowHistoryEvent, useNodesInteractions, useWorkflowHistory } from './hooks'
|
import { WorkflowHistoryEvent, useNodesInteractions, useWorkflowHistory } from './hooks'
|
||||||
import { CUSTOM_NODE } from './constants'
|
import { CUSTOM_NODE, ITERATION_PADDING } from './constants'
|
||||||
import { getIterationStartNode, getLoopStartNode } from './utils'
|
import { getIterationStartNode, getLoopStartNode } from './utils'
|
||||||
import CustomNode from './nodes'
|
import CustomNode from './nodes'
|
||||||
import CustomNoteNode from './note-node'
|
import CustomNoteNode from './note-node'
|
||||||
@@ -41,7 +41,33 @@ const CandidateNode = () => {
|
|||||||
} = store.getState()
|
} = store.getState()
|
||||||
const { screenToFlowPosition } = reactflow
|
const { screenToFlowPosition } = reactflow
|
||||||
const nodes = getNodes()
|
const nodes = getNodes()
|
||||||
const { x, y } = screenToFlowPosition({ x: mousePosition.pageX, y: mousePosition.pageY })
|
// Get mouse position in flow coordinates (this is where the top-left corner should be)
|
||||||
|
let { x, y } = screenToFlowPosition({ x: mousePosition.pageX, y: mousePosition.pageY })
|
||||||
|
|
||||||
|
// If the node has a parent (e.g., inside iteration), apply constraints and convert to relative position
|
||||||
|
if (candidateNode.parentId) {
|
||||||
|
const parentNode = nodes.find(node => node.id === candidateNode.parentId)
|
||||||
|
if (parentNode && parentNode.position) {
|
||||||
|
// Apply boundary constraints for iteration nodes
|
||||||
|
if (candidateNode.data.isInIteration) {
|
||||||
|
const nodeWidth = candidateNode.width || 0
|
||||||
|
const nodeHeight = candidateNode.height || 0
|
||||||
|
const minX = parentNode.position.x + ITERATION_PADDING.left
|
||||||
|
const maxX = parentNode.position.x + (parentNode.width || 0) - ITERATION_PADDING.right - nodeWidth
|
||||||
|
const minY = parentNode.position.y + ITERATION_PADDING.top
|
||||||
|
const maxY = parentNode.position.y + (parentNode.height || 0) - ITERATION_PADDING.bottom - nodeHeight
|
||||||
|
|
||||||
|
// Constrain position
|
||||||
|
x = Math.max(minX, Math.min(maxX, x))
|
||||||
|
y = Math.max(minY, Math.min(maxY, y))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert to relative position
|
||||||
|
x = x - parentNode.position.x
|
||||||
|
y = y - parentNode.position.y
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const newNodes = produce(nodes, (draft) => {
|
const newNodes = produce(nodes, (draft) => {
|
||||||
draft.push({
|
draft.push({
|
||||||
...candidateNode,
|
...candidateNode,
|
||||||
@@ -59,6 +85,20 @@ const CandidateNode = () => {
|
|||||||
|
|
||||||
if (candidateNode.data.type === BlockEnum.Loop)
|
if (candidateNode.data.type === BlockEnum.Loop)
|
||||||
draft.push(getLoopStartNode(candidateNode.id))
|
draft.push(getLoopStartNode(candidateNode.id))
|
||||||
|
|
||||||
|
// Update parent iteration node's _children array
|
||||||
|
if (candidateNode.parentId && candidateNode.data.isInIteration) {
|
||||||
|
const parentNode = draft.find(node => node.id === candidateNode.parentId)
|
||||||
|
if (parentNode && parentNode.data.type === BlockEnum.Iteration) {
|
||||||
|
if (!parentNode.data._children)
|
||||||
|
parentNode.data._children = []
|
||||||
|
|
||||||
|
parentNode.data._children.push({
|
||||||
|
nodeId: candidateNode.id,
|
||||||
|
nodeType: candidateNode.data.type,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
setNodes(newNodes)
|
setNodes(newNodes)
|
||||||
if (candidateNode.type === CUSTOM_NOTE_NODE)
|
if (candidateNode.type === CUSTOM_NOTE_NODE)
|
||||||
@@ -84,6 +124,34 @@ const CandidateNode = () => {
|
|||||||
if (!candidateNode)
|
if (!candidateNode)
|
||||||
return null
|
return null
|
||||||
|
|
||||||
|
// Apply boundary constraints if node is inside iteration
|
||||||
|
if (candidateNode.parentId && candidateNode.data.isInIteration) {
|
||||||
|
const { getNodes } = store.getState()
|
||||||
|
const nodes = getNodes()
|
||||||
|
const parentNode = nodes.find(node => node.id === candidateNode.parentId)
|
||||||
|
|
||||||
|
if (parentNode && parentNode.position) {
|
||||||
|
const { screenToFlowPosition, flowToScreenPosition } = reactflow
|
||||||
|
// Get mouse position in flow coordinates
|
||||||
|
const flowPosition = screenToFlowPosition({ x: mousePosition.pageX, y: mousePosition.pageY })
|
||||||
|
|
||||||
|
// Calculate boundaries in flow coordinates
|
||||||
|
const nodeWidth = candidateNode.width || 0
|
||||||
|
const nodeHeight = candidateNode.height || 0
|
||||||
|
const minX = parentNode.position.x + ITERATION_PADDING.left
|
||||||
|
const maxX = parentNode.position.x + (parentNode.width || 0) - ITERATION_PADDING.right - nodeWidth
|
||||||
|
const minY = parentNode.position.y + ITERATION_PADDING.top
|
||||||
|
const maxY = parentNode.position.y + (parentNode.height || 0) - ITERATION_PADDING.bottom - nodeHeight
|
||||||
|
|
||||||
|
// Constrain position
|
||||||
|
const constrainedX = Math.max(minX, Math.min(maxX, flowPosition.x))
|
||||||
|
const constrainedY = Math.max(minY, Math.min(maxY, flowPosition.y))
|
||||||
|
|
||||||
|
// Convert back to screen coordinates
|
||||||
|
flowToScreenPosition({ x: constrainedX, y: constrainedY })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className='absolute z-10'
|
className='absolute z-10'
|
||||||
|
|||||||
@@ -737,7 +737,7 @@ export const useNodesInteractions = () => {
|
|||||||
targetHandle = 'target',
|
targetHandle = 'target',
|
||||||
toolDefaultValue,
|
toolDefaultValue,
|
||||||
},
|
},
|
||||||
{ prevNodeId, prevNodeSourceHandle, nextNodeId, nextNodeTargetHandle },
|
{ prevNodeId, prevNodeSourceHandle, nextNodeId, nextNodeTargetHandle, skipAutoConnect },
|
||||||
) => {
|
) => {
|
||||||
if (getNodesReadOnly()) return
|
if (getNodesReadOnly()) return
|
||||||
|
|
||||||
@@ -830,7 +830,7 @@ export const useNodesInteractions = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let newEdge = null
|
let newEdge = null
|
||||||
if (nodeType !== BlockEnum.DataSource) {
|
if (nodeType !== BlockEnum.DataSource && !skipAutoConnect) {
|
||||||
newEdge = {
|
newEdge = {
|
||||||
id: `${prevNodeId}-${prevNodeSourceHandle}-${newNode.id}-${targetHandle}`,
|
id: `${prevNodeId}-${prevNodeSourceHandle}-${newNode.id}-${targetHandle}`,
|
||||||
type: CUSTOM_EDGE,
|
type: CUSTOM_EDGE,
|
||||||
@@ -970,6 +970,7 @@ export const useNodesInteractions = () => {
|
|||||||
nodeType !== BlockEnum.IfElse
|
nodeType !== BlockEnum.IfElse
|
||||||
&& nodeType !== BlockEnum.QuestionClassifier
|
&& nodeType !== BlockEnum.QuestionClassifier
|
||||||
&& nodeType !== BlockEnum.LoopEnd
|
&& nodeType !== BlockEnum.LoopEnd
|
||||||
|
&& !skipAutoConnect
|
||||||
) {
|
) {
|
||||||
newEdge = {
|
newEdge = {
|
||||||
id: `${newNode.id}-${sourceHandle}-${nextNodeId}-${nextNodeTargetHandle}`,
|
id: `${newNode.id}-${sourceHandle}-${nextNodeId}-${nextNodeTargetHandle}`,
|
||||||
@@ -1119,7 +1120,7 @@ export const useNodesInteractions = () => {
|
|||||||
)
|
)
|
||||||
let newPrevEdge = null
|
let newPrevEdge = null
|
||||||
|
|
||||||
if (nodeType !== BlockEnum.DataSource) {
|
if (nodeType !== BlockEnum.DataSource && !skipAutoConnect) {
|
||||||
newPrevEdge = {
|
newPrevEdge = {
|
||||||
id: `${prevNodeId}-${prevNodeSourceHandle}-${newNode.id}-${targetHandle}`,
|
id: `${prevNodeId}-${prevNodeSourceHandle}-${newNode.id}-${targetHandle}`,
|
||||||
type: CUSTOM_EDGE,
|
type: CUSTOM_EDGE,
|
||||||
@@ -1159,6 +1160,7 @@ export const useNodesInteractions = () => {
|
|||||||
nodeType !== BlockEnum.IfElse
|
nodeType !== BlockEnum.IfElse
|
||||||
&& nodeType !== BlockEnum.QuestionClassifier
|
&& nodeType !== BlockEnum.QuestionClassifier
|
||||||
&& nodeType !== BlockEnum.LoopEnd
|
&& nodeType !== BlockEnum.LoopEnd
|
||||||
|
&& !skipAutoConnect
|
||||||
) {
|
) {
|
||||||
newNextEdge = {
|
newNextEdge = {
|
||||||
id: `${newNode.id}-${sourceHandle}-${nextNodeId}-${nextNodeTargetHandle}`,
|
id: `${newNode.id}-${sourceHandle}-${nextNodeId}-${nextNodeTargetHandle}`,
|
||||||
|
|||||||
@@ -15,7 +15,9 @@ import {
|
|||||||
useNodesSyncDraft,
|
useNodesSyncDraft,
|
||||||
} from '@/app/components/workflow/hooks'
|
} from '@/app/components/workflow/hooks'
|
||||||
import ShortcutsName from '@/app/components/workflow/shortcuts-name'
|
import ShortcutsName from '@/app/components/workflow/shortcuts-name'
|
||||||
import type { Node } from '@/app/components/workflow/types'
|
import { BlockEnum, type Node } from '@/app/components/workflow/types'
|
||||||
|
import PanelAddBlock from '@/app/components/workflow/nodes/iteration/panel-add-block'
|
||||||
|
import type { IterationNodeType } from '@/app/components/workflow/nodes/iteration/types'
|
||||||
|
|
||||||
type PanelOperatorPopupProps = {
|
type PanelOperatorPopupProps = {
|
||||||
id: string
|
id: string
|
||||||
@@ -51,6 +53,9 @@ const PanelOperatorPopup = ({
|
|||||||
(showChangeBlock || canRunBySingle(data.type, isChildNode)) && (
|
(showChangeBlock || canRunBySingle(data.type, isChildNode)) && (
|
||||||
<>
|
<>
|
||||||
<div className='p-1'>
|
<div className='p-1'>
|
||||||
|
{data.type === BlockEnum.Iteration && (
|
||||||
|
<PanelAddBlock iterationNodeData={data as IterationNodeType} onClosePopup={onClosePopup}/>
|
||||||
|
)}
|
||||||
{
|
{
|
||||||
canRunBySingle(data.type, isChildNode) && (
|
canRunBySingle(data.type, isChildNode) && (
|
||||||
<div
|
<div
|
||||||
|
|||||||
116
web/app/components/workflow/nodes/iteration/panel-add-block.tsx
Normal file
116
web/app/components/workflow/nodes/iteration/panel-add-block.tsx
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
import {
|
||||||
|
memo,
|
||||||
|
useCallback,
|
||||||
|
useState,
|
||||||
|
} from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import type { OffsetOptions } from '@floating-ui/react'
|
||||||
|
import { useStoreApi } from 'reactflow'
|
||||||
|
|
||||||
|
import BlockSelector from '@/app/components/workflow/block-selector'
|
||||||
|
import type {
|
||||||
|
OnSelectBlock,
|
||||||
|
} from '@/app/components/workflow/types'
|
||||||
|
import {
|
||||||
|
BlockEnum,
|
||||||
|
} from '@/app/components/workflow/types'
|
||||||
|
import { useAvailableBlocks, useNodesMetaData, useNodesReadOnly, usePanelInteractions } from '../../hooks'
|
||||||
|
import type { IterationNodeType } from './types'
|
||||||
|
import { useWorkflowStore } from '../../store'
|
||||||
|
import { generateNewNode, getNodeCustomTypeByNodeDataType } from '../../utils'
|
||||||
|
import { ITERATION_CHILDREN_Z_INDEX } from '../../constants'
|
||||||
|
|
||||||
|
type AddBlockProps = {
|
||||||
|
renderTrigger?: (open: boolean) => React.ReactNode
|
||||||
|
offset?: OffsetOptions
|
||||||
|
iterationNodeData: IterationNodeType
|
||||||
|
onClosePopup: () => void
|
||||||
|
}
|
||||||
|
const AddBlock = ({
|
||||||
|
offset,
|
||||||
|
iterationNodeData,
|
||||||
|
onClosePopup,
|
||||||
|
}: AddBlockProps) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const store = useStoreApi()
|
||||||
|
const workflowStore = useWorkflowStore()
|
||||||
|
const { nodesReadOnly } = useNodesReadOnly()
|
||||||
|
const { handlePaneContextmenuCancel } = usePanelInteractions()
|
||||||
|
const [open, setOpen] = useState(false)
|
||||||
|
const { availableNextBlocks } = useAvailableBlocks(BlockEnum.Start, false)
|
||||||
|
const { nodesMap: nodesMetaDataMap } = useNodesMetaData()
|
||||||
|
|
||||||
|
const handleOpenChange = useCallback((open: boolean) => {
|
||||||
|
setOpen(open)
|
||||||
|
if (!open)
|
||||||
|
handlePaneContextmenuCancel()
|
||||||
|
}, [handlePaneContextmenuCancel])
|
||||||
|
|
||||||
|
const handleSelect = useCallback<OnSelectBlock>((type, toolDefaultValue) => {
|
||||||
|
const { getNodes } = store.getState()
|
||||||
|
const nodes = getNodes()
|
||||||
|
const nodesWithSameType = nodes.filter(node => node.data.type === type)
|
||||||
|
const { defaultValue } = nodesMetaDataMap![type]
|
||||||
|
|
||||||
|
// Find the parent iteration node
|
||||||
|
const parentIterationNode = nodes.find(node => node.data.start_node_id === iterationNodeData.start_node_id)
|
||||||
|
|
||||||
|
const { newNode } = generateNewNode({
|
||||||
|
type: getNodeCustomTypeByNodeDataType(type),
|
||||||
|
data: {
|
||||||
|
...(defaultValue as any),
|
||||||
|
title: nodesWithSameType.length > 0 ? `${defaultValue.title} ${nodesWithSameType.length + 1}` : defaultValue.title,
|
||||||
|
...toolDefaultValue,
|
||||||
|
_isCandidate: true,
|
||||||
|
// Set iteration-specific properties
|
||||||
|
isInIteration: true,
|
||||||
|
iteration_id: parentIterationNode?.id,
|
||||||
|
},
|
||||||
|
position: {
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// Set parent and z-index for iteration child
|
||||||
|
if (parentIterationNode) {
|
||||||
|
newNode.parentId = parentIterationNode.id
|
||||||
|
newNode.extent = 'parent' as any
|
||||||
|
newNode.zIndex = ITERATION_CHILDREN_Z_INDEX
|
||||||
|
}
|
||||||
|
|
||||||
|
workflowStore.setState({
|
||||||
|
candidateNode: newNode,
|
||||||
|
})
|
||||||
|
onClosePopup()
|
||||||
|
}, [store, workflowStore, nodesMetaDataMap, iterationNodeData.start_node_id, onClosePopup])
|
||||||
|
|
||||||
|
const renderTrigger = () => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className='flex h-8 cursor-pointer items-center justify-between rounded-lg px-3 text-sm text-text-secondary hover:bg-state-base-hover'
|
||||||
|
>
|
||||||
|
{t('workflow.common.addBlock')}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BlockSelector
|
||||||
|
open={open}
|
||||||
|
onOpenChange={handleOpenChange}
|
||||||
|
disabled={nodesReadOnly}
|
||||||
|
onSelect={handleSelect}
|
||||||
|
placement='right-start'
|
||||||
|
offset={offset ?? {
|
||||||
|
mainAxis: 4,
|
||||||
|
crossAxis: -8,
|
||||||
|
}}
|
||||||
|
trigger={renderTrigger}
|
||||||
|
popupClassName='!min-w-[256px]'
|
||||||
|
availableBlocksTypes={availableNextBlocks}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default memo(AddBlock)
|
||||||
@@ -379,6 +379,7 @@ export type OnNodeAdd = (
|
|||||||
prevNodeSourceHandle?: string
|
prevNodeSourceHandle?: string
|
||||||
nextNodeId?: string
|
nextNodeId?: string
|
||||||
nextNodeTargetHandle?: string
|
nextNodeTargetHandle?: string
|
||||||
|
skipAutoConnect?: boolean
|
||||||
},
|
},
|
||||||
) => void
|
) => void
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user