fix: basic vueflow using custom node & edge
This commit is contained in:
parent
c61be69ca2
commit
42247bc31f
5 changed files with 205 additions and 3 deletions
|
|
@ -1,15 +1,45 @@
|
|||
<script setup>
|
||||
import { VueFlow } from "@vue-flow/core";
|
||||
import { VueFlow, useVueFlow, Panel, PanelPosition } from "@vue-flow/core";
|
||||
import { Background } from "@vue-flow/background";
|
||||
import TransitionEdge from "./components/TransitionEdge.vue";
|
||||
import StateNode from "./components/StateNode.vue";
|
||||
import { useStore } from "./store";
|
||||
|
||||
let store = useStore();
|
||||
let { nodes, onConnect, addNodes, addEdges } = useVueFlow();
|
||||
|
||||
function add_state() {
|
||||
let state_id = (nodes.value.length + 1).toString();
|
||||
addNodes([
|
||||
{
|
||||
id: state_id,
|
||||
type: "state",
|
||||
label: "State " + state_id,
|
||||
position: { x: 250, y: 100 }
|
||||
}
|
||||
]);
|
||||
}
|
||||
|
||||
onConnect(params => {
|
||||
params.animated = true;
|
||||
params.type = "transition";
|
||||
addEdges([params]);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="workflow-container">
|
||||
<VueFlow v-model="store.workflow.elements">
|
||||
<VueFlow v-model="store.workflow.elements" connection-mode="loose">
|
||||
<Background pattern-color="#aaa" gap="10" />
|
||||
<Panel :position="PanelPosition.TopRight">
|
||||
<button @click="add_state">Add State</button>
|
||||
</Panel>
|
||||
<template #node-state="node">
|
||||
<StateNode :node="node" />
|
||||
</template>
|
||||
<template #edge-transition="props">
|
||||
<TransitionEdge v-bind="props" />
|
||||
</template>
|
||||
</VueFlow>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -25,4 +55,16 @@ let store = useStore();
|
|||
border: 1px solid var(--border-color);
|
||||
background-color: var(--fg-color);
|
||||
}
|
||||
|
||||
:deep(.transition-edge) {
|
||||
stroke: var(--gray-600);
|
||||
stroke-width: 1.5px;
|
||||
}
|
||||
|
||||
:deep(.selected) {
|
||||
.transition-edge {
|
||||
stroke: var(--primary);
|
||||
stroke-width: 2px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
64
frappe/public/js/workflow_builder/components/StateNode.vue
Normal file
64
frappe/public/js/workflow_builder/components/StateNode.vue
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
<script setup>
|
||||
import { Handle, Position } from "@vue-flow/core";
|
||||
|
||||
const props = defineProps({
|
||||
node: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="node" tabindex="0">
|
||||
<div v-if="node.label" class="node-label">{{ node.label }}</div>
|
||||
<div v-else class="node-placeholder text-muted">{{ __("No Label") }}</div>
|
||||
<Handle class="handle handle-a" type="source" :position="Position.Top" id="a" />
|
||||
<Handle class="handle handle-b" type="source" :position="Position.Right" id="b" />
|
||||
<Handle class="handle handle-c" type="source" :position="Position.Bottom" id="c" />
|
||||
<Handle class="handle handle-d" type="source" :position="Position.Left" id="d" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.node {
|
||||
position: relative;
|
||||
background-color: var(--fg-color);
|
||||
font-weight: 500;
|
||||
border-radius: 50%;
|
||||
padding: 25px;
|
||||
color: var(--gray-600);
|
||||
border: 1px solid var(--gray-600);
|
||||
box-shadow: var(--shadow-base);
|
||||
}
|
||||
|
||||
.vue-flow__node.selected .node {
|
||||
outline: 1.5px solid var(--primary);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.handle {
|
||||
position: absolute;
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
background-color: var(--gray-600);
|
||||
border-radius: 50%;
|
||||
transition: all 0.2s ease-in-out;
|
||||
|
||||
&.handle-a {
|
||||
top: -12px;
|
||||
}
|
||||
|
||||
&.handle-b {
|
||||
right: -12px;
|
||||
}
|
||||
|
||||
&.handle-c {
|
||||
bottom: -12px;
|
||||
}
|
||||
|
||||
&.handle-d {
|
||||
left: -12px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,84 @@
|
|||
<script setup>
|
||||
import { computed, watch } from "vue";
|
||||
import { getSmoothStepPath, SmoothStepEdge, useVueFlow } from "@vue-flow/core";
|
||||
|
||||
let { findEdge } = useVueFlow();
|
||||
|
||||
const props = defineProps({
|
||||
id: { type: String, required: true },
|
||||
sourceX: { type: Number, required: true },
|
||||
sourceY: { type: Number, required: true },
|
||||
targetX: { type: Number, required: true },
|
||||
targetY: { type: Number, required: true },
|
||||
sourcePosition: { type: String, required: false },
|
||||
targetPosition: { type: String, required: false },
|
||||
sourceHandle: { type: String, required: false },
|
||||
targetHandle: { type: String, required: false },
|
||||
sourceNode: { type: Object, required: true },
|
||||
targetNode: { type: Object, required: true },
|
||||
markerEnd: { type: String, required: false },
|
||||
selected: { type: Boolean, required: false },
|
||||
data: { type: Object, required: false }
|
||||
});
|
||||
|
||||
let marker_end = {
|
||||
type: "arrow",
|
||||
width: 20,
|
||||
height: 20,
|
||||
strokeWidth: 1.5,
|
||||
color: "#687178"
|
||||
};
|
||||
|
||||
let marker_end_primary = {
|
||||
type: "arrow",
|
||||
width: 15,
|
||||
height: 15,
|
||||
strokeWidth: 1.5,
|
||||
color: "#2490ef"
|
||||
};
|
||||
|
||||
watch(
|
||||
() => props.selected,
|
||||
() => {
|
||||
findEdge(props.id).markerEnd = props.selected ? marker_end_primary : marker_end;
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
const d = computed(() => {
|
||||
return getSmoothStepPath({
|
||||
sourceX: props.sourceX,
|
||||
sourceY: props.sourceY,
|
||||
targetX: props.targetX,
|
||||
targetY: props.targetY,
|
||||
sourceHandle: props.sourceHandle,
|
||||
targetHandle: props.targetHandle,
|
||||
sourcePosition: props.sourcePosition,
|
||||
targetPosition: props.targetPosition,
|
||||
targetNode: props.targetNode,
|
||||
borderRadius: 30
|
||||
});
|
||||
});
|
||||
</script>
|
||||
<script>
|
||||
export default {
|
||||
inheritAttrs: false
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<SmoothStepEdge class="transition-edge" :id="id" :path="d[0]" :markerEnd="markerEnd" />
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.access {
|
||||
pointer-events: all;
|
||||
cursor: pointer;
|
||||
position: absolute;
|
||||
font-size: var(--text-sm);
|
||||
padding: 2px 6px;
|
||||
border-radius: 16px;
|
||||
background-color: var(--fg-color);
|
||||
border: 1px solid var(--gray-600);
|
||||
box-shadow: var(--shadow-base);
|
||||
}
|
||||
</style>
|
||||
|
|
@ -3,7 +3,19 @@ import { ref } from "vue";
|
|||
|
||||
export const useStore = defineStore("workflow-builder-store", () => {
|
||||
let workflow = ref({
|
||||
elements: [],
|
||||
elements: [
|
||||
{ id: "1", label: "Open", type: "state", position: { x: 300, y: 150 } },
|
||||
{ id: "2", label: "Approved", type: "state", position: { x: 700, y: 150 } },
|
||||
{
|
||||
id: "edge-1-2",
|
||||
source: "1",
|
||||
target: "2",
|
||||
type: "transition",
|
||||
sourceHandle: "b",
|
||||
targetHandle: "d",
|
||||
animated: true,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
return {
|
||||
|
|
|
|||
0
frappe/public/js/workflow_builder/utils.js
Normal file
0
frappe/public/js/workflow_builder/utils.js
Normal file
Loading…
Add table
Reference in a new issue