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:
Sambasiva Suda 2023-10-03 17:13:51 +05:30 committed by GitHub
parent c519239ff2
commit 53c795f4b8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 531 additions and 424 deletions

View file

@ -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>

View file

@ -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>

View file

@ -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"

View file

@ -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"
>

View file

@ -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 {

View file

@ -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;

View file

@ -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");

View file

@ -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,

View file

@ -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 &gt; 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 &gt; 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,