diff --git a/frappe/desk/desktop.py b/frappe/desk/desktop.py index e4fdbe98e0..0b01a252fa 100644 --- a/frappe/desk/desktop.py +++ b/frappe/desk/desktop.py @@ -51,9 +51,13 @@ class Workspace: self.allowed_pages = get_allowed_pages(cache=True) self.allowed_reports = get_allowed_reports(cache=True) - self.onboarding_list = self.get_onboarding_list() if not minimal: + if self.doc.content: + self.onboarding_list = [ + x["data"]["onboarding_name"] for x in loads(self.doc.content) if x["type"] == "onboarding" + ] + self.table_counts = get_table_with_counts() self.restricted_doctypes = ( frappe.cache.get_value("domain_restricted_doctypes") or build_domain_restricted_doctype_cache() @@ -62,14 +66,6 @@ class Workspace: frappe.cache.get_value("domain_restricted_pages") or build_domain_restricted_page_cache() ) - def get_onboarding_list(self): - return frappe.get_all( - "Module Onboarding", - filters={"is_complete": 0, "module": self.page_name}, - pluck="name", - order_by="creation", - ) - def is_permitted(self): """Return true if `Has Role` is not set or the user is allowed.""" from frappe.utils import has_common @@ -160,6 +156,7 @@ class Workspace: self.cards = {"items": self.get_links()} self.charts = {"items": self.get_charts()} self.shortcuts = {"items": self.get_shortcuts()} + self.onboardings = {"items": []} self.quick_lists = {"items": self.get_quick_lists()} self.number_cards = {"items": self.get_number_cards()} self.custom_blocks = {"items": self.get_custom_blocks()} @@ -370,6 +367,7 @@ def get_desktop_page(page: str): "charts": workspace.charts, "shortcuts": workspace.shortcuts, "cards": workspace.cards, + "onboardings": workspace.onboardings, "quick_lists": workspace.quick_lists, "number_cards": workspace.number_cards, "custom_blocks": workspace.custom_blocks, @@ -650,7 +648,7 @@ def prepare_widget(config, doctype, parentfield): @frappe.whitelist() -def update_onboarding_step(name: str, field: str, value: any): +def update_onboarding_step(name: str | int, field: str, value: int | str): """Update status of onboaridng step Args: @@ -687,6 +685,12 @@ def get_onboarding_data(module: str): if onboarding_doc.is_complete: return [] + # Check if user is allowed + allowed_roles = set(onboarding_doc.get_allowed_roles()) + user_roles = set(frappe.get_roles()) + if not allowed_roles & user_roles: + return None + item = { "label": _(module), "title": _(onboarding_doc.title), diff --git a/frappe/desk/doctype/onboarding_step/onboarding_step.json b/frappe/desk/doctype/onboarding_step/onboarding_step.json index 45efd216ff..7037c334bf 100644 --- a/frappe/desk/doctype/onboarding_step/onboarding_step.json +++ b/frappe/desk/doctype/onboarding_step/onboarding_step.json @@ -32,7 +32,9 @@ "validate_action", "field", "value_to_validate", - "video_url" + "video_url", + "section_break_ajog", + "route_options" ], "fields": [ { @@ -61,7 +63,7 @@ "fieldname": "action", "fieldtype": "Select", "label": "Action", - "options": "Create Entry\nUpdate Settings\nShow Form Tour\nView Report\nGo to Page\nWatch Video", + "options": "Create Entry\nUpdate Settings\nShow Form Tour\nView Report\nGo to Page\nView Docs", "reqd": 1 }, { @@ -145,7 +147,7 @@ "label": "Is Single" }, { - "depends_on": "eval:doc.action == \"Go to Page\"", + "depends_on": "eval:doc.action == \"Go to Page\" || doc.action === \"View Docs\"", "description": "Example: #Tree/Account", "fieldname": "path", "fieldtype": "Data", @@ -214,10 +216,19 @@ "fieldtype": "Link", "label": "Form Tour", "options": "Form Tour" + }, + { + "fieldname": "section_break_ajog", + "fieldtype": "Section Break" + }, + { + "fieldname": "route_options", + "fieldtype": "Code", + "label": "Route Options" } ], "links": [], - "modified": "2026-02-21 08:37:30.532549", + "modified": "2026-02-23 21:03:51.131292", "modified_by": "Administrator", "module": "Desk", "name": "Onboarding Step", diff --git a/frappe/desk/doctype/onboarding_step/onboarding_step.py b/frappe/desk/doctype/onboarding_step/onboarding_step.py index e12e847244..6a6a8b9ea1 100644 --- a/frappe/desk/doctype/onboarding_step/onboarding_step.py +++ b/frappe/desk/doctype/onboarding_step/onboarding_step.py @@ -18,7 +18,7 @@ class OnboardingStep(Document): from frappe.types import DF action: DF.Literal[ - "Create Entry", "Update Settings", "Show Form Tour", "View Report", "Go to Page", "Watch Video" + "Create Entry", "Update Settings", "Show Form Tour", "View Report", "Go to Page", "View Docs" ] action_label: DF.Data | None callback_message: DF.SmallText | None @@ -36,6 +36,7 @@ class OnboardingStep(Document): report_description: DF.Data | None report_reference_doctype: DF.Data | None report_type: DF.Data | None + route_options: DF.Code | None show_form_tour: DF.Check show_full_form: DF.Check title: DF.Data diff --git a/frappe/public/js/desk.bundle.js b/frappe/public/js/desk.bundle.js index 945a4e5ee2..b5dad9619f 100644 --- a/frappe/public/js/desk.bundle.js +++ b/frappe/public/js/desk.bundle.js @@ -109,8 +109,8 @@ import "./frappe/utils/dashboard_utils.js"; import "./frappe/ui/chart.js"; import "./frappe/ui/datatable.js"; import "./frappe/ui/driver.js"; -import "./frappe/ui/user_onboarding/user_onboarding.bundle.js"; import "./frappe/scanner"; import "./frappe/ui/address_autocomplete/autocomplete_dialog.js"; import "./frappe/ui/desktop_icon.html"; +import "./frappe/ui/user_onboarding/user_onboarding.bundle.js"; diff --git a/frappe/public/js/frappe/ui/sidebar/sidebar.html b/frappe/public/js/frappe/ui/sidebar/sidebar.html index 7e0d700800..ef5e6d630a 100644 --- a/frappe/public/js/frappe/ui/sidebar/sidebar.html +++ b/frappe/public/js/frappe/ui/sidebar/sidebar.html @@ -42,8 +42,8 @@

- {%= frappe.utils.icon("getting-started" , "sm", "", "", "text-ink-gray-7 current-color", true)%} - {%= __("Continue Onboarding") %} + {%= frappe.utils.icon("user-check" , "sm", "", "", "text-ink-gray-7 current-color", true)%} + {%= __("Getting started") %}

@@ -90,6 +90,6 @@
-
+
diff --git a/frappe/public/js/frappe/ui/sidebar/sidebar.js b/frappe/public/js/frappe/ui/sidebar/sidebar.js index 281baa107f..84f4665ffd 100644 --- a/frappe/public/js/frappe/ui/sidebar/sidebar.js +++ b/frappe/public/js/frappe/ui/sidebar/sidebar.js @@ -78,26 +78,49 @@ frappe.ui.Sidebar = class Sidebar { } } - setup_onboarding() { - let me = this; - this.$onboarding = this.wrapper.find(".user_onboarding"); + remove_onboarding_wrapper() { this.$onboarding.empty(); this.wrapper.find(".onboarding-sidebar").removeClass("hidden"); + } + + setup_onboarding() { + let me = this; + this.$onboarding = this.wrapper.find(".user-onboarding"); + + if (!this.sidebar_data || !this.sidebar_data.module_onboarding) { + this.remove_onboarding_wrapper(); + return; + } + + let module_name = this.sidebar_data.module_onboarding; + + if (this?.onboarding_widget[module_name]) { + return; + } + + this.remove_onboarding_wrapper(); + if (module_name) { + if ( + this?.onboarding_widget[module_name] && + this.onboarding_widget[module_name].hide_panel + ) { + return; + } - if (this.sidebar_data && this.sidebar_data.module_onboarding) { return frappe .call({ method: "frappe.desk.desktop.get_onboarding_data", args: { // send sorted min requirements to increase chance of cache hit - module: this.sidebar_data.module_onboarding, + module: module_name, }, type: "GET", }) .then((data) => { if (data.message?.length > 0) { let onboarding_data = data.message[0]; - me.onboarding_widget = new frappe.ui.UserOnboarding({ + me.onboarding_widget = {}; + me.onboarding_widget[module_name] = new frappe.ui.UserOnboarding({ title: onboarding_data.title, steps: onboarding_data.items, wrapper: me.$onboarding, @@ -133,6 +156,10 @@ frappe.ui.Sidebar = class Sidebar { this.workspace_sidebar_items = updated_items; } setup(workspace_title) { + if (!this.onboarding_widget) { + this.onboarding_widget = {}; + } + $(document).trigger("sidebar_setup", { sidebar: this }); this.sidebar_title = workspace_title; this.check_for_private_workspace(workspace_title); @@ -146,6 +173,10 @@ frappe.ui.Sidebar = class Sidebar { this.setup_onboarding(); this.wrapper.find(".onboarding-sidebar").click(() => { + if (this.sidebar_data?.module_onboarding) { + delete this.onboarding_widget[this.sidebar_data.module_onboarding]; + } + this.setup_onboarding(); }); } diff --git a/frappe/public/js/frappe/ui/user_onboarding/OnboardingPanel.vue b/frappe/public/js/frappe/ui/user_onboarding/OnboardingPanel.vue index 923a0d7f13..68150e9744 100644 --- a/frappe/public/js/frappe/ui/user_onboarding/OnboardingPanel.vue +++ b/frappe/public/js/frappe/ui/user_onboarding/OnboardingPanel.vue @@ -45,6 +45,8 @@ const visible = computed({ set: (val) => emit("update:modelValue", val), }); +let skippAll = false; + const completedCount = computed( () => props.steps.filter((step) => step.is_complete || step.is_skipped).length ); @@ -68,10 +70,27 @@ function skipAll(skips) { markSkip(step); } }); + + skippAll = true; +} + +function resetAll(skips) { + skips.forEach((step) => { + if (!step.is_complete && step.is_skipped) { + markReset(step); + } + }); + + skippAll = false; } function handleAction(step) { if (step.is_complete) return; + if (step.is_skipped) return; + + if (step.route_options && typeof step.route_options === "string") { + frappe.route_options = JSON.parse(step.route_options); + } const actions = { "Create Entry": createEntry, @@ -79,6 +98,7 @@ function handleAction(step) { "Update Settings": updateSettings, "View Report": openReport, "Go to Page": goToPage, + "View Docs": viewDocs, }; if (step.action && actions[step.action]) { @@ -88,13 +108,22 @@ function handleAction(step) { } } +function viewDocs(step) { + window.open(step.path, "_blank"); + markComplete(step); +} + function goToPage(step) { + toggleCollapse(); + frappe.set_route(step.path).then(() => { markComplete(step); }); } function openReport(step) { + toggleCollapse(); + const route = frappe.utils.generate_route({ name: step.reference_report, type: "report", @@ -162,7 +191,6 @@ async function createEntry(step) { }; frappe.route_hooks.after_save = callback; - if (step.show_full_form) { frappe.set_route("Form", step.reference_document, "new"); } else { @@ -204,31 +232,42 @@ function markReset(step) {