From 5171d6edc913a10a0170367259927615f8ee1758 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Sat, 9 Apr 2022 23:38:11 +0200 Subject: [PATCH 001/203] feat: read-only geolocation (GDE-86) - render map into display_area - hide draw controls if read-only - remove useless refresh_button --- .../js/frappe/form/controls/geolocation.js | 112 ++++++++---------- 1 file changed, 50 insertions(+), 62 deletions(-) diff --git a/frappe/public/js/frappe/form/controls/geolocation.js b/frappe/public/js/frappe/form/controls/geolocation.js index 688e7da3e0..008f90bc72 100644 --- a/frappe/public/js/frappe/form/controls/geolocation.js +++ b/frappe/public/js/frappe/form/controls/geolocation.js @@ -6,69 +6,69 @@ frappe.ui.form.ControlGeolocation = class ControlGeolocation extends frappe.ui.f async make() { await frappe.require(this.required_libs); super.make(); + $(this.input_area).addClass("hidden"); } - make_wrapper() { + set_disp_area(value) { // Create the elements for map area - super.make_wrapper(); - - let $input_wrapper = this.$wrapper.find('.control-input-wrapper'); + if (!this.disp_area) return; + this.map_id = frappe.dom.get_unique_id(); this.map_area = $( `
` ); - this.map_area.prependTo($input_wrapper); - this.$wrapper.find('.control-input').addClass("hidden"); + + $(this.disp_area).html(this.map_area); + $(this.disp_area).removeClass("like-disabled-input"); + $(this.disp_area).css("display", "block"); if (this.frm) { - this.make_map(); + this.make_map(value); } else { $(document).on('frappe.ui.Dialog:shown', () => { - this.make_map(); + this.make_map(value); }); } } - make_map() { + make_map(value) { this.bind_leaflet_map(); this.bind_leaflet_draw_control(); + this.bind_leaflet_event_listeners(); this.bind_leaflet_locate_control(); - this.bind_leaflet_refresh_button(); + this.bind_leaflet_data(value); } - format_for_input(value) { - if (!this.map) return; - // render raw value from db into map + bind_leaflet_data(value) { + /* render raw value from db into map */ + if (!this.map || !value) return; this.clear_editable_layers(); - if(value) { - var data_layers = new L.FeatureGroup() - .addLayer(L.geoJson(JSON.parse(value),{ - pointToLayer: function(geoJsonPoint, latlng) { - if (geoJsonPoint.properties.point_type == "circle"){ - return L.circle(latlng, {radius: geoJsonPoint.properties.radius}); - } else if (geoJsonPoint.properties.point_type == "circlemarker") { - return L.circleMarker(latlng, {radius: geoJsonPoint.properties.radius}); - } - else { - return L.marker(latlng); - } + + var data_layers = new L.FeatureGroup() + .addLayer(L.geoJson(JSON.parse(value),{ + pointToLayer: function(geoJsonPoint, latlng) { + if (geoJsonPoint.properties.point_type == "circle"){ + return L.circle(latlng, {radius: geoJsonPoint.properties.radius}); + } else if (geoJsonPoint.properties.point_type == "circlemarker") { + return L.circleMarker(latlng, {radius: geoJsonPoint.properties.radius}); } - })); - this.add_non_group_layers(data_layers, this.editableLayers); - try { - this.map.fitBounds(this.editableLayers.getBounds(), { - padding: [50,50] - }); - } - catch(err) { - // suppress error if layer has a point. - } - this.editableLayers.addTo(this.map); - } else { - this.map.setView(frappe.utils.map_defaults.center, frappe.utils.map_defaults.zoom); + else { + return L.marker(latlng); + } + } + })); + this.add_non_group_layers(data_layers, this.editableLayers); + try { + this.map.fitBounds(this.editableLayers.getBounds(), { + padding: [50,50] + }); } + catch(err) { + // suppress error if layer has a point. + } + this.editableLayers.addTo(this.map); this.map.invalidateSize(); } @@ -98,9 +98,12 @@ frappe.ui.form.ControlGeolocation = class ControlGeolocation extends frappe.ui.f L.Icon.Default.imagePath = '/assets/frappe/images/leaflet/'; this.map = L.map(this.map_id); + this.map.setView(frappe.utils.map_defaults.center, frappe.utils.map_defaults.zoom); L.tileLayer(frappe.utils.map_defaults.tiles, frappe.utils.map_defaults.options).addTo(this.map); + + this.editableLayers = new L.FeatureGroup(); } bind_leaflet_locate_control() { @@ -110,9 +113,13 @@ frappe.ui.form.ControlGeolocation = class ControlGeolocation extends frappe.ui.f } bind_leaflet_draw_control() { - this.editableLayers = new L.FeatureGroup(); + if (!frappe.perm.has_perm(this.doctype, this.df.permlevel, 'write', this.doc)) return; - var options = { + this.map.addControl(this.get_leaflet_controls()); + } + + get_leaflet_controls() { + return new L.Control.Draw({ position: 'topleft', draw: { polyline: { @@ -142,12 +149,10 @@ frappe.ui.form.ControlGeolocation = class ControlGeolocation extends frappe.ui.f featureGroup: this.editableLayers, //REQUIRED!! remove: true } - }; - - // create control and add to map - this.drawControl = new L.Control.Draw(options); - this.map.addControl(this.drawControl); + }); + } + bind_leaflet_event_listeners() { this.map.on('draw:created', (e) => { var type = e.layerType, layer = e.layer; @@ -165,23 +170,6 @@ frappe.ui.form.ControlGeolocation = class ControlGeolocation extends frappe.ui.f }); } - bind_leaflet_refresh_button() { - L.easyButton({ - id: 'refresh-map-'+this.df.fieldname, - position: 'topright', - type: 'replace', - leafletClasses: true, - states:[{ - stateName: 'refresh-map', - onClick: function(button, map){ - map._onResize(); - }, - title: 'Refresh map', - icon: 'fa fa-refresh' - }] - }).addTo(this.map); - } - add_non_group_layers(source_layer, target_group) { // https://gis.stackexchange.com/a/203773 // Would benefit from https://github.com/Leaflet/Leaflet/issues/4461 From 2a84b18d13afb2cf3491db318f23cf0ad1a0d028 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Mon, 23 Jan 2023 18:59:16 +0100 Subject: [PATCH 002/203] feat: load address and contact display - Handle missing permissions - Split into smaller functions --- frappe/contacts/address_and_contact.py | 70 ++-------------------- frappe/contacts/doctype/address/address.py | 20 +++++++ frappe/contacts/doctype/contact/contact.py | 42 +++++++++++++ 3 files changed, 67 insertions(+), 65 deletions(-) diff --git a/frappe/contacts/address_and_contact.py b/frappe/contacts/address_and_contact.py index 4df32c6705..dc866a8d94 100644 --- a/frappe/contacts/address_and_contact.py +++ b/frappe/contacts/address_and_contact.py @@ -1,77 +1,17 @@ # Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors # License: MIT. See LICENSE -import functools -import re - import frappe from frappe import _ -def load_address_and_contact(doc, key=None): +def load_address_and_contact(doc, key=None) -> None: """Loads address list and contact list in `__onload`""" - from frappe.contacts.doctype.address.address import get_address_display, get_condensed_address + from frappe.contacts.doctype.address.address import get_address_display_list + from frappe.contacts.doctype.contact.contact import get_contact_display_list - filters = [ - ["Dynamic Link", "link_doctype", "=", doc.doctype], - ["Dynamic Link", "link_name", "=", doc.name], - ["Dynamic Link", "parenttype", "=", "Address"], - ] - address_list = frappe.get_list("Address", filters=filters, fields=["*"], order_by="creation asc") - - address_list = [a.update({"display": get_address_display(a)}) for a in address_list] - - address_list = sorted( - address_list, - key=functools.cmp_to_key( - lambda a, b: (int(a.is_primary_address - b.is_primary_address)) - or (1 if a.modified - b.modified else 0) - ), - reverse=True, - ) - - doc.set_onload("addr_list", address_list) - - contact_list = [] - filters = [ - ["Dynamic Link", "link_doctype", "=", doc.doctype], - ["Dynamic Link", "link_name", "=", doc.name], - ["Dynamic Link", "parenttype", "=", "Contact"], - ] - contact_list = frappe.get_list("Contact", filters=filters, fields=["*"]) - - for contact in contact_list: - contact["email_ids"] = frappe.get_all( - "Contact Email", - filters={"parenttype": "Contact", "parent": contact.name, "is_primary": 0}, - fields=["email_id"], - ) - - contact["phone_nos"] = frappe.get_all( - "Contact Phone", - filters={ - "parenttype": "Contact", - "parent": contact.name, - "is_primary_phone": 0, - "is_primary_mobile_no": 0, - }, - fields=["phone"], - ) - - if contact.address: - address = frappe.get_doc("Address", contact.address) - contact["address"] = get_condensed_address(address) - - contact_list = sorted( - contact_list, - key=functools.cmp_to_key( - lambda a, b: (int(a.is_primary_contact - b.is_primary_contact)) - or (1 if a.modified - b.modified else 0) - ), - reverse=True, - ) - - doc.set_onload("contact_list", contact_list) + doc.set_onload("addr_list", get_address_display_list(doc.doctype, doc.name)) + doc.set_onload("contact_list", get_contact_display_list(doc.doctype, doc.name)) def has_permission(doc, ptype, user): diff --git a/frappe/contacts/doctype/address/address.py b/frappe/contacts/doctype/address/address.py index 5fe22eb7f2..70324d1c22 100644 --- a/frappe/contacts/doctype/address/address.py +++ b/frappe/contacts/doctype/address/address.py @@ -289,3 +289,23 @@ def get_condensed_address(doc): def update_preferred_address(address, field): frappe.db.set_value("Address", address, field, 0) + + +def get_address_display_list(doctype: str, name: str) -> list[dict]: + if not frappe.has_permission("Address", "read"): + return [] + + address_list = frappe.get_list( + "Address", + filters=[ + ["Dynamic Link", "link_doctype", "=", doctype], + ["Dynamic Link", "link_name", "=", name], + ["Dynamic Link", "parenttype", "=", "Address"], + ], + fields=["*"], + order_by="is_primary_address DESC, creation ASC", + ) + for a in address_list: + a["display"] = get_address_display(a) + + return address_list diff --git a/frappe/contacts/doctype/contact/contact.py b/frappe/contacts/doctype/contact/contact.py index e7d250148b..e58a5a2b7a 100644 --- a/frappe/contacts/doctype/contact/contact.py +++ b/frappe/contacts/doctype/contact/contact.py @@ -341,3 +341,45 @@ def get_full_name( full_name = company return full_name + + +def get_contact_display_list(doctype: str, name: str) -> list[dict]: + from frappe.contacts.doctype.address.address import get_condensed_address + + if not frappe.has_permission("Contact", "read"): + return [] + + contact_list = frappe.get_list( + "Contact", + filters=[ + ["Dynamic Link", "link_doctype", "=", doctype], + ["Dynamic Link", "link_name", "=", name], + ["Dynamic Link", "parenttype", "=", "Contact"], + ], + fields=["*"], + order_by="is_primary_contact DESC, creation ASC", + ) + + for contact in contact_list: + contact["email_ids"] = frappe.get_all( + "Contact Email", + filters={"parenttype": "Contact", "parent": contact.name, "is_primary": 0}, + fields=["email_id"], + ) + + contact["phone_nos"] = frappe.get_all( + "Contact Phone", + filters={ + "parenttype": "Contact", + "parent": contact.name, + "is_primary_phone": 0, + "is_primary_mobile_no": 0, + }, + fields=["phone"], + ) + + if contact.address and frappe.has_permission("Address", "read"): + address = frappe.get_doc("Address", contact.address) + contact["address"] = get_condensed_address(address) + + return contact_list From 5d838e4fe9bdce3c4637fe68a952cf5fc41bbd20 Mon Sep 17 00:00:00 2001 From: Smit Vora Date: Sat, 15 Apr 2023 19:38:22 +0530 Subject: [PATCH 003/203] fix: set default user value only if enabled --- frappe/public/js/frappe/model/perm.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/frappe/public/js/frappe/model/perm.js b/frappe/public/js/frappe/model/perm.js index 8ed7d6b028..fc501a3ae5 100644 --- a/frappe/public/js/frappe/model/perm.js +++ b/frappe/public/js/frappe/model/perm.js @@ -290,10 +290,9 @@ $.extend(frappe.perm, { const allowed_docs = filtered_perms.map((perm) => perm.doc); if (with_default_doc) { - const default_doc = - allowed_docs.length === 1 - ? allowed_docs - : filtered_perms.filter((perm) => perm.is_default).map((record) => record.doc); + const default_doc = filtered_perms + .filter((perm) => perm.is_default) + .map((record) => record.doc); return { allowed_records: allowed_docs, From acbf784e2a9a2d254948c8d4e53cf218bf65e786 Mon Sep 17 00:00:00 2001 From: Maharshi Patel Date: Thu, 18 May 2023 02:15:09 +0530 Subject: [PATCH 004/203] feat: add fields for onboarding ui tours. Fields required for UI Tours. --- .../doctype/form_tour_settings/__init__.py | 0 .../form_tour_settings/form_tour_settings.js | 8 ++ .../form_tour_settings.json | 51 ++++++++ .../form_tour_settings/form_tour_settings.py | 15 +++ .../test_form_tour_settings.py | 9 ++ .../form_tour_settings_item/__init__.py | 0 .../form_tour_settings_item.json | 61 ++++++++++ .../form_tour_settings_item.py | 9 ++ frappe/desk/doctype/form_tour/form_tour.json | 113 +++++++++++++++++- .../form_tour_step/form_tour_step.json | 112 ++++++++++++++++- 10 files changed, 368 insertions(+), 10 deletions(-) create mode 100644 frappe/core/doctype/form_tour_settings/__init__.py create mode 100644 frappe/core/doctype/form_tour_settings/form_tour_settings.js create mode 100644 frappe/core/doctype/form_tour_settings/form_tour_settings.json create mode 100644 frappe/core/doctype/form_tour_settings/form_tour_settings.py create mode 100644 frappe/core/doctype/form_tour_settings/test_form_tour_settings.py create mode 100644 frappe/core/doctype/form_tour_settings_item/__init__.py create mode 100644 frappe/core/doctype/form_tour_settings_item/form_tour_settings_item.json create mode 100644 frappe/core/doctype/form_tour_settings_item/form_tour_settings_item.py diff --git a/frappe/core/doctype/form_tour_settings/__init__.py b/frappe/core/doctype/form_tour_settings/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frappe/core/doctype/form_tour_settings/form_tour_settings.js b/frappe/core/doctype/form_tour_settings/form_tour_settings.js new file mode 100644 index 0000000000..123e51dbf9 --- /dev/null +++ b/frappe/core/doctype/form_tour_settings/form_tour_settings.js @@ -0,0 +1,8 @@ +// Copyright (c) 2023, Frappe Technologies and contributors +// For license information, please see license.txt + +// frappe.ui.form.on("Form Tour Settings", { +// refresh(frm) { + +// }, +// }); diff --git a/frappe/core/doctype/form_tour_settings/form_tour_settings.json b/frappe/core/doctype/form_tour_settings/form_tour_settings.json new file mode 100644 index 0000000000..15795edde5 --- /dev/null +++ b/frappe/core/doctype/form_tour_settings/form_tour_settings.json @@ -0,0 +1,51 @@ +{ + "actions": [], + "allow_rename": 1, + "creation": "2023-05-11 18:07:26.879273", + "default_view": "List", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "onboarding_tours", + "form_tours" + ], + "fields": [ + { + "fieldname": "form_tours", + "fieldtype": "Table", + "label": "Form Tours", + "options": "Form Tour Settings Item" + }, + { + "default": "\"[]\"", + "fieldname": "onboarding_tours", + "fieldtype": "JSON", + "hidden": 1, + "label": "Onboarding Tours" + } + ], + "index_web_pages_for_search": 1, + "issingle": 1, + "links": [], + "modified": "2023-05-17 16:45:21.362524", + "modified_by": "Administrator", + "module": "Core", + "name": "Form Tour Settings", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "print": 1, + "read": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC", + "states": [] +} \ No newline at end of file diff --git a/frappe/core/doctype/form_tour_settings/form_tour_settings.py b/frappe/core/doctype/form_tour_settings/form_tour_settings.py new file mode 100644 index 0000000000..52b68286d7 --- /dev/null +++ b/frappe/core/doctype/form_tour_settings/form_tour_settings.py @@ -0,0 +1,15 @@ +# Copyright (c) 2023, Frappe Technologies and contributors +# For license information, please see license.txt + +import json + +import frappe +from frappe.model.document import Document + + +class FormTourSettings(Document): + def on_update(self): + onboarding_tours = [[tour.form_tour, json.loads(tour.page_route)] for tour in self.form_tours] + frappe.db.set_single_value( + "Form Tour Settings", "onboarding_tours", json.dumps(onboarding_tours) + ) diff --git a/frappe/core/doctype/form_tour_settings/test_form_tour_settings.py b/frappe/core/doctype/form_tour_settings/test_form_tour_settings.py new file mode 100644 index 0000000000..95838ecb97 --- /dev/null +++ b/frappe/core/doctype/form_tour_settings/test_form_tour_settings.py @@ -0,0 +1,9 @@ +# Copyright (c) 2023, Frappe Technologies and Contributors +# See license.txt + +# import frappe +from frappe.tests.utils import FrappeTestCase + + +class TestFormTourSettings(FrappeTestCase): + pass diff --git a/frappe/core/doctype/form_tour_settings_item/__init__.py b/frappe/core/doctype/form_tour_settings_item/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frappe/core/doctype/form_tour_settings_item/form_tour_settings_item.json b/frappe/core/doctype/form_tour_settings_item/form_tour_settings_item.json new file mode 100644 index 0000000000..54ab61da21 --- /dev/null +++ b/frappe/core/doctype/form_tour_settings_item/form_tour_settings_item.json @@ -0,0 +1,61 @@ +{ + "actions": [], + "allow_rename": 1, + "creation": "2023-05-11 18:10:15.194034", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "form_tour", + "view", + "list_view", + "page_route" + ], + "fields": [ + { + "fieldname": "form_tour", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Form Tour", + "options": "Form Tour", + "read_only": 1 + }, + { + "fetch_from": "form_tour.view_name", + "fieldname": "view", + "fieldtype": "Data", + "in_list_view": 1, + "label": "View", + "read_only": 1 + }, + { + "fetch_from": "form_tour.list_name", + "fieldname": "list_view", + "fieldtype": "Data", + "in_list_view": 1, + "label": "List View", + "read_only": 1 + }, + { + "fetch_from": "form_tour.page_route", + "fieldname": "page_route", + "fieldtype": "Data", + "hidden": 1, + "in_list_view": 1, + "label": "Page Route", + "read_only": 1 + } + ], + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2023-05-17 22:22:58.507769", + "modified_by": "Administrator", + "module": "Core", + "name": "Form Tour Settings Item", + "owner": "Administrator", + "permissions": [], + "sort_field": "modified", + "sort_order": "DESC", + "states": [] +} \ No newline at end of file diff --git a/frappe/core/doctype/form_tour_settings_item/form_tour_settings_item.py b/frappe/core/doctype/form_tour_settings_item/form_tour_settings_item.py new file mode 100644 index 0000000000..0958b000ad --- /dev/null +++ b/frappe/core/doctype/form_tour_settings_item/form_tour_settings_item.py @@ -0,0 +1,9 @@ +# Copyright (c) 2023, Frappe Technologies and contributors +# For license information, please see license.txt + +# import frappe +from frappe.model.document import Document + + +class FormTourSettingsItem(Document): + pass diff --git a/frappe/desk/doctype/form_tour/form_tour.json b/frappe/desk/doctype/form_tour/form_tour.json index 6f3bd56a4e..3890da468a 100644 --- a/frappe/desk/doctype/form_tour/form_tour.json +++ b/frappe/desk/doctype/form_tour/form_tour.json @@ -7,27 +7,39 @@ "engine": "InnoDB", "field_order": [ "title", + "view_name", + "workspace_name", + "list_name", + "report_name", + "dashboard_name", + "new_document_form", + "page_name", "reference_doctype", "module", "column_break_6", + "ui_tour", + "track_steps", + "reset_tours", "is_standard", "save_on_complete", "first_document", "include_name_field", + "page_route", "section_break_3", "steps" ], "fields": [ { + "depends_on": "eval:(!doc.ui_tour || doc.is_ui_tour && [\"Workspaces\", \"Page\", \"Tree\"].indexOf(doc.view_name) == -1);", "fieldname": "reference_doctype", "fieldtype": "Link", "in_list_view": 1, "label": "Reference Document", - "options": "DocType", - "reqd": 1 + "mandatory_depends_on": "eval:(!doc.ui_tour)", + "options": "DocType" }, { - "depends_on": "reference_doctype", + "depends_on": "eval:(doc.ui_tour || doc.reference_doctype)", "fieldname": "steps", "fieldtype": "Table", "label": "Steps", @@ -47,6 +59,7 @@ }, { "default": "0", + "depends_on": "eval:(!doc.ui_tour)", "fieldname": "save_on_complete", "fieldtype": "Check", "label": "Save on Completion" @@ -72,21 +85,110 @@ }, { "default": "0", + "depends_on": "eval:(!doc.ui_tour)", "fieldname": "first_document", "fieldtype": "Check", "label": "Show First Document Tour" }, { "default": "0", - "depends_on": "eval:!doc.first_document", + "depends_on": "eval:(!doc.ui_tour && !doc.first_document)", "fieldname": "include_name_field", "fieldtype": "Check", "label": "Include Name Field" + }, + { + "default": "0", + "fieldname": "ui_tour", + "fieldtype": "Check", + "label": "UI Tour", + "set_only_once": 1 + }, + { + "depends_on": "is_ui_tour", + "fieldname": "page_route", + "fieldtype": "JSON", + "hidden": 1, + "label": "Page Route" + }, + { + "default": "0", + "depends_on": "ui_tour", + "description": "Please check this if you want to reset this tour and show it to all users.", + "fieldname": "reset_tours", + "fieldtype": "Check", + "label": "Reset Tours" + }, + { + "depends_on": "eval:(doc.ui_tour && doc.view_name == \"List\" && doc.list_name == \"Dashboard\")", + "fetch_from": ".", + "fieldname": "dashboard_name", + "fieldtype": "Link", + "label": "Select Dashboard", + "options": "Dashboard" + }, + { + "depends_on": "ui_tour", + "fieldname": "view_name", + "fieldtype": "Select", + "label": "View", + "mandatory_depends_on": "ui_tour", + "options": "Workspaces\nList\nForm\nTree\nPage" + }, + { + "depends_on": "eval:(doc.ui_tour && doc.view_name == \"Workspaces\")", + "fetch_from": ".", + "fieldname": "workspace_name", + "fieldtype": "Link", + "label": "Select Workspace", + "options": "Workspace" + }, + { + "depends_on": "eval:(doc.ui_tour && doc.view_name == \"Page\")", + "fetch_from": ".", + "fieldname": "page_name", + "fieldtype": "Link", + "label": "Select Page", + "mandatory_depends_on": "eval:(doc.ui_tour && doc.view_name == \"Page\")", + "options": "Page" + }, + { + "default": "List", + "depends_on": "eval:(doc.ui_tour && doc.view_name == \"List\")", + "fetch_from": ".", + "fieldname": "list_name", + "fieldtype": "Select", + "label": "Select List View", + "mandatory_depends_on": "eval:(doc.ui_tour && doc.view_name == \"List\")", + "options": "List\nReport\nDashboard\nKanban\nGantt\nCalendar\nFile\nImage\nInbox\nMap" + }, + { + "depends_on": "eval:(doc.ui_tour && doc.view_name == \"List\" && doc.list_name == \"Report\")", + "fetch_from": ".", + "fieldname": "report_name", + "fieldtype": "Link", + "label": "Select Report", + "options": "Report" + }, + { + "default": "0", + "depends_on": "ui_tour", + "description": "The next tour will start from where the user left off.", + "fieldname": "track_steps", + "fieldtype": "Check", + "label": "Track Steps" + }, + { + "default": "0", + "depends_on": "eval: (doc.ui_tour && doc.view_name == \"Form\")", + "fieldname": "new_document_form", + "fieldtype": "Check", + "label": "New Document Form" } ], "index_web_pages_for_search": 1, "links": [], - "modified": "2021-11-24 12:03:45.449311", + "modified": "2023-05-18 01:28:24.593730", "modified_by": "Administrator", "module": "Desk", "name": "Form Tour", @@ -108,5 +210,6 @@ ], "sort_field": "modified", "sort_order": "DESC", + "states": [], "track_changes": 1 } \ No newline at end of file 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 7eb6eab223..f0cb8751b7 100644 --- a/frappe/desk/doctype/form_tour_step/form_tour_step.json +++ b/frappe/desk/doctype/form_tour_step/form_tour_step.json @@ -4,18 +4,29 @@ "doctype": "DocType", "engine": "InnoDB", "field_order": [ + "ui_tour", "is_table_field", "section_break_2", + "title", "parent_fieldname", "fieldname", - "title", + "element_selector", + "parent_element_selector", "description", + "ondemand_description", "column_break_2", "position", + "hide_buttons", + "popover_element", + "modal_trigger", + "offset_x", + "offset_y", + "next_on_click", "label", "fieldtype", "has_next_condition", "next_step_condition", + "next_form_tour", "section_break_13", "child_doctype" ], @@ -31,18 +42,20 @@ "columns": 4, "fieldname": "description", "fieldtype": "HTML Editor", + "ignore_xss_filter": 1, "in_list_view": 1, "label": "Description", "reqd": 1 }, { - "depends_on": "eval: (!doc.is_table_field || (doc.is_table_field && doc.parent_fieldname))", + "depends_on": "eval: (!doc.ui_tour && (!doc.is_table_field || (doc.is_table_field && doc.parent_fieldname)))", "fieldname": "fieldname", "fieldtype": "Select", "label": "Fieldname", - "reqd": 1 + "mandatory_depends_on": "eval: (!doc.ui_tour)" }, { + "depends_on": "eval:(!doc.ui_tour)", "fieldname": "label", "fieldtype": "Data", "in_list_view": 1, @@ -70,12 +83,14 @@ }, { "default": "0", + "depends_on": "eval:(!doc.ui_tour)", "fieldname": "has_next_condition", "fieldtype": "Check", "label": "Has Next Condition" }, { "default": "0", + "depends_on": "eval:(!doc.ui_tour)", "fieldname": "fieldtype", "fieldtype": "Data", "label": "Fieldtype", @@ -83,6 +98,7 @@ }, { "default": "0", + "depends_on": "eval:(!doc.ui_tour)", "fieldname": "is_table_field", "fieldtype": "Check", "label": "Is Table Field" @@ -105,17 +121,103 @@ "read_only": 1 }, { - "depends_on": "is_table_field", + "depends_on": "eval: (!doc.ui_tour || doc.is_table_field)", "fieldname": "parent_fieldname", "fieldtype": "Select", "label": "Parent Field", "mandatory_depends_on": "is_table_field" + }, + { + "default": "0", + "fetch_from": "next_form_tour.ui_tour", + "fieldname": "ui_tour", + "fieldtype": "Check", + "in_list_view": 1, + "label": "UI Tour" + }, + { + "depends_on": "eval:(doc.ui_tour)", + "description": "CSS selector for the element you want to highlight.", + "fieldname": "element_selector", + "fieldtype": "Data", + "label": "Element Selector", + "mandatory_depends_on": "eval:(doc.ui_tour)", + "reqd": 1 + }, + { + "depends_on": "eval:(doc.ui_tour)", + "description": "Mozilla doesn't support :has() so you can pass parent selector here as workaround", + "fieldname": "parent_element_selector", + "fieldtype": "Data", + "label": "Parent Element Selector" + }, + { + "depends_on": "eval:(doc.ui_tour)", + "fieldname": "next_form_tour", + "fieldtype": "Link", + "label": "Next Form Tour", + "options": "Form Tour" + }, + { + "default": "0", + "depends_on": "eval:(doc.ui_tour)", + "description": "Hide Previous, Next and Close button on highlight dialog.", + "fieldname": "hide_buttons", + "fieldtype": "Check", + "label": "Hide Buttons" + }, + { + "default": "0", + "depends_on": "eval:(doc.ui_tour)", + "description": "Move to next step when clicked inside highlighted area.", + "fieldname": "next_on_click", + "fieldtype": "Check", + "label": "Next on Click" + }, + { + "default": "0", + "depends_on": "eval:(doc.ui_tour)", + "description": "when clicked on element it will focus popover if present.", + "fieldname": "popover_element", + "fieldtype": "Check", + "label": "Popover Element" + }, + { + "default": "0", + "depends_on": "eval:(doc.ui_tour)", + "fieldname": "offset_x", + "fieldtype": "Int", + "label": "Offset X" + }, + { + "default": "0", + "depends_on": "eval:(doc.ui_tour)", + "fieldname": "offset_y", + "fieldtype": "Int", + "label": "Offset Y" + }, + { + "default": "0", + "depends_on": "eval:(doc.ui_tour)", + "description": "Enable if on click\nopens modal.", + "fieldname": "modal_trigger", + "fieldtype": "Check", + "label": "Modal Trigger" + }, + { + "columns": 4, + "depends_on": "eval: (doc.popover_element || doc.modal_trigger)", + "fieldname": "ondemand_description", + "fieldtype": "HTML Editor", + "ignore_xss_filter": 1, + "in_list_view": 1, + "label": "Popover or Modal Description" } ], "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2022-01-27 15:18:36.481801", + "modified": "2023-05-18 01:55:44.245357", "modified_by": "Administrator", "module": "Desk", "name": "Form Tour Step", From 10628b9b060032e4c89f9ba3833b6e693681a148 Mon Sep 17 00:00:00 2001 From: Maharshi Patel Date: Thu, 18 May 2023 02:33:40 +0530 Subject: [PATCH 005/203] feat: add onboarding and status on boot Add List of all onboarding tours to frappe.boot and onboarding status on frappe.boot.user --- frappe/boot.py | 3 +++ frappe/core/doctype/user/user.json | 18 ++++++++++++++++-- frappe/utils/user.py | 2 ++ 3 files changed, 21 insertions(+), 2 deletions(-) diff --git a/frappe/boot.py b/frappe/boot.py index 83c9902020..7f4677c249 100644 --- a/frappe/boot.py +++ b/frappe/boot.py @@ -68,6 +68,9 @@ def get_bootinfo(): bootinfo.home_folder = frappe.db.get_value("File", {"is_home_folder": 1}) bootinfo.navbar_settings = get_navbar_settings() bootinfo.notification_settings = get_notification_settings() + bootinfo.onboarding_tours = frappe.parse_json( + frappe.db.get_single_value("Form Tour Settings", "onboarding_tours") or "[]" + ) set_time_zone(bootinfo) # ipinfo diff --git a/frappe/core/doctype/user/user.json b/frappe/core/doctype/user/user.json index 00e1cffa88..683063700f 100644 --- a/frappe/core/doctype/user/user.json +++ b/frappe/core/doctype/user/user.json @@ -92,6 +92,8 @@ "generate_keys", "column_break_65", "api_secret", + "onboarding_tours_section", + "onboarding_status", "connections_tab" ], "fields": [ @@ -691,6 +693,18 @@ "fieldname": "desk_settings_section", "fieldtype": "Section Break", "label": "Desk Settings" + }, + { + "fieldname": "onboarding_tours_section", + "fieldtype": "Section Break", + "hidden": 1, + "label": "Onboarding Tours" + }, + { + "fieldname": "onboarding_status", + "fieldtype": "JSON", + "hidden": 1, + "label": "Onboarding Status" } ], "icon": "fa fa-user", @@ -753,7 +767,7 @@ "link_fieldname": "user" } ], - "modified": "2022-09-19 16:05:46.485242", + "modified": "2023-05-17 22:56:28.260931", "modified_by": "Administrator", "module": "Core", "name": "User", @@ -792,4 +806,4 @@ "states": [], "title_field": "full_name", "track_changes": 1 -} +} \ No newline at end of file diff --git a/frappe/utils/user.py b/frappe/utils/user.py index 8dcb2b7ca3..f37e52f7be 100644 --- a/frappe/utils/user.py +++ b/frappe/utils/user.py @@ -221,6 +221,7 @@ class UserPermissions: "mute_sounds", "send_me_a_copy", "user_type", + "onboarding_status", ], as_dict=True, ) @@ -229,6 +230,7 @@ class UserPermissions: self.build_permissions() d.name = self.name + d.onboarding_status = frappe.parse_json(d.onboarding_status) d.roles = self.get_roles() d.defaults = self.get_defaults() for key in ( From 458f03525e77a969fd54c5aa86ab8e978505154b Mon Sep 17 00:00:00 2001 From: Maharshi Patel Date: Thu, 18 May 2023 02:58:05 +0530 Subject: [PATCH 006/203] fix: typo in reference_doctype depends_on --- frappe/desk/doctype/form_tour/form_tour.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frappe/desk/doctype/form_tour/form_tour.json b/frappe/desk/doctype/form_tour/form_tour.json index 3890da468a..9cc3f65b96 100644 --- a/frappe/desk/doctype/form_tour/form_tour.json +++ b/frappe/desk/doctype/form_tour/form_tour.json @@ -30,7 +30,7 @@ ], "fields": [ { - "depends_on": "eval:(!doc.ui_tour || doc.is_ui_tour && [\"Workspaces\", \"Page\", \"Tree\"].indexOf(doc.view_name) == -1);", + "depends_on": "eval:(!doc.ui_tour || doc.ui_tour && [\"Workspaces\", \"Page\", \"Tree\"].indexOf(doc.view_name) == -1);", "fieldname": "reference_doctype", "fieldtype": "Link", "in_list_view": 1, @@ -188,7 +188,7 @@ ], "index_web_pages_for_search": 1, "links": [], - "modified": "2023-05-18 01:28:24.593730", + "modified": "2023-05-18 02:47:03.528693", "modified_by": "Administrator", "module": "Desk", "name": "Form Tour", From 8bd0ab6db4748beaf5fc74d323e0ea2a5197560b Mon Sep 17 00:00:00 2001 From: Maharshi Patel Date: Thu, 18 May 2023 03:04:36 +0530 Subject: [PATCH 007/203] feat: form tour logic and ui tour routes on client-side added some form logic required by ui tours and to generate page_route based on conditions. on server-side added logic to update Form Tour Settings. --- frappe/desk/doctype/form_tour/form_tour.js | 181 +++++++++++++++++---- frappe/desk/doctype/form_tour/form_tour.py | 58 +++++-- 2 files changed, 191 insertions(+), 48 deletions(-) diff --git a/frappe/desk/doctype/form_tour/form_tour.js b/frappe/desk/doctype/form_tour/form_tour.js index 1e67e10779..df22f3fd9e 100644 --- a/frappe/desk/doctype/form_tour/form_tour.js +++ b/frappe/desk/doctype/form_tour/form_tour.js @@ -2,36 +2,64 @@ // For license information, please see license.txt 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); - let route_changed = null; - - if (issingle) { - route_changed = frappe.set_route("Form", frm.doc.reference_doctype); - } else if (frm.doc.first_document) { - const name = await get_first_document(frm.doc.reference_doctype); - route_changed = frappe.set_route("Form", frm.doc.reference_doctype, name); - } else { - route_changed = frappe.set_route("Form", frm.doc.reference_doctype, "new"); + frm.fields_dict["report_name"].get_query = function (doc) { + if (doc.reference_doctype) { + return { + filters: { + ref_doctype: doc.reference_doctype, + }, + }; } - route_changed.then(() => { - const tour_name = frm.doc.name; - cur_frm.tour.init({ tour_name }).then(() => cur_frm.tour.start()); - }); - }); - }, + return {}; + }; + frm.fields_dict["reference_doctype"].get_query = function (doc) { + return { + filters: { + istable: 0, + }, + }; + }; + !frm.doc.ui_tour && + frm.add_custom_button(__("Show Tour"), async () => { + const issingle = await check_if_single(frm.doc.reference_doctype); + let route_changed = null; + if (issingle) { + route_changed = frappe.set_route("Form", frm.doc.reference_doctype); + } else if (frm.doc.first_document) { + const name = await get_first_document(frm.doc.reference_doctype); + route_changed = frappe.set_route("Form", frm.doc.reference_doctype, name); + } else { + route_changed = frappe.set_route("Form", frm.doc.reference_doctype, "new"); + } + route_changed.then(() => { + const tour_name = frm.doc.name; + cur_frm.tour.init({ tour_name }).then(() => cur_frm.tour.start()); + }); + }); + }, + async report_name(frm) { + if (!frm.doc.report_name) return; + let { message } = await frappe.db.get_value("Report", frm.doc.report_name, "ref_doctype"); + frm.set_value("reference_doctype", message?.ref_doctype || ""); + }, + async before_save(frm) { + if ( + frm.doc.select_view == "List" && + frm.doc.list_name == "Dashboard" && + frm.doc.dashboard_name && + frm.doc.reference_doctype + ) { + frappe.throw( + "Referance Doctype and Dashboard Name both can't be used at the same time." + ); + } + frm.doc.page_route = JSON.stringify(await get_path(frm)); + }, disable_form: function (frm) { frm.set_read_only(); frm.fields @@ -42,18 +70,6 @@ frappe.ui.form.on("Form Tour", { frm.disable_save(); }, - setup_queries(frm) { - frm.set_query("reference_doctype", function () { - return { - filters: { - istable: 0, - }, - }; - }); - - frm.trigger("reference_doctype"); - }, - reference_doctype(frm) { if (!frm.doc.reference_doctype) return; @@ -78,6 +94,23 @@ frappe.ui.form.on("Form Tour", { [""].concat(options) ); }); + // remove report name if reference doctype is changed and report name is not valid. + frappe.db + .get_list( + "Report", + { + filters: { + ref_doctype: frm.doc.reference_doctype, + }, + }, + { fields: ["name"] } + ) + .then((reports) => { + if (reports.findIndex((r) => r.name == frm.doc.report_name) == -1) { + frm.set_value("report_name", ""); + frm.refresh_field("report_name"); + } + }); }, }); @@ -115,6 +148,10 @@ async function check_if_single(doctype) { const { message } = await frappe.db.get_value("DocType", doctype, "issingle"); return message.issingle || 0; } +async function check_if_private_workspace(name) { + const { message } = await frappe.db.get_value("Workspace", name, "public"); + return !message.public || 0; +} async function get_first_document(doctype) { let docname; @@ -125,3 +162,75 @@ async function get_first_document(doctype) { return docname || "new"; } + +async function get_path(frm) { + let route = [frm.doc.view_name]; + switch (route[0]) { + case "Workspaces": + frm.doc.list_name = ""; + frm.doc.new_document_form = 0; + frm.doc.report_name = ""; + frm.doc.page_name = ""; + frm.doc.dashboard_name = ""; + frm.doc.reference_doctype = ""; + if (!frm.doc.workspace_name) { + route.push("*"); + return route; + } + if (await check_if_private_workspace(frm.doc.workspace_name)) { + route.push("private"); + } + route.push(frm.doc.workspace_name); + return route; + case "List": + frm.doc.workspace_name = ""; + frm.doc.new_document_form = 0; + frm.doc.list_name != "Report" && (frm.doc.report_name = ""); + frm.doc.page_name = ""; + frm.doc.dashboard_name = ""; + if (frm.doc.list_name == "File") return ["List", "File"]; + if (!frm.doc.reference_doctype) { + if (frm.doc.list_name == "Dashboard") + return ["dashboard-view", frm.doc.dashboard_name || "*"]; + route.push("*"); + } else { + route.push(frm.doc.reference_doctype); + } + route.push(frm.doc.list_name); + return route; + case "Form": + frm.doc.workspace_name = ""; + frm.doc.list_name = ""; + frm.doc.report_name = ""; + frm.doc.page_name = ""; + frm.doc.dashboard_name = ""; + if (!frm.doc.reference_doctype) { + route.push("*"); + frm.doc.new_document_form && route.push("new-*"); + return route; + } + route.push(frm.doc.reference_doctype); + if (await check_if_single(frm.doc.reference_doctype)) { + route.push(frm.doc.reference_doctype); + } else if (frm.doc.new_document_form) { + route.push("new-" + frappe.router.slug(frm.doc.reference_doctype)); + } + return route; + case "Tree": + frm.doc.workspace_name = ""; + frm.doc.list_name = ""; + frm.doc.new_document_form = 0; + frm.doc.report_name = ""; + frm.doc.page_name = ""; + frm.doc.dashboard_name = ""; + return route; + case "Page": + frm.doc.workspace_name = ""; + frm.doc.list_name = ""; + frm.doc.new_document_form = 0; + frm.doc.report_name = ""; + frm.doc.dashboard_name = ""; + frm.doc.reference_doctype = ""; + return [frm.doc.page_name]; + } +} diff --git a/frappe/desk/doctype/form_tour/form_tour.py b/frappe/desk/doctype/form_tour/form_tour.py index 6248b43e62..48b2bd1f2d 100644 --- a/frappe/desk/doctype/form_tour/form_tour.py +++ b/frappe/desk/doctype/form_tour/form_tour.py @@ -8,20 +8,54 @@ from frappe.modules.export_file import export_to_files class FormTour(Document): def before_save(self): - meta = frappe.get_meta(self.reference_doctype) - for step in self.steps: - if step.is_table_field and step.parent_fieldname: - parent_field_df = meta.get_field(step.parent_fieldname) - step.child_doctype = parent_field_df.options + if not self.ui_tour: + meta = frappe.get_meta(self.reference_doctype) + for step in self.steps: + if step.is_table_field and step.parent_fieldname: + parent_field_df = meta.get_field(step.parent_fieldname) + step.child_doctype = parent_field_df.options - field_df = frappe.get_meta(step.child_doctype).get_field(step.fieldname) - step.label = field_df.label - step.fieldtype = field_df.fieldtype - else: - field_df = meta.get_field(step.fieldname) - step.label = field_df.label - step.fieldtype = field_df.fieldtype + field_df = frappe.get_meta(step.child_doctype).get_field(step.fieldname) + step.label = field_df.label + step.fieldtype = field_df.fieldtype + else: + field_df = meta.get_field(step.fieldname) + step.label = field_df.label + step.fieldtype = field_df.fieldtype + elif self.reset_tours: + self.reset_tours = 0 + for user in frappe.get_all("User"): + user_doc = frappe.get_doc("User", user.name) + onboarding_status = frappe.parse_json(user_doc.onboarding_status) + if self.name in onboarding_status: + del onboarding_status[self.name] + user_doc.onboarding_status = frappe.as_json(onboarding_status) + user_doc.save() def on_update(self): if frappe.conf.developer_mode and self.is_standard: export_to_files([["Form Tour", self.name]], self.module) + if self.ui_tour: + form_tour_settings = frappe.get_doc("Form Tour Settings", "Form Tour Settings") + in_settings = False + child_index = 0 + for tour in form_tour_settings.form_tours: + if tour.form_tour == self.name: + in_settings = True + child_index = tour.idx + form_tour_settings.remove(tour) + if not in_settings: + child_index = len(form_tour_settings.form_tours) + 1 + child = frappe.new_doc("Form Tour Settings Item") + child.update( + { + "idx": child_index, + "form_tour": self.name, + "parent": "Form Tour Settings", + "parentfield": "form_tours", + "parenttype": "Form Tour Settings", + } + ) + child.save() + form_tour_settings.form_tours.insert(child_index, child) + form_tour_settings.save() From 7ee486c89607a5a4acb98f76f77d7a1242ace69a Mon Sep 17 00:00:00 2001 From: Maharshi Patel Date: Thu, 18 May 2023 03:54:48 +0530 Subject: [PATCH 008/203] fix: clear dashboard_name only if list name is not Dashboard --- frappe/desk/doctype/form_tour/form_tour.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/desk/doctype/form_tour/form_tour.js b/frappe/desk/doctype/form_tour/form_tour.js index df22f3fd9e..8c14e72f56 100644 --- a/frappe/desk/doctype/form_tour/form_tour.js +++ b/frappe/desk/doctype/form_tour/form_tour.js @@ -186,8 +186,8 @@ async function get_path(frm) { frm.doc.workspace_name = ""; frm.doc.new_document_form = 0; frm.doc.list_name != "Report" && (frm.doc.report_name = ""); + frm.doc.list_name != "Dashboard" && (frm.doc.dashboard_name = ""); frm.doc.page_name = ""; - frm.doc.dashboard_name = ""; if (frm.doc.list_name == "File") return ["List", "File"]; if (!frm.doc.reference_doctype) { if (frm.doc.list_name == "Dashboard") From b7541cc163e977c8c7b1b19d70e83b43a6900d52 Mon Sep 17 00:00:00 2001 From: Maharshi Patel Date: Thu, 18 May 2023 03:58:37 +0530 Subject: [PATCH 009/203] feat: onboarding class and tour on router change. Added frappe.router on_change to find matching tours based on where user is and run them using frappe.ui.OnboardingTour. --- frappe/hooks.py | 1 + frappe/public/js/onboarding.bundle.js | 345 ++++++++++++++++++++++++++ 2 files changed, 346 insertions(+) create mode 100644 frappe/public/js/onboarding.bundle.js diff --git a/frappe/hooks.py b/frappe/hooks.py index 6d8c00d483..5eb9426f2e 100644 --- a/frappe/hooks.py +++ b/frappe/hooks.py @@ -30,6 +30,7 @@ app_include_js = [ "controls.bundle.js", "report.bundle.js", "telemetry.bundle.js", + "onboarding.bundle.js", ] app_include_css = [ "desk.bundle.css", diff --git a/frappe/public/js/onboarding.bundle.js b/frappe/public/js/onboarding.bundle.js new file mode 100644 index 0000000000..d96b350a70 --- /dev/null +++ b/frappe/public/js/onboarding.bundle.js @@ -0,0 +1,345 @@ +frappe.ui.OnboardingTour = class OnboardingTour { + constructor() { + this.driver_steps = []; + this.last_step_saved = null; + this.last_element_clicked = null; + } + + init_driver() { + this.driver = new frappe.Driver({ + className: "frappe-driver", + allowClose: false, + padding: 10, + overlayClickNext: false, + keyboardControl: true, + nextBtnText: "Next", + prevBtnText: "Previous", + opacity: 0.25, + onHighlighted: step => { + frappe.ui.next_form_tour = step.options.step_info?.next_form_tour; + const wait_for_node = setInterval(() => { + if (!step.popover.node) return; + if (step.options.step_info?.offset_x) { + step.popover.node.style.left = `${step.popover.node.offsetLeft + + step.options.step_info.offset_x}px`; + } + if (step.options.step_info?.offset_y) { + step.popover.node.style.top = `${step.popover.node.offsetTop + + step.options.step_info.offset_y}px`; + } + if (step.popover.closeBtnNode) { + step.popover.closeBtnNode.onclick = () => { + if (!this.driver.hasNextStep()) { + this.on_finish && this.on_finish(); + !frappe.boot.user.onboarding_status[this.tour.name] && + (frappe.boot.user.onboarding_status[this.tour.name] = {}); + frappe.boot.user.onboarding_status[ + this.tour.name + ].is_complete = true; + frappe.utils.debounce( + () => + frappe.db.set_value( + "User", + frappe.boot.user.name, + "onboarding_status", + JSON.stringify(frappe.boot.user.onboarding_status) + ), + 1000 + )(); + } + }; + } + clearInterval(wait_for_node); + }, 300); + + // focus on first input. + // TODO : later add option to select which input to focus as well. + const $input = $(step.node) + .find("input") + .get(0); + if ($input) frappe.utils.sleep(200).then(() => $input.focus()); + } + }); + } + + async init({ tour_name, start_step }) { + this.tour = await frappe.db.get_doc("Form Tour", tour_name); + this.init_driver(); + this.build_steps(); + this.update_driver_steps(); + if (!this.tour.track_steps) { + start_step = 0; + } + this.start(start_step); + } + + build_steps() { + this.driver_steps = []; + this.tour.steps.forEach(step => { + const on_next = async el => { + const step_index = this.driver.steps.indexOf(el); + if (step_index == -1 || this.last_step_saved?.name == step.name) return; + frappe.boot.user.onboarding_status[this.tour.name] = { + steps_complete: step_index + }; + if (!this.driver.hasNextStep()) { + this.on_finish && this.on_finish(); + frappe.boot.user.onboarding_status[this.tour.name].is_complete = true; + } + this.last_step_saved = step; + frappe.utils.debounce( + () => + frappe.db.set_value( + "User", + frappe.boot.user.name, + "onboarding_status", + JSON.stringify(frappe.boot.user.onboarding_status) + ), + 1000 + )(); + }; + const driver_step = this.get_step(step, on_next); + driver_step.element && this.driver_steps.push(driver_step); + }); + } + + get_step(step_info, on_next) { + const { + name, + element_selector, + title, + description, + ondemand_description, + position, + parent_element_selector, + hide_buttons, + next_on_click, + popover_element, + modal_trigger + } = step_info; + let element = cur_page?.page.querySelector(element_selector); + !element && (element = document.querySelector(element_selector)); + if (parent_element_selector) { + element = element.closest(parent_element_selector); + } + if (element && (next_on_click || hide_buttons || modal_trigger)) { + $(element).on("click", () => { + if ( + !this.driver.getHighlightedElement() || + this.driver.getHighlightedElement().node.id?.startsWith("popover") + ) + return; + + if ( + modal_trigger && + (!this.last_element_clicked || + new Date().getTime() - new Date(this.last_element_clicked).getTime() > + 1000) + ) { + this.last_element_clicked = new Date().getTime(); + this.handle_modal_steps(this.driver.currentStep, title, ondemand_description); + return; + } + + if (!popover_element) { + on_next(this.driver.getHighlightedElement()); + this.driver.moveNext(); + this.driver.overlay.refresh(); + } + + if (!this.driver.getHighlightedElement()) return; + on_next(this.driver.getHighlightedElement()); + let popover = this.driver + .getHighlightedElement() + .node.getAttribute("aria-describedby") + ? this.driver.getHighlightedElement().node + : this.driver + .getHighlightedElement() + .node.querySelector('[aria-describedby^="popover"]'); + + if (!popover) return; + + let popover_id = popover.getAttribute("aria-describedby"); + let step_index = this.driver.steps.indexOf(this.driver.getHighlightedElement()); + + if (this.driver_steps[step_index + 1]?.element.id == popover_id) return; + + this.driver_steps = this.driver_steps.filter( + step => !step.element.id?.startsWith("popover") + ); + + let new_step = { ...this.driver_steps[step_index] }; + new_step.element = document.getElementById(popover_id); + new_step.showButtons = false; + ondemand_description && (new_step.popover.description = ondemand_description); + + this.driver_steps.splice(this.driver.currentStep + 1, 0, new_step); + this.update_driver_steps(); + this.driver.moveNext(); + this.driver.overlay.refresh(); + + $(popover).one("hide.bs.popover", e => { + this.driver_steps.splice(this.driver.currentStep, 1); + this.driver_steps[this.driver.currentStep - 1].showButtons = true; + new_step.popover.description = description; + this.update_driver_steps(); + this.driver.movePrevious(); + this.driver.overlay.refresh(); + }); + }); + } + + let showButtons = true; + if (popover_element || hide_buttons) { + showButtons = false; + } + return { + element, + name, + popover: { + title, + description, + position: frappe.router.slug(position || "Bottom") + }, + onNext: on_next, + step_info: step_info, + showButtons + }; + } + + 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); + } + + handle_modal_steps(step, title, description) { + setTimeout(() => { + const modal_element = $(".modal-content"); + const attach_dialog_step = { + element: modal_element[0], + allowClose: false, + overlayClickNext: false, + popover: { + title, + description, + position: "left-center", + doneBtnText: __("Next") + } + }; + this.driver_steps.splice(step + 1, 0, attach_dialog_step); + this.update_driver_steps(); // need to define again, since driver.js only considers steps which are inside DOM + this.driver.reset(); + this.driver.start(step + 1); + this.driver.overlay.refresh(); + modal_element.closest(".modal").one("hide.bs.modal", () => { + this.driver_steps.splice(this.driver.currentStep, 1); + this.update_driver_steps(); + this.driver.movePrevious(); + this.driver.moveNext(); + this.driver.overlay.refresh(); + }); + }, 500); + } +}; +// As of now Tours are only for desktop as it is annoying on mobile. +// Also lot of elements are hidden on mobile so until we find a better way to do it. +if (window.matchMedia("(max-device-width: 992px)").matches) return; + +frappe.router.on("change", () => { + let route = frappe.router.current_route; + + if (route[0] === "") return; + + let tour_name; + let matching_tours = []; + let start_step; + if (route[0] == "query-report") { + route = ["List", route[1], "Report"]; + } + if (route[0] != "dashboard-view") { + frappe.boot.onboarding_tours && + frappe.boot.onboarding_tours.forEach(tour => { + let tour_route = tour[1]; + length = Math.min(route.length, tour_route.length); + if (length >= 1 && route[0] != tour_route[0]) return; + if (length >= 2 && tour_route[1] != "*" && route[1] != tour_route[1]) return; + if ( + length >= 3 && + ["*", "new-*"].indexOf(tour_route[2]) == -1 && + route[2] != tour_route[2] + ) + return; + matching_tours.push(tour); + }); + } + console.log(matching_tours); + matching_tours = matching_tours.filter(tour => { + if (frappe.boot.user.onboarding_status[tour[0]]?.is_complete == true) return false; + return true; + }); + matching_tours = matching_tours.map(tour => { + if (frappe.boot.user.onboarding_status[tour[0]]?.steps_complete != undefined) { + tour.push(frappe.boot.user.onboarding_status[tour[0]].steps_complete); + } + return tour; + }); + if (matching_tours.length == 0) return; + let current_tour = matching_tours.find( + tour => tour[0] == frappe.ui.currentTourInstance?.tour.name + ); + if (current_tour) { + tour_name = current_tour[0]; + start_step = current_tour.at(-1); + if (typeof start_step != "number") { + start_step = 0; + } + } else if (frappe.ui.next_form_tour) { + let current_tour = matching_tours.find(tour => tour[0] == frappe.ui.next_form_tour); + tour_name = current_tour[0]; + start_step = current_tour.at(-1); + if (typeof start_step != "number") { + start_step = 0; + } else { + start_step += 1; + } + frappe.ui.next_form_tour = undefined; + } else { + tour_name = matching_tours[0][0]; + start_step = matching_tours[0].at(-1); + if (typeof start_step != "number") { + start_step = 0; + } else { + start_step += 1; + } + } + if (!tour_name) return; + if (frappe.ui.currentTourInstance) { + frappe.ui.currentTourInstance.driver_steps = []; + frappe.ui.currentTourInstance.driver.reset(true); + frappe.ui.currentTourInstance.update_driver_steps(); + } + const tour = (frappe.ui.currentTourInstance = new frappe.ui.OnboardingTour()); + // wait for workspace and/or data to load. + const wait_for_data = setInterval(() => { + if (cur_page?.page.querySelector(".workspace-sidebar-skeleton")) return; + if (cur_page?.page.querySelector(".workspace-skeleton")) return; + if (document.body.getAttribute("data-ajax-state") === "complete") { + frappe.utils.sleep(500).then(() => { + tour.init({ + tour_name, + start_step + }); + clearInterval(wait_for_data); + }); + } + }, 100); +}); From d4997160e892f4547da85cea272b0a9ca662b6f8 Mon Sep 17 00:00:00 2001 From: Maharshi Patel Date: Thu, 18 May 2023 12:25:53 +0530 Subject: [PATCH 010/203] fix(minor): use matching next_tour if found --- frappe/public/js/onboarding.bundle.js | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/frappe/public/js/onboarding.bundle.js b/frappe/public/js/onboarding.bundle.js index d96b350a70..bd68b5facf 100644 --- a/frappe/public/js/onboarding.bundle.js +++ b/frappe/public/js/onboarding.bundle.js @@ -14,7 +14,7 @@ frappe.ui.OnboardingTour = class OnboardingTour { keyboardControl: true, nextBtnText: "Next", prevBtnText: "Previous", - opacity: 0.25, + opacity: 0.5, onHighlighted: step => { frappe.ui.next_form_tour = step.options.step_info?.next_form_tour; const wait_for_node = setInterval(() => { @@ -281,7 +281,6 @@ frappe.router.on("change", () => { matching_tours.push(tour); }); } - console.log(matching_tours); matching_tours = matching_tours.filter(tour => { if (frappe.boot.user.onboarding_status[tour[0]]?.is_complete == true) return false; return true; @@ -296,21 +295,21 @@ frappe.router.on("change", () => { let current_tour = matching_tours.find( tour => tour[0] == frappe.ui.currentTourInstance?.tour.name ); + let next_tour = matching_tours.find(tour => tour[0] == frappe.ui.next_form_tour); if (current_tour) { tour_name = current_tour[0]; start_step = current_tour.at(-1); if (typeof start_step != "number") { start_step = 0; } - } else if (frappe.ui.next_form_tour) { - let current_tour = matching_tours.find(tour => tour[0] == frappe.ui.next_form_tour); - tour_name = current_tour[0]; - start_step = current_tour.at(-1); - if (typeof start_step != "number") { - start_step = 0; - } else { - start_step += 1; - } + } else if (next_tour) { + tour_name = next_tour[0]; + start_step = next_tour.at(-1); + if (typeof start_step != "number") { + start_step = 0; + } else { + start_step += 1; + } frappe.ui.next_form_tour = undefined; } else { tour_name = matching_tours[0][0]; From 76c118bac77fc0f159575097bacd1e38ed425194 Mon Sep 17 00:00:00 2001 From: Maharshi Patel Date: Thu, 18 May 2023 12:27:37 +0530 Subject: [PATCH 011/203] feat: frappe framework basic UI tours. --- .../main_workspace_tour.json | 80 +++++++++ .../todo_list_tour/todo_list_tour.json | 159 ++++++++++++++++++ .../tools_workspace_tour.json | 79 +++++++++ 3 files changed, 318 insertions(+) create mode 100644 frappe/desk/form_tour/main_workspace_tour/main_workspace_tour.json create mode 100644 frappe/desk/form_tour/todo_list_tour/todo_list_tour.json create mode 100644 frappe/desk/form_tour/tools_workspace_tour/tools_workspace_tour.json diff --git a/frappe/desk/form_tour/main_workspace_tour/main_workspace_tour.json b/frappe/desk/form_tour/main_workspace_tour/main_workspace_tour.json new file mode 100644 index 0000000000..62888ebdac --- /dev/null +++ b/frappe/desk/form_tour/main_workspace_tour/main_workspace_tour.json @@ -0,0 +1,80 @@ +{ + "creation": "2023-05-18 12:08:23.196462", + "dashboard_name": "", + "docstatus": 0, + "doctype": "Form Tour", + "first_document": 0, + "idx": 0, + "include_name_field": 0, + "is_standard": 1, + "list_name": "", + "modified": "2023-05-18 12:21:54.389743", + "modified_by": "Administrator", + "module": "Desk", + "name": "Main Workspace Tour", + "new_document_form": 0, + "owner": "Administrator", + "page_name": "", + "page_route": "[\"Workspaces\",\"Build\"]", + "reference_doctype": "", + "report_name": "", + "reset_tours": 0, + "save_on_complete": 0, + "steps": [ + { + "description": "
You can access different things like report, settings, documents (any doctypes), and modules. It saves you time by eliminating the need to navigate through menus.
", + "element_selector": "#navbar-search", + "fieldtype": "0", + "has_next_condition": 0, + "hide_buttons": 0, + "is_table_field": 0, + "modal_trigger": 0, + "next_on_click": 0, + "offset_x": 0, + "offset_y": 0, + "parent_element_selector": ".input-group.search-bar", + "popover_element": 0, + "position": "Left", + "title": "Awesomebar", + "ui_tour": 1 + }, + { + "description": "
Workspaces can be used to quickly access various modules and features. It organizes the available functionalities into logical groups.
", + "element_selector": ".col-lg-2.layout-side-section", + "fieldtype": "0", + "has_next_condition": 0, + "hide_buttons": 0, + "is_table_field": 0, + "modal_trigger": 0, + "next_on_click": 0, + "offset_x": 0, + "offset_y": 0, + "popover_element": 0, + "position": "Right", + "title": "Workspace List", + "ui_tour": 1 + }, + { + "description": "
Click to visit the Workspace
", + "element_selector": ".desk-sidebar-item.standard-sidebar-item > [title=\"Tools\"]", + "fieldtype": "0", + "has_next_condition": 0, + "hide_buttons": 1, + "is_table_field": 0, + "modal_trigger": 0, + "next_form_tour": "New Tools Tour", + "next_on_click": 1, + "offset_x": 0, + "offset_y": 0, + "popover_element": 0, + "position": "Right", + "title": "Tools Workspace", + "ui_tour": 1 + } + ], + "title": "Main Workspace Tour", + "track_steps": 1, + "ui_tour": 1, + "view_name": "Workspaces", + "workspace_name": "Build" +} \ No newline at end of file diff --git a/frappe/desk/form_tour/todo_list_tour/todo_list_tour.json b/frappe/desk/form_tour/todo_list_tour/todo_list_tour.json new file mode 100644 index 0000000000..6a561e6f51 --- /dev/null +++ b/frappe/desk/form_tour/todo_list_tour/todo_list_tour.json @@ -0,0 +1,159 @@ +{ + "creation": "2023-05-18 12:12:01.839494", + "dashboard_name": "", + "docstatus": 0, + "doctype": "Form Tour", + "first_document": 0, + "idx": 0, + "include_name_field": 0, + "is_standard": 1, + "list_name": "List", + "modified": "2023-05-18 12:22:07.306556", + "modified_by": "Administrator", + "module": "Desk", + "name": "Todo List Tour", + "new_document_form": 0, + "owner": "Administrator", + "page_name": "", + "page_route": "[\"List\",\"ToDo\",\"List\"]", + "reference_doctype": "ToDo", + "report_name": "", + "reset_tours": 0, + "save_on_complete": 0, + "steps": [ + { + "description": "
List View
", + "element_selector": ".layout-main-section.frappe-card", + "fieldtype": "0", + "has_next_condition": 0, + "hide_buttons": 0, + "is_table_field": 0, + "modal_trigger": 0, + "next_on_click": 0, + "offset_x": 0, + "offset_y": 0, + "popover_element": 0, + "position": "Left", + "title": "TODO", + "ui_tour": 1 + }, + { + "description": "
List View as the name suggest is used to see documents/records in list format.
", + "element_selector": ".frappe-list", + "fieldtype": "0", + "has_next_condition": 0, + "hide_buttons": 0, + "is_table_field": 0, + "modal_trigger": 0, + "next_on_click": 0, + "offset_x": 0, + "offset_y": 0, + "popover_element": 0, + "position": "Top Center", + "title": "TODO List", + "ui_tour": 1 + }, + { + "description": "
Using Quick filter you can refine and narrow down the displayed data by applying specific criteria or conditions
", + "element_selector": ".list-sidebar.overlay-sidebar.hidden-xs.hidden-sm", + "fieldtype": "0", + "has_next_condition": 0, + "hide_buttons": 0, + "is_table_field": 0, + "modal_trigger": 0, + "next_on_click": 0, + "offset_x": 0, + "offset_y": 0, + "popover_element": 0, + "position": "Right", + "title": "Sidebar", + "ui_tour": 1 + }, + { + "description": "
You can also filter using this inputs
", + "element_selector": ".standard-filter-section.flex", + "fieldtype": "0", + "has_next_condition": 0, + "hide_buttons": 0, + "is_table_field": 0, + "modal_trigger": 0, + "next_on_click": 1, + "offset_x": 0, + "offset_y": 0, + "popover_element": 0, + "position": "Bottom", + "title": "Input Filters", + "ui_tour": 1 + }, + { + "description": "
Click on the Filter button
", + "element_selector": ".filter-selector > .btn-group", + "fieldtype": "0", + "has_next_condition": 0, + "hide_buttons": 0, + "is_table_field": 0, + "modal_trigger": 0, + "next_on_click": 1, + "offset_x": 0, + "offset_y": 0, + "ondemand_description": "
Aou can add multiple filters and hit apply to refine results
", + "popover_element": 1, + "position": "Left", + "title": "Advanced Filters", + "ui_tour": 1 + }, + { + "description": "
Click here to remove all filter
", + "element_selector": ".filter-selector > .btn-group > .filter-x-button", + "fieldtype": "0", + "has_next_condition": 0, + "hide_buttons": 0, + "is_table_field": 0, + "modal_trigger": 0, + "next_on_click": 1, + "offset_x": 0, + "offset_y": 0, + "popover_element": 0, + "position": "Left", + "title": "Clear Filters", + "ui_tour": 1 + }, + { + "description": "
You can arrange data in ascending or descending order based on selected attributes.\n
    \n
    \n
  1. Click on Last Updated On
  2. \n
    \n
  3. Select the Attribute based on which you want to sort
  4. \n
\n
", + "element_selector": ".sort-selector", + "fieldtype": "0", + "has_next_condition": 0, + "hide_buttons": 0, + "is_table_field": 0, + "modal_trigger": 0, + "next_on_click": 0, + "offset_x": -20, + "offset_y": 0, + "popover_element": 0, + "position": "Left", + "title": "Sort By", + "ui_tour": 1 + }, + { + "description": "
Click to change ascending or descending order.\n
", + "element_selector": ".sort-selector > .btn-group > .btn-order", + "fieldtype": "0", + "has_next_condition": 0, + "hide_buttons": 0, + "is_table_field": 0, + "modal_trigger": 0, + "next_on_click": 0, + "offset_x": -20, + "offset_y": 0, + "popover_element": 0, + "position": "Left", + "title": "Sort By", + "ui_tour": 1 + } + ], + "title": "Todo List Tour", + "track_steps": 1, + "ui_tour": 1, + "view_name": "List", + "workspace_name": "" +} \ No newline at end of file diff --git a/frappe/desk/form_tour/tools_workspace_tour/tools_workspace_tour.json b/frappe/desk/form_tour/tools_workspace_tour/tools_workspace_tour.json new file mode 100644 index 0000000000..6f10c69328 --- /dev/null +++ b/frappe/desk/form_tour/tools_workspace_tour/tools_workspace_tour.json @@ -0,0 +1,79 @@ +{ + "creation": "2023-05-18 12:09:40.792239", + "dashboard_name": "", + "docstatus": 0, + "doctype": "Form Tour", + "first_document": 0, + "idx": 0, + "include_name_field": 0, + "is_standard": 1, + "list_name": "", + "modified": "2023-05-18 12:22:01.208707", + "modified_by": "Administrator", + "module": "Desk", + "name": "Tools Workspace Tour", + "new_document_form": 0, + "owner": "Administrator", + "page_name": "", + "page_route": "[\"Workspaces\",\"Tools\"]", + "reference_doctype": "", + "report_name": "", + "reset_tours": 0, + "save_on_complete": 0, + "steps": [ + { + "description": "
This is Tools Workspace
", + "element_selector": ".codex-editor", + "fieldtype": "0", + "has_next_condition": 0, + "hide_buttons": 0, + "is_table_field": 0, + "modal_trigger": 0, + "next_on_click": 0, + "offset_x": 0, + "offset_y": 0, + "popover_element": 0, + "position": "Left", + "title": "Workspace", + "ui_tour": 1 + }, + { + "description": "
Workspace have cards that serve as links to different modules and features. For instance, the Email List card provides easy access to related components like Newsletter and Email Group
", + "element_selector": "[card_name=\"Email\"]", + "fieldtype": "0", + "has_next_condition": 0, + "hide_buttons": 0, + "is_table_field": 0, + "modal_trigger": 0, + "next_on_click": 0, + "offset_x": 0, + "offset_y": 0, + "popover_element": 0, + "position": "Right", + "title": "Email Card", + "ui_tour": 1 + }, + { + "description": "
Shortcuts are a set of clickable links that serve as direct links to frequently accessed modules and features
", + "element_selector": "[shortcut_name=\"ToDo\"]", + "fieldtype": "0", + "has_next_condition": 0, + "hide_buttons": 1, + "is_table_field": 0, + "modal_trigger": 0, + "next_form_tour": "Todo List Tour", + "next_on_click": 0, + "offset_x": 0, + "offset_y": 0, + "popover_element": 0, + "position": "Right", + "title": "Todo Shortcut", + "ui_tour": 1 + } + ], + "title": "Tools Workspace Tour", + "track_steps": 1, + "ui_tour": 1, + "view_name": "Workspaces", + "workspace_name": "Tools" +} \ No newline at end of file From 20fe275ba2965e860d12c62934ab5d9606599433 Mon Sep 17 00:00:00 2001 From: Maharshi Patel Date: Thu, 18 May 2023 12:43:10 +0530 Subject: [PATCH 012/203] fix: set default for user onboarding_status --- frappe/core/doctype/user/user.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/frappe/core/doctype/user/user.json b/frappe/core/doctype/user/user.json index 683063700f..35a58a5851 100644 --- a/frappe/core/doctype/user/user.json +++ b/frappe/core/doctype/user/user.json @@ -701,6 +701,7 @@ "label": "Onboarding Tours" }, { + "default": "{}", "fieldname": "onboarding_status", "fieldtype": "JSON", "hidden": 1, @@ -767,7 +768,7 @@ "link_fieldname": "user" } ], - "modified": "2023-05-17 22:56:28.260931", + "modified": "2023-05-18 12:41:36.765029", "modified_by": "Administrator", "module": "Core", "name": "User", From b4387086b4cf34b45f5d78852bcec7d8b0c179be Mon Sep 17 00:00:00 2001 From: Maharshi Patel Date: Fri, 19 May 2023 11:26:03 +0530 Subject: [PATCH 013/203] fix: linter for onboarding.bundle.js --- frappe/public/js/onboarding.bundle.js | 64 +++++++++++++-------------- 1 file changed, 32 insertions(+), 32 deletions(-) diff --git a/frappe/public/js/onboarding.bundle.js b/frappe/public/js/onboarding.bundle.js index bd68b5facf..28419700b6 100644 --- a/frappe/public/js/onboarding.bundle.js +++ b/frappe/public/js/onboarding.bundle.js @@ -15,17 +15,19 @@ frappe.ui.OnboardingTour = class OnboardingTour { nextBtnText: "Next", prevBtnText: "Previous", opacity: 0.5, - onHighlighted: step => { + onHighlighted: (step) => { frappe.ui.next_form_tour = step.options.step_info?.next_form_tour; const wait_for_node = setInterval(() => { if (!step.popover.node) return; if (step.options.step_info?.offset_x) { - step.popover.node.style.left = `${step.popover.node.offsetLeft + - step.options.step_info.offset_x}px`; + step.popover.node.style.left = `${ + step.popover.node.offsetLeft + step.options.step_info.offset_x + }px`; } if (step.options.step_info?.offset_y) { - step.popover.node.style.top = `${step.popover.node.offsetTop + - step.options.step_info.offset_y}px`; + step.popover.node.style.top = `${ + step.popover.node.offsetTop + step.options.step_info.offset_y + }px`; } if (step.popover.closeBtnNode) { step.popover.closeBtnNode.onclick = () => { @@ -54,11 +56,9 @@ frappe.ui.OnboardingTour = class OnboardingTour { // focus on first input. // TODO : later add option to select which input to focus as well. - const $input = $(step.node) - .find("input") - .get(0); + const $input = $(step.node).find("input").get(0); if ($input) frappe.utils.sleep(200).then(() => $input.focus()); - } + }, }); } @@ -75,12 +75,12 @@ frappe.ui.OnboardingTour = class OnboardingTour { build_steps() { this.driver_steps = []; - this.tour.steps.forEach(step => { - const on_next = async el => { + this.tour.steps.forEach((step) => { + const on_next = async (el) => { const step_index = this.driver.steps.indexOf(el); if (step_index == -1 || this.last_step_saved?.name == step.name) return; frappe.boot.user.onboarding_status[this.tour.name] = { - steps_complete: step_index + steps_complete: step_index, }; if (!this.driver.hasNextStep()) { this.on_finish && this.on_finish(); @@ -115,7 +115,7 @@ frappe.ui.OnboardingTour = class OnboardingTour { hide_buttons, next_on_click, popover_element, - modal_trigger + modal_trigger, } = step_info; let element = cur_page?.page.querySelector(element_selector); !element && (element = document.querySelector(element_selector)); @@ -165,7 +165,7 @@ frappe.ui.OnboardingTour = class OnboardingTour { if (this.driver_steps[step_index + 1]?.element.id == popover_id) return; this.driver_steps = this.driver_steps.filter( - step => !step.element.id?.startsWith("popover") + (step) => !step.element.id?.startsWith("popover") ); let new_step = { ...this.driver_steps[step_index] }; @@ -178,7 +178,7 @@ frappe.ui.OnboardingTour = class OnboardingTour { this.driver.moveNext(); this.driver.overlay.refresh(); - $(popover).one("hide.bs.popover", e => { + $(popover).one("hide.bs.popover", (e) => { this.driver_steps.splice(this.driver.currentStep, 1); this.driver_steps[this.driver.currentStep - 1].showButtons = true; new_step.popover.description = description; @@ -199,11 +199,11 @@ frappe.ui.OnboardingTour = class OnboardingTour { popover: { title, description, - position: frappe.router.slug(position || "Bottom") + position: frappe.router.slug(position || "Bottom"), }, onNext: on_next, step_info: step_info, - showButtons + showButtons, }; } @@ -232,8 +232,8 @@ frappe.ui.OnboardingTour = class OnboardingTour { title, description, position: "left-center", - doneBtnText: __("Next") - } + doneBtnText: __("Next"), + }, }; this.driver_steps.splice(step + 1, 0, attach_dialog_step); this.update_driver_steps(); // need to define again, since driver.js only considers steps which are inside DOM @@ -267,7 +267,7 @@ frappe.router.on("change", () => { } if (route[0] != "dashboard-view") { frappe.boot.onboarding_tours && - frappe.boot.onboarding_tours.forEach(tour => { + frappe.boot.onboarding_tours.forEach((tour) => { let tour_route = tour[1]; length = Math.min(route.length, tour_route.length); if (length >= 1 && route[0] != tour_route[0]) return; @@ -281,11 +281,11 @@ frappe.router.on("change", () => { matching_tours.push(tour); }); } - matching_tours = matching_tours.filter(tour => { + matching_tours = matching_tours.filter((tour) => { if (frappe.boot.user.onboarding_status[tour[0]]?.is_complete == true) return false; return true; }); - matching_tours = matching_tours.map(tour => { + matching_tours = matching_tours.map((tour) => { if (frappe.boot.user.onboarding_status[tour[0]]?.steps_complete != undefined) { tour.push(frappe.boot.user.onboarding_status[tour[0]].steps_complete); } @@ -293,9 +293,9 @@ frappe.router.on("change", () => { }); if (matching_tours.length == 0) return; let current_tour = matching_tours.find( - tour => tour[0] == frappe.ui.currentTourInstance?.tour.name + (tour) => tour[0] == frappe.ui.currentTourInstance?.tour.name ); - let next_tour = matching_tours.find(tour => tour[0] == frappe.ui.next_form_tour); + let next_tour = matching_tours.find((tour) => tour[0] == frappe.ui.next_form_tour); if (current_tour) { tour_name = current_tour[0]; start_step = current_tour.at(-1); @@ -303,13 +303,13 @@ frappe.router.on("change", () => { start_step = 0; } } else if (next_tour) { - tour_name = next_tour[0]; - start_step = next_tour.at(-1); - if (typeof start_step != "number") { - start_step = 0; - } else { - start_step += 1; - } + tour_name = next_tour[0]; + start_step = next_tour.at(-1); + if (typeof start_step != "number") { + start_step = 0; + } else { + start_step += 1; + } frappe.ui.next_form_tour = undefined; } else { tour_name = matching_tours[0][0]; @@ -335,7 +335,7 @@ frappe.router.on("change", () => { frappe.utils.sleep(500).then(() => { tour.init({ tour_name, - start_step + start_step, }); clearInterval(wait_for_data); }); From 5f6a6a025a3029a59b220c3f5556e67a646e7096 Mon Sep 17 00:00:00 2001 From: marination Date: Fri, 19 May 2023 16:56:54 +0530 Subject: [PATCH 014/203] fix: Load map libraries at build time to avoid async issues during geolocation render - When gelocation control is built, it awaits loading of libraries - Withi the control everything is await but a layer above where multiple controls are sequentially built this breaks - Form render goes ahead without waiting for the gelocation control - The `onload_post_render` in `Location` cannot find the map wrapper as the control is still being built - Therefore, on a hard load, the control does not show up and appears on soft reload. --- .../public/js/frappe/form/controls/geolocation.js | 14 -------------- .../lib/leaflet_control_locate/L.Control.Locate.js | 2 +- frappe/public/js/libs.bundle.js | 4 ++++ frappe/public/scss/desk.bundle.scss | 5 +++++ 4 files changed, 10 insertions(+), 15 deletions(-) diff --git a/frappe/public/js/frappe/form/controls/geolocation.js b/frappe/public/js/frappe/form/controls/geolocation.js index 42534a3c39..aa75a2282b 100644 --- a/frappe/public/js/frappe/form/controls/geolocation.js +++ b/frappe/public/js/frappe/form/controls/geolocation.js @@ -4,7 +4,6 @@ frappe.ui.form.ControlGeolocation = class ControlGeolocation extends frappe.ui.f static horizontal = false; async make() { - await frappe.require(this.required_libs); super.make(); } @@ -216,17 +215,4 @@ frappe.ui.form.ControlGeolocation = class ControlGeolocation extends frappe.ui.f this.editableLayers.removeLayer(l); }); } - - get required_libs() { - return [ - "assets/frappe/js/lib/leaflet_easy_button/easy-button.css", - "assets/frappe/js/lib/leaflet_control_locate/L.Control.Locate.css", - "assets/frappe/js/lib/leaflet_draw/leaflet.draw.css", - "assets/frappe/js/lib/leaflet/leaflet.css", - "assets/frappe/js/lib/leaflet/leaflet.js", - "assets/frappe/js/lib/leaflet_easy_button/easy-button.js", - "assets/frappe/js/lib/leaflet_draw/leaflet.draw.js", - "assets/frappe/js/lib/leaflet_control_locate/L.Control.Locate.js", - ]; - } }; diff --git a/frappe/public/js/lib/leaflet_control_locate/L.Control.Locate.js b/frappe/public/js/lib/leaflet_control_locate/L.Control.Locate.js index 8544e17a04..8ea44ce00c 100644 --- a/frappe/public/js/lib/leaflet_control_locate/L.Control.Locate.js +++ b/frappe/public/js/lib/leaflet_control_locate/L.Control.Locate.js @@ -17,7 +17,7 @@ You can find the project at: https://github.com/domoritz/leaflet-locatecontrol if (typeof window !== 'undefined' && window.L) { module.exports = factory(L); } else { - module.exports = factory(require('leaflet')); + module.exports = factory(require('../leaflet/leaflet.js')); } } diff --git a/frappe/public/js/libs.bundle.js b/frappe/public/js/libs.bundle.js index e4e172c1b4..77704bb173 100644 --- a/frappe/public/js/libs.bundle.js +++ b/frappe/public/js/libs.bundle.js @@ -1,5 +1,9 @@ import "./jquery-bootstrap"; import "./lib/moment"; +import "../js/lib/leaflet/leaflet.js"; +import "../js/lib/leaflet_easy_button/easy-button.js"; +import "../js/lib/leaflet_draw/leaflet.draw.js"; +import "../js/lib/leaflet_control_locate/L.Control.Locate.js"; import Sortable from "sortablejs"; window.SetVueGlobals = (app) => { diff --git a/frappe/public/scss/desk.bundle.scss b/frappe/public/scss/desk.bundle.scss index 10fd116d6c..6b192bb3ff 100644 --- a/frappe/public/scss/desk.bundle.scss +++ b/frappe/public/scss/desk.bundle.scss @@ -4,3 +4,8 @@ @import "~frappe-charts/dist/frappe-charts.min"; @import "~plyr/dist/plyr"; @import "./desk/index"; + +@import "frappe/public/js/lib/leaflet/leaflet.css"; +@import "frappe/public/js/lib/leaflet_easy_button/easy-button.css"; +@import "frappe/public/js/lib/leaflet_control_locate/L.Control.Locate.css"; +@import "frappe/public/js/lib/leaflet_draw/leaflet.draw.css"; \ No newline at end of file From 77e73ccea4ade94b6b37972328643f69394fba41 Mon Sep 17 00:00:00 2001 From: Hussain Nagaria Date: Sun, 21 May 2023 16:35:57 +0530 Subject: [PATCH 015/203] feat: support for dynamic URL in webhook Closes #11921 --- frappe/integrations/doctype/webhook/webhook.json | 12 ++++++++++-- frappe/integrations/doctype/webhook/webhook.py | 15 ++++++++++----- 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/frappe/integrations/doctype/webhook/webhook.json b/frappe/integrations/doctype/webhook/webhook.json index a21e460659..9b402c6a27 100644 --- a/frappe/integrations/doctype/webhook/webhook.json +++ b/frappe/integrations/doctype/webhook/webhook.json @@ -18,8 +18,9 @@ "html_condition", "sb_webhook", "request_url", - "request_method", + "is_dynamic_url", "cb_webhook", + "request_method", "request_structure", "sb_security", "enable_security", @@ -200,10 +201,17 @@ { "fieldname": "section_break_28", "fieldtype": "Section Break" + }, + { + "default": "0", + "description": "On checking this option, URL will be treated like a jinja template string", + "fieldname": "is_dynamic_url", + "fieldtype": "Check", + "label": "Is Dynamic URL?" } ], "links": [], - "modified": "2022-07-11 08:54:10.740512", + "modified": "2023-05-21 16:30:10.740512", "modified_by": "Administrator", "module": "Integrations", "name": "Webhook", diff --git a/frappe/integrations/doctype/webhook/webhook.py b/frappe/integrations/doctype/webhook/webhook.py index 7d168c659f..1b56a1b129 100644 --- a/frappe/integrations/doctype/webhook/webhook.py +++ b/frappe/integrations/doctype/webhook/webhook.py @@ -115,29 +115,34 @@ def enqueue_webhook(doc, webhook) -> None: webhook: Webhook = frappe.get_doc("Webhook", webhook.get("name")) headers = get_webhook_headers(doc, webhook) data = get_webhook_data(doc, webhook) - r = None + if webhook.is_dynamic_url: + request_url = frappe.render_template(webhook.request_url, get_context(doc)) + else: + request_url = webhook.request_url + + r = None for i in range(3): try: r = requests.request( method=webhook.request_method, - url=webhook.request_url, + url=request_url, data=json.dumps(data, default=str), headers=headers, timeout=5, ) r.raise_for_status() frappe.logger().debug({"webhook_success": r.text}) - log_request(webhook.name, doc.name, webhook.request_url, headers, data, r) + log_request(webhook.name, doc.name, request_url, headers, data, r) break except requests.exceptions.ReadTimeout as e: frappe.logger().debug({"webhook_error": e, "try": i + 1}) - log_request(webhook.name, doc.name, webhook.request_url, headers, data) + log_request(webhook.name, doc.name, request_url, headers, data) except Exception as e: frappe.logger().debug({"webhook_error": e, "try": i + 1}) - log_request(webhook.name, doc.name, webhook.request_url, headers, data, r) + log_request(webhook.name, doc.name, request_url, headers, data, r) sleep(3 * i + 1) if i != 2: continue From 8f8c6b51e87f7659b87ac9c6af34932b35098da0 Mon Sep 17 00:00:00 2001 From: Hussain Nagaria Date: Sun, 21 May 2023 16:36:08 +0530 Subject: [PATCH 016/203] test: tests for dynamic URL --- .../doctype/webhook/test_webhook.py | 54 +++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/frappe/integrations/doctype/webhook/test_webhook.py b/frappe/integrations/doctype/webhook/test_webhook.py index 8284db7fd3..1092d2a253 100644 --- a/frappe/integrations/doctype/webhook/test_webhook.py +++ b/frappe/integrations/doctype/webhook/test_webhook.py @@ -206,3 +206,57 @@ class TestWebhook(FrappeTestCase): enqueue_webhook(doc, wh) log = frappe.get_last_doc("Webhook Request Log") self.assertEqual(len(json.loads(log.response)["json"]), 3) + + def test_webhook_with_dynamic_url_enabled(self): + wh_config = { + "doctype": "Webhook", + "webhook_doctype": "Note", + "webhook_docevent": "after_insert", + "enabled": 1, + "request_url": "https://httpbin.org/anything/{{ doc.doctype }}", + "is_dynamic_url": 1, + "request_method": "POST", + "request_structure": "JSON", + "webhook_json": '{}', + "meets_condition": "Yes", + "webhook_headers": [ + { + "key": "Content-Type", + "value": "application/json", + } + ], + } + + with get_test_webhook(wh_config) as wh: + doc = frappe.new_doc("Note") + doc.title = "Test Webhook Note" + enqueue_webhook(doc, wh) + log = frappe.get_last_doc("Webhook Request Log") + self.assertEqual(json.loads(log.response)["url"], "https://httpbin.org/anything/Note") + + def test_webhook_with_dynamic_url_disabled(self): + wh_config = { + "doctype": "Webhook", + "webhook_doctype": "Note", + "webhook_docevent": "after_insert", + "enabled": 1, + "request_url": "https://httpbin.org/anything/{{doc.doctype}}", + "is_dynamic_url": 0, + "request_method": "POST", + "request_structure": "JSON", + "webhook_json": '{}', + "meets_condition": "Yes", + "webhook_headers": [ + { + "key": "Content-Type", + "value": "application/json", + } + ], + } + + with get_test_webhook(wh_config) as wh: + doc = frappe.new_doc("Note") + doc.title = "Test Webhook Note" + enqueue_webhook(doc, wh) + log = frappe.get_last_doc("Webhook Request Log") + self.assertEqual(json.loads(log.response)["url"], "https://httpbin.org/anything/{{doc.doctype}}") From 3f08e80d888b279b3a7ad1ebdb852e1d7c8b340a Mon Sep 17 00:00:00 2001 From: Hussain Nagaria Date: Sun, 21 May 2023 16:44:34 +0530 Subject: [PATCH 017/203] style: lint test file --- frappe/integrations/doctype/webhook/test_webhook.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/frappe/integrations/doctype/webhook/test_webhook.py b/frappe/integrations/doctype/webhook/test_webhook.py index 1092d2a253..7235447662 100644 --- a/frappe/integrations/doctype/webhook/test_webhook.py +++ b/frappe/integrations/doctype/webhook/test_webhook.py @@ -217,7 +217,7 @@ class TestWebhook(FrappeTestCase): "is_dynamic_url": 1, "request_method": "POST", "request_structure": "JSON", - "webhook_json": '{}', + "webhook_json": "{}", "meets_condition": "Yes", "webhook_headers": [ { @@ -244,7 +244,7 @@ class TestWebhook(FrappeTestCase): "is_dynamic_url": 0, "request_method": "POST", "request_structure": "JSON", - "webhook_json": '{}', + "webhook_json": "{}", "meets_condition": "Yes", "webhook_headers": [ { @@ -259,4 +259,6 @@ class TestWebhook(FrappeTestCase): doc.title = "Test Webhook Note" enqueue_webhook(doc, wh) log = frappe.get_last_doc("Webhook Request Log") - self.assertEqual(json.loads(log.response)["url"], "https://httpbin.org/anything/{{doc.doctype}}") + self.assertEqual( + json.loads(log.response)["url"], "https://httpbin.org/anything/{{doc.doctype}}" + ) From 9f83ead7a2ac42b51e45c91456bfb383ba67e118 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Mon, 22 May 2023 14:48:19 +0530 Subject: [PATCH 018/203] fix: remove setup_complete events This can trigger along side actual setup complete event causing two parallel requests. --- frappe/desk/page/setup_wizard/setup_wizard.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/frappe/desk/page/setup_wizard/setup_wizard.js b/frappe/desk/page/setup_wizard/setup_wizard.js index 56cb61696c..3f902689f8 100644 --- a/frappe/desk/page/setup_wizard/setup_wizard.js +++ b/frappe/desk/page/setup_wizard/setup_wizard.js @@ -122,8 +122,6 @@ frappe.setup.SetupWizard = class SetupWizard extends frappe.ui.Slides { show_slide(id) { if (id === this.slides.length) { - // show_slide called on last slide - this.action_on_complete(); return; } super.show_slide(id); From c643d4a33cc58d10c0a3d81c9ed9df9bf6cc0776 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Mon, 22 May 2023 16:15:53 +0530 Subject: [PATCH 019/203] chore: capitalization --- frappe/core/doctype/system_settings/system_settings.json | 2 +- frappe/desk/page/setup_wizard/setup_wizard.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/frappe/core/doctype/system_settings/system_settings.json b/frappe/core/doctype/system_settings/system_settings.json index 4249f250b7..091dc1df1e 100644 --- a/frappe/core/doctype/system_settings/system_settings.json +++ b/frappe/core/doctype/system_settings/system_settings.json @@ -548,7 +548,7 @@ "default": "1", "fieldname": "enable_telemetry", "fieldtype": "Check", - "label": "Allow Sending Usage Data for Improving applications" + "label": "Allow Sending Usage Data for Improving Applications" } ], "icon": "fa fa-cog", diff --git a/frappe/desk/page/setup_wizard/setup_wizard.js b/frappe/desk/page/setup_wizard/setup_wizard.js index 3f902689f8..862ac8c14d 100644 --- a/frappe/desk/page/setup_wizard/setup_wizard.js +++ b/frappe/desk/page/setup_wizard/setup_wizard.js @@ -401,7 +401,7 @@ frappe.setup.slides_settings = [ }, { fieldname: "enable_telemetry", - label: __("Allow Sending Usage Data for Improving applications"), + label: __("Allow Sending Usage Data for Improving Applications"), fieldtype: "Check", default: 1, }, From 892150eb7397bf229d132b3621fafa81f568bc61 Mon Sep 17 00:00:00 2001 From: marination Date: Fri, 19 May 2023 16:56:54 +0530 Subject: [PATCH 020/203] fix: Load map libraries at build time to avoid async issues during geolocation render - When gelocation control is built, it awaits loading of libraries - Withi the control everything is await but a layer above where multiple controls are sequentially built this breaks - Form render goes ahead without waiting for the gelocation control - The `onload_post_render` in `Location` cannot find the map wrapper as the control is still being built - Therefore, on a hard load, the control does not show up and appears on soft reload. --- .../public/js/frappe/form/controls/geolocation.js | 14 -------------- .../lib/leaflet_control_locate/L.Control.Locate.js | 2 +- frappe/public/js/libs.bundle.js | 4 ++++ frappe/public/scss/desk.bundle.scss | 5 +++++ 4 files changed, 10 insertions(+), 15 deletions(-) diff --git a/frappe/public/js/frappe/form/controls/geolocation.js b/frappe/public/js/frappe/form/controls/geolocation.js index c2ce336266..026457b856 100644 --- a/frappe/public/js/frappe/form/controls/geolocation.js +++ b/frappe/public/js/frappe/form/controls/geolocation.js @@ -4,7 +4,6 @@ frappe.ui.form.ControlGeolocation = class ControlGeolocation extends frappe.ui.f static horizontal = false; async make() { - await frappe.require(this.required_libs); super.make(); $(this.input_area).addClass("hidden"); } @@ -200,17 +199,4 @@ frappe.ui.form.ControlGeolocation = class ControlGeolocation extends frappe.ui.f this.editableLayers.removeLayer(l); }); } - - get required_libs() { - return [ - "assets/frappe/js/lib/leaflet_easy_button/easy-button.css", - "assets/frappe/js/lib/leaflet_control_locate/L.Control.Locate.css", - "assets/frappe/js/lib/leaflet_draw/leaflet.draw.css", - "assets/frappe/js/lib/leaflet/leaflet.css", - "assets/frappe/js/lib/leaflet/leaflet.js", - "assets/frappe/js/lib/leaflet_easy_button/easy-button.js", - "assets/frappe/js/lib/leaflet_draw/leaflet.draw.js", - "assets/frappe/js/lib/leaflet_control_locate/L.Control.Locate.js", - ]; - } }; diff --git a/frappe/public/js/lib/leaflet_control_locate/L.Control.Locate.js b/frappe/public/js/lib/leaflet_control_locate/L.Control.Locate.js index 8544e17a04..8ea44ce00c 100644 --- a/frappe/public/js/lib/leaflet_control_locate/L.Control.Locate.js +++ b/frappe/public/js/lib/leaflet_control_locate/L.Control.Locate.js @@ -17,7 +17,7 @@ You can find the project at: https://github.com/domoritz/leaflet-locatecontrol if (typeof window !== 'undefined' && window.L) { module.exports = factory(L); } else { - module.exports = factory(require('leaflet')); + module.exports = factory(require('../leaflet/leaflet.js')); } } diff --git a/frappe/public/js/libs.bundle.js b/frappe/public/js/libs.bundle.js index e4e172c1b4..77704bb173 100644 --- a/frappe/public/js/libs.bundle.js +++ b/frappe/public/js/libs.bundle.js @@ -1,5 +1,9 @@ import "./jquery-bootstrap"; import "./lib/moment"; +import "../js/lib/leaflet/leaflet.js"; +import "../js/lib/leaflet_easy_button/easy-button.js"; +import "../js/lib/leaflet_draw/leaflet.draw.js"; +import "../js/lib/leaflet_control_locate/L.Control.Locate.js"; import Sortable from "sortablejs"; window.SetVueGlobals = (app) => { diff --git a/frappe/public/scss/desk.bundle.scss b/frappe/public/scss/desk.bundle.scss index 10fd116d6c..6b192bb3ff 100644 --- a/frappe/public/scss/desk.bundle.scss +++ b/frappe/public/scss/desk.bundle.scss @@ -4,3 +4,8 @@ @import "~frappe-charts/dist/frappe-charts.min"; @import "~plyr/dist/plyr"; @import "./desk/index"; + +@import "frappe/public/js/lib/leaflet/leaflet.css"; +@import "frappe/public/js/lib/leaflet_easy_button/easy-button.css"; +@import "frappe/public/js/lib/leaflet_control_locate/L.Control.Locate.css"; +@import "frappe/public/js/lib/leaflet_draw/leaflet.draw.css"; \ No newline at end of file From d6edc1530ec8705b41e57318f3e8ed719542dc2b Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Mon, 22 May 2023 15:08:26 +0200 Subject: [PATCH 021/203] refactor: const instead of var, indentation --- .../js/frappe/form/controls/geolocation.js | 63 ++++++++++--------- 1 file changed, 33 insertions(+), 30 deletions(-) diff --git a/frappe/public/js/frappe/form/controls/geolocation.js b/frappe/public/js/frappe/form/controls/geolocation.js index 026457b856..c886864059 100644 --- a/frappe/public/js/frappe/form/controls/geolocation.js +++ b/frappe/public/js/frappe/form/controls/geolocation.js @@ -10,14 +10,14 @@ frappe.ui.form.ControlGeolocation = class ControlGeolocation extends frappe.ui.f set_disp_area(value) { // Create the elements for map area - if (!this.disp_area) return; - + if (!this.disp_area) { + return; + } + this.map_id = frappe.dom.get_unique_id(); this.map_area = $( `
-
+
` ); @@ -46,7 +46,7 @@ frappe.ui.form.ControlGeolocation = class ControlGeolocation extends frappe.ui.f this.map.zoomControl.remove(); } else { this.bind_leaflet_draw_control(); - this.bind_leaflet_event_listeners(); + this.bind_leaflet_event_listeners(); this.bind_leaflet_locate_control(); this.bind_leaflet_data(value); } @@ -54,29 +54,30 @@ frappe.ui.form.ControlGeolocation = class ControlGeolocation extends frappe.ui.f bind_leaflet_data(value) { /* render raw value from db into map */ - if (!this.map || !value) return; + if (!this.map || !value) { + return; + } this.clear_editable_layers(); - var data_layers = new L.FeatureGroup() - .addLayer(L.geoJson(JSON.parse(value),{ - pointToLayer: function(geoJsonPoint, latlng) { - if (geoJsonPoint.properties.point_type == "circle"){ - return L.circle(latlng, {radius: geoJsonPoint.properties.radius}); + const data_layers = new L.FeatureGroup().addLayer( + L.geoJson(JSON.parse(value), { + pointToLayer: function (geoJsonPoint, latlng) { + if (geoJsonPoint.properties.point_type == "circle") { + return L.circle(latlng, { radius: geoJsonPoint.properties.radius }); } else if (geoJsonPoint.properties.point_type == "circlemarker") { - return L.circleMarker(latlng, {radius: geoJsonPoint.properties.radius}); - } - else { + return L.circleMarker(latlng, { radius: geoJsonPoint.properties.radius }); + } else { return L.marker(latlng); } - } - })); + }, + }) + ); this.add_non_group_layers(data_layers, this.editableLayers); try { this.map.fitBounds(this.editableLayers.getBounds(), { - padding: [50,50] + padding: [50, 50], }); - } - catch(err) { + } catch (err) { // suppress error if layer has a point. } this.editableLayers.addTo(this.map); @@ -84,10 +85,10 @@ frappe.ui.form.ControlGeolocation = class ControlGeolocation extends frappe.ui.f } bind_leaflet_map() { - var circleToGeoJSON = L.Circle.prototype.toGeoJSON; + const circleToGeoJSON = L.Circle.prototype.toGeoJSON; L.Circle.include({ toGeoJSON: function () { - var feature = circleToGeoJSON.call(this); + const feature = circleToGeoJSON.call(this); feature.properties = { point_type: "circle", radius: this.getRadius(), @@ -98,7 +99,7 @@ frappe.ui.form.ControlGeolocation = class ControlGeolocation extends frappe.ui.f L.CircleMarker.include({ toGeoJSON: function () { - var feature = circleToGeoJSON.call(this); + const feature = circleToGeoJSON.call(this); feature.properties = { point_type: "circlemarker", radius: this.getRadius(), @@ -114,8 +115,8 @@ frappe.ui.form.ControlGeolocation = class ControlGeolocation extends frappe.ui.f L.tileLayer(frappe.utils.map_defaults.tiles, frappe.utils.map_defaults.options).addTo( this.map ); - - this.editableLayers = new L.FeatureGroup(); + + this.editableLayers = new L.FeatureGroup(); } bind_leaflet_locate_control() { @@ -125,7 +126,9 @@ frappe.ui.form.ControlGeolocation = class ControlGeolocation extends frappe.ui.f } bind_leaflet_draw_control() { - if (!frappe.perm.has_perm(this.doctype, this.df.permlevel, 'write', this.doc)) return; + if (!frappe.perm.has_perm(this.doctype, this.df.permlevel, "write", this.doc)) { + return; + } this.map.addControl(this.get_leaflet_controls()); } @@ -159,13 +162,13 @@ frappe.ui.form.ControlGeolocation = class ControlGeolocation extends frappe.ui.f }, edit: { featureGroup: this.editableLayers, //REQUIRED!! - remove: true - } + remove: true, + }, }); } bind_leaflet_event_listeners() { - this.map.on('draw:created', (e) => { + this.map.on("draw:created", (e) => { var type = e.layerType, layer = e.layer; if (type === "marker") { @@ -176,7 +179,7 @@ frappe.ui.form.ControlGeolocation = class ControlGeolocation extends frappe.ui.f }); this.map.on("draw:deleted draw:edited", (e) => { - var layer = e.layer; + const { layer } = e; this.editableLayers.removeLayer(layer); this.set_value(JSON.stringify(this.editableLayers.toGeoJSON())); }); From 50b15be1c26dee5fa65bde64863323ede2460051 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Mon, 22 May 2023 15:09:01 +0200 Subject: [PATCH 022/203] fix: handle read only property --- frappe/public/js/frappe/form/controls/geolocation.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/frappe/public/js/frappe/form/controls/geolocation.js b/frappe/public/js/frappe/form/controls/geolocation.js index c886864059..37d799e96d 100644 --- a/frappe/public/js/frappe/form/controls/geolocation.js +++ b/frappe/public/js/frappe/form/controls/geolocation.js @@ -126,7 +126,10 @@ frappe.ui.form.ControlGeolocation = class ControlGeolocation extends frappe.ui.f } bind_leaflet_draw_control() { - if (!frappe.perm.has_perm(this.doctype, this.df.permlevel, "write", this.doc)) { + if ( + !frappe.perm.has_perm(this.doctype, this.df.permlevel, "write", this.doc) || + this.df.read_only + ) { return; } From 253b9bb1f5a8e1fa1e6289dd9d7fa87f6bd90c82 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Mon, 22 May 2023 21:36:59 +0530 Subject: [PATCH 023/203] fix(UX): activate next step automatically --- frappe/public/js/frappe/widgets/onboarding_widget.js | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/frappe/public/js/frappe/widgets/onboarding_widget.js b/frappe/public/js/frappe/widgets/onboarding_widget.js index daacfb79c6..1994bc7e73 100644 --- a/frappe/public/js/frappe/widgets/onboarding_widget.js +++ b/frappe/public/js/frappe/widgets/onboarding_widget.js @@ -438,6 +438,7 @@ export default class OnboardingWidget extends Widget { }; this.update_step_status(step, "is_complete", 1, callback); + this.activate_next_step(step); } skip_step(step) { @@ -451,6 +452,16 @@ export default class OnboardingWidget extends Widget { }; this.update_step_status(step, "is_skipped", 1, callback); + this.activate_next_step(step); + } + + activate_next_step(step) { + let current_step_index = this.steps.findIndex((s) => s == step); + let next_step = this.steps[current_step_index + 1]; + + if (!next_step) return; + + this.show_step(next_step); } update_step_status(step, status, value, callback) { From baa7c9dd40b60a6e4e531d95040c02fc2b89cf7a Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Mon, 22 May 2023 21:41:13 +0530 Subject: [PATCH 024/203] chore: track dismissed onboardings --- frappe/public/js/frappe/widgets/onboarding_widget.js | 1 + 1 file changed, 1 insertion(+) diff --git a/frappe/public/js/frappe/widgets/onboarding_widget.js b/frappe/public/js/frappe/widgets/onboarding_widget.js index 1994bc7e73..3d7dbdf49f 100644 --- a/frappe/public/js/frappe/widgets/onboarding_widget.js +++ b/frappe/public/js/frappe/widgets/onboarding_widget.js @@ -563,6 +563,7 @@ export default class OnboardingWidget extends Widget { localStorage.setItem("dismissed-onboarding", JSON.stringify(dismissed)); this.delete(true, true); this.widget.closest(".ce-block").hide(); + frappe.telemetry.capture("dismissed_" + frappe.scrub(this.title), "frappe_onboarding"); }); dismiss.appendTo(this.action_area); } From 98d56d2412c3b63c3688646593eebba922dada37 Mon Sep 17 00:00:00 2001 From: Christian Werner Date: Mon, 22 May 2023 17:42:41 +0200 Subject: [PATCH 025/203] Update de.csv Update Translation 'No' means 'Nein' in German and not 'nothing' 'Kein' --- frappe/translations/de.csv | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/translations/de.csv b/frappe/translations/de.csv index 1876b80720..d1f503be7c 100644 --- a/frappe/translations/de.csv +++ b/frappe/translations/de.csv @@ -3920,7 +3920,7 @@ Javascript,Javascript, Ldap settings,LDAP Einstellungen, Mobile number,Handynummer, Mx,Mx, -No,Kein, +No,Nein, Not found,Nicht gefunden, Notes:,Anmerkungen:, Notify by email,Per E-Mail benachrichtigen, From 9fa2655bd995ffb75e9a244f7b4c6c4103b2287b Mon Sep 17 00:00:00 2001 From: Hussain Nagaria Date: Mon, 22 May 2023 23:11:21 +0530 Subject: [PATCH 026/203] fix: bring back modified field in json file --- frappe/integrations/doctype/webhook/webhook.json | 1 + 1 file changed, 1 insertion(+) diff --git a/frappe/integrations/doctype/webhook/webhook.json b/frappe/integrations/doctype/webhook/webhook.json index d30a605f06..cfb2a2e01c 100644 --- a/frappe/integrations/doctype/webhook/webhook.json +++ b/frappe/integrations/doctype/webhook/webhook.json @@ -218,6 +218,7 @@ "link_fieldname": "webhook" } ], + "modified": "2023-05-22 16:30:10.740512", "modified_by": "Administrator", "module": "Integrations", "name": "Webhook", From 5b881636bbd10a272d4b3d8b4068c346dcc3983f Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Tue, 23 May 2023 10:40:28 +0530 Subject: [PATCH 027/203] chore: track route views for first few days Identifying where users drop off is tricky without knowing what they are doing in system. Tracking first few days routes will likely give some insights. --- frappe/public/js/telemetry/index.js | 12 ++++++++++++ frappe/utils/telemetry.py | 12 ++++++++++++ 2 files changed, 24 insertions(+) diff --git a/frappe/public/js/telemetry/index.js b/frappe/public/js/telemetry/index.js index f7e2c9646a..48afaa5258 100644 --- a/frappe/public/js/telemetry/index.js +++ b/frappe/public/js/telemetry/index.js @@ -6,6 +6,7 @@ class TelemetryManager { this.project_id = frappe.boot.posthog_project_id; this.telemetry_host = frappe.boot.posthog_host; + this.site_age = frappe.boot.telemetry_site_age; if (cint(frappe.boot.enable_telemetry) && this.project_id && this.telemetry_host) { this.enabled = true; @@ -24,6 +25,7 @@ class TelemetryManager { }); posthog.identify(frappe.boot.sitename); this.send_heartbeat(); + this.register_pageview_handler(); } catch (e) { console.trace("Failed to initialize telemetry", e); this.enabled = false; @@ -50,6 +52,16 @@ class TelemetryManager { this.capture("heartbeat", "frappe"); } } + + register_pageview_handler() { + if (this.site_age && this.site_age > 5) { + return; + } + + frappe.router.on("change", () => { + posthog.capture("$pageview"); + }); + } } frappe.telemetry = new TelemetryManager(); diff --git a/frappe/utils/telemetry.py b/frappe/utils/telemetry.py index b5bc13dd57..ba06afbf83 100644 --- a/frappe/utils/telemetry.py +++ b/frappe/utils/telemetry.py @@ -8,6 +8,8 @@ from contextlib import suppress from posthog import Posthog import frappe +from frappe.utils import getdate +from frappe.utils.caching import site_cache POSTHOG_PROJECT_FIELD = "posthog_project_id" POSTHOG_HOST_FIELD = "posthog_host" @@ -20,6 +22,16 @@ def add_bootinfo(bootinfo): bootinfo.posthog_host = frappe.conf.get(POSTHOG_HOST_FIELD) bootinfo.posthog_project_id = frappe.conf.get(POSTHOG_PROJECT_FIELD) bootinfo.enable_telemetry = True + bootinfo.telemetry_site_age = site_age() + + +@site_cache(ttl=60 * 60 * 12) +def site_age(): + try: + est_creation = frappe.db.get_value("User", "Administrator", "creation") + return (getdate() - getdate(est_creation)).days + except Exception: + pass def init_telemetry(): From 05b1793fcd73cc89e3ef133cd256c067ad639803 Mon Sep 17 00:00:00 2001 From: Maharshi Patel Date: Tue, 23 May 2023 12:42:12 +0530 Subject: [PATCH 028/203] fix: load onboarding_tours async loads onboarding_tours if enable_onboarding is checked and form tours are available for user. --- frappe/boot.py | 6 +- frappe/hooks.py | 1 - frappe/public/js/frappe/desk.js | 13 ++++ frappe/public/js/onboarding_tours.bundle.js | 1 + .../onboarding_tours.js} | 60 +++++++++---------- 5 files changed, 47 insertions(+), 34 deletions(-) create mode 100644 frappe/public/js/onboarding_tours.bundle.js rename frappe/public/js/{onboarding.bundle.js => onboarding_tours/onboarding_tours.js} (89%) diff --git a/frappe/boot.py b/frappe/boot.py index 7f4677c249..2e8cb56f33 100644 --- a/frappe/boot.py +++ b/frappe/boot.py @@ -68,8 +68,10 @@ def get_bootinfo(): bootinfo.home_folder = frappe.db.get_value("File", {"is_home_folder": 1}) bootinfo.navbar_settings = get_navbar_settings() bootinfo.notification_settings = get_notification_settings() - bootinfo.onboarding_tours = frappe.parse_json( - frappe.db.get_single_value("Form Tour Settings", "onboarding_tours") or "[]" + bootinfo.onboarding_tours = ( + frappe.parse_json(frappe.db.get_single_value("Form Tour Settings", "onboarding_tours") or "[]") + if frappe.get_system_settings("enable_onboarding") + else "[]" ) set_time_zone(bootinfo) diff --git a/frappe/hooks.py b/frappe/hooks.py index 5eb9426f2e..6d8c00d483 100644 --- a/frappe/hooks.py +++ b/frappe/hooks.py @@ -30,7 +30,6 @@ app_include_js = [ "controls.bundle.js", "report.bundle.js", "telemetry.bundle.js", - "onboarding.bundle.js", ] app_include_css = [ "desk.bundle.css", diff --git a/frappe/public/js/frappe/desk.js b/frappe/public/js/frappe/desk.js index a299e7c5ae..27bfe9ae87 100644 --- a/frappe/public/js/frappe/desk.js +++ b/frappe/public/js/frappe/desk.js @@ -74,6 +74,19 @@ frappe.Application = class Application { // page container this.make_page_container(); + if (!window.Cypress) { + let pending_tours = + frappe.boot.onboarding_tours.findIndex((tour) => { + frappe.boot.user.onboarding_status[tour[0]]?.is_complete == true; + }) == -1; + if (pending_tours && frappe.boot.onboarding_tours.length > 0) { + frappe.require("onboarding_tours.bundle.js", () => { + frappe.utils.sleep(1000).then(() => { + frappe.ui.init_onboarding_tour(); + }); + }); + } + } this.set_route(); // trigger app startup diff --git a/frappe/public/js/onboarding_tours.bundle.js b/frappe/public/js/onboarding_tours.bundle.js new file mode 100644 index 0000000000..90788cedfc --- /dev/null +++ b/frappe/public/js/onboarding_tours.bundle.js @@ -0,0 +1 @@ +import "./onboarding_tours/onboarding_tours.js"; \ No newline at end of file diff --git a/frappe/public/js/onboarding.bundle.js b/frappe/public/js/onboarding_tours/onboarding_tours.js similarity index 89% rename from frappe/public/js/onboarding.bundle.js rename to frappe/public/js/onboarding_tours/onboarding_tours.js index 28419700b6..16e457ee71 100644 --- a/frappe/public/js/onboarding.bundle.js +++ b/frappe/public/js/onboarding_tours/onboarding_tours.js @@ -31,24 +31,23 @@ frappe.ui.OnboardingTour = class OnboardingTour { } if (step.popover.closeBtnNode) { step.popover.closeBtnNode.onclick = () => { + this.on_finish && this.on_finish(); + !frappe.boot.user.onboarding_status[this.tour.name] && + (frappe.boot.user.onboarding_status[this.tour.name] = {}); + frappe.boot.user.onboarding_status[this.tour.name].is_complete = true; if (!this.driver.hasNextStep()) { - this.on_finish && this.on_finish(); - !frappe.boot.user.onboarding_status[this.tour.name] && - (frappe.boot.user.onboarding_status[this.tour.name] = {}); frappe.boot.user.onboarding_status[ this.tour.name - ].is_complete = true; - frappe.utils.debounce( - () => - frappe.db.set_value( - "User", - frappe.boot.user.name, - "onboarding_status", - JSON.stringify(frappe.boot.user.onboarding_status) - ), - 1000 - )(); + ].all_steps_completed = true; } + + frappe.call({ + method: "frappe.desk.doctype.form_tour_settings.form_tour_settings.update_user_status", + args: { + value: JSON.stringify(frappe.boot.user.onboarding_status), + step: JSON.stringify(step.options.step_info), + }, + }); }; } clearInterval(wait_for_node); @@ -87,16 +86,13 @@ frappe.ui.OnboardingTour = class OnboardingTour { frappe.boot.user.onboarding_status[this.tour.name].is_complete = true; } this.last_step_saved = step; - frappe.utils.debounce( - () => - frappe.db.set_value( - "User", - frappe.boot.user.name, - "onboarding_status", - JSON.stringify(frappe.boot.user.onboarding_status) - ), - 1000 - )(); + frappe.call({ + method: "frappe.desk.doctype.form_tour_settings.form_tour_settings.update_user_status", + args: { + value: JSON.stringify(frappe.boot.user.onboarding_status), + step: JSON.stringify(step), + }, + }); }; const driver_step = this.get_step(step, on_next); driver_step.element && this.driver_steps.push(driver_step); @@ -250,13 +246,9 @@ frappe.ui.OnboardingTour = class OnboardingTour { }, 500); } }; -// As of now Tours are only for desktop as it is annoying on mobile. -// Also lot of elements are hidden on mobile so until we find a better way to do it. -if (window.matchMedia("(max-device-width: 992px)").matches) return; -frappe.router.on("change", () => { +frappe.ui.init_onboarding_tour = () => { let route = frappe.router.current_route; - if (route[0] === "") return; let tour_name; @@ -293,7 +285,7 @@ frappe.router.on("change", () => { }); if (matching_tours.length == 0) return; let current_tour = matching_tours.find( - (tour) => tour[0] == frappe.ui.currentTourInstance?.tour.name + (tour) => tour[0] == frappe.ui.currentTourInstance?.tour?.name ); let next_tour = matching_tours.find((tour) => tour[0] == frappe.ui.next_form_tour); if (current_tour) { @@ -341,4 +333,10 @@ frappe.router.on("change", () => { }); } }, 100); -}); +}; +// As of now Tours are only for desktop as it is annoying on mobile. +// Also lot of elements are hidden on mobile so until we find a better way to do it. +window.matchMedia("(min-device-width: 992px)").matches && + frappe.router.on("change", () => { + frappe.ui.init_onboarding_tour(); + }); From 028508c947dcc8dd7e4df85465acaabf6786256f Mon Sep 17 00:00:00 2001 From: Maharshi Patel Date: Tue, 23 May 2023 13:25:37 +0530 Subject: [PATCH 029/203] fix: remove from settings from core module. --- .../doctype/form_tour_settings/__init__.py | 0 .../form_tour_settings/form_tour_settings.js | 8 --- .../form_tour_settings.json | 51 ---------------- .../form_tour_settings/form_tour_settings.py | 15 ----- .../test_form_tour_settings.py | 9 --- .../form_tour_settings_item/__init__.py | 0 .../form_tour_settings_item.json | 61 ------------------- .../form_tour_settings_item.py | 9 --- 8 files changed, 153 deletions(-) delete mode 100644 frappe/core/doctype/form_tour_settings/__init__.py delete mode 100644 frappe/core/doctype/form_tour_settings/form_tour_settings.js delete mode 100644 frappe/core/doctype/form_tour_settings/form_tour_settings.json delete mode 100644 frappe/core/doctype/form_tour_settings/form_tour_settings.py delete mode 100644 frappe/core/doctype/form_tour_settings/test_form_tour_settings.py delete mode 100644 frappe/core/doctype/form_tour_settings_item/__init__.py delete mode 100644 frappe/core/doctype/form_tour_settings_item/form_tour_settings_item.json delete mode 100644 frappe/core/doctype/form_tour_settings_item/form_tour_settings_item.py diff --git a/frappe/core/doctype/form_tour_settings/__init__.py b/frappe/core/doctype/form_tour_settings/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/frappe/core/doctype/form_tour_settings/form_tour_settings.js b/frappe/core/doctype/form_tour_settings/form_tour_settings.js deleted file mode 100644 index 123e51dbf9..0000000000 --- a/frappe/core/doctype/form_tour_settings/form_tour_settings.js +++ /dev/null @@ -1,8 +0,0 @@ -// Copyright (c) 2023, Frappe Technologies and contributors -// For license information, please see license.txt - -// frappe.ui.form.on("Form Tour Settings", { -// refresh(frm) { - -// }, -// }); diff --git a/frappe/core/doctype/form_tour_settings/form_tour_settings.json b/frappe/core/doctype/form_tour_settings/form_tour_settings.json deleted file mode 100644 index 15795edde5..0000000000 --- a/frappe/core/doctype/form_tour_settings/form_tour_settings.json +++ /dev/null @@ -1,51 +0,0 @@ -{ - "actions": [], - "allow_rename": 1, - "creation": "2023-05-11 18:07:26.879273", - "default_view": "List", - "doctype": "DocType", - "editable_grid": 1, - "engine": "InnoDB", - "field_order": [ - "onboarding_tours", - "form_tours" - ], - "fields": [ - { - "fieldname": "form_tours", - "fieldtype": "Table", - "label": "Form Tours", - "options": "Form Tour Settings Item" - }, - { - "default": "\"[]\"", - "fieldname": "onboarding_tours", - "fieldtype": "JSON", - "hidden": 1, - "label": "Onboarding Tours" - } - ], - "index_web_pages_for_search": 1, - "issingle": 1, - "links": [], - "modified": "2023-05-17 16:45:21.362524", - "modified_by": "Administrator", - "module": "Core", - "name": "Form Tour Settings", - "owner": "Administrator", - "permissions": [ - { - "create": 1, - "delete": 1, - "email": 1, - "print": 1, - "read": 1, - "role": "System Manager", - "share": 1, - "write": 1 - } - ], - "sort_field": "modified", - "sort_order": "DESC", - "states": [] -} \ No newline at end of file diff --git a/frappe/core/doctype/form_tour_settings/form_tour_settings.py b/frappe/core/doctype/form_tour_settings/form_tour_settings.py deleted file mode 100644 index 52b68286d7..0000000000 --- a/frappe/core/doctype/form_tour_settings/form_tour_settings.py +++ /dev/null @@ -1,15 +0,0 @@ -# Copyright (c) 2023, Frappe Technologies and contributors -# For license information, please see license.txt - -import json - -import frappe -from frappe.model.document import Document - - -class FormTourSettings(Document): - def on_update(self): - onboarding_tours = [[tour.form_tour, json.loads(tour.page_route)] for tour in self.form_tours] - frappe.db.set_single_value( - "Form Tour Settings", "onboarding_tours", json.dumps(onboarding_tours) - ) diff --git a/frappe/core/doctype/form_tour_settings/test_form_tour_settings.py b/frappe/core/doctype/form_tour_settings/test_form_tour_settings.py deleted file mode 100644 index 95838ecb97..0000000000 --- a/frappe/core/doctype/form_tour_settings/test_form_tour_settings.py +++ /dev/null @@ -1,9 +0,0 @@ -# Copyright (c) 2023, Frappe Technologies and Contributors -# See license.txt - -# import frappe -from frappe.tests.utils import FrappeTestCase - - -class TestFormTourSettings(FrappeTestCase): - pass diff --git a/frappe/core/doctype/form_tour_settings_item/__init__.py b/frappe/core/doctype/form_tour_settings_item/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/frappe/core/doctype/form_tour_settings_item/form_tour_settings_item.json b/frappe/core/doctype/form_tour_settings_item/form_tour_settings_item.json deleted file mode 100644 index 54ab61da21..0000000000 --- a/frappe/core/doctype/form_tour_settings_item/form_tour_settings_item.json +++ /dev/null @@ -1,61 +0,0 @@ -{ - "actions": [], - "allow_rename": 1, - "creation": "2023-05-11 18:10:15.194034", - "doctype": "DocType", - "editable_grid": 1, - "engine": "InnoDB", - "field_order": [ - "form_tour", - "view", - "list_view", - "page_route" - ], - "fields": [ - { - "fieldname": "form_tour", - "fieldtype": "Link", - "in_list_view": 1, - "label": "Form Tour", - "options": "Form Tour", - "read_only": 1 - }, - { - "fetch_from": "form_tour.view_name", - "fieldname": "view", - "fieldtype": "Data", - "in_list_view": 1, - "label": "View", - "read_only": 1 - }, - { - "fetch_from": "form_tour.list_name", - "fieldname": "list_view", - "fieldtype": "Data", - "in_list_view": 1, - "label": "List View", - "read_only": 1 - }, - { - "fetch_from": "form_tour.page_route", - "fieldname": "page_route", - "fieldtype": "Data", - "hidden": 1, - "in_list_view": 1, - "label": "Page Route", - "read_only": 1 - } - ], - "index_web_pages_for_search": 1, - "istable": 1, - "links": [], - "modified": "2023-05-17 22:22:58.507769", - "modified_by": "Administrator", - "module": "Core", - "name": "Form Tour Settings Item", - "owner": "Administrator", - "permissions": [], - "sort_field": "modified", - "sort_order": "DESC", - "states": [] -} \ No newline at end of file diff --git a/frappe/core/doctype/form_tour_settings_item/form_tour_settings_item.py b/frappe/core/doctype/form_tour_settings_item/form_tour_settings_item.py deleted file mode 100644 index 0958b000ad..0000000000 --- a/frappe/core/doctype/form_tour_settings_item/form_tour_settings_item.py +++ /dev/null @@ -1,9 +0,0 @@ -# Copyright (c) 2023, Frappe Technologies and contributors -# For license information, please see license.txt - -# import frappe -from frappe.model.document import Document - - -class FormTourSettingsItem(Document): - pass From 4e74051732c98449fee63baf190a5a9c199378b3 Mon Sep 17 00:00:00 2001 From: Maharshi Patel Date: Tue, 23 May 2023 15:18:15 +0530 Subject: [PATCH 030/203] fix: add form tour settings to desk module. --- .../doctype/form_tour_settings/__init__.py | 0 .../form_tour_settings/form_tour_settings.js | 12 ++++ .../form_tour_settings.json | 51 ++++++++++++++++ .../form_tour_settings/form_tour_settings.py | 33 ++++++++++ .../test_form_tour_settings.py | 9 +++ .../form_tour_settings_item/__init__.py | 0 .../form_tour_settings_item.json | 61 +++++++++++++++++++ .../form_tour_settings_item.py | 9 +++ 8 files changed, 175 insertions(+) create mode 100644 frappe/desk/doctype/form_tour_settings/__init__.py create mode 100644 frappe/desk/doctype/form_tour_settings/form_tour_settings.js create mode 100644 frappe/desk/doctype/form_tour_settings/form_tour_settings.json create mode 100644 frappe/desk/doctype/form_tour_settings/form_tour_settings.py create mode 100644 frappe/desk/doctype/form_tour_settings/test_form_tour_settings.py create mode 100644 frappe/desk/doctype/form_tour_settings_item/__init__.py create mode 100644 frappe/desk/doctype/form_tour_settings_item/form_tour_settings_item.json create mode 100644 frappe/desk/doctype/form_tour_settings_item/form_tour_settings_item.py diff --git a/frappe/desk/doctype/form_tour_settings/__init__.py b/frappe/desk/doctype/form_tour_settings/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frappe/desk/doctype/form_tour_settings/form_tour_settings.js b/frappe/desk/doctype/form_tour_settings/form_tour_settings.js new file mode 100644 index 0000000000..a9d7f62890 --- /dev/null +++ b/frappe/desk/doctype/form_tour_settings/form_tour_settings.js @@ -0,0 +1,12 @@ +// Copyright (c) 2023, Frappe Technologies and contributors +// For license information, please see license.txt + +frappe.ui.form.on("Form Tour Settings", { + refresh(frm) { + frm.dashboard.add_comment( + "This page is used to set priority for the UI form tours. If there are more than 1 matching tours found for the page, the tour with the highest priority will run.", + "blue", + true + ); + }, +}); diff --git a/frappe/desk/doctype/form_tour_settings/form_tour_settings.json b/frappe/desk/doctype/form_tour_settings/form_tour_settings.json new file mode 100644 index 0000000000..15795edde5 --- /dev/null +++ b/frappe/desk/doctype/form_tour_settings/form_tour_settings.json @@ -0,0 +1,51 @@ +{ + "actions": [], + "allow_rename": 1, + "creation": "2023-05-11 18:07:26.879273", + "default_view": "List", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "onboarding_tours", + "form_tours" + ], + "fields": [ + { + "fieldname": "form_tours", + "fieldtype": "Table", + "label": "Form Tours", + "options": "Form Tour Settings Item" + }, + { + "default": "\"[]\"", + "fieldname": "onboarding_tours", + "fieldtype": "JSON", + "hidden": 1, + "label": "Onboarding Tours" + } + ], + "index_web_pages_for_search": 1, + "issingle": 1, + "links": [], + "modified": "2023-05-17 16:45:21.362524", + "modified_by": "Administrator", + "module": "Core", + "name": "Form Tour Settings", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "print": 1, + "read": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC", + "states": [] +} \ No newline at end of file diff --git a/frappe/desk/doctype/form_tour_settings/form_tour_settings.py b/frappe/desk/doctype/form_tour_settings/form_tour_settings.py new file mode 100644 index 0000000000..7d5e38fe37 --- /dev/null +++ b/frappe/desk/doctype/form_tour_settings/form_tour_settings.py @@ -0,0 +1,33 @@ +# Copyright (c) 2023, Frappe Technologies and contributors +# For license information, please see license.txt + +import json + +import frappe +from frappe.model.document import Document + + +class FormTourSettings(Document): + def before_save(self): + self.onboarding_tours = json.dumps( + [[tour.form_tour, json.loads(tour.page_route)] for tour in self.form_tours] + ) + + +@frappe.whitelist() +def update_user_status(value, step): + from frappe.utils.telemetry import capture + + step = frappe.parse_json(step) + tour = frappe.parse_json(value) + # from frappe.utils.telemetry import capture + capture( + frappe.scrub(f"{step.parent}_{step.title}"), + app="frappe_ui_tours", + properties={ + "is_completed": tour.is_completed + }, + ) + frappe.db.set_value( + "User", frappe.session.user, "onboarding_status", value, update_modified=False + ) diff --git a/frappe/desk/doctype/form_tour_settings/test_form_tour_settings.py b/frappe/desk/doctype/form_tour_settings/test_form_tour_settings.py new file mode 100644 index 0000000000..95838ecb97 --- /dev/null +++ b/frappe/desk/doctype/form_tour_settings/test_form_tour_settings.py @@ -0,0 +1,9 @@ +# Copyright (c) 2023, Frappe Technologies and Contributors +# See license.txt + +# import frappe +from frappe.tests.utils import FrappeTestCase + + +class TestFormTourSettings(FrappeTestCase): + pass diff --git a/frappe/desk/doctype/form_tour_settings_item/__init__.py b/frappe/desk/doctype/form_tour_settings_item/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frappe/desk/doctype/form_tour_settings_item/form_tour_settings_item.json b/frappe/desk/doctype/form_tour_settings_item/form_tour_settings_item.json new file mode 100644 index 0000000000..54ab61da21 --- /dev/null +++ b/frappe/desk/doctype/form_tour_settings_item/form_tour_settings_item.json @@ -0,0 +1,61 @@ +{ + "actions": [], + "allow_rename": 1, + "creation": "2023-05-11 18:10:15.194034", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "form_tour", + "view", + "list_view", + "page_route" + ], + "fields": [ + { + "fieldname": "form_tour", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Form Tour", + "options": "Form Tour", + "read_only": 1 + }, + { + "fetch_from": "form_tour.view_name", + "fieldname": "view", + "fieldtype": "Data", + "in_list_view": 1, + "label": "View", + "read_only": 1 + }, + { + "fetch_from": "form_tour.list_name", + "fieldname": "list_view", + "fieldtype": "Data", + "in_list_view": 1, + "label": "List View", + "read_only": 1 + }, + { + "fetch_from": "form_tour.page_route", + "fieldname": "page_route", + "fieldtype": "Data", + "hidden": 1, + "in_list_view": 1, + "label": "Page Route", + "read_only": 1 + } + ], + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2023-05-17 22:22:58.507769", + "modified_by": "Administrator", + "module": "Core", + "name": "Form Tour Settings Item", + "owner": "Administrator", + "permissions": [], + "sort_field": "modified", + "sort_order": "DESC", + "states": [] +} \ No newline at end of file diff --git a/frappe/desk/doctype/form_tour_settings_item/form_tour_settings_item.py b/frappe/desk/doctype/form_tour_settings_item/form_tour_settings_item.py new file mode 100644 index 0000000000..0958b000ad --- /dev/null +++ b/frappe/desk/doctype/form_tour_settings_item/form_tour_settings_item.py @@ -0,0 +1,9 @@ +# Copyright (c) 2023, Frappe Technologies and contributors +# For license information, please see license.txt + +# import frappe +from frappe.model.document import Document + + +class FormTourSettingsItem(Document): + pass From 20d0e2809202e230c42276b6bccf62a21add3816 Mon Sep 17 00:00:00 2001 From: Maharshi Patel Date: Tue, 23 May 2023 15:20:35 +0530 Subject: [PATCH 031/203] fix: remove onboarding_tours_section --- frappe/core/doctype/user/user.json | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/frappe/core/doctype/user/user.json b/frappe/core/doctype/user/user.json index 35a58a5851..26940ac9d9 100644 --- a/frappe/core/doctype/user/user.json +++ b/frappe/core/doctype/user/user.json @@ -92,7 +92,6 @@ "generate_keys", "column_break_65", "api_secret", - "onboarding_tours_section", "onboarding_status", "connections_tab" ], @@ -694,12 +693,6 @@ "fieldtype": "Section Break", "label": "Desk Settings" }, - { - "fieldname": "onboarding_tours_section", - "fieldtype": "Section Break", - "hidden": 1, - "label": "Onboarding Tours" - }, { "default": "{}", "fieldname": "onboarding_status", @@ -768,7 +761,7 @@ "link_fieldname": "user" } ], - "modified": "2023-05-18 12:41:36.765029", + "modified": "2023-05-22 09:29:35.277539", "modified_by": "Administrator", "module": "Core", "name": "User", From 2451d8d263eded576e06e2731eeee4c0bf92e1cc Mon Sep 17 00:00:00 2001 From: Maharshi Patel Date: Tue, 23 May 2023 15:22:27 +0530 Subject: [PATCH 032/203] fix: remove always required for element_selector. --- frappe/desk/doctype/form_tour_step/form_tour_step.json | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) 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 f0cb8751b7..006bb53b7e 100644 --- a/frappe/desk/doctype/form_tour_step/form_tour_step.json +++ b/frappe/desk/doctype/form_tour_step/form_tour_step.json @@ -141,8 +141,7 @@ "fieldname": "element_selector", "fieldtype": "Data", "label": "Element Selector", - "mandatory_depends_on": "eval:(doc.ui_tour)", - "reqd": 1 + "mandatory_depends_on": "eval:(doc.ui_tour)" }, { "depends_on": "eval:(doc.ui_tour)", @@ -217,7 +216,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2023-05-18 01:55:44.245357", + "modified": "2023-05-19 16:35:14.424275", "modified_by": "Administrator", "module": "Desk", "name": "Form Tour Step", From 4557a7b66469254f2b0e7adbf4fc0043ab4a3bab Mon Sep 17 00:00:00 2001 From: Maharshi Patel Date: Tue, 23 May 2023 15:23:58 +0530 Subject: [PATCH 033/203] fix: minor changes as per review. --- frappe/desk/doctype/form_tour/form_tour.js | 160 ++++++++++--------- frappe/desk/doctype/form_tour/form_tour.json | 15 +- frappe/desk/doctype/form_tour/form_tour.py | 31 ++-- 3 files changed, 112 insertions(+), 94 deletions(-) diff --git a/frappe/desk/doctype/form_tour/form_tour.js b/frappe/desk/doctype/form_tour/form_tour.js index 8c14e72f56..34536de317 100644 --- a/frappe/desk/doctype/form_tour/form_tour.js +++ b/frappe/desk/doctype/form_tour/form_tour.js @@ -6,44 +6,23 @@ frappe.ui.form.on("Form Tour", { if (frm.doc.is_standard && !frappe.boot.developer_mode) { frm.trigger("disable_form"); } - frm.fields_dict["report_name"].get_query = function (doc) { - if (doc.reference_doctype) { + frm.set_query("reference_doctype", () => { + return { filters: { istable: 0 } }; + }); + frm.set_query("report_name", () => { + if (frm.doc.reference_doctype) { return { filters: { - ref_doctype: doc.reference_doctype, - }, + ref_doctype: frm.doc.reference_doctype + } }; } return {}; - }; - frm.fields_dict["reference_doctype"].get_query = function (doc) { - return { - filters: { - istable: 0, - }, - }; - }; - !frm.doc.ui_tour && - frm.add_custom_button(__("Show Tour"), async () => { - const issingle = await check_if_single(frm.doc.reference_doctype); - let route_changed = null; - - if (issingle) { - route_changed = frappe.set_route("Form", frm.doc.reference_doctype); - } else if (frm.doc.first_document) { - const name = await get_first_document(frm.doc.reference_doctype); - route_changed = frappe.set_route("Form", frm.doc.reference_doctype, name); - } else { - route_changed = frappe.set_route("Form", frm.doc.reference_doctype, "new"); - } - route_changed.then(() => { - const tour_name = frm.doc.name; - cur_frm.tour.init({ tour_name }).then(() => cur_frm.tour.start()); - }); - }); + }); + !frm.is_new() && add_custom_button(frm); }, async report_name(frm) { - if (!frm.doc.report_name) return; + if (!frm.doc.ui_tour || !frm.doc.report_name) return; let { message } = await frappe.db.get_value("Report", frm.doc.report_name, "ref_doctype"); frm.set_value("reference_doctype", message?.ref_doctype || ""); }, @@ -58,13 +37,13 @@ frappe.ui.form.on("Form Tour", { "Referance Doctype and Dashboard Name both can't be used at the same time." ); } - frm.doc.page_route = JSON.stringify(await get_path(frm)); + frm.doc.ui_tour && (frm.doc.page_route = JSON.stringify(await get_path(frm))); }, - disable_form: function (frm) { + disable_form: function(frm) { frm.set_read_only(); frm.fields - .filter((field) => field.has_input) - .forEach((field) => { + .filter(field => field.has_input) + .forEach(field => { frm.set_df_property(field.df.fieldname, "read_only", "1"); }); frm.disable_save(); @@ -73,8 +52,8 @@ frappe.ui.form.on("Form Tour", { reference_doctype(frm) { if (!frm.doc.reference_doctype) return; - frm.set_fields_as_options("fieldname", frm.doc.reference_doctype, (df) => !df.hidden).then( - (options) => { + frm.set_fields_as_options("fieldname", frm.doc.reference_doctype, df => !df.hidden).then( + options => { frm.fields_dict.steps.grid.update_docfield_property( "fieldname", "options", @@ -86,34 +65,73 @@ frappe.ui.form.on("Form Tour", { frm.set_fields_as_options( "parent_fieldname", frm.doc.reference_doctype, - (df) => df.fieldtype == "Table" && !df.hidden - ).then((options) => { + df => df.fieldtype == "Table" && !df.hidden + ).then(options => { frm.fields_dict.steps.grid.update_docfield_property( "parent_fieldname", "options", [""].concat(options) ); }); - // remove report name if reference doctype is changed and report name is not valid. - frappe.db - .get_list( - "Report", - { - filters: { - ref_doctype: frm.doc.reference_doctype, + if (!frm.doc.ui_tour) { + // remove report name if reference doctype is changed and report name is not valid. + frappe.db + .get_list( + "Report", + { + filters: { + ref_doctype: frm.doc.reference_doctype + } }, - }, - { fields: ["name"] } - ) - .then((reports) => { - if (reports.findIndex((r) => r.name == frm.doc.report_name) == -1) { - frm.set_value("report_name", ""); - frm.refresh_field("report_name"); - } - }); - }, + { fields: ["name"] } + ) + .then(reports => { + if (reports.findIndex(r => r.name == frm.doc.report_name) == -1) { + frm.set_value("report_name", ""); + frm.refresh_field("report_name"); + } + }); + } + } }); +add_custom_button = frm => { + if (frm.doc.ui_tour) { + frm.add_custom_button(__("Reset"), function() { + frappe.confirm( + __("This will reset this tour and show it to all users. Are you sure?"), + function() { + frappe.call({ + method: "frappe.desk.doctype.form_tour.form_tour.reset_tour", + args: { + tour_name: frm.doc.name + } + }); + }, + delete frappe.boot.user.onboarding_status[frm.doc.name] + ); + }); + } else { + frm.add_custom_button(__("Show Tour"), async () => { + const issingle = await check_if_single(frm.doc.reference_doctype); + let route_changed = null; + + if (issingle) { + route_changed = frappe.set_route("Form", frm.doc.reference_doctype); + } else if (frm.doc.first_document) { + const name = await get_first_document(frm.doc.reference_doctype); + route_changed = frappe.set_route("Form", frm.doc.reference_doctype, name); + } else { + route_changed = frappe.set_route("Form", frm.doc.reference_doctype, "new"); + } + route_changed.then(() => { + const tour_name = frm.doc.name; + cur_frm.tour.init({ tour_name }).then(() => cur_frm.tour.start()); + }); + }); + } +}; + frappe.ui.form.on("Form Tour Step", { form_render(frm, cdt, cdn) { if (locals[cdt][cdn].is_table_field) { @@ -125,23 +143,21 @@ frappe.ui.form.on("Form Tour Step", { const parent_fieldname_df = frappe .get_meta(frm.doc.reference_doctype) - .fields.find((df) => df.fieldname == child_row.parent_fieldname); + .fields.find(df => df.fieldname == child_row.parent_fieldname); - frm.set_fields_as_options( - "fieldname", - parent_fieldname_df.options, - (df) => !df.hidden - ).then((options) => { - frm.fields_dict.steps.grid.update_docfield_property( - "fieldname", - "options", - [""].concat(options) - ); - if (child_row.fieldname) { - frappe.model.set_value(cdt, cdn, "fieldname", child_row.fieldname); + frm.set_fields_as_options("fieldname", parent_fieldname_df.options, df => !df.hidden).then( + options => { + frm.fields_dict.steps.grid.update_docfield_property( + "fieldname", + "options", + [""].concat(options) + ); + if (child_row.fieldname) { + frappe.model.set_value(cdt, cdn, "fieldname", child_row.fieldname); + } } - }); - }, + ); + } }); async function check_if_single(doctype) { @@ -156,7 +172,7 @@ async function check_if_private_workspace(name) { async function get_first_document(doctype) { let docname; - await frappe.db.get_list(doctype, { order_by: "creation" }).then((res) => { + await frappe.db.get_list(doctype, { order_by: "creation" }).then(res => { if (Array.isArray(res) && res.length) docname = res[0].name; }); diff --git a/frappe/desk/doctype/form_tour/form_tour.json b/frappe/desk/doctype/form_tour/form_tour.json index 9cc3f65b96..48a8a5cd2f 100644 --- a/frappe/desk/doctype/form_tour/form_tour.json +++ b/frappe/desk/doctype/form_tour/form_tour.json @@ -19,7 +19,6 @@ "column_break_6", "ui_tour", "track_steps", - "reset_tours", "is_standard", "save_on_complete", "first_document", @@ -111,14 +110,6 @@ "hidden": 1, "label": "Page Route" }, - { - "default": "0", - "depends_on": "ui_tour", - "description": "Please check this if you want to reset this tour and show it to all users.", - "fieldname": "reset_tours", - "fieldtype": "Check", - "label": "Reset Tours" - }, { "depends_on": "eval:(doc.ui_tour && doc.view_name == \"List\" && doc.list_name == \"Dashboard\")", "fetch_from": ".", @@ -188,7 +179,7 @@ ], "index_web_pages_for_search": 1, "links": [], - "modified": "2023-05-18 02:47:03.528693", + "modified": "2023-05-23 11:35:14.195031", "modified_by": "Administrator", "module": "Desk", "name": "Form Tour", @@ -206,6 +197,10 @@ "role": "System Manager", "share": 1, "write": 1 + }, + { + "read": 1, + "role": "All" } ], "sort_field": "modified", diff --git a/frappe/desk/doctype/form_tour/form_tour.py b/frappe/desk/doctype/form_tour/form_tour.py index 48b2bd1f2d..6b5b6885b0 100644 --- a/frappe/desk/doctype/form_tour/form_tour.py +++ b/frappe/desk/doctype/form_tour/form_tour.py @@ -14,7 +14,6 @@ class FormTour(Document): if step.is_table_field and step.parent_fieldname: parent_field_df = meta.get_field(step.parent_fieldname) step.child_doctype = parent_field_df.options - field_df = frappe.get_meta(step.child_doctype).get_field(step.fieldname) step.label = field_df.label step.fieldtype = field_df.fieldtype @@ -22,19 +21,8 @@ class FormTour(Document): field_df = meta.get_field(step.fieldname) step.label = field_df.label step.fieldtype = field_df.fieldtype - elif self.reset_tours: - self.reset_tours = 0 - for user in frappe.get_all("User"): - user_doc = frappe.get_doc("User", user.name) - onboarding_status = frappe.parse_json(user_doc.onboarding_status) - if self.name in onboarding_status: - del onboarding_status[self.name] - user_doc.onboarding_status = frappe.as_json(onboarding_status) - user_doc.save() def on_update(self): - if frappe.conf.developer_mode and self.is_standard: - export_to_files([["Form Tour", self.name]], self.module) if self.ui_tour: form_tour_settings = frappe.get_doc("Form Tour Settings", "Form Tour Settings") in_settings = False @@ -59,3 +47,22 @@ class FormTour(Document): child.save() form_tour_settings.form_tours.insert(child_index, child) form_tour_settings.save() + if frappe.conf.developer_mode and self.is_standard: + export_to_files([["Form Tour", self.name]], self.module) + + def on_trash(self): + if self.ui_tour: + form_tour_settings = frappe.get_doc("Form Tour Settings", "Form Tour Settings") + for tour in form_tour_settings.form_tours: + if tour.form_tour == self.name: + form_tour_settings.remove(tour); + form_tour_settings.save() + +@frappe.whitelist() +def reset_tour(tour_name): + for user in frappe.get_all("User"): + user_doc = frappe.get_doc("User", user.name) + onboarding_status = frappe.parse_json(user_doc.onboarding_status) + onboarding_status.pop(tour_name, None) + user_doc.onboarding_status = frappe.as_json(onboarding_status) + user_doc.save() \ No newline at end of file From 69705ec0868a75a5d76568c9d71207e278615daf Mon Sep 17 00:00:00 2001 From: Maharshi Patel Date: Tue, 23 May 2023 15:36:59 +0530 Subject: [PATCH 034/203] fix: update module in json to Desk --- frappe/desk/doctype/form_tour_settings/form_tour_settings.json | 2 +- .../form_tour_settings_item/form_tour_settings_item.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/frappe/desk/doctype/form_tour_settings/form_tour_settings.json b/frappe/desk/doctype/form_tour_settings/form_tour_settings.json index 15795edde5..ff1e2df518 100644 --- a/frappe/desk/doctype/form_tour_settings/form_tour_settings.json +++ b/frappe/desk/doctype/form_tour_settings/form_tour_settings.json @@ -30,7 +30,7 @@ "links": [], "modified": "2023-05-17 16:45:21.362524", "modified_by": "Administrator", - "module": "Core", + "module": "Desk", "name": "Form Tour Settings", "owner": "Administrator", "permissions": [ diff --git a/frappe/desk/doctype/form_tour_settings_item/form_tour_settings_item.json b/frappe/desk/doctype/form_tour_settings_item/form_tour_settings_item.json index 54ab61da21..01f05393a3 100644 --- a/frappe/desk/doctype/form_tour_settings_item/form_tour_settings_item.json +++ b/frappe/desk/doctype/form_tour_settings_item/form_tour_settings_item.json @@ -51,7 +51,7 @@ "links": [], "modified": "2023-05-17 22:22:58.507769", "modified_by": "Administrator", - "module": "Core", + "module": "Desk", "name": "Form Tour Settings Item", "owner": "Administrator", "permissions": [], From 119313810e64b044337e05ec31fdd772251283fa Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Tue, 23 May 2023 15:39:00 +0530 Subject: [PATCH 035/203] fix: misc onboarding fixes (#21078) * fix: show fields without label too * fix: make form tour step editable * fix: hide duplicate save buttons [skip ci] --- frappe/desk/doctype/form_tour_step/form_tour_step.json | 3 ++- frappe/public/js/frappe/form/form.js | 3 ++- frappe/public/js/frappe/form/form_tour.js | 4 ++-- 3 files changed, 6 insertions(+), 4 deletions(-) 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 7eb6eab223..d0c57ba1e6 100644 --- a/frappe/desk/doctype/form_tour_step/form_tour_step.json +++ b/frappe/desk/doctype/form_tour_step/form_tour_step.json @@ -2,6 +2,7 @@ "actions": [], "creation": "2021-05-21 23:05:45.342114", "doctype": "DocType", + "editable_grid": 1, "engine": "InnoDB", "field_order": [ "is_table_field", @@ -115,7 +116,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2022-01-27 15:18:36.481801", + "modified": "2023-05-23 13:09:15.923043", "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 47917422b5..4622c58155 100644 --- a/frappe/public/js/frappe/form/form.js +++ b/frappe/public/js/frappe/form/form.js @@ -1994,7 +1994,8 @@ frappe.ui.form.Form = class FrappeForm { return new Promise((resolve) => { frappe.model.with_doctype(reference_doctype, () => { frappe.get_meta(reference_doctype).fields.map((df) => { - filter_function(df) && options.push({ label: df.label, value: df.fieldname }); + filter_function(df) && + options.push({ label: df.label || df.fieldname, value: df.fieldname }); }); options && this.set_df_property( diff --git a/frappe/public/js/frappe/form/form_tour.js b/frappe/public/js/frappe/form/form_tour.js index dbdd673aea..f7953c08b2 100644 --- a/frappe/public/js/frappe/form/form_tour.js +++ b/frappe/public/js/frappe/form/form_tour.js @@ -261,10 +261,10 @@ frappe.ui.form.FormTour = class FormTour { allowClose: false, overlayClickNext: false, popover: { - title: __("Save"), + title: __("Save the document."), description: "", position: "left", - doneBtnText: __("Save"), + showButtons: false, }, onNext: () => { this.frm.save(); From 22e3ec8bf4b49c6bb4cef017652e54354bd4cc5d Mon Sep 17 00:00:00 2001 From: Aditya Hase Date: Tue, 23 May 2023 15:47:13 +0530 Subject: [PATCH 036/203] fix(build): Propogate exit code from yarn build to bench build (#21084) When `yarn build` fails `bench build` ignores the error and exits with exit code 0 --- esbuild/esbuild.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/esbuild/esbuild.js b/esbuild/esbuild.js index 3c5c305665..4804f0e25f 100644 --- a/esbuild/esbuild.js +++ b/esbuild/esbuild.js @@ -87,7 +87,10 @@ const NODE_PATHS = [].concat( execute() .then(() => RUN_BUILD_COMMAND && run_build_command_for_apps(APPS)) - .catch((e) => console.error(e)); + .catch((e) => { + console.error(e); + throw e; + }); if (WATCH_MODE) { // listen for open files in editor event From a182414610698effb018f2fc7f33a2956169b880 Mon Sep 17 00:00:00 2001 From: PeterG Date: Wed, 24 May 2023 11:50:37 +0545 Subject: [PATCH 037/203] fix(workflow): populate doc from db in apply_workflow (#21068) --- frappe/model/workflow.py | 1 + 1 file changed, 1 insertion(+) diff --git a/frappe/model/workflow.py b/frappe/model/workflow.py index 8338157996..d61d2b3a2b 100644 --- a/frappe/model/workflow.py +++ b/frappe/model/workflow.py @@ -102,6 +102,7 @@ def is_transition_condition_satisfied(transition, doc) -> bool: def apply_workflow(doc, action): """Allow workflow action on the current doc""" doc = frappe.get_doc(frappe.parse_json(doc)) + doc.load_from_db() workflow = get_workflow(doc.doctype) transitions = get_transitions(doc, workflow) user = frappe.session.user From 9fec2fb499631e23d7278259619ea4a67bcf1a71 Mon Sep 17 00:00:00 2001 From: Maharshi Patel Date: Wed, 24 May 2023 11:42:35 +0530 Subject: [PATCH 038/203] fix: change onboarding_status type to long text json type breaks unrelated tests and in mariadb json is alias for long text. --- frappe/core/doctype/user/user.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frappe/core/doctype/user/user.json b/frappe/core/doctype/user/user.json index 26940ac9d9..20e7f05fab 100644 --- a/frappe/core/doctype/user/user.json +++ b/frappe/core/doctype/user/user.json @@ -696,7 +696,7 @@ { "default": "{}", "fieldname": "onboarding_status", - "fieldtype": "JSON", + "fieldtype": "Long Text", "hidden": 1, "label": "Onboarding Status" } @@ -761,7 +761,7 @@ "link_fieldname": "user" } ], - "modified": "2023-05-22 09:29:35.277539", + "modified": "2023-05-24 11:25:27.040415", "modified_by": "Administrator", "module": "Core", "name": "User", From 19c4b396ae91869a6a62b06f67904fe22f43a4ec Mon Sep 17 00:00:00 2001 From: Maharshi Patel Date: Wed, 24 May 2023 11:44:36 +0530 Subject: [PATCH 039/203] chore: fix linters --- frappe/desk/doctype/form_tour/form_tour.js | 68 ++++++++++--------- .../form_tour_settings/form_tour_settings.py | 4 +- frappe/public/js/frappe/desk.js | 2 +- 3 files changed, 37 insertions(+), 37 deletions(-) diff --git a/frappe/desk/doctype/form_tour/form_tour.js b/frappe/desk/doctype/form_tour/form_tour.js index 34536de317..b9010fff0c 100644 --- a/frappe/desk/doctype/form_tour/form_tour.js +++ b/frappe/desk/doctype/form_tour/form_tour.js @@ -13,8 +13,8 @@ frappe.ui.form.on("Form Tour", { if (frm.doc.reference_doctype) { return { filters: { - ref_doctype: frm.doc.reference_doctype - } + ref_doctype: frm.doc.reference_doctype, + }, }; } return {}; @@ -39,11 +39,11 @@ frappe.ui.form.on("Form Tour", { } frm.doc.ui_tour && (frm.doc.page_route = JSON.stringify(await get_path(frm))); }, - disable_form: function(frm) { + disable_form: function (frm) { frm.set_read_only(); frm.fields - .filter(field => field.has_input) - .forEach(field => { + .filter((field) => field.has_input) + .forEach((field) => { frm.set_df_property(field.df.fieldname, "read_only", "1"); }); frm.disable_save(); @@ -52,8 +52,8 @@ frappe.ui.form.on("Form Tour", { reference_doctype(frm) { if (!frm.doc.reference_doctype) return; - frm.set_fields_as_options("fieldname", frm.doc.reference_doctype, df => !df.hidden).then( - options => { + frm.set_fields_as_options("fieldname", frm.doc.reference_doctype, (df) => !df.hidden).then( + (options) => { frm.fields_dict.steps.grid.update_docfield_property( "fieldname", "options", @@ -65,8 +65,8 @@ frappe.ui.form.on("Form Tour", { frm.set_fields_as_options( "parent_fieldname", frm.doc.reference_doctype, - df => df.fieldtype == "Table" && !df.hidden - ).then(options => { + (df) => df.fieldtype == "Table" && !df.hidden + ).then((options) => { frm.fields_dict.steps.grid.update_docfield_property( "parent_fieldname", "options", @@ -80,32 +80,32 @@ frappe.ui.form.on("Form Tour", { "Report", { filters: { - ref_doctype: frm.doc.reference_doctype - } + ref_doctype: frm.doc.reference_doctype, + }, }, { fields: ["name"] } ) - .then(reports => { - if (reports.findIndex(r => r.name == frm.doc.report_name) == -1) { + .then((reports) => { + if (reports.findIndex((r) => r.name == frm.doc.report_name) == -1) { frm.set_value("report_name", ""); frm.refresh_field("report_name"); } }); } - } + }, }); -add_custom_button = frm => { +add_custom_button = (frm) => { if (frm.doc.ui_tour) { - frm.add_custom_button(__("Reset"), function() { + frm.add_custom_button(__("Reset"), function () { frappe.confirm( __("This will reset this tour and show it to all users. Are you sure?"), - function() { + function () { frappe.call({ method: "frappe.desk.doctype.form_tour.form_tour.reset_tour", args: { - tour_name: frm.doc.name - } + tour_name: frm.doc.name, + }, }); }, delete frappe.boot.user.onboarding_status[frm.doc.name] @@ -143,21 +143,23 @@ frappe.ui.form.on("Form Tour Step", { const parent_fieldname_df = frappe .get_meta(frm.doc.reference_doctype) - .fields.find(df => df.fieldname == child_row.parent_fieldname); + .fields.find((df) => df.fieldname == child_row.parent_fieldname); - frm.set_fields_as_options("fieldname", parent_fieldname_df.options, df => !df.hidden).then( - options => { - frm.fields_dict.steps.grid.update_docfield_property( - "fieldname", - "options", - [""].concat(options) - ); - if (child_row.fieldname) { - frappe.model.set_value(cdt, cdn, "fieldname", child_row.fieldname); - } + frm.set_fields_as_options( + "fieldname", + parent_fieldname_df.options, + (df) => !df.hidden + ).then((options) => { + frm.fields_dict.steps.grid.update_docfield_property( + "fieldname", + "options", + [""].concat(options) + ); + if (child_row.fieldname) { + frappe.model.set_value(cdt, cdn, "fieldname", child_row.fieldname); } - ); - } + }); + }, }); async function check_if_single(doctype) { @@ -172,7 +174,7 @@ async function check_if_private_workspace(name) { async function get_first_document(doctype) { let docname; - await frappe.db.get_list(doctype, { order_by: "creation" }).then(res => { + await frappe.db.get_list(doctype, { order_by: "creation" }).then((res) => { if (Array.isArray(res) && res.length) docname = res[0].name; }); diff --git a/frappe/desk/doctype/form_tour_settings/form_tour_settings.py b/frappe/desk/doctype/form_tour_settings/form_tour_settings.py index 7d5e38fe37..57de8d66ee 100644 --- a/frappe/desk/doctype/form_tour_settings/form_tour_settings.py +++ b/frappe/desk/doctype/form_tour_settings/form_tour_settings.py @@ -24,9 +24,7 @@ def update_user_status(value, step): capture( frappe.scrub(f"{step.parent}_{step.title}"), app="frappe_ui_tours", - properties={ - "is_completed": tour.is_completed - }, + properties={"is_completed": tour.is_completed}, ) frappe.db.set_value( "User", frappe.session.user, "onboarding_status", value, update_modified=False diff --git a/frappe/public/js/frappe/desk.js b/frappe/public/js/frappe/desk.js index 27bfe9ae87..720f19c56e 100644 --- a/frappe/public/js/frappe/desk.js +++ b/frappe/public/js/frappe/desk.js @@ -82,7 +82,7 @@ frappe.Application = class Application { if (pending_tours && frappe.boot.onboarding_tours.length > 0) { frappe.require("onboarding_tours.bundle.js", () => { frappe.utils.sleep(1000).then(() => { - frappe.ui.init_onboarding_tour(); + frappe.ui.init_onboarding_tour(); }); }); } From 78dc9fc9acf9c873afe28fd5e06e5c4bf6f5e620 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Wed, 24 May 2023 12:06:04 +0530 Subject: [PATCH 040/203] fix: fieldtype json -> text --- frappe/desk/doctype/form_tour/form_tour.json | 4 ++-- .../desk/doctype/form_tour_settings/form_tour_settings.json | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/frappe/desk/doctype/form_tour/form_tour.json b/frappe/desk/doctype/form_tour/form_tour.json index 48a8a5cd2f..1eeda5598e 100644 --- a/frappe/desk/doctype/form_tour/form_tour.json +++ b/frappe/desk/doctype/form_tour/form_tour.json @@ -106,7 +106,7 @@ { "depends_on": "is_ui_tour", "fieldname": "page_route", - "fieldtype": "JSON", + "fieldtype": "Small Text", "hidden": 1, "label": "Page Route" }, @@ -179,7 +179,7 @@ ], "index_web_pages_for_search": 1, "links": [], - "modified": "2023-05-23 11:35:14.195031", + "modified": "2023-05-24 12:05:25.223405", "modified_by": "Administrator", "module": "Desk", "name": "Form Tour", diff --git a/frappe/desk/doctype/form_tour_settings/form_tour_settings.json b/frappe/desk/doctype/form_tour_settings/form_tour_settings.json index ff1e2df518..755c347264 100644 --- a/frappe/desk/doctype/form_tour_settings/form_tour_settings.json +++ b/frappe/desk/doctype/form_tour_settings/form_tour_settings.json @@ -20,7 +20,7 @@ { "default": "\"[]\"", "fieldname": "onboarding_tours", - "fieldtype": "JSON", + "fieldtype": "Small Text", "hidden": 1, "label": "Onboarding Tours" } @@ -28,7 +28,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2023-05-17 16:45:21.362524", + "modified": "2023-05-24 12:05:52.676242", "modified_by": "Administrator", "module": "Desk", "name": "Form Tour Settings", From 8d63e2f18385d1059b9c87a90c3c3f7bcabde0cb Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Wed, 24 May 2023 12:31:21 +0530 Subject: [PATCH 041/203] fix: clear boot cache on updating form tour --- frappe/desk/doctype/form_tour/form_tour.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/frappe/desk/doctype/form_tour/form_tour.py b/frappe/desk/doctype/form_tour/form_tour.py index 6b5b6885b0..0e8bf05472 100644 --- a/frappe/desk/doctype/form_tour/form_tour.py +++ b/frappe/desk/doctype/form_tour/form_tour.py @@ -23,6 +23,7 @@ class FormTour(Document): step.fieldtype = field_df.fieldtype def on_update(self): + frappe.cache().delete_key("bootinfo") if self.ui_tour: form_tour_settings = frappe.get_doc("Form Tour Settings", "Form Tour Settings") in_settings = False @@ -55,9 +56,10 @@ class FormTour(Document): form_tour_settings = frappe.get_doc("Form Tour Settings", "Form Tour Settings") for tour in form_tour_settings.form_tours: if tour.form_tour == self.name: - form_tour_settings.remove(tour); + form_tour_settings.remove(tour) form_tour_settings.save() - + + @frappe.whitelist() def reset_tour(tour_name): for user in frappe.get_all("User"): @@ -65,4 +67,4 @@ def reset_tour(tour_name): onboarding_status = frappe.parse_json(user_doc.onboarding_status) onboarding_status.pop(tour_name, None) user_doc.onboarding_status = frappe.as_json(onboarding_status) - user_doc.save() \ No newline at end of file + user_doc.save() From 09ce5f7a94a08302fbaca136ced1c059fea14f8e Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Wed, 24 May 2023 12:13:38 +0530 Subject: [PATCH 042/203] chore: fix main workspace tour copy - fix copy - make it run on any workspace --- .../main_workspace_tour/main_workspace_tour.json | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/frappe/desk/form_tour/main_workspace_tour/main_workspace_tour.json b/frappe/desk/form_tour/main_workspace_tour/main_workspace_tour.json index 62888ebdac..afd0583cfb 100644 --- a/frappe/desk/form_tour/main_workspace_tour/main_workspace_tour.json +++ b/frappe/desk/form_tour/main_workspace_tour/main_workspace_tour.json @@ -8,21 +8,20 @@ "include_name_field": 0, "is_standard": 1, "list_name": "", - "modified": "2023-05-18 12:21:54.389743", + "modified": "2023-05-24 12:43:43.741781", "modified_by": "Administrator", "module": "Desk", "name": "Main Workspace Tour", "new_document_form": 0, "owner": "Administrator", "page_name": "", - "page_route": "[\"Workspaces\",\"Build\"]", + "page_route": "[\"Workspaces\",\"*\"]", "reference_doctype": "", "report_name": "", - "reset_tours": 0, "save_on_complete": 0, "steps": [ { - "description": "
You can access different things like report, settings, documents (any doctypes), and modules. It saves you time by eliminating the need to navigate through menus.
", + "description": "This is Awesomebar, it helps you to navigate anywhere in the system, find documents, reports, settings, create new records and many more things.", "element_selector": "#navbar-search", "fieldtype": "0", "has_next_condition": 0, @@ -39,7 +38,7 @@ "ui_tour": 1 }, { - "description": "
Workspaces can be used to quickly access various modules and features. It organizes the available functionalities into logical groups.
", + "description": "These are workspaces. Each module workspace provides insightful information and shortcuts on one page. \n\n

\n\nTip: You can build custom workspaces for your needs.", "element_selector": ".col-lg-2.layout-side-section", "fieldtype": "0", "has_next_condition": 0, @@ -56,7 +55,7 @@ }, { "description": "
Click to visit the Workspace
", - "element_selector": ".desk-sidebar-item.standard-sidebar-item > [title=\"Tools\"]", + "element_selector": ".desk-sidebar-item.standard-sidebar-item > [title=\"Users\"]", "fieldtype": "0", "has_next_condition": 0, "hide_buttons": 1, @@ -68,7 +67,7 @@ "offset_y": 0, "popover_element": 0, "position": "Right", - "title": "Tools Workspace", + "title": "Users Workspace", "ui_tour": 1 } ], @@ -76,5 +75,5 @@ "track_steps": 1, "ui_tour": 1, "view_name": "Workspaces", - "workspace_name": "Build" + "workspace_name": "" } \ No newline at end of file From 657c2ba3b221635408f22aa492d4601e88ec6300 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Wed, 24 May 2023 12:48:07 +0530 Subject: [PATCH 043/203] fix: clear cached onboarding status on update --- frappe/desk/doctype/form_tour_settings/form_tour_settings.py | 1 + 1 file changed, 1 insertion(+) diff --git a/frappe/desk/doctype/form_tour_settings/form_tour_settings.py b/frappe/desk/doctype/form_tour_settings/form_tour_settings.py index 57de8d66ee..5c711f207b 100644 --- a/frappe/desk/doctype/form_tour_settings/form_tour_settings.py +++ b/frappe/desk/doctype/form_tour_settings/form_tour_settings.py @@ -29,3 +29,4 @@ def update_user_status(value, step): frappe.db.set_value( "User", frappe.session.user, "onboarding_status", value, update_modified=False ) + frappe.cache().hdel("bootinfo", frappe.session.user) From 4376ebb3dc45d0d17eae720e4a60619e19fcfa5d Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Wed, 24 May 2023 12:51:23 +0530 Subject: [PATCH 044/203] refactor: replace todo tour with user tour adding more users is most likely to improve adoption. so better place to show tours is user doctype? --- .../user_list_tour/user_list_tour.json | 95 +++++++++++ .../todo_list_tour/todo_list_tour.json | 159 ------------------ .../tools_workspace_tour.json | 79 --------- .../users_workspace_tour.json | 62 +++++++ 4 files changed, 157 insertions(+), 238 deletions(-) create mode 100644 frappe/core/form_tour/user_list_tour/user_list_tour.json delete mode 100644 frappe/desk/form_tour/todo_list_tour/todo_list_tour.json delete mode 100644 frappe/desk/form_tour/tools_workspace_tour/tools_workspace_tour.json create mode 100644 frappe/desk/form_tour/users_workspace_tour/users_workspace_tour.json diff --git a/frappe/core/form_tour/user_list_tour/user_list_tour.json b/frappe/core/form_tour/user_list_tour/user_list_tour.json new file mode 100644 index 0000000000..83ae481d25 --- /dev/null +++ b/frappe/core/form_tour/user_list_tour/user_list_tour.json @@ -0,0 +1,95 @@ +{ + "creation": "2023-05-24 12:53:02.844582", + "dashboard_name": "", + "docstatus": 0, + "doctype": "Form Tour", + "first_document": 0, + "idx": 0, + "include_name_field": 0, + "is_standard": 1, + "list_name": "List", + "modified": "2023-05-24 13:21:29.552864", + "modified_by": "Administrator", + "module": "Core", + "name": "User List Tour", + "new_document_form": 0, + "owner": "Administrator", + "page_name": "", + "page_route": "[\"List\",\"User\",\"List\"]", + "reference_doctype": "User", + "report_name": "", + "save_on_complete": 0, + "steps": [ + { + "description": "List view shows all the documents for a particular DocType. Here you can see all the current enabled users in the system. ", + "element_selector": ".frappe-list", + "fieldtype": "0", + "has_next_condition": 0, + "hide_buttons": 0, + "is_table_field": 0, + "modal_trigger": 0, + "next_on_click": 0, + "offset_x": 0, + "offset_y": 0, + "popover_element": 0, + "position": "Top Center", + "title": "Users List", + "ui_tour": 1 + }, + { + "description": "These are filters. You can use them to narrow down list of records.", + "element_selector": ".standard-filter-section.flex", + "fieldtype": "0", + "has_next_condition": 0, + "hide_buttons": 0, + "is_table_field": 0, + "modal_trigger": 0, + "next_on_click": 1, + "offset_x": 0, + "offset_y": 0, + "popover_element": 0, + "position": "Bottom", + "title": "Filters", + "ui_tour": 1 + }, + { + "description": "When standard filters are not enough you can use advance filters. ", + "element_selector": ".filter-selector > .btn-group", + "fieldtype": "0", + "has_next_condition": 0, + "hide_buttons": 0, + "is_table_field": 0, + "modal_trigger": 0, + "next_on_click": 0, + "offset_x": 0, + "offset_y": 0, + "ondemand_description": "Advance filters are applied on fields with different operators. \n
\nClick on \"Apply Filters\" to continue.", + "popover_element": 0, + "position": "Left", + "title": "Advanced Filters", + "ui_tour": 1 + }, + { + "description": "Let's create a new user.", + "element_selector": ".btn-primary.primary-action", + "fieldtype": "0", + "has_next_condition": 0, + "hide_buttons": 1, + "is_table_field": 0, + "modal_trigger": 0, + "next_on_click": 1, + "offset_x": 0, + "offset_y": 0, + "parent_element_selector": "", + "popover_element": 0, + "position": "Bottom", + "title": "New User", + "ui_tour": 1 + } + ], + "title": "User List Tour", + "track_steps": 1, + "ui_tour": 1, + "view_name": "List", + "workspace_name": "" +} \ No newline at end of file diff --git a/frappe/desk/form_tour/todo_list_tour/todo_list_tour.json b/frappe/desk/form_tour/todo_list_tour/todo_list_tour.json deleted file mode 100644 index 6a561e6f51..0000000000 --- a/frappe/desk/form_tour/todo_list_tour/todo_list_tour.json +++ /dev/null @@ -1,159 +0,0 @@ -{ - "creation": "2023-05-18 12:12:01.839494", - "dashboard_name": "", - "docstatus": 0, - "doctype": "Form Tour", - "first_document": 0, - "idx": 0, - "include_name_field": 0, - "is_standard": 1, - "list_name": "List", - "modified": "2023-05-18 12:22:07.306556", - "modified_by": "Administrator", - "module": "Desk", - "name": "Todo List Tour", - "new_document_form": 0, - "owner": "Administrator", - "page_name": "", - "page_route": "[\"List\",\"ToDo\",\"List\"]", - "reference_doctype": "ToDo", - "report_name": "", - "reset_tours": 0, - "save_on_complete": 0, - "steps": [ - { - "description": "
List View
", - "element_selector": ".layout-main-section.frappe-card", - "fieldtype": "0", - "has_next_condition": 0, - "hide_buttons": 0, - "is_table_field": 0, - "modal_trigger": 0, - "next_on_click": 0, - "offset_x": 0, - "offset_y": 0, - "popover_element": 0, - "position": "Left", - "title": "TODO", - "ui_tour": 1 - }, - { - "description": "
List View as the name suggest is used to see documents/records in list format.
", - "element_selector": ".frappe-list", - "fieldtype": "0", - "has_next_condition": 0, - "hide_buttons": 0, - "is_table_field": 0, - "modal_trigger": 0, - "next_on_click": 0, - "offset_x": 0, - "offset_y": 0, - "popover_element": 0, - "position": "Top Center", - "title": "TODO List", - "ui_tour": 1 - }, - { - "description": "
Using Quick filter you can refine and narrow down the displayed data by applying specific criteria or conditions
", - "element_selector": ".list-sidebar.overlay-sidebar.hidden-xs.hidden-sm", - "fieldtype": "0", - "has_next_condition": 0, - "hide_buttons": 0, - "is_table_field": 0, - "modal_trigger": 0, - "next_on_click": 0, - "offset_x": 0, - "offset_y": 0, - "popover_element": 0, - "position": "Right", - "title": "Sidebar", - "ui_tour": 1 - }, - { - "description": "
You can also filter using this inputs
", - "element_selector": ".standard-filter-section.flex", - "fieldtype": "0", - "has_next_condition": 0, - "hide_buttons": 0, - "is_table_field": 0, - "modal_trigger": 0, - "next_on_click": 1, - "offset_x": 0, - "offset_y": 0, - "popover_element": 0, - "position": "Bottom", - "title": "Input Filters", - "ui_tour": 1 - }, - { - "description": "
Click on the Filter button
", - "element_selector": ".filter-selector > .btn-group", - "fieldtype": "0", - "has_next_condition": 0, - "hide_buttons": 0, - "is_table_field": 0, - "modal_trigger": 0, - "next_on_click": 1, - "offset_x": 0, - "offset_y": 0, - "ondemand_description": "
Aou can add multiple filters and hit apply to refine results
", - "popover_element": 1, - "position": "Left", - "title": "Advanced Filters", - "ui_tour": 1 - }, - { - "description": "
Click here to remove all filter
", - "element_selector": ".filter-selector > .btn-group > .filter-x-button", - "fieldtype": "0", - "has_next_condition": 0, - "hide_buttons": 0, - "is_table_field": 0, - "modal_trigger": 0, - "next_on_click": 1, - "offset_x": 0, - "offset_y": 0, - "popover_element": 0, - "position": "Left", - "title": "Clear Filters", - "ui_tour": 1 - }, - { - "description": "
You can arrange data in ascending or descending order based on selected attributes.\n
    \n
    \n
  1. Click on Last Updated On
  2. \n
    \n
  3. Select the Attribute based on which you want to sort
  4. \n
\n
", - "element_selector": ".sort-selector", - "fieldtype": "0", - "has_next_condition": 0, - "hide_buttons": 0, - "is_table_field": 0, - "modal_trigger": 0, - "next_on_click": 0, - "offset_x": -20, - "offset_y": 0, - "popover_element": 0, - "position": "Left", - "title": "Sort By", - "ui_tour": 1 - }, - { - "description": "
Click to change ascending or descending order.\n
", - "element_selector": ".sort-selector > .btn-group > .btn-order", - "fieldtype": "0", - "has_next_condition": 0, - "hide_buttons": 0, - "is_table_field": 0, - "modal_trigger": 0, - "next_on_click": 0, - "offset_x": -20, - "offset_y": 0, - "popover_element": 0, - "position": "Left", - "title": "Sort By", - "ui_tour": 1 - } - ], - "title": "Todo List Tour", - "track_steps": 1, - "ui_tour": 1, - "view_name": "List", - "workspace_name": "" -} \ No newline at end of file diff --git a/frappe/desk/form_tour/tools_workspace_tour/tools_workspace_tour.json b/frappe/desk/form_tour/tools_workspace_tour/tools_workspace_tour.json deleted file mode 100644 index 6f10c69328..0000000000 --- a/frappe/desk/form_tour/tools_workspace_tour/tools_workspace_tour.json +++ /dev/null @@ -1,79 +0,0 @@ -{ - "creation": "2023-05-18 12:09:40.792239", - "dashboard_name": "", - "docstatus": 0, - "doctype": "Form Tour", - "first_document": 0, - "idx": 0, - "include_name_field": 0, - "is_standard": 1, - "list_name": "", - "modified": "2023-05-18 12:22:01.208707", - "modified_by": "Administrator", - "module": "Desk", - "name": "Tools Workspace Tour", - "new_document_form": 0, - "owner": "Administrator", - "page_name": "", - "page_route": "[\"Workspaces\",\"Tools\"]", - "reference_doctype": "", - "report_name": "", - "reset_tours": 0, - "save_on_complete": 0, - "steps": [ - { - "description": "
This is Tools Workspace
", - "element_selector": ".codex-editor", - "fieldtype": "0", - "has_next_condition": 0, - "hide_buttons": 0, - "is_table_field": 0, - "modal_trigger": 0, - "next_on_click": 0, - "offset_x": 0, - "offset_y": 0, - "popover_element": 0, - "position": "Left", - "title": "Workspace", - "ui_tour": 1 - }, - { - "description": "
Workspace have cards that serve as links to different modules and features. For instance, the Email List card provides easy access to related components like Newsletter and Email Group
", - "element_selector": "[card_name=\"Email\"]", - "fieldtype": "0", - "has_next_condition": 0, - "hide_buttons": 0, - "is_table_field": 0, - "modal_trigger": 0, - "next_on_click": 0, - "offset_x": 0, - "offset_y": 0, - "popover_element": 0, - "position": "Right", - "title": "Email Card", - "ui_tour": 1 - }, - { - "description": "
Shortcuts are a set of clickable links that serve as direct links to frequently accessed modules and features
", - "element_selector": "[shortcut_name=\"ToDo\"]", - "fieldtype": "0", - "has_next_condition": 0, - "hide_buttons": 1, - "is_table_field": 0, - "modal_trigger": 0, - "next_form_tour": "Todo List Tour", - "next_on_click": 0, - "offset_x": 0, - "offset_y": 0, - "popover_element": 0, - "position": "Right", - "title": "Todo Shortcut", - "ui_tour": 1 - } - ], - "title": "Tools Workspace Tour", - "track_steps": 1, - "ui_tour": 1, - "view_name": "Workspaces", - "workspace_name": "Tools" -} \ No newline at end of file diff --git a/frappe/desk/form_tour/users_workspace_tour/users_workspace_tour.json b/frappe/desk/form_tour/users_workspace_tour/users_workspace_tour.json new file mode 100644 index 0000000000..97159ba6e3 --- /dev/null +++ b/frappe/desk/form_tour/users_workspace_tour/users_workspace_tour.json @@ -0,0 +1,62 @@ +{ + "creation": "2023-05-24 12:50:23.740052", + "dashboard_name": "", + "docstatus": 0, + "doctype": "Form Tour", + "first_document": 0, + "idx": 0, + "include_name_field": 0, + "is_standard": 1, + "list_name": "", + "modified": "2023-05-24 13:01:56.539128", + "modified_by": "Administrator", + "module": "Desk", + "name": "Users Workspace Tour", + "new_document_form": 0, + "owner": "Administrator", + "page_name": "", + "page_route": "[\"Workspaces\",\"Users\"]", + "reference_doctype": "", + "report_name": "", + "save_on_complete": 0, + "steps": [ + { + "description": "This is Users Workspace. You'll find all shortcuts for user, roles and permission management here.", + "element_selector": ".codex-editor", + "fieldtype": "0", + "has_next_condition": 0, + "hide_buttons": 0, + "is_table_field": 0, + "modal_trigger": 0, + "next_on_click": 0, + "offset_x": 0, + "offset_y": 0, + "popover_element": 0, + "position": "Left", + "title": "Workspace", + "ui_tour": 1 + }, + { + "description": "This is a shortcut to User DocType. \n
\n\nLet's Click on the User shortcut to explore all users in System.", + "element_selector": "[shortcut_name=\"User\"]", + "fieldtype": "0", + "has_next_condition": 0, + "hide_buttons": 1, + "is_table_field": 0, + "modal_trigger": 0, + "next_form_tour": "User List Tour", + "next_on_click": 0, + "offset_x": 0, + "offset_y": 0, + "popover_element": 0, + "position": "Right", + "title": "Users Shortcut", + "ui_tour": 1 + } + ], + "title": "Users Workspace Tour", + "track_steps": 1, + "ui_tour": 1, + "view_name": "Workspaces", + "workspace_name": "Users" +} \ No newline at end of file From a39d478e2c819d10cf8f21d5c6efadb0440f9395 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Wed, 24 May 2023 13:27:53 +0530 Subject: [PATCH 045/203] fix: email notification --- frappe/core/doctype/user/user.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/frappe/core/doctype/user/user.py b/frappe/core/doctype/user/user.py index 14266e4cd8..8e00aa7f0f 100644 --- a/frappe/core/doctype/user/user.py +++ b/frappe/core/doctype/user/user.py @@ -282,6 +282,10 @@ class User(Document): self.email_new_password(new_password) except frappe.OutgoingEmailError: + frappe.clear_last_message() + frappe.msgprint( + _("Please setup default outgoing Email Account from Settings > Email Account"), alert=True + ) # email server not set, don't send email self.log_error("Unable to send new password notification") From 49fe6e0c985dccc30d08d5677683eb27b6f39682 Mon Sep 17 00:00:00 2001 From: Gursheen Kaur Anand <40693548+GursheenK@users.noreply.github.com> Date: Wed, 24 May 2023 14:47:06 +0530 Subject: [PATCH 046/203] feat: patches.txt template added by default for new apps #21046 (#21070) * Added patches.txt template in boilerplate * test: new app patches.txt * style: formatting --------- Co-authored-by: Gursheen Anand Co-authored-by: Ankush Menat --- frappe/modules/patch_handler.py | 51 +++++++++++++++++--------------- frappe/tests/test_boilerplate.py | 7 ++++- frappe/utils/boilerplate.py | 10 ++++++- 3 files changed, 42 insertions(+), 26 deletions(-) diff --git a/frappe/modules/patch_handler.py b/frappe/modules/patch_handler.py index 8b25ffcb8e..bf0bd3d869 100644 --- a/frappe/modules/patch_handler.py +++ b/frappe/modules/patch_handler.py @@ -101,40 +101,43 @@ def get_patches_from_app(app: str, patch_type: PatchType | None = None) -> list[ 2. plain text file with each line representing a patch. """ - patches_txt = frappe.get_pymodule_path(app, "patches.txt") + patches_file = frappe.get_pymodule_path(app, "patches.txt") try: - # Attempt to parse as ini file with pre/post patches - # allow_no_value: patches are not key value pairs - # delimiters = '\n' to avoid treating default `:` and `=` in execute as k:v delimiter - parser = configparser.ConfigParser(allow_no_value=True, delimiters="\n") - # preserve case - parser.optionxform = str - parser.read(patches_txt) - - # empty file - if not parser.sections(): - return [] - - if not patch_type: - return [patch for patch in parser[PatchType.pre_model_sync.value]] + [ - patch for patch in parser[PatchType.post_model_sync.value] - ] - - if patch_type.value in parser.sections(): - return [patch for patch in parser[patch_type.value]] - else: - frappe.throw(frappe._("Patch type {} not found in patches.txt").format(patch_type)) - + return parse_as_configfile(patches_file, patch_type) except configparser.MissingSectionHeaderError: # treat as old format with each line representing a single patch # backward compatbility with old patches.txt format if not patch_type or patch_type == PatchType.pre_model_sync: - return frappe.get_file_items(patches_txt) + return frappe.get_file_items(patches_file) return [] +def parse_as_configfile(patches_file: str, patch_type: PatchType | None = None) -> list[str]: + # Attempt to parse as ini file with pre/post patches + # allow_no_value: patches are not key value pairs + # delimiters = '\n' to avoid treating default `:` and `=` in execute as k:v delimiter + parser = configparser.ConfigParser(allow_no_value=True, delimiters="\n") + # preserve case + parser.optionxform = str + parser.read(patches_file) + + # empty file + if not parser.sections(): + return [] + + if not patch_type: + return [patch for patch in parser[PatchType.pre_model_sync.value]] + [ + patch for patch in parser[PatchType.post_model_sync.value] + ] + + if patch_type.value in parser.sections(): + return [patch for patch in parser[patch_type.value]] + else: + frappe.throw(frappe._("Patch type {} not found in patches.txt").format(patch_type)) + + def reload_doc(args): import frappe.modules diff --git a/frappe/tests/test_boilerplate.py b/frappe/tests/test_boilerplate.py index 0f58e84df4..717fdc7ab8 100644 --- a/frappe/tests/test_boilerplate.py +++ b/frappe/tests/test_boilerplate.py @@ -12,7 +12,7 @@ import git import yaml import frappe -from frappe.modules.patch_handler import get_all_patches +from frappe.modules.patch_handler import get_all_patches, parse_as_configfile from frappe.utils.boilerplate import ( PatchCreator, _create_app_boilerplate, @@ -138,6 +138,11 @@ class TestBoilerPlate(unittest.TestCase): app_repo = git.Repo(new_app_dir) self.assertEqual(app_repo.active_branch.name, "develop") + patches_file = os.path.join(new_app_dir, app_name, "patches.txt") + self.assertTrue(os.path.exists(patches_file), msg=f"{patches_file} not found") + + self.assertEqual(parse_as_configfile(patches_file), []) + def test_create_app_without_git_init(self): app_name = "test_app_no_git" diff --git a/frappe/utils/boilerplate.py b/frappe/utils/boilerplate.py index 2e8a5088ed..0d786972fb 100644 --- a/frappe/utils/boilerplate.py +++ b/frappe/utils/boilerplate.py @@ -138,7 +138,8 @@ def _create_app_boilerplate(dest, hooks, no_git=False): with open(os.path.join(dest, hooks.app_name, hooks.app_name, "hooks.py"), "w") as f: f.write(frappe.as_unicode(hooks_template.format(**hooks))) - touch_file(os.path.join(dest, hooks.app_name, hooks.app_name, "patches.txt")) + with open(os.path.join(dest, hooks.app_name, hooks.app_name, "patches.txt"), "w") as f: + f.write(frappe.as_unicode(patches_template.format(**hooks))) app_directory = os.path.join(dest, hooks.app_name) @@ -631,3 +632,10 @@ jobs: env: TYPE: server """ + +patches_template = """[pre_model_sync] +# Patches added in this section will be executed before doctypes are migrated +# Read docs to understand patches: https://frappeframework.com/docs/v14/user/en/database-migrations + +[post_model_sync] +# Patches added in this section will be executed after doctypes are migrated""" From 6065179080e69a1fe82b7fce4390ee7bba7979f5 Mon Sep 17 00:00:00 2001 From: Sagar Vora Date: Wed, 24 May 2023 15:05:50 +0530 Subject: [PATCH 047/203] chore: bump `requests`, `chardet`, and `dropbox` to latest versions (#21080) * chore: bump `requests` and `chardet` * chore: bump `dropbox` to `11.36.0`, ignore `GHSA-4xqq-73wg-5mjp` during pip-audit --- .github/workflows/linters.yml | 2 +- pyproject.toml | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/linters.yml b/.github/workflows/linters.yml index be343c1254..c563f9e43f 100644 --- a/.github/workflows/linters.yml +++ b/.github/workflows/linters.yml @@ -97,4 +97,4 @@ jobs: pip install pip-audit cd ${GITHUB_WORKSPACE} sed -i '/dropbox/d' pyproject.toml # Remove dropbox temporarily https://github.com/dropbox/dropbox-sdk-python/pull/456 - pip-audit --desc on . + pip-audit --desc on --ignore-vuln GHSA-4xqq-73wg-5mjp . diff --git a/pyproject.toml b/pyproject.toml index f2688e97ed..aa89eed928 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,7 +30,7 @@ dependencies = [ "bleach-allowlist~=1.0.3", "bleach~=3.3.0", "cairocffi==1.2.0", - "chardet~=4.0.0", + "chardet~=5.1.0", "croniter~=1.3.5", "cryptography~=39.0.1", "email-reply-parser~=0.5.12", @@ -61,7 +61,7 @@ dependencies = [ "redis~=4.5.4", "hiredis~=2.0.0", "requests-oauthlib~=1.3.0", - "requests~=2.27.1", + "requests~=2.31.0", "rq~=1.11.1", "rsa>=4.1", "semantic-version~=2.10.0", @@ -75,7 +75,7 @@ dependencies = [ # integration dependencies "boto3~=1.18.49", - "dropbox~=11.7.0", + "dropbox~=11.36.0", "google-api-python-client~=2.2.0", "google-auth-oauthlib~=0.4.4", "google-auth~=1.29.0", From d155c3f843c798ce760a916457a794456e95a07f Mon Sep 17 00:00:00 2001 From: Marica Date: Wed, 24 May 2023 15:07:02 +0530 Subject: [PATCH 048/203] fix: Exclude Geolocation from "hide empty read-only field" (#21088) --- frappe/public/js/frappe/form/controls/base_control.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/public/js/frappe/form/controls/base_control.js b/frappe/public/js/frappe/form/controls/base_control.js index 2188f29e94..fe665cee06 100644 --- a/frappe/public/js/frappe/form/controls/base_control.js +++ b/frappe/public/js/frappe/form/controls/base_control.js @@ -125,7 +125,7 @@ frappe.ui.form.Control = class BaseControl { status === "Read" && !this.only_input && is_null(value) && - !in_list(["HTML", "Image", "Button"], this.df.fieldtype) + !in_list(["HTML", "Image", "Button", "Geolocation"], this.df.fieldtype) ) { // eslint-disable-next-line if (explain) console.log("By Hide Read-only, null fields: None"); // eslint-disable-line no-console From 5290bbb1c647b6b4c5f1ea2b3334cb2a0d83963c Mon Sep 17 00:00:00 2001 From: Maharshi Patel Date: Wed, 24 May 2023 15:40:48 +0530 Subject: [PATCH 049/203] fix: reload form_tour_settings reload form_tour_settings and form_tour_settings_item before form_tour because form_tour on_update have get_doc for form_tour_settings. --- frappe/utils/install.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/frappe/utils/install.py b/frappe/utils/install.py index df918c27e0..50feab0475 100644 --- a/frappe/utils/install.py +++ b/frappe/utils/install.py @@ -14,6 +14,8 @@ def before_install(): frappe.reload_doc("core", "doctype", "doctype_action") frappe.reload_doc("core", "doctype", "doctype_link") frappe.reload_doc("desk", "doctype", "form_tour_step") + frappe.reload_doc("desk", "doctype", "form_tour_settings") + frappe.reload_doc("desk", "doctype", "form_tour_settings_item") frappe.reload_doc("desk", "doctype", "form_tour") frappe.reload_doc("core", "doctype", "doctype") From f612d84247a6d8298900f72ec2af20e5291669c6 Mon Sep 17 00:00:00 2001 From: Maharshi Patel Date: Wed, 24 May 2023 15:42:16 +0530 Subject: [PATCH 050/203] fix: change onboarding_status fieldtype change onboarding_status to small text and handle null cases. --- frappe/boot.py | 2 +- frappe/core/doctype/user/user.json | 4 ++-- frappe/public/js/frappe/desk.js | 6 +++++- frappe/public/js/onboarding_tours/onboarding_tours.js | 5 ++++- 4 files changed, 12 insertions(+), 5 deletions(-) diff --git a/frappe/boot.py b/frappe/boot.py index 2e8cb56f33..a54e6e2f07 100644 --- a/frappe/boot.py +++ b/frappe/boot.py @@ -71,7 +71,7 @@ def get_bootinfo(): bootinfo.onboarding_tours = ( frappe.parse_json(frappe.db.get_single_value("Form Tour Settings", "onboarding_tours") or "[]") if frappe.get_system_settings("enable_onboarding") - else "[]" + else [] ) set_time_zone(bootinfo) diff --git a/frappe/core/doctype/user/user.json b/frappe/core/doctype/user/user.json index 20e7f05fab..654f20936e 100644 --- a/frappe/core/doctype/user/user.json +++ b/frappe/core/doctype/user/user.json @@ -696,7 +696,7 @@ { "default": "{}", "fieldname": "onboarding_status", - "fieldtype": "Long Text", + "fieldtype": "Small Text", "hidden": 1, "label": "Onboarding Status" } @@ -761,7 +761,7 @@ "link_fieldname": "user" } ], - "modified": "2023-05-24 11:25:27.040415", + "modified": "2023-05-24 15:20:06.434506", "modified_by": "Administrator", "module": "Core", "name": "User", diff --git a/frappe/public/js/frappe/desk.js b/frappe/public/js/frappe/desk.js index 720f19c56e..f92bb3a1bf 100644 --- a/frappe/public/js/frappe/desk.js +++ b/frappe/public/js/frappe/desk.js @@ -74,7 +74,11 @@ frappe.Application = class Application { // page container this.make_page_container(); - if (!window.Cypress) { + if ( + !window.Cypress && + frappe.boot.onboarding_tours && + frappe.boot.user.onboarding_status != null + ) { let pending_tours = frappe.boot.onboarding_tours.findIndex((tour) => { frappe.boot.user.onboarding_status[tour[0]]?.is_complete == true; diff --git a/frappe/public/js/onboarding_tours/onboarding_tours.js b/frappe/public/js/onboarding_tours/onboarding_tours.js index 16e457ee71..63ecd6d670 100644 --- a/frappe/public/js/onboarding_tours/onboarding_tours.js +++ b/frappe/public/js/onboarding_tours/onboarding_tours.js @@ -248,6 +248,9 @@ frappe.ui.OnboardingTour = class OnboardingTour { }; frappe.ui.init_onboarding_tour = () => { + typeof frappe.boot.onboarding_tours == "undefined" && frappe.boot.onboarding_tours == []; + typeof frappe.boot.user.onboarding_status == "undefined" && + frappe.boot.user.onboarding_status == {}; let route = frappe.router.current_route; if (route[0] === "") return; @@ -313,7 +316,7 @@ frappe.ui.init_onboarding_tour = () => { } } if (!tour_name) return; - if (frappe.ui.currentTourInstance) { + if (frappe.ui.currentTourInstance?.driver) { frappe.ui.currentTourInstance.driver_steps = []; frappe.ui.currentTourInstance.driver.reset(true); frappe.ui.currentTourInstance.update_driver_steps(); From 6354a018de798c49c69de28c3493b04fa43bf96c Mon Sep 17 00:00:00 2001 From: Richard Case <110036763+casesolved-co-uk@users.noreply.github.com> Date: Wed, 24 May 2023 11:30:32 +0100 Subject: [PATCH 051/203] feat: wkhtmltopdf logging (#19935) * feat: wkhtmltopdf logging * fix: must supply output function --- frappe/utils/logger.py | 28 ++++++++++++++++++++++++++++ frappe/utils/pdf.py | 12 ++++++++++-- 2 files changed, 38 insertions(+), 2 deletions(-) diff --git a/frappe/utils/logger.py b/frappe/utils/logger.py index ddb81f3d79..8976130a7c 100755 --- a/frappe/utils/logger.py +++ b/frappe/utils/logger.py @@ -1,6 +1,8 @@ # imports - standard imports import logging import os +import sys +from contextlib import contextmanager from copy import deepcopy from logging.handlers import RotatingFileHandler from typing import Literal @@ -123,3 +125,29 @@ def sanitized_dict(form_dict): if secret_kw in k: sanitized_dict[k] = "********" return sanitized_dict + + +@contextmanager +def pipe_to_log(logger_fn, stream=None): + "Pass an existing logger function e.g. logger.info. Stream defaults to stdout" + # late bind source + if stream is None: + stream = sys.stdout + + stream_int = stream.fileno() + r_int, w_int = os.pipe() + + # copy stream_fd before it is overwritten + with os.fdopen(os.dup(stream_int), "wb") as copied: + stream.flush() + os.dup2(w_int, stream_int) # $ exec >&pipe + try: + with os.fdopen(w_int, "wb"): + yield stream + finally: + # restore stream to its previous value + stream.flush() + os.dup2(copied.fileno(), stream_int) # $ exec >&copied + with os.fdopen(r_int, newline="") as r: + text = r.read() + logger_fn(text) diff --git a/frappe/utils/pdf.py b/frappe/utils/pdf.py index 0c273854f7..a0ba4a6de1 100644 --- a/frappe/utils/pdf.py +++ b/frappe/utils/pdf.py @@ -14,6 +14,7 @@ import frappe from frappe import _ from frappe.utils import scrub_urls from frappe.utils.jinja_globals import bundled_asset, is_rtl +from frappe.utils.logger import pipe_to_log PDF_CONTENT_ERRORS = [ "ContentNotFoundError", @@ -22,6 +23,9 @@ PDF_CONTENT_ERRORS = [ "RemoteHostClosedError", ] +logger = frappe.logger("wkhtmltopdf", max_size=100000, file_count=3) +logger.setLevel("INFO") + def pdf_header_html(soup, head, content, styles, html_id, css): return frappe.render_template( @@ -59,8 +63,13 @@ def get_pdf(html, options=None, output: PdfWriter | None = None): options.update({"disable-smart-shrinking": ""}) try: + # wkhtmltopdf writes the pdf to stdout and errors to stderr + # pdfkit v1.0.0 writes the pdf to file or returns it + # stderr is written to sys.stdout if verbose=True is supplied # Set filename property to false, so no file is actually created - filedata = pdfkit.from_string(html, options=options or {}, verbose=True) + # defaults to redirecting stdout + with pipe_to_log(logger.info): + filedata = pdfkit.from_string(html, False, options=options or {}, verbose=True) # create in-memory binary streams from filedata and create a PdfReader object reader = PdfReader(io.BytesIO(filedata)) @@ -118,7 +127,6 @@ def prepare_options(html, options): "print-media-type": None, "background": None, "images": None, - "quiet": None, # 'no-outline': None, "encoding": "UTF-8", # 'load-error-handling': 'ignore' From 4ab98b998defa244419bf826e5a183799bb01b36 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Wed, 24 May 2023 17:43:51 +0530 Subject: [PATCH 052/203] fix: setup wizard recursion in routing (#21101) - setup wizard shows slide from index in route - if you visit `/app/setup-wizard` then index in route is `undefined` - because of hacky code we do `id + ''` so `undefined` becomes `"undefined"` a truthy value The recursion of undefined > 0 > undefined > 0 continues until browser intervenes and stops further routing. --- frappe/desk/page/setup_wizard/setup_wizard.js | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/frappe/desk/page/setup_wizard/setup_wizard.js b/frappe/desk/page/setup_wizard/setup_wizard.js index 862ac8c14d..7d68fd683c 100644 --- a/frappe/desk/page/setup_wizard/setup_wizard.js +++ b/frappe/desk/page/setup_wizard/setup_wizard.js @@ -49,19 +49,14 @@ frappe.pages["setup-wizard"].on_page_load = function (wrapper) { }; frappe.wizard = new frappe.setup.SetupWizard(wizard_settings); frappe.setup.run_event("after_load"); - let route = frappe.get_route(); - if (route) { - frappe.wizard.show_slide(route[1]); - } + frappe.wizard.show_slide(cint(frappe.get_route()[1])); }, }); }); }; frappe.pages["setup-wizard"].on_page_show = function () { - if (frappe.get_route()[1]) { - frappe.wizard && frappe.wizard.show_slide(frappe.get_route()[1]); - } + frappe.wizard && frappe.wizard.show_slide(cint(frappe.get_route()[1])); }; frappe.setup.on("before_load", function () { @@ -125,7 +120,7 @@ frappe.setup.SetupWizard = class SetupWizard extends frappe.ui.Slides { return; } super.show_slide(id); - frappe.set_route(this.page_name, id + ""); + frappe.set_route(this.page_name, cstr(id)); } show_hide_prev_next(id) { From af3213a44582b1d6c1bb1ecc396241c386bfe61c Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Wed, 24 May 2023 18:13:16 +0530 Subject: [PATCH 053/203] fix: offset log cleanup to avoid deadlocks (#21105) - Email queue is cleared at midnight - Another worker at same time might be email Email This causes random failures almost daily --- frappe/hooks.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/frappe/hooks.py b/frappe/hooks.py index 6d8c00d483..edf572b642 100644 --- a/frappe/hooks.py +++ b/frappe/hooks.py @@ -195,6 +195,14 @@ scheduler_events = { "0/10 * * * *": [ "frappe.email.doctype.email_account.email_account.pull", ], + # Hourly but offset by 30 minutes + # "30 * * * *": [ + # + # ], + # Daily but offset by 45 minutes + "45 0 * * *": [ + "frappe.core.doctype.log_settings.log_settings.run_log_clean_up", + ], }, "all": [ "frappe.email.queue.flush", @@ -227,7 +235,6 @@ scheduler_events = { "frappe.automation.doctype.auto_repeat.auto_repeat.make_auto_repeat_entry", "frappe.automation.doctype.auto_repeat.auto_repeat.set_auto_repeat_as_completed", "frappe.email.doctype.unhandled_email.unhandled_email.remove_old_unhandled_emails", - "frappe.core.doctype.log_settings.log_settings.run_log_clean_up", ], "daily_long": [ "frappe.integrations.doctype.dropbox_settings.dropbox_settings.take_backups_daily", From 32396c7fe5f19ec3342250f5af321f99edf51e19 Mon Sep 17 00:00:00 2001 From: Maharshi Patel Date: Thu, 25 May 2023 11:42:24 +0530 Subject: [PATCH 054/203] fix: guess module if not set in form tour if unable to guess save in Desk module. --- frappe/desk/doctype/form_tour/form_tour.json | 5 ++--- frappe/desk/doctype/form_tour/form_tour.py | 8 ++++++++ 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/frappe/desk/doctype/form_tour/form_tour.json b/frappe/desk/doctype/form_tour/form_tour.json index 1eeda5598e..95ec270f2c 100644 --- a/frappe/desk/doctype/form_tour/form_tour.json +++ b/frappe/desk/doctype/form_tour/form_tour.json @@ -70,10 +70,10 @@ "label": "Is Standard" }, { + "depends_on": "eval: doc.ui_tour && doc.is_standard", "fetch_from": "reference_doctype.module", "fieldname": "module", "fieldtype": "Link", - "hidden": 1, "label": "Module", "options": "Module Def", "read_only": 1 @@ -104,7 +104,6 @@ "set_only_once": 1 }, { - "depends_on": "is_ui_tour", "fieldname": "page_route", "fieldtype": "Small Text", "hidden": 1, @@ -179,7 +178,7 @@ ], "index_web_pages_for_search": 1, "links": [], - "modified": "2023-05-24 12:05:25.223405", + "modified": "2023-05-25 11:30:44.396248", "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 0e8bf05472..2271563e71 100644 --- a/frappe/desk/doctype/form_tour/form_tour.py +++ b/frappe/desk/doctype/form_tour/form_tour.py @@ -8,6 +8,14 @@ from frappe.modules.export_file import export_to_files class FormTour(Document): def before_save(self): + if self.is_standard and not self.module: + if self.workspace_name: + self.module = frappe.db.get_value("Workspace", self.workspace_name, "module") + elif self.dashboard_name: + dashboard_doctype = frappe.db.get_value("Dashboard", self.dashboard_name, "module") + self.module = frappe.db.get_value("DocType", dashboard_doctype, "module") + else: + self.module = "Desk" if not self.ui_tour: meta = frappe.get_meta(self.reference_doctype) for step in self.steps: From 40b78692fea0a3a88444b0a454721c5fd5e2ba83 Mon Sep 17 00:00:00 2001 From: Maharshi Patel Date: Thu, 25 May 2023 11:44:19 +0530 Subject: [PATCH 055/203] chore: fix linters missing translate function and newline at end of file. --- frappe/desk/doctype/form_tour/form_tour.js | 2 +- frappe/public/js/onboarding_tours.bundle.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/frappe/desk/doctype/form_tour/form_tour.js b/frappe/desk/doctype/form_tour/form_tour.js index b9010fff0c..8a65cc1619 100644 --- a/frappe/desk/doctype/form_tour/form_tour.js +++ b/frappe/desk/doctype/form_tour/form_tour.js @@ -34,7 +34,7 @@ frappe.ui.form.on("Form Tour", { frm.doc.reference_doctype ) { frappe.throw( - "Referance Doctype and Dashboard Name both can't be used at the same time." + __("Referance Doctype and Dashboard Name both can't be used at the same time.") ); } frm.doc.ui_tour && (frm.doc.page_route = JSON.stringify(await get_path(frm))); diff --git a/frappe/public/js/onboarding_tours.bundle.js b/frappe/public/js/onboarding_tours.bundle.js index 90788cedfc..6ed7934b78 100644 --- a/frappe/public/js/onboarding_tours.bundle.js +++ b/frappe/public/js/onboarding_tours.bundle.js @@ -1 +1 @@ -import "./onboarding_tours/onboarding_tours.js"; \ No newline at end of file +import "./onboarding_tours/onboarding_tours.js"; From c5e62cac261d0c6bb42f2a8ca59272f44ae0d9d2 Mon Sep 17 00:00:00 2001 From: Ritwik Puri Date: Thu, 25 May 2023 12:29:20 +0530 Subject: [PATCH 056/203] fix: allow setting default in longtext and text columns (#21089) --- frappe/database/schema.py | 1 - 1 file changed, 1 deletion(-) diff --git a/frappe/database/schema.py b/frappe/database/schema.py index 7a8330595e..11948eda66 100644 --- a/frappe/database/schema.py +++ b/frappe/database/schema.py @@ -205,7 +205,6 @@ class DbColumn: self.default and (self.default not in frappe.db.DEFAULT_SHORTCUTS) and not cstr(self.default).startswith(":") - and column_def not in ("text", "longtext") ): column_def += f" default {frappe.db.escape(self.default)}" From 76d7e6e3791bcaeb8346576f105d12e47dd31bf5 Mon Sep 17 00:00:00 2001 From: Yash Jane Date: Thu, 25 May 2023 13:19:07 +0530 Subject: [PATCH 057/203] feat: added email template customization option for welcome and password reset emails --- .../system_settings/system_settings.json | 16 +++++++++++++- frappe/core/doctype/user/user.py | 21 +++++++++++++++---- 2 files changed, 32 insertions(+), 5 deletions(-) diff --git a/frappe/core/doctype/system_settings/system_settings.json b/frappe/core/doctype/system_settings/system_settings.json index 091dc1df1e..5efe87da25 100644 --- a/frappe/core/doctype/system_settings/system_settings.json +++ b/frappe/core/doctype/system_settings/system_settings.json @@ -72,6 +72,8 @@ "disable_standard_email_footer", "hide_footer_in_auto_email_reports", "attach_view_link", + "welcome_email_template", + "reset_password_template", "prepared_report_section", "max_auto_email_report_per_user", "system_updates_section", @@ -549,12 +551,24 @@ "fieldname": "enable_telemetry", "fieldtype": "Check", "label": "Allow Sending Usage Data for Improving Applications" + }, + { + "fieldname": "welcome_email_template", + "fieldtype": "Link", + "label": "Welcome Email Template", + "options": "Email Template" + }, + { + "fieldname": "reset_password_template", + "fieldtype": "Link", + "label": "Reset Password Template", + "options": "Email Template" } ], "icon": "fa fa-cog", "issingle": 1, "links": [], - "modified": "2023-04-23 11:14:59.302851", + "modified": "2023-05-25 13:02:54.808773", "modified_by": "Administrator", "module": "Core", "name": "System Settings", diff --git a/frappe/core/doctype/user/user.py b/frappe/core/doctype/user/user.py index 14266e4cd8..de49b00bbd 100644 --- a/frappe/core/doctype/user/user.py +++ b/frappe/core/doctype/user/user.py @@ -325,7 +325,10 @@ class User(Document): return (self.first_name or "") + (self.first_name and " " or "") + (self.last_name or "") def password_reset_mail(self, link): - self.send_login_mail(_("Password Reset"), "password_reset", {"link": link}, now=True) + + reset_password_template = frappe.db.get_system_setting("reset_password_template") + + self.send_login_mail(_("Password Reset"), "password_reset", {"link": link}, now=True, custom_template=reset_password_template) def send_welcome_mail_to_user(self): from frappe.utils import get_url @@ -342,16 +345,19 @@ class User(Document): else: subject = _("Complete Registration") + welcome_email_template = frappe.db.get_system_setting("welcome_email_template") + self.send_login_mail( subject, "new_user", - dict( + dict( link=link, site_url=get_url(), ), + custom_template=welcome_email_template, ) - def send_login_mail(self, subject, template, add_args, now=None): + def send_login_mail(self, subject, template, add_args, now=None, custom_template=None): """send mail with login details""" from frappe.utils import get_url from frappe.utils.user import get_user_fullname @@ -374,11 +380,18 @@ class User(Document): frappe.session.user not in STANDARD_USERS and get_formatted_email(frappe.session.user) or None ) + if custom_template: + from frappe.email.doctype.email_template.email_template import get_email_template + email_template = get_email_template(custom_template, args) + subject = email_template.get("subject") + content = email_template.get("message") + frappe.sendmail( recipients=self.email, sender=sender, subject=subject, - template=template, + template=template if not custom_template else None, + content=content if custom_template else None, args=args, header=[subject, "green"], delayed=(not now) if now is not None else self.flags.delay_emails, From 81c103a7414dc48ccd3396ec5534eb0217dc242e Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Thu, 25 May 2023 14:07:19 +0530 Subject: [PATCH 058/203] refactor: remove form tour settings --- frappe/boot.py | 7 +-- frappe/desk/doctype/form_tour/form_tour.py | 62 ++++++++++--------- .../doctype/form_tour_settings/__init__.py | 0 .../form_tour_settings/form_tour_settings.js | 12 ---- .../form_tour_settings.json | 51 --------------- .../form_tour_settings/form_tour_settings.py | 32 ---------- .../test_form_tour_settings.py | 9 --- .../form_tour_settings_item/__init__.py | 0 .../form_tour_settings_item.json | 61 ------------------ .../form_tour_settings_item.py | 9 --- .../js/onboarding_tours/onboarding_tours.js | 4 +- frappe/utils/install.py | 2 - 12 files changed, 36 insertions(+), 213 deletions(-) delete mode 100644 frappe/desk/doctype/form_tour_settings/__init__.py delete mode 100644 frappe/desk/doctype/form_tour_settings/form_tour_settings.js delete mode 100644 frappe/desk/doctype/form_tour_settings/form_tour_settings.json delete mode 100644 frappe/desk/doctype/form_tour_settings/form_tour_settings.py delete mode 100644 frappe/desk/doctype/form_tour_settings/test_form_tour_settings.py delete mode 100644 frappe/desk/doctype/form_tour_settings_item/__init__.py delete mode 100644 frappe/desk/doctype/form_tour_settings_item/form_tour_settings_item.json delete mode 100644 frappe/desk/doctype/form_tour_settings_item/form_tour_settings_item.py diff --git a/frappe/boot.py b/frappe/boot.py index a54e6e2f07..37d89365c4 100644 --- a/frappe/boot.py +++ b/frappe/boot.py @@ -8,6 +8,7 @@ import frappe import frappe.defaults import frappe.desk.desk_page from frappe.core.doctype.navbar_settings.navbar_settings import get_app_logo, get_navbar_settings +from frappe.desk.doctype.form_tour.form_tour import get_onboarding_ui_tours from frappe.desk.doctype.route_history.route_history import frequently_visited_links from frappe.desk.form.load import get_meta_bundle from frappe.email.inbox import get_email_accounts @@ -68,11 +69,7 @@ def get_bootinfo(): bootinfo.home_folder = frappe.db.get_value("File", {"is_home_folder": 1}) bootinfo.navbar_settings = get_navbar_settings() bootinfo.notification_settings = get_notification_settings() - bootinfo.onboarding_tours = ( - frappe.parse_json(frappe.db.get_single_value("Form Tour Settings", "onboarding_tours") or "[]") - if frappe.get_system_settings("enable_onboarding") - else [] - ) + bootinfo.onboarding_tours = get_onboarding_ui_tours() set_time_zone(bootinfo) # ipinfo diff --git a/frappe/desk/doctype/form_tour/form_tour.py b/frappe/desk/doctype/form_tour/form_tour.py index 2271563e71..0b1a22f64c 100644 --- a/frappe/desk/doctype/form_tour/form_tour.py +++ b/frappe/desk/doctype/form_tour/form_tour.py @@ -1,6 +1,8 @@ # Copyright (c) 2021, Frappe Technologies and contributors # License: MIT. See LICENSE +import json + import frappe from frappe.model.document import Document from frappe.modules.export_file import export_to_files @@ -32,40 +34,12 @@ class FormTour(Document): def on_update(self): frappe.cache().delete_key("bootinfo") - if self.ui_tour: - form_tour_settings = frappe.get_doc("Form Tour Settings", "Form Tour Settings") - in_settings = False - child_index = 0 - for tour in form_tour_settings.form_tours: - if tour.form_tour == self.name: - in_settings = True - child_index = tour.idx - form_tour_settings.remove(tour) - if not in_settings: - child_index = len(form_tour_settings.form_tours) + 1 - child = frappe.new_doc("Form Tour Settings Item") - child.update( - { - "idx": child_index, - "form_tour": self.name, - "parent": "Form Tour Settings", - "parentfield": "form_tours", - "parenttype": "Form Tour Settings", - } - ) - child.save() - form_tour_settings.form_tours.insert(child_index, child) - form_tour_settings.save() + if frappe.conf.developer_mode and self.is_standard: export_to_files([["Form Tour", self.name]], self.module) def on_trash(self): - if self.ui_tour: - form_tour_settings = frappe.get_doc("Form Tour Settings", "Form Tour Settings") - for tour in form_tour_settings.form_tours: - if tour.form_tour == self.name: - form_tour_settings.remove(tour) - form_tour_settings.save() + frappe.cache().delete_key("bootinfo") @frappe.whitelist() @@ -76,3 +50,31 @@ def reset_tour(tour_name): onboarding_status.pop(tour_name, None) user_doc.onboarding_status = frappe.as_json(onboarding_status) user_doc.save() + + +@frappe.whitelist() +def update_user_status(value, step): + from frappe.utils.telemetry import capture + + step = frappe.parse_json(step) + tour = frappe.parse_json(value) + + capture( + frappe.scrub(f"{step.parent}_{step.title}"), + app="frappe_ui_tours", + properties={"is_completed": tour.is_completed}, + ) + frappe.db.set_value( + "User", frappe.session.user, "onboarding_status", value, update_modified=False + ) + + frappe.cache().hdel("bootinfo", frappe.session.user) + + +def get_onboarding_ui_tours(): + if not frappe.get_system_settings("enable_onboarding"): + return [] + + ui_tours = frappe.get_all("Form Tour", filters={"ui_tour": 1}, fields=["page_route", "name"]) + + return [[tour.name, json.loads(tour.page_route)] for tour in ui_tours] diff --git a/frappe/desk/doctype/form_tour_settings/__init__.py b/frappe/desk/doctype/form_tour_settings/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/frappe/desk/doctype/form_tour_settings/form_tour_settings.js b/frappe/desk/doctype/form_tour_settings/form_tour_settings.js deleted file mode 100644 index a9d7f62890..0000000000 --- a/frappe/desk/doctype/form_tour_settings/form_tour_settings.js +++ /dev/null @@ -1,12 +0,0 @@ -// Copyright (c) 2023, Frappe Technologies and contributors -// For license information, please see license.txt - -frappe.ui.form.on("Form Tour Settings", { - refresh(frm) { - frm.dashboard.add_comment( - "This page is used to set priority for the UI form tours. If there are more than 1 matching tours found for the page, the tour with the highest priority will run.", - "blue", - true - ); - }, -}); diff --git a/frappe/desk/doctype/form_tour_settings/form_tour_settings.json b/frappe/desk/doctype/form_tour_settings/form_tour_settings.json deleted file mode 100644 index 755c347264..0000000000 --- a/frappe/desk/doctype/form_tour_settings/form_tour_settings.json +++ /dev/null @@ -1,51 +0,0 @@ -{ - "actions": [], - "allow_rename": 1, - "creation": "2023-05-11 18:07:26.879273", - "default_view": "List", - "doctype": "DocType", - "editable_grid": 1, - "engine": "InnoDB", - "field_order": [ - "onboarding_tours", - "form_tours" - ], - "fields": [ - { - "fieldname": "form_tours", - "fieldtype": "Table", - "label": "Form Tours", - "options": "Form Tour Settings Item" - }, - { - "default": "\"[]\"", - "fieldname": "onboarding_tours", - "fieldtype": "Small Text", - "hidden": 1, - "label": "Onboarding Tours" - } - ], - "index_web_pages_for_search": 1, - "issingle": 1, - "links": [], - "modified": "2023-05-24 12:05:52.676242", - "modified_by": "Administrator", - "module": "Desk", - "name": "Form Tour Settings", - "owner": "Administrator", - "permissions": [ - { - "create": 1, - "delete": 1, - "email": 1, - "print": 1, - "read": 1, - "role": "System Manager", - "share": 1, - "write": 1 - } - ], - "sort_field": "modified", - "sort_order": "DESC", - "states": [] -} \ No newline at end of file diff --git a/frappe/desk/doctype/form_tour_settings/form_tour_settings.py b/frappe/desk/doctype/form_tour_settings/form_tour_settings.py deleted file mode 100644 index 5c711f207b..0000000000 --- a/frappe/desk/doctype/form_tour_settings/form_tour_settings.py +++ /dev/null @@ -1,32 +0,0 @@ -# Copyright (c) 2023, Frappe Technologies and contributors -# For license information, please see license.txt - -import json - -import frappe -from frappe.model.document import Document - - -class FormTourSettings(Document): - def before_save(self): - self.onboarding_tours = json.dumps( - [[tour.form_tour, json.loads(tour.page_route)] for tour in self.form_tours] - ) - - -@frappe.whitelist() -def update_user_status(value, step): - from frappe.utils.telemetry import capture - - step = frappe.parse_json(step) - tour = frappe.parse_json(value) - # from frappe.utils.telemetry import capture - capture( - frappe.scrub(f"{step.parent}_{step.title}"), - app="frappe_ui_tours", - properties={"is_completed": tour.is_completed}, - ) - frappe.db.set_value( - "User", frappe.session.user, "onboarding_status", value, update_modified=False - ) - frappe.cache().hdel("bootinfo", frappe.session.user) diff --git a/frappe/desk/doctype/form_tour_settings/test_form_tour_settings.py b/frappe/desk/doctype/form_tour_settings/test_form_tour_settings.py deleted file mode 100644 index 95838ecb97..0000000000 --- a/frappe/desk/doctype/form_tour_settings/test_form_tour_settings.py +++ /dev/null @@ -1,9 +0,0 @@ -# Copyright (c) 2023, Frappe Technologies and Contributors -# See license.txt - -# import frappe -from frappe.tests.utils import FrappeTestCase - - -class TestFormTourSettings(FrappeTestCase): - pass diff --git a/frappe/desk/doctype/form_tour_settings_item/__init__.py b/frappe/desk/doctype/form_tour_settings_item/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/frappe/desk/doctype/form_tour_settings_item/form_tour_settings_item.json b/frappe/desk/doctype/form_tour_settings_item/form_tour_settings_item.json deleted file mode 100644 index 01f05393a3..0000000000 --- a/frappe/desk/doctype/form_tour_settings_item/form_tour_settings_item.json +++ /dev/null @@ -1,61 +0,0 @@ -{ - "actions": [], - "allow_rename": 1, - "creation": "2023-05-11 18:10:15.194034", - "doctype": "DocType", - "editable_grid": 1, - "engine": "InnoDB", - "field_order": [ - "form_tour", - "view", - "list_view", - "page_route" - ], - "fields": [ - { - "fieldname": "form_tour", - "fieldtype": "Link", - "in_list_view": 1, - "label": "Form Tour", - "options": "Form Tour", - "read_only": 1 - }, - { - "fetch_from": "form_tour.view_name", - "fieldname": "view", - "fieldtype": "Data", - "in_list_view": 1, - "label": "View", - "read_only": 1 - }, - { - "fetch_from": "form_tour.list_name", - "fieldname": "list_view", - "fieldtype": "Data", - "in_list_view": 1, - "label": "List View", - "read_only": 1 - }, - { - "fetch_from": "form_tour.page_route", - "fieldname": "page_route", - "fieldtype": "Data", - "hidden": 1, - "in_list_view": 1, - "label": "Page Route", - "read_only": 1 - } - ], - "index_web_pages_for_search": 1, - "istable": 1, - "links": [], - "modified": "2023-05-17 22:22:58.507769", - "modified_by": "Administrator", - "module": "Desk", - "name": "Form Tour Settings Item", - "owner": "Administrator", - "permissions": [], - "sort_field": "modified", - "sort_order": "DESC", - "states": [] -} \ No newline at end of file diff --git a/frappe/desk/doctype/form_tour_settings_item/form_tour_settings_item.py b/frappe/desk/doctype/form_tour_settings_item/form_tour_settings_item.py deleted file mode 100644 index 0958b000ad..0000000000 --- a/frappe/desk/doctype/form_tour_settings_item/form_tour_settings_item.py +++ /dev/null @@ -1,9 +0,0 @@ -# Copyright (c) 2023, Frappe Technologies and contributors -# For license information, please see license.txt - -# import frappe -from frappe.model.document import Document - - -class FormTourSettingsItem(Document): - pass diff --git a/frappe/public/js/onboarding_tours/onboarding_tours.js b/frappe/public/js/onboarding_tours/onboarding_tours.js index 63ecd6d670..63215d3659 100644 --- a/frappe/public/js/onboarding_tours/onboarding_tours.js +++ b/frappe/public/js/onboarding_tours/onboarding_tours.js @@ -42,7 +42,7 @@ frappe.ui.OnboardingTour = class OnboardingTour { } frappe.call({ - method: "frappe.desk.doctype.form_tour_settings.form_tour_settings.update_user_status", + method: "frappe.desk.doctype.form_tour.form_tour.update_user_status", args: { value: JSON.stringify(frappe.boot.user.onboarding_status), step: JSON.stringify(step.options.step_info), @@ -87,7 +87,7 @@ frappe.ui.OnboardingTour = class OnboardingTour { } this.last_step_saved = step; frappe.call({ - method: "frappe.desk.doctype.form_tour_settings.form_tour_settings.update_user_status", + method: "frappe.desk.doctype.form_tour.form_tour.update_user_status", args: { value: JSON.stringify(frappe.boot.user.onboarding_status), step: JSON.stringify(step), diff --git a/frappe/utils/install.py b/frappe/utils/install.py index 50feab0475..df918c27e0 100644 --- a/frappe/utils/install.py +++ b/frappe/utils/install.py @@ -14,8 +14,6 @@ def before_install(): frappe.reload_doc("core", "doctype", "doctype_action") frappe.reload_doc("core", "doctype", "doctype_link") frappe.reload_doc("desk", "doctype", "form_tour_step") - frappe.reload_doc("desk", "doctype", "form_tour_settings") - frappe.reload_doc("desk", "doctype", "form_tour_settings_item") frappe.reload_doc("desk", "doctype", "form_tour") frappe.reload_doc("core", "doctype", "doctype") From 7c82876305e37233d50b4f8a6f3361f78d5b4c0f Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Thu, 25 May 2023 14:33:12 +0530 Subject: [PATCH 059/203] fix: patch existing users/site --- frappe/desk/doctype/form_tour/patches/__init__.py | 0 .../doctype/form_tour/patches/introduce_ui_tours.py | 13 +++++++++++++ frappe/patches.txt | 4 +++- 3 files changed, 16 insertions(+), 1 deletion(-) create mode 100644 frappe/desk/doctype/form_tour/patches/__init__.py create mode 100644 frappe/desk/doctype/form_tour/patches/introduce_ui_tours.py diff --git a/frappe/desk/doctype/form_tour/patches/__init__.py b/frappe/desk/doctype/form_tour/patches/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frappe/desk/doctype/form_tour/patches/introduce_ui_tours.py b/frappe/desk/doctype/form_tour/patches/introduce_ui_tours.py new file mode 100644 index 0000000000..2ca981dae7 --- /dev/null +++ b/frappe/desk/doctype/form_tour/patches/introduce_ui_tours.py @@ -0,0 +1,13 @@ +import json + +import frappe + + +def execute(): + """Handle introduction of UI tours""" + completed = {} + for tour in frappe.get_all("Form Tour", {"ui_tour": 1}, pluck="name"): + completed[tour] = {"is_complete": True} + + User = frappe.qb.DocType("User") + frappe.qb.update(User).set("onboarding_status", json.dumps(completed)).run() diff --git a/frappe/patches.txt b/frappe/patches.txt index fa9d884386..a4eb40a36f 100644 --- a/frappe/patches.txt +++ b/frappe/patches.txt @@ -196,6 +196,7 @@ frappe.patches.v14_0.update_webforms frappe.patches.v14_0.delete_payment_gateways frappe.patches.v15_0.remove_event_streaming frappe.patches.v15_0.copy_disable_prepared_report_to_prepared_report +execute:frappe.reload_doc("desk", "doctype", "Form Tour") [post_model_sync] execute:frappe.get_doc('Role', 'Guest').save() # remove desk access @@ -223,4 +224,5 @@ frappe.patches.v14_0.disable_email_accounts_with_oauth execute:frappe.delete_doc("Page", "translation-tool", force=1) frappe.patches.v15_0.remove_prepared_report_settings_from_system_settings frappe.patches.v14_0.remove_manage_subscriptions_from_navbar -frappe.patches.v15_0.remove_background_jobs_from_dropdown \ No newline at end of file +frappe.patches.v15_0.remove_background_jobs_from_dropdown +frappe.desk.doctype.form_tour.patches.introduce_ui_tours From 1634ee0eb2399e8845f4c72ab6a7be566ab10525 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Thu, 25 May 2023 15:49:39 +0530 Subject: [PATCH 060/203] chore: update readme remove manual install and setup production --- README.md | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 562437d5d1..d3b76648a2 100644 --- a/README.md +++ b/README.md @@ -56,10 +56,15 @@ Full-stack web application framework that uses Python and MariaDB on the server ## Installation -* [Install via Docker](https://github.com/frappe/frappe_docker) -* [Install via Frappe Bench](https://github.com/frappe/bench) -* [Offical Documentation](https://frappeframework.com/docs/user/en/installation) -* [Managed Hosting on Frappe Cloud](https://frappecloud.com/frappe/signup) +### Production +* [Managed Hosting on Frappe Cloud](https://frappecloud.com/) +* [Easy install script using Docker images](https://github.com/frappe/bench/tree/develop#easy-install-script) +* [Manual install using Docker images](https://github.com/frappe/frappe_docker) + +### Development +* [Easy install script using Docker images](https://github.com/frappe/bench/tree/develop#easy-install-script) +* [Development installlation on bare metal](https://frappeframework.com/docs/user/en/installation) + ## Contributing From 1eabd21bb8719e5f23989d8bf4865fc83cb168a7 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Thu, 25 May 2023 17:10:43 +0530 Subject: [PATCH 061/203] chore!: remove mixpanel code (#21112) --- frappe/public/js/frappe/desk.js | 13 ------------- .../includes/app_analytics/mixpanel_analytics.html | 6 ------ frappe/www/app.html | 1 - frappe/www/app.py | 1 - 4 files changed, 21 deletions(-) delete mode 100644 frappe/templates/includes/app_analytics/mixpanel_analytics.html diff --git a/frappe/public/js/frappe/desk.js b/frappe/public/js/frappe/desk.js index f92bb3a1bf..fba4678cbb 100644 --- a/frappe/public/js/frappe/desk.js +++ b/frappe/public/js/frappe/desk.js @@ -38,7 +38,6 @@ frappe.Application = class Application { this.load_user_permissions(); this.make_nav_bar(); this.set_favicon(); - this.setup_analytics(); this.set_fullwidth_if_enabled(); this.add_browser_class(); this.setup_energy_point_listeners(); @@ -512,18 +511,6 @@ frappe.Application = class Application { }); } - setup_analytics() { - if (window.mixpanel) { - window.mixpanel.identify(frappe.session.user); - window.mixpanel.people.set({ - $first_name: frappe.boot.user.first_name, - $last_name: frappe.boot.user.last_name, - $created: frappe.boot.user.creation, - $email: frappe.session.user, - }); - } - } - add_browser_class() { $("html").addClass(frappe.utils.get_browser().name.toLowerCase()); } diff --git a/frappe/templates/includes/app_analytics/mixpanel_analytics.html b/frappe/templates/includes/app_analytics/mixpanel_analytics.html deleted file mode 100644 index 286593be04..0000000000 --- a/frappe/templates/includes/app_analytics/mixpanel_analytics.html +++ /dev/null @@ -1,6 +0,0 @@ -{% if mixpanel_id %} - -{% endif %} \ No newline at end of file diff --git a/frappe/www/app.html b/frappe/www/app.html index a7468cfc30..ceceaf3219 100644 --- a/frappe/www/app.html +++ b/frappe/www/app.html @@ -52,7 +52,6 @@ {% endfor %} {% include "templates/includes/app_analytics/google_analytics.html" %} - {% include "templates/includes/app_analytics/mixpanel_analytics.html" %} {% for sound in (sounds or []) %}