From 748e2ebb2b82621b05187c5769468a8d110e4240 Mon Sep 17 00:00:00 2001 From: hrwx Date: Mon, 15 Nov 2021 13:30:13 +0000 Subject: [PATCH 1/2] feat: multistep webforms --- frappe/public/js/frappe/web_form/web_form.js | 175 ++++++++++++++++++ frappe/website/doctype/web_form/web_form.json | 9 +- 2 files changed, 183 insertions(+), 1 deletion(-) diff --git a/frappe/public/js/frappe/web_form/web_form.js b/frappe/public/js/frappe/web_form/web_form.js index 964a8ad0bb..8c975136dd 100644 --- a/frappe/public/js/frappe/web_form/web_form.js +++ b/frappe/public/js/frappe/web_form/web_form.js @@ -9,6 +9,7 @@ export default class WebForm extends frappe.ui.FieldGroup { frappe.web_form = this; frappe.web_form.events = {}; Object.assign(frappe.web_form.events, EventEmitterMixin); + this.current_section = 0; } prepare(web_form_doc, doc) { @@ -19,12 +20,16 @@ export default class WebForm extends frappe.ui.FieldGroup { make() { super.make(); + this.set_sections(); this.set_field_values(); + this.setup_listeners(); if (this.introduction_text) this.set_form_description(this.introduction_text); if (this.allow_print && !this.is_new) this.setup_print_button(); if (this.allow_delete && !this.is_new) this.setup_delete_button(); if (this.is_new) this.setup_cancel_button(); this.setup_primary_action(); + this.setup_previous_next_button(); + this.toggle_section(); $(".link-btn").remove(); // webform client script @@ -40,6 +45,79 @@ export default class WebForm extends frappe.ui.FieldGroup { }; } + setup_listeners() { + // Event listener for triggering Save/Next button for Multi Step Forms + // Do not use `on` event here since that can be used by user which will render this function useless + // setTimeout has 200ms delay so that all the base_control triggers for the fields have been run + let me = this; + + if (!me.is_multi_step_form) { + return; + } + + for (let field of $(".input-with-feedback")) { + $(field).change((e) => { + setTimeout(() => { + e.stopPropagation(); + me.toggle_buttons(); + }, 200); + }); + } + } + + set_sections() { + if (this.sections.length) return; + + this.sections = $(`.form-section`); + } + + setup_previous_next_button() { + let me = this; + + if (!me.is_multi_step_form) { + return; + } + + $('.web-form-footer').after(` + + `); + + $('.btn-previous').on('click', function () { + let is_validated = me.validate_section(); + + if (!is_validated) return; + + for (let idx = me.current_section; idx < me.sections.length; idx--) { + let is_empty = me.is_previous_section_empty(idx); + me.current_section = me.current_section > 0 ? me.current_section - 1 : me.current_section; + + if (!is_empty) { + break + } + } + me.toggle_section(); + }); + + $('.btn-next').on('click', function () { + let is_validated = me.validate_section(); + + if (!is_validated) return; + + for (let idx = me.current_section; idx < me.sections.length; idx++) { + let is_empty = me.is_next_section_empty(idx); + me.current_section = me.current_section < me.sections.length ? me.current_section + 1 : me.current_section; + + if (!is_empty) { + break + } + } + me.toggle_section(); + }); + } + set_field_values() { if (this.doc.name) this.set_values(this.doc); else return; @@ -104,6 +182,103 @@ export default class WebForm extends frappe.ui.FieldGroup { ); } + validate_section() { + if (this.allow_incomplete) return true; + + let fields = $(`.form-section:eq(${this.current_section}) .form-control`); + let errors = [] + + for (let field of fields) { + let fieldname = $(field).attr("data-fieldname"); + if (!fieldname) continue; + + field = this.fields_dict[fieldname]; + + if (field.get_value) { + let value = field.get_value(); + if (field.df.reqd && is_null(typeof value === 'string' ? strip_html(value) : value)) errors.push(__(field.df.label)); + + if (field.df.reqd && field.df.fieldtype === 'Text Editor' && is_null(strip_html(cstr(value)))) errors.push(__(field.df.label)); + } + } + + if (errors.length) { + frappe.msgprint({ + title: __('Missing Values Required'), + message: __('Following fields have missing values:') + + '

', + indicator: 'orange' + }); + return false; + } + + return true; + } + + toggle_section() { + if (!this.is_multi_step_form) return; + + this.toggle_previous_button(); + this.hide_sections(); + this.show_section(); + this.toggle_buttons(); + } + + toggle_buttons() { + for (let idx = this.current_section; idx < this.sections.length; idx++) { + if (this.is_next_section_empty(idx)) { + this.show_save_and_hide_next_button(); + } else { + this.show_next_and_hide_save_button(); + break; + } + } + } + + is_next_section_empty(section) { + if (section + 1 > this.sections.length) return true; + + let _section = $(`.form-section:eq(${section + 1})`); + let visible_controls = _section.find(".frappe-control:not(.hide-control)"); + + return !visible_controls.length ? true : false; + } + + is_previous_section_empty(section) { + if (section - 1 > this.sections.length) return true; + + let _section = $(`.form-section:eq(${section - 1})`); + let visible_controls = _section.find(".frappe-control:not(.hide-control)"); + + return !visible_controls.length ? true : false; + } + + show_save_and_hide_next_button() { + $('.btn-next').hide(); + $('.web-form-footer').show(); + } + + show_next_and_hide_save_button() { + $('.btn-next').show(); + $('.web-form-footer').hide(); + } + + toggle_previous_button() { + this.current_section == 0 ? $('.btn-previous').hide() : $('.btn-previous').show(); + } + + show_section() { + $(`.form-section:eq(${this.current_section})`).show(); + } + + hide_sections() { + for (let idx=0; idx < this.sections.length; idx++) { + if (idx !== this.current_section) { + $(`.form-section:eq(${idx})`).hide(); + } + } + } + save() { let is_new = this.is_new; if (this.validate && !this.validate()) { diff --git a/frappe/website/doctype/web_form/web_form.json b/frappe/website/doctype/web_form/web_form.json index 9199183a65..260489fe25 100644 --- a/frappe/website/doctype/web_form/web_form.json +++ b/frappe/website/doctype/web_form/web_form.json @@ -11,6 +11,7 @@ "module", "column_break_4", "is_standard", + "is_multi_step_form", "published", "login_required", "route_to_success_link", @@ -355,13 +356,19 @@ "fieldname": "apply_document_permissions", "fieldtype": "Check", "label": "Apply Document Permissions" + }, + { + "default": "0", + "fieldname": "is_multi_step_form", + "fieldtype": "Check", + "label": "Is Multi Step Form" } ], "has_web_view": 1, "icon": "icon-edit", "is_published_field": "published", "links": [], - "modified": "2020-08-07 13:12:03.945686", + "modified": "2021-11-15 14:12:44.624573", "modified_by": "Administrator", "module": "Website", "name": "Web Form", From 1b906d8c78ab7a9f2a556a53d547952473bf32ae Mon Sep 17 00:00:00 2001 From: hrwx Date: Sun, 19 Dec 2021 00:58:17 +0000 Subject: [PATCH 2/2] feat: test cases for webform --- cypress/integration/web_form.js | 29 +++++++++++++++++++++++++++++ frappe/tests/ui_test_helpers.py | 7 +++++++ 2 files changed, 36 insertions(+) create mode 100644 cypress/integration/web_form.js diff --git a/cypress/integration/web_form.js b/cypress/integration/web_form.js new file mode 100644 index 0000000000..acdb4f6198 --- /dev/null +++ b/cypress/integration/web_form.js @@ -0,0 +1,29 @@ +context('Web Form', () => { + before(() => { + cy.login('Administrator', 'frappe') + }); + + it('Navigate and Submit a WebForm', () => { + cy.visit('/update-profile'); + cy.get_field('last_name', 'Data').type('_Test User', {force: true}).wait(200); + cy.get('.web-form-actions .btn-primary').click(); + cy.wait(500); + cy.get('.modal.show > .modal-dialog').should('be.visible'); + }); + + it('Timeline should have submit and cancel activity information', () => { + cy.call('frappe.tests.ui_test_helpers.update_webform_to_multistep').then(r => { + cy.visit('/update-profile'); + cy.get_field('last_name', 'Data').type('_Test User', {force: true}).wait(200); + cy.get('.btn-next').should('be.visible'); + cy.get('.web-form-footer .btn-primary').should('not.be.visible'); + cy.get('.btn-next').click(); + cy.get('.btn-previous').should('be.visible'); + cy.get('.btn-next').should('not.be.visible'); + cy.get('.web-form-footer .btn-primary').should('be.visible'); + cy.get('.web-form-actions .btn-primary').click(); + cy.wait(500); + cy.get('.modal.show > .modal-dialog').should('be.visible'); + }); + }); +}); diff --git a/frappe/tests/ui_test_helpers.py b/frappe/tests/ui_test_helpers.py index 3e9b3519bc..075bb9fcec 100644 --- a/frappe/tests/ui_test_helpers.py +++ b/frappe/tests/ui_test_helpers.py @@ -244,3 +244,10 @@ def create_topic_and_reply(web_page): }) reply.save() + + +@frappe.whitelist() +def update_webform_to_multistep(): + doc = frappe.get_doc("Web Form", "edit-profile") + doc.is_multi_step_form = 1 + doc.save()