Canvas.vue 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304
  1. <script lang="ts" setup>
  2. import type { IWorkflow, XYPosition, ConnectStartEvent, CanvasNodeMoveEvent } from '../Interface'
  3. import type { SourceType } from '@repo/nodes'
  4. import type { NodeMouseEvent, Connection, NodeDragEvent } from '@vue-flow/core'
  5. import { ref } from 'vue'
  6. import { VueFlow, useVueFlow, MarkerType } from '@vue-flow/core'
  7. import { MiniMap } from '@vue-flow/minimap'
  8. import CanvasNode from './elements/nodes/CanvasNode.vue'
  9. import CanvasEdge from './elements/edges/CanvasEdge.vue'
  10. import CanvasArrowHeadMarker from './elements/edges/CanvasArrowHeadMarker.vue'
  11. import CanvasBackground from './elements/background/CanvasBackground.vue'
  12. import CanvasControlBar from './elements/CanvasControlBar.vue'
  13. import ConditionNode from './elements/node-temp/ConditionNode.vue'
  14. import StartNode from './elements/node-temp/StartNode.vue'
  15. import HttpNode from './elements/node-temp/HttpNode1.vue'
  16. import EndNode from './elements/node-temp/EndNode.vue'
  17. import CodeNode from './elements/node-temp/CodeNode.vue'
  18. import DataBaseNode from './elements/node-temp/DataBaseNode.vue'
  19. defineOptions({
  20. name: 'workflow-canvas'
  21. })
  22. const emit = defineEmits<{
  23. 'update:node:size': [id: string, size: { width: number; height: number }]
  24. 'update:node:position': [id: string, position: XYPosition]
  25. 'update:nodes:position': [events: CanvasNodeMoveEvent[]]
  26. 'update:node:activated': [id: string, event?: MouseEvent]
  27. 'update:node:deactivated': [id: string]
  28. 'update:node:enabled': [id: string]
  29. 'update:node:selected': [id?: string]
  30. 'update:node:name': [id: string]
  31. 'update:node:parameters': [id: string, parameters: Record<string, unknown>]
  32. 'update:node:inputs': [id: string]
  33. 'update:node:outputs': [id: string]
  34. 'update:node:attrs': [id: string, attrs: Record<string, unknown>]
  35. 'update:logs-open': [open?: boolean]
  36. 'update:logs:input-open': [open?: boolean]
  37. 'update:logs:output-open': [open?: boolean]
  38. 'update:has-range-selection': [isActive: boolean]
  39. 'click:node': [id: string, position: XYPosition]
  40. 'click:node:add': [id: string, handle: string]
  41. 'run:node': [id: string]
  42. 'copy:production:url': [id: string]
  43. 'copy:test:url': [id: string]
  44. 'delete:node': [id: string]
  45. 'replace:node': [id: string]
  46. 'create:node': [source: any]
  47. 'create:sticky': []
  48. 'delete:nodes': [ids: string[]]
  49. 'update:nodes:enabled': [ids: string[]]
  50. 'copy:nodes': [ids: string[]]
  51. 'duplicate:nodes': [ids: string[]]
  52. 'cut:nodes': [ids: string[]]
  53. 'drag-and-drop': [position: XYPosition, event: DragEvent]
  54. 'delete:connection': [connection: Connection]
  55. 'create:connection:start': [handle: ConnectStartEvent]
  56. 'create:connection': [connection: Connection]
  57. 'create:connection:end': [connection: Connection, event?: MouseEvent]
  58. 'create:connection:cancelled': [
  59. handle: ConnectStartEvent,
  60. position: XYPosition,
  61. event?: MouseEvent
  62. ]
  63. 'click:connection:add': [connection: Connection]
  64. run: []
  65. }>()
  66. const props = withDefaults(
  67. defineProps<{
  68. id?: string
  69. nodes: IWorkflow['nodes']
  70. edges: IWorkflow['edges']
  71. readOnly?: boolean
  72. }>(),
  73. {
  74. id: 'canvas',
  75. readOnly: false,
  76. nodes: () => [],
  77. edges: () => []
  78. }
  79. )
  80. const showMinimap = ref(false)
  81. const vueFlow = useVueFlow(props.id)
  82. const { viewport, viewportRef, project, zoomIn, zoomOut, fitView, zoomTo } = vueFlow
  83. /**
  84. * Returns the position of a mouse or touch event
  85. */
  86. const getMousePosition = (event: MouseEvent | TouchEvent): XYPosition => {
  87. const x = (event && 'clientX' in event ? event.clientX : event?.touches?.[0]?.clientX) ?? 0
  88. const y = (event && 'clientY' in event ? event.clientY : event?.touches?.[0]?.clientY) ?? 0
  89. return { x, y }
  90. }
  91. function getProjectedPosition(event?: MouseEvent | TouchEvent) {
  92. const bounds = viewportRef.value?.getBoundingClientRect() ?? { left: 0, top: 0 }
  93. const { x, y } = event ? getMousePosition(event) : { x: 0, y: 0 }
  94. return project({
  95. x: x - bounds.left,
  96. y: y - bounds.top
  97. })
  98. }
  99. /**
  100. * Events
  101. */
  102. const onNodeClick = ({ node, event }: NodeMouseEvent) => {
  103. emit('click:node', node.id, getProjectedPosition(event))
  104. }
  105. function onDrop(event: DragEvent) {
  106. const position = getProjectedPosition(event)
  107. emit('drag-and-drop', position, event)
  108. }
  109. const onZoomIn = () => {
  110. zoomIn()
  111. }
  112. const onZoomOut = () => {
  113. zoomOut()
  114. }
  115. const onZoomToFit = () => {
  116. fitView()
  117. }
  118. const onResetZoom = () => {
  119. zoomTo(1)
  120. }
  121. const onAddNode = (value: SourceType | string) => {
  122. emit('create:node', value)
  123. }
  124. const onToggleMinimap = () => {
  125. showMinimap.value = !showMinimap.value
  126. }
  127. function onUpdateNodesPosition(events: CanvasNodeMoveEvent[]) {
  128. emit('update:nodes:position', events)
  129. }
  130. function onUpdateNodePosition(id: string, position: XYPosition) {
  131. emit('update:node:position', id, position)
  132. }
  133. function onNodeDragStop(event: NodeDragEvent) {
  134. onUpdateNodesPosition(event.nodes.map(({ id, position }) => ({ id, position })))
  135. }
  136. function onUpdateNodeAttrs(id: string, attrs: Record<string, unknown>) {
  137. emit('update:node:attrs', id, attrs)
  138. }
  139. /**
  140. * Connections / Edges
  141. */
  142. const connectionCreated = ref(false)
  143. const connectingHandle = ref<ConnectStartEvent>()
  144. const connectedHandle = ref<Connection>()
  145. function onConnectStart(handle: ConnectStartEvent) {
  146. emit('create:connection:start', handle)
  147. connectingHandle.value = handle
  148. connectionCreated.value = false
  149. }
  150. function onConnect(connection: Connection) {
  151. emit('create:connection', connection)
  152. connectedHandle.value = connection
  153. connectionCreated.value = true
  154. }
  155. function onConnectEnd(event?: MouseEvent) {
  156. if (connectedHandle.value) {
  157. emit('create:connection:end', connectedHandle.value, event)
  158. } else if (connectingHandle.value) {
  159. emit('create:connection:cancelled', connectingHandle.value, getProjectedPosition(event), event)
  160. }
  161. connectedHandle.value = undefined
  162. connectingHandle.value = undefined
  163. }
  164. function onDeleteConnection(connection: Connection) {
  165. emit('delete:connection', connection)
  166. }
  167. function onClickConnectionAdd(connection: Connection) {
  168. emit('click:connection:add', connection)
  169. }
  170. /**
  171. * Handle
  172. */
  173. const handleRun = () => {
  174. emit('run')
  175. }
  176. </script>
  177. <template>
  178. <VueFlow
  179. :id="id"
  180. :nodes="nodes"
  181. :edges="edges"
  182. :connection-line-options="{ markerEnd: MarkerType.ArrowClosed }"
  183. :connection-radius="60"
  184. snap-to-grid
  185. :snap-grid="[16, 16]"
  186. @node-click="onNodeClick"
  187. @node-drag-stop="onNodeDragStop"
  188. @drop="onDrop"
  189. @connect="onConnect"
  190. @connect-start="onConnectStart"
  191. @connect-end="onConnectEnd"
  192. v-bind="$attrs"
  193. >
  194. <template #node-canvas-node="nodeProps">
  195. <CanvasNode v-bind="nodeProps" @move="onUpdateNodePosition" @update="onUpdateNodeAttrs" />
  196. </template>
  197. <template #node-start-node="nodeProps">
  198. <StartNode v-bind="nodeProps" />
  199. </template>
  200. <template #node-end-node="nodeProps">
  201. <EndNode v-bind="nodeProps" />
  202. </template>
  203. <template #node-http-node="nodeProps">
  204. <HttpNode v-bind="nodeProps" />
  205. </template>
  206. <template #node-code-node="nodeProps">
  207. <CodeNode v-bind="nodeProps" />
  208. </template>
  209. <template #node-database-node="nodeProps">
  210. <DataBaseNode v-bind="nodeProps" />
  211. </template>
  212. <template #node-condition-node="nodeProps">
  213. <ConditionNode v-bind="nodeProps" />
  214. </template>
  215. <template #edge-canvas-edge="edgeProps">
  216. <CanvasEdge
  217. v-bind="edgeProps"
  218. marker-end="url(#custom-arrow-head-marker)"
  219. @add="onClickConnectionAdd"
  220. @delete="onDeleteConnection"
  221. />
  222. </template>
  223. <template #background>
  224. <rect width="100%" height="100%" fill="#f0f0f0" />
  225. </template>
  226. <MiniMap
  227. v-show="showMinimap"
  228. :height="120"
  229. :width="180"
  230. :node-border-radius="16"
  231. class="bottom-40px! bg-#f5f5f5 border border-solid border-gray-300"
  232. position="bottom-left"
  233. pannable
  234. zoomable
  235. />
  236. <CanvasControlBar
  237. @zoom-in="onZoomIn"
  238. @zoom-out="onZoomOut"
  239. @zoom-to-fit="onZoomToFit"
  240. @reset-zoom="onResetZoom"
  241. @add-node="onAddNode"
  242. @run="handleRun"
  243. @toggle-minimap="onToggleMinimap"
  244. />
  245. <slot name="canvas-background" v-bind="{ viewport }">
  246. <CanvasBackground :viewport="viewport" :striped="readOnly" />
  247. </slot>
  248. <CanvasArrowHeadMarker id="custom-arrow-head-marker" />
  249. </VueFlow>
  250. </template>
  251. <style>
  252. @import '@vue-flow/core/dist/style.css';
  253. @import '@vue-flow/core/dist/theme-default.css';
  254. </style>