diff --git a/web/app/(commonLayout)/workflow/page.tsx b/web/app/(commonLayout)/workflow/page.tsx index fc9d485c49..2da702befd 100644 --- a/web/app/(commonLayout)/workflow/page.tsx +++ b/web/app/(commonLayout)/workflow/page.tsx @@ -57,6 +57,7 @@ const initialEdges = [ sourceHandle: 'source', target: '2', targetHandle: 'target', + deletable: false, }, { id: '1', diff --git a/web/app/components/workflow/hooks.ts b/web/app/components/workflow/hooks.ts index d506d59085..30134c26bf 100644 --- a/web/app/components/workflow/hooks.ts +++ b/web/app/components/workflow/hooks.ts @@ -14,12 +14,10 @@ import type { SelectedNode, } from './types' import { NodeInitialData } from './constants' -import { useStore } from './store' import { initialNodesPosition } from './utils' export const useWorkflow = () => { const store = useStoreApi() - const setSelectedNode = useStore(state => state.setSelectedNode) const handleEnterNode = useCallback((_, node) => { const { @@ -71,6 +69,25 @@ export const useWorkflow = () => { setEdges(newEdges) }, [store]) + const handleSelectNode = useCallback((nodeId: string, cancelSelection?: boolean) => { + const { + getNodes, + setNodes, + } = store.getState() + + const newNodes = produce(getNodes(), (draft) => { + const selectedNode = draft.find(node => node.id === nodeId) + + if (selectedNode) { + if (cancelSelection) + selectedNode.selected = false + else + selectedNode.selected = true + } + }) + setNodes(newNodes) + }, [store]) + const handleEnterEdge = useCallback((_, edge) => { const { edges, @@ -97,33 +114,19 @@ export const useWorkflow = () => { setEdges(newEdges) }, [store]) - const handleSelectNode = useCallback((selectNode: SelectedNode, cancelSelection?: boolean) => { + const handleDeleteEdge = useCallback(() => { const { - getNodes, - setNodes, + edges, + setEdges, } = store.getState() - if (cancelSelection) { - setSelectedNode(null) - const newNodes = produce(getNodes(), (draft) => { - draft.forEach((item) => { - item.data.selected = false - }) - }) - setNodes(newNodes) - } - else { - setSelectedNode(selectNode) - const newNodes = produce(getNodes(), (draft) => { - draft.forEach((item) => { - if (item.id === selectNode.id) - item.data.selected = true - else - item.data.selected = false - }) - }) - setNodes(newNodes) - } - }, [setSelectedNode, store]) + const newEdges = produce(edges, (draft) => { + const index = draft.findIndex(edge => edge.selected) + + if (index > -1) + draft.splice(index, 1) + }) + setEdges(newEdges) + }, [store]) const handleUpdateNodeData = useCallback(({ id, data }: SelectedNode) => { const { @@ -136,8 +139,7 @@ export const useWorkflow = () => { currentNode.data = { ...currentNode.data, ...data } }) setNodes(newNodes) - setSelectedNode({ id, data }) - }, [store, setSelectedNode]) + }, [store]) const handleAddNextNode = useCallback((currentNodeId: string, nodeType: BlockEnum, sourceHandle: string) => { const { @@ -151,10 +153,7 @@ export const useWorkflow = () => { const nextNode: Node = { id: `${Date.now()}`, type: 'custom', - data: { - ...NodeInitialData[nodeType], - selected: true, - }, + data: NodeInitialData[nodeType], position: { x: currentNode.position.x + 304, y: currentNode.position.y, @@ -179,8 +178,7 @@ export const useWorkflow = () => { draft.push(newEdge) }) setEdges(newEdges) - setSelectedNode(nextNode) - }, [store, setSelectedNode]) + }, [store]) const handleChangeCurrentNode = useCallback((parentNodeId: string, currentNodeId: string, nodeType: BlockEnum, sourceHandle: string) => { const { @@ -195,10 +193,7 @@ export const useWorkflow = () => { const newCurrentNode: Node = { id: `${Date.now()}`, type: 'custom', - data: { - ...NodeInitialData[nodeType], - selected: true, - }, + data: NodeInitialData[nodeType], position: { x: currentNode.position.x, y: currentNode.position.y, @@ -227,6 +222,28 @@ export const useWorkflow = () => { setEdges(newEdges) }, [store]) + const handleDeleteNode = useCallback((nodeId: string) => { + const { + getNodes, + setNodes, + edges, + setEdges, + } = store.getState() + + const newNodes = produce(getNodes(), (draft) => { + const index = draft.findIndex(node => node.id === nodeId) + + if (index > -1) + draft.splice(index, 1) + }) + setNodes(newNodes) + const connectedEdges = getConnectedEdges([{ id: nodeId } as Node], edges) + const newEdges = produce(edges, (draft) => { + return draft.filter(edge => !connectedEdges.find(connectedEdge => connectedEdge.id === edge.id)) + }) + setEdges(newEdges) + }, [store]) + const handleInitialLayoutNodes = useCallback(() => { const { getNodes, @@ -283,12 +300,14 @@ export const useWorkflow = () => { return { handleEnterNode, handleLeaveNode, + handleSelectNode, handleEnterEdge, handleLeaveEdge, - handleSelectNode, + handleDeleteEdge, handleUpdateNodeData, handleAddNextNode, handleChangeCurrentNode, + handleDeleteNode, handleInitialLayoutNodes, handleUpdateNodesPosition, } diff --git a/web/app/components/workflow/index.tsx b/web/app/components/workflow/index.tsx index 6faacc0809..4e7d3836b0 100644 --- a/web/app/components/workflow/index.tsx +++ b/web/app/components/workflow/index.tsx @@ -1,19 +1,17 @@ import type { FC } from 'react' -import { - memo, - useEffect, - useMemo, -} from 'react' -import produce from 'immer' -import type { Edge } from 'reactflow' +import { memo } from 'react' +import { useKeyPress } from 'ahooks' import ReactFlow, { Background, ReactFlowProvider, useEdgesState, - // useNodesInitialized, useNodesState, } from 'reactflow' import 'reactflow/dist/style.css' +import type { + Edge, + Node, +} from './types' import { useWorkflow } from './hooks' import Header from './header' import CustomNode from './nodes' @@ -21,7 +19,6 @@ import ZoomInOut from './zoom-in-out' import CustomEdge from './custom-edge' import CustomConnectionLine from './custom-connection-line' import Panel from './panel' -import { BlockEnum, type Node } from './types' const nodeTypes = { custom: CustomNode, @@ -31,73 +28,25 @@ const edgeTypes = { } type WorkflowProps = { - selectedNodeId?: string nodes: Node[] edges: Edge[] } const Workflow: FC = memo(({ nodes: initialNodes, edges: initialEdges, - selectedNodeId: initialSelectedNodeId, }) => { - const initialData: { - nodes: Node[] - edges: Edge[] - needUpdatePosition: boolean - } = useMemo(() => { - const start = initialNodes.find(node => node.data.type === BlockEnum.Start) - - if (start?.position) { - return { - nodes: initialNodes, - edges: initialEdges, - needUpdatePosition: false, - } - } - - return { - nodes: produce(initialNodes, (draft) => { - draft.forEach((node) => { - node.position = { x: 0, y: 0 } - node.data = { ...node.data, hidden: true } - }) - }), - edges: produce(initialEdges, (draft) => { - draft.forEach((edge) => { - edge.hidden = true - }) - }), - needUpdatePosition: true, - } - }, [initialNodes, initialEdges]) - // const nodesInitialized = useNodesInitialized({ - // includeHiddenNodes: true, - // }) - const [nodes, setNodes, onNodesChange] = useNodesState(initialData.nodes) - const [edges, setEdges, onEdgesChange] = useEdgesState(initialData.edges) + const [nodes] = useNodesState(initialNodes) + const [edges, _, onEdgesChange] = useEdgesState(initialEdges) const { handleEnterNode, handleLeaveNode, handleEnterEdge, handleLeaveEdge, - handleSelectNode, - handleInitialLayoutNodes, + handleDeleteEdge, } = useWorkflow() - // useEffect(() => { - // if (nodesInitialized) - // handleInitialLayoutNodes() - // }, [nodesInitialized]) - - useEffect(() => { - if (initialSelectedNodeId) { - const initialSelectedNode = nodes.find(n => n.id === initialSelectedNodeId) - - if (initialSelectedNode) - handleSelectNode({ id: initialSelectedNodeId, data: initialSelectedNode.data }) - } - }, [initialSelectedNodeId]) + useKeyPress('Backspace', handleDeleteEdge) return (
@@ -111,11 +60,12 @@ const Workflow: FC = memo(({ edges={edges} onNodeMouseEnter={handleEnterNode} onNodeMouseLeave={handleLeaveNode} - onEdgesChange={onEdgesChange} onEdgeMouseEnter={handleEnterEdge} onEdgeMouseLeave={handleLeaveEdge} + onEdgesChange={onEdgesChange} multiSelectionKeyCode={null} connectionLineComponent={CustomConnectionLine} + deleteKeyCode={null} > = memo(({ Workflow.displayName = 'Workflow' const WorkflowWrap: FC = ({ - selectedNodeId, nodes, edges, }) => { return ( - {selectedNodeId} 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 index e541ef53aa..c8db263f94 100644 --- a/web/app/components/workflow/nodes/_base/components/next-step/index.tsx +++ b/web/app/components/workflow/nodes/_base/components/next-step/index.tsx @@ -7,14 +7,17 @@ import { } 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 = () => { +type NextStepProps = { + selectedNode: Node +} +const NextStep = ({ + selectedNode, +}: NextStepProps) => { 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) diff --git a/web/app/components/workflow/nodes/_base/components/panel-operator.tsx b/web/app/components/workflow/nodes/_base/components/panel-operator.tsx new file mode 100644 index 0000000000..4d54bd0118 --- /dev/null +++ b/web/app/components/workflow/nodes/_base/components/panel-operator.tsx @@ -0,0 +1,77 @@ +import { + memo, + useState, +} from 'react' +import { useWorkflow } from '../../../hooks' +import { DotsHorizontal } from '@/app/components/base/icons/src/vender/line/general' +import { + PortalToFollowElem, + PortalToFollowElemContent, + PortalToFollowElemTrigger, +} from '@/app/components/base/portal-to-follow-elem' + +type PanelOperatorProps = { + nodeId: string +} +const PanelOperator = ({ + nodeId, +}: PanelOperatorProps) => { + const { handleDeleteNode } = useWorkflow() + const [open, setOpen] = useState(false) + + return ( + + setOpen(v => !v)}> +
+ +
+
+ +
+
+
Change Block
+
Help Link
+
+
+
+
handleDeleteNode(nodeId)} + > + Delete +
+
+
+
+
+
+ ABOUT +
+
A tool for performing a Google SERP search and extracting snippets and webpages.Input should be a search query.
+
+
+ Created By Dify +
+
+
+
+
+
+ ) +} + +export default memo(PanelOperator) diff --git a/web/app/components/workflow/nodes/_base/node.tsx b/web/app/components/workflow/nodes/_base/node.tsx index 0b21e8f81f..465d3dd0d9 100644 --- a/web/app/components/workflow/nodes/_base/node.tsx +++ b/web/app/components/workflow/nodes/_base/node.tsx @@ -16,8 +16,9 @@ type BaseNodeProps = { } & NodeProps const BaseNode: FC = ({ - id: nodeId, + id, data, + selected, children, }) => { const { handleSelectNode } = useWorkflow() @@ -27,10 +28,9 @@ const BaseNode: FC = ({ className={` group relative w-[240px] bg-[#fcfdff] rounded-2xl shadow-xs hover:shadow-lg - ${data.hidden && 'opacity-0'} - ${data.selected ? 'border-[2px] border-primary-600' : 'border border-white'} + ${selected ? 'border-[2px] border-primary-600' : 'border border-white'} `} - onClick={() => handleSelectNode({ id: nodeId, data })} + onClick={() => handleSelectNode(id)} >
@@ -49,7 +49,7 @@ const BaseNode: FC = ({ { children && (
- {cloneElement(children, { id: nodeId, data })} + {cloneElement(children, { id, data })}
) } diff --git a/web/app/components/workflow/nodes/_base/panel.tsx b/web/app/components/workflow/nodes/_base/panel.tsx index d8c6a4ea99..0460c1ca48 100644 --- a/web/app/components/workflow/nodes/_base/panel.tsx +++ b/web/app/components/workflow/nodes/_base/panel.tsx @@ -7,23 +7,23 @@ import { memo, useCallback, } from 'react' -import type { SelectedNode } from '../../types' +import type { Node } from '../../types' import BlockIcon from '../../block-icon' import { useWorkflow } from '../../hooks' import NextStep from './components/next-step' +import PanelOperator from './components/panel-operator' import { DescriptionInput, TitleInput, } from './components/title-description-input' import { - DotsHorizontal, XClose, } from '@/app/components/base/icons/src/vender/line/general' import { GitBranch01 } from '@/app/components/base/icons/src/vender/line/development' type BasePanelProps = { children: ReactElement -} & SelectedNode +} & Node const BasePanel: FC = ({ id, @@ -42,7 +42,7 @@ const BasePanel: FC = ({ }, [handleUpdateNodeData, id, data]) return ( -
+
= ({ onChange={handleTitleChange} />
-
- -
+
handleSelectNode({ id, data }, true)} + onClick={() => handleSelectNode(id, true)} >
@@ -85,7 +83,7 @@ const BasePanel: FC = ({
Add the next block in this workflow
- +
) diff --git a/web/app/components/workflow/nodes/index.tsx b/web/app/components/workflow/nodes/index.tsx index e17a1a318d..38a8b15fdb 100644 --- a/web/app/components/workflow/nodes/index.tsx +++ b/web/app/components/workflow/nodes/index.tsx @@ -1,6 +1,7 @@ import { memo } from 'react' import type { NodeProps } from 'reactflow' -import { BlockEnum, type SelectedNode } from '../types' +import type { Node } from '../types' +import { BlockEnum } from '../types' import { NodeComponentMap, PanelComponentMap, @@ -44,7 +45,7 @@ const CustomNode = memo((props: NodeProps) => { }) CustomNode.displayName = 'CustomNode' -export const Panel = memo((props: SelectedNode) => { +export const Panel = memo((props: Node) => { const nodeData = props.data const PanelComponent = PanelComponentMap[nodeData.type] diff --git a/web/app/components/workflow/panel/debug-and-preview/index.tsx b/web/app/components/workflow/panel/debug-and-preview/index.tsx index ce33d64206..6b84be64e4 100644 --- a/web/app/components/workflow/panel/debug-and-preview/index.tsx +++ b/web/app/components/workflow/panel/debug-and-preview/index.tsx @@ -4,7 +4,7 @@ import ChatWrapper from './chat-wrapper' const DebugAndPreview: FC = () => { return (
diff --git a/web/app/components/workflow/panel/index.tsx b/web/app/components/workflow/panel/index.tsx index 26c2443645..588068a117 100644 --- a/web/app/components/workflow/panel/index.tsx +++ b/web/app/components/workflow/panel/index.tsx @@ -3,6 +3,8 @@ import { memo, useMemo, } from 'react' +import { useNodes } from 'reactflow' +import type { CommonNodeType } from '../types' import { Panel as NodePanel } from '../nodes' import { useStore } from '../store' import WorkflowInfo from './workflow-info' @@ -11,7 +13,8 @@ import RunHistory from './run-history' const Panel: FC = () => { const mode = useStore(state => state.mode) - const selectedNode = useStore(state => state.selectedNode) + const nodes = useNodes() + const selectedNode = nodes.find(node => node.selected) const showRunHistory = useStore(state => state.showRunHistory) const { showWorkflowInfoPanel, @@ -26,7 +29,7 @@ const Panel: FC = () => { }, [mode, selectedNode]) return ( -
+
{ showNodePanel && ( diff --git a/web/app/components/workflow/panel/run-history.tsx b/web/app/components/workflow/panel/run-history.tsx index ade18d1a80..61753846b4 100644 --- a/web/app/components/workflow/panel/run-history.tsx +++ b/web/app/components/workflow/panel/run-history.tsx @@ -10,7 +10,7 @@ const RunHistory = () => { const setShowRunHistory = useStore(state => state.setShowRunHistory) return ( -
+
Run History
{ return ( -
+
diff --git a/web/app/components/workflow/store.ts b/web/app/components/workflow/store.ts index 4eef0b3060..45fb529462 100644 --- a/web/app/components/workflow/store.ts +++ b/web/app/components/workflow/store.ts @@ -1,21 +1,16 @@ import { create } from 'zustand' -import type { SelectedNode } from './types' type State = { mode: string - selectedNode: SelectedNode | null showRunHistory: boolean } type Action = { - setSelectedNode: (node: SelectedNode | null) => void setShowRunHistory: (showRunHistory: boolean) => void } export const useStore = create(set => ({ mode: 'workflow', - selectedNode: null, - setSelectedNode: node => set(() => ({ selectedNode: node })), showRunHistory: false, setShowRunHistory: showRunHistory => set(() => ({ showRunHistory })), })) diff --git a/web/app/components/workflow/types.ts b/web/app/components/workflow/types.ts index cc86706c9f..fd23a6aefb 100644 --- a/web/app/components/workflow/types.ts +++ b/web/app/components/workflow/types.ts @@ -24,13 +24,11 @@ export type Branch = { } export type CommonNodeType = { - hidden?: boolean position?: { x: number y: number } sortIndexInBranches?: number - selected?: boolean hovering?: boolean branches?: Branch[] title: string diff --git a/web/app/components/workflow/utils.ts b/web/app/components/workflow/utils.ts index e7e62510cb..b572c7b02f 100644 --- a/web/app/components/workflow/utils.ts +++ b/web/app/components/workflow/utils.ts @@ -12,7 +12,6 @@ export const initialNodesPosition = (oldNodes: Node[], edges: Edge[]) => { const nodes = cloneDeep(oldNodes) const start = nodes.find(node => node.data.type === BlockEnum.Start)! - start.data.hidden = false start.position.x = 0 start.position.y = 0 start.data.position = { @@ -38,7 +37,6 @@ export const initialNodesPosition = (oldNodes: Node[], edges: Edge[]) => { if (outgoers.length) { queue.push(...outgoers.map((outgoer) => { - outgoer.data.hidden = false outgoer.data.position = { x: depth + 1, y: breadth,