diff --git a/frappe/public/js/frappe/ui/dialog.js b/frappe/public/js/frappe/ui/dialog.js index 0f4332a91a..d77481f8b9 100644 --- a/frappe/public/js/frappe/ui/dialog.js +++ b/frappe/public/js/frappe/ui/dialog.js @@ -42,6 +42,8 @@ frappe.ui.Dialog = class Dialog extends frappe.ui.FieldGroup { this.body = this.$body.get(0); this.$message = $('').appendTo(this.modal_body); this.header = this.$wrapper.find(".modal-header"); + this.buttons = this.header.find('.buttons'); + this.set_indicator(); // make fields (if any) super.make(); @@ -164,6 +166,11 @@ frappe.ui.Dialog = class Dialog extends frappe.ui.FieldGroup { set_title(t) { this.$wrapper.find(".modal-title").html(t); } + set_indicator() { + if (this.indicator) { + this.header.find('.indicator').removeClass().addClass('indicator ' + this.indicator); + } + } show() { // show it if ( this.animate ) { diff --git a/frappe/public/js/frappe/ui/messages.js b/frappe/public/js/frappe/ui/messages.js index c6bc994a9d..ab20feeedd 100644 --- a/frappe/public/js/frappe/ui/messages.js +++ b/frappe/public/js/frappe/ui/messages.js @@ -53,6 +53,33 @@ frappe.confirm = function(message, ifyes, ifno) { return d; } +frappe.warn = function(title, message_html, proceed_action, primary_label) { + const d = new frappe.ui.Dialog({ + title: title, + indicator: 'red', + fields: [ + { + fieldtype: 'HTML', + fieldname: 'warning_message', + options: `
${message_html}
` + } + ], + primary_action_label: primary_label, + primary_action: () => { + if (proceed_action) proceed_action(); + d.hide(); + }, + secondary_action_label: __("Cancel"), + }); + + d.buttons.find('.btn-primary').removeClass('btn-primary').addClass('btn-danger'); + const modal_footer = $(``).insertAfter($(d.modal_body)); + modal_footer.html(d.buttons); + + d.show(); + return d; +}; + frappe.prompt = function(fields, callback, title, primary_label) { if (typeof fields === "string") { fields = [{ diff --git a/frappe/public/js/frappe/utils/utils.js b/frappe/public/js/frappe/utils/utils.js index 24fa946fc4..f4dde5804f 100644 --- a/frappe/public/js/frappe/utils/utils.js +++ b/frappe/public/js/frappe/utils/utils.js @@ -292,6 +292,25 @@ Object.assign(frappe.utils, { return frappe.utils.guess_style(text, null, true); }, + get_indicator_color: function(state) { + return frappe.db.get_list('Workflow State', {filters: {name: state}, fields: ['name', 'style']}).then(res => { + const state = res[0]; + if (!state.style) { + return frappe.utils.guess_colour(state.name); + } + const style = state.style; + const colour_map = { + "Success": "green", + "Warning": "orange", + "Danger": "red", + "Primary": "blue", + }; + + return colour_map[style]; + }); + + }, + sort: function(list, key, compare_type, reverse) { if(!list || list.length < 2) return list || []; diff --git a/frappe/workflow/doctype/workflow/workflow.js b/frappe/workflow/doctype/workflow/workflow.js index 6e12f5fa46..aba6f6fe48 100644 --- a/frappe/workflow/doctype/workflow/workflow.js +++ b/frappe/workflow/doctype/workflow/workflow.js @@ -5,7 +5,35 @@ frappe.ui.form.on("Workflow", { frm.set_query("document_type", {"issingle": 0, "istable": 0}); }, refresh: function(frm) { + if (frm.doc.document_type) { + frm.add_custom_button(__('Go to {0} List', [frm.doc.document_type]), () => { + frappe.set_route('List', frm.doc.document_type); + }); + } + frm.events.update_field_options(frm); + frm.ignore_warning = frm.is_new() ? true : false; + + if (frm.is_new()) { + return; + } + + frm.states = null; + frm.trigger('make_state_table'); + frm.trigger('get_orphaned_states_and_count').then(() => { + frm.trigger('render_state_table'); + }); + }, + validate: (frm) => { + if (frm.ignore_warning) { + return; + } + return frm.trigger('get_orphaned_states_and_count').then(() => { + if (frm.states && frm.states.length) { + frappe.validated = false; + frm.trigger('create_warning_dialog'); + } + }); }, document_type: function(frm) { frm.events.update_field_options(frm); @@ -19,6 +47,101 @@ frappe.ui.form.on("Workflow", { frappe.meta.get_docfield("Workflow Document State", "update_field", frm.doc.name).options = [""].concat(resp); }) } - } -}) + }, + create_warning_dialog: function(frm) { + const warning_html = + `

+ ${__('Are you sure you want to save this document?')} +

+

${__(`There are documents which have workflow states that do not exist in this Workflow. + It is recommended that you add these states to the Workflow and change their states + before removing these states.`)} +

`; + const message_html = warning_html + frm.state_table_html; + let proceed_action = () => { + frm.ignore_warning = true; + frm.save(); + }; + + frappe.warn(__(`Worflow States Don't Exist`), message_html, proceed_action, __(`Save Anyway`)); + }, + set_table_html: function(frm) { + + const promises = frm.states.map(r => { + const state = r[frm.doc.workflow_state_field]; + return frappe.utils.get_indicator_color(state).then(color => { + return ` + +
+ ${r[frm.doc.workflow_state_field]} +
+ + ${r.count}`; + }); + }); + + Promise.all(promises).then(rows => { + const rows_html = rows.join(''); + frm.state_table_html = (` + + + + + + + + ${rows_html} + +
${__('State')}${__('Count')}
`); + }); + }, + get_orphaned_states_and_count: function(frm) { + let states_list = []; + frm.doc.states.map(state => states_list.push(state.state)); + return frappe.xcall('frappe.workflow.doctype.workflow.workflow.get_workflow_state_count', { + doctype: frm.doc.document_type, + workflow_state_field: frm.doc.workflow_state_field, + states: states_list + }).then(result => { + if (result && result.length) { + frm.states = result; + return frm.trigger('set_table_html'); + } + }); + }, + make_state_table: function(frm) { + const wrapper = frm.get_field('states').$wrapper; + if (frm.state_table) { + frm.state_table.empty(); + } + frm.state_table = $(`
`).insertAfter(wrapper); + }, + render_state_table: function(frm) { + if (frm.states && frm.states.length) { + const form_state_table_html = + `

+ ${'Document States that do not exist in your Workflow'} +

+ ${frm.state_table_html} +
`; + frm.state_table.html(form_state_table_html); + + $(frm.state_table).find('a.orphaned-state').on('click', (e) => { + const state = $(e.currentTarget).text(); + let filters = {}; + filters[frm.doc.workflow_state_field] = state; + frappe.set_route('List', frm.doc.document_type, filters); + }); + } + } + +}); + +frappe.ui.form.on("Workflow Document State", { + states_remove: function(frm) { + frm.trigger('get_orphaned_states_and_count').then(() => { + frm.trigger('render_state_table'); + }); + } +}); diff --git a/frappe/workflow/doctype/workflow/workflow.py b/frappe/workflow/doctype/workflow/workflow.py index 62e0b39b08..553f21dbab 100644 --- a/frappe/workflow/doctype/workflow/workflow.py +++ b/frappe/workflow/doctype/workflow/workflow.py @@ -59,7 +59,7 @@ class Workflow(Document): def update_doc_status(self): ''' - Checks if the docstatus of a state was updated. + Checks if the docstatus of a state was updated. If yes then the docstatus of the document with same state will be updated ''' doc_before_save = self.get_doc_before_save() @@ -112,3 +112,15 @@ class Workflow(Document): def get_fieldnames_for(doctype): return [f.fieldname for f in frappe.get_meta(doctype).fields \ if f.fieldname not in no_value_fields] + +@frappe.whitelist() +def get_workflow_state_count(doctype, workflow_state_field, states): + states = frappe.parse_json(states) + result = frappe.get_all( + doctype, + fields=[workflow_state_field, 'count(*) as count', 'docstatus'], + filters = {'workflow_state': ['not in', states]}, + group_by = workflow_state_field + ) + return [r for r in result if r[workflow_state_field]] +