Canvas.vue 8.9 KB

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