diff --git a/web/app/(commonLayout)/workflow/page.tsx b/web/app/(commonLayout)/workflow/page.tsx index 96e08eab68..fc9d485c49 100644 --- a/web/app/(commonLayout)/workflow/page.tsx +++ b/web/app/(commonLayout)/workflow/page.tsx @@ -8,14 +8,26 @@ const initialNodes = [ { id: '1', type: 'custom', - // position: { x: 130, y: 130 }, + position: { x: 130, y: 130 }, data: { type: 'start' }, }, { id: '2', type: 'custom', position: { x: 434, y: 130 }, - data: { type: 'if-else' }, + data: { + type: 'if-else', + branches: [ + { + id: 'if-true', + name: 'IS TRUE', + }, + { + id: 'if-false', + name: 'IS FALSE', + }, + ], + }, }, { id: '3', @@ -50,7 +62,7 @@ const initialEdges = [ id: '1', type: 'custom', source: '2', - sourceHandle: 'condition1', + sourceHandle: 'if-true', target: '3', targetHandle: 'target', }, @@ -58,7 +70,7 @@ const initialEdges = [ id: '2', type: 'custom', source: '2', - sourceHandle: 'condition2', + sourceHandle: 'if-false', target: '4', targetHandle: 'target', }, diff --git a/web/app/components/workflow/constants.ts b/web/app/components/workflow/constants.ts index 3905601f63..79064a4071 100644 --- a/web/app/components/workflow/constants.ts +++ b/web/app/components/workflow/constants.ts @@ -92,3 +92,7 @@ export const NodeInitialData = { desc: '', }, } + +export const NODE_WIDTH = 220 +export const X_OFFSET = 64 +export const Y_OFFSET = 39 diff --git a/web/app/components/workflow/hooks.ts b/web/app/components/workflow/hooks.ts index eafca61291..d506d59085 100644 --- a/web/app/components/workflow/hooks.ts +++ b/web/app/components/workflow/hooks.ts @@ -10,6 +10,7 @@ import { } from 'reactflow' import type { BlockEnum, + Node, SelectedNode, } from './types' import { NodeInitialData } from './constants' @@ -123,13 +124,14 @@ export const useWorkflow = () => { setNodes(newNodes) } }, [setSelectedNode, store]) + const handleUpdateNodeData = useCallback(({ id, data }: SelectedNode) => { const { getNodes, setNodes, } = store.getState() const newNodes = produce(getNodes(), (draft) => { - const currentNode = draft.find(n => n.id === id)! + const currentNode = draft.find(node => node.id === id)! currentNode.data = { ...currentNode.data, ...data } }) @@ -137,7 +139,7 @@ export const useWorkflow = () => { setSelectedNode({ id, data }) }, [store, setSelectedNode]) - const handleAddNextNode = useCallback((currentNodeId: string, nodeType: BlockEnum, branchId?: string) => { + const handleAddNextNode = useCallback((currentNodeId: string, nodeType: BlockEnum, sourceHandle: string) => { const { getNodes, setNodes, @@ -146,35 +148,85 @@ export const useWorkflow = () => { } = store.getState() const nodes = getNodes() const currentNode = nodes.find(node => node.id === currentNodeId)! - const nextNode = { + const nextNode: Node = { id: `${Date.now()}`, type: 'custom', - data: { ...NodeInitialData[nodeType], selected: true }, + data: { + ...NodeInitialData[nodeType], + selected: true, + }, position: { x: currentNode.position.x + 304, y: currentNode.position.y, }, } + const newEdge = { + id: `${currentNode.id}-${nextNode.id}`, + type: 'custom', + source: currentNode.id, + sourceHandle, + target: nextNode.id, + targetHandle: 'target', + } const newNodes = produce(nodes, (draft) => { - draft.forEach((item) => { - item.data = { ...item.data, selected: false } + draft.forEach((node) => { + node.data = { ...node.data, selected: false } }) draft.push(nextNode) }) setNodes(newNodes) const newEdges = produce(edges, (draft) => { - draft.push({ - id: `${currentNode.id}-${nextNode.id}`, - type: 'custom', - source: currentNode.id, - sourceHandle: branchId || 'source', - target: nextNode.id, - targetHandle: 'target', - }) + draft.push(newEdge) }) setEdges(newEdges) setSelectedNode(nextNode) }, [store, setSelectedNode]) + + const handleChangeCurrentNode = useCallback((parentNodeId: string, currentNodeId: string, nodeType: BlockEnum, sourceHandle: string) => { + const { + getNodes, + setNodes, + edges, + setEdges, + } = store.getState() + const nodes = getNodes() + const currentNode = nodes.find(node => node.id === currentNodeId)! + const connectedEdges = getConnectedEdges([currentNode], edges) + const newCurrentNode: Node = { + id: `${Date.now()}`, + type: 'custom', + data: { + ...NodeInitialData[nodeType], + selected: true, + }, + position: { + x: currentNode.position.x, + y: currentNode.position.y, + }, + } + const newEdge = { + id: `${parentNodeId}-${newCurrentNode.id}`, + type: 'custom', + source: parentNodeId, + sourceHandle, + target: newCurrentNode.id, + targetHandle: 'target', + } + const newNodes = produce(nodes, (draft) => { + const index = draft.findIndex(node => node.id === currentNodeId) + + draft.splice(index, 1, newCurrentNode) + }) + setNodes(newNodes) + const newEdges = produce(edges, (draft) => { + const filtered = draft.filter(edge => !connectedEdges.find(connectedEdge => connectedEdge.id === edge.id)) + filtered.push(newEdge) + + return filtered + }) + setEdges(newEdges) + }, [store]) + const handleInitialLayoutNodes = useCallback(() => { const { getNodes, @@ -191,6 +243,43 @@ export const useWorkflow = () => { })) }, [store]) + const handleUpdateNodesPosition = useCallback(() => { + const { + getNodes, + setNodes, + } = store.getState() + + const nodes = getNodes() + const groups = nodes.reduce((acc, cur) => { + const x = cur.data.position.x + + if (!acc[x]) + acc[x] = [cur] + else + acc[x].push(cur) + + return acc + }, {} as Record) + const heightMap: Record = {} + + Object.keys(groups).forEach((key) => { + let baseHeight = 0 + groups[key].sort((a, b) => a.data.position!.y - b.data.position!.y).forEach((node) => { + heightMap[node.id] = baseHeight + baseHeight = node.height! + 39 + }) + }) + setNodes(produce(nodes, (draft) => { + draft.forEach((node) => { + node.position = { + ...node.position, + x: node.data.position.x * (220 + 64), + y: heightMap[node.id], + } + }) + })) + }, [store]) + return { handleEnterNode, handleLeaveNode, @@ -199,6 +288,8 @@ export const useWorkflow = () => { handleSelectNode, handleUpdateNodeData, handleAddNextNode, + handleChangeCurrentNode, handleInitialLayoutNodes, + handleUpdateNodesPosition, } } diff --git a/web/app/components/workflow/index.tsx b/web/app/components/workflow/index.tsx index c259139d1c..6faacc0809 100644 --- a/web/app/components/workflow/index.tsx +++ b/web/app/components/workflow/index.tsx @@ -10,7 +10,7 @@ import ReactFlow, { Background, ReactFlowProvider, useEdgesState, - useNodesInitialized, + // useNodesInitialized, useNodesState, } from 'reactflow' import 'reactflow/dist/style.css' @@ -70,10 +70,10 @@ const Workflow: FC = memo(({ needUpdatePosition: true, } }, [initialNodes, initialEdges]) - const nodesInitialized = useNodesInitialized({ - includeHiddenNodes: true, - }) - const [nodes] = useNodesState(initialData.nodes) + // const nodesInitialized = useNodesInitialized({ + // includeHiddenNodes: true, + // }) + const [nodes, setNodes, onNodesChange] = useNodesState(initialData.nodes) const [edges, setEdges, onEdgesChange] = useEdgesState(initialData.edges) const { @@ -85,11 +85,10 @@ const Workflow: FC = memo(({ handleInitialLayoutNodes, } = useWorkflow() - useEffect(() => { - console.log(nodesInitialized, '2') - if (nodesInitialized && initialData.needUpdatePosition) - handleInitialLayoutNodes() - }, [nodesInitialized]) + // useEffect(() => { + // if (nodesInitialized) + // handleInitialLayoutNodes() + // }, [nodesInitialized]) useEffect(() => { if (initialSelectedNodeId) { diff --git a/web/app/components/workflow/nodes/_base/components/next-step.tsx b/web/app/components/workflow/nodes/_base/components/next-step.tsx deleted file mode 100644 index 35fd2da9a6..0000000000 --- a/web/app/components/workflow/nodes/_base/components/next-step.tsx +++ /dev/null @@ -1,175 +0,0 @@ -import { - memo, - useCallback, -} from 'react' -import { - getOutgoers, - useStoreApi, -} from 'reactflow' -import BlockIcon from '../../../block-icon' -import type { Node } from '../../../types' -import { useStore } from '../../../store' -import BlockSelector from '../../../block-selector' -import { Plus } from '@/app/components/base/icons/src/vender/line/general' -import Button from '@/app/components/base/button' - -const NextStep = () => { - const store = useStoreApi() - const selectedNode = useStore(state => state.selectedNode) - const outgoers: Node[] = getOutgoers(selectedNode as Node, store.getState().getNodes(), store.getState().edges) - const svgHeight = outgoers.length > 1 ? (outgoers.length + 1) * 36 + 12 * outgoers.length : 36 - - const renderAddNextNodeTrigger = useCallback((open: boolean) => { - return ( -
-
- -
- SELECT NEXT BLOCK -
- ) - }, []) - - const renderChangeCurrentNodeTrigger = useCallback((open: boolean) => { - return ( - - ) - }, []) - - return ( -
-
- -
- - { - outgoers.length < 2 && ( - - - - - - ) - } - { - outgoers.length > 1 && ( - - { - Array(outgoers.length + 1).fill(0).map((_, index) => ( - - { - index === 0 && ( - - ) - } - { - index > 0 && ( - - ) - } - - - )) - } - - - ) - } - -
- { - !!outgoers.length && outgoers.map(outgoer => ( -
-
- IS TRUE -
- -
{outgoer.data.title}
- {}} - placement='top-end' - offset={{ - mainAxis: 6, - crossAxis: 8, - }} - trigger={renderChangeCurrentNodeTrigger} - popupClassName='!w-[328px]' - /> -
- )) - } - { - (!outgoers.length || outgoers.length > 1) && ( - {}} - placement='top' - offset={0} - trigger={renderAddNextNodeTrigger} - popupClassName='!w-[328px]' - /> - ) - } -
-
- ) -} - -export default memo(NextStep) diff --git a/web/app/components/workflow/nodes/_base/components/next-step/add.tsx b/web/app/components/workflow/nodes/_base/components/next-step/add.tsx new file mode 100644 index 0000000000..4aad178233 --- /dev/null +++ b/web/app/components/workflow/nodes/_base/components/next-step/add.tsx @@ -0,0 +1,61 @@ +import { + memo, + useCallback, +} from 'react' +import BlockSelector from '../../../../block-selector' +import { useWorkflow } from '../../../../hooks' +import type { BlockEnum } from '../../../../types' +import { Plus } from '@/app/components/base/icons/src/vender/line/general' + +type AddProps = { + nodeId: string + sourceHandle: string + branchName?: string +} +const Add = ({ + nodeId, + sourceHandle, + branchName, +}: AddProps) => { + const { handleAddNextNode } = useWorkflow() + + const handleSelect = useCallback((type: BlockEnum) => { + handleAddNextNode(nodeId, type, sourceHandle) + }, [nodeId, sourceHandle, handleAddNextNode]) + + const renderTrigger = useCallback((open: boolean) => { + return ( +
+ { + branchName && ( +
+ {branchName.toLocaleUpperCase()} +
+ ) + } +
+ +
+ SELECT NEXT BLOCK +
+ ) + }, [branchName]) + + return ( + + ) +} + +export default memo(Add) diff --git a/web/app/components/workflow/nodes/_base/components/next-step/index.tsx b/web/app/components/workflow/nodes/_base/components/next-step/index.tsx new file mode 100644 index 0000000000..e541ef53aa --- /dev/null +++ b/web/app/components/workflow/nodes/_base/components/next-step/index.tsx @@ -0,0 +1,90 @@ +import { memo } from 'react' +import { + getConnectedEdges, + getOutgoers, + useEdges, + useStoreApi, +} from 'reactflow' +import BlockIcon from '../../../../block-icon' +import type { Node } from '../../../../types' +import { useStore } from '../../../../store' +import Add from './add' +import Item from './item' +import Line from './line' + +const NextStep = () => { + const store = useStoreApi() + const selectedNode = useStore(state => state.selectedNode) + const branches = selectedNode?.data.branches + const edges = useEdges() + const outgoers = getOutgoers(selectedNode as Node, store.getState().getNodes(), edges) + const connectedEdges = getConnectedEdges([selectedNode] as Node[], edges).filter(edge => edge.source === selectedNode!.id) + + return ( +
+
+ +
+ +
+ { + !branches && !!outgoers.length && ( + + ) + } + { + !branches && !outgoers.length && ( + + ) + } + { + branches?.length && ( + branches.map((branch) => { + const connected = connectedEdges.find(edge => edge.sourceHandle === branch.id) + const target = outgoers.find(outgoer => outgoer.id === connected?.target) + + return ( +
+ { + connected && ( + + ) + } + { + !connected && ( + + ) + } +
+ ) + }) + ) + } +
+
+ ) +} + +export default memo(NextStep) diff --git a/web/app/components/workflow/nodes/_base/components/next-step/item.tsx b/web/app/components/workflow/nodes/_base/components/next-step/item.tsx new file mode 100644 index 0000000000..df718b85fe --- /dev/null +++ b/web/app/components/workflow/nodes/_base/components/next-step/item.tsx @@ -0,0 +1,75 @@ +import { + memo, + useCallback, +} from 'react' +import type { + BlockEnum, + CommonNodeType, +} from '../../../../types' +import BlockIcon from '../../../../block-icon' +import BlockSelector from '../../../../block-selector' +import { useWorkflow } from '../../../../hooks' +import Button from '@/app/components/base/button' + +type ItemProps = { + parentNodeId: string + nodeId: string + sourceHandle: string + branchName?: string + data: CommonNodeType +} +const Item = ({ + parentNodeId, + nodeId, + sourceHandle, + branchName, + data, +}: ItemProps) => { + const { handleChangeCurrentNode } = useWorkflow() + const handleSelect = useCallback((type: BlockEnum) => { + handleChangeCurrentNode(parentNodeId, nodeId, type, sourceHandle) + }, [parentNodeId, nodeId, sourceHandle, handleChangeCurrentNode]) + const renderTrigger = useCallback((open: boolean) => { + return ( + + ) + }, []) + + return ( +
+ { + branchName && ( +
+ {branchName.toLocaleUpperCase()} +
+ ) + } + +
{data.title}
+ +
+ ) +} + +export default memo(Item) diff --git a/web/app/components/workflow/nodes/_base/components/next-step/line.tsx b/web/app/components/workflow/nodes/_base/components/next-step/line.tsx new file mode 100644 index 0000000000..40f6899e27 --- /dev/null +++ b/web/app/components/workflow/nodes/_base/components/next-step/line.tsx @@ -0,0 +1,59 @@ +import { memo } from 'react' + +type LineProps = { + linesNumber: number +} +const Line = ({ + linesNumber, +}: LineProps) => { + const svgHeight = linesNumber * 36 + (linesNumber - 1) * 12 + + return ( + + { + Array(linesNumber).fill(0).map((_, index) => ( + + { + index === 0 && ( + <> + + + + ) + } + { + index > 0 && ( + + ) + } + + + )) + } + + ) +} + +export default memo(Line) diff --git a/web/app/components/workflow/nodes/_base/components/node-handle.tsx b/web/app/components/workflow/nodes/_base/components/node-handle.tsx index 840ce2e66f..8221ee8982 100644 --- a/web/app/components/workflow/nodes/_base/components/node-handle.tsx +++ b/web/app/components/workflow/nodes/_base/components/node-handle.tsx @@ -7,7 +7,7 @@ import { Handle, Position, getConnectedEdges, - useStoreApi, + useEdges, } from 'reactflow' import { BlockEnum } from '../../../types' import type { Node } from '../../../types' @@ -15,7 +15,7 @@ import BlockSelector from '../../../block-selector' import { useWorkflow } from '../../../hooks' type NodeHandleProps = { - handleId?: string + handleId: string handleClassName?: string nodeSelectorClassName?: string } & Pick @@ -28,9 +28,10 @@ export const NodeTargetHandle = ({ nodeSelectorClassName, }: NodeHandleProps) => { const [open, setOpen] = useState(false) - const store = useStoreApi() - const connectedEdges = getConnectedEdges([{ id } as Node], store.getState().edges) + const edges = useEdges() + const connectedEdges = getConnectedEdges([{ id } as Node], edges) const connected = connectedEdges.find(edge => edge.targetHandle === handleId && edge.target === id) + const handleOpenChange = useCallback((v: boolean) => { setOpen(v) }, []) @@ -86,8 +87,8 @@ export const NodeSourceHandle = ({ }: NodeHandleProps) => { const [open, setOpen] = useState(false) const { handleAddNextNode } = useWorkflow() - const store = useStoreApi() - const connectedEdges = getConnectedEdges([{ id } as Node], store.getState().edges) + const edges = useEdges() + const connectedEdges = getConnectedEdges([{ id } as Node], edges) const connected = connectedEdges.find(edge => edge.sourceHandle === handleId && edge.source === id) const handleOpenChange = useCallback((v: boolean) => { setOpen(v) @@ -97,8 +98,8 @@ export const NodeSourceHandle = ({ handleOpenChange(!open) } const handleSelect = useCallback((type: BlockEnum) => { - handleAddNextNode(id, type) - }, [handleAddNextNode, id]) + handleAddNextNode(id, type, handleId) + }, [handleAddNextNode, id, handleId]) return ( <> diff --git a/web/app/components/workflow/nodes/if-else/node.tsx b/web/app/components/workflow/nodes/if-else/node.tsx index ae93bef101..c4be391b89 100644 --- a/web/app/components/workflow/nodes/if-else/node.tsx +++ b/web/app/components/workflow/nodes/if-else/node.tsx @@ -17,7 +17,7 @@ const IfElseNode: FC> = (props) => {
IF
@@ -41,7 +41,7 @@ const IfElseNode: FC> = (props) => {
ELSE