use-nodes-interactions.ts 39 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238
  1. import type { MouseEvent } from 'react'
  2. import { useCallback, useRef } from 'react'
  3. import { useTranslation } from 'react-i18next'
  4. import produce from 'immer'
  5. import type {
  6. NodeDragHandler,
  7. NodeMouseHandler,
  8. OnConnect,
  9. OnConnectEnd,
  10. OnConnectStart,
  11. ResizeParamsWithDirection,
  12. } from 'reactflow'
  13. import {
  14. getConnectedEdges,
  15. getOutgoers,
  16. useReactFlow,
  17. useStoreApi,
  18. } from 'reactflow'
  19. import type { ToolDefaultValue } from '../block-selector/types'
  20. import type {
  21. Edge,
  22. Node,
  23. OnNodeAdd,
  24. } from '../types'
  25. import { BlockEnum } from '../types'
  26. import { useWorkflowStore } from '../store'
  27. import {
  28. ITERATION_CHILDREN_Z_INDEX,
  29. ITERATION_PADDING,
  30. NODES_INITIAL_DATA,
  31. NODE_WIDTH_X_OFFSET,
  32. X_OFFSET,
  33. Y_OFFSET,
  34. } from '../constants'
  35. import {
  36. genNewNodeTitleFromOld,
  37. generateNewNode,
  38. getNodesConnectedSourceOrTargetHandleIdsMap,
  39. getTopLeftNodePosition,
  40. } from '../utils'
  41. import type { IterationNodeType } from '../nodes/iteration/types'
  42. import type { VariableAssignerNodeType } from '../nodes/variable-assigner/types'
  43. import { useNodeIterationInteractions } from '../nodes/iteration/use-interactions'
  44. import { useNodesSyncDraft } from './use-nodes-sync-draft'
  45. import { useHelpline } from './use-helpline'
  46. import {
  47. useNodesReadOnly,
  48. useWorkflow,
  49. } from './use-workflow'
  50. export const useNodesInteractions = () => {
  51. const { t } = useTranslation()
  52. const store = useStoreApi()
  53. const workflowStore = useWorkflowStore()
  54. const reactflow = useReactFlow()
  55. const { handleSyncWorkflowDraft } = useNodesSyncDraft()
  56. const {
  57. getAfterNodesInSameBranch,
  58. } = useWorkflow()
  59. const { getNodesReadOnly } = useNodesReadOnly()
  60. const { handleSetHelpline } = useHelpline()
  61. const {
  62. handleNodeIterationChildDrag,
  63. handleNodeIterationChildrenCopy,
  64. } = useNodeIterationInteractions()
  65. const dragNodeStartPosition = useRef({ x: 0, y: 0 } as { x: number; y: number })
  66. const handleNodeDragStart = useCallback<NodeDragHandler>((_, node) => {
  67. workflowStore.setState({ nodeAnimation: false })
  68. if (getNodesReadOnly())
  69. return
  70. if (node.data.isIterationStart)
  71. return
  72. dragNodeStartPosition.current = { x: node.position.x, y: node.position.y }
  73. }, [workflowStore, getNodesReadOnly])
  74. const handleNodeDrag = useCallback<NodeDragHandler>((e, node: Node) => {
  75. if (getNodesReadOnly())
  76. return
  77. if (node.data.isIterationStart)
  78. return
  79. const {
  80. getNodes,
  81. setNodes,
  82. } = store.getState()
  83. e.stopPropagation()
  84. const nodes = getNodes()
  85. const { restrictPosition } = handleNodeIterationChildDrag(node)
  86. const {
  87. showHorizontalHelpLineNodes,
  88. showVerticalHelpLineNodes,
  89. } = handleSetHelpline(node)
  90. const showHorizontalHelpLineNodesLength = showHorizontalHelpLineNodes.length
  91. const showVerticalHelpLineNodesLength = showVerticalHelpLineNodes.length
  92. const newNodes = produce(nodes, (draft) => {
  93. const currentNode = draft.find(n => n.id === node.id)!
  94. if (showVerticalHelpLineNodesLength > 0)
  95. currentNode.position.x = showVerticalHelpLineNodes[0].position.x
  96. else if (restrictPosition.x !== undefined)
  97. currentNode.position.x = restrictPosition.x
  98. else
  99. currentNode.position.x = node.position.x
  100. if (showHorizontalHelpLineNodesLength > 0)
  101. currentNode.position.y = showHorizontalHelpLineNodes[0].position.y
  102. else if (restrictPosition.y !== undefined)
  103. currentNode.position.y = restrictPosition.y
  104. else
  105. currentNode.position.y = node.position.y
  106. })
  107. setNodes(newNodes)
  108. }, [store, getNodesReadOnly, handleSetHelpline, handleNodeIterationChildDrag])
  109. const handleNodeDragStop = useCallback<NodeDragHandler>((_, node) => {
  110. const {
  111. setHelpLineHorizontal,
  112. setHelpLineVertical,
  113. } = workflowStore.getState()
  114. if (getNodesReadOnly())
  115. return
  116. const { x, y } = dragNodeStartPosition.current
  117. if (!(x === node.position.x && y === node.position.y)) {
  118. setHelpLineHorizontal()
  119. setHelpLineVertical()
  120. handleSyncWorkflowDraft()
  121. }
  122. }, [handleSyncWorkflowDraft, workflowStore, getNodesReadOnly])
  123. const handleNodeEnter = useCallback<NodeMouseHandler>((_, node) => {
  124. if (getNodesReadOnly())
  125. return
  126. const {
  127. getNodes,
  128. setNodes,
  129. edges,
  130. setEdges,
  131. } = store.getState()
  132. const nodes = getNodes()
  133. const {
  134. connectingNodePayload,
  135. setEnteringNodePayload,
  136. } = workflowStore.getState()
  137. if (connectingNodePayload) {
  138. if (connectingNodePayload.nodeId === node.id)
  139. return
  140. const connectingNode: Node = nodes.find(n => n.id === connectingNodePayload.nodeId)!
  141. const sameLevel = connectingNode.parentId === node.parentId
  142. if (sameLevel) {
  143. setEnteringNodePayload({
  144. nodeId: node.id,
  145. })
  146. const fromType = connectingNodePayload.handleType
  147. const newNodes = produce(nodes, (draft) => {
  148. draft.forEach((n) => {
  149. if (n.id === node.id && fromType === 'source' && (node.data.type === BlockEnum.VariableAssigner || node.data.type === BlockEnum.VariableAggregator)) {
  150. if (!node.data.advanced_settings?.group_enabled)
  151. n.data._isEntering = true
  152. }
  153. if (n.id === node.id && fromType === 'target' && (connectingNode.data.type === BlockEnum.VariableAssigner || connectingNode.data.type === BlockEnum.VariableAggregator) && node.data.type !== BlockEnum.IfElse && node.data.type !== BlockEnum.QuestionClassifier)
  154. n.data._isEntering = true
  155. })
  156. })
  157. setNodes(newNodes)
  158. }
  159. }
  160. const newEdges = produce(edges, (draft) => {
  161. const connectedEdges = getConnectedEdges([node], edges)
  162. connectedEdges.forEach((edge) => {
  163. const currentEdge = draft.find(e => e.id === edge.id)
  164. if (currentEdge)
  165. currentEdge.data._connectedNodeIsHovering = true
  166. })
  167. })
  168. setEdges(newEdges)
  169. }, [store, workflowStore, getNodesReadOnly])
  170. const handleNodeLeave = useCallback<NodeMouseHandler>(() => {
  171. if (getNodesReadOnly())
  172. return
  173. const {
  174. setEnteringNodePayload,
  175. } = workflowStore.getState()
  176. setEnteringNodePayload(undefined)
  177. const {
  178. getNodes,
  179. setNodes,
  180. edges,
  181. setEdges,
  182. } = store.getState()
  183. const newNodes = produce(getNodes(), (draft) => {
  184. draft.forEach((node) => {
  185. node.data._isEntering = false
  186. })
  187. })
  188. setNodes(newNodes)
  189. const newEdges = produce(edges, (draft) => {
  190. draft.forEach((edge) => {
  191. edge.data._connectedNodeIsHovering = false
  192. })
  193. })
  194. setEdges(newEdges)
  195. }, [store, workflowStore, getNodesReadOnly])
  196. const handleNodeSelect = useCallback((nodeId: string, cancelSelection?: boolean) => {
  197. const {
  198. getNodes,
  199. setNodes,
  200. edges,
  201. setEdges,
  202. } = store.getState()
  203. const nodes = getNodes()
  204. const selectedNode = nodes.find(node => node.data.selected)
  205. if (!cancelSelection && selectedNode?.id === nodeId)
  206. return
  207. const newNodes = produce(nodes, (draft) => {
  208. draft.forEach((node) => {
  209. if (node.id === nodeId)
  210. node.data.selected = !cancelSelection
  211. else
  212. node.data.selected = false
  213. })
  214. })
  215. setNodes(newNodes)
  216. const connectedEdges = getConnectedEdges([{ id: nodeId } as Node], edges).map(edge => edge.id)
  217. const newEdges = produce(edges, (draft) => {
  218. draft.forEach((edge) => {
  219. if (connectedEdges.includes(edge.id)) {
  220. edge.data = {
  221. ...edge.data,
  222. _connectedNodeIsSelected: !cancelSelection,
  223. }
  224. }
  225. else {
  226. edge.data = {
  227. ...edge.data,
  228. _connectedNodeIsSelected: false,
  229. }
  230. }
  231. })
  232. })
  233. setEdges(newEdges)
  234. handleSyncWorkflowDraft()
  235. }, [store, handleSyncWorkflowDraft])
  236. const handleNodeClick = useCallback<NodeMouseHandler>((_, node) => {
  237. handleNodeSelect(node.id)
  238. }, [handleNodeSelect])
  239. const handleNodeConnect = useCallback<OnConnect>(({
  240. source,
  241. sourceHandle,
  242. target,
  243. targetHandle,
  244. }) => {
  245. if (source === target)
  246. return
  247. if (getNodesReadOnly())
  248. return
  249. const {
  250. getNodes,
  251. setNodes,
  252. edges,
  253. setEdges,
  254. } = store.getState()
  255. const nodes = getNodes()
  256. const targetNode = nodes.find(node => node.id === target!)
  257. const sourceNode = nodes.find(node => node.id === source!)
  258. if (targetNode?.parentId !== sourceNode?.parentId)
  259. return
  260. if (targetNode?.data.isIterationStart)
  261. return
  262. const needDeleteEdges = edges.filter((edge) => {
  263. if (
  264. (edge.source === source && edge.sourceHandle === sourceHandle)
  265. || (edge.target === target && edge.targetHandle === targetHandle && targetNode?.data.type !== BlockEnum.VariableAssigner && targetNode?.data.type !== BlockEnum.VariableAggregator)
  266. )
  267. return true
  268. return false
  269. })
  270. const needDeleteEdgesIds = needDeleteEdges.map(edge => edge.id)
  271. const newEdge = {
  272. id: `${source}-${sourceHandle}-${target}-${targetHandle}`,
  273. type: 'custom',
  274. source: source!,
  275. target: target!,
  276. sourceHandle,
  277. targetHandle,
  278. data: {
  279. sourceType: nodes.find(node => node.id === source)!.data.type,
  280. targetType: nodes.find(node => node.id === target)!.data.type,
  281. isInIteration: !!targetNode?.parentId,
  282. iteration_id: targetNode?.parentId,
  283. },
  284. zIndex: targetNode?.parentId ? ITERATION_CHILDREN_Z_INDEX : 0,
  285. }
  286. const nodesConnectedSourceOrTargetHandleIdsMap = getNodesConnectedSourceOrTargetHandleIdsMap(
  287. [
  288. ...needDeleteEdges.map(edge => ({ type: 'remove', edge })),
  289. { type: 'add', edge: newEdge },
  290. ],
  291. nodes,
  292. )
  293. const newNodes = produce(nodes, (draft: Node[]) => {
  294. draft.forEach((node) => {
  295. if (nodesConnectedSourceOrTargetHandleIdsMap[node.id]) {
  296. node.data = {
  297. ...node.data,
  298. ...nodesConnectedSourceOrTargetHandleIdsMap[node.id],
  299. }
  300. }
  301. })
  302. })
  303. setNodes(newNodes)
  304. const newEdges = produce(edges, (draft) => {
  305. const filtered = draft.filter(edge => !needDeleteEdgesIds.includes(edge.id))
  306. filtered.push(newEdge)
  307. return filtered
  308. })
  309. setEdges(newEdges)
  310. handleSyncWorkflowDraft()
  311. }, [store, handleSyncWorkflowDraft, getNodesReadOnly])
  312. const handleNodeConnectStart = useCallback<OnConnectStart>((_, { nodeId, handleType, handleId }) => {
  313. if (getNodesReadOnly())
  314. return
  315. if (nodeId && handleType) {
  316. const { setConnectingNodePayload } = workflowStore.getState()
  317. const { getNodes } = store.getState()
  318. const node = getNodes().find(n => n.id === nodeId)!
  319. if (!node.data.isIterationStart) {
  320. setConnectingNodePayload({
  321. nodeId,
  322. nodeType: node.data.type,
  323. handleType,
  324. handleId,
  325. })
  326. }
  327. }
  328. }, [store, workflowStore, getNodesReadOnly])
  329. const handleNodeConnectEnd = useCallback<OnConnectEnd>((e: any) => {
  330. if (getNodesReadOnly())
  331. return
  332. const {
  333. connectingNodePayload,
  334. setConnectingNodePayload,
  335. enteringNodePayload,
  336. setEnteringNodePayload,
  337. } = workflowStore.getState()
  338. if (connectingNodePayload && enteringNodePayload) {
  339. const {
  340. setShowAssignVariablePopup,
  341. hoveringAssignVariableGroupId,
  342. } = workflowStore.getState()
  343. const { screenToFlowPosition } = reactflow
  344. const {
  345. getNodes,
  346. setNodes,
  347. } = store.getState()
  348. const nodes = getNodes()
  349. const fromHandleType = connectingNodePayload.handleType
  350. const fromHandleId = connectingNodePayload.handleId
  351. const fromNode = nodes.find(n => n.id === connectingNodePayload.nodeId)!
  352. const fromNodeParent = nodes.find(n => n.id === fromNode.parentId)
  353. const toNode = nodes.find(n => n.id === enteringNodePayload.nodeId)!
  354. const toParentNode = nodes.find(n => n.id === toNode.parentId)
  355. if (fromNode.parentId !== toNode.parentId)
  356. return
  357. const { x, y } = screenToFlowPosition({ x: e.x, y: e.y })
  358. if (fromHandleType === 'source' && (toNode.data.type === BlockEnum.VariableAssigner || toNode.data.type === BlockEnum.VariableAggregator)) {
  359. const groupEnabled = toNode.data.advanced_settings?.group_enabled
  360. if (
  361. (groupEnabled && hoveringAssignVariableGroupId)
  362. || !groupEnabled
  363. ) {
  364. const newNodes = produce(nodes, (draft) => {
  365. draft.forEach((node) => {
  366. if (node.id === toNode.id) {
  367. node.data._showAddVariablePopup = true
  368. node.data._holdAddVariablePopup = true
  369. }
  370. })
  371. })
  372. setNodes(newNodes)
  373. setShowAssignVariablePopup({
  374. nodeId: fromNode.id,
  375. nodeData: fromNode.data,
  376. variableAssignerNodeId: toNode.id,
  377. variableAssignerNodeData: toNode.data,
  378. variableAssignerNodeHandleId: hoveringAssignVariableGroupId || 'target',
  379. parentNode: toParentNode,
  380. x: x - toNode.positionAbsolute!.x,
  381. y: y - toNode.positionAbsolute!.y,
  382. })
  383. handleNodeConnect({
  384. source: fromNode.id,
  385. sourceHandle: fromHandleId,
  386. target: toNode.id,
  387. targetHandle: hoveringAssignVariableGroupId || 'target',
  388. })
  389. }
  390. }
  391. if (fromHandleType === 'target' && (fromNode.data.type === BlockEnum.VariableAssigner || fromNode.data.type === BlockEnum.VariableAggregator) && toNode.data.type !== BlockEnum.IfElse && toNode.data.type !== BlockEnum.QuestionClassifier) {
  392. const newNodes = produce(nodes, (draft) => {
  393. draft.forEach((node) => {
  394. if (node.id === toNode.id) {
  395. node.data._showAddVariablePopup = true
  396. node.data._holdAddVariablePopup = true
  397. }
  398. })
  399. })
  400. setNodes(newNodes)
  401. setShowAssignVariablePopup({
  402. nodeId: toNode.id,
  403. nodeData: toNode.data,
  404. variableAssignerNodeId: fromNode.id,
  405. variableAssignerNodeData: fromNode.data,
  406. variableAssignerNodeHandleId: fromHandleId || 'target',
  407. parentNode: fromNodeParent,
  408. x: x - toNode.positionAbsolute!.x,
  409. y: y - toNode.positionAbsolute!.y,
  410. })
  411. handleNodeConnect({
  412. source: toNode.id,
  413. sourceHandle: 'source',
  414. target: fromNode.id,
  415. targetHandle: fromHandleId,
  416. })
  417. }
  418. }
  419. setConnectingNodePayload(undefined)
  420. setEnteringNodePayload(undefined)
  421. }, [store, handleNodeConnect, getNodesReadOnly, workflowStore, reactflow])
  422. const handleNodeDelete = useCallback((nodeId: string) => {
  423. if (getNodesReadOnly())
  424. return
  425. const {
  426. getNodes,
  427. setNodes,
  428. edges,
  429. setEdges,
  430. } = store.getState()
  431. const nodes = getNodes()
  432. const currentNodeIndex = nodes.findIndex(node => node.id === nodeId)
  433. const currentNode = nodes[currentNodeIndex]
  434. if (!currentNode)
  435. return
  436. if (currentNode.data.type === BlockEnum.Start)
  437. return
  438. if (currentNode.data.type === BlockEnum.Iteration) {
  439. const iterationChildren = nodes.filter(node => node.parentId === currentNode.id)
  440. if (iterationChildren.length) {
  441. if (currentNode.data._isBundled) {
  442. iterationChildren.forEach((child) => {
  443. handleNodeDelete(child.id)
  444. })
  445. return handleNodeDelete(nodeId)
  446. }
  447. else {
  448. const { setShowConfirm, showConfirm } = workflowStore.getState()
  449. if (!showConfirm) {
  450. setShowConfirm({
  451. title: t('workflow.nodes.iteration.deleteTitle'),
  452. desc: t('workflow.nodes.iteration.deleteDesc') || '',
  453. onConfirm: () => {
  454. iterationChildren.forEach((child) => {
  455. handleNodeDelete(child.id)
  456. })
  457. handleNodeDelete(nodeId)
  458. handleSyncWorkflowDraft()
  459. setShowConfirm(undefined)
  460. },
  461. })
  462. return
  463. }
  464. }
  465. }
  466. }
  467. const connectedEdges = getConnectedEdges([{ id: nodeId } as Node], edges)
  468. const nodesConnectedSourceOrTargetHandleIdsMap = getNodesConnectedSourceOrTargetHandleIdsMap(connectedEdges.map(edge => ({ type: 'remove', edge })), nodes)
  469. const newNodes = produce(nodes, (draft: Node[]) => {
  470. draft.forEach((node) => {
  471. if (nodesConnectedSourceOrTargetHandleIdsMap[node.id]) {
  472. node.data = {
  473. ...node.data,
  474. ...nodesConnectedSourceOrTargetHandleIdsMap[node.id],
  475. }
  476. }
  477. if (node.id === currentNode.parentId) {
  478. node.data._children = node.data._children?.filter(child => child !== nodeId)
  479. if (currentNode.id === (node as Node<IterationNodeType>).data.start_node_id) {
  480. (node as Node<IterationNodeType>).data.start_node_id = '';
  481. (node as Node<IterationNodeType>).data.startNodeType = undefined
  482. }
  483. }
  484. })
  485. draft.splice(currentNodeIndex, 1)
  486. })
  487. setNodes(newNodes)
  488. const newEdges = produce(edges, (draft) => {
  489. return draft.filter(edge => !connectedEdges.find(connectedEdge => connectedEdge.id === edge.id))
  490. })
  491. setEdges(newEdges)
  492. handleSyncWorkflowDraft()
  493. }, [store, handleSyncWorkflowDraft, getNodesReadOnly, workflowStore, t])
  494. const handleNodeAdd = useCallback<OnNodeAdd>((
  495. {
  496. nodeType,
  497. sourceHandle = 'source',
  498. targetHandle = 'target',
  499. toolDefaultValue,
  500. },
  501. {
  502. prevNodeId,
  503. prevNodeSourceHandle,
  504. nextNodeId,
  505. nextNodeTargetHandle,
  506. },
  507. ) => {
  508. if (getNodesReadOnly())
  509. return
  510. const {
  511. getNodes,
  512. setNodes,
  513. edges,
  514. setEdges,
  515. } = store.getState()
  516. const nodes = getNodes()
  517. const nodesWithSameType = nodes.filter(node => node.data.type === nodeType)
  518. const newNode = generateNewNode({
  519. data: {
  520. ...NODES_INITIAL_DATA[nodeType],
  521. title: nodesWithSameType.length > 0 ? `${t(`workflow.blocks.${nodeType}`)} ${nodesWithSameType.length + 1}` : t(`workflow.blocks.${nodeType}`),
  522. ...(toolDefaultValue || {}),
  523. selected: true,
  524. _showAddVariablePopup: (nodeType === BlockEnum.VariableAssigner || nodeType === BlockEnum.VariableAggregator) && !!prevNodeId,
  525. _holdAddVariablePopup: false,
  526. },
  527. position: {
  528. x: 0,
  529. y: 0,
  530. },
  531. })
  532. if (prevNodeId && !nextNodeId) {
  533. const prevNodeIndex = nodes.findIndex(node => node.id === prevNodeId)
  534. const prevNode = nodes[prevNodeIndex]
  535. const outgoers = getOutgoers(prevNode, nodes, edges).sort((a, b) => a.position.y - b.position.y)
  536. const lastOutgoer = outgoers[outgoers.length - 1]
  537. newNode.data._connectedTargetHandleIds = [targetHandle]
  538. newNode.data._connectedSourceHandleIds = []
  539. newNode.position = {
  540. x: lastOutgoer ? lastOutgoer.position.x : prevNode.position.x + prevNode.width! + X_OFFSET,
  541. y: lastOutgoer ? lastOutgoer.position.y + lastOutgoer.height! + Y_OFFSET : prevNode.position.y,
  542. }
  543. newNode.parentId = prevNode.parentId
  544. newNode.extent = prevNode.extent
  545. if (prevNode.parentId) {
  546. newNode.data.isInIteration = true
  547. newNode.data.iteration_id = prevNode.parentId
  548. newNode.zIndex = ITERATION_CHILDREN_Z_INDEX
  549. }
  550. const newEdge: Edge = {
  551. id: `${prevNodeId}-${prevNodeSourceHandle}-${newNode.id}-${targetHandle}`,
  552. type: 'custom',
  553. source: prevNodeId,
  554. sourceHandle: prevNodeSourceHandle,
  555. target: newNode.id,
  556. targetHandle,
  557. data: {
  558. sourceType: prevNode.data.type,
  559. targetType: newNode.data.type,
  560. isInIteration: !!prevNode.parentId,
  561. iteration_id: prevNode.parentId,
  562. _connectedNodeIsSelected: true,
  563. },
  564. zIndex: prevNode.parentId ? ITERATION_CHILDREN_Z_INDEX : 0,
  565. }
  566. const nodesConnectedSourceOrTargetHandleIdsMap = getNodesConnectedSourceOrTargetHandleIdsMap(
  567. [
  568. { type: 'add', edge: newEdge },
  569. ],
  570. nodes,
  571. )
  572. const newNodes = produce(nodes, (draft: Node[]) => {
  573. draft.forEach((node) => {
  574. node.data.selected = false
  575. if (nodesConnectedSourceOrTargetHandleIdsMap[node.id]) {
  576. node.data = {
  577. ...node.data,
  578. ...nodesConnectedSourceOrTargetHandleIdsMap[node.id],
  579. }
  580. }
  581. if (node.data.type === BlockEnum.Iteration && prevNode.parentId === node.id)
  582. node.data._children?.push(newNode.id)
  583. })
  584. draft.push(newNode)
  585. })
  586. setNodes(newNodes)
  587. if (newNode.data.type === BlockEnum.VariableAssigner || newNode.data.type === BlockEnum.VariableAggregator) {
  588. const { setShowAssignVariablePopup } = workflowStore.getState()
  589. setShowAssignVariablePopup({
  590. nodeId: prevNode.id,
  591. nodeData: prevNode.data,
  592. variableAssignerNodeId: newNode.id,
  593. variableAssignerNodeData: (newNode.data as VariableAssignerNodeType),
  594. variableAssignerNodeHandleId: targetHandle,
  595. parentNode: nodes.find(node => node.id === newNode.parentId),
  596. x: -25,
  597. y: 44,
  598. })
  599. }
  600. const newEdges = produce(edges, (draft) => {
  601. draft.forEach((item) => {
  602. item.data = {
  603. ...item.data,
  604. _connectedNodeIsSelected: false,
  605. }
  606. })
  607. draft.push(newEdge)
  608. })
  609. setEdges(newEdges)
  610. }
  611. if (!prevNodeId && nextNodeId) {
  612. const nextNodeIndex = nodes.findIndex(node => node.id === nextNodeId)
  613. const nextNode = nodes[nextNodeIndex]!
  614. if ((nodeType !== BlockEnum.IfElse) && (nodeType !== BlockEnum.QuestionClassifier))
  615. newNode.data._connectedSourceHandleIds = [sourceHandle]
  616. newNode.data._connectedTargetHandleIds = []
  617. newNode.position = {
  618. x: nextNode.position.x,
  619. y: nextNode.position.y,
  620. }
  621. newNode.parentId = nextNode.parentId
  622. newNode.extent = nextNode.extent
  623. if (nextNode.parentId) {
  624. newNode.data.isInIteration = true
  625. newNode.data.iteration_id = nextNode.parentId
  626. newNode.zIndex = ITERATION_CHILDREN_Z_INDEX
  627. }
  628. if (nextNode.data.isIterationStart)
  629. newNode.data.isIterationStart = true
  630. let newEdge
  631. if ((nodeType !== BlockEnum.IfElse) && (nodeType !== BlockEnum.QuestionClassifier)) {
  632. newEdge = {
  633. id: `${newNode.id}-${sourceHandle}-${nextNodeId}-${nextNodeTargetHandle}`,
  634. type: 'custom',
  635. source: newNode.id,
  636. sourceHandle,
  637. target: nextNodeId,
  638. targetHandle: nextNodeTargetHandle,
  639. data: {
  640. sourceType: newNode.data.type,
  641. targetType: nextNode.data.type,
  642. isInIteration: !!nextNode.parentId,
  643. iteration_id: nextNode.parentId,
  644. _connectedNodeIsSelected: true,
  645. },
  646. zIndex: nextNode.parentId ? ITERATION_CHILDREN_Z_INDEX : 0,
  647. }
  648. }
  649. let nodesConnectedSourceOrTargetHandleIdsMap: Record<string, any>
  650. if (newEdge) {
  651. nodesConnectedSourceOrTargetHandleIdsMap = getNodesConnectedSourceOrTargetHandleIdsMap(
  652. [
  653. { type: 'add', edge: newEdge },
  654. ],
  655. nodes,
  656. )
  657. }
  658. const afterNodesInSameBranch = getAfterNodesInSameBranch(nextNodeId!)
  659. const afterNodesInSameBranchIds = afterNodesInSameBranch.map(node => node.id)
  660. const newNodes = produce(nodes, (draft) => {
  661. draft.forEach((node) => {
  662. node.data.selected = false
  663. if (afterNodesInSameBranchIds.includes(node.id))
  664. node.position.x += NODE_WIDTH_X_OFFSET
  665. if (nodesConnectedSourceOrTargetHandleIdsMap?.[node.id]) {
  666. node.data = {
  667. ...node.data,
  668. ...nodesConnectedSourceOrTargetHandleIdsMap[node.id],
  669. }
  670. }
  671. if (node.data.type === BlockEnum.Iteration && nextNode.parentId === node.id)
  672. node.data._children?.push(newNode.id)
  673. if (node.data.type === BlockEnum.Iteration && node.data.start_node_id === nextNodeId) {
  674. node.data.start_node_id = newNode.id
  675. node.data.startNodeType = newNode.data.type
  676. }
  677. if (node.id === nextNodeId && node.data.isIterationStart)
  678. node.data.isIterationStart = false
  679. })
  680. draft.push(newNode)
  681. })
  682. setNodes(newNodes)
  683. if (newEdge) {
  684. const newEdges = produce(edges, (draft) => {
  685. draft.forEach((item) => {
  686. item.data = {
  687. ...item.data,
  688. _connectedNodeIsSelected: false,
  689. }
  690. })
  691. draft.push(newEdge)
  692. })
  693. setEdges(newEdges)
  694. }
  695. }
  696. if (prevNodeId && nextNodeId) {
  697. const prevNode = nodes.find(node => node.id === prevNodeId)!
  698. const nextNode = nodes.find(node => node.id === nextNodeId)!
  699. newNode.data._connectedTargetHandleIds = [targetHandle]
  700. newNode.data._connectedSourceHandleIds = [sourceHandle]
  701. newNode.position = {
  702. x: nextNode.position.x,
  703. y: nextNode.position.y,
  704. }
  705. newNode.parentId = prevNode.parentId
  706. newNode.extent = prevNode.extent
  707. if (prevNode.parentId) {
  708. newNode.data.isInIteration = true
  709. newNode.data.iteration_id = prevNode.parentId
  710. newNode.zIndex = ITERATION_CHILDREN_Z_INDEX
  711. }
  712. const currentEdgeIndex = edges.findIndex(edge => edge.source === prevNodeId && edge.target === nextNodeId)
  713. const newPrevEdge = {
  714. id: `${prevNodeId}-${prevNodeSourceHandle}-${newNode.id}-${targetHandle}`,
  715. type: 'custom',
  716. source: prevNodeId,
  717. sourceHandle: prevNodeSourceHandle,
  718. target: newNode.id,
  719. targetHandle,
  720. data: {
  721. sourceType: prevNode.data.type,
  722. targetType: newNode.data.type,
  723. isInIteration: !!prevNode.parentId,
  724. iteration_id: prevNode.parentId,
  725. _connectedNodeIsSelected: true,
  726. },
  727. zIndex: prevNode.parentId ? ITERATION_CHILDREN_Z_INDEX : 0,
  728. }
  729. let newNextEdge: Edge | null = null
  730. if (nodeType !== BlockEnum.IfElse && nodeType !== BlockEnum.QuestionClassifier) {
  731. newNextEdge = {
  732. id: `${newNode.id}-${sourceHandle}-${nextNodeId}-${nextNodeTargetHandle}`,
  733. type: 'custom',
  734. source: newNode.id,
  735. sourceHandle,
  736. target: nextNodeId,
  737. targetHandle: nextNodeTargetHandle,
  738. data: {
  739. sourceType: newNode.data.type,
  740. targetType: nextNode.data.type,
  741. isInIteration: !!nextNode.parentId,
  742. iteration_id: nextNode.parentId,
  743. _connectedNodeIsSelected: true,
  744. },
  745. zIndex: nextNode.parentId ? ITERATION_CHILDREN_Z_INDEX : 0,
  746. }
  747. }
  748. const nodesConnectedSourceOrTargetHandleIdsMap = getNodesConnectedSourceOrTargetHandleIdsMap(
  749. [
  750. { type: 'remove', edge: edges[currentEdgeIndex] },
  751. { type: 'add', edge: newPrevEdge },
  752. ...(newNextEdge ? [{ type: 'add', edge: newNextEdge }] : []),
  753. ],
  754. [...nodes, newNode],
  755. )
  756. const afterNodesInSameBranch = getAfterNodesInSameBranch(nextNodeId!)
  757. const afterNodesInSameBranchIds = afterNodesInSameBranch.map(node => node.id)
  758. const newNodes = produce(nodes, (draft) => {
  759. draft.forEach((node) => {
  760. node.data.selected = false
  761. if (nodesConnectedSourceOrTargetHandleIdsMap[node.id]) {
  762. node.data = {
  763. ...node.data,
  764. ...nodesConnectedSourceOrTargetHandleIdsMap[node.id],
  765. }
  766. }
  767. if (afterNodesInSameBranchIds.includes(node.id))
  768. node.position.x += NODE_WIDTH_X_OFFSET
  769. if (node.data.type === BlockEnum.Iteration && prevNode.parentId === node.id)
  770. node.data._children?.push(newNode.id)
  771. })
  772. draft.push(newNode)
  773. })
  774. setNodes(newNodes)
  775. if (newNode.data.type === BlockEnum.VariableAssigner || newNode.data.type === BlockEnum.VariableAggregator) {
  776. const { setShowAssignVariablePopup } = workflowStore.getState()
  777. setShowAssignVariablePopup({
  778. nodeId: prevNode.id,
  779. nodeData: prevNode.data,
  780. variableAssignerNodeId: newNode.id,
  781. variableAssignerNodeData: newNode.data as VariableAssignerNodeType,
  782. variableAssignerNodeHandleId: targetHandle,
  783. parentNode: nodes.find(node => node.id === newNode.parentId),
  784. x: -25,
  785. y: 44,
  786. })
  787. }
  788. const newEdges = produce(edges, (draft) => {
  789. draft.splice(currentEdgeIndex, 1)
  790. draft.forEach((item) => {
  791. item.data = {
  792. ...item.data,
  793. _connectedNodeIsSelected: false,
  794. }
  795. })
  796. draft.push(newPrevEdge)
  797. if (newNextEdge)
  798. draft.push(newNextEdge)
  799. })
  800. setEdges(newEdges)
  801. }
  802. handleSyncWorkflowDraft()
  803. }, [store, workflowStore, handleSyncWorkflowDraft, getAfterNodesInSameBranch, getNodesReadOnly, t])
  804. const handleNodeChange = useCallback((
  805. currentNodeId: string,
  806. nodeType: BlockEnum,
  807. sourceHandle: string,
  808. toolDefaultValue?: ToolDefaultValue,
  809. ) => {
  810. if (getNodesReadOnly())
  811. return
  812. const {
  813. getNodes,
  814. setNodes,
  815. edges,
  816. setEdges,
  817. } = store.getState()
  818. const nodes = getNodes()
  819. const currentNode = nodes.find(node => node.id === currentNodeId)!
  820. const connectedEdges = getConnectedEdges([currentNode], edges)
  821. const nodesWithSameType = nodes.filter(node => node.data.type === nodeType)
  822. const newCurrentNode = generateNewNode({
  823. data: {
  824. ...NODES_INITIAL_DATA[nodeType],
  825. title: nodesWithSameType.length > 0 ? `${t(`workflow.blocks.${nodeType}`)} ${nodesWithSameType.length + 1}` : t(`workflow.blocks.${nodeType}`),
  826. ...(toolDefaultValue || {}),
  827. _connectedSourceHandleIds: [],
  828. _connectedTargetHandleIds: [],
  829. selected: currentNode.data.selected,
  830. isInIteration: currentNode.data.isInIteration,
  831. iteration_id: currentNode.data.iteration_id,
  832. isIterationStart: currentNode.data.isIterationStart,
  833. },
  834. position: {
  835. x: currentNode.position.x,
  836. y: currentNode.position.y,
  837. },
  838. parentId: currentNode.parentId,
  839. extent: currentNode.extent,
  840. zIndex: currentNode.zIndex,
  841. })
  842. const nodesConnectedSourceOrTargetHandleIdsMap = getNodesConnectedSourceOrTargetHandleIdsMap(
  843. [
  844. ...connectedEdges.map(edge => ({ type: 'remove', edge })),
  845. ],
  846. nodes,
  847. )
  848. const newNodes = produce(nodes, (draft) => {
  849. draft.forEach((node) => {
  850. node.data.selected = false
  851. if (nodesConnectedSourceOrTargetHandleIdsMap[node.id]) {
  852. node.data = {
  853. ...node.data,
  854. ...nodesConnectedSourceOrTargetHandleIdsMap[node.id],
  855. }
  856. }
  857. if (node.id === currentNode.parentId && currentNode.data.isIterationStart) {
  858. node.data._children = [
  859. newCurrentNode.id,
  860. ...(node.data._children || []),
  861. ].filter(child => child !== currentNodeId)
  862. node.data.start_node_id = newCurrentNode.id
  863. node.data.startNodeType = newCurrentNode.data.type
  864. }
  865. })
  866. const index = draft.findIndex(node => node.id === currentNodeId)
  867. draft.splice(index, 1, newCurrentNode)
  868. })
  869. setNodes(newNodes)
  870. const newEdges = produce(edges, (draft) => {
  871. const filtered = draft.filter(edge => !connectedEdges.find(connectedEdge => connectedEdge.id === edge.id))
  872. return filtered
  873. })
  874. setEdges(newEdges)
  875. handleSyncWorkflowDraft()
  876. }, [store, handleSyncWorkflowDraft, getNodesReadOnly, t])
  877. const handleNodeCancelRunningStatus = useCallback(() => {
  878. const {
  879. getNodes,
  880. setNodes,
  881. } = store.getState()
  882. const nodes = getNodes()
  883. const newNodes = produce(nodes, (draft) => {
  884. draft.forEach((node) => {
  885. node.data._runningStatus = undefined
  886. })
  887. })
  888. setNodes(newNodes)
  889. }, [store])
  890. const handleNodesCancelSelected = useCallback(() => {
  891. const {
  892. getNodes,
  893. setNodes,
  894. } = store.getState()
  895. const nodes = getNodes()
  896. const newNodes = produce(nodes, (draft) => {
  897. draft.forEach((node) => {
  898. node.data.selected = false
  899. })
  900. })
  901. setNodes(newNodes)
  902. }, [store])
  903. const handleNodeContextMenu = useCallback((e: MouseEvent, node: Node) => {
  904. e.preventDefault()
  905. const container = document.querySelector('#workflow-container')
  906. const { x, y } = container!.getBoundingClientRect()
  907. workflowStore.setState({
  908. nodeMenu: {
  909. top: e.clientY - y,
  910. left: e.clientX - x,
  911. nodeId: node.id,
  912. },
  913. })
  914. handleNodeSelect(node.id)
  915. }, [workflowStore, handleNodeSelect])
  916. const handleNodesCopy = useCallback(() => {
  917. if (getNodesReadOnly())
  918. return
  919. const {
  920. setClipboardElements,
  921. shortcutsDisabled,
  922. showFeaturesPanel,
  923. } = workflowStore.getState()
  924. if (shortcutsDisabled || showFeaturesPanel)
  925. return
  926. const {
  927. getNodes,
  928. } = store.getState()
  929. const nodes = getNodes()
  930. const bundledNodes = nodes.filter(node => node.data._isBundled && node.data.type !== BlockEnum.Start && !node.data.isInIteration)
  931. if (bundledNodes.length) {
  932. setClipboardElements(bundledNodes)
  933. return
  934. }
  935. const selectedNode = nodes.find(node => node.data.selected && node.data.type !== BlockEnum.Start)
  936. if (selectedNode)
  937. setClipboardElements([selectedNode])
  938. }, [getNodesReadOnly, store, workflowStore])
  939. const handleNodesPaste = useCallback(() => {
  940. if (getNodesReadOnly())
  941. return
  942. const {
  943. clipboardElements,
  944. shortcutsDisabled,
  945. showFeaturesPanel,
  946. mousePosition,
  947. } = workflowStore.getState()
  948. if (shortcutsDisabled || showFeaturesPanel)
  949. return
  950. const {
  951. getNodes,
  952. setNodes,
  953. } = store.getState()
  954. const nodesToPaste: Node[] = []
  955. const nodes = getNodes()
  956. if (clipboardElements.length) {
  957. const { x, y } = getTopLeftNodePosition(clipboardElements)
  958. const { screenToFlowPosition } = reactflow
  959. const currentPosition = screenToFlowPosition({ x: mousePosition.pageX, y: mousePosition.pageY })
  960. const offsetX = currentPosition.x - x
  961. const offsetY = currentPosition.y - y
  962. clipboardElements.forEach((nodeToPaste, index) => {
  963. const nodeType = nodeToPaste.data.type
  964. const newNode = generateNewNode({
  965. data: {
  966. ...NODES_INITIAL_DATA[nodeType],
  967. ...nodeToPaste.data,
  968. selected: false,
  969. _isBundled: false,
  970. _connectedSourceHandleIds: [],
  971. _connectedTargetHandleIds: [],
  972. title: genNewNodeTitleFromOld(nodeToPaste.data.title),
  973. },
  974. position: {
  975. x: nodeToPaste.position.x + offsetX,
  976. y: nodeToPaste.position.y + offsetY,
  977. },
  978. extent: nodeToPaste.extent,
  979. zIndex: nodeToPaste.zIndex,
  980. })
  981. newNode.id = newNode.id + index
  982. let newChildren: Node[] = []
  983. if (nodeToPaste.data.type === BlockEnum.Iteration) {
  984. newNode.data._children = [];
  985. (newNode.data as IterationNodeType).start_node_id = ''
  986. newChildren = handleNodeIterationChildrenCopy(nodeToPaste.id, newNode.id)
  987. newChildren.forEach((child) => {
  988. newNode.data._children?.push(child.id)
  989. if (child.data.isIterationStart)
  990. (newNode.data as IterationNodeType).start_node_id = child.id
  991. })
  992. }
  993. nodesToPaste.push(newNode)
  994. if (newChildren.length)
  995. nodesToPaste.push(...newChildren)
  996. })
  997. setNodes([...nodes, ...nodesToPaste])
  998. handleSyncWorkflowDraft()
  999. }
  1000. }, [t, getNodesReadOnly, store, workflowStore, handleSyncWorkflowDraft, reactflow, handleNodeIterationChildrenCopy])
  1001. const handleNodesDuplicate = useCallback(() => {
  1002. if (getNodesReadOnly())
  1003. return
  1004. handleNodesCopy()
  1005. handleNodesPaste()
  1006. }, [getNodesReadOnly, handleNodesCopy, handleNodesPaste])
  1007. const handleNodesDelete = useCallback(() => {
  1008. if (getNodesReadOnly())
  1009. return
  1010. const {
  1011. shortcutsDisabled,
  1012. showFeaturesPanel,
  1013. } = workflowStore.getState()
  1014. if (shortcutsDisabled || showFeaturesPanel)
  1015. return
  1016. const {
  1017. getNodes,
  1018. edges,
  1019. } = store.getState()
  1020. const nodes = getNodes()
  1021. const bundledNodes = nodes.filter(node => node.data._isBundled && node.data.type !== BlockEnum.Start)
  1022. if (bundledNodes.length) {
  1023. bundledNodes.forEach(node => handleNodeDelete(node.id))
  1024. return
  1025. }
  1026. const edgeSelected = edges.some(edge => edge.selected)
  1027. if (edgeSelected)
  1028. return
  1029. const selectedNode = nodes.find(node => node.data.selected && node.data.type !== BlockEnum.Start)
  1030. if (selectedNode)
  1031. handleNodeDelete(selectedNode.id)
  1032. }, [store, workflowStore, getNodesReadOnly, handleNodeDelete])
  1033. const handleNodeResize = useCallback((nodeId: string, params: ResizeParamsWithDirection) => {
  1034. if (getNodesReadOnly())
  1035. return
  1036. const {
  1037. getNodes,
  1038. setNodes,
  1039. } = store.getState()
  1040. const { x, y, width, height } = params
  1041. const nodes = getNodes()
  1042. const currentNode = nodes.find(n => n.id === nodeId)!
  1043. const childrenNodes = nodes.filter(n => currentNode.data._children?.includes(n.id))
  1044. let rightNode: Node
  1045. let bottomNode: Node
  1046. childrenNodes.forEach((n) => {
  1047. if (rightNode) {
  1048. if (n.position.x + n.width! > rightNode.position.x + rightNode.width!)
  1049. rightNode = n
  1050. }
  1051. else {
  1052. rightNode = n
  1053. }
  1054. if (bottomNode) {
  1055. if (n.position.y + n.height! > bottomNode.position.y + bottomNode.height!)
  1056. bottomNode = n
  1057. }
  1058. else {
  1059. bottomNode = n
  1060. }
  1061. })
  1062. if (rightNode! && bottomNode!) {
  1063. if (width < rightNode!.position.x + rightNode.width! + ITERATION_PADDING.right)
  1064. return
  1065. if (height < bottomNode.position.y + bottomNode.height! + ITERATION_PADDING.bottom)
  1066. return
  1067. }
  1068. const newNodes = produce(nodes, (draft) => {
  1069. draft.forEach((n) => {
  1070. if (n.id === nodeId) {
  1071. n.data.width = width
  1072. n.data.height = height
  1073. n.width = width
  1074. n.height = height
  1075. n.position.x = x
  1076. n.position.y = y
  1077. }
  1078. })
  1079. })
  1080. setNodes(newNodes)
  1081. handleSyncWorkflowDraft()
  1082. }, [store, getNodesReadOnly, handleSyncWorkflowDraft])
  1083. return {
  1084. handleNodeDragStart,
  1085. handleNodeDrag,
  1086. handleNodeDragStop,
  1087. handleNodeEnter,
  1088. handleNodeLeave,
  1089. handleNodeSelect,
  1090. handleNodeClick,
  1091. handleNodeConnect,
  1092. handleNodeConnectStart,
  1093. handleNodeConnectEnd,
  1094. handleNodeDelete,
  1095. handleNodeChange,
  1096. handleNodeAdd,
  1097. handleNodeCancelRunningStatus,
  1098. handleNodesCancelSelected,
  1099. handleNodeContextMenu,
  1100. handleNodesCopy,
  1101. handleNodesPaste,
  1102. handleNodesDuplicate,
  1103. handleNodesDelete,
  1104. handleNodeResize,
  1105. }
  1106. }