use-nodes-interactions.ts 40 KB

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