| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348 |
- <script lang="ts" setup>
- import type {
- IWorkflow,
- XYPosition,
- ConnectStartEvent,
- CanvasNodeMoveEvent,
- IWorkflowNode
- } from '../Interface'
- import type { SourceType } from '@repo/nodes'
- import type { NodeMouseEvent, Connection, NodeDragEvent } from '@vue-flow/core'
- import { ref, onMounted, computed, provide } from 'vue'
- import { VueFlow, useVueFlow, MarkerType } from '@vue-flow/core'
- import { MiniMap } from '@vue-flow/minimap'
- import CanvasNode from './elements/nodes/CanvasNode.vue'
- import CanvasEdge from './elements/edges/CanvasEdge.vue'
- import CanvasArrowHeadMarker from './elements/edges/CanvasArrowHeadMarker.vue'
- import CanvasBackground from './elements/background/CanvasBackground.vue'
- import CanvasControlBar from './elements/control-bar/CanvasControlBar.vue'
- import ConditionNode from './elements/node-temp/ConditionNode.vue'
- import StartNode from './elements/node-temp/StartNode.vue'
- import HttpNode from './elements/node-temp/HttpNode1.vue'
- import EndNode from './elements/node-temp/EndNode.vue'
- import CodeNode from './elements/node-temp/CodeNode.vue'
- import DataBaseNode from './elements/node-temp/DataBaseNode.vue'
- defineOptions({
- name: 'workflow-canvas'
- })
- const emit = defineEmits<{
- 'update:node:size': [id: string, size: { width: number; height: number }]
- 'update:node:position': [id: string, position: XYPosition]
- 'update:nodes:position': [events: CanvasNodeMoveEvent[]]
- 'update:node:activated': [id: string, event?: MouseEvent]
- 'update:node:deactivated': [id: string]
- 'update:node:enabled': [id: string]
- 'update:node:selected': [id?: string]
- 'update:node:name': [id: string]
- 'update:node:parameters': [id: string, parameters: Record<string, unknown>]
- 'update:node:inputs': [id: string]
- 'update:node:outputs': [id: string]
- 'update:node:attrs': [id: string, attrs: Record<string, unknown>]
- 'update:logs-open': [open?: boolean]
- 'update:logs:input-open': [open?: boolean]
- 'update:logs:output-open': [open?: boolean]
- 'update:has-range-selection': [isActive: boolean]
- 'click:node': [id: string, position: XYPosition]
- 'click:node:add': [id: string, handle: string]
- 'initialized:nodes': []
- 'run:node': [id: string]
- 'copy:production:url': [id: string]
- 'copy:test:url': [id: string]
- 'delete:node': [id: string]
- 'replace:node': [id: string]
- 'create:node': [source: any]
- 'create:sticky': []
- 'delete:nodes': [ids: string[]]
- 'update:nodes:enabled': [ids: string[]]
- 'copy:nodes': [ids: string[]]
- 'duplicate:nodes': [ids: string[]]
- 'cut:nodes': [ids: string[]]
- 'drag-and-drop': [position: XYPosition, event: DragEvent]
- 'delete:connection': [connection: Connection]
- 'create:connection:start': [handle: ConnectStartEvent]
- 'create:connection': [connection: Connection]
- 'create:connection:end': [connection: Connection, event?: MouseEvent]
- 'create:connection:cancelled': [
- handle: ConnectStartEvent,
- position: XYPosition,
- event?: MouseEvent
- ]
- 'click:connection:add': [connection: Connection]
- run: []
- }>()
- const props = withDefaults(
- defineProps<{
- id?: string
- nodes: IWorkflow['nodes']
- edges: IWorkflow['edges']
- readOnly?: boolean
- }>(),
- {
- id: 'canvas',
- readOnly: false,
- nodes: () => [],
- edges: () => []
- }
- )
- const showMinimap = ref(false)
- const vueFlow = useVueFlow(props.id)
- const { viewport, viewportRef, project, zoomIn, zoomOut, fitView, zoomTo } = vueFlow
- const nodeDataById = computed((): Record<string, IWorkflowNode['data']> => {
- return props.nodes.reduce<Record<string, IWorkflowNode['data']>>((acc, node) => {
- acc[node.id] = node.data as IWorkflowNode['data']
- return acc
- }, {})
- })
- /**
- * Returns the position of a mouse or touch event
- */
- const getMousePosition = (event: MouseEvent | TouchEvent): XYPosition => {
- const x = (event && 'clientX' in event ? event.clientX : event?.touches?.[0]?.clientX) ?? 0
- const y = (event && 'clientY' in event ? event.clientY : event?.touches?.[0]?.clientY) ?? 0
- return { x, y }
- }
- function getProjectedPosition(event?: MouseEvent | TouchEvent) {
- const bounds = viewportRef.value?.getBoundingClientRect() ?? { left: 0, top: 0 }
- const { x, y } = event ? getMousePosition(event) : { x: 0, y: 0 }
- return project({
- x: x - bounds.left,
- y: y - bounds.top
- })
- }
- /**
- * Events
- */
- const onNodeClick = ({ node, event }: NodeMouseEvent) => {
- emit('click:node', node.id, getProjectedPosition(event))
- }
- function onDrop(event: DragEvent) {
- const position = getProjectedPosition(event)
- emit('drag-and-drop', position, event)
- }
- const onZoomIn = () => {
- zoomIn()
- }
- const onZoomOut = () => {
- zoomOut()
- }
- const onZoomToFit = () => {
- fitView()
- }
- const onResetZoom = () => {
- zoomTo(1)
- }
- const onAddNode = (value: SourceType | string) => {
- emit('create:node', value)
- }
- const onToggleMinimap = () => {
- showMinimap.value = !showMinimap.value
- }
- function onUpdateNodesPosition(events: CanvasNodeMoveEvent[]) {
- emit('update:nodes:position', events)
- }
- function onUpdateNodePosition(id: string, position: XYPosition) {
- emit('update:node:position', id, position)
- }
- function onNodeDragStop(event: NodeDragEvent) {
- onUpdateNodesPosition(event.nodes.map(({ id, position }) => ({ id, position })))
- }
- function onUpdateNodeAttrs(id: string, attrs: Record<string, unknown>) {
- emit('update:node:attrs', id, attrs)
- }
- /**
- * Connections / Edges
- */
- const connectionCreated = ref(false)
- const connectingHandle = ref<ConnectStartEvent>()
- const connectedHandle = ref<Connection>()
- function onConnectStart(handle: ConnectStartEvent) {
- emit('create:connection:start', handle)
- connectingHandle.value = handle
- connectionCreated.value = false
- }
- function onConnect(connection: Connection) {
- emit('create:connection', connection)
- connectedHandle.value = connection
- connectionCreated.value = true
- }
- function onConnectEnd(event?: MouseEvent) {
- if (connectedHandle.value) {
- emit('create:connection:end', connectedHandle.value, event)
- } else if (connectingHandle.value) {
- emit('create:connection:cancelled', connectingHandle.value, getProjectedPosition(event), event)
- }
- connectedHandle.value = undefined
- connectingHandle.value = undefined
- }
- function onDeleteConnection(connection: Connection) {
- emit('delete:connection', connection)
- }
- function onClickConnectionAdd(connection: Connection) {
- emit('click:connection:add', connection)
- }
- let loaded = false
- function onNodesInitialized() {
- if (!loaded) {
- onZoomToFit()
- loaded = true
- emit('initialized:nodes')
- }
- }
- /**
- * Handle
- */
- const handleRun = () => {
- emit('run')
- }
- onMounted(() => {
- fitView()
- })
- provide('vueflow', {
- id: props.id,
- nodes: props.nodes,
- edges: props.edges,
- vueFlow
- })
- defineExpose({
- vueFlow
- })
- </script>
- <template>
- <VueFlow
- :id="id"
- :nodes="nodes"
- :edges="edges"
- :connection-line-options="{ markerEnd: MarkerType.ArrowClosed }"
- :connection-radius="60"
- snap-to-grid
- :snap-grid="[16, 16]"
- @node-click="onNodeClick"
- @node-drag-stop="onNodeDragStop"
- @drop="onDrop"
- @connect="onConnect"
- @connect-start="onConnectStart"
- @connect-end="onConnectEnd"
- @nodes-initialized="onNodesInitialized"
- v-bind="$attrs"
- >
- <template #node-canvas-node="nodeProps">
- <CanvasNode
- v-bind="nodeProps"
- :data="nodeDataById[nodeProps.id]!"
- @move="onUpdateNodePosition"
- @update="onUpdateNodeAttrs"
- />
- </template>
- <template #node-start-node="nodeProps">
- <StartNode v-bind="nodeProps" :data="nodeDataById[nodeProps.id]" />
- </template>
- <template #node-end-node="nodeProps">
- <EndNode v-bind="nodeProps" :data="nodeDataById[nodeProps.id]" />
- </template>
- <template #node-http-node="nodeProps">
- <HttpNode v-bind="nodeProps" :data="nodeDataById[nodeProps.id]!" />
- </template>
- <template #node-code-node="nodeProps">
- <CodeNode v-bind="nodeProps" :data="nodeDataById[nodeProps.id]!" />
- </template>
- <template #node-database-node="nodeProps">
- <DataBaseNode v-bind="nodeProps" :data="nodeDataById[nodeProps.id]!" />
- </template>
- <template #node-condition-node="nodeProps">
- <ConditionNode v-bind="nodeProps" :data="nodeDataById[nodeProps.id]!" />
- </template>
- <template #edge-canvas-edge="edgeProps">
- <CanvasEdge
- v-bind="edgeProps"
- marker-end="url(#custom-arrow-head-marker)"
- @add="onClickConnectionAdd"
- @delete="onDeleteConnection"
- />
- </template>
- <template #background>
- <rect width="100%" height="100%" fill="#f0f0f0" />
- </template>
- <MiniMap
- v-show="showMinimap"
- :height="120"
- :width="180"
- :node-border-radius="16"
- class="bottom-40px! bg-#f5f5f5 border border-solid border-gray-300"
- position="bottom-left"
- pannable
- zoomable
- />
- <CanvasControlBar
- @zoom-in="onZoomIn"
- @zoom-out="onZoomOut"
- @zoom-to-fit="onZoomToFit"
- @reset-zoom="onResetZoom"
- @add-node="onAddNode"
- @run="handleRun"
- @toggle-minimap="onToggleMinimap"
- />
- <slot name="canvas-background" v-bind="{ viewport }">
- <CanvasBackground :viewport="viewport" :striped="readOnly" />
- </slot>
- <CanvasArrowHeadMarker id="custom-arrow-head-marker" />
- </VueFlow>
- </template>
- <style>
- @import '@vue-flow/core/dist/style.css';
- @import '@vue-flow/core/dist/theme-default.css';
- </style>
|