fix: Workflow Builder not updating diagram with added states/transitions in table (#22429)
Co-authored-by: Shariq Ansari <sharique.rik@gmail.com>
This commit is contained in:
parent
c519239ff2
commit
53c795f4b8
9 changed files with 531 additions and 424 deletions
|
|
@ -1,286 +1,276 @@
|
|||
<script setup>
|
||||
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 ActionNode from "./components/ActionNode.vue";
|
||||
import ConnectionLine from "./components/ConnectionLine.vue";
|
||||
import Sidebar from "./components/Sidebar.vue";
|
||||
import { useStore } from "./store";
|
||||
import { validate_transitions } from "./utils";
|
||||
import { ref, computed, nextTick, onMounted, watch } from "vue";
|
||||
import {
|
||||
onClickOutside,
|
||||
useMagicKeys,
|
||||
whenever,
|
||||
useActiveElement,
|
||||
} from "@vueuse/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 ActionNode from "./components/ActionNode.vue";
|
||||
import ConnectionLine from "./components/ConnectionLine.vue";
|
||||
import Sidebar from "./components/Sidebar.vue";
|
||||
import { useStore } from "./store";
|
||||
import { validate_transitions } from "./utils";
|
||||
import { ref, computed, nextTick, onMounted, watch } from "vue";
|
||||
import { onClickOutside, useMagicKeys, whenever, useActiveElement } from "@vueuse/core";
|
||||
|
||||
let store = useStore();
|
||||
let store = useStore();
|
||||
|
||||
const {
|
||||
nodes,
|
||||
getEdges,
|
||||
getSelectedNodes,
|
||||
findNode,
|
||||
onNodeDragStop,
|
||||
onConnect,
|
||||
onEdgeUpdate,
|
||||
onEdgeUpdateEnd,
|
||||
addNodes,
|
||||
addEdges,
|
||||
setEdges,
|
||||
updateEdge,
|
||||
removeNodes,
|
||||
endConnection,
|
||||
onPaneReady,
|
||||
fitView,
|
||||
zoomIn,
|
||||
zoomOut,
|
||||
project,
|
||||
vueFlowRef,
|
||||
} = useVueFlow();
|
||||
const {
|
||||
nodes,
|
||||
getEdges,
|
||||
getSelectedNodes,
|
||||
findNode,
|
||||
onNodeDragStop,
|
||||
onConnect,
|
||||
onEdgeUpdate,
|
||||
onEdgeUpdateEnd,
|
||||
addNodes,
|
||||
addEdges,
|
||||
setEdges,
|
||||
updateEdge,
|
||||
removeNodes,
|
||||
endConnection,
|
||||
onPaneReady,
|
||||
fitView,
|
||||
zoomIn,
|
||||
zoomOut,
|
||||
project,
|
||||
vueFlowRef,
|
||||
} = useVueFlow();
|
||||
|
||||
let main = ref(null);
|
||||
onClickOutside(main, loose_focus);
|
||||
let main = ref(null);
|
||||
// this change to keep the state as it is when saving
|
||||
//onClickOutside(main, loose_focus);
|
||||
|
||||
// cmd/ctrl + s to save the form
|
||||
const {
|
||||
meta_s,
|
||||
ctrl_s,
|
||||
Backspace,
|
||||
meta_backspace,
|
||||
ctrl_backspace,
|
||||
} = useMagicKeys();
|
||||
whenever(
|
||||
() => meta_s.value || ctrl_s.value,
|
||||
() => {
|
||||
store.save_changes();
|
||||
}
|
||||
);
|
||||
// cmd/ctrl + s to save the form
|
||||
const { meta_s, ctrl_s, Backspace, meta_backspace, ctrl_backspace } = useMagicKeys();
|
||||
whenever(
|
||||
() => meta_s.value || ctrl_s.value,
|
||||
() => {
|
||||
store.save_changes();
|
||||
}
|
||||
);
|
||||
|
||||
const activeElement = useActiveElement();
|
||||
const notUsingInput = computed(
|
||||
() =>
|
||||
activeElement.value?.tagName !== "INPUT" &&
|
||||
activeElement.value?.tagName !== "TEXTAREA"
|
||||
);
|
||||
const activeElement = useActiveElement();
|
||||
const notUsingInput = computed(
|
||||
() => activeElement.value?.tagName !== "INPUT" && activeElement.value?.tagName !== "TEXTAREA"
|
||||
);
|
||||
|
||||
whenever(
|
||||
() => Backspace.value || meta_backspace.value || ctrl_backspace.value,
|
||||
() => {
|
||||
if (meta_backspace.value || ctrl_backspace.value) return;
|
||||
if (store.workflow.selected) {
|
||||
if (
|
||||
notUsingInput.value &&
|
||||
(store.workflow.selected.type === "state" ||
|
||||
store.workflow.selected.type === "action")
|
||||
) {
|
||||
removeNodes([store.workflow.selected.id]);
|
||||
if (store.workflow.selected.data?.state) {
|
||||
let connected_nodes = [];
|
||||
connected_nodes = nodes.value
|
||||
.filter(
|
||||
(node) =>
|
||||
node.data.from ==
|
||||
store.workflow.selected.data.state ||
|
||||
node.data.to ==
|
||||
store.workflow.selected.data.state
|
||||
)
|
||||
.map((node) => node.id);
|
||||
removeNodes(connected_nodes);
|
||||
}
|
||||
store.workflow.selected = null;
|
||||
nextTick(() => store.ref_history.commit());
|
||||
whenever(
|
||||
() => Backspace.value || meta_backspace.value || ctrl_backspace.value,
|
||||
() => {
|
||||
if (meta_backspace.value || ctrl_backspace.value) return;
|
||||
if (store.workflow.selected) {
|
||||
if (
|
||||
notUsingInput.value &&
|
||||
(store.workflow.selected.type === "state" ||
|
||||
store.workflow.selected.type === "action")
|
||||
) {
|
||||
removeNodes([store.workflow.selected.id]);
|
||||
if (store.workflow.selected.data?.state) {
|
||||
let connected_nodes = [];
|
||||
connected_nodes = nodes.value
|
||||
.filter(
|
||||
(node) =>
|
||||
node.data.from_id == store.workflow.selected.id ||
|
||||
node.data.to_id == store.workflow.selected.id
|
||||
)
|
||||
.map((node) => node.id);
|
||||
removeNodes(connected_nodes);
|
||||
}
|
||||
store.workflow.selected = null;
|
||||
nextTick(() => store.ref_history.commit());
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
onNodeDragStop(() => {
|
||||
nextTick(() => store.ref_history.commit());
|
||||
});
|
||||
onNodeDragStop(() => {
|
||||
nextTick(() => store.ref_history.commit());
|
||||
});
|
||||
|
||||
onConnect((edge) => {
|
||||
let source_node = findNode(edge.source);
|
||||
let target_node = findNode(edge.target);
|
||||
onConnect((edge) => {
|
||||
let source_node = findNode(edge.source);
|
||||
let target_node = findNode(edge.target);
|
||||
|
||||
let error = validate_transitions(source_node.data, target_node.data);
|
||||
if (error) {
|
||||
endConnection();
|
||||
nextTick(() =>
|
||||
frappe.throw({
|
||||
title: "Invalid Transition",
|
||||
message: error,
|
||||
})
|
||||
);
|
||||
return;
|
||||
}
|
||||
let error = validate_transitions(source_node.data, target_node.data);
|
||||
if (error) {
|
||||
endConnection();
|
||||
nextTick(() =>
|
||||
frappe.throw({
|
||||
title: "Invalid Transition",
|
||||
message: error,
|
||||
})
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
let source_center = {
|
||||
x: source_node.position.x + source_node.dimensions.width / 2,
|
||||
y: source_node.position.y + source_node.dimensions.height / 2,
|
||||
};
|
||||
let source_center = {
|
||||
x: source_node.position.x + source_node.dimensions.width / 2,
|
||||
y: source_node.position.y + source_node.dimensions.height / 2,
|
||||
};
|
||||
|
||||
let target_center = {
|
||||
x: target_node.position.x + target_node.dimensions.width / 2,
|
||||
y: target_node.position.y + target_node.dimensions.height / 2,
|
||||
};
|
||||
let target_center = {
|
||||
x: target_node.position.x + target_node.dimensions.width / 2,
|
||||
y: target_node.position.y + target_node.dimensions.height / 2,
|
||||
};
|
||||
|
||||
let center_x = (source_center.x + target_center.x) / 2;
|
||||
let center_y = source_center.y;
|
||||
let center_x = (source_center.x + target_center.x) / 2;
|
||||
let center_y = source_center.y;
|
||||
|
||||
const action_node = {
|
||||
id: "action-" + frappe.utils.get_random(5),
|
||||
type: "action",
|
||||
position: { x: center_x, y: center_y },
|
||||
selected: true,
|
||||
data: {
|
||||
action: "",
|
||||
allowed: "All",
|
||||
from: source_node.data.state,
|
||||
to: target_node.data.state,
|
||||
let action_ids = nodes.value
|
||||
.filter((node) => node.type == "action")
|
||||
.map((node) => parseInt(node.id.replace("action-", "")));
|
||||
let action_id = action_ids.length ? (Math.max(...action_ids) + 1).toString() : "1";
|
||||
|
||||
const action_node = {
|
||||
id: "action-" + action_id,
|
||||
type: "action",
|
||||
position: { x: center_x, y: center_y },
|
||||
selected: true,
|
||||
data: {
|
||||
action: "",
|
||||
allowed: "All",
|
||||
from: source_node.data.state,
|
||||
to: target_node.data.state,
|
||||
from_id: source_node.id,
|
||||
to_id: target_node.id,
|
||||
},
|
||||
};
|
||||
addNodes([action_node]);
|
||||
|
||||
let action_edge = {
|
||||
source: edge.source,
|
||||
sourceHandle: edge.sourceHandle,
|
||||
target: action_node.id,
|
||||
targetHandle: "left",
|
||||
type: "transition",
|
||||
updatable: true,
|
||||
animated: true,
|
||||
};
|
||||
let state_edge = {
|
||||
source: action_node.id,
|
||||
sourceHandle: "right",
|
||||
target: edge.target,
|
||||
targetHandle: edge.targetHandle,
|
||||
type: "transition",
|
||||
updatable: true,
|
||||
animated: true,
|
||||
};
|
||||
addEdges([action_edge, state_edge]);
|
||||
|
||||
nextTick(() => {
|
||||
const node = findNode(action_node.id);
|
||||
const stop = watch(
|
||||
() => node.dimensions,
|
||||
(dimensions) => {
|
||||
if (dimensions.width > 0 && dimensions.height > 0) {
|
||||
node.position = {
|
||||
x: node.position.x - node.dimensions.width / 2,
|
||||
y: node.position.y - node.dimensions.height / 2,
|
||||
};
|
||||
stop();
|
||||
node.selected = true;
|
||||
store.workflow.selected = node;
|
||||
store.ref_history.commit();
|
||||
}
|
||||
},
|
||||
};
|
||||
addNodes([action_node]);
|
||||
|
||||
let action_edge = {
|
||||
source: edge.source,
|
||||
sourceHandle: edge.sourceHandle,
|
||||
target: action_node.id,
|
||||
targetHandle: "left",
|
||||
type: "transition",
|
||||
updatable: true,
|
||||
animated: true,
|
||||
};
|
||||
let state_edge = {
|
||||
source: action_node.id,
|
||||
sourceHandle: "right",
|
||||
target: edge.target,
|
||||
targetHandle: edge.targetHandle,
|
||||
type: "transition",
|
||||
updatable: true,
|
||||
animated: true,
|
||||
};
|
||||
addEdges([action_edge, state_edge]);
|
||||
|
||||
nextTick(() => {
|
||||
const node = findNode(action_node.id);
|
||||
const stop = watch(
|
||||
() => node.dimensions,
|
||||
(dimensions) => {
|
||||
if (dimensions.width > 0 && dimensions.height > 0) {
|
||||
node.position = {
|
||||
x: node.position.x - node.dimensions.width / 2,
|
||||
y: node.position.y - node.dimensions.height / 2,
|
||||
};
|
||||
stop();
|
||||
node.selected = true;
|
||||
store.workflow.selected = node;
|
||||
store.ref_history.commit();
|
||||
}
|
||||
},
|
||||
{ deep: true, flush: "post" }
|
||||
);
|
||||
});
|
||||
{ deep: true, flush: "post" }
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
onEdgeUpdateEnd(({ edge }) => {
|
||||
getSelectedNodes.value?.forEach((node) => (node.selected = false));
|
||||
if (edge.source.startsWith("action-")) {
|
||||
setTimeout(() => (findNode(edge.source).selected = true));
|
||||
} else if (edge.target.startsWith("action-")) {
|
||||
setTimeout(() => (findNode(edge.target).selected = true));
|
||||
}
|
||||
});
|
||||
|
||||
onEdgeUpdate(({ edge, connection }) => {
|
||||
if (
|
||||
(connection.source == edge.source &&
|
||||
connection.target != edge.target) ||
|
||||
(connection.source != edge.source &&
|
||||
connection.target == edge.target) ||
|
||||
connection.source === connection.target
|
||||
)
|
||||
return;
|
||||
|
||||
updateEdge(edge, connection);
|
||||
setEdges(getEdges.value);
|
||||
nextTick(() => store.ref_history.commit());
|
||||
});
|
||||
|
||||
function onDragOver(event) {
|
||||
event.preventDefault();
|
||||
|
||||
if (event.dataTransfer) {
|
||||
event.dataTransfer.dropEffect = "move";
|
||||
}
|
||||
onEdgeUpdateEnd(({ edge }) => {
|
||||
getSelectedNodes.value?.forEach((node) => (node.selected = false));
|
||||
if (edge.source.startsWith("action-")) {
|
||||
setTimeout(() => (findNode(edge.source).selected = true));
|
||||
} else if (edge.target.startsWith("action-")) {
|
||||
setTimeout(() => (findNode(edge.target).selected = true));
|
||||
}
|
||||
});
|
||||
|
||||
function onDrop(event) {
|
||||
const { left, top } = vueFlowRef.value.getBoundingClientRect();
|
||||
onEdgeUpdate(({ edge, connection }) => {
|
||||
if (
|
||||
(connection.source == edge.source && connection.target != edge.target) ||
|
||||
(connection.source != edge.source && connection.target == edge.target) ||
|
||||
connection.source === connection.target
|
||||
)
|
||||
return;
|
||||
|
||||
getSelectedNodes.value?.forEach((node) => (node.selected = false));
|
||||
updateEdge(edge, connection);
|
||||
setEdges(getEdges.value);
|
||||
nextTick(() => store.ref_history.commit());
|
||||
});
|
||||
|
||||
const position = project({
|
||||
x: event.clientX - left,
|
||||
y: event.clientY - top,
|
||||
});
|
||||
function onDragOver(event) {
|
||||
event.preventDefault();
|
||||
|
||||
let state_ids = nodes.value
|
||||
.filter((node) => node.type == "state")
|
||||
.map((node) => node.id);
|
||||
let state_id = state_ids.length
|
||||
? (Math.max(...state_ids) + 1).toString()
|
||||
: "1";
|
||||
const new_state = {
|
||||
id: state_id,
|
||||
type: "state",
|
||||
position,
|
||||
selected: true,
|
||||
data: {
|
||||
state: "",
|
||||
doc_status: "Draft",
|
||||
allow_edit: "All",
|
||||
if (event.dataTransfer) {
|
||||
event.dataTransfer.dropEffect = "move";
|
||||
}
|
||||
}
|
||||
|
||||
function onDrop(event) {
|
||||
const { left, top } = vueFlowRef.value.getBoundingClientRect();
|
||||
|
||||
getSelectedNodes.value?.forEach((node) => (node.selected = false));
|
||||
|
||||
const position = project({
|
||||
x: event.clientX - left,
|
||||
y: event.clientY - top,
|
||||
});
|
||||
|
||||
let state_ids = nodes.value.filter((node) => node.type == "state").map((node) => node.id);
|
||||
let state_id = state_ids.length ? (Math.max(...state_ids) + 1).toString() : "1";
|
||||
const new_state = {
|
||||
id: state_id,
|
||||
type: "state",
|
||||
position,
|
||||
selected: true,
|
||||
data: {
|
||||
state: "",
|
||||
doc_status: "Draft",
|
||||
allow_edit: "All",
|
||||
},
|
||||
};
|
||||
|
||||
addNodes([new_state]);
|
||||
|
||||
nextTick(() => {
|
||||
const node = findNode(new_state.id);
|
||||
const stop = watch(
|
||||
() => node.dimensions,
|
||||
(dimensions) => {
|
||||
if (dimensions.width > 0 && dimensions.height > 0) {
|
||||
node.position = {
|
||||
x: node.position.x - node.dimensions.width / 2,
|
||||
y: node.position.y - node.dimensions.height / 2,
|
||||
};
|
||||
stop();
|
||||
store.workflow.selected = node;
|
||||
store.ref_history.commit();
|
||||
}
|
||||
},
|
||||
};
|
||||
{ deep: true, flush: "post" }
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
addNodes([new_state]);
|
||||
|
||||
nextTick(() => {
|
||||
const node = findNode(new_state.id);
|
||||
const stop = watch(
|
||||
() => node.dimensions,
|
||||
(dimensions) => {
|
||||
if (dimensions.width > 0 && dimensions.height > 0) {
|
||||
node.position = {
|
||||
x: node.position.x - node.dimensions.width / 2,
|
||||
y: node.position.y - node.dimensions.height / 2,
|
||||
};
|
||||
stop();
|
||||
store.workflow.selected = node;
|
||||
store.ref_history.commit();
|
||||
}
|
||||
},
|
||||
{ deep: true, flush: "post" }
|
||||
);
|
||||
});
|
||||
function onDragStart(event) {
|
||||
if (event.dataTransfer) {
|
||||
event.dataTransfer.effectAllowed = "move";
|
||||
}
|
||||
loose_focus();
|
||||
}
|
||||
|
||||
function onDragStart(event) {
|
||||
if (event.dataTransfer) {
|
||||
event.dataTransfer.effectAllowed = "move";
|
||||
}
|
||||
loose_focus();
|
||||
}
|
||||
|
||||
function loose_focus() {
|
||||
function loose_focus() {
|
||||
if (store.workflow.selected) {
|
||||
getSelectedNodes.value?.forEach((node) => (node.selected = false));
|
||||
store.workflow.selected = null;
|
||||
store.ref_history.commit();
|
||||
}
|
||||
}
|
||||
|
||||
onPaneReady(() => fitView());
|
||||
onMounted(() => store.fetch());
|
||||
onPaneReady(() => fitView());
|
||||
onMounted(() => store.fetch());
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
@ -288,11 +278,7 @@
|
|||
<div class="sidebar-container" @click.stop>
|
||||
<Sidebar />
|
||||
</div>
|
||||
<div
|
||||
class="workflow-container"
|
||||
@drop="onDrop"
|
||||
@click.stop="loose_focus"
|
||||
>
|
||||
<div class="workflow-container" @drop="onDrop" @click.stop="loose_focus">
|
||||
<VueFlow
|
||||
v-model="store.workflow.elements"
|
||||
connection-mode="loose"
|
||||
|
|
@ -312,21 +298,9 @@
|
|||
</div>
|
||||
</Panel>
|
||||
<Panel :position="PanelPosition.BottomLeft">
|
||||
<button class="btn btn-sm btn-default mr-2" @click="zoomIn">
|
||||
+
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-sm btn-default mr-2"
|
||||
@click="zoomOut"
|
||||
>
|
||||
-
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-sm btn-default"
|
||||
@click="fitView()"
|
||||
>
|
||||
Fit
|
||||
</button>
|
||||
<button class="btn btn-sm btn-default mr-2" @click="zoomIn">+</button>
|
||||
<button class="btn btn-sm btn-default mr-2" @click="zoomOut">-</button>
|
||||
<button class="btn btn-sm btn-default" @click="fitView()">Fit</button>
|
||||
</Panel>
|
||||
<template #node-state="node">
|
||||
<StateNode :node="node" />
|
||||
|
|
@ -346,54 +320,50 @@
|
|||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "@vue-flow/core/dist/style.css";
|
||||
@import "@vue-flow/core/dist/theme-default.css";
|
||||
@import "@vue-flow/core/dist/style.css";
|
||||
@import "@vue-flow/core/dist/theme-default.css";
|
||||
|
||||
.main {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
height: calc(
|
||||
100vh - var(--navbar-height) - var(--page-head-height) - 65px
|
||||
);
|
||||
.main {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
height: calc(100vh - var(--navbar-height) - var(--page-head-height) - 65px);
|
||||
|
||||
&.resizing {
|
||||
user-select: none;
|
||||
cursor: col-resize;
|
||||
}
|
||||
|
||||
.sidebar-container {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
margin-right: 10px;
|
||||
border-radius: var(--border-radius-lg);
|
||||
border: 1px solid var(--border-color);
|
||||
background-color: var(--fg-color);
|
||||
}
|
||||
&.resizing {
|
||||
user-select: none;
|
||||
cursor: col-resize;
|
||||
}
|
||||
.workflow-container {
|
||||
width: 100%;
|
||||
height: calc(
|
||||
100vh - var(--navbar-height) - var(--page-head-height) - 65px
|
||||
);
|
||||
|
||||
.sidebar-container {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
margin-right: 10px;
|
||||
border-radius: var(--border-radius-lg);
|
||||
border: 1px solid var(--border-color);
|
||||
background-color: var(--fg-color);
|
||||
}
|
||||
}
|
||||
.workflow-container {
|
||||
width: 100%;
|
||||
height: calc(100vh - var(--navbar-height) - var(--page-head-height) - 65px);
|
||||
border-radius: var(--border-radius-lg);
|
||||
border: 1px solid var(--border-color);
|
||||
background-color: var(--fg-color);
|
||||
}
|
||||
|
||||
.drag-handle {
|
||||
background-color: var(--fg-color);
|
||||
cursor: grab !important;
|
||||
}
|
||||
.drag-handle {
|
||||
background-color: var(--fg-color);
|
||||
cursor: grab !important;
|
||||
}
|
||||
|
||||
:deep(.transition-edge) {
|
||||
stroke: var(--gray-600);
|
||||
stroke-width: 1.5px;
|
||||
}
|
||||
:deep(.transition-edge) {
|
||||
stroke: var(--gray-600);
|
||||
stroke-width: 1.5px;
|
||||
}
|
||||
|
||||
:deep(.selected) {
|
||||
.transition-edge {
|
||||
stroke: var(--primary);
|
||||
stroke-width: 2px;
|
||||
}
|
||||
:deep(.selected) {
|
||||
.transition-edge {
|
||||
stroke: var(--primary);
|
||||
stroke-width: 2px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -17,19 +17,18 @@ let properties = computed(() => {
|
|||
});
|
||||
if (store.workflow.selected && "action" in store.workflow.selected.data) {
|
||||
title.value = "Transition Properties";
|
||||
return store.transitionfields.filter(df => {
|
||||
if (in_list(["action", "allowed", "allow_self_approval", "condition"], df.fieldname)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
return store.transitionfields.filter((df) =>
|
||||
in_list(["action", "allowed", "allow_self_approval", "condition"], df.fieldname)
|
||||
);
|
||||
} else if (store.workflow.selected && "state" in store.workflow.selected.data) {
|
||||
title.value = "State Properties";
|
||||
let allow_edit = store.statefields.find(df => df.fieldname == "allow_edit");
|
||||
store.statefields = store.statefields.filter(df => df.fieldname != "allow_edit");
|
||||
let allow_edit = store.statefields.find((df) => df.fieldname == "allow_edit");
|
||||
store.statefields = store.statefields.filter(
|
||||
(df) => !in_list(["allow_edit", "workflow_builder_id"], df.fieldname)
|
||||
);
|
||||
store.statefields.splice(2, 0, allow_edit);
|
||||
|
||||
return store.statefields.filter(df => {
|
||||
return store.statefields.filter((df) => {
|
||||
if (df.fieldname == "doc_status") {
|
||||
df.options = ["Draft", "Submitted", "Cancelled"];
|
||||
df.description = "";
|
||||
|
|
@ -41,12 +40,9 @@ let properties = computed(() => {
|
|||
});
|
||||
}
|
||||
title.value = "Workflow Details";
|
||||
return store.workflowfields.filter(df => {
|
||||
if (in_list(["states", "transitions", "workflow_data", "workflow_name"], df.fieldname)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
return store.workflowfields.filter(
|
||||
(df) => !in_list(["states", "transitions", "workflow_data", "workflow_name"], df.fieldname)
|
||||
);
|
||||
});
|
||||
</script>
|
||||
|
||||
|
|
|
|||
|
|
@ -6,8 +6,8 @@ import { useStore } from "../store";
|
|||
const props = defineProps({
|
||||
node: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const isValidConnection = ({ source, target }) => {
|
||||
|
|
@ -25,26 +25,30 @@ let store = useStore();
|
|||
const { findNode } = useVueFlow();
|
||||
watch(
|
||||
() => findNode(props.node.id)?.selected,
|
||||
val => {
|
||||
(val) => {
|
||||
if (val) store.workflow.selected = props.node;
|
||||
}
|
||||
);
|
||||
|
||||
let label = computed(() => findNode(props.node.id)?.data?.state);
|
||||
|
||||
watch(() => props.node.data, () => {
|
||||
store.ref_history.commit();
|
||||
}, { deep: true });
|
||||
watch(
|
||||
() => props.node.data,
|
||||
() => {
|
||||
store.ref_history.commit();
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="node" tabindex="0" @click.stop="store.workflow.selected = node">
|
||||
<div class="node" tabindex="0" @click.stop>
|
||||
<div v-if="label" class="node-label">{{ label }}</div>
|
||||
<div v-else class="node-placeholder text-muted">{{ __("No Label") }}</div>
|
||||
<Handle
|
||||
v-for="handle in ['top', 'right', 'bottom', 'left']"
|
||||
class="handle"
|
||||
:style="{ [handle]: '-12px'}"
|
||||
:style="{ [handle]: '-12px' }"
|
||||
type="source"
|
||||
:position="handle"
|
||||
:id="handle"
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ const props = defineProps({
|
|||
targetNode: { type: Object, required: true },
|
||||
markerEnd: { type: String, required: false },
|
||||
selected: { type: Boolean, required: false },
|
||||
data: { type: Object, required: false }
|
||||
data: { type: Object, required: false },
|
||||
});
|
||||
|
||||
let marker_end = {
|
||||
|
|
@ -28,7 +28,7 @@ let marker_end = {
|
|||
width: 15,
|
||||
height: 15,
|
||||
strokeWidth: 1.5,
|
||||
color: "#687178"
|
||||
color: "#687178",
|
||||
};
|
||||
|
||||
let marker_end_primary = {
|
||||
|
|
@ -36,12 +36,12 @@ let marker_end_primary = {
|
|||
width: 11,
|
||||
height: 11,
|
||||
strokeWidth: 1.7,
|
||||
color: "#171717"
|
||||
color: "#171717",
|
||||
};
|
||||
|
||||
watch(
|
||||
() => props.selected,
|
||||
val => {
|
||||
(val) => {
|
||||
let target_is_action = props.target?.startsWith("action-");
|
||||
val && selectAction(target_is_action);
|
||||
if (target_is_action) return;
|
||||
|
|
@ -53,7 +53,7 @@ watch(
|
|||
function selectAction(target_is_action) {
|
||||
let action = target_is_action ? props.targetNode : props.sourceNode;
|
||||
if (action.selected) return;
|
||||
getSelectedNodes.value?.forEach(node => (node.selected = false));
|
||||
getSelectedNodes.value?.forEach((node) => (node.selected = false));
|
||||
nextTick(() => (action.selected = true));
|
||||
}
|
||||
|
||||
|
|
@ -68,13 +68,13 @@ const d = computed(() => {
|
|||
sourcePosition: props.sourcePosition,
|
||||
targetPosition: props.targetPosition,
|
||||
targetNode: props.targetNode,
|
||||
borderRadius: 30
|
||||
borderRadius: 30,
|
||||
});
|
||||
});
|
||||
</script>
|
||||
<script>
|
||||
export default {
|
||||
inheritAttrs: false
|
||||
inheritAttrs: false,
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
|
|
@ -85,7 +85,7 @@ export default {
|
|||
:style="{
|
||||
transform: `translate(-50%, -50%) translate(${d[1]}px, ${d[2]}px)`,
|
||||
borderColor: selected ? 'var(--primary)' : 'var(--gray-600)',
|
||||
borderWidth: selected ? '1.5px' : '1px'
|
||||
borderWidth: selected ? '1.5px' : '1px',
|
||||
}"
|
||||
class="access nodrag nopan"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -53,15 +53,13 @@ export const useStore = defineStore("workflow-builder-store", () => {
|
|||
}));
|
||||
}
|
||||
|
||||
if (
|
||||
workflow_doc.value.workflow_data &&
|
||||
typeof workflow_doc.value.workflow_data == "string" &&
|
||||
JSON.parse(workflow_doc.value.workflow_data).length
|
||||
) {
|
||||
workflow.value.elements = JSON.parse(workflow_doc.value.workflow_data);
|
||||
} else {
|
||||
workflow.value.elements = get_workflow_elements(workflow_doc.value);
|
||||
}
|
||||
const workflow_data =
|
||||
(workflow_doc.value.workflow_data &&
|
||||
typeof workflow_doc.value.workflow_data == "string" &&
|
||||
JSON.parse(workflow_doc.value.workflow_data)) ||
|
||||
[];
|
||||
|
||||
workflow.value.elements = get_workflow_elements(workflow_doc.value, workflow_data);
|
||||
|
||||
setup_undo_redo();
|
||||
setup_breadcrumbs();
|
||||
|
|
@ -79,11 +77,12 @@ export const useStore = defineStore("workflow-builder-store", () => {
|
|||
doc.states = get_updated_states();
|
||||
doc.transitions = get_updated_transitions();
|
||||
validate_workflow(doc);
|
||||
clean_workflow_data();
|
||||
doc.workflow_data = JSON.stringify(workflow.value.elements);
|
||||
const workflow_data = clean_workflow_data();
|
||||
doc.workflow_data = JSON.stringify(workflow_data);
|
||||
await frappe.call("frappe.client.save", { doc });
|
||||
frappe.toast("Workflow updated successfully");
|
||||
fetch();
|
||||
// this change to keep the state as it is when saving
|
||||
//fetch();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
} finally {
|
||||
|
|
@ -103,7 +102,28 @@ export const useStore = defineStore("workflow-builder-store", () => {
|
|||
}
|
||||
|
||||
function clean_workflow_data() {
|
||||
workflow.value.elements.forEach((el) => (el.selected = false));
|
||||
return workflow.value.elements.map((el) => {
|
||||
const {
|
||||
selected,
|
||||
dragging,
|
||||
resizing,
|
||||
data,
|
||||
events,
|
||||
initialized,
|
||||
sourceNode,
|
||||
targetNode,
|
||||
...obj
|
||||
} = el;
|
||||
|
||||
if (el.type == "action") {
|
||||
obj.data = {
|
||||
from_id: data.from_id,
|
||||
to_id: data.to_id,
|
||||
};
|
||||
}
|
||||
|
||||
return obj;
|
||||
});
|
||||
}
|
||||
|
||||
function setup_breadcrumbs() {
|
||||
|
|
@ -122,18 +142,15 @@ export const useStore = defineStore("workflow-builder-store", () => {
|
|||
Submitted: 1,
|
||||
Cancelled: 2,
|
||||
};
|
||||
let docfield = "Workflow Document State";
|
||||
let df = frappe.model.get_new_doc(docfield);
|
||||
df.name = frappe.utils.get_random(8);
|
||||
Object.assign(df, data);
|
||||
df.doc_status = doc_status_map[data.doc_status];
|
||||
return df;
|
||||
data.doc_status = doc_status_map[data.doc_status];
|
||||
return data;
|
||||
}
|
||||
|
||||
function get_updated_states() {
|
||||
let states = [];
|
||||
workflow.value.elements.forEach((element) => {
|
||||
if (element.type == "state") {
|
||||
element.data.workflow_builder_id = element.id;
|
||||
states.push(get_state_df(element.data));
|
||||
}
|
||||
});
|
||||
|
|
@ -141,11 +158,7 @@ export const useStore = defineStore("workflow-builder-store", () => {
|
|||
}
|
||||
|
||||
function get_transition_df(data) {
|
||||
let docfield = "Workflow Transition";
|
||||
let df = frappe.model.get_new_doc(docfield);
|
||||
df.name = frappe.utils.get_random(8);
|
||||
Object.assign(df, data);
|
||||
return df;
|
||||
return data;
|
||||
}
|
||||
|
||||
function get_updated_transitions() {
|
||||
|
|
@ -154,6 +167,7 @@ export const useStore = defineStore("workflow-builder-store", () => {
|
|||
|
||||
workflow.value.elements.forEach((element) => {
|
||||
if (element.type == "action") {
|
||||
element.data.workflow_builder_id = element.id;
|
||||
actions.push(element);
|
||||
}
|
||||
});
|
||||
|
|
@ -181,21 +195,21 @@ export const useStore = defineStore("workflow-builder-store", () => {
|
|||
return transitions;
|
||||
}
|
||||
|
||||
let undo_redo_keyboard_event = onKeyDown(true, (e) => {
|
||||
if (!ref_history.value) return;
|
||||
if (e.ctrlKey || e.metaKey) {
|
||||
if (e.key === "z" && !e.shiftKey && ref_history.value.canUndo) {
|
||||
ref_history.value.undo();
|
||||
} else if (e.key === "z" && e.shiftKey && ref_history.value.canRedo) {
|
||||
ref_history.value.redo();
|
||||
let undo_redo_keyboard_event = () =>
|
||||
onKeyDown(true, (e) => {
|
||||
if (!ref_history.value) return;
|
||||
if (e.ctrlKey || e.metaKey) {
|
||||
if (e.key === "z" && !e.shiftKey && ref_history.value.canUndo) {
|
||||
ref_history.value.undo();
|
||||
} else if (e.key === "z" && e.shiftKey && ref_history.value.canRedo) {
|
||||
ref_history.value.redo();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
function setup_undo_redo() {
|
||||
ref_history.value = useManualRefHistory(workflow, { clone: true });
|
||||
|
||||
undo_redo_keyboard_event;
|
||||
undo_redo_keyboard_event();
|
||||
}
|
||||
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -1,44 +1,112 @@
|
|||
export function get_workflow_elements(workflow) {
|
||||
export function get_workflow_elements(workflow, workflow_data) {
|
||||
let elements = [];
|
||||
let states = {};
|
||||
let actions = {};
|
||||
let transitions = {};
|
||||
|
||||
let x = 150;
|
||||
let y = 100;
|
||||
|
||||
function state_obj(id, data) {
|
||||
let state = {
|
||||
id: id.toString(),
|
||||
type: "state",
|
||||
position: { x: x, y: y },
|
||||
data: data,
|
||||
};
|
||||
if (!states[data.state]) {
|
||||
states[data.state] = [id, { x: x, y: y }];
|
||||
workflow_data.forEach((node) => {
|
||||
if (node.type == "state") {
|
||||
states[node.id] = node;
|
||||
} else if (node.type == "action") {
|
||||
actions[node.id] = node;
|
||||
} else if (node.type == "transition") {
|
||||
transitions[`edge-${node.source}-${node.target}`] = node;
|
||||
|
||||
if (node.source.startsWith("action-")) {
|
||||
const action = actions[node.source];
|
||||
if (!action.data.to_id) {
|
||||
action.data.to_id = node.target;
|
||||
}
|
||||
node.sourceNode = action;
|
||||
node.targetNode = states[node.target];
|
||||
} else {
|
||||
const action = actions[node.target];
|
||||
if (!action.data.from_id) {
|
||||
action.data.from_id = node.source;
|
||||
}
|
||||
node.targetNode = action;
|
||||
node.sourceNode = states[node.source];
|
||||
}
|
||||
}
|
||||
return state;
|
||||
});
|
||||
|
||||
function state_obj(id, data) {
|
||||
let state = states[id];
|
||||
|
||||
if (state) {
|
||||
state.data = data;
|
||||
} else {
|
||||
state = {
|
||||
id: id.toString(),
|
||||
type: "state",
|
||||
position: { x, y },
|
||||
data,
|
||||
};
|
||||
}
|
||||
|
||||
Object.assign(state, {
|
||||
initialized: true,
|
||||
selected: false,
|
||||
dragging: false,
|
||||
resizing: false,
|
||||
});
|
||||
return (states[id] = state);
|
||||
}
|
||||
|
||||
function action_obj(id, data, position) {
|
||||
return {
|
||||
id: "action-" + id,
|
||||
type: "action",
|
||||
position: position,
|
||||
data: data,
|
||||
};
|
||||
let action = actions[id];
|
||||
|
||||
if (action) {
|
||||
data.from_id = action.data.from_id;
|
||||
(data.to_id = action.data.to_id), (action.data = data);
|
||||
} else {
|
||||
action = {
|
||||
id,
|
||||
type: "action",
|
||||
position,
|
||||
data,
|
||||
};
|
||||
}
|
||||
|
||||
Object.assign(action, {
|
||||
initialized: true,
|
||||
selected: false,
|
||||
dragging: false,
|
||||
resizing: false,
|
||||
});
|
||||
return (actions[id] = action);
|
||||
}
|
||||
|
||||
function transition_obj(id, source, target) {
|
||||
return {
|
||||
id: "edge-" + id,
|
||||
type: "transition",
|
||||
source: source.toString(),
|
||||
target: target.toString(),
|
||||
sourceHandle: "right",
|
||||
targetHandle: "left",
|
||||
updatable: true,
|
||||
animated: true,
|
||||
};
|
||||
let transition = transitions[id];
|
||||
|
||||
if (!transition) {
|
||||
transition = {
|
||||
id,
|
||||
type: "transition",
|
||||
source: source.toString(),
|
||||
target: target.toString(),
|
||||
sourceHandle: "right",
|
||||
targetHandle: "left",
|
||||
updatable: true,
|
||||
animated: true,
|
||||
};
|
||||
}
|
||||
|
||||
Object.assign(transition, {
|
||||
initialized: true,
|
||||
selected: false,
|
||||
dragging: false,
|
||||
resizing: false,
|
||||
});
|
||||
return (transitions[id] = transition);
|
||||
}
|
||||
|
||||
let state_id = Math.max(...workflow.states.map((state) => state.workflow_builder_id || 0));
|
||||
|
||||
workflow.states.forEach((state, i) => {
|
||||
x += 400;
|
||||
let doc_status_map = {
|
||||
|
|
@ -46,38 +114,51 @@ export function get_workflow_elements(workflow) {
|
|||
1: "Submitted",
|
||||
2: "Cancelled",
|
||||
};
|
||||
|
||||
const id = state.workflow_builder_id || ++state_id;
|
||||
elements.push(
|
||||
state_obj(i + 1, {
|
||||
state: state.state,
|
||||
state_obj(id, {
|
||||
...state,
|
||||
doc_status: doc_status_map[state.doc_status],
|
||||
allow_edit: state.allow_edit,
|
||||
update_field: state.update_field,
|
||||
update_value: state.update_value,
|
||||
is_optional_state: state.is_optional_state,
|
||||
next_action_email_template: state.next_action_email_template,
|
||||
message: state.message,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
let action_id = Math.max(
|
||||
...workflow.transitions.map(
|
||||
(transition) => transition.workflow_builder_id?.replace("action-", "") || 0
|
||||
)
|
||||
);
|
||||
|
||||
workflow.transitions.forEach((transition, i) => {
|
||||
let source = states[transition.state];
|
||||
let target = states[transition.next_state];
|
||||
let position = { x: source[1].x + 250, y: y + 20 };
|
||||
const id = transition.workflow_builder_id || "action-" + ++action_id;
|
||||
let action = actions[id];
|
||||
let source, target;
|
||||
|
||||
if (action && action.data.from_id && action.data.to_id) {
|
||||
source = states[action.data.from_id];
|
||||
target = states[action.data.to_id];
|
||||
} else {
|
||||
source = Object.values(states).filter(
|
||||
(state) => state.data?.state == transition.state
|
||||
)[0];
|
||||
target = Object.values(states).filter(
|
||||
(state) => state.data?.state == transition.next_state
|
||||
)[0];
|
||||
}
|
||||
|
||||
let position = { x: source.position.x + 250, y: y + 20 };
|
||||
let data = {
|
||||
...transition,
|
||||
from_id: source.id,
|
||||
to_id: target.id,
|
||||
from: transition.state,
|
||||
to: transition.next_state,
|
||||
action: transition.action,
|
||||
allowed: transition.allowed,
|
||||
allow_self_approval: transition.allow_self_approval,
|
||||
condition: transition.condition,
|
||||
};
|
||||
|
||||
let action = "action-" + (i + 1);
|
||||
|
||||
elements.push(action_obj(i + 1, data, position));
|
||||
elements.push(transition_obj(source[0] + "-" + action, source[0], action));
|
||||
elements.push(transition_obj(action + "-" + target[0], action, target[0]));
|
||||
elements.push(action_obj(id, data, position));
|
||||
elements.push(transition_obj("edge-" + source.id + "-" + id, source.id, id));
|
||||
elements.push(transition_obj("edge-" + id + "-" + target.id, id, target.id));
|
||||
});
|
||||
|
||||
return elements;
|
||||
|
|
|
|||
|
|
@ -197,6 +197,34 @@ frappe.ui.form.on("Workflow", {
|
|||
});
|
||||
|
||||
frappe.ui.form.on("Workflow Document State", {
|
||||
state: function (_, cdt, cdn) {
|
||||
var row = locals[cdt][cdn];
|
||||
delete row.workflow_builder_id;
|
||||
},
|
||||
|
||||
states_remove: function (frm) {
|
||||
frm.trigger("get_orphaned_states_and_count").then(() => {
|
||||
frm.trigger("render_state_table");
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
frappe.ui.form.on("Workflow Transition", {
|
||||
state: function (_, cdt, cdn) {
|
||||
var row = locals[cdt][cdn];
|
||||
delete row.workflow_builder_id;
|
||||
},
|
||||
|
||||
next_state: function (_, cdt, cdn) {
|
||||
var row = locals[cdt][cdn];
|
||||
delete row.workflow_builder_id;
|
||||
},
|
||||
|
||||
action: function (_, cdt, cdn) {
|
||||
var row = locals[cdt][cdn];
|
||||
delete row.workflow_builder_id;
|
||||
},
|
||||
|
||||
states_remove: function (frm) {
|
||||
frm.trigger("get_orphaned_states_and_count").then(() => {
|
||||
frm.trigger("render_state_table");
|
||||
|
|
|
|||
|
|
@ -15,7 +15,8 @@
|
|||
"next_action_email_template",
|
||||
"allow_edit",
|
||||
"section_break_9",
|
||||
"message"
|
||||
"message",
|
||||
"workflow_builder_id"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
|
|
@ -88,6 +89,12 @@
|
|||
{
|
||||
"fieldname": "section_break_9",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "workflow_builder_id",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 1,
|
||||
"label": "Workflow Builder ID"
|
||||
}
|
||||
],
|
||||
"idx": 1,
|
||||
|
|
|
|||
|
|
@ -14,7 +14,8 @@
|
|||
"conditions",
|
||||
"condition",
|
||||
"column_break_7",
|
||||
"example"
|
||||
"example",
|
||||
"workflow_build_id"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
|
|
@ -84,6 +85,12 @@
|
|||
"fieldtype": "HTML",
|
||||
"label": "Example",
|
||||
"options": "<pre><code>doc.grand_total > 0</code></pre>\n\n<p>Conditions should be written in simple Python. Please use properties available in the form only.</p>\n<p>Allowed functions: \n</p><ul>\n<li>frappe.db.get_value</li>\n<li>frappe.db.get_list</li>\n<li>frappe.session</li>\n<li>frappe.utils.now_datetime</li>\n<li>frappe.utils.get_datetime</li>\n<li>frappe.utils.add_to_date</li>\n<li>frappe.utils.now</li>\n</ul>\n<p>Example: </p><pre><code>doc.creation > frappe.utils.add_to_date(frappe.utils.now_datetime(), days=-5, as_string=True, as_datetime=True) </code></pre><p></p>"
|
||||
},
|
||||
{
|
||||
"fieldname": "workflow_builder_id",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 1,
|
||||
"label": "Workflow Builder ID"
|
||||
}
|
||||
],
|
||||
"idx": 1,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue