diff --git a/cypress/integration/form_tour.js b/cypress/integration/form_tour.js new file mode 100644 index 0000000000..d12be63f3b --- /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/desk/doctype/form_tour/form_tour.js b/frappe/desk/doctype/form_tour/form_tour.js index 94c6806b50..efb853cfa5 100644 --- a/frappe/desk/doctype/form_tour/form_tour.js +++ b/frappe/desk/doctype/form_tour/form_tour.js @@ -3,6 +3,45 @@ frappe.ui.form.on('Form Tour', { setup: function(frm) { + if (!frm.doc.is_standard || frappe.boot.developer_mode) { + frm.trigger('setup_queries'); + } + }, + + refresh(frm) { + 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) { + 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: { @@ -20,5 +59,65 @@ 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]; +} + +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 diff --git a/frappe/desk/doctype/form_tour/form_tour.json b/frappe/desk/doctype/form_tour/form_tour.json index 8e09a5d63a..e4ea528fcc 100644 --- a/frappe/desk/doctype/form_tour/form_tour.json +++ b/frappe/desk/doctype/form_tour/form_tour.json @@ -8,7 +8,9 @@ "field_order": [ "title", "reference_doctype", - "completed", + "module", + "is_standard", + "save_on_complete", "section_break_3", "steps" ], @@ -19,23 +21,16 @@ "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", "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" @@ -46,11 +41,32 @@ "label": "Title", "reqd": 1, "unique": 1 + }, + { + "default": "0", + "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-05-26 19:36:59.093753", + "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 dd762395c4..dbc667ce28 100644 --- a/frappe/desk/doctype/form_tour/form_tour.py +++ b/frappe/desk/doctype/form_tour/form_tour.py @@ -3,9 +3,33 @@ 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 @@ -16,17 +40,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 a772a2498a..3b6c91a208 100644 --- a/frappe/desk/doctype/form_tour_step/form_tour_step.json +++ b/frappe/desk/doctype/form_tour_step/form_tour_step.json @@ -4,14 +4,22 @@ "doctype": "DocType", "engine": "InnoDB", "field_order": [ + "is_table_field", + "section_break_2", + "parent_field", "field", "title", "description", "column_break_2", "position", - "fieldname", "label", - "condition" + "has_next_condition", + "next_step_condition", + "section_break_13", + "fieldname", + "parent_fieldname", + "fieldtype", + "child_doctype" ], "fields": [ { @@ -30,6 +38,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 +73,73 @@ "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 + }, + { + "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-05-26 19:44:48.737453", + "modified": "2021-06-06 20:52:21.076972", "modified_by": "Administrator", "module": "Desk", "name": "Form Tour Step", @@ -82,4 +148,4 @@ "sort_field": "modified", "sort_order": "DESC", "track_changes": 1 -} +} \ No newline at end of file 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) 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/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.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..7f7ec9ce4f --- /dev/null +++ b/frappe/public/js/frappe/form/form_tour.js @@ -0,0 +1,252 @@ +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, + 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)); + } + + 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.update_driver_steps(); + } + + build_steps() { + this.driver_steps = []; + this.tour.steps.forEach((step) => { + const on_next = () => { + if (!this.is_next_condition_satisfied(step)) { + 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); + + if (step.fieldtype == 'Table') this.handle_table_step(step); + if (step.is_table_field) this.handle_child_table_step(step); + }); + + if (this.tour.save_on_complete) { + this.add_step_to_save(); + } + } + + 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); + } + + get_step(step_info, on_next) { + const { name, fieldname, title, description, position, is_table_field } = step_info; + const field = this.frm.get_field(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: frappe.router.slug(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 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 + 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 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, + 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/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 = () => { 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 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