feat: workflow interaction (#4214)
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import type { MouseEvent } from 'react'
|
||||
import { useCallback, useRef } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import produce from 'immer'
|
||||
@@ -11,6 +12,7 @@ import type {
|
||||
import {
|
||||
getConnectedEdges,
|
||||
getOutgoers,
|
||||
useReactFlow,
|
||||
useStoreApi,
|
||||
} from 'reactflow'
|
||||
import type { ToolDefaultValue } from '../block-selector/types'
|
||||
@@ -29,6 +31,7 @@ import {
|
||||
import {
|
||||
generateNewNode,
|
||||
getNodesConnectedSourceOrTargetHandleIdsMap,
|
||||
getTopLeftNodePosition,
|
||||
} from '../utils'
|
||||
import { useNodesExtraData } from './use-nodes-data'
|
||||
import { useNodesSyncDraft } from './use-nodes-sync-draft'
|
||||
@@ -41,6 +44,7 @@ export const useNodesInteractions = () => {
|
||||
const { t } = useTranslation()
|
||||
const store = useStoreApi()
|
||||
const workflowStore = useWorkflowStore()
|
||||
const reactflow = useReactFlow()
|
||||
const nodesExtraData = useNodesExtraData()
|
||||
const { handleSyncWorkflowDraft } = useNodesSyncDraft()
|
||||
const {
|
||||
@@ -705,132 +709,6 @@ export const useNodesInteractions = () => {
|
||||
handleSyncWorkflowDraft()
|
||||
}, [store, handleSyncWorkflowDraft, getNodesReadOnly, t])
|
||||
|
||||
const handleNodeCopySelected = useCallback((): undefined | Node[] => {
|
||||
if (getNodesReadOnly())
|
||||
return
|
||||
|
||||
const {
|
||||
setClipboardElements,
|
||||
shortcutsDisabled,
|
||||
showFeaturesPanel,
|
||||
} = workflowStore.getState()
|
||||
|
||||
if (shortcutsDisabled || showFeaturesPanel)
|
||||
return
|
||||
|
||||
const {
|
||||
getNodes,
|
||||
} = store.getState()
|
||||
|
||||
const nodes = getNodes()
|
||||
const nodesToCopy = nodes.filter(node => node.data.selected && node.data.type !== BlockEnum.Start)
|
||||
|
||||
setClipboardElements(nodesToCopy)
|
||||
|
||||
return nodesToCopy
|
||||
}, [getNodesReadOnly, store, workflowStore])
|
||||
|
||||
const handleNodePaste = useCallback((): undefined | Node[] => {
|
||||
if (getNodesReadOnly())
|
||||
return
|
||||
|
||||
const {
|
||||
clipboardElements,
|
||||
shortcutsDisabled,
|
||||
showFeaturesPanel,
|
||||
} = workflowStore.getState()
|
||||
|
||||
if (shortcutsDisabled || showFeaturesPanel)
|
||||
return
|
||||
|
||||
const {
|
||||
getNodes,
|
||||
setNodes,
|
||||
} = store.getState()
|
||||
|
||||
const nodesToPaste: Node[] = []
|
||||
const nodes = getNodes()
|
||||
|
||||
for (const nodeToPaste of clipboardElements) {
|
||||
const nodeType = nodeToPaste.data.type
|
||||
const nodesWithSameType = nodes.filter(node => node.data.type === nodeType)
|
||||
|
||||
const newNode = generateNewNode({
|
||||
data: {
|
||||
...NODES_INITIAL_DATA[nodeType],
|
||||
...nodeToPaste.data,
|
||||
_connectedSourceHandleIds: [],
|
||||
_connectedTargetHandleIds: [],
|
||||
title: nodesWithSameType.length > 0 ? `${t(`workflow.blocks.${nodeType}`)} ${nodesWithSameType.length + 1}` : t(`workflow.blocks.${nodeType}`),
|
||||
selected: true,
|
||||
},
|
||||
position: {
|
||||
x: nodeToPaste.position.x + 10,
|
||||
y: nodeToPaste.position.y + 10,
|
||||
},
|
||||
})
|
||||
nodesToPaste.push(newNode)
|
||||
}
|
||||
|
||||
setNodes([...nodes.map((n: Node) => ({ ...n, selected: false, data: { ...n.data, selected: false } })), ...nodesToPaste])
|
||||
|
||||
handleSyncWorkflowDraft()
|
||||
|
||||
return nodesToPaste
|
||||
}, [getNodesReadOnly, handleSyncWorkflowDraft, store, t, workflowStore])
|
||||
|
||||
const handleNodeDuplicateSelected = useCallback(() => {
|
||||
if (getNodesReadOnly())
|
||||
return
|
||||
|
||||
handleNodeCopySelected()
|
||||
handleNodePaste()
|
||||
}, [getNodesReadOnly, handleNodeCopySelected, handleNodePaste])
|
||||
|
||||
const handleNodeCut = useCallback(() => {
|
||||
if (getNodesReadOnly())
|
||||
return
|
||||
|
||||
const nodesToCut = handleNodeCopySelected()
|
||||
if (!nodesToCut)
|
||||
return
|
||||
|
||||
for (const node of nodesToCut)
|
||||
handleNodeDelete(node.id)
|
||||
}, [getNodesReadOnly, handleNodeCopySelected, handleNodeDelete])
|
||||
|
||||
const handleNodeDeleteSelected = useCallback(() => {
|
||||
if (getNodesReadOnly())
|
||||
return
|
||||
|
||||
const {
|
||||
shortcutsDisabled,
|
||||
showFeaturesPanel,
|
||||
} = workflowStore.getState()
|
||||
|
||||
if (shortcutsDisabled || showFeaturesPanel)
|
||||
return
|
||||
|
||||
const {
|
||||
getNodes,
|
||||
edges,
|
||||
} = store.getState()
|
||||
|
||||
const currentEdgeIndex = edges.findIndex(edge => edge.selected)
|
||||
|
||||
if (currentEdgeIndex > -1)
|
||||
return
|
||||
|
||||
const nodes = getNodes()
|
||||
const nodesToDelete = nodes.filter(node => node.data.selected)
|
||||
|
||||
if (!nodesToDelete)
|
||||
return
|
||||
|
||||
for (const node of nodesToDelete)
|
||||
handleNodeDelete(node.id)
|
||||
}, [getNodesReadOnly, handleNodeDelete, store, workflowStore])
|
||||
|
||||
const handleNodeCancelRunningStatus = useCallback(() => {
|
||||
const {
|
||||
getNodes,
|
||||
@@ -861,6 +739,173 @@ export const useNodesInteractions = () => {
|
||||
setNodes(newNodes)
|
||||
}, [store])
|
||||
|
||||
const handleNodeContextMenu = useCallback((e: MouseEvent, node: Node) => {
|
||||
e.preventDefault()
|
||||
const container = document.querySelector('#workflow-container')
|
||||
const { x, y } = container!.getBoundingClientRect()
|
||||
workflowStore.setState({
|
||||
nodeMenu: {
|
||||
top: e.clientY - y,
|
||||
left: e.clientX - x,
|
||||
nodeId: node.id,
|
||||
},
|
||||
})
|
||||
handleNodeSelect(node.id)
|
||||
}, [workflowStore, handleNodeSelect])
|
||||
|
||||
const handleNodesCopy = useCallback(() => {
|
||||
if (getNodesReadOnly())
|
||||
return
|
||||
|
||||
const {
|
||||
setClipboardElements,
|
||||
shortcutsDisabled,
|
||||
showFeaturesPanel,
|
||||
} = workflowStore.getState()
|
||||
|
||||
if (shortcutsDisabled || showFeaturesPanel)
|
||||
return
|
||||
|
||||
const {
|
||||
getNodes,
|
||||
} = store.getState()
|
||||
|
||||
const nodes = getNodes()
|
||||
const bundledNodes = nodes.filter(node => node.data._isBundled && node.data.type !== BlockEnum.Start && node.data.type !== BlockEnum.End)
|
||||
|
||||
if (bundledNodes.length) {
|
||||
setClipboardElements(bundledNodes)
|
||||
return
|
||||
}
|
||||
|
||||
const selectedNode = nodes.find(node => node.data.selected && node.data.type !== BlockEnum.Start && node.data.type !== BlockEnum.End)
|
||||
|
||||
if (selectedNode)
|
||||
setClipboardElements([selectedNode])
|
||||
}, [getNodesReadOnly, store, workflowStore])
|
||||
|
||||
const handleNodesPaste = useCallback(() => {
|
||||
if (getNodesReadOnly())
|
||||
return
|
||||
|
||||
const {
|
||||
clipboardElements,
|
||||
shortcutsDisabled,
|
||||
showFeaturesPanel,
|
||||
mousePosition,
|
||||
} = workflowStore.getState()
|
||||
|
||||
if (shortcutsDisabled || showFeaturesPanel)
|
||||
return
|
||||
|
||||
const {
|
||||
getNodes,
|
||||
setNodes,
|
||||
} = store.getState()
|
||||
|
||||
const nodesToPaste: Node[] = []
|
||||
const nodes = getNodes()
|
||||
|
||||
if (clipboardElements.length) {
|
||||
const { x, y } = getTopLeftNodePosition(clipboardElements)
|
||||
const { screenToFlowPosition } = reactflow
|
||||
const currentPosition = screenToFlowPosition({ x: mousePosition.pageX, y: mousePosition.pageY })
|
||||
const offsetX = currentPosition.x - x
|
||||
const offsetY = currentPosition.y - y
|
||||
clipboardElements.forEach((nodeToPaste, index) => {
|
||||
const nodeType = nodeToPaste.data.type
|
||||
const nodesWithSameType = nodes.filter(node => node.data.type === nodeType)
|
||||
|
||||
const newNode = generateNewNode({
|
||||
data: {
|
||||
...NODES_INITIAL_DATA[nodeType],
|
||||
...nodeToPaste.data,
|
||||
selected: false,
|
||||
_isBundled: false,
|
||||
_connectedSourceHandleIds: [],
|
||||
_connectedTargetHandleIds: [],
|
||||
title: nodesWithSameType.length > 0 ? `${t(`workflow.blocks.${nodeType}`)} ${nodesWithSameType.length + 1}` : t(`workflow.blocks.${nodeType}`),
|
||||
},
|
||||
position: {
|
||||
x: nodeToPaste.position.x + offsetX,
|
||||
y: nodeToPaste.position.y + offsetY,
|
||||
},
|
||||
})
|
||||
newNode.id = newNode.id + index
|
||||
nodesToPaste.push(newNode)
|
||||
})
|
||||
|
||||
setNodes([...nodes, ...nodesToPaste])
|
||||
handleSyncWorkflowDraft()
|
||||
}
|
||||
}, [t, getNodesReadOnly, store, workflowStore, handleSyncWorkflowDraft, reactflow])
|
||||
|
||||
const handleNodesDuplicate = useCallback(() => {
|
||||
if (getNodesReadOnly())
|
||||
return
|
||||
|
||||
const {
|
||||
getNodes,
|
||||
setNodes,
|
||||
} = store.getState()
|
||||
const nodes = getNodes()
|
||||
|
||||
const selectedNode = nodes.find(node => node.data.selected && node.data.type !== BlockEnum.Start && node.data.type !== BlockEnum.End)
|
||||
|
||||
if (selectedNode) {
|
||||
const nodeType = selectedNode.data.type
|
||||
const nodesWithSameType = nodes.filter(node => node.data.type === nodeType)
|
||||
|
||||
const newNode = generateNewNode({
|
||||
data: {
|
||||
...NODES_INITIAL_DATA[nodeType as BlockEnum],
|
||||
...selectedNode.data,
|
||||
selected: false,
|
||||
_isBundled: false,
|
||||
_connectedSourceHandleIds: [],
|
||||
_connectedTargetHandleIds: [],
|
||||
title: nodesWithSameType.length > 0 ? `${t(`workflow.blocks.${nodeType}`)} ${nodesWithSameType.length + 1}` : t(`workflow.blocks.${nodeType}`),
|
||||
},
|
||||
position: {
|
||||
x: selectedNode.position.x + selectedNode.width! + 10,
|
||||
y: selectedNode.position.y,
|
||||
},
|
||||
})
|
||||
|
||||
setNodes([...nodes, newNode])
|
||||
}
|
||||
}, [store, t, getNodesReadOnly])
|
||||
|
||||
const handleNodesDelete = useCallback(() => {
|
||||
if (getNodesReadOnly())
|
||||
return
|
||||
|
||||
const {
|
||||
shortcutsDisabled,
|
||||
showFeaturesPanel,
|
||||
} = workflowStore.getState()
|
||||
|
||||
if (shortcutsDisabled || showFeaturesPanel)
|
||||
return
|
||||
|
||||
const {
|
||||
getNodes,
|
||||
} = store.getState()
|
||||
|
||||
const nodes = getNodes()
|
||||
const bundledNodes = nodes.filter(node => node.data._isBundled && node.data.type !== BlockEnum.Start && node.data.type !== BlockEnum.End)
|
||||
|
||||
if (bundledNodes.length) {
|
||||
bundledNodes.forEach(node => handleNodeDelete(node.id))
|
||||
return
|
||||
}
|
||||
|
||||
const selectedNode = nodes.find(node => node.data.selected && node.data.type !== BlockEnum.Start && node.data.type !== BlockEnum.End)
|
||||
|
||||
if (selectedNode)
|
||||
handleNodeDelete(selectedNode.id)
|
||||
}, [store, workflowStore, getNodesReadOnly, handleNodeDelete])
|
||||
|
||||
return {
|
||||
handleNodeDragStart,
|
||||
handleNodeDrag,
|
||||
@@ -875,12 +920,12 @@ export const useNodesInteractions = () => {
|
||||
handleNodeDelete,
|
||||
handleNodeChange,
|
||||
handleNodeAdd,
|
||||
handleNodeDuplicateSelected,
|
||||
handleNodeCopySelected,
|
||||
handleNodeCut,
|
||||
handleNodeDeleteSelected,
|
||||
handleNodePaste,
|
||||
handleNodeCancelRunningStatus,
|
||||
handleNodesCancelSelected,
|
||||
handleNodeContextMenu,
|
||||
handleNodesCopy,
|
||||
handleNodesPaste,
|
||||
handleNodesDuplicate,
|
||||
handleNodesDelete,
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user