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 56KB

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