diff --git a/frappe/desk/desktop.py b/frappe/desk/desktop.py index 14b735db10..e2bc851aad 100644 --- a/frappe/desk/desktop.py +++ b/frappe/desk/desktop.py @@ -51,8 +51,9 @@ class Workspace: self.allowed_reports = get_allowed_reports(cache=True) if not minimal: - self.onboarding_doc = self.get_onboarding_doc() - self.onboarding = None + if self.doc.content: + self.onboarding_list = [x['data']['onboarding_name'] for x in loads(self.doc.content) if x['type'] == 'onboarding'] + self.onboardings = [] self.table_counts = get_table_with_counts() self.restricted_doctypes = frappe.cache().get_value("domain_restricted_doctypes") or build_domain_restriced_doctype_cache() @@ -125,18 +126,18 @@ class Workspace: return self.user.allow_modules - def get_onboarding_doc(self): + def get_onboarding_doc(self, onboarding): # Check if onboarding is enabled if not frappe.get_system_settings("enable_onboarding"): return None - if not self.doc.onboarding: + if not self.onboarding_list: return None - if frappe.db.get_value("Module Onboarding", self.doc.onboarding, "is_complete"): + if frappe.db.get_value("Module Onboarding", onboarding, "is_complete"): return None - doc = frappe.get_doc("Module Onboarding", self.doc.onboarding) + doc = frappe.get_doc("Module Onboarding", onboarding) # Check if user is allowed allowed_roles = set(doc.get_allowed_roles()) @@ -200,14 +201,9 @@ class Workspace: 'items': self.get_shortcuts() } - if self.onboarding_doc: - self.onboarding = { - 'label': _(self.onboarding_doc.title), - 'subtitle': _(self.onboarding_doc.subtitle), - 'success': _(self.onboarding_doc.success_message), - 'docs_url': self.onboarding_doc.documentation_url, - 'items': self.get_onboarding_steps() - } + self.onboardings = { + 'items': self.get_onboardings() + } def _doctype_contains_a_record(self, name): exists = self.table_counts.get(name, False) @@ -336,9 +332,26 @@ class Workspace: return items @handle_not_exist - def get_onboarding_steps(self): + def get_onboardings(self): + if self.onboarding_list: + for onboarding in self.onboarding_list: + onboarding_doc = self.get_onboarding_doc(onboarding) + if onboarding_doc: + item = { + 'label': _(onboarding), + 'title': _(onboarding_doc.title), + 'subtitle': _(onboarding_doc.subtitle), + 'success': _(onboarding_doc.success_message), + 'docs_url': onboarding_doc.documentation_url, + 'items': self.get_onboarding_steps(onboarding_doc) + } + self.onboardings.append(item) + return self.onboardings + + @handle_not_exist + def get_onboarding_steps(self, onboarding_doc): steps = [] - for doc in self.onboarding_doc.get_steps(): + for doc in onboarding_doc.get_steps(): step = doc.as_dict().copy() step.label = _(doc.title) if step.action == "Create Entry": @@ -367,7 +380,7 @@ def get_desktop_page(page): 'charts': wspace.charts, 'shortcuts': wspace.shortcuts, 'cards': wspace.cards, - 'onboarding': wspace.onboarding, + 'onboardings': wspace.onboardings, 'allow_customization': not wspace.doc.disable_user_customization } except DoesNotExistError: diff --git a/frappe/desk/doctype/onboarding_step/onboarding_step.py b/frappe/desk/doctype/onboarding_step/onboarding_step.py index 10bd8926ce..2336ff52f8 100644 --- a/frappe/desk/doctype/onboarding_step/onboarding_step.py +++ b/frappe/desk/doctype/onboarding_step/onboarding_step.py @@ -2,10 +2,26 @@ # Copyright (c) 2020, Frappe Technologies and contributors # For license information, please see license.txt -# import frappe +import frappe +from frappe import _ +import json from frappe.model.document import Document class OnboardingStep(Document): def before_export(self, doc): doc.is_complete = 0 doc.is_skipped = 0 + + +@frappe.whitelist() +def get_onboarding_steps(ob_steps): + steps = [] + for s in json.loads(ob_steps): + doc = frappe.get_doc('Onboarding Step', s.get('step')) + step = doc.as_dict().copy() + step.label = _(doc.title) + if step.action == "Create Entry": + step.is_submittable = frappe.db.get_value("DocType", step.reference_document, 'is_submittable', cache=True) + steps.append(step) + + return steps diff --git a/frappe/patches/v13_0/update_workspace2.py b/frappe/patches/v13_0/update_workspace2.py index 53abe2994b..f5f54fc7ee 100644 --- a/frappe/patches/v13_0/update_workspace2.py +++ b/frappe/patches/v13_0/update_workspace2.py @@ -13,6 +13,8 @@ def execute(): def create_content(doc): content = [] + if doc.onboarding: + content.append({"type":"onboarding","data":{"onboarding_name":doc.onboarding,"col":12,"pt":0,"pr":0,"pb":0,"pl":0}}) if doc.charts: invalid_links = [] for c in doc.charts: @@ -55,7 +57,6 @@ def update_wspace(doc, seq, content): doc.title = doc.extends doc.extends = '' doc.category = '' - doc.restrict_to_domain = '' doc.onboarding = '' doc.extends_another_page = 0 doc.is_default = 0 diff --git a/frappe/public/js/frappe/views/workspace/blocks/index.js b/frappe/public/js/frappe/views/workspace/blocks/index.js index d8b303133f..00a9b8c83a 100644 --- a/frappe/public/js/frappe/views/workspace/blocks/index.js +++ b/frappe/public/js/frappe/views/workspace/blocks/index.js @@ -5,6 +5,7 @@ import Card from "./card"; import Chart from "./chart"; import Shortcut from "./shortcut"; import Spacer from "./spacer"; +import Onboarding from "./onboarding"; // import tunes import SpacingTune from "./spacing_tune"; @@ -18,6 +19,7 @@ frappe.wspace_block.blocks = { chart: Chart, shortcut: Shortcut, spacer: Spacer, + onboarding: Onboarding, }; frappe.wspace_block.tunes = { diff --git a/frappe/public/js/frappe/views/workspace/blocks/onboarding.js b/frappe/public/js/frappe/views/workspace/blocks/onboarding.js new file mode 100644 index 0000000000..8c8892c903 --- /dev/null +++ b/frappe/public/js/frappe/views/workspace/blocks/onboarding.js @@ -0,0 +1,128 @@ +import get_dialog_constructor from "../../../widgets/widget_dialog.js"; +import Block from "./block.js"; +export default class Onboarding extends Block { + static get toolbox() { + return { + title: 'Onboarding', + icon: '' + }; + } + + static get isReadOnlySupported() { + return true; + } + + constructor({ data, api, config, readOnly, block }) { + super({ data, api, config, readOnly, block }); + this.col = this.data.col ? this.data.col : "12"; + this.pt = this.data.pt ? this.data.pt : "0"; + this.pr = this.data.pr ? this.data.pr : "0"; + this.pb = this.data.pb ? this.data.pb : "0"; + this.pl = this.data.pl ? this.data.pl : "0"; + this.allow_customization = !this.readOnly; + this.options = { + allow_sorting: this.allow_customization, + allow_create: this.allow_customization, + allow_delete: this.allow_customization, + allow_hiding: false, + allow_edit: true + }; + } + + new(block, widget_type = block) { + const dialog_class = get_dialog_constructor(widget_type); + let block_name = block+'_name'; + this.dialog = new dialog_class({ + label: this.label, + type: widget_type, + primary_action: (widget) => { + widget.in_customize_mode = 1; + this.block_widget = frappe.widget.make_widget({ + ...widget, + widget_type: widget_type, + container: this.wrapper, + options: { + ...this.options, + on_delete: () => this.api.blocks.delete(), + on_edit: () => this.on_edit(this.block_widget) + }, + new: true + }); + this.block_widget.customize(this.options); + this.wrapper.setAttribute(block_name, this.block_widget.label || this.block_widget.onboarding_name); + this.new_block_widget = this.block_widget.get_config(); + this.add_tune_button(); + }, + }); + + if (!this.readOnly && this.data && !this.data[block_name]) { + this.dialog.make(); + } + } + + make(block, block_name) { + let block_data = this.config.page_data['onboardings'].items.find(obj => { + return obj.label == block_name; + }); + if (!block_data) return false; + this.wrapper.innerHTML = ''; + block_data.in_customize_mode = !this.readOnly; + this.block_widget = frappe.widget.make_widget({ + container: this.wrapper, + widget_type: 'onboarding', + in_customize_mode: block_data.in_customize_mode, + options: { + ...this.options, + on_delete: () => this.api.blocks.delete(), + on_edit: () => this.on_edit(this.block_widget) + }, + label: block_data.label, + title: block_data.title || __("Let's Get Started"), + subtitle: block_data.subtitle, + steps: block_data.items, + success: block_data.success, + docs_url: block_data.docs_url, + user_can_dismiss: block_data.user_can_dismiss, + }); + this.wrapper.setAttribute(block+'_name', block_name); + if (!this.readOnly) { + this.block_widget.customize(this.options); + } + return true; + } + + render() { + this.wrapper = document.createElement('div'); + this.new('onboarding'); + + if (this.data && this.data.onboarding_name) { + let has_data = this.make('onboarding', this.data.onboarding_name); + if (!has_data) return; + } + + if (!this.readOnly) { + this.add_tune_button(); + } + return this.wrapper; + } + + validate(savedData) { + if (!savedData.onboarding_name) { + return false; + } + + return true; + } + + save(blockContent) { + return { + onboarding_name: blockContent.getAttribute('onboarding_name'), + col: this.get_col(), + pt: this.get_padding("t"), + pr: this.get_padding("r"), + pb: this.get_padding("b"), + pl: this.get_padding("l"), + new: this.new_block_widget + }; + } +} \ No newline at end of file diff --git a/frappe/public/js/frappe/views/workspace/workspace.js b/frappe/public/js/frappe/views/workspace/workspace.js index 512a56c1d1..14540e5111 100644 --- a/frappe/public/js/frappe/views/workspace/workspace.js +++ b/frappe/public/js/frappe/views/workspace/workspace.js @@ -61,6 +61,12 @@ frappe.views.Workspace = class Workspace { page_data: this.page_data || [] } }, + onboarding: { + class: frappe.wspace_block.blocks['onboarding'], + config: { + page_data: this.page_data || [] + } + }, spacer: frappe.wspace_block.blocks['spacer'], spacingTune: frappe.wspace_block.tunes['spacing_tune'], }; @@ -323,6 +329,7 @@ frappe.views.Workspace = class Workspace { this.editor.configuration.tools.chart.config.page_data = this.page_data; this.editor.configuration.tools.shortcut.config.page_data = this.page_data; this.editor.configuration.tools.card.config.page_data = this.page_data; + this.editor.configuration.tools.onboarding.config.page_data = this.page_data; this.editor.render({ blocks: this.content || [] }); }); } else { diff --git a/frappe/public/js/frappe/widgets/base_widget.js b/frappe/public/js/frappe/widgets/base_widget.js index 90a2c1e641..bfb0371bd3 100644 --- a/frappe/public/js/frappe/widgets/base_widget.js +++ b/frappe/public/js/frappe/widgets/base_widget.js @@ -127,7 +127,7 @@ export default class Widget { } set_title(max_chars) { - let base = this.label || this.name; + let base = this.title || this.label || this.name; let title = max_chars ? frappe.ellipsis(base, max_chars) : base; if (this.icon) { @@ -136,7 +136,7 @@ export default class Widget { } else { this.title_field[0].innerHTML = title; if (max_chars) { - this.title_field[0].setAttribute('title', this.label); + this.title_field[0].setAttribute('title', this.title || this.label); } } this.subtitle && this.subtitle_field.html(this.subtitle); @@ -169,7 +169,7 @@ export default class Widget { primary_action: (data) => { Object.assign(this, data); data.name = this.name; - + this.new = true; this.refresh(); this.options.on_edit && this.options.on_edit(data); }, diff --git a/frappe/public/js/frappe/widgets/onboarding_widget.js b/frappe/public/js/frappe/widgets/onboarding_widget.js index b487c0134f..259bf9bc69 100644 --- a/frappe/public/js/frappe/widgets/onboarding_widget.js +++ b/frappe/public/js/frappe/widgets/onboarding_widget.js @@ -3,7 +3,23 @@ import Widget from "./base_widget.js"; frappe.provide("frappe.utils"); export default class OnboardingWidget extends Widget { + + async refresh() { + this.new && await this.get_onboarding_data(); + this.set_title(); + this.set_actions(); + this.set_body(); + this.setup_events(); + } + + get_config() { + return { + label: this.onboarding_name + }; + } + make_body() { + this.body.empty(); this.steps_wrapper = $(`
`).appendTo(this.body); this.step_preview = $(`
@@ -477,11 +493,13 @@ export default class OnboardingWidget extends Widget { } is_dismissed() { + if (this.in_customize_mode) return false; + let dismissed = JSON.parse( localStorage.getItem("dismissed-onboarding") || "{}" ); - if (Object.keys(dismissed).includes(this.label)) { - let last_hidden = new Date(dismissed[this.label]); + if (Object.keys(dismissed).includes(this.title)) { + let last_hidden = new Date(dismissed[this.title]); let today = new Date(); let diff = frappe.datetime.get_hour_diff(today, last_hidden); return diff < 24; @@ -490,6 +508,8 @@ export default class OnboardingWidget extends Widget { } set_actions() { + if (this.in_customize_mode) return; + this.action_area.empty(); const dismiss = $( `
${__('Dismiss', null, 'Stop showing the onboarding widget.')}
` @@ -498,7 +518,7 @@ export default class OnboardingWidget extends Widget { let dismissed = JSON.parse( localStorage.getItem("dismissed-onboarding") || "{}" ); - dismissed[this.label] = frappe.datetime.now_datetime(); + dismissed[this.title] = frappe.datetime.now_datetime(); localStorage.setItem( "dismissed-onboarding", @@ -508,4 +528,27 @@ export default class OnboardingWidget extends Widget { }); dismiss.appendTo(this.action_area); } + + get_onboarding_data() { + return frappe.model + .with_doc("Module Onboarding", this.onboarding_name) + .then(onboarding_doc => { + if (onboarding_doc) { + this.onboarding_doc = onboarding_doc; + this.label = onboarding_doc.label; + this.title = onboarding_doc.title || __("Let's Get Started"); + this.subtitle = onboarding_doc.subtitle; + this.success = onboarding_doc.success; + this.docs_url = onboarding_doc.docs_url; + this.user_can_dismiss = onboarding_doc.user_can_dismiss; + const method = + "frappe.desk.doctype.onboarding_step.onboarding_step.get_onboarding_steps"; + return frappe + .xcall(method, { ob_steps: onboarding_doc.steps }) + .then(steps => { + this.steps = steps; + }); + } + }); + } } diff --git a/frappe/public/js/frappe/widgets/widget_dialog.js b/frappe/public/js/frappe/widgets/widget_dialog.js index a552efd216..9262627f02 100644 --- a/frappe/public/js/frappe/widgets/widget_dialog.js +++ b/frappe/public/js/frappe/widgets/widget_dialog.js @@ -124,6 +124,24 @@ class ChartDialog extends WidgetDialog { } } +class OnboardingDialog extends WidgetDialog { + constructor(opts) { + super(opts); + } + + get_fields() { + return [ + { + fieldtype: "Link", + fieldname: "onboarding_name", + label: "Onboarding Name", + options: "Module Onboarding", + reqd: 1, + } + ]; + } +} + class CardDialog extends WidgetDialog { constructor(opts) { super(opts); @@ -531,6 +549,7 @@ export default function get_dialog_constructor(type) { shortcut: ShortcutDialog, number_card: NumberCardDialog, links: CardDialog, + onboarding: OnboardingDialog }; return widget_map[type] || WidgetDialog;