mirror of
https://git.mirrors.martin98.com/https://github.com/langgenius/dify.git
synced 2025-08-15 01:05:59 +08:00
Merge remote-tracking branch 'origin/feat/workflow-parallel-support' into feat/workflow-parallel-support
This commit is contained in:
commit
35d9c59a29
@ -384,7 +384,7 @@ export const useNodesInteractions = () => {
|
|||||||
handleSyncWorkflowDraft()
|
handleSyncWorkflowDraft()
|
||||||
saveStateToHistory(WorkflowHistoryEvent.NodeConnect)
|
saveStateToHistory(WorkflowHistoryEvent.NodeConnect)
|
||||||
}
|
}
|
||||||
}, [getNodesReadOnly, store, handleSyncWorkflowDraft, saveStateToHistory])
|
}, [getNodesReadOnly, store, handleSyncWorkflowDraft, saveStateToHistory, checkNestedParallelLimit])
|
||||||
|
|
||||||
const handleNodeConnectStart = useCallback<OnConnectStart>((_, { nodeId, handleType, handleId }) => {
|
const handleNodeConnectStart = useCallback<OnConnectStart>((_, { nodeId, handleType, handleId }) => {
|
||||||
if (getNodesReadOnly())
|
if (getNodesReadOnly())
|
||||||
@ -930,7 +930,7 @@ export const useNodesInteractions = () => {
|
|||||||
}
|
}
|
||||||
handleSyncWorkflowDraft()
|
handleSyncWorkflowDraft()
|
||||||
saveStateToHistory(WorkflowHistoryEvent.NodeAdd)
|
saveStateToHistory(WorkflowHistoryEvent.NodeAdd)
|
||||||
}, [getNodesReadOnly, store, t, handleSyncWorkflowDraft, saveStateToHistory, workflowStore, getAfterNodesInSameBranch])
|
}, [getNodesReadOnly, store, t, handleSyncWorkflowDraft, saveStateToHistory, workflowStore, getAfterNodesInSameBranch, checkNestedParallelLimit])
|
||||||
|
|
||||||
const handleNodeChange = useCallback((
|
const handleNodeChange = useCallback((
|
||||||
currentNodeId: string,
|
currentNodeId: string,
|
||||||
@ -1254,6 +1254,42 @@ export const useNodesInteractions = () => {
|
|||||||
saveStateToHistory(WorkflowHistoryEvent.NodeResize)
|
saveStateToHistory(WorkflowHistoryEvent.NodeResize)
|
||||||
}, [getNodesReadOnly, store, handleSyncWorkflowDraft, saveStateToHistory])
|
}, [getNodesReadOnly, store, handleSyncWorkflowDraft, saveStateToHistory])
|
||||||
|
|
||||||
|
const handleNodeDisconnect = useCallback((nodeId: string) => {
|
||||||
|
if (getNodesReadOnly())
|
||||||
|
return
|
||||||
|
|
||||||
|
const {
|
||||||
|
getNodes,
|
||||||
|
setNodes,
|
||||||
|
edges,
|
||||||
|
setEdges,
|
||||||
|
} = store.getState()
|
||||||
|
const nodes = getNodes()
|
||||||
|
const currentNode = nodes.find(node => node.id === nodeId)!
|
||||||
|
const connectedEdges = getConnectedEdges([currentNode], edges)
|
||||||
|
const nodesConnectedSourceOrTargetHandleIdsMap = getNodesConnectedSourceOrTargetHandleIdsMap(
|
||||||
|
connectedEdges.map(edge => ({ type: 'remove', edge })),
|
||||||
|
nodes,
|
||||||
|
)
|
||||||
|
const newNodes = produce(nodes, (draft: Node[]) => {
|
||||||
|
draft.forEach((node) => {
|
||||||
|
if (nodesConnectedSourceOrTargetHandleIdsMap[node.id]) {
|
||||||
|
node.data = {
|
||||||
|
...node.data,
|
||||||
|
...nodesConnectedSourceOrTargetHandleIdsMap[node.id],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
setNodes(newNodes)
|
||||||
|
const newEdges = produce(edges, (draft) => {
|
||||||
|
return draft.filter(edge => !connectedEdges.find(connectedEdge => connectedEdge.id === edge.id))
|
||||||
|
})
|
||||||
|
setEdges(newEdges)
|
||||||
|
handleSyncWorkflowDraft()
|
||||||
|
saveStateToHistory(WorkflowHistoryEvent.EdgeDelete)
|
||||||
|
}, [store, getNodesReadOnly, handleSyncWorkflowDraft, saveStateToHistory])
|
||||||
|
|
||||||
const handleHistoryBack = useCallback(() => {
|
const handleHistoryBack = useCallback(() => {
|
||||||
if (getNodesReadOnly() || getWorkflowReadOnly())
|
if (getNodesReadOnly() || getWorkflowReadOnly())
|
||||||
return
|
return
|
||||||
@ -1306,6 +1342,7 @@ export const useNodesInteractions = () => {
|
|||||||
handleNodesDuplicate,
|
handleNodesDuplicate,
|
||||||
handleNodesDelete,
|
handleNodesDelete,
|
||||||
handleNodeResize,
|
handleNodeResize,
|
||||||
|
handleNodeDisconnect,
|
||||||
handleHistoryBack,
|
handleHistoryBack,
|
||||||
handleHistoryForward,
|
handleHistoryForward,
|
||||||
}
|
}
|
||||||
|
@ -300,7 +300,6 @@ export const useWorkflow = () => {
|
|||||||
|
|
||||||
const checkNestedParallelLimit = useCallback((nodes: Node[], edges: Edge[], parentNodeId?: string) => {
|
const checkNestedParallelLimit = useCallback((nodes: Node[], edges: Edge[], parentNodeId?: string) => {
|
||||||
const parallelList = getParallelInfo(nodes, edges, parentNodeId)
|
const parallelList = getParallelInfo(nodes, edges, parentNodeId)
|
||||||
console.log(parallelList, 'parallelList')
|
|
||||||
|
|
||||||
for (let i = 0; i < parallelList.length; i++) {
|
for (let i = 0; i < parallelList.length; i++) {
|
||||||
const parallel = parallelList[i]
|
const parallel = parallelList[i]
|
||||||
@ -313,7 +312,7 @@ export const useWorkflow = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}, [])
|
}, [t, workflowStore])
|
||||||
|
|
||||||
const isValidConnection = useCallback(({ source, target }: Connection) => {
|
const isValidConnection = useCallback(({ source, target }: Connection) => {
|
||||||
const {
|
const {
|
||||||
|
@ -421,7 +421,6 @@ const WorkflowWrap = memo(() => {
|
|||||||
citation: features.retriever_resource || { enabled: false },
|
citation: features.retriever_resource || { enabled: false },
|
||||||
moderation: features.sensitive_word_avoidance || { enabled: false },
|
moderation: features.sensitive_word_avoidance || { enabled: false },
|
||||||
}
|
}
|
||||||
// getParallelInfo(nodesData, edgesData)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ReactFlowProvider>
|
<ReactFlowProvider>
|
||||||
|
@ -21,13 +21,13 @@ type AddProps = {
|
|||||||
nodeId: string
|
nodeId: string
|
||||||
nodeData: CommonNodeType
|
nodeData: CommonNodeType
|
||||||
sourceHandle: string
|
sourceHandle: string
|
||||||
branchName?: string
|
isParallel?: boolean
|
||||||
}
|
}
|
||||||
const Add = ({
|
const Add = ({
|
||||||
nodeId,
|
nodeId,
|
||||||
nodeData,
|
nodeData,
|
||||||
sourceHandle,
|
sourceHandle,
|
||||||
branchName,
|
isParallel,
|
||||||
}: AddProps) => {
|
}: AddProps) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { handleNodeAdd } = useNodesInteractions()
|
const { handleNodeAdd } = useNodesInteractions()
|
||||||
@ -57,23 +57,19 @@ const Add = ({
|
|||||||
${nodesReadOnly && '!cursor-not-allowed'}
|
${nodesReadOnly && '!cursor-not-allowed'}
|
||||||
`}
|
`}
|
||||||
>
|
>
|
||||||
{
|
|
||||||
branchName && (
|
|
||||||
<div
|
|
||||||
className='absolute left-1 right-1 -top-[7.5px] flex items-center h-3 text-[10px] text-text-placeholder font-semibold'
|
|
||||||
title={branchName.toLocaleUpperCase()}
|
|
||||||
>
|
|
||||||
<div className='inline-block px-0.5 rounded-[5px] bg-background-default truncate'>{branchName.toLocaleUpperCase()}</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
<div className='flex items-center justify-center mr-1.5 w-5 h-5 rounded-[5px] bg-background-default-dimm'>
|
<div className='flex items-center justify-center mr-1.5 w-5 h-5 rounded-[5px] bg-background-default-dimm'>
|
||||||
<RiAddLine className='w-3 h-3' />
|
<RiAddLine className='w-3 h-3' />
|
||||||
</div>
|
</div>
|
||||||
{t('workflow.panel.selectNextStep')}
|
<div className='flex items-center uppercase'>
|
||||||
|
{
|
||||||
|
isParallel
|
||||||
|
? t('workflow.common.addParallelNode')
|
||||||
|
: t('workflow.panel.selectNextStep')
|
||||||
|
}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}, [branchName, t, nodesReadOnly])
|
}, [t, nodesReadOnly, isParallel])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BlockSelector
|
<BlockSelector
|
||||||
|
@ -0,0 +1,55 @@
|
|||||||
|
import Add from './add'
|
||||||
|
import Item from './item'
|
||||||
|
import type {
|
||||||
|
CommonNodeType,
|
||||||
|
Node,
|
||||||
|
} from '@/app/components/workflow/types'
|
||||||
|
|
||||||
|
type ContainerProps = {
|
||||||
|
nodeId: string
|
||||||
|
nodeData: CommonNodeType
|
||||||
|
sourceHandle: string
|
||||||
|
nextNodes: Node[]
|
||||||
|
branchName?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const Container = ({
|
||||||
|
nodeId,
|
||||||
|
nodeData,
|
||||||
|
sourceHandle,
|
||||||
|
nextNodes,
|
||||||
|
branchName,
|
||||||
|
}: ContainerProps) => {
|
||||||
|
return (
|
||||||
|
<div className='p-0.5 space-y-0.5 rounded-[10px] bg-background-section-burn'>
|
||||||
|
{
|
||||||
|
branchName && (
|
||||||
|
<div
|
||||||
|
className='flex items-center px-2 system-2xs-semibold-uppercase text-text-tertiary truncate'
|
||||||
|
title={branchName}
|
||||||
|
>
|
||||||
|
{branchName}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
{
|
||||||
|
nextNodes.map(nextNode => (
|
||||||
|
<Item
|
||||||
|
key={nextNode.id}
|
||||||
|
nodeId={nextNode.id}
|
||||||
|
data={nextNode.data}
|
||||||
|
sourceHandle='source'
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
<Add
|
||||||
|
isParallel={!!nextNodes.length}
|
||||||
|
nodeId={nodeId}
|
||||||
|
nodeData={nodeData}
|
||||||
|
sourceHandle={sourceHandle}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Container
|
@ -1,4 +1,5 @@
|
|||||||
import { memo } from 'react'
|
import { memo, useMemo } from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
import {
|
import {
|
||||||
getConnectedEdges,
|
getConnectedEdges,
|
||||||
getOutgoers,
|
getOutgoers,
|
||||||
@ -8,13 +9,11 @@ import {
|
|||||||
import { useToolIcon } from '../../../../hooks'
|
import { useToolIcon } from '../../../../hooks'
|
||||||
import BlockIcon from '../../../../block-icon'
|
import BlockIcon from '../../../../block-icon'
|
||||||
import type {
|
import type {
|
||||||
Branch,
|
|
||||||
Node,
|
Node,
|
||||||
} from '../../../../types'
|
} from '../../../../types'
|
||||||
import { BlockEnum } from '../../../../types'
|
import { BlockEnum } from '../../../../types'
|
||||||
import Add from './add'
|
|
||||||
import Item from './item'
|
|
||||||
import Line from './line'
|
import Line from './line'
|
||||||
|
import Container from './container'
|
||||||
|
|
||||||
type NextStepProps = {
|
type NextStepProps = {
|
||||||
selectedNode: Node
|
selectedNode: Node
|
||||||
@ -22,15 +21,33 @@ type NextStepProps = {
|
|||||||
const NextStep = ({
|
const NextStep = ({
|
||||||
selectedNode,
|
selectedNode,
|
||||||
}: NextStepProps) => {
|
}: NextStepProps) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
const data = selectedNode.data
|
const data = selectedNode.data
|
||||||
const toolIcon = useToolIcon(data)
|
const toolIcon = useToolIcon(data)
|
||||||
const store = useStoreApi()
|
const store = useStoreApi()
|
||||||
const branches = data._targetBranches || []
|
const branches = useMemo(() => {
|
||||||
|
return data._targetBranches || []
|
||||||
|
}, [data])
|
||||||
const nodeWithBranches = data.type === BlockEnum.IfElse || data.type === BlockEnum.QuestionClassifier
|
const nodeWithBranches = data.type === BlockEnum.IfElse || data.type === BlockEnum.QuestionClassifier
|
||||||
const edges = useEdges()
|
const edges = useEdges()
|
||||||
const outgoers = getOutgoers(selectedNode as Node, store.getState().getNodes(), edges)
|
const outgoers = getOutgoers(selectedNode as Node, store.getState().getNodes(), edges)
|
||||||
const connectedEdges = getConnectedEdges([selectedNode] as Node[], edges).filter(edge => edge.source === selectedNode!.id)
|
const connectedEdges = getConnectedEdges([selectedNode] as Node[], edges).filter(edge => edge.source === selectedNode!.id)
|
||||||
|
|
||||||
|
const branchesOutgoers = useMemo(() => {
|
||||||
|
if (!branches?.length)
|
||||||
|
return []
|
||||||
|
|
||||||
|
return branches.map((branch) => {
|
||||||
|
const connected = connectedEdges.filter(edge => edge.sourceHandle === branch.id)
|
||||||
|
const nextNodes = connected.map(edge => outgoers.find(outgoer => outgoer.id === edge.target)!)
|
||||||
|
|
||||||
|
return {
|
||||||
|
branch,
|
||||||
|
nextNodes,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}, [branches, connectedEdges, outgoers])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='flex py-1'>
|
<div className='flex py-1'>
|
||||||
<div className='shrink-0 relative flex items-center justify-center w-9 h-9 bg-background-default rounded-lg border-[0.5px] border-divider-regular shadow-xs'>
|
<div className='shrink-0 relative flex items-center justify-center w-9 h-9 bg-background-default rounded-lg border-[0.5px] border-divider-regular shadow-xs'>
|
||||||
@ -39,60 +56,33 @@ const NextStep = ({
|
|||||||
toolIcon={toolIcon}
|
toolIcon={toolIcon}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Line linesNumber={nodeWithBranches ? branches.length : 1} />
|
<Line
|
||||||
<div className='grow'>
|
list={nodeWithBranches ? branchesOutgoers.map(item => item.nextNodes.length + 1) : [1]}
|
||||||
{
|
|
||||||
!nodeWithBranches && !!outgoers.length && (
|
|
||||||
<Item
|
|
||||||
nodeId={outgoers[0].id}
|
|
||||||
data={outgoers[0].data}
|
|
||||||
sourceHandle='source'
|
|
||||||
/>
|
/>
|
||||||
)
|
<div className='grow space-y-2'>
|
||||||
}
|
|
||||||
{
|
{
|
||||||
!nodeWithBranches && !outgoers.length && (
|
!nodeWithBranches && (
|
||||||
<Add
|
<Container
|
||||||
nodeId={selectedNode!.id}
|
nodeId={selectedNode!.id}
|
||||||
nodeData={selectedNode!.data}
|
nodeData={selectedNode!.data}
|
||||||
sourceHandle='source'
|
sourceHandle='source'
|
||||||
|
nextNodes={outgoers}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
!!branches?.length && nodeWithBranches && (
|
nodeWithBranches && (
|
||||||
branches.map((branch: Branch) => {
|
branchesOutgoers.map((item, index) => {
|
||||||
const connected = connectedEdges.find(edge => edge.sourceHandle === branch.id)
|
|
||||||
const target = outgoers.find(outgoer => outgoer.id === connected?.target)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<Container
|
||||||
key={branch.id}
|
key={item.branch.id}
|
||||||
className='mb-3 last-of-type:mb-0'
|
|
||||||
>
|
|
||||||
{
|
|
||||||
connected && (
|
|
||||||
<Item
|
|
||||||
data={target!.data!}
|
|
||||||
nodeId={target!.id}
|
|
||||||
sourceHandle={branch.id}
|
|
||||||
branchName={branch.name}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
{
|
|
||||||
!connected && (
|
|
||||||
<Add
|
|
||||||
key={branch.id}
|
|
||||||
nodeId={selectedNode!.id}
|
nodeId={selectedNode!.id}
|
||||||
nodeData={selectedNode!.data}
|
nodeData={selectedNode!.data}
|
||||||
sourceHandle={branch.id}
|
sourceHandle={item.branch.id}
|
||||||
branchName={branch.name}
|
nextNodes={item.nextNodes}
|
||||||
|
branchName={item.branch.name || `${t('workflow.nodes.questionClassifiers.class')} ${index + 1}`}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -1,94 +1,82 @@
|
|||||||
import {
|
import {
|
||||||
memo,
|
memo,
|
||||||
useCallback,
|
useCallback,
|
||||||
|
useState,
|
||||||
} from 'react'
|
} from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { intersection } from 'lodash-es'
|
import Operator from './operator'
|
||||||
import type {
|
import type {
|
||||||
CommonNodeType,
|
CommonNodeType,
|
||||||
OnSelectBlock,
|
|
||||||
} from '@/app/components/workflow/types'
|
} from '@/app/components/workflow/types'
|
||||||
import BlockIcon from '@/app/components/workflow/block-icon'
|
import BlockIcon from '@/app/components/workflow/block-icon'
|
||||||
import BlockSelector from '@/app/components/workflow/block-selector'
|
|
||||||
import {
|
import {
|
||||||
useAvailableBlocks,
|
|
||||||
useNodesInteractions,
|
useNodesInteractions,
|
||||||
useNodesReadOnly,
|
useNodesReadOnly,
|
||||||
useToolIcon,
|
useToolIcon,
|
||||||
} from '@/app/components/workflow/hooks'
|
} from '@/app/components/workflow/hooks'
|
||||||
import Button from '@/app/components/base/button'
|
import Button from '@/app/components/base/button'
|
||||||
|
import cn from '@/utils/classnames'
|
||||||
|
|
||||||
type ItemProps = {
|
type ItemProps = {
|
||||||
nodeId: string
|
nodeId: string
|
||||||
sourceHandle: string
|
sourceHandle: string
|
||||||
branchName?: string
|
|
||||||
data: CommonNodeType
|
data: CommonNodeType
|
||||||
}
|
}
|
||||||
const Item = ({
|
const Item = ({
|
||||||
nodeId,
|
nodeId,
|
||||||
sourceHandle,
|
sourceHandle,
|
||||||
branchName,
|
|
||||||
data,
|
data,
|
||||||
}: ItemProps) => {
|
}: ItemProps) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { handleNodeChange } = useNodesInteractions()
|
const [open, setOpen] = useState(false)
|
||||||
const { nodesReadOnly } = useNodesReadOnly()
|
const { nodesReadOnly } = useNodesReadOnly()
|
||||||
|
const { handleNodeSelect } = useNodesInteractions()
|
||||||
const toolIcon = useToolIcon(data)
|
const toolIcon = useToolIcon(data)
|
||||||
const {
|
|
||||||
availablePrevBlocks,
|
|
||||||
availableNextBlocks,
|
|
||||||
} = useAvailableBlocks(data.type, data.isInIteration)
|
|
||||||
|
|
||||||
const handleSelect = useCallback<OnSelectBlock>((type, toolDefaultValue) => {
|
const handleOpenChange = useCallback((v: boolean) => {
|
||||||
handleNodeChange(nodeId, type, sourceHandle, toolDefaultValue)
|
setOpen(v)
|
||||||
}, [nodeId, sourceHandle, handleNodeChange])
|
}, [])
|
||||||
const renderTrigger = useCallback((open: boolean) => {
|
|
||||||
return (
|
|
||||||
<Button
|
|
||||||
size='small'
|
|
||||||
className={`
|
|
||||||
hidden group-hover:flex
|
|
||||||
${open && '!bg-gray-100 !flex'}
|
|
||||||
`}
|
|
||||||
>
|
|
||||||
{t('workflow.panel.change')}
|
|
||||||
</Button>
|
|
||||||
)
|
|
||||||
}, [t])
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className='relative group flex items-center mb-3 last-of-type:mb-0 px-2 h-9 rounded-lg border-[0.5px] border-divider-regular bg-background-default hover:bg-background-default-hover shadow-xs text-xs text-text-secondary cursor-pointer'
|
className='relative group flex items-center last-of-type:mb-0 px-2 h-9 rounded-lg border-[0.5px] border-divider-regular bg-background-default hover:bg-background-default-hover shadow-xs text-xs text-text-secondary cursor-pointer'
|
||||||
>
|
>
|
||||||
{
|
|
||||||
branchName && (
|
|
||||||
<div
|
|
||||||
className='absolute left-1 right-1 -top-[7.5px] flex items-center h-3 text-[10px] text-gray-500 font-semibold'
|
|
||||||
title={branchName.toLocaleUpperCase()}
|
|
||||||
>
|
|
||||||
<div className='inline-block px-0.5 rounded-[5px] bg-white truncate'>{branchName.toLocaleUpperCase()}</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
<BlockIcon
|
<BlockIcon
|
||||||
type={data.type}
|
type={data.type}
|
||||||
toolIcon={toolIcon}
|
toolIcon={toolIcon}
|
||||||
className='shrink-0 mr-1.5'
|
className='shrink-0 mr-1.5'
|
||||||
/>
|
/>
|
||||||
<div className='grow system-xs-medium text-text-secondary'>{data.title}</div>
|
<div
|
||||||
|
className='grow system-xs-medium text-text-secondary truncate'
|
||||||
|
title={data.title}
|
||||||
|
>
|
||||||
|
{data.title}
|
||||||
|
</div>
|
||||||
{
|
{
|
||||||
!nodesReadOnly && (
|
!nodesReadOnly && (
|
||||||
<BlockSelector
|
<>
|
||||||
onSelect={handleSelect}
|
<Button
|
||||||
placement='top-end'
|
className='hidden group-hover:flex shrink-0 mr-1'
|
||||||
offset={{
|
size='small'
|
||||||
mainAxis: 6,
|
onClick={() => handleNodeSelect(nodeId)}
|
||||||
crossAxis: 8,
|
>
|
||||||
}}
|
{t('workflow.common.jumpToNode')}
|
||||||
trigger={renderTrigger}
|
</Button>
|
||||||
popupClassName='!w-[328px]'
|
<div
|
||||||
availableBlocksTypes={intersection(availablePrevBlocks, availableNextBlocks).filter(item => item !== data.type)}
|
className={cn(
|
||||||
|
'hidden shrink-0 group-hover:flex items-center',
|
||||||
|
open && 'flex',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Operator
|
||||||
|
data={data}
|
||||||
|
nodeId={nodeId}
|
||||||
|
sourceHandle={sourceHandle}
|
||||||
|
open={open}
|
||||||
|
onOpenChange={handleOpenChange}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,17 +1,30 @@
|
|||||||
import { memo } from 'react'
|
import { memo } from 'react'
|
||||||
|
|
||||||
type LineProps = {
|
type LineProps = {
|
||||||
linesNumber: number
|
list: number[]
|
||||||
}
|
}
|
||||||
const Line = ({
|
const Line = ({
|
||||||
linesNumber,
|
list,
|
||||||
}: LineProps) => {
|
}: LineProps) => {
|
||||||
const svgHeight = linesNumber * 36 + (linesNumber - 1) * 12
|
const listHeight = list.map((item) => {
|
||||||
|
return item * 36 + (item - 1) * 2 + 12 + 6
|
||||||
|
})
|
||||||
|
const processedList = listHeight.map((item, index) => {
|
||||||
|
if (index === 0)
|
||||||
|
return item
|
||||||
|
|
||||||
|
return listHeight.slice(0, index).reduce((acc, cur) => acc + cur, 0) + item
|
||||||
|
})
|
||||||
|
const processedListLength = processedList.length
|
||||||
|
const svgHeight = processedList[processedListLength - 1] + (processedListLength - 1) * 8
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<svg className='shrink-0 w-6' style={{ height: svgHeight }}>
|
<svg className='shrink-0 w-6' style={{ height: svgHeight }}>
|
||||||
{
|
{
|
||||||
Array(linesNumber).fill(0).map((_, index) => (
|
processedList.map((item, index) => {
|
||||||
|
const prevItem = index > 0 ? processedList[index - 1] : 0
|
||||||
|
const space = prevItem + index * 8 + 16
|
||||||
|
return (
|
||||||
<g key={index}>
|
<g key={index}>
|
||||||
{
|
{
|
||||||
index === 0 && (
|
index === 0 && (
|
||||||
@ -35,7 +48,7 @@ const Line = ({
|
|||||||
{
|
{
|
||||||
index > 0 && (
|
index > 0 && (
|
||||||
<path
|
<path
|
||||||
d={`M0,18 Q12,18 12,28 L12,${index * 48 + 18 - 10} Q12,${index * 48 + 18} 24,${index * 48 + 18}`}
|
d={`M0,18 Q12,18 12,28 L12,${space - 10 + 2} Q12,${space + 2} 24,${space + 2}`}
|
||||||
strokeWidth={1}
|
strokeWidth={1}
|
||||||
fill='none'
|
fill='none'
|
||||||
className='stroke-divider-soild'
|
className='stroke-divider-soild'
|
||||||
@ -44,13 +57,14 @@ const Line = ({
|
|||||||
}
|
}
|
||||||
<rect
|
<rect
|
||||||
x={23}
|
x={23}
|
||||||
y={index * 48 + 18 - 2}
|
y={space}
|
||||||
width={1}
|
width={1}
|
||||||
height={4}
|
height={4}
|
||||||
className='fill-divider-soild-alt'
|
className='fill-divider-soild-alt'
|
||||||
/>
|
/>
|
||||||
</g>
|
</g>
|
||||||
))
|
)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
</svg>
|
</svg>
|
||||||
)
|
)
|
||||||
|
@ -0,0 +1,129 @@
|
|||||||
|
import {
|
||||||
|
useCallback,
|
||||||
|
} from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { RiMoreFill } from '@remixicon/react'
|
||||||
|
import { intersection } from 'lodash-es'
|
||||||
|
import {
|
||||||
|
PortalToFollowElem,
|
||||||
|
PortalToFollowElemContent,
|
||||||
|
PortalToFollowElemTrigger,
|
||||||
|
} from '@/app/components/base/portal-to-follow-elem'
|
||||||
|
import Button from '@/app/components/base/button'
|
||||||
|
import BlockSelector from '@/app/components/workflow/block-selector'
|
||||||
|
import {
|
||||||
|
useAvailableBlocks,
|
||||||
|
useNodesInteractions,
|
||||||
|
} from '@/app/components/workflow/hooks'
|
||||||
|
import type {
|
||||||
|
CommonNodeType,
|
||||||
|
OnSelectBlock,
|
||||||
|
} from '@/app/components/workflow/types'
|
||||||
|
|
||||||
|
type ChangeItemProps = {
|
||||||
|
data: CommonNodeType
|
||||||
|
nodeId: string
|
||||||
|
sourceHandle: string
|
||||||
|
}
|
||||||
|
const ChangeItem = ({
|
||||||
|
data,
|
||||||
|
nodeId,
|
||||||
|
sourceHandle,
|
||||||
|
}: ChangeItemProps) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
const { handleNodeChange } = useNodesInteractions()
|
||||||
|
const {
|
||||||
|
availablePrevBlocks,
|
||||||
|
availableNextBlocks,
|
||||||
|
} = useAvailableBlocks(data.type, data.isInIteration)
|
||||||
|
|
||||||
|
const handleSelect = useCallback<OnSelectBlock>((type, toolDefaultValue) => {
|
||||||
|
handleNodeChange(nodeId, type, sourceHandle, toolDefaultValue)
|
||||||
|
}, [nodeId, sourceHandle, handleNodeChange])
|
||||||
|
|
||||||
|
const renderTrigger = useCallback(() => {
|
||||||
|
return (
|
||||||
|
<div className='flex items-center px-2 h-8 rounded-lg cursor-pointer hover:bg-state-base-hover'>
|
||||||
|
{t('workflow.panel.change')}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}, [t])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BlockSelector
|
||||||
|
onSelect={handleSelect}
|
||||||
|
placement='top-end'
|
||||||
|
offset={{
|
||||||
|
mainAxis: 6,
|
||||||
|
crossAxis: 8,
|
||||||
|
}}
|
||||||
|
trigger={renderTrigger}
|
||||||
|
popupClassName='!w-[328px]'
|
||||||
|
availableBlocksTypes={intersection(availablePrevBlocks, availableNextBlocks).filter(item => item !== data.type)}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
type OperatorProps = {
|
||||||
|
open: boolean
|
||||||
|
onOpenChange: (v: boolean) => void
|
||||||
|
data: CommonNodeType
|
||||||
|
nodeId: string
|
||||||
|
sourceHandle: string
|
||||||
|
}
|
||||||
|
const Operator = ({
|
||||||
|
open,
|
||||||
|
onOpenChange,
|
||||||
|
data,
|
||||||
|
nodeId,
|
||||||
|
sourceHandle,
|
||||||
|
}: OperatorProps) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const {
|
||||||
|
handleNodeDelete,
|
||||||
|
handleNodeDisconnect,
|
||||||
|
} = useNodesInteractions()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PortalToFollowElem
|
||||||
|
placement='bottom-end'
|
||||||
|
offset={{ mainAxis: 4, crossAxis: -4 }}
|
||||||
|
open={open}
|
||||||
|
onOpenChange={onOpenChange}
|
||||||
|
>
|
||||||
|
<PortalToFollowElemTrigger onClick={() => onOpenChange(!open)}>
|
||||||
|
<Button className='p-0 w-6 h-6'>
|
||||||
|
<RiMoreFill className='w-4 h-4' />
|
||||||
|
</Button>
|
||||||
|
</PortalToFollowElemTrigger>
|
||||||
|
<PortalToFollowElemContent className='z-10'>
|
||||||
|
<div className='min-w-[120px] rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur shadow-lg system-md-regular text-text-secondary'>
|
||||||
|
<div className='p-1'>
|
||||||
|
<ChangeItem
|
||||||
|
data={data}
|
||||||
|
nodeId={nodeId}
|
||||||
|
sourceHandle={sourceHandle}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
className='flex items-center px-2 h-8 rounded-lg cursor-pointer hover:bg-state-base-hover'
|
||||||
|
onClick={() => handleNodeDisconnect(nodeId)}
|
||||||
|
>
|
||||||
|
{t('workflow.common.disconnect')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className='p-1'>
|
||||||
|
<div
|
||||||
|
className='flex items-center px-2 h-8 rounded-lg cursor-pointer hover:bg-state-base-hover'
|
||||||
|
onClick={() => handleNodeDelete(nodeId)}
|
||||||
|
>
|
||||||
|
{t('common.operation.delete')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</PortalToFollowElemContent>
|
||||||
|
</PortalToFollowElem>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Operator
|
@ -748,7 +748,5 @@ export const getParallelInfo = (nodes: Node[], edges: Edge[], parentNodeId?: str
|
|||||||
traverse(nodeHandle)
|
traverse(nodeHandle)
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(parallelList, 'parallelList')
|
|
||||||
|
|
||||||
return parallelList
|
return parallelList
|
||||||
}
|
}
|
||||||
|
@ -90,6 +90,9 @@ const translation = {
|
|||||||
limit: 'Parallelism is limited to {{num}} branches.',
|
limit: 'Parallelism is limited to {{num}} branches.',
|
||||||
depthLimit: 'Parallel nesting layer limit of {{num}} layers',
|
depthLimit: 'Parallel nesting layer limit of {{num}} layers',
|
||||||
},
|
},
|
||||||
|
disconnect: 'Disconnect',
|
||||||
|
jumpToNode: 'Jump to this node',
|
||||||
|
addParallelNode: 'Add Parallel Node',
|
||||||
},
|
},
|
||||||
env: {
|
env: {
|
||||||
envPanelTitle: 'Environment Variables',
|
envPanelTitle: 'Environment Variables',
|
||||||
|
@ -90,6 +90,9 @@ const translation = {
|
|||||||
limit: '并行分支限制为 {{num}} 个',
|
limit: '并行分支限制为 {{num}} 个',
|
||||||
depthLimit: '并行嵌套层数限制 {{num}} 层',
|
depthLimit: '并行嵌套层数限制 {{num}} 层',
|
||||||
},
|
},
|
||||||
|
disconnect: '断开连接',
|
||||||
|
jumpToNode: '跳转到节点',
|
||||||
|
addParallelNode: '添加并行节点',
|
||||||
},
|
},
|
||||||
env: {
|
env: {
|
||||||
envPanelTitle: '环境变量',
|
envPanelTitle: '环境变量',
|
||||||
|
Loading…
x
Reference in New Issue
Block a user