From 3a943b5426b88e9b25b0f8462d0c5168be0b31ec Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Sat, 5 Jun 2021 14:51:07 +0530 Subject: [PATCH 1/9] refactor: Form Tour Class --- .../form_tour_step/form_tour_step.json | 4 +- frappe/public/js/frappe/form/form.js | 52 ++---------- frappe/public/js/frappe/form/form_tour.js | 79 +++++++++++++++++++ 3 files changed, 86 insertions(+), 49 deletions(-) create mode 100644 frappe/public/js/frappe/form/form_tour.js diff --git a/frappe/desk/doctype/form_tour_step/form_tour_step.json b/frappe/desk/doctype/form_tour_step/form_tour_step.json index a772a2498a..8b36b54db7 100644 --- a/frappe/desk/doctype/form_tour_step/form_tour_step.json +++ b/frappe/desk/doctype/form_tour_step/form_tour_step.json @@ -11,7 +11,7 @@ "position", "fieldname", "label", - "condition" + "next_step_condition" ], "fields": [ { @@ -73,7 +73,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2021-05-26 19:44:48.737453", + "modified": "2021-05-27 19:44:48.737453", "modified_by": "Administrator", "module": "Desk", "name": "Form Tour Step", diff --git a/frappe/public/js/frappe/form/form.js b/frappe/public/js/frappe/form/form.js index a24c6ab0d6..6ddbd089fa 100644 --- a/frappe/public/js/frappe/form/form.js +++ b/frappe/public/js/frappe/form/form.js @@ -12,6 +12,7 @@ import './script_manager'; import './script_helpers'; import './sidebar/form_sidebar'; import './footer/footer'; +import './form_tour'; frappe.ui.form.Controller = class FormController { constructor(opts) { @@ -152,6 +153,10 @@ frappe.ui.form.Form = class FrappeForm { parent: $('
').insertAfter(this.layout.wrapper.find('.form-message')) }); + this.tour = new frappe.ui.form.FormTour({ + frm: this + }); + // workflow state this.states = new frappe.ui.form.States({ frm: this @@ -1606,53 +1611,6 @@ frappe.ui.form.Form = class FrappeForm { }, 1000); } - show_tour(on_finish) { - const tour_info = frappe.tour[this.doctype]; - - if (!Array.isArray(tour_info)) { - return; - } - - const driver = new frappe.Driver({ - className: 'frappe-driver', - allowClose: false, - padding: 10, - overlayClickNext: true, - keyboardControl: true, - nextBtnText: 'Next', - prevBtnText: 'Previous', - opacity: 0.25 - }); - - this.layout.sections.forEach(section => section.collapse(false)); - - let steps = tour_info.map(step => { - let field = this.get_docfield(step.fieldname); - return { - element: `.frappe-control[data-fieldname='${step.fieldname}']`, - popover: { - title: step.title || field.label, - description: step.description, - position: step.position || 'bottom' - }, - onNext: () => { - const next_condition_satisfied = this.layout.evaluate_depends_on_value(step.next_step_condition || true); - if (!next_condition_satisfied) { - driver.preventMove(); - } - - if (!driver.hasNextStep()) { - on_finish && on_finish(); - } - } - }; - }); - - driver.defineSteps(steps); - frappe.router.on('change', () => driver.reset()); - driver.start(); - } - setup_docinfo_change_listener() { let doctype = this.doctype; let docname = this.docname; diff --git a/frappe/public/js/frappe/form/form_tour.js b/frappe/public/js/frappe/form/form_tour.js new file mode 100644 index 0000000000..e0cf694a5b --- /dev/null +++ b/frappe/public/js/frappe/form/form_tour.js @@ -0,0 +1,79 @@ +frappe.ui.form.FormTour = class FormTour { + constructor({ frm }) { + this.frm = frm; + this.driver_steps = []; + + this.init_driver(); + } + + init_driver() { + this.driver = new frappe.Driver({ + className: 'frappe-driver', + allowClose: false, + padding: 10, + overlayClickNext: true, + keyboardControl: true, + nextBtnText: 'Next', + prevBtnText: 'Previous', + opacity: 0.25 + }); + + frappe.router.on('change', () => this.driver.reset()); + this.frm.layout.sections.forEach(section => section.collapse(false)); + } + + async set_tour(tour_name, on_finish) { + this.tour = await frappe.db.get_doc('Form Tour', tour_name); + this.on_finish = on_finish; + this.build_steps(); + this.define_steps(); + } + + build_steps() { + this.tour.steps.forEach((step, idx) => { + const me = this; + const on_next = () => { + const next_condition_satisfied = this.frm.layout.evaluate_depends_on_value(step.next_step_condition || true); + + if (!next_condition_satisfied) { + me.driver.preventMove(); + } + + if (!me.driver.hasNextStep()) { + me.on_finish && me.on_finish(); + } + } + + const driver_step = this.get_step(step, on_next); + this.driver_steps.push(driver_step); + }); + } + + get_step(step_info, on_next) { + const field = this.frm.get_field(step_info.fieldname); + // if field is a child table field, `field` will be undefined + const element = field ? field.wrapper : `.frappe-control[data-fieldname='${step_info.fieldname}']`; + const title = step_info.title || field.df.label; + const description = step_info.description; + const position = step_info.position || 'bottom'; + return { + element, + popover: { title, description, position }, + onNext: on_next + }; + } + + define_steps(steps = []) { + if (steps.length == 0) { + steps = this.driver_steps; + } + this.driver.defineSteps(steps); + } + + start_tour() { + if (this.driver_steps.length == 0) { + return; + } + this.driver.start(); + } +}; \ No newline at end of file From 052ae6b53c80b5f8456dcc3022df3e39c04e8fbc Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Sat, 5 Jun 2021 15:08:40 +0530 Subject: [PATCH 2/9] feat: backward compatibility --- frappe/public/js/frappe/form/form_tour.js | 14 ++++++++++---- .../public/js/frappe/widgets/onboarding_widget.js | 14 ++++++++++---- 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/frappe/public/js/frappe/form/form_tour.js b/frappe/public/js/frappe/form/form_tour.js index e0cf694a5b..32f348163e 100644 --- a/frappe/public/js/frappe/form/form_tour.js +++ b/frappe/public/js/frappe/form/form_tour.js @@ -22,9 +22,15 @@ frappe.ui.form.FormTour = class FormTour { this.frm.layout.sections.forEach(section => section.collapse(false)); } - async set_tour(tour_name, on_finish) { - this.tour = await frappe.db.get_doc('Form Tour', tour_name); - this.on_finish = on_finish; + async init({ tour_name, on_finish }) { + if (tour_name) { + this.tour = await frappe.db.get_doc('Form Tour', tour_name); + } else { + this.tour = { steps: frappe.tour[this.frm.doctype] } + } + + if (on_finish) this.on_finish = on_finish; + this.build_steps(); this.define_steps(); } @@ -70,7 +76,7 @@ frappe.ui.form.FormTour = class FormTour { this.driver.defineSteps(steps); } - start_tour() { + start() { if (this.driver_steps.length == 0) { return; } diff --git a/frappe/public/js/frappe/widgets/onboarding_widget.js b/frappe/public/js/frappe/widgets/onboarding_widget.js index e552a7dd55..b487c0134f 100644 --- a/frappe/public/js/frappe/widgets/onboarding_widget.js +++ b/frappe/public/js/frappe/widgets/onboarding_widget.js @@ -203,7 +203,7 @@ export default class OnboardingWidget extends Widget { frappe.route_hooks = {}; frappe.route_hooks.after_load = (frm) => { - frm.show_tour(() => { + const on_finish = () => { let msg_dialog = frappe.msgprint({ message: __("Let's take you back to onboarding"), title: __("Great Job"), @@ -217,7 +217,10 @@ export default class OnboardingWidget extends Widget { label: () => __("Continue"), }, }); - }); + }; + frm.tour + .init({ on_finish }) + .then(() => frm.tour.start()); }; frappe.set_route(route); @@ -290,12 +293,15 @@ export default class OnboardingWidget extends Widget { frappe.route_hooks = {}; frappe.route_hooks.after_load = (frm) => { - frm.show_tour(() => { + const on_finish = () => { frappe.msgprint({ message: __("Awesome, now try making an entry yourself"), title: __("Great"), }); - }); + }; + frm.tour + .init({ on_finish }) + .then(() => frm.tour.start()); }; let callback = () => { From a5250ccd82920938ae26cc4d7ccc9cbe542d1c61 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Sat, 5 Jun 2021 22:35:21 +0530 Subject: [PATCH 3/9] feat: Form Tour for Grids --- frappe/desk/doctype/form_tour/form_tour.js | 55 ++++ frappe/desk/doctype/form_tour/form_tour.json | 13 +- frappe/desk/doctype/form_tour/form_tour.py | 14 +- .../form_tour_step/form_tour_step.json | 63 +++- frappe/public/js/frappe/desk.js | 4 +- frappe/public/js/frappe/form/form_tour.js | 288 ++++++++++++++---- frappe/public/js/frappe/utils/utils.js | 10 +- frappe/public/scss/desk/driver.scss | 12 + 8 files changed, 381 insertions(+), 78 deletions(-) diff --git a/frappe/desk/doctype/form_tour/form_tour.js b/frappe/desk/doctype/form_tour/form_tour.js index 94c6806b50..2a7050d5e8 100644 --- a/frappe/desk/doctype/form_tour/form_tour.js +++ b/frappe/desk/doctype/form_tour/form_tour.js @@ -20,5 +20,60 @@ frappe.ui.form.on('Form Tour', { } }; }); + + frm.set_query("parent_field", "steps", function() { + return { + query: "frappe.desk.doctype.form_tour.form_tour.get_docfield_list", + filters: { + doctype: frm.doc.reference_doctype, + fieldtype: "Table", + hidden: 0, + } + }; + }); + + frm.trigger('reference_doctype'); + }, + + reference_doctype(frm) { + if (!frm.doc.reference_doctype) return; + + frappe.db.get_list('DocField', { + filters: { + parent: frm.doc.reference_doctype, + parenttype: 'DocType', + fieldtype: 'Table' + }, + fields: ['options'] + }).then(res => { + if (Array.isArray(res)) { + frm.child_doctypes = res.map(r => r.options); + } + }); + } }); + +frappe.ui.form.on('Form Tour Step', { + parent_field(frm, cdt, cdn) { + const child_row = locals[cdt][cdn]; + frappe.model.set_value(cdt, cdn, 'field', ''); + const field_control = get_child_field("steps", cdn, "field"); + field_control.get_query = function() { + return { + query: "frappe.desk.doctype.form_tour.form_tour.get_docfield_list", + filters: { + doctype: child_row.child_doctype, + hidden: 0 + } + }; + } + } +}); + +function get_child_field(child_table, child_name, fieldname) { + // gets the field from grid row form + const grid = cur_frm.fields_dict[child_table].grid; + const grid_row = grid.grid_rows_by_docname[child_name]; + return grid_row.grid_form.fields_dict[fieldname]; +} \ No newline at end of file diff --git a/frappe/desk/doctype/form_tour/form_tour.json b/frappe/desk/doctype/form_tour/form_tour.json index 8e09a5d63a..c28dd5762c 100644 --- a/frappe/desk/doctype/form_tour/form_tour.json +++ b/frappe/desk/doctype/form_tour/form_tour.json @@ -9,6 +9,7 @@ "title", "reference_doctype", "completed", + "save_on_complete", "section_break_3", "steps" ], @@ -19,10 +20,10 @@ "in_list_view": 1, "label": "Reference Document", "options": "DocType", - "reqd": 1, - "unique": 1 + "reqd": 1 }, { + "depends_on": "reference_doctype", "fieldname": "steps", "fieldtype": "Table", "label": "Steps", @@ -46,11 +47,17 @@ "label": "Title", "reqd": 1, "unique": 1 + }, + { + "default": "0", + "fieldname": "save_on_complete", + "fieldtype": "Check", + "label": "Save on Completion" } ], "index_web_pages_for_search": 1, "links": [], - "modified": "2021-05-26 19:36:59.093753", + "modified": "2021-06-05 21:39:52.416111", "modified_by": "Administrator", "module": "Desk", "name": "Form Tour", diff --git a/frappe/desk/doctype/form_tour/form_tour.py b/frappe/desk/doctype/form_tour/form_tour.py index dd762395c4..8d6b1b2a7c 100644 --- a/frappe/desk/doctype/form_tour/form_tour.py +++ b/frappe/desk/doctype/form_tour/form_tour.py @@ -16,17 +16,23 @@ def get_docfield_list(doctype, txt, searchfield, start, page_len, filters): ['fieldtype', 'like', '%' + txt + '%'] ] - parent_doctype = filters.pop('doctype') - excluded_fieldtypes = ['Column Break'] - excluded_fieldtypes += filters.get('excluded_fieldtypes', []) + parent_doctype = filters.get('doctype') + fieldtype = filters.get('fieldtype') + if not fieldtype: + excluded_fieldtypes = ['Column Break'] + excluded_fieldtypes += filters.get('excluded_fieldtypes', []) + fieldtype_filter = ['not in', excluded_fieldtypes] + else: + fieldtype_filter = fieldtype docfields = frappe.get_all( doctype, fields=["name as value", "label", "fieldtype"], - filters={'parent': parent_doctype, 'fieldtype': ['not in', excluded_fieldtypes]}, + filters={'parent': parent_doctype, 'fieldtype': fieldtype_filter}, or_filters=or_filters, limit_start=start, limit_page_length=page_len, + order_by="idx", as_list=1, ) return docfields diff --git a/frappe/desk/doctype/form_tour_step/form_tour_step.json b/frappe/desk/doctype/form_tour_step/form_tour_step.json index 8b36b54db7..de2ec18f6e 100644 --- a/frappe/desk/doctype/form_tour_step/form_tour_step.json +++ b/frappe/desk/doctype/form_tour_step/form_tour_step.json @@ -4,6 +4,9 @@ "doctype": "DocType", "engine": "InnoDB", "field_order": [ + "is_table_field", + "section_break_2", + "parent_field", "field", "title", "description", @@ -11,7 +14,11 @@ "position", "fieldname", "label", - "next_step_condition" + "has_next_condition", + "next_step_condition", + "section_break_13", + "fieldtype", + "child_doctype" ], "fields": [ { @@ -30,6 +37,7 @@ "reqd": 1 }, { + "depends_on": "eval: (!doc.is_table_field || (doc.is_table_field && doc.parent_field))", "fieldname": "field", "fieldtype": "Link", "label": "Field", @@ -64,16 +72,65 @@ "options": "Left\nLeft Center\nLeft Bottom\nTop\nTop Center\nTop Right\nRight\nRight Center\nRight Bottom\nBottom\nBottom Center\nBottom Right\nMid Center" }, { + "depends_on": "has_next_condition", "fieldname": "next_step_condition", "fieldtype": "Code", "label": "Next Step Condition", + "oldfieldname": "condition", "options": "JS" + }, + { + "default": "0", + "fieldname": "has_next_condition", + "fieldtype": "Check", + "label": "Has Next Condition" + }, + { + "default": "0", + "fetch_from": "field.fieldtype", + "fieldname": "fieldtype", + "fieldtype": "Data", + "hidden": 1, + "label": "Fieldtype", + "read_only": 1 + }, + { + "default": "0", + "fieldname": "is_table_field", + "fieldtype": "Check", + "label": "Is Table Field" + }, + { + "fieldname": "section_break_2", + "fieldtype": "Section Break" + }, + { + "depends_on": "is_table_field", + "fieldname": "parent_field", + "fieldtype": "Link", + "label": "Parent Field", + "mandatory_depends_on": "is_table_field", + "options": "DocField" + }, + { + "fieldname": "section_break_13", + "fieldtype": "Section Break", + "hidden": 1, + "label": "Hidden Fields" + }, + { + "fetch_from": "parent_field.options", + "fieldname": "child_doctype", + "fieldtype": "Data", + "hidden": 1, + "label": "Child Doctype", + "read_only": 1 } ], "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2021-05-27 19:44:48.737453", + "modified": "2021-06-05 21:45:19.938002", "modified_by": "Administrator", "module": "Desk", "name": "Form Tour Step", @@ -82,4 +139,4 @@ "sort_field": "modified", "sort_order": "DESC", "track_changes": 1 -} +} \ No newline at end of file diff --git a/frappe/public/js/frappe/desk.js b/frappe/public/js/frappe/desk.js index 46812f5fb6..65c0139b65 100644 --- a/frappe/public/js/frappe/desk.js +++ b/frappe/public/js/frappe/desk.js @@ -607,9 +607,7 @@ frappe.Application = class Application { let doc = JSON.parse(pasted_data); if (doc.doctype) { e.preventDefault(); - let sleep = (time) => { - return new Promise((resolve) => setTimeout(resolve, time)); - }; + const sleep = frappe.utils.sleep; frappe.dom.freeze(__('Creating {0}', [doc.doctype]) + '...'); // to avoid abrupt UX diff --git a/frappe/public/js/frappe/form/form_tour.js b/frappe/public/js/frappe/form/form_tour.js index 32f348163e..148b6a91b0 100644 --- a/frappe/public/js/frappe/form/form_tour.js +++ b/frappe/public/js/frappe/form/form_tour.js @@ -1,13 +1,13 @@ frappe.ui.form.FormTour = class FormTour { - constructor({ frm }) { - this.frm = frm; - this.driver_steps = []; + constructor({ frm }) { + this.frm = frm; + this.driver_steps = []; - this.init_driver(); - } + this.init_driver(); + } - init_driver() { - this.driver = new frappe.Driver({ + init_driver() { + this.driver = new frappe.Driver({ className: 'frappe-driver', allowClose: false, padding: 10, @@ -15,71 +15,233 @@ frappe.ui.form.FormTour = class FormTour { keyboardControl: true, nextBtnText: 'Next', prevBtnText: 'Previous', - opacity: 0.25 + opacity: 0.25, + onHighlighted: (step) => { + // if last step is to save, then attach a listener to save button + if (step.options.is_save_step) { + $(step.options.element).one('click', () => this.driver.reset()); + } + + // focus on input + const $input = $(step.node).find('input').get(0); + if ($input) + frappe.utils.sleep(200).then(() => $input.focus()); + } }); - frappe.router.on('change', () => this.driver.reset()); - this.frm.layout.sections.forEach(section => section.collapse(false)); - } + frappe.router.on('change', () => this.driver.reset()); + this.frm.layout.sections.forEach(section => section.collapse(false)); + } - async init({ tour_name, on_finish }) { - if (tour_name) { - this.tour = await frappe.db.get_doc('Form Tour', tour_name); - } else { - this.tour = { steps: frappe.tour[this.frm.doctype] } - } - - if (on_finish) this.on_finish = on_finish; + async init({ tour_name, on_finish }) { + if (tour_name) { + this.tour = await frappe.db.get_doc('Form Tour', tour_name); + } else { + this.tour = { steps: frappe.tour[this.frm.doctype] } + } + + if (on_finish) this.on_finish = on_finish; - this.build_steps(); - this.define_steps(); - } + this.build_steps(); + this.update_driver_steps(); + } - build_steps() { - this.tour.steps.forEach((step, idx) => { - const me = this; - const on_next = () => { - const next_condition_satisfied = this.frm.layout.evaluate_depends_on_value(step.next_step_condition || true); + build_steps() { + this.driver_steps = []; + this.tour.steps.forEach((step) => { + const on_next = () => { + if (!this.is_next_condition_satisfied(step)) { + this.driver.preventMove(); + } - if (!next_condition_satisfied) { - me.driver.preventMove(); - } + if (!this.driver.hasNextStep()) { + this.on_finish && this.on_finish(); + } + } - if (!me.driver.hasNextStep()) { - me.on_finish && me.on_finish(); - } - } - - const driver_step = this.get_step(step, on_next); + const driver_step = this.get_step(step, on_next); this.driver_steps.push(driver_step); - }); - } + + if (step.fieldtype == 'Table') this.handle_table_step(step); + if (step.is_table_field) this.handle_child_table_step(step); + }); - get_step(step_info, on_next) { - const field = this.frm.get_field(step_info.fieldname); - // if field is a child table field, `field` will be undefined - const element = field ? field.wrapper : `.frappe-control[data-fieldname='${step_info.fieldname}']`; - const title = step_info.title || field.df.label; - const description = step_info.description; - const position = step_info.position || 'bottom'; - return { - element, - popover: { title, description, position }, - onNext: on_next - }; - } + if (this.tour.save_on_complete) { + this.add_step_to_save(); + } + } - define_steps(steps = []) { - if (steps.length == 0) { - steps = this.driver_steps; - } - this.driver.defineSteps(steps); - } + is_next_condition_satisfied(step) { + const form = step.is_table_field ? this.frm.cur_grid.grid_form : this.frm; + return form.layout.evaluate_depends_on_value(step.next_step_condition || true); + } - start() { - if (this.driver_steps.length == 0) { - return; - } - this.driver.start(); - } + get_step(step_info, on_next) { + const { name, fieldname, title, description, position } = step_info; + const field = this.frm.get_field(fieldname); + // if field is a child table field, `field` will be undefined + const element = field ? field.wrapper : `.frappe-control[data-fieldname='${fieldname}']`; + + return { + element, + name, + popover: { title, description, position }, + onNext: on_next + }; + } + + update_driver_steps(steps = []) { + if (steps.length == 0) { + steps = this.driver_steps; + } + this.driver.defineSteps(steps); + } + + start(idx = 0) { + if (this.driver_steps.length == 0) { + return; + } + this.driver.start(idx); + } + + get_next_step() { + // returns the next step only if driver is active + if (this.driver.isActivated & this.driver.hasNextStep()) { + const current_step = this.driver.currentStep; + return this.driver.steps[current_step + 1]; + } + return; + } + + handle_table_step(step_info) { + const is_last_step = step_info.idx == this.tour.steps.length; + + if (!is_last_step) { + // if next step field is inside currently highlighted table field + // then check if there is a row -> if not, then prompt to add row + // then edit the first row and hightlight next step + + const curr_step = step_info; + const next_step = this.tour.steps[curr_step.idx]; + const is_next_field_in_curr_table = next_step.parent_field == curr_step.field; + + if (!is_next_field_in_curr_table) return; + + const table_has_rows = this.frm.doc[curr_step.fieldname].length > 0; + if (table_has_rows) { + // table already has rows + // then just edit the first one on next step + const curr_driver_step = this.driver_steps.find(s => s.name == curr_step.name); + curr_driver_step.onNext = () => { + if (this.is_next_condition_satisfied(curr_step)) { + this.expand_row_and_proceed(curr_step, curr_step.idx); + } else { + this.driver.preventMove(); + } + }; + this.update_driver_steps(); + + } else { + this.add_new_row_step(curr_step); + } + } + } + + add_new_row_step(step) { + const $add_row = `.frappe-control[data-fieldname='${step.fieldname}'] .grid-add-row`; + const add_row_step = { + element: $add_row, + popover: { title: __("Add a Row"), description: "" }, + onNext: () => { + if (!cur_frm.cur_grid) { + this.driver.preventMove(); + } + } + }; + this.driver_steps.push(add_row_step); + + // setup a listener on add row button + // so, once the row is added, move to next step automatically + $($add_row).one('click', () => { + this.expand_row_and_proceed(step, step.idx + 1); // +1 since add row step is added + }); + } + + expand_row_and_proceed(step, start_from) { + this.open_first_row_of(step.fieldname); + this.update_driver_steps(); // need to define again, since driver.js only considers steps which are inside DOM + frappe.utils.sleep(300).then(() => this.driver.start(start_from)) + } + + open_first_row_of(fieldname) { + this.frm.fields_dict[fieldname].grid.grid_rows[0].toggle_view(); + + // setup a listener on close row button + // so, once the row is closed, move to next step automatically + const $close_row = '.grid-row-open .grid-collapse-row'; + $($close_row).one('click', () => { + const next_step = this.get_next_step(); + const next_element = next_step.options.is_save_step ? null : next_step.node; + + frappe.utils.scroll_to(next_element, true, 150, null, () => { + this.driver.moveNext(); + frappe.flags.disable_auto_scroll = false; + }); + frappe.flags.disable_auto_scroll = true; + }); + } + + handle_child_table_step(step_info) { + const is_last_step = step_info.idx == this.tour.steps.length; + + if (!is_last_step) { + const curr_step = step_info; + const next_step = this.tour.steps[curr_step.idx]; + const field = this.frm.get_field(next_step.fieldname); + + if (!field) return; + + // next step highlights parent field + // so, add a step to prompt user to collapse grid form + this.add_collapse_row_step(); + + } else if (this.tour.save_on_complete) { + // if last step & save on complete is checked + // add a step to prompt user to collapse grid form + // to be able to save as a last step + this.add_collapse_row_step(); + } + } + + add_collapse_row_step() { + const $close_row = '.grid-row-open .grid-collapse-row'; + const close_row_step = { + element: $close_row, + popover: { title: __("Collapse"), description: "", position: "left" }, + onNext: () => { + if (cur_frm.cur_grid) { + this.driver.preventMove(); + } + } + }; + this.driver_steps.push(close_row_step); + } + + add_step_to_save() { + const $save_btn = '.standard-actions .primary-action'; + const save_step = { + element: $save_btn, + is_save_step: true, + allowClose: false, + overlayClickNext: false, + popover: { + title: __("Save"), + description: "", + position: "left", + doneBtnText: __("Save") + } + }; + this.driver_steps.push(save_step); + frappe.ui.form.on(this.frm.doctype, 'after_save', () => this.on_finish && this.on_finish()); + } }; \ No newline at end of file diff --git a/frappe/public/js/frappe/utils/utils.js b/frappe/public/js/frappe/utils/utils.js index 484d9c65f1..8d3d0edd53 100644 --- a/frappe/public/js/frappe/utils/utils.js +++ b/frappe/public/js/frappe/utils/utils.js @@ -268,7 +268,9 @@ Object.assign(frappe.utils, {

'); return content.html(); }, - scroll_to: function(element, animate=true, additional_offset, element_to_be_scrolled) { + scroll_to: function(element, animate=true, additional_offset, element_to_be_scrolled, callback) { + if (frappe.flags.disable_auto_scroll) return; + element_to_be_scrolled = element_to_be_scrolled || $("html, body"); let scroll_top = 0; if (element) { @@ -289,7 +291,7 @@ Object.assign(frappe.utils, { } if (animate) { - element_to_be_scrolled.animate({ scrollTop: scroll_top }); + element_to_be_scrolled.animate({ scrollTop: scroll_top }).promise().then(callback); } else { element_to_be_scrolled.scrollTop(scroll_top); } @@ -1319,5 +1321,9 @@ Object.assign(frappe.utils, { let e = clipboard_paste_event; let clipboard_data = e.clipboardData || window.clipboardData || e.originalEvent.clipboardData; return clipboard_data.getData('Text'); + }, + + sleep(time) { + return new Promise((resolve) => setTimeout(resolve, time)); } }); diff --git a/frappe/public/scss/desk/driver.scss b/frappe/public/scss/desk/driver.scss index ddf594393f..4135d9667b 100644 --- a/frappe/public/scss/desk/driver.scss +++ b/frappe/public/scss/desk/driver.scss @@ -64,4 +64,16 @@ div#driver-popover-item { input.driver-highlighted-element { background-color: var(--fg-color); +} + +.driver-fix-stacking { + z-index: auto !important; + position: unset !important; + opacity: 1.0 !important; + transform: none !important; + filter: none !important; + perspective: none !important; + transform-style: flat !important; + transform-box: border-box !important; + will-change: unset !important; } \ No newline at end of file From 17b3c701ed0555c0a16717d3663c1d99497997c0 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Sun, 6 Jun 2021 21:13:45 +0530 Subject: [PATCH 4/9] feat: standard form tours --- frappe/desk/doctype/form_tour/form_tour.js | 22 +++++++++++++++ frappe/desk/doctype/form_tour/form_tour.json | 27 ++++++++++++------- frappe/desk/doctype/form_tour/form_tour.py | 25 ++++++++++++++++- .../form_tour_step/form_tour_step.json | 13 +++++++-- frappe/model/sync.py | 2 +- 5 files changed, 76 insertions(+), 13 deletions(-) diff --git a/frappe/desk/doctype/form_tour/form_tour.js b/frappe/desk/doctype/form_tour/form_tour.js index 2a7050d5e8..3f1e983868 100644 --- a/frappe/desk/doctype/form_tour/form_tour.js +++ b/frappe/desk/doctype/form_tour/form_tour.js @@ -3,6 +3,28 @@ frappe.ui.form.on('Form Tour', { setup: function(frm) { + if (!frm.doc.is_standard) { + frm.trigger('setup_queries'); + } + }, + + refresh(frm) { + if (frm.doc.is_standard && !frappe.boot.developer_mode) { + frm.trigger("disable_form"); + } + }, + + disable_form: function(frm) { + frm.set_read_only(); + frm.fields + .filter((field) => field.has_input) + .forEach((field) => { + frm.set_df_property(field.df.fieldname, "read_only", "1"); + }); + frm.disable_save(); + }, + + setup_queries(frm) { frm.set_query("reference_doctype", function() { return { filters: { diff --git a/frappe/desk/doctype/form_tour/form_tour.json b/frappe/desk/doctype/form_tour/form_tour.json index c28dd5762c..e4ea528fcc 100644 --- a/frappe/desk/doctype/form_tour/form_tour.json +++ b/frappe/desk/doctype/form_tour/form_tour.json @@ -8,7 +8,8 @@ "field_order": [ "title", "reference_doctype", - "completed", + "module", + "is_standard", "save_on_complete", "section_break_3", "steps" @@ -30,13 +31,6 @@ "options": "Form Tour Step", "reqd": 1 }, - { - "default": "0", - "depends_on": "eval: doc.__islocal != 1", - "fieldname": "completed", - "fieldtype": "Check", - "label": "Mark as Completed" - }, { "fieldname": "section_break_3", "fieldtype": "Section Break" @@ -53,11 +47,26 @@ "fieldname": "save_on_complete", "fieldtype": "Check", "label": "Save on Completion" + }, + { + "default": "0", + "fieldname": "is_standard", + "fieldtype": "Check", + "label": "Is Standard" + }, + { + "fetch_from": "reference_doctype.module", + "fieldname": "module", + "fieldtype": "Link", + "hidden": 1, + "label": "Module", + "options": "Module Def", + "read_only": 1 } ], "index_web_pages_for_search": 1, "links": [], - "modified": "2021-06-05 21:39:52.416111", + "modified": "2021-06-06 20:32:54.068774", "modified_by": "Administrator", "module": "Desk", "name": "Form Tour", diff --git a/frappe/desk/doctype/form_tour/form_tour.py b/frappe/desk/doctype/form_tour/form_tour.py index 8d6b1b2a7c..7d1cd4637e 100644 --- a/frappe/desk/doctype/form_tour/form_tour.py +++ b/frappe/desk/doctype/form_tour/form_tour.py @@ -3,9 +3,32 @@ import frappe from frappe.model.document import Document +from frappe.modules.export_file import export_to_files class FormTour(Document): - pass + def before_insert(self): + if not self.is_standard: return + + # while syncing, set proper docfield reference + for d in self.steps: + if not frappe.db.exists('DocField', d.field): + d.field = frappe.db.get_value('DocField', { + 'fieldname': d.fieldname, 'parent': self.reference_doctype, 'fieldtype': d.fieldtype + }, "name") + + if d.is_table_field and not frappe.db.exists('DocField', d.parent_field): + d.parent_field = frappe.db.get_value('DocField', { + 'fieldname': d.parent_fieldname, 'parent': self.reference_doctype, 'fieldtype': 'Table' + }, "name") + + def on_update(self): + if frappe.conf.developer_mode and self.is_standard: + export_to_files([['Form Tour', self.name]], self.module) + + def before_export(self, doc): + for d in doc.steps: + d.field = "" + d.parent_field = "" @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs diff --git a/frappe/desk/doctype/form_tour_step/form_tour_step.json b/frappe/desk/doctype/form_tour_step/form_tour_step.json index de2ec18f6e..3b6c91a208 100644 --- a/frappe/desk/doctype/form_tour_step/form_tour_step.json +++ b/frappe/desk/doctype/form_tour_step/form_tour_step.json @@ -12,11 +12,12 @@ "description", "column_break_2", "position", - "fieldname", "label", "has_next_condition", "next_step_condition", "section_break_13", + "fieldname", + "parent_fieldname", "fieldtype", "child_doctype" ], @@ -125,12 +126,20 @@ "hidden": 1, "label": "Child Doctype", "read_only": 1 + }, + { + "fetch_from": "parent_field.fieldname", + "fieldname": "parent_fieldname", + "fieldtype": "Data", + "hidden": 1, + "label": "Parent Fieldname", + "read_only": 1 } ], "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2021-06-05 21:45:19.938002", + "modified": "2021-06-06 20:52:21.076972", "modified_by": "Administrator", "module": "Desk", "name": "Form Tour Step", diff --git a/frappe/model/sync.py b/frappe/model/sync.py index 28f9deb25d..836f70dd55 100644 --- a/frappe/model/sync.py +++ b/frappe/model/sync.py @@ -82,7 +82,7 @@ def get_doc_files(files, start_path): document_types = ['doctype', 'page', 'report', 'dashboard_chart_source', 'print_format', 'website_theme', 'web_form', 'web_template', 'notification', 'print_style', 'data_migration_mapping', 'data_migration_plan', 'workspace', - 'onboarding_step', 'module_onboarding'] + 'onboarding_step', 'module_onboarding', 'form_tour'] for doctype in document_types: doctype_path = os.path.join(start_path, doctype) From a81d4fd4ebf638ada949541359feb68a4a28ee22 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Wed, 16 Jun 2021 13:32:55 +0530 Subject: [PATCH 5/9] fix: sider issues --- frappe/desk/doctype/form_tour/form_tour.js | 2 +- frappe/desk/doctype/form_tour/form_tour.py | 3 ++- frappe/public/js/frappe/form/dashboard.js | 2 +- frappe/public/js/frappe/form/form_tour.js | 8 ++++---- 4 files changed, 8 insertions(+), 7 deletions(-) diff --git a/frappe/desk/doctype/form_tour/form_tour.js b/frappe/desk/doctype/form_tour/form_tour.js index 3f1e983868..d394a10757 100644 --- a/frappe/desk/doctype/form_tour/form_tour.js +++ b/frappe/desk/doctype/form_tour/form_tour.js @@ -89,7 +89,7 @@ frappe.ui.form.on('Form Tour Step', { hidden: 0 } }; - } + }; } }); diff --git a/frappe/desk/doctype/form_tour/form_tour.py b/frappe/desk/doctype/form_tour/form_tour.py index 7d1cd4637e..dbc667ce28 100644 --- a/frappe/desk/doctype/form_tour/form_tour.py +++ b/frappe/desk/doctype/form_tour/form_tour.py @@ -7,7 +7,8 @@ from frappe.modules.export_file import export_to_files class FormTour(Document): def before_insert(self): - if not self.is_standard: return + if not self.is_standard: + return # while syncing, set proper docfield reference for d in self.steps: diff --git a/frappe/public/js/frappe/form/dashboard.js b/frappe/public/js/frappe/form/dashboard.js index eb7a6edc5d..6833f68073 100644 --- a/frappe/public/js/frappe/form/dashboard.js +++ b/frappe/public/js/frappe/form/dashboard.js @@ -179,7 +179,7 @@ frappe.ui.form.Dashboard = class FormDashboard { return; } this.render_links(); - this.set_open_count(); + // this.set_open_count(); show = true; } diff --git a/frappe/public/js/frappe/form/form_tour.js b/frappe/public/js/frappe/form/form_tour.js index 148b6a91b0..f84dc6a5ec 100644 --- a/frappe/public/js/frappe/form/form_tour.js +++ b/frappe/public/js/frappe/form/form_tour.js @@ -37,7 +37,7 @@ frappe.ui.form.FormTour = class FormTour { if (tour_name) { this.tour = await frappe.db.get_doc('Form Tour', tour_name); } else { - this.tour = { steps: frappe.tour[this.frm.doctype] } + this.tour = { steps: frappe.tour[this.frm.doctype] }; } if (on_finish) this.on_finish = on_finish; @@ -51,13 +51,13 @@ frappe.ui.form.FormTour = class FormTour { this.tour.steps.forEach((step) => { const on_next = () => { if (!this.is_next_condition_satisfied(step)) { - this.driver.preventMove(); + this.driver.preventMove(); } if (!this.driver.hasNextStep()) { this.on_finish && this.on_finish(); } - } + }; const driver_step = this.get_step(step, on_next); this.driver_steps.push(driver_step); @@ -170,7 +170,7 @@ frappe.ui.form.FormTour = class FormTour { expand_row_and_proceed(step, start_from) { this.open_first_row_of(step.fieldname); this.update_driver_steps(); // need to define again, since driver.js only considers steps which are inside DOM - frappe.utils.sleep(300).then(() => this.driver.start(start_from)) + frappe.utils.sleep(300).then(() => this.driver.start(start_from)); } open_first_row_of(fieldname) { From a754d451769ab0a237f19597c09392466577dfc2 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Sat, 19 Jun 2021 19:59:03 +0530 Subject: [PATCH 6/9] feat: custom button to jump to tour --- frappe/desk/doctype/form_tour/form_tour.js | 24 +++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/frappe/desk/doctype/form_tour/form_tour.js b/frappe/desk/doctype/form_tour/form_tour.js index d394a10757..e5ea373c4c 100644 --- a/frappe/desk/doctype/form_tour/form_tour.js +++ b/frappe/desk/doctype/form_tour/form_tour.js @@ -3,7 +3,7 @@ frappe.ui.form.on('Form Tour', { setup: function(frm) { - if (!frm.doc.is_standard) { + if (!frm.doc.is_standard || frappe.boot.developer_mode) { frm.trigger('setup_queries'); } }, @@ -12,6 +12,23 @@ frappe.ui.form.on('Form Tour', { if (frm.doc.is_standard && !frappe.boot.developer_mode) { frm.trigger("disable_form"); } + + frm.add_custom_button(__('Show Tour'), async () => { + const issingle = await check_if_single(frm.doc.reference_doctype); + + if (issingle) { + frappe.set_route('Form', frm.doc.reference_doctype) + } else { + const new_name = 'new-' + frappe.scrub(frm.doc.reference_doctype) + '-1'; + frappe.set_route('Form', frm.doc.reference_doctype, new_name); + } + frappe.utils.sleep(500).then(() => { + const tour_name = frm.doc.name; + cur_frm.tour + .init({ tour_name }) + .then(() => cur_frm.tour.start()); + }); + }); }, disable_form: function(frm) { @@ -98,4 +115,9 @@ function get_child_field(child_table, child_name, fieldname) { const grid = cur_frm.fields_dict[child_table].grid; const grid_row = grid.grid_rows_by_docname[child_name]; return grid_row.grid_form.fields_dict[fieldname]; +} + +async function check_if_single(doctype) { + const { message } = await frappe.db.get_value('DocType', doctype, 'issingle'); + return message.issingle || 0; } \ No newline at end of file From 6ad15231617579b5b62c292884a2a0a039eb6886 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Wed, 23 Jun 2021 14:18:38 +0530 Subject: [PATCH 7/9] fix: table field & primary action selector --- frappe/public/js/frappe/form/form_tour.js | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/frappe/public/js/frappe/form/form_tour.js b/frappe/public/js/frappe/form/form_tour.js index f84dc6a5ec..7f7ec9ce4f 100644 --- a/frappe/public/js/frappe/form/form_tour.js +++ b/frappe/public/js/frappe/form/form_tour.js @@ -77,15 +77,18 @@ frappe.ui.form.FormTour = class FormTour { } get_step(step_info, on_next) { - const { name, fieldname, title, description, position } = step_info; + const { name, fieldname, title, description, position, is_table_field } = step_info; const field = this.frm.get_field(fieldname); - // if field is a child table field, `field` will be undefined - const element = field ? field.wrapper : `.frappe-control[data-fieldname='${fieldname}']`; + let element = field ? field.wrapper : `.frappe-control[data-fieldname='${fieldname}']`; + + if (is_table_field) { + element = `.grid-row-open .frappe-control[data-fieldname='${fieldname}']`; + } return { element, name, - popover: { title, description, position }, + popover: { title, description, position: frappe.router.slug(position) }, onNext: on_next }; } @@ -126,8 +129,9 @@ frappe.ui.form.FormTour = class FormTour { const is_next_field_in_curr_table = next_step.parent_field == curr_step.field; if (!is_next_field_in_curr_table) return; - - const table_has_rows = this.frm.doc[curr_step.fieldname].length > 0; + + const rows = this.frm.doc[curr_step.fieldname]; + const table_has_rows = rows && rows.length > 0; if (table_has_rows) { // table already has rows // then just edit the first one on next step @@ -228,7 +232,8 @@ frappe.ui.form.FormTour = class FormTour { } add_step_to_save() { - const $save_btn = '.standard-actions .primary-action'; + const page_id = `#page-${this.frm.doctype}`; + const $save_btn = `${page_id} .standard-actions .primary-action`; const save_step = { element: $save_btn, is_save_step: true, From ef4c6223a8e3f5de847e567fce096eaad9c750c9 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Wed, 23 Jun 2021 14:19:06 +0530 Subject: [PATCH 8/9] feat: add ui tests --- cypress/integration/form_tour.js | 88 ++++++++++++++++++++++++++++++++ frappe/tests/ui_test_helpers.py | 49 ++++++++++++++++++ 2 files changed, 137 insertions(+) create mode 100644 cypress/integration/form_tour.js diff --git a/cypress/integration/form_tour.js b/cypress/integration/form_tour.js new file mode 100644 index 0000000000..e1544a4cdf --- /dev/null +++ b/cypress/integration/form_tour.js @@ -0,0 +1,88 @@ +context('Form Tour', () => { + before(() => { + cy.login(); + cy.visit('/app/form-tour'); + return cy.window().its('frappe').then(frappe => { + return frappe.call("frappe.tests.ui_test_helpers.create_form_tour"); + }); + }); + + const open_test_form_tour = () => { + cy.visit('/app/form-tour/Test Form Tour'); + cy.get('button[data-label="Show%20Tour"]').should('be.visible').and('contain', 'Show Tour').as('show_tour'); + cy.get('@show_tour').click(); + cy.wait(500); + cy.url().should('include', '/app/contact'); + }; + + it('jump to a form tour', open_test_form_tour); + + it('navigates a form tour', () => { + open_test_form_tour(); + + cy.get('#driver-popover-item').should('be.visible'); + cy.get('.frappe-control[data-fieldname="first_name"]').as('first_name'); + cy.get('@first_name').should('have.class', 'driver-highlighted-element'); + cy.get('.driver-next-btn').as('next_btn'); + + // next btn shouldn't move to next step, if first name is not entered + cy.get('@next_btn').click(); + cy.wait(500); + cy.get('@first_name').should('have.class', 'driver-highlighted-element'); + + // after filling the field, next step should be highlighted + cy.fill_field('first_name', 'Test Name', 'Data') + cy.wait(500); + cy.get('@next_btn').click(); + cy.wait(500); + + // assert field is highlighted + cy.get('.frappe-control[data-fieldname="last_name"]').as('last_name'); + cy.get('@last_name').should('have.class', 'driver-highlighted-element'); + + // after filling the field, next step should be highlighted + cy.fill_field('last_name', 'Test Last Name', 'Data') + cy.wait(500); + cy.get('@next_btn').click(); + cy.wait(500); + + // assert field is highlighted + cy.get('.frappe-control[data-fieldname="phone_nos"]').as('phone_nos'); + cy.get('@phone_nos').should('have.class', 'driver-highlighted-element'); + + // move to next step + cy.wait(500); + cy.get('@next_btn').click(); + cy.wait(500); + + // assert add row btn is highlighted + cy.get('@phone_nos').find('.grid-add-row').as('add_row'); + cy.get('@add_row').should('have.class', 'driver-highlighted-element'); + + // add a row & move to next step + cy.wait(500); + cy.get('@add_row').click(); + cy.wait(500); + + // assert table field is highlighted + cy.get('.grid-row-open .frappe-control[data-fieldname="phone"]').as('phone'); + cy.get('@phone').should('have.class', 'driver-highlighted-element'); + // enter value in a table field + cy.fill_table_field('phone_nos', '1', 'phone', '1234567890'); + + // move to collapse row step + cy.wait(500); + cy.get('@next_btn').click(); + cy.wait(500); + + // collapse row + cy.get('.grid-row-open .grid-collapse-row').click(); + cy.wait(500); + + // assert save btn is highlighted + cy.get('.primary-action').should('have.class', 'driver-highlighted-element'); + cy.get('@next_btn').should('contain', 'Save'); + + }); +}); + \ No newline at end of file diff --git a/frappe/tests/ui_test_helpers.py b/frappe/tests/ui_test_helpers.py index 7670f99698..cd441f8f10 100644 --- a/frappe/tests/ui_test_helpers.py +++ b/frappe/tests/ui_test_helpers.py @@ -131,3 +131,52 @@ def insert_contact(first_name, phone_number): }) doc.append('phone_nos', {'phone': phone_number}) doc.insert() + +@frappe.whitelist() +def create_form_tour(): + if frappe.db.exists('Form Tour', {'name': 'Test Form Tour'}): + return + + def get_docfield_name(filters): + return frappe.db.get_value('DocField', filters, "name") + + tour = frappe.get_doc({ + 'doctype': 'Form Tour', + 'title': 'Test Form Tour', + 'reference_doctype': 'Contact', + 'save_on_complete': 1, + 'steps': [{ + "title": "Test Title 1", + "description": "Test Description 1", + "has_next_condition": 1, + "next_step_condition": "eval: doc.first_name", + "field": get_docfield_name({'parent': 'Contact', 'fieldname': 'first_name'}), + "fieldname": "first_name", + "fieldtype": "Data" + },{ + "title": "Test Title 2", + "description": "Test Description 2", + "has_next_condition": 1, + "next_step_condition": "eval: doc.last_name", + "field": get_docfield_name({'parent': 'Contact', 'fieldname': 'last_name'}), + "fieldname": "last_name", + "fieldtype": "Data" + },{ + "title": "Test Title 3", + "description": "Test Description 3", + "field": get_docfield_name({'parent': 'Contact', 'fieldname': 'phone_nos'}), + "fieldname": "phone_nos", + "fieldtype": "Table" + },{ + "title": "Test Title 4", + "description": "Test Description 4", + "is_table_field": 1, + "parent_field": get_docfield_name({'parent': 'Contact', 'fieldname': 'phone_nos'}), + "field": get_docfield_name({'parent': 'Contact Phone', 'fieldname': 'phone'}), + "next_step_condition": "eval: doc.phone", + "has_next_condition": 1, + "fieldname": "phone", + "fieldtype": "Data" + }] + }) + tour.insert() \ No newline at end of file From 1f687a13edddadb7253f9cca7fcb8621b32a2768 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Wed, 23 Jun 2021 14:27:50 +0530 Subject: [PATCH 9/9] fix: add missing semicolons --- cypress/integration/form_tour.js | 4 ++-- frappe/desk/doctype/form_tour/form_tour.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/cypress/integration/form_tour.js b/cypress/integration/form_tour.js index e1544a4cdf..d12be63f3b 100644 --- a/cypress/integration/form_tour.js +++ b/cypress/integration/form_tour.js @@ -31,7 +31,7 @@ context('Form Tour', () => { cy.get('@first_name').should('have.class', 'driver-highlighted-element'); // after filling the field, next step should be highlighted - cy.fill_field('first_name', 'Test Name', 'Data') + cy.fill_field('first_name', 'Test Name', 'Data'); cy.wait(500); cy.get('@next_btn').click(); cy.wait(500); @@ -41,7 +41,7 @@ context('Form Tour', () => { cy.get('@last_name').should('have.class', 'driver-highlighted-element'); // after filling the field, next step should be highlighted - cy.fill_field('last_name', 'Test Last Name', 'Data') + cy.fill_field('last_name', 'Test Last Name', 'Data'); cy.wait(500); cy.get('@next_btn').click(); cy.wait(500); diff --git a/frappe/desk/doctype/form_tour/form_tour.js b/frappe/desk/doctype/form_tour/form_tour.js index e5ea373c4c..efb853cfa5 100644 --- a/frappe/desk/doctype/form_tour/form_tour.js +++ b/frappe/desk/doctype/form_tour/form_tour.js @@ -17,7 +17,7 @@ frappe.ui.form.on('Form Tour', { const issingle = await check_if_single(frm.doc.reference_doctype); if (issingle) { - frappe.set_route('Form', frm.doc.reference_doctype) + frappe.set_route('Form', frm.doc.reference_doctype); } else { const new_name = 'new-' + frappe.scrub(frm.doc.reference_doctype) + '-1'; frappe.set_route('Form', frm.doc.reference_doctype, new_name);