|
|
@@ -1,11 +1,15 @@
|
|
|
<script lang="ts" setup>
|
|
|
-import { VueFlow, useVueFlow, type NodeMouseEvent, MarkerType } from '@vue-flow/core'
|
|
|
-import { MiniMap } from '@vue-flow/minimap'
|
|
|
-import type { IWorkflow, XYPosition } from '../Interface'
|
|
|
+import type { IWorkflow, XYPosition, ConnectStartEvent } from '../Interface'
|
|
|
import type { SourceType } from '@repo/nodes'
|
|
|
+import type { NodeMouseEvent, Connection } from '@vue-flow/core'
|
|
|
+
|
|
|
+import { ref } from 'vue'
|
|
|
+import { VueFlow, useVueFlow, MarkerType } from '@vue-flow/core'
|
|
|
+import { MiniMap } from '@vue-flow/minimap'
|
|
|
|
|
|
-import CanvasNode from './elements/CanvasNode.vue'
|
|
|
-import CanvasEdge from './elements/CanvasEdge.vue'
|
|
|
+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/CanvasControlBar.vue'
|
|
|
import ConditionNode from './elements/node-temp/ConditionNode.vue'
|
|
|
@@ -16,54 +20,64 @@ import CodeNode from './elements/node-temp/CodeNode.vue'
|
|
|
import DataBaseNode from './elements/node-temp/DataBaseNode.vue'
|
|
|
|
|
|
defineOptions({
|
|
|
- name: 'workflow-canvas'
|
|
|
+ name: 'workflow-canvas'
|
|
|
})
|
|
|
|
|
|
const emit = defineEmits<{
|
|
|
- 'update:node:position': [id: string, position: XYPosition]
|
|
|
- '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: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]
|
|
|
- '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]
|
|
|
- 'run': []
|
|
|
+ 'update:node:position': [id: string, position: XYPosition]
|
|
|
+ '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: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]
|
|
|
+ '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: () => []
|
|
|
- }
|
|
|
+ defineProps<{
|
|
|
+ id?: string
|
|
|
+ nodes: IWorkflow['nodes']
|
|
|
+ edges: IWorkflow['edges']
|
|
|
+ readOnly?: boolean
|
|
|
+ }>(),
|
|
|
+ {
|
|
|
+ id: 'canvas',
|
|
|
+ readOnly: false,
|
|
|
+ nodes: () => [],
|
|
|
+ edges: () => []
|
|
|
+ }
|
|
|
)
|
|
|
|
|
|
const vueFlow = useVueFlow(props.id)
|
|
|
@@ -74,103 +88,183 @@ const { viewport, viewportRef, project, zoomIn, zoomOut, fitView, zoomTo } = vue
|
|
|
* 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
|
|
|
+ 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 }
|
|
|
+ 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 }
|
|
|
+ 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
|
|
|
- })
|
|
|
+ return project({
|
|
|
+ x: x - bounds.left,
|
|
|
+ y: y - bounds.top
|
|
|
+ })
|
|
|
}
|
|
|
|
|
|
+/**
|
|
|
+ * Events
|
|
|
+ */
|
|
|
+
|
|
|
const onNodeClick = ({ node, event }: NodeMouseEvent) => {
|
|
|
- emit('click:node', node.id, getProjectedPosition(event))
|
|
|
+ emit('click:node', node.id, getProjectedPosition(event))
|
|
|
}
|
|
|
|
|
|
function onDrop(event: DragEvent) {
|
|
|
- const position = getProjectedPosition(event)
|
|
|
+ const position = getProjectedPosition(event)
|
|
|
|
|
|
- emit('drag-and-drop', position, event)
|
|
|
+ emit('drag-and-drop', position, event)
|
|
|
}
|
|
|
|
|
|
const onZoomIn = () => {
|
|
|
- zoomIn()
|
|
|
+ zoomIn()
|
|
|
}
|
|
|
|
|
|
const onZoomOut = () => {
|
|
|
- zoomOut()
|
|
|
+ zoomOut()
|
|
|
}
|
|
|
|
|
|
const onZoomToFit = () => {
|
|
|
- fitView()
|
|
|
+ fitView()
|
|
|
}
|
|
|
|
|
|
const onResetZoom = () => {
|
|
|
- zoomTo(1)
|
|
|
+ zoomTo(1)
|
|
|
}
|
|
|
const onAddNode = (value: SourceType) => {
|
|
|
- emit('create:node', value)
|
|
|
+ emit('create:node', value)
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * 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)
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * Handle
|
|
|
+ */
|
|
|
const handleRun = () => {
|
|
|
- emit('run')
|
|
|
+ emit('run')
|
|
|
}
|
|
|
console.log(props.nodes)
|
|
|
</script>
|
|
|
|
|
|
<template>
|
|
|
- <VueFlow :id="id" :nodes="nodes" :edges="edges" :connection-line-options="{ markerEnd: MarkerType.ArrowClosed }"
|
|
|
- :connection-radius="60" @node-click="onNodeClick" @drop="onDrop" v-bind="$attrs">
|
|
|
- <template #node-canvas-node="nodeProps">
|
|
|
- <CanvasNode v-bind="nodeProps" />
|
|
|
- </template>
|
|
|
+ <VueFlow
|
|
|
+ :id="id"
|
|
|
+ :nodes="nodes"
|
|
|
+ :edges="edges"
|
|
|
+ :connection-line-options="{ markerEnd: MarkerType.ArrowClosed }"
|
|
|
+ :connection-radius="60"
|
|
|
+ @node-click="onNodeClick"
|
|
|
+ @drop="onDrop"
|
|
|
+ @connect="onConnect"
|
|
|
+ @connect-start="onConnectStart"
|
|
|
+ @connect-end="onConnectEnd"
|
|
|
+ v-bind="$attrs"
|
|
|
+ >
|
|
|
+ <template #node-canvas-node="nodeProps">
|
|
|
+ <CanvasNode v-bind="nodeProps" />
|
|
|
+ </template>
|
|
|
+
|
|
|
+ <template #node-start-node="nodeProps">
|
|
|
+ <StartNode v-bind="nodeProps" />
|
|
|
+ </template>
|
|
|
|
|
|
- <template #node-start-node="nodeProps">
|
|
|
- <StartNode v-bind="nodeProps" />
|
|
|
- </template>
|
|
|
+ <template #node-end-node="nodeProps">
|
|
|
+ <EndNode v-bind="nodeProps" />
|
|
|
+ </template>
|
|
|
|
|
|
- <template #node-end-node="nodeProps">
|
|
|
- <EndNode v-bind="nodeProps" />
|
|
|
- </template>
|
|
|
+ <template #node-http-node="nodeProps">
|
|
|
+ <HttpNode v-bind="nodeProps" />
|
|
|
+ </template>
|
|
|
|
|
|
- <template #node-http-node="nodeProps">
|
|
|
- <HttpNode v-bind="nodeProps" />
|
|
|
- </template>
|
|
|
+ <template #node-code-node="nodeProps">
|
|
|
+ <CodeNode v-bind="nodeProps" />
|
|
|
+ </template>
|
|
|
|
|
|
- <template #node-code-node="nodeProps">
|
|
|
- <CodeNode v-bind="nodeProps" />
|
|
|
- </template>
|
|
|
+ <template #node-database-node="nodeProps">
|
|
|
+ <DataBaseNode v-bind="nodeProps" />
|
|
|
+ </template>
|
|
|
|
|
|
- <template #node-database-node="nodeProps">
|
|
|
- <DataBaseNode v-bind="nodeProps" />
|
|
|
- </template>
|
|
|
+ <template #node-condition-node="nodeProps">
|
|
|
+ <ConditionNode v-bind="nodeProps" />
|
|
|
+ </template>
|
|
|
|
|
|
- <template #node-condition-node="nodeProps">
|
|
|
- <ConditionNode v-bind="nodeProps" />
|
|
|
- </template>
|
|
|
+ <template #edge-canvas-edge="edgeProps">
|
|
|
+ <CanvasEdge
|
|
|
+ v-bind="edgeProps"
|
|
|
+ marker-end="url(#custom-arrow-head-marker)"
|
|
|
+ @add="onClickConnectionAdd"
|
|
|
+ @delete="onDeleteConnection"
|
|
|
+ />
|
|
|
+ </template>
|
|
|
|
|
|
- <template #edge-canvas-edge="edgeProps">
|
|
|
- <CanvasEdge v-bind="edgeProps" />
|
|
|
- </template>
|
|
|
+ <template #background>
|
|
|
+ <rect width="100%" height="100%" fill="#f0f0f0" />
|
|
|
+ </template>
|
|
|
|
|
|
- <template #background>
|
|
|
- <rect width="100%" height="100%" fill="#f0f0f0" />
|
|
|
- </template>
|
|
|
+ <MiniMap
|
|
|
+ :height="120"
|
|
|
+ :width="180"
|
|
|
+ :node-border-radius="16"
|
|
|
+ class="bg-white bottom-40px!"
|
|
|
+ position="bottom-left"
|
|
|
+ pannable
|
|
|
+ zoomable
|
|
|
+ />
|
|
|
|
|
|
- <MiniMap :height="120" :width="180" :node-border-radius="16" class="bg-white bottom-40px!"
|
|
|
- position="bottom-left" pannable zoomable />
|
|
|
+ <CanvasControlBar
|
|
|
+ @zoom-in="onZoomIn"
|
|
|
+ @zoom-out="onZoomOut"
|
|
|
+ @zoom-to-fit="onZoomToFit"
|
|
|
+ @reset-zoom="onResetZoom"
|
|
|
+ @add-node="onAddNode"
|
|
|
+ @run="handleRun"
|
|
|
+ />
|
|
|
|
|
|
- <CanvasControlBar @zoom-in="onZoomIn" @zoom-out="onZoomOut" @zoom-to-fit="onZoomToFit" @reset-zoom="onResetZoom"
|
|
|
- @add-node="onAddNode" @run="handleRun" />
|
|
|
+ <CanvasBackground :viewport="viewport" :striped="readOnly" />
|
|
|
|
|
|
- <CanvasBackground :viewport="viewport" :striped="readOnly" />
|
|
|
- </VueFlow>
|
|
|
+ <CanvasArrowHeadMarker id="custom-arrow-head-marker" />
|
|
|
+ </VueFlow>
|
|
|
</template>
|
|
|
|
|
|
<style>
|