Merge pull request #13528 from nextchamp-saqib/form-tour-for-grids
feat: Form Tour for Grids
This commit is contained in:
commit
518c82fc82
14 changed files with 658 additions and 78 deletions
88
cypress/integration/form_tour.js
Normal file
88
cypress/integration/form_tour.js
Normal file
|
|
@ -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');
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -179,7 +179,7 @@ frappe.ui.form.Dashboard = class FormDashboard {
|
|||
return;
|
||||
}
|
||||
this.render_links();
|
||||
this.set_open_count();
|
||||
// this.set_open_count();
|
||||
show = true;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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: $('<div class="form-dashboard">').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;
|
||||
|
|
|
|||
252
frappe/public/js/frappe/form/form_tour.js
Normal file
252
frappe/public/js/frappe/form/form_tour.js
Normal file
|
|
@ -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());
|
||||
}
|
||||
};
|
||||
|
|
@ -268,7 +268,9 @@ Object.assign(frappe.utils, {
|
|||
</a></p>');
|
||||
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));
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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 = () => {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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()
|
||||
Loading…
Add table
Reference in a new issue