You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

use-nodes-interactions.ts 54KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671
  1. import type { MouseEvent } from 'react'
  2. import { useCallback, useRef, useState } 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. CUSTOM_EDGE,
  29. ITERATION_CHILDREN_Z_INDEX,
  30. ITERATION_PADDING,
  31. LOOP_CHILDREN_Z_INDEX,
  32. LOOP_PADDING,
  33. NODES_INITIAL_DATA,
  34. NODE_WIDTH_X_OFFSET,
  35. X_OFFSET,
  36. Y_OFFSET,
  37. } from '../constants'
  38. import {
  39. genNewNodeTitleFromOld,
  40. generateNewNode,
  41. getNodeCustomTypeByNodeDataType,
  42. getNodesConnectedSourceOrTargetHandleIdsMap,
  43. getTopLeftNodePosition,
  44. } from '../utils'
  45. import { CUSTOM_NOTE_NODE } from '../note-node/constants'
  46. import type { IterationNodeType } from '../nodes/iteration/types'
  47. import type { LoopNodeType } from '../nodes/loop/types'
  48. import { CUSTOM_ITERATION_START_NODE } from '../nodes/iteration-start/constants'
  49. import { CUSTOM_LOOP_START_NODE } from '../nodes/loop-start/constants'
  50. import type { VariableAssignerNodeType } from '../nodes/variable-assigner/types'
  51. import { useNodeIterationInteractions } from '../nodes/iteration/use-interactions'
  52. import { useNodeLoopInteractions } from '../nodes/loop/use-interactions'
  53. import { useWorkflowHistoryStore } from '../workflow-history-store'
  54. import { useNodesSyncDraft } from './use-nodes-sync-draft'
  55. import { useHelpline } from './use-helpline'
  56. import {
  57. useNodesReadOnly,
  58. useWorkflow,
  59. useWorkflowReadOnly,
  60. } from './use-workflow'
  61. import { WorkflowHistoryEvent, useWorkflowHistory } from './use-workflow-history'
  62. import useInspectVarsCrud from './use-inspect-vars-crud'
  63. import { getNodeUsedVars } from '../nodes/_base/components/variable/utils'
  64. export const useNodesInteractions = () => {
  65. const { t } = useTranslation()
  66. const store = useStoreApi()
  67. const workflowStore = useWorkflowStore()
  68. const reactflow = useReactFlow()
  69. const { store: workflowHistoryStore } = useWorkflowHistoryStore()
  70. const { handleSyncWorkflowDraft } = useNodesSyncDraft()
  71. const {
  72. checkNestedParallelLimit,
  73. getAfterNodesInSameBranch,
  74. } = useWorkflow()
  75. const { getNodesReadOnly } = useNodesReadOnly()
  76. const { getWorkflowReadOnly } = useWorkflowReadOnly()
  77. const { handleSetHelpline } = useHelpline()
  78. const {
  79. handleNodeIterationChildDrag,
  80. handleNodeIterationChildrenCopy,
  81. } = useNodeIterationInteractions()
  82. const {
  83. handleNodeLoopChildDrag,
  84. handleNodeLoopChildrenCopy,
  85. } = useNodeLoopInteractions()
  86. const dragNodeStartPosition = useRef({ x: 0, y: 0 } as { x: number; y: number })
  87. const { saveStateToHistory, undo, redo } = useWorkflowHistory()
  88. const handleNodeDragStart = useCallback<NodeDragHandler>((_, node) => {
  89. workflowStore.setState({ nodeAnimation: false })
  90. if (getNodesReadOnly())
  91. return
  92. if (node.type === CUSTOM_ITERATION_START_NODE || node.type === CUSTOM_NOTE_NODE)
  93. return
  94. if (node.type === CUSTOM_LOOP_START_NODE || node.type === CUSTOM_NOTE_NODE)
  95. return
  96. dragNodeStartPosition.current = { x: node.position.x, y: node.position.y }
  97. }, [workflowStore, getNodesReadOnly])
  98. const handleNodeDrag = useCallback<NodeDragHandler>((e, node: Node) => {
  99. if (getNodesReadOnly())
  100. return
  101. if (node.type === CUSTOM_ITERATION_START_NODE)
  102. return
  103. if (node.type === CUSTOM_LOOP_START_NODE)
  104. return
  105. const {
  106. getNodes,
  107. setNodes,
  108. } = store.getState()
  109. e.stopPropagation()
  110. const nodes = getNodes()
  111. const { restrictPosition } = handleNodeIterationChildDrag(node)
  112. const { restrictPosition: restrictLoopPosition } = handleNodeLoopChildDrag(node)
  113. const {
  114. showHorizontalHelpLineNodes,
  115. showVerticalHelpLineNodes,
  116. } = handleSetHelpline(node)
  117. const showHorizontalHelpLineNodesLength = showHorizontalHelpLineNodes.length
  118. const showVerticalHelpLineNodesLength = showVerticalHelpLineNodes.length
  119. const newNodes = produce(nodes, (draft) => {
  120. const currentNode = draft.find(n => n.id === node.id)!
  121. if (showVerticalHelpLineNodesLength > 0)
  122. currentNode.position.x = showVerticalHelpLineNodes[0].position.x
  123. else if (restrictPosition.x !== undefined)
  124. currentNode.position.x = restrictPosition.x
  125. else if (restrictLoopPosition.x !== undefined)
  126. currentNode.position.x = restrictLoopPosition.x
  127. else
  128. currentNode.position.x = node.position.x
  129. if (showHorizontalHelpLineNodesLength > 0)
  130. currentNode.position.y = showHorizontalHelpLineNodes[0].position.y
  131. else if (restrictPosition.y !== undefined)
  132. currentNode.position.y = restrictPosition.y
  133. else if (restrictLoopPosition.y !== undefined)
  134. currentNode.position.y = restrictLoopPosition.y
  135. else
  136. currentNode.position.y = node.position.y
  137. })
  138. setNodes(newNodes)
  139. }, [getNodesReadOnly, store, handleNodeIterationChildDrag, handleNodeLoopChildDrag, handleSetHelpline])
  140. const handleNodeDragStop = useCallback<NodeDragHandler>((_, node) => {
  141. const {
  142. setHelpLineHorizontal,
  143. setHelpLineVertical,
  144. } = workflowStore.getState()
  145. if (getNodesReadOnly())
  146. return
  147. const { x, y } = dragNodeStartPosition.current
  148. if (!(x === node.position.x && y === node.position.y)) {
  149. setHelpLineHorizontal()
  150. setHelpLineVertical()
  151. handleSyncWorkflowDraft()
  152. if (x !== 0 && y !== 0) {
  153. // selecting a note will trigger a drag stop event with x and y as 0
  154. saveStateToHistory(WorkflowHistoryEvent.NodeDragStop)
  155. }
  156. }
  157. }, [workflowStore, getNodesReadOnly, saveStateToHistory, handleSyncWorkflowDraft])
  158. const handleNodeEnter = useCallback<NodeMouseHandler>((_, node) => {
  159. if (getNodesReadOnly())
  160. return
  161. if (node.type === CUSTOM_NOTE_NODE || node.type === CUSTOM_ITERATION_START_NODE)
  162. return
  163. if (node.type === CUSTOM_LOOP_START_NODE || node.type === CUSTOM_NOTE_NODE)
  164. return
  165. const {
  166. getNodes,
  167. setNodes,
  168. edges,
  169. setEdges,
  170. } = store.getState()
  171. const nodes = getNodes()
  172. const {
  173. connectingNodePayload,
  174. setEnteringNodePayload,
  175. } = workflowStore.getState()
  176. if (connectingNodePayload) {
  177. if (connectingNodePayload.nodeId === node.id)
  178. return
  179. const connectingNode: Node = nodes.find(n => n.id === connectingNodePayload.nodeId)!
  180. const sameLevel = connectingNode.parentId === node.parentId
  181. if (sameLevel) {
  182. setEnteringNodePayload({
  183. nodeId: node.id,
  184. nodeData: node.data as VariableAssignerNodeType,
  185. })
  186. const fromType = connectingNodePayload.handleType
  187. const newNodes = produce(nodes, (draft) => {
  188. draft.forEach((n) => {
  189. if (n.id === node.id && fromType === 'source' && (node.data.type === BlockEnum.VariableAssigner || node.data.type === BlockEnum.VariableAggregator)) {
  190. if (!node.data.advanced_settings?.group_enabled)
  191. n.data._isEntering = true
  192. }
  193. 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)
  194. n.data._isEntering = true
  195. })
  196. })
  197. setNodes(newNodes)
  198. }
  199. }
  200. const newEdges = produce(edges, (draft) => {
  201. const connectedEdges = getConnectedEdges([node], edges)
  202. connectedEdges.forEach((edge) => {
  203. const currentEdge = draft.find(e => e.id === edge.id)
  204. if (currentEdge)
  205. currentEdge.data._connectedNodeIsHovering = true
  206. })
  207. })
  208. setEdges(newEdges)
  209. }, [store, workflowStore, getNodesReadOnly])
  210. const handleNodeLeave = useCallback<NodeMouseHandler>((_, node) => {
  211. if (getNodesReadOnly())
  212. return
  213. if (node.type === CUSTOM_NOTE_NODE || node.type === CUSTOM_ITERATION_START_NODE)
  214. return
  215. if (node.type === CUSTOM_NOTE_NODE || node.type === CUSTOM_LOOP_START_NODE)
  216. return
  217. const {
  218. setEnteringNodePayload,
  219. } = workflowStore.getState()
  220. setEnteringNodePayload(undefined)
  221. const {
  222. getNodes,
  223. setNodes,
  224. edges,
  225. setEdges,
  226. } = store.getState()
  227. const newNodes = produce(getNodes(), (draft) => {
  228. draft.forEach((node) => {
  229. node.data._isEntering = false
  230. })
  231. })
  232. setNodes(newNodes)
  233. const newEdges = produce(edges, (draft) => {
  234. draft.forEach((edge) => {
  235. edge.data._connectedNodeIsHovering = false
  236. })
  237. })
  238. setEdges(newEdges)
  239. }, [store, workflowStore, getNodesReadOnly])
  240. const handleNodeSelect = useCallback((nodeId: string, cancelSelection?: boolean, initShowLastRunTab?: boolean) => {
  241. if(initShowLastRunTab)
  242. workflowStore.setState({ initShowLastRunTab: true })
  243. const {
  244. getNodes,
  245. setNodes,
  246. edges,
  247. setEdges,
  248. } = store.getState()
  249. const nodes = getNodes()
  250. const selectedNode = nodes.find(node => node.data.selected)
  251. if (!cancelSelection && selectedNode?.id === nodeId)
  252. return
  253. const newNodes = produce(nodes, (draft) => {
  254. draft.forEach((node) => {
  255. if (node.id === nodeId)
  256. node.data.selected = !cancelSelection
  257. else
  258. node.data.selected = false
  259. })
  260. })
  261. setNodes(newNodes)
  262. const connectedEdges = getConnectedEdges([{ id: nodeId } as Node], edges).map(edge => edge.id)
  263. const newEdges = produce(edges, (draft) => {
  264. draft.forEach((edge) => {
  265. if (connectedEdges.includes(edge.id)) {
  266. edge.data = {
  267. ...edge.data,
  268. _connectedNodeIsSelected: !cancelSelection,
  269. }
  270. }
  271. else {
  272. edge.data = {
  273. ...edge.data,
  274. _connectedNodeIsSelected: false,
  275. }
  276. }
  277. })
  278. })
  279. setEdges(newEdges)
  280. handleSyncWorkflowDraft()
  281. }, [store, handleSyncWorkflowDraft])
  282. const handleNodeClick = useCallback<NodeMouseHandler>((_, node) => {
  283. if (node.type === CUSTOM_ITERATION_START_NODE)
  284. return
  285. if (node.type === CUSTOM_LOOP_START_NODE)
  286. return
  287. handleNodeSelect(node.id)
  288. }, [handleNodeSelect])
  289. const handleNodeConnect = useCallback<OnConnect>(({
  290. source,
  291. sourceHandle,
  292. target,
  293. targetHandle,
  294. }) => {
  295. if (source === target)
  296. return
  297. if (getNodesReadOnly())
  298. return
  299. const {
  300. getNodes,
  301. setNodes,
  302. edges,
  303. setEdges,
  304. } = store.getState()
  305. const nodes = getNodes()
  306. const targetNode = nodes.find(node => node.id === target!)
  307. const sourceNode = nodes.find(node => node.id === source!)
  308. if (targetNode?.parentId !== sourceNode?.parentId)
  309. return
  310. if (sourceNode?.type === CUSTOM_NOTE_NODE || targetNode?.type === CUSTOM_NOTE_NODE)
  311. return
  312. if (edges.find(edge => edge.source === source && edge.sourceHandle === sourceHandle && edge.target === target && edge.targetHandle === targetHandle))
  313. return
  314. const parendNode = nodes.find(node => node.id === targetNode?.parentId)
  315. const isInIteration = parendNode && parendNode.data.type === BlockEnum.Iteration
  316. const isInLoop = !!parendNode && parendNode.data.type === BlockEnum.Loop
  317. const newEdge = {
  318. id: `${source}-${sourceHandle}-${target}-${targetHandle}`,
  319. type: CUSTOM_EDGE,
  320. source: source!,
  321. target: target!,
  322. sourceHandle,
  323. targetHandle,
  324. data: {
  325. sourceType: nodes.find(node => node.id === source)!.data.type,
  326. targetType: nodes.find(node => node.id === target)!.data.type,
  327. isInIteration,
  328. iteration_id: isInIteration ? targetNode?.parentId : undefined,
  329. isInLoop,
  330. loop_id: isInLoop ? targetNode?.parentId : undefined,
  331. },
  332. zIndex: targetNode?.parentId ? (isInIteration ? ITERATION_CHILDREN_Z_INDEX : LOOP_CHILDREN_Z_INDEX) : 0,
  333. }
  334. const nodesConnectedSourceOrTargetHandleIdsMap = getNodesConnectedSourceOrTargetHandleIdsMap(
  335. [
  336. { type: 'add', edge: newEdge },
  337. ],
  338. nodes,
  339. )
  340. const newNodes = produce(nodes, (draft: Node[]) => {
  341. draft.forEach((node) => {
  342. if (nodesConnectedSourceOrTargetHandleIdsMap[node.id]) {
  343. node.data = {
  344. ...node.data,
  345. ...nodesConnectedSourceOrTargetHandleIdsMap[node.id],
  346. }
  347. }
  348. })
  349. })
  350. const newEdges = produce(edges, (draft) => {
  351. draft.push(newEdge)
  352. })
  353. if (checkNestedParallelLimit(newNodes, newEdges, targetNode?.parentId)) {
  354. setNodes(newNodes)
  355. setEdges(newEdges)
  356. handleSyncWorkflowDraft()
  357. saveStateToHistory(WorkflowHistoryEvent.NodeConnect)
  358. }
  359. else {
  360. const {
  361. setConnectingNodePayload,
  362. setEnteringNodePayload,
  363. } = workflowStore.getState()
  364. setConnectingNodePayload(undefined)
  365. setEnteringNodePayload(undefined)
  366. }
  367. }, [getNodesReadOnly, store, workflowStore, handleSyncWorkflowDraft, saveStateToHistory, checkNestedParallelLimit])
  368. const handleNodeConnectStart = useCallback<OnConnectStart>((_, { nodeId, handleType, handleId }) => {
  369. if (getNodesReadOnly())
  370. return
  371. if (nodeId && handleType) {
  372. const { setConnectingNodePayload } = workflowStore.getState()
  373. const { getNodes } = store.getState()
  374. const node = getNodes().find(n => n.id === nodeId)!
  375. if (node.type === CUSTOM_NOTE_NODE)
  376. return
  377. if (node.data.type === BlockEnum.VariableAggregator || node.data.type === BlockEnum.VariableAssigner) {
  378. if (handleType === 'target')
  379. return
  380. }
  381. setConnectingNodePayload({
  382. nodeId,
  383. nodeType: node.data.type,
  384. handleType,
  385. handleId,
  386. })
  387. }
  388. }, [store, workflowStore, getNodesReadOnly])
  389. const handleNodeConnectEnd = useCallback<OnConnectEnd>((e: any) => {
  390. if (getNodesReadOnly())
  391. return
  392. const {
  393. connectingNodePayload,
  394. setConnectingNodePayload,
  395. enteringNodePayload,
  396. setEnteringNodePayload,
  397. } = workflowStore.getState()
  398. if (connectingNodePayload && enteringNodePayload) {
  399. const {
  400. setShowAssignVariablePopup,
  401. hoveringAssignVariableGroupId,
  402. } = workflowStore.getState()
  403. const { screenToFlowPosition } = reactflow
  404. const {
  405. getNodes,
  406. setNodes,
  407. } = store.getState()
  408. const nodes = getNodes()
  409. const fromHandleType = connectingNodePayload.handleType
  410. const fromHandleId = connectingNodePayload.handleId
  411. const fromNode = nodes.find(n => n.id === connectingNodePayload.nodeId)!
  412. const toNode = nodes.find(n => n.id === enteringNodePayload.nodeId)!
  413. const toParentNode = nodes.find(n => n.id === toNode.parentId)
  414. if (fromNode.parentId !== toNode.parentId)
  415. return
  416. const { x, y } = screenToFlowPosition({ x: e.x, y: e.y })
  417. if (fromHandleType === 'source' && (toNode.data.type === BlockEnum.VariableAssigner || toNode.data.type === BlockEnum.VariableAggregator)) {
  418. const groupEnabled = toNode.data.advanced_settings?.group_enabled
  419. const firstGroupId = toNode.data.advanced_settings?.groups[0].groupId
  420. let handleId = 'target'
  421. if (groupEnabled) {
  422. if (hoveringAssignVariableGroupId)
  423. handleId = hoveringAssignVariableGroupId
  424. else
  425. handleId = firstGroupId
  426. }
  427. const newNodes = produce(nodes, (draft) => {
  428. draft.forEach((node) => {
  429. if (node.id === toNode.id) {
  430. node.data._showAddVariablePopup = true
  431. node.data._holdAddVariablePopup = true
  432. }
  433. })
  434. })
  435. setNodes(newNodes)
  436. setShowAssignVariablePopup({
  437. nodeId: fromNode.id,
  438. nodeData: fromNode.data,
  439. variableAssignerNodeId: toNode.id,
  440. variableAssignerNodeData: toNode.data,
  441. variableAssignerNodeHandleId: handleId,
  442. parentNode: toParentNode,
  443. x: x - toNode.positionAbsolute!.x,
  444. y: y - toNode.positionAbsolute!.y,
  445. })
  446. handleNodeConnect({
  447. source: fromNode.id,
  448. sourceHandle: fromHandleId,
  449. target: toNode.id,
  450. targetHandle: 'target',
  451. })
  452. }
  453. }
  454. setConnectingNodePayload(undefined)
  455. setEnteringNodePayload(undefined)
  456. }, [store, handleNodeConnect, getNodesReadOnly, workflowStore, reactflow])
  457. const { deleteNodeInspectorVars } = useInspectVarsCrud()
  458. const handleNodeDelete = useCallback((nodeId: string) => {
  459. if (getNodesReadOnly())
  460. return
  461. const {
  462. getNodes,
  463. setNodes,
  464. edges,
  465. setEdges,
  466. } = store.getState()
  467. const nodes = getNodes()
  468. const currentNodeIndex = nodes.findIndex(node => node.id === nodeId)
  469. const currentNode = nodes[currentNodeIndex]
  470. if (!currentNode)
  471. return
  472. if (currentNode.data.type === BlockEnum.Start)
  473. return
  474. deleteNodeInspectorVars(nodeId)
  475. if (currentNode.data.type === BlockEnum.Iteration) {
  476. const iterationChildren = nodes.filter(node => node.parentId === currentNode.id)
  477. if (iterationChildren.length) {
  478. if (currentNode.data._isBundled) {
  479. iterationChildren.forEach((child) => {
  480. handleNodeDelete(child.id)
  481. })
  482. return handleNodeDelete(nodeId)
  483. }
  484. else {
  485. if (iterationChildren.length === 1) {
  486. handleNodeDelete(iterationChildren[0].id)
  487. handleNodeDelete(nodeId)
  488. return
  489. }
  490. const { setShowConfirm, showConfirm } = workflowStore.getState()
  491. if (!showConfirm) {
  492. setShowConfirm({
  493. title: t('workflow.nodes.iteration.deleteTitle'),
  494. desc: t('workflow.nodes.iteration.deleteDesc') || '',
  495. onConfirm: () => {
  496. iterationChildren.forEach((child) => {
  497. handleNodeDelete(child.id)
  498. })
  499. handleNodeDelete(nodeId)
  500. handleSyncWorkflowDraft()
  501. setShowConfirm(undefined)
  502. },
  503. })
  504. return
  505. }
  506. }
  507. }
  508. }
  509. if (currentNode.data.type === BlockEnum.Loop) {
  510. const loopChildren = nodes.filter(node => node.parentId === currentNode.id)
  511. if (loopChildren.length) {
  512. if (currentNode.data._isBundled) {
  513. loopChildren.forEach((child) => {
  514. handleNodeDelete(child.id)
  515. })
  516. return handleNodeDelete(nodeId)
  517. }
  518. else {
  519. if (loopChildren.length === 1) {
  520. handleNodeDelete(loopChildren[0].id)
  521. handleNodeDelete(nodeId)
  522. return
  523. }
  524. const { setShowConfirm, showConfirm } = workflowStore.getState()
  525. if (!showConfirm) {
  526. setShowConfirm({
  527. title: t('workflow.nodes.loop.deleteTitle'),
  528. desc: t('workflow.nodes.loop.deleteDesc') || '',
  529. onConfirm: () => {
  530. loopChildren.forEach((child) => {
  531. handleNodeDelete(child.id)
  532. })
  533. handleNodeDelete(nodeId)
  534. handleSyncWorkflowDraft()
  535. setShowConfirm(undefined)
  536. },
  537. })
  538. return
  539. }
  540. }
  541. }
  542. }
  543. const connectedEdges = getConnectedEdges([{ id: nodeId } as Node], edges)
  544. const nodesConnectedSourceOrTargetHandleIdsMap = getNodesConnectedSourceOrTargetHandleIdsMap(connectedEdges.map(edge => ({ type: 'remove', edge })), nodes)
  545. const newNodes = produce(nodes, (draft: Node[]) => {
  546. draft.forEach((node) => {
  547. if (nodesConnectedSourceOrTargetHandleIdsMap[node.id]) {
  548. node.data = {
  549. ...node.data,
  550. ...nodesConnectedSourceOrTargetHandleIdsMap[node.id],
  551. }
  552. }
  553. if (node.id === currentNode.parentId)
  554. node.data._children = node.data._children?.filter(child => child.nodeId !== nodeId)
  555. })
  556. draft.splice(currentNodeIndex, 1)
  557. })
  558. setNodes(newNodes)
  559. const newEdges = produce(edges, (draft) => {
  560. return draft.filter(edge => !connectedEdges.find(connectedEdge => connectedEdge.id === edge.id))
  561. })
  562. setEdges(newEdges)
  563. handleSyncWorkflowDraft()
  564. if (currentNode.type === CUSTOM_NOTE_NODE)
  565. saveStateToHistory(WorkflowHistoryEvent.NoteDelete)
  566. else
  567. saveStateToHistory(WorkflowHistoryEvent.NodeDelete)
  568. }, [getNodesReadOnly, store, deleteNodeInspectorVars, handleSyncWorkflowDraft, saveStateToHistory, workflowStore, t])
  569. const handleNodeAdd = useCallback<OnNodeAdd>((
  570. {
  571. nodeType,
  572. sourceHandle = 'source',
  573. targetHandle = 'target',
  574. toolDefaultValue,
  575. },
  576. {
  577. prevNodeId,
  578. prevNodeSourceHandle,
  579. nextNodeId,
  580. nextNodeTargetHandle,
  581. },
  582. ) => {
  583. if (getNodesReadOnly())
  584. return
  585. const {
  586. getNodes,
  587. setNodes,
  588. edges,
  589. setEdges,
  590. } = store.getState()
  591. const nodes = getNodes()
  592. const nodesWithSameType = nodes.filter(node => node.data.type === nodeType)
  593. const {
  594. newNode,
  595. newIterationStartNode,
  596. newLoopStartNode,
  597. } = generateNewNode({
  598. type: getNodeCustomTypeByNodeDataType(nodeType),
  599. data: {
  600. ...NODES_INITIAL_DATA[nodeType],
  601. title: nodesWithSameType.length > 0 ? `${t(`workflow.blocks.${nodeType}`)} ${nodesWithSameType.length + 1}` : t(`workflow.blocks.${nodeType}`),
  602. ...(toolDefaultValue || {}),
  603. selected: true,
  604. _showAddVariablePopup: (nodeType === BlockEnum.VariableAssigner || nodeType === BlockEnum.VariableAggregator) && !!prevNodeId,
  605. _holdAddVariablePopup: false,
  606. },
  607. position: {
  608. x: 0,
  609. y: 0,
  610. },
  611. })
  612. if (prevNodeId && !nextNodeId) {
  613. const prevNodeIndex = nodes.findIndex(node => node.id === prevNodeId)
  614. const prevNode = nodes[prevNodeIndex]
  615. const outgoers = getOutgoers(prevNode, nodes, edges).sort((a, b) => a.position.y - b.position.y)
  616. const lastOutgoer = outgoers[outgoers.length - 1]
  617. newNode.data._connectedTargetHandleIds = [targetHandle]
  618. newNode.data._connectedSourceHandleIds = []
  619. newNode.position = {
  620. x: lastOutgoer ? lastOutgoer.position.x : prevNode.position.x + prevNode.width! + X_OFFSET,
  621. y: lastOutgoer ? lastOutgoer.position.y + lastOutgoer.height! + Y_OFFSET : prevNode.position.y,
  622. }
  623. newNode.parentId = prevNode.parentId
  624. newNode.extent = prevNode.extent
  625. const parentNode = nodes.find(node => node.id === prevNode.parentId) || null
  626. const isInIteration = !!parentNode && parentNode.data.type === BlockEnum.Iteration
  627. const isInLoop = !!parentNode && parentNode.data.type === BlockEnum.Loop
  628. if (prevNode.parentId) {
  629. newNode.data.isInIteration = isInIteration
  630. newNode.data.isInLoop = isInLoop
  631. if (isInIteration) {
  632. newNode.data.iteration_id = parentNode.id
  633. newNode.zIndex = ITERATION_CHILDREN_Z_INDEX
  634. }
  635. if (isInLoop) {
  636. newNode.data.loop_id = parentNode.id
  637. newNode.zIndex = LOOP_CHILDREN_Z_INDEX
  638. }
  639. if (isInIteration && (newNode.data.type === BlockEnum.Answer || newNode.data.type === BlockEnum.Tool || newNode.data.type === BlockEnum.Assigner)) {
  640. const iterNodeData: IterationNodeType = parentNode.data
  641. iterNodeData._isShowTips = true
  642. }
  643. if (isInLoop && (newNode.data.type === BlockEnum.Answer || newNode.data.type === BlockEnum.Tool || newNode.data.type === BlockEnum.Assigner)) {
  644. const iterNodeData: IterationNodeType = parentNode.data
  645. iterNodeData._isShowTips = true
  646. }
  647. }
  648. const newEdge: Edge = {
  649. id: `${prevNodeId}-${prevNodeSourceHandle}-${newNode.id}-${targetHandle}`,
  650. type: CUSTOM_EDGE,
  651. source: prevNodeId,
  652. sourceHandle: prevNodeSourceHandle,
  653. target: newNode.id,
  654. targetHandle,
  655. data: {
  656. sourceType: prevNode.data.type,
  657. targetType: newNode.data.type,
  658. isInIteration,
  659. isInLoop,
  660. iteration_id: isInIteration ? prevNode.parentId : undefined,
  661. loop_id: isInLoop ? prevNode.parentId : undefined,
  662. _connectedNodeIsSelected: true,
  663. },
  664. zIndex: prevNode.parentId ? (isInIteration ? ITERATION_CHILDREN_Z_INDEX : LOOP_CHILDREN_Z_INDEX) : 0,
  665. }
  666. const nodesConnectedSourceOrTargetHandleIdsMap = getNodesConnectedSourceOrTargetHandleIdsMap(
  667. [
  668. { type: 'add', edge: newEdge },
  669. ],
  670. nodes,
  671. )
  672. const newNodes = produce(nodes, (draft: Node[]) => {
  673. draft.forEach((node) => {
  674. node.data.selected = false
  675. if (nodesConnectedSourceOrTargetHandleIdsMap[node.id]) {
  676. node.data = {
  677. ...node.data,
  678. ...nodesConnectedSourceOrTargetHandleIdsMap[node.id],
  679. }
  680. }
  681. if (node.data.type === BlockEnum.Iteration && prevNode.parentId === node.id)
  682. node.data._children?.push({ nodeId: newNode.id, nodeType: newNode.data.type })
  683. if (node.data.type === BlockEnum.Loop && prevNode.parentId === node.id)
  684. node.data._children?.push({ nodeId: newNode.id, nodeType: newNode.data.type })
  685. })
  686. draft.push(newNode)
  687. if (newIterationStartNode)
  688. draft.push(newIterationStartNode)
  689. if (newLoopStartNode)
  690. draft.push(newLoopStartNode)
  691. })
  692. if (newNode.data.type === BlockEnum.VariableAssigner || newNode.data.type === BlockEnum.VariableAggregator) {
  693. const { setShowAssignVariablePopup } = workflowStore.getState()
  694. setShowAssignVariablePopup({
  695. nodeId: prevNode.id,
  696. nodeData: prevNode.data,
  697. variableAssignerNodeId: newNode.id,
  698. variableAssignerNodeData: (newNode.data as VariableAssignerNodeType),
  699. variableAssignerNodeHandleId: targetHandle,
  700. parentNode: nodes.find(node => node.id === newNode.parentId),
  701. x: -25,
  702. y: 44,
  703. })
  704. }
  705. const newEdges = produce(edges, (draft) => {
  706. draft.forEach((item) => {
  707. item.data = {
  708. ...item.data,
  709. _connectedNodeIsSelected: false,
  710. }
  711. })
  712. draft.push(newEdge)
  713. })
  714. if (checkNestedParallelLimit(newNodes, newEdges, prevNode.parentId)) {
  715. setNodes(newNodes)
  716. setEdges(newEdges)
  717. }
  718. else {
  719. return false
  720. }
  721. }
  722. if (!prevNodeId && nextNodeId) {
  723. const nextNodeIndex = nodes.findIndex(node => node.id === nextNodeId)
  724. const nextNode = nodes[nextNodeIndex]!
  725. if ((nodeType !== BlockEnum.IfElse) && (nodeType !== BlockEnum.QuestionClassifier))
  726. newNode.data._connectedSourceHandleIds = [sourceHandle]
  727. newNode.data._connectedTargetHandleIds = []
  728. newNode.position = {
  729. x: nextNode.position.x,
  730. y: nextNode.position.y,
  731. }
  732. newNode.parentId = nextNode.parentId
  733. newNode.extent = nextNode.extent
  734. const parentNode = nodes.find(node => node.id === nextNode.parentId) || null
  735. const isInIteration = !!parentNode && parentNode.data.type === BlockEnum.Iteration
  736. const isInLoop = !!parentNode && parentNode.data.type === BlockEnum.Loop
  737. if (parentNode && nextNode.parentId) {
  738. newNode.data.isInIteration = isInIteration
  739. newNode.data.isInLoop = isInLoop
  740. if (isInIteration) {
  741. newNode.data.iteration_id = parentNode.id
  742. newNode.zIndex = ITERATION_CHILDREN_Z_INDEX
  743. }
  744. if (isInLoop) {
  745. newNode.data.loop_id = parentNode.id
  746. newNode.zIndex = LOOP_CHILDREN_Z_INDEX
  747. }
  748. }
  749. let newEdge
  750. if ((nodeType !== BlockEnum.IfElse) && (nodeType !== BlockEnum.QuestionClassifier) && (nodeType !== BlockEnum.LoopEnd)) {
  751. newEdge = {
  752. id: `${newNode.id}-${sourceHandle}-${nextNodeId}-${nextNodeTargetHandle}`,
  753. type: CUSTOM_EDGE,
  754. source: newNode.id,
  755. sourceHandle,
  756. target: nextNodeId,
  757. targetHandle: nextNodeTargetHandle,
  758. data: {
  759. sourceType: newNode.data.type,
  760. targetType: nextNode.data.type,
  761. isInIteration,
  762. isInLoop,
  763. iteration_id: isInIteration ? nextNode.parentId : undefined,
  764. loop_id: isInLoop ? nextNode.parentId : undefined,
  765. _connectedNodeIsSelected: true,
  766. },
  767. zIndex: nextNode.parentId ? (isInIteration ? ITERATION_CHILDREN_Z_INDEX : LOOP_CHILDREN_Z_INDEX) : 0,
  768. }
  769. }
  770. let nodesConnectedSourceOrTargetHandleIdsMap: Record<string, any>
  771. if (newEdge) {
  772. nodesConnectedSourceOrTargetHandleIdsMap = getNodesConnectedSourceOrTargetHandleIdsMap(
  773. [
  774. { type: 'add', edge: newEdge },
  775. ],
  776. nodes,
  777. )
  778. }
  779. const afterNodesInSameBranch = getAfterNodesInSameBranch(nextNodeId!)
  780. const afterNodesInSameBranchIds = afterNodesInSameBranch.map(node => node.id)
  781. const newNodes = produce(nodes, (draft) => {
  782. draft.forEach((node) => {
  783. node.data.selected = false
  784. if (afterNodesInSameBranchIds.includes(node.id))
  785. node.position.x += NODE_WIDTH_X_OFFSET
  786. if (nodesConnectedSourceOrTargetHandleIdsMap?.[node.id]) {
  787. node.data = {
  788. ...node.data,
  789. ...nodesConnectedSourceOrTargetHandleIdsMap[node.id],
  790. }
  791. }
  792. if (node.data.type === BlockEnum.Iteration && nextNode.parentId === node.id)
  793. node.data._children?.push({ nodeId: newNode.id, nodeType: newNode.data.type })
  794. if (node.data.type === BlockEnum.Iteration && node.data.start_node_id === nextNodeId) {
  795. node.data.start_node_id = newNode.id
  796. node.data.startNodeType = newNode.data.type
  797. }
  798. if (node.data.type === BlockEnum.Loop && nextNode.parentId === node.id)
  799. node.data._children?.push({ nodeId: newNode.id, nodeType: newNode.data.type })
  800. if (node.data.type === BlockEnum.Loop && node.data.start_node_id === nextNodeId) {
  801. node.data.start_node_id = newNode.id
  802. node.data.startNodeType = newNode.data.type
  803. }
  804. })
  805. draft.push(newNode)
  806. if (newIterationStartNode)
  807. draft.push(newIterationStartNode)
  808. if (newLoopStartNode)
  809. draft.push(newLoopStartNode)
  810. })
  811. if (newEdge) {
  812. const newEdges = produce(edges, (draft) => {
  813. draft.forEach((item) => {
  814. item.data = {
  815. ...item.data,
  816. _connectedNodeIsSelected: false,
  817. }
  818. })
  819. draft.push(newEdge)
  820. })
  821. if (checkNestedParallelLimit(newNodes, newEdges, nextNode.parentId)) {
  822. setNodes(newNodes)
  823. setEdges(newEdges)
  824. }
  825. else {
  826. return false
  827. }
  828. }
  829. else {
  830. if (checkNestedParallelLimit(newNodes, edges))
  831. setNodes(newNodes)
  832. else
  833. return false
  834. }
  835. }
  836. if (prevNodeId && nextNodeId) {
  837. const prevNode = nodes.find(node => node.id === prevNodeId)!
  838. const nextNode = nodes.find(node => node.id === nextNodeId)!
  839. newNode.data._connectedTargetHandleIds = [targetHandle]
  840. newNode.data._connectedSourceHandleIds = [sourceHandle]
  841. newNode.position = {
  842. x: nextNode.position.x,
  843. y: nextNode.position.y,
  844. }
  845. newNode.parentId = prevNode.parentId
  846. newNode.extent = prevNode.extent
  847. const parentNode = nodes.find(node => node.id === prevNode.parentId) || null
  848. const isInIteration = !!parentNode && parentNode.data.type === BlockEnum.Iteration
  849. const isInLoop = !!parentNode && parentNode.data.type === BlockEnum.Loop
  850. if (parentNode && prevNode.parentId) {
  851. newNode.data.isInIteration = isInIteration
  852. newNode.data.isInLoop = isInLoop
  853. if (isInIteration) {
  854. newNode.data.iteration_id = parentNode.id
  855. newNode.zIndex = ITERATION_CHILDREN_Z_INDEX
  856. }
  857. if (isInLoop) {
  858. newNode.data.loop_id = parentNode.id
  859. newNode.zIndex = LOOP_CHILDREN_Z_INDEX
  860. }
  861. }
  862. const currentEdgeIndex = edges.findIndex(edge => edge.source === prevNodeId && edge.target === nextNodeId)
  863. const newPrevEdge = {
  864. id: `${prevNodeId}-${prevNodeSourceHandle}-${newNode.id}-${targetHandle}`,
  865. type: CUSTOM_EDGE,
  866. source: prevNodeId,
  867. sourceHandle: prevNodeSourceHandle,
  868. target: newNode.id,
  869. targetHandle,
  870. data: {
  871. sourceType: prevNode.data.type,
  872. targetType: newNode.data.type,
  873. isInIteration,
  874. isInLoop,
  875. iteration_id: isInIteration ? prevNode.parentId : undefined,
  876. loop_id: isInLoop ? prevNode.parentId : undefined,
  877. _connectedNodeIsSelected: true,
  878. },
  879. zIndex: prevNode.parentId ? (isInIteration ? ITERATION_CHILDREN_Z_INDEX : LOOP_CHILDREN_Z_INDEX) : 0,
  880. }
  881. let newNextEdge: Edge | null = null
  882. const nextNodeParentNode = nodes.find(node => node.id === nextNode.parentId) || null
  883. const isNextNodeInIteration = !!nextNodeParentNode && nextNodeParentNode.data.type === BlockEnum.Iteration
  884. const isNextNodeInLoop = !!nextNodeParentNode && nextNodeParentNode.data.type === BlockEnum.Loop
  885. if (nodeType !== BlockEnum.IfElse && nodeType !== BlockEnum.QuestionClassifier && nodeType !== BlockEnum.LoopEnd) {
  886. newNextEdge = {
  887. id: `${newNode.id}-${sourceHandle}-${nextNodeId}-${nextNodeTargetHandle}`,
  888. type: CUSTOM_EDGE,
  889. source: newNode.id,
  890. sourceHandle,
  891. target: nextNodeId,
  892. targetHandle: nextNodeTargetHandle,
  893. data: {
  894. sourceType: newNode.data.type,
  895. targetType: nextNode.data.type,
  896. isInIteration: isNextNodeInIteration,
  897. isInLoop: isNextNodeInLoop,
  898. iteration_id: isNextNodeInIteration ? nextNode.parentId : undefined,
  899. loop_id: isNextNodeInLoop ? nextNode.parentId : undefined,
  900. _connectedNodeIsSelected: true,
  901. },
  902. zIndex: nextNode.parentId ? (isNextNodeInIteration ? ITERATION_CHILDREN_Z_INDEX : LOOP_CHILDREN_Z_INDEX) : 0,
  903. }
  904. }
  905. const nodesConnectedSourceOrTargetHandleIdsMap = getNodesConnectedSourceOrTargetHandleIdsMap(
  906. [
  907. { type: 'remove', edge: edges[currentEdgeIndex] },
  908. { type: 'add', edge: newPrevEdge },
  909. ...(newNextEdge ? [{ type: 'add', edge: newNextEdge }] : []),
  910. ],
  911. [...nodes, newNode],
  912. )
  913. const afterNodesInSameBranch = getAfterNodesInSameBranch(nextNodeId!)
  914. const afterNodesInSameBranchIds = afterNodesInSameBranch.map(node => node.id)
  915. const newNodes = produce(nodes, (draft) => {
  916. draft.forEach((node) => {
  917. node.data.selected = false
  918. if (nodesConnectedSourceOrTargetHandleIdsMap[node.id]) {
  919. node.data = {
  920. ...node.data,
  921. ...nodesConnectedSourceOrTargetHandleIdsMap[node.id],
  922. }
  923. }
  924. if (afterNodesInSameBranchIds.includes(node.id))
  925. node.position.x += NODE_WIDTH_X_OFFSET
  926. if (node.data.type === BlockEnum.Iteration && prevNode.parentId === node.id)
  927. node.data._children?.push({ nodeId: newNode.id, nodeType: newNode.data.type })
  928. if (node.data.type === BlockEnum.Loop && prevNode.parentId === node.id)
  929. node.data._children?.push({ nodeId: newNode.id, nodeType: newNode.data.type })
  930. })
  931. draft.push(newNode)
  932. if (newIterationStartNode)
  933. draft.push(newIterationStartNode)
  934. if (newLoopStartNode)
  935. draft.push(newLoopStartNode)
  936. })
  937. setNodes(newNodes)
  938. if (newNode.data.type === BlockEnum.VariableAssigner || newNode.data.type === BlockEnum.VariableAggregator) {
  939. const { setShowAssignVariablePopup } = workflowStore.getState()
  940. setShowAssignVariablePopup({
  941. nodeId: prevNode.id,
  942. nodeData: prevNode.data,
  943. variableAssignerNodeId: newNode.id,
  944. variableAssignerNodeData: newNode.data as VariableAssignerNodeType,
  945. variableAssignerNodeHandleId: targetHandle,
  946. parentNode: nodes.find(node => node.id === newNode.parentId),
  947. x: -25,
  948. y: 44,
  949. })
  950. }
  951. const newEdges = produce(edges, (draft) => {
  952. draft.splice(currentEdgeIndex, 1)
  953. draft.forEach((item) => {
  954. item.data = {
  955. ...item.data,
  956. _connectedNodeIsSelected: false,
  957. }
  958. })
  959. draft.push(newPrevEdge)
  960. if (newNextEdge)
  961. draft.push(newNextEdge)
  962. })
  963. setEdges(newEdges)
  964. }
  965. handleSyncWorkflowDraft()
  966. saveStateToHistory(WorkflowHistoryEvent.NodeAdd)
  967. }, [getNodesReadOnly, store, t, handleSyncWorkflowDraft, saveStateToHistory, workflowStore, getAfterNodesInSameBranch, checkNestedParallelLimit])
  968. const handleNodeChange = useCallback((
  969. currentNodeId: string,
  970. nodeType: BlockEnum,
  971. sourceHandle: string,
  972. toolDefaultValue?: ToolDefaultValue,
  973. ) => {
  974. if (getNodesReadOnly())
  975. return
  976. const {
  977. getNodes,
  978. setNodes,
  979. edges,
  980. setEdges,
  981. } = store.getState()
  982. const nodes = getNodes()
  983. const currentNode = nodes.find(node => node.id === currentNodeId)!
  984. const connectedEdges = getConnectedEdges([currentNode], edges)
  985. const nodesWithSameType = nodes.filter(node => node.data.type === nodeType)
  986. const {
  987. newNode: newCurrentNode,
  988. newIterationStartNode,
  989. newLoopStartNode,
  990. } = generateNewNode({
  991. type: getNodeCustomTypeByNodeDataType(nodeType),
  992. data: {
  993. ...NODES_INITIAL_DATA[nodeType],
  994. title: nodesWithSameType.length > 0 ? `${t(`workflow.blocks.${nodeType}`)} ${nodesWithSameType.length + 1}` : t(`workflow.blocks.${nodeType}`),
  995. ...(toolDefaultValue || {}),
  996. _connectedSourceHandleIds: [],
  997. _connectedTargetHandleIds: [],
  998. selected: currentNode.data.selected,
  999. isInIteration: currentNode.data.isInIteration,
  1000. isInLoop: currentNode.data.isInLoop,
  1001. iteration_id: currentNode.data.iteration_id,
  1002. loop_id: currentNode.data.loop_id,
  1003. },
  1004. position: {
  1005. x: currentNode.position.x,
  1006. y: currentNode.position.y,
  1007. },
  1008. parentId: currentNode.parentId,
  1009. extent: currentNode.extent,
  1010. zIndex: currentNode.zIndex,
  1011. })
  1012. const nodesConnectedSourceOrTargetHandleIdsMap = getNodesConnectedSourceOrTargetHandleIdsMap(
  1013. [
  1014. ...connectedEdges.map(edge => ({ type: 'remove', edge })),
  1015. ],
  1016. nodes,
  1017. )
  1018. const newNodes = produce(nodes, (draft) => {
  1019. draft.forEach((node) => {
  1020. node.data.selected = false
  1021. if (nodesConnectedSourceOrTargetHandleIdsMap[node.id]) {
  1022. node.data = {
  1023. ...node.data,
  1024. ...nodesConnectedSourceOrTargetHandleIdsMap[node.id],
  1025. }
  1026. }
  1027. })
  1028. const index = draft.findIndex(node => node.id === currentNodeId)
  1029. draft.splice(index, 1, newCurrentNode)
  1030. if (newIterationStartNode)
  1031. draft.push(newIterationStartNode)
  1032. if (newLoopStartNode)
  1033. draft.push(newLoopStartNode)
  1034. })
  1035. setNodes(newNodes)
  1036. const newEdges = produce(edges, (draft) => {
  1037. const filtered = draft.filter(edge => !connectedEdges.find(connectedEdge => connectedEdge.id === edge.id))
  1038. return filtered
  1039. })
  1040. setEdges(newEdges)
  1041. handleSyncWorkflowDraft()
  1042. saveStateToHistory(WorkflowHistoryEvent.NodeChange)
  1043. }, [getNodesReadOnly, store, t, handleSyncWorkflowDraft, saveStateToHistory])
  1044. const handleNodesCancelSelected = useCallback(() => {
  1045. const {
  1046. getNodes,
  1047. setNodes,
  1048. } = store.getState()
  1049. const nodes = getNodes()
  1050. const newNodes = produce(nodes, (draft) => {
  1051. draft.forEach((node) => {
  1052. node.data.selected = false
  1053. })
  1054. })
  1055. setNodes(newNodes)
  1056. }, [store])
  1057. const handleNodeContextMenu = useCallback((e: MouseEvent, node: Node) => {
  1058. if (node.type === CUSTOM_NOTE_NODE || node.type === CUSTOM_ITERATION_START_NODE)
  1059. return
  1060. if (node.type === CUSTOM_NOTE_NODE || node.type === CUSTOM_LOOP_START_NODE)
  1061. return
  1062. e.preventDefault()
  1063. const container = document.querySelector('#workflow-container')
  1064. const { x, y } = container!.getBoundingClientRect()
  1065. workflowStore.setState({
  1066. nodeMenu: {
  1067. top: e.clientY - y,
  1068. left: e.clientX - x,
  1069. nodeId: node.id,
  1070. },
  1071. })
  1072. handleNodeSelect(node.id)
  1073. }, [workflowStore, handleNodeSelect])
  1074. const handleNodesCopy = useCallback((nodeId?: string) => {
  1075. if (getNodesReadOnly())
  1076. return
  1077. const { setClipboardElements } = workflowStore.getState()
  1078. const {
  1079. getNodes,
  1080. } = store.getState()
  1081. const nodes = getNodes()
  1082. if (nodeId) {
  1083. // If nodeId is provided, copy that specific node
  1084. const nodeToCopy = nodes.find(node => node.id === nodeId && node.data.type !== BlockEnum.Start
  1085. && node.type !== CUSTOM_ITERATION_START_NODE && node.type !== CUSTOM_LOOP_START_NODE && node.data.type !== BlockEnum.LoopEnd)
  1086. if (nodeToCopy)
  1087. setClipboardElements([nodeToCopy])
  1088. }
  1089. else {
  1090. // If no nodeId is provided, fall back to the current behavior
  1091. const bundledNodes = nodes.filter(node => node.data._isBundled && node.data.type !== BlockEnum.Start
  1092. && !node.data.isInIteration && !node.data.isInLoop)
  1093. if (bundledNodes.length) {
  1094. setClipboardElements(bundledNodes)
  1095. return
  1096. }
  1097. const selectedNode = nodes.find(node => node.data.selected && node.data.type !== BlockEnum.Start && node.data.type !== BlockEnum.LoopEnd)
  1098. if (selectedNode)
  1099. setClipboardElements([selectedNode])
  1100. }
  1101. }, [getNodesReadOnly, store, workflowStore])
  1102. const handleNodesPaste = useCallback(() => {
  1103. if (getNodesReadOnly())
  1104. return
  1105. const {
  1106. clipboardElements,
  1107. mousePosition,
  1108. } = workflowStore.getState()
  1109. const {
  1110. getNodes,
  1111. setNodes,
  1112. edges,
  1113. setEdges,
  1114. } = store.getState()
  1115. const nodesToPaste: Node[] = []
  1116. const edgesToPaste: Edge[] = []
  1117. const nodes = getNodes()
  1118. if (clipboardElements.length) {
  1119. const { x, y } = getTopLeftNodePosition(clipboardElements)
  1120. const { screenToFlowPosition } = reactflow
  1121. const currentPosition = screenToFlowPosition({ x: mousePosition.pageX, y: mousePosition.pageY })
  1122. const offsetX = currentPosition.x - x
  1123. const offsetY = currentPosition.y - y
  1124. let idMapping: Record<string, string> = {}
  1125. clipboardElements.forEach((nodeToPaste, index) => {
  1126. const nodeType = nodeToPaste.data.type
  1127. const {
  1128. newNode,
  1129. newIterationStartNode,
  1130. newLoopStartNode,
  1131. } = generateNewNode({
  1132. type: nodeToPaste.type,
  1133. data: {
  1134. ...NODES_INITIAL_DATA[nodeType],
  1135. ...nodeToPaste.data,
  1136. selected: false,
  1137. _isBundled: false,
  1138. _connectedSourceHandleIds: [],
  1139. _connectedTargetHandleIds: [],
  1140. title: genNewNodeTitleFromOld(nodeToPaste.data.title),
  1141. },
  1142. position: {
  1143. x: nodeToPaste.position.x + offsetX,
  1144. y: nodeToPaste.position.y + offsetY,
  1145. },
  1146. extent: nodeToPaste.extent,
  1147. zIndex: nodeToPaste.zIndex,
  1148. })
  1149. newNode.id = newNode.id + index
  1150. // This new node is movable and can be placed anywhere
  1151. let newChildren: Node[] = []
  1152. if (nodeToPaste.data.type === BlockEnum.Iteration) {
  1153. newIterationStartNode!.parentId = newNode.id;
  1154. (newNode.data as IterationNodeType).start_node_id = newIterationStartNode!.id
  1155. const oldIterationStartNode = nodes
  1156. .find(n => n.parentId === nodeToPaste.id && n.type === CUSTOM_ITERATION_START_NODE)
  1157. idMapping[oldIterationStartNode!.id] = newIterationStartNode!.id
  1158. const { copyChildren, newIdMapping } = handleNodeIterationChildrenCopy(nodeToPaste.id, newNode.id, idMapping)
  1159. newChildren = copyChildren
  1160. idMapping = newIdMapping
  1161. newChildren.forEach((child) => {
  1162. newNode.data._children?.push({ nodeId: child.id, nodeType: child.data.type })
  1163. })
  1164. newChildren.push(newIterationStartNode!)
  1165. }
  1166. if (nodeToPaste.data.type === BlockEnum.Loop) {
  1167. newLoopStartNode!.parentId = newNode.id;
  1168. (newNode.data as LoopNodeType).start_node_id = newLoopStartNode!.id
  1169. newChildren = handleNodeLoopChildrenCopy(nodeToPaste.id, newNode.id)
  1170. newChildren.forEach((child) => {
  1171. newNode.data._children?.push({ nodeId: child.id, nodeType: child.data.type })
  1172. })
  1173. newChildren.push(newLoopStartNode!)
  1174. }
  1175. nodesToPaste.push(newNode)
  1176. if (newChildren.length)
  1177. nodesToPaste.push(...newChildren)
  1178. })
  1179. edges.forEach((edge) => {
  1180. const sourceId = idMapping[edge.source]
  1181. const targetId = idMapping[edge.target]
  1182. if (sourceId && targetId) {
  1183. const newEdge: Edge = {
  1184. ...edge,
  1185. id: `${sourceId}-${edge.sourceHandle}-${targetId}-${edge.targetHandle}`,
  1186. source: sourceId,
  1187. target: targetId,
  1188. data: {
  1189. ...edge.data,
  1190. _connectedNodeIsSelected: false,
  1191. },
  1192. }
  1193. edgesToPaste.push(newEdge)
  1194. }
  1195. })
  1196. setNodes([...nodes, ...nodesToPaste])
  1197. setEdges([...edges, ...edgesToPaste])
  1198. saveStateToHistory(WorkflowHistoryEvent.NodePaste)
  1199. handleSyncWorkflowDraft()
  1200. }
  1201. }, [getNodesReadOnly, workflowStore, store, reactflow, saveStateToHistory, handleSyncWorkflowDraft, handleNodeIterationChildrenCopy, handleNodeLoopChildrenCopy])
  1202. const handleNodesDuplicate = useCallback((nodeId?: string) => {
  1203. if (getNodesReadOnly())
  1204. return
  1205. handleNodesCopy(nodeId)
  1206. handleNodesPaste()
  1207. }, [getNodesReadOnly, handleNodesCopy, handleNodesPaste])
  1208. const handleNodesDelete = useCallback(() => {
  1209. if (getNodesReadOnly())
  1210. return
  1211. const {
  1212. getNodes,
  1213. edges,
  1214. } = store.getState()
  1215. const nodes = getNodes()
  1216. const bundledNodes = nodes.filter(node => node.data._isBundled && node.data.type !== BlockEnum.Start)
  1217. if (bundledNodes.length) {
  1218. bundledNodes.forEach(node => handleNodeDelete(node.id))
  1219. return
  1220. }
  1221. const edgeSelected = edges.some(edge => edge.selected)
  1222. if (edgeSelected)
  1223. return
  1224. const selectedNode = nodes.find(node => node.data.selected && node.data.type !== BlockEnum.Start)
  1225. if (selectedNode)
  1226. handleNodeDelete(selectedNode.id)
  1227. }, [store, getNodesReadOnly, handleNodeDelete])
  1228. const handleNodeResize = useCallback((nodeId: string, params: ResizeParamsWithDirection) => {
  1229. if (getNodesReadOnly())
  1230. return
  1231. const {
  1232. getNodes,
  1233. setNodes,
  1234. } = store.getState()
  1235. const { x, y, width, height } = params
  1236. const nodes = getNodes()
  1237. const currentNode = nodes.find(n => n.id === nodeId)!
  1238. const childrenNodes = nodes.filter(n => currentNode.data._children?.find((c: any) => c.nodeId === n.id))
  1239. let rightNode: Node
  1240. let bottomNode: Node
  1241. childrenNodes.forEach((n) => {
  1242. if (rightNode) {
  1243. if (n.position.x + n.width! > rightNode.position.x + rightNode.width!)
  1244. rightNode = n
  1245. }
  1246. else {
  1247. rightNode = n
  1248. }
  1249. if (bottomNode) {
  1250. if (n.position.y + n.height! > bottomNode.position.y + bottomNode.height!)
  1251. bottomNode = n
  1252. }
  1253. else {
  1254. bottomNode = n
  1255. }
  1256. })
  1257. if (rightNode! && bottomNode!) {
  1258. const parentNode = nodes.find(n => n.id === rightNode.parentId)
  1259. const paddingMap = parentNode?.data.type === BlockEnum.Iteration ? ITERATION_PADDING : LOOP_PADDING
  1260. if (width < rightNode!.position.x + rightNode.width! + paddingMap.right)
  1261. return
  1262. if (height < bottomNode.position.y + bottomNode.height! + paddingMap.bottom)
  1263. return
  1264. }
  1265. const newNodes = produce(nodes, (draft) => {
  1266. draft.forEach((n) => {
  1267. if (n.id === nodeId) {
  1268. n.data.width = width
  1269. n.data.height = height
  1270. n.width = width
  1271. n.height = height
  1272. n.position.x = x
  1273. n.position.y = y
  1274. }
  1275. })
  1276. })
  1277. setNodes(newNodes)
  1278. handleSyncWorkflowDraft()
  1279. saveStateToHistory(WorkflowHistoryEvent.NodeResize)
  1280. }, [getNodesReadOnly, store, handleSyncWorkflowDraft, saveStateToHistory])
  1281. const handleNodeDisconnect = useCallback((nodeId: string) => {
  1282. if (getNodesReadOnly())
  1283. return
  1284. const {
  1285. getNodes,
  1286. setNodes,
  1287. edges,
  1288. setEdges,
  1289. } = store.getState()
  1290. const nodes = getNodes()
  1291. const currentNode = nodes.find(node => node.id === nodeId)!
  1292. const connectedEdges = getConnectedEdges([currentNode], edges)
  1293. const nodesConnectedSourceOrTargetHandleIdsMap = getNodesConnectedSourceOrTargetHandleIdsMap(
  1294. connectedEdges.map(edge => ({ type: 'remove', edge })),
  1295. nodes,
  1296. )
  1297. const newNodes = produce(nodes, (draft: Node[]) => {
  1298. draft.forEach((node) => {
  1299. if (nodesConnectedSourceOrTargetHandleIdsMap[node.id]) {
  1300. node.data = {
  1301. ...node.data,
  1302. ...nodesConnectedSourceOrTargetHandleIdsMap[node.id],
  1303. }
  1304. }
  1305. })
  1306. })
  1307. setNodes(newNodes)
  1308. const newEdges = produce(edges, (draft) => {
  1309. return draft.filter(edge => !connectedEdges.find(connectedEdge => connectedEdge.id === edge.id))
  1310. })
  1311. setEdges(newEdges)
  1312. handleSyncWorkflowDraft()
  1313. saveStateToHistory(WorkflowHistoryEvent.EdgeDelete)
  1314. }, [store, getNodesReadOnly, handleSyncWorkflowDraft, saveStateToHistory])
  1315. const handleHistoryBack = useCallback(() => {
  1316. if (getNodesReadOnly() || getWorkflowReadOnly())
  1317. return
  1318. const { setEdges, setNodes } = store.getState()
  1319. undo()
  1320. const { edges, nodes } = workflowHistoryStore.getState()
  1321. if (edges.length === 0 && nodes.length === 0)
  1322. return
  1323. setEdges(edges)
  1324. setNodes(nodes)
  1325. }, [store, undo, workflowHistoryStore, getNodesReadOnly, getWorkflowReadOnly])
  1326. const handleHistoryForward = useCallback(() => {
  1327. if (getNodesReadOnly() || getWorkflowReadOnly())
  1328. return
  1329. const { setEdges, setNodes } = store.getState()
  1330. redo()
  1331. const { edges, nodes } = workflowHistoryStore.getState()
  1332. if (edges.length === 0 && nodes.length === 0)
  1333. return
  1334. setEdges(edges)
  1335. setNodes(nodes)
  1336. }, [redo, store, workflowHistoryStore, getNodesReadOnly, getWorkflowReadOnly])
  1337. const [isDimming, setIsDimming] = useState(false)
  1338. /** Add opacity-30 to all nodes except the nodeId */
  1339. const dimOtherNodes = useCallback(() => {
  1340. if (isDimming)
  1341. return
  1342. const { getNodes, setNodes, edges, setEdges } = store.getState()
  1343. const nodes = getNodes()
  1344. const selectedNode = nodes.find(n => n.data.selected)
  1345. if (!selectedNode)
  1346. return
  1347. setIsDimming(true)
  1348. // const workflowNodes = useStore(s => s.getNodes())
  1349. const workflowNodes = nodes
  1350. const usedVars = getNodeUsedVars(selectedNode)
  1351. const dependencyNodes: Node[] = []
  1352. usedVars.forEach((valueSelector) => {
  1353. const node = workflowNodes.find(node => node.id === valueSelector?.[0])
  1354. if (node) {
  1355. if (!dependencyNodes.includes(node))
  1356. dependencyNodes.push(node)
  1357. }
  1358. })
  1359. const outgoers = getOutgoers(selectedNode as Node, nodes as Node[], edges)
  1360. for (let currIdx = 0; currIdx < outgoers.length; currIdx++) {
  1361. const node = outgoers[currIdx]
  1362. const outgoersForNode = getOutgoers(node, nodes as Node[], edges)
  1363. outgoersForNode.forEach((item) => {
  1364. const existed = outgoers.some(v => v.id === item.id)
  1365. if (!existed)
  1366. outgoers.push(item)
  1367. })
  1368. }
  1369. const dependentNodes: Node[] = []
  1370. outgoers.forEach((node) => {
  1371. const usedVars = getNodeUsedVars(node)
  1372. const used = usedVars.some(v => v?.[0] === selectedNode.id)
  1373. if (used) {
  1374. const existed = dependentNodes.some(v => v.id === node.id)
  1375. if (!existed)
  1376. dependentNodes.push(node)
  1377. }
  1378. })
  1379. const dimNodes = [...dependencyNodes, ...dependentNodes, selectedNode]
  1380. const newNodes = produce(nodes, (draft) => {
  1381. draft.forEach((n) => {
  1382. const dimNode = dimNodes.find(v => v.id === n.id)
  1383. if (!dimNode)
  1384. n.data._dimmed = true
  1385. })
  1386. })
  1387. setNodes(newNodes)
  1388. const tempEdges: Edge[] = []
  1389. dependencyNodes.forEach((n) => {
  1390. tempEdges.push({
  1391. id: `tmp_${n.id}-source-${selectedNode.id}-target`,
  1392. type: CUSTOM_EDGE,
  1393. source: n.id,
  1394. sourceHandle: 'source_tmp',
  1395. target: selectedNode.id,
  1396. targetHandle: 'target_tmp',
  1397. animated: true,
  1398. data: {
  1399. sourceType: n.data.type,
  1400. targetType: selectedNode.data.type,
  1401. _isTemp: true,
  1402. _connectedNodeIsHovering: true,
  1403. },
  1404. })
  1405. })
  1406. dependentNodes.forEach((n) => {
  1407. tempEdges.push({
  1408. id: `tmp_${selectedNode.id}-source-${n.id}-target`,
  1409. type: CUSTOM_EDGE,
  1410. source: selectedNode.id,
  1411. sourceHandle: 'source_tmp',
  1412. target: n.id,
  1413. targetHandle: 'target_tmp',
  1414. animated: true,
  1415. data: {
  1416. sourceType: selectedNode.data.type,
  1417. targetType: n.data.type,
  1418. _isTemp: true,
  1419. _connectedNodeIsHovering: true,
  1420. },
  1421. })
  1422. })
  1423. const newEdges = produce(edges, (draft) => {
  1424. draft.forEach((e) => {
  1425. e.data._dimmed = true
  1426. })
  1427. draft.push(...tempEdges)
  1428. })
  1429. setEdges(newEdges)
  1430. }, [isDimming, store])
  1431. /** Restore all nodes to full opacity */
  1432. const undimAllNodes = useCallback(() => {
  1433. const { getNodes, setNodes, edges, setEdges } = store.getState()
  1434. const nodes = getNodes()
  1435. setIsDimming(false)
  1436. const newNodes = produce(nodes, (draft) => {
  1437. draft.forEach((n) => {
  1438. n.data._dimmed = false
  1439. })
  1440. })
  1441. setNodes(newNodes)
  1442. const newEdges = produce(edges.filter(e => !e.data._isTemp), (draft) => {
  1443. draft.forEach((e) => {
  1444. e.data._dimmed = false
  1445. })
  1446. })
  1447. setEdges(newEdges)
  1448. }, [store])
  1449. return {
  1450. handleNodeDragStart,
  1451. handleNodeDrag,
  1452. handleNodeDragStop,
  1453. handleNodeEnter,
  1454. handleNodeLeave,
  1455. handleNodeSelect,
  1456. handleNodeClick,
  1457. handleNodeConnect,
  1458. handleNodeConnectStart,
  1459. handleNodeConnectEnd,
  1460. handleNodeDelete,
  1461. handleNodeChange,
  1462. handleNodeAdd,
  1463. handleNodesCancelSelected,
  1464. handleNodeContextMenu,
  1465. handleNodesCopy,
  1466. handleNodesPaste,
  1467. handleNodesDuplicate,
  1468. handleNodesDelete,
  1469. handleNodeResize,
  1470. handleNodeDisconnect,
  1471. handleHistoryBack,
  1472. handleHistoryForward,
  1473. dimOtherNodes,
  1474. undimAllNodes,
  1475. }
  1476. }