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-workflow-interactions.ts 9.6KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332
  1. import {
  2. useCallback,
  3. } from 'react'
  4. import { useReactFlow, useStoreApi } from 'reactflow'
  5. import produce from 'immer'
  6. import { useStore, useWorkflowStore } from '../store'
  7. import {
  8. CUSTOM_NODE,
  9. NODE_LAYOUT_HORIZONTAL_PADDING,
  10. NODE_LAYOUT_VERTICAL_PADDING,
  11. WORKFLOW_DATA_UPDATE,
  12. } from '../constants'
  13. import type { Node, WorkflowDataUpdater } from '../types'
  14. import { BlockEnum, ControlMode } from '../types'
  15. import {
  16. getLayoutByDagre,
  17. getLayoutForChildNodes,
  18. initialEdges,
  19. initialNodes,
  20. } from '../utils'
  21. import {
  22. useNodesReadOnly,
  23. useSelectionInteractions,
  24. useWorkflowReadOnly,
  25. } from '../hooks'
  26. import { useEdgesInteractionsWithoutSync } from './use-edges-interactions-without-sync'
  27. import { useNodesInteractionsWithoutSync } from './use-nodes-interactions-without-sync'
  28. import { useNodesSyncDraft } from './use-nodes-sync-draft'
  29. import { WorkflowHistoryEvent, useWorkflowHistory } from './use-workflow-history'
  30. import { useEventEmitterContextContext } from '@/context/event-emitter'
  31. export const useWorkflowInteractions = () => {
  32. const workflowStore = useWorkflowStore()
  33. const { handleNodeCancelRunningStatus } = useNodesInteractionsWithoutSync()
  34. const { handleEdgeCancelRunningStatus } = useEdgesInteractionsWithoutSync()
  35. const handleCancelDebugAndPreviewPanel = useCallback(() => {
  36. workflowStore.setState({
  37. showDebugAndPreviewPanel: false,
  38. workflowRunningData: undefined,
  39. })
  40. handleNodeCancelRunningStatus()
  41. handleEdgeCancelRunningStatus()
  42. }, [workflowStore, handleNodeCancelRunningStatus, handleEdgeCancelRunningStatus])
  43. return {
  44. handleCancelDebugAndPreviewPanel,
  45. }
  46. }
  47. export const useWorkflowMoveMode = () => {
  48. const setControlMode = useStore(s => s.setControlMode)
  49. const {
  50. getNodesReadOnly,
  51. } = useNodesReadOnly()
  52. const { handleSelectionCancel } = useSelectionInteractions()
  53. const handleModePointer = useCallback(() => {
  54. if (getNodesReadOnly())
  55. return
  56. setControlMode(ControlMode.Pointer)
  57. }, [getNodesReadOnly, setControlMode])
  58. const handleModeHand = useCallback(() => {
  59. if (getNodesReadOnly())
  60. return
  61. setControlMode(ControlMode.Hand)
  62. handleSelectionCancel()
  63. }, [getNodesReadOnly, setControlMode, handleSelectionCancel])
  64. return {
  65. handleModePointer,
  66. handleModeHand,
  67. }
  68. }
  69. export const useWorkflowOrganize = () => {
  70. const workflowStore = useWorkflowStore()
  71. const store = useStoreApi()
  72. const reactflow = useReactFlow()
  73. const { getNodesReadOnly } = useNodesReadOnly()
  74. const { saveStateToHistory } = useWorkflowHistory()
  75. const { handleSyncWorkflowDraft } = useNodesSyncDraft()
  76. const handleLayout = useCallback(async () => {
  77. if (getNodesReadOnly())
  78. return
  79. workflowStore.setState({ nodeAnimation: true })
  80. const {
  81. getNodes,
  82. edges,
  83. setNodes,
  84. } = store.getState()
  85. const { setViewport } = reactflow
  86. const nodes = getNodes()
  87. const loopAndIterationNodes = nodes.filter(
  88. node => (node.data.type === BlockEnum.Loop || node.data.type === BlockEnum.Iteration)
  89. && !node.parentId
  90. && node.type === CUSTOM_NODE,
  91. )
  92. const childLayoutsMap: Record<string, any> = {}
  93. loopAndIterationNodes.forEach((node) => {
  94. childLayoutsMap[node.id] = getLayoutForChildNodes(node.id, nodes, edges)
  95. })
  96. const containerSizeChanges: Record<string, { width: number, height: number }> = {}
  97. loopAndIterationNodes.forEach((parentNode) => {
  98. const childLayout = childLayoutsMap[parentNode.id]
  99. if (!childLayout) return
  100. let minX = Infinity
  101. let minY = Infinity
  102. let maxX = -Infinity
  103. let maxY = -Infinity
  104. let hasChildren = false
  105. const childNodes = nodes.filter(node => node.parentId === parentNode.id)
  106. childNodes.forEach((node) => {
  107. if (childLayout.node(node.id)) {
  108. hasChildren = true
  109. const childNodeWithPosition = childLayout.node(node.id)
  110. const nodeX = childNodeWithPosition.x - node.width! / 2
  111. const nodeY = childNodeWithPosition.y - node.height! / 2
  112. minX = Math.min(minX, nodeX)
  113. minY = Math.min(minY, nodeY)
  114. maxX = Math.max(maxX, nodeX + node.width!)
  115. maxY = Math.max(maxY, nodeY + node.height!)
  116. }
  117. })
  118. if (hasChildren) {
  119. const requiredWidth = maxX - minX + NODE_LAYOUT_HORIZONTAL_PADDING * 2
  120. const requiredHeight = maxY - minY + NODE_LAYOUT_VERTICAL_PADDING * 2
  121. containerSizeChanges[parentNode.id] = {
  122. width: Math.max(parentNode.width || 0, requiredWidth),
  123. height: Math.max(parentNode.height || 0, requiredHeight),
  124. }
  125. }
  126. })
  127. const nodesWithUpdatedSizes = produce(nodes, (draft) => {
  128. draft.forEach((node) => {
  129. if ((node.data.type === BlockEnum.Loop || node.data.type === BlockEnum.Iteration)
  130. && containerSizeChanges[node.id]) {
  131. node.width = containerSizeChanges[node.id].width
  132. node.height = containerSizeChanges[node.id].height
  133. if (node.data.type === BlockEnum.Loop) {
  134. node.data.width = containerSizeChanges[node.id].width
  135. node.data.height = containerSizeChanges[node.id].height
  136. }
  137. else if (node.data.type === BlockEnum.Iteration) {
  138. node.data.width = containerSizeChanges[node.id].width
  139. node.data.height = containerSizeChanges[node.id].height
  140. }
  141. }
  142. })
  143. })
  144. const layout = getLayoutByDagre(nodesWithUpdatedSizes, edges)
  145. const rankMap = {} as Record<string, Node>
  146. nodesWithUpdatedSizes.forEach((node) => {
  147. if (!node.parentId && node.type === CUSTOM_NODE) {
  148. const rank = layout.node(node.id).rank!
  149. if (!rankMap[rank]) {
  150. rankMap[rank] = node
  151. }
  152. else {
  153. if (rankMap[rank].position.y > node.position.y)
  154. rankMap[rank] = node
  155. }
  156. }
  157. })
  158. const newNodes = produce(nodesWithUpdatedSizes, (draft) => {
  159. draft.forEach((node) => {
  160. if (!node.parentId && node.type === CUSTOM_NODE) {
  161. const nodeWithPosition = layout.node(node.id)
  162. node.position = {
  163. x: nodeWithPosition.x - node.width! / 2,
  164. y: nodeWithPosition.y - node.height! / 2 + rankMap[nodeWithPosition.rank!].height! / 2,
  165. }
  166. }
  167. })
  168. loopAndIterationNodes.forEach((parentNode) => {
  169. const childLayout = childLayoutsMap[parentNode.id]
  170. if (!childLayout) return
  171. const childNodes = draft.filter(node => node.parentId === parentNode.id)
  172. let minX = Infinity
  173. let minY = Infinity
  174. childNodes.forEach((node) => {
  175. if (childLayout.node(node.id)) {
  176. const childNodeWithPosition = childLayout.node(node.id)
  177. const nodeX = childNodeWithPosition.x - node.width! / 2
  178. const nodeY = childNodeWithPosition.y - node.height! / 2
  179. minX = Math.min(minX, nodeX)
  180. minY = Math.min(minY, nodeY)
  181. }
  182. })
  183. childNodes.forEach((node) => {
  184. if (childLayout.node(node.id)) {
  185. const childNodeWithPosition = childLayout.node(node.id)
  186. node.position = {
  187. x: NODE_LAYOUT_HORIZONTAL_PADDING + (childNodeWithPosition.x - node.width! / 2 - minX),
  188. y: NODE_LAYOUT_VERTICAL_PADDING + (childNodeWithPosition.y - node.height! / 2 - minY),
  189. }
  190. }
  191. })
  192. })
  193. })
  194. setNodes(newNodes)
  195. const zoom = 0.7
  196. setViewport({
  197. x: 0,
  198. y: 0,
  199. zoom,
  200. })
  201. saveStateToHistory(WorkflowHistoryEvent.LayoutOrganize)
  202. setTimeout(() => {
  203. handleSyncWorkflowDraft()
  204. })
  205. }, [getNodesReadOnly, store, reactflow, workflowStore, handleSyncWorkflowDraft, saveStateToHistory])
  206. return {
  207. handleLayout,
  208. }
  209. }
  210. export const useWorkflowZoom = () => {
  211. const { handleSyncWorkflowDraft } = useNodesSyncDraft()
  212. const { getWorkflowReadOnly } = useWorkflowReadOnly()
  213. const {
  214. zoomIn,
  215. zoomOut,
  216. zoomTo,
  217. fitView,
  218. } = useReactFlow()
  219. const handleFitView = useCallback(() => {
  220. if (getWorkflowReadOnly())
  221. return
  222. fitView()
  223. handleSyncWorkflowDraft()
  224. }, [getWorkflowReadOnly, fitView, handleSyncWorkflowDraft])
  225. const handleBackToOriginalSize = useCallback(() => {
  226. if (getWorkflowReadOnly())
  227. return
  228. zoomTo(1)
  229. handleSyncWorkflowDraft()
  230. }, [getWorkflowReadOnly, zoomTo, handleSyncWorkflowDraft])
  231. const handleSizeToHalf = useCallback(() => {
  232. if (getWorkflowReadOnly())
  233. return
  234. zoomTo(0.5)
  235. handleSyncWorkflowDraft()
  236. }, [getWorkflowReadOnly, zoomTo, handleSyncWorkflowDraft])
  237. const handleZoomOut = useCallback(() => {
  238. if (getWorkflowReadOnly())
  239. return
  240. zoomOut()
  241. handleSyncWorkflowDraft()
  242. }, [getWorkflowReadOnly, zoomOut, handleSyncWorkflowDraft])
  243. const handleZoomIn = useCallback(() => {
  244. if (getWorkflowReadOnly())
  245. return
  246. zoomIn()
  247. handleSyncWorkflowDraft()
  248. }, [getWorkflowReadOnly, zoomIn, handleSyncWorkflowDraft])
  249. return {
  250. handleFitView,
  251. handleBackToOriginalSize,
  252. handleSizeToHalf,
  253. handleZoomOut,
  254. handleZoomIn,
  255. }
  256. }
  257. export const useWorkflowUpdate = () => {
  258. const reactflow = useReactFlow()
  259. const { eventEmitter } = useEventEmitterContextContext()
  260. const handleUpdateWorkflowCanvas = useCallback((payload: WorkflowDataUpdater) => {
  261. const {
  262. nodes,
  263. edges,
  264. viewport,
  265. } = payload
  266. const { setViewport } = reactflow
  267. eventEmitter?.emit({
  268. type: WORKFLOW_DATA_UPDATE,
  269. payload: {
  270. nodes: initialNodes(nodes, edges),
  271. edges: initialEdges(edges, nodes),
  272. },
  273. } as any)
  274. setViewport(viewport)
  275. }, [eventEmitter, reactflow])
  276. return {
  277. handleUpdateWorkflowCanvas,
  278. }
  279. }