@@ -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"
>
diff --git a/frappe/public/js/workflow_builder/store.js b/frappe/public/js/workflow_builder/store.js
index 781e53e555..aaccc21b31 100644
--- a/frappe/public/js/workflow_builder/store.js
+++ b/frappe/public/js/workflow_builder/store.js
@@ -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 {
diff --git a/frappe/public/js/workflow_builder/utils.js b/frappe/public/js/workflow_builder/utils.js
index d21c7be587..6f1fcb9c76 100644
--- a/frappe/public/js/workflow_builder/utils.js
+++ b/frappe/public/js/workflow_builder/utils.js
@@ -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;
diff --git a/frappe/workflow/doctype/workflow/workflow.js b/frappe/workflow/doctype/workflow/workflow.js
index 91a71f5ac1..21ed3d6134 100644
--- a/frappe/workflow/doctype/workflow/workflow.js
+++ b/frappe/workflow/doctype/workflow/workflow.js
@@ -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");
diff --git a/frappe/workflow/doctype/workflow_document_state/workflow_document_state.json b/frappe/workflow/doctype/workflow_document_state/workflow_document_state.json
index 1bfe74d100..d3d325adf4 100644
--- a/frappe/workflow/doctype/workflow_document_state/workflow_document_state.json
+++ b/frappe/workflow/doctype/workflow_document_state/workflow_document_state.json
@@ -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,
diff --git a/frappe/workflow/doctype/workflow_transition/workflow_transition.json b/frappe/workflow/doctype/workflow_transition/workflow_transition.json
index 695332ebff..8379c1a168 100644
--- a/frappe/workflow/doctype/workflow_transition/workflow_transition.json
+++ b/frappe/workflow/doctype/workflow_transition/workflow_transition.json
@@ -14,7 +14,8 @@
"conditions",
"condition",
"column_break_7",
- "example"
+ "example",
+ "workflow_build_id"
],
"fields": [
{
@@ -84,6 +85,12 @@
"fieldtype": "HTML",
"label": "Example",
"options": "doc.grand_total > 0
\n\nConditions should be written in simple Python. Please use properties available in the form only.
\nAllowed functions: \n
\n- frappe.db.get_value
\n- frappe.db.get_list
\n- frappe.session
\n- frappe.utils.now_datetime
\n- frappe.utils.get_datetime
\n- frappe.utils.add_to_date
\n- frappe.utils.now
\n
\nExample:
doc.creation > frappe.utils.add_to_date(frappe.utils.now_datetime(), days=-5, as_string=True, as_datetime=True)
"
+ },
+ {
+ "fieldname": "workflow_builder_id",
+ "fieldtype": "Data",
+ "hidden": 1,
+ "label": "Workflow Builder ID"
}
],
"idx": 1,