diff --git a/frappe/boot.py b/frappe/boot.py index ebfaa83fea..dc8c99199e 100644 --- a/frappe/boot.py +++ b/frappe/boot.py @@ -528,18 +528,27 @@ def get_sentry_dsn(): def get_sidebar_items(): + from frappe.utils.install import auto_generate_sidebar_from_module + sidebars = frappe.get_all( "Workspace Sidebar", fields=["name", "header_icon"], filters={"name": ["not like", "%My Workspaces%"]} ) + module_sidebars = auto_generate_sidebar_from_module() + sidebars.extend(module_sidebars) add_user_specific_sidebar(sidebars) sidebar_items = {} for s in sidebars: - w = frappe.get_doc("Workspace Sidebar", s["name"]) - sidebar_items[s["name"].lower()] = { - "label": s["name"], + sidebar_title = s.get("name") + if sidebar_title: + w = frappe.get_doc("Workspace Sidebar", sidebar_title) + else: + sidebar_title = s.title + w = s + sidebar_items[sidebar_title.lower()] = { + "label": sidebar_title, "items": [], - "header_icon": s["header_icon"], + "header_icon": s.get("header_icon"), "module": w.module, "app": w.app, } @@ -568,13 +577,12 @@ def get_sidebar_items(): "report_type": report_type, "ref_doctype": ref_doctype, } - if ( - "My Workspaces" in s["name"] + "My Workspaces" in sidebar_title or si.type == "Section Break" or w.is_item_allowed(si.link_to, si.link_type) ): - sidebar_items[s["name"].lower()]["items"].append(workspace_sidebar) + sidebar_items[sidebar_title.lower()]["items"].append(workspace_sidebar) old_name = f"my workspaces-{frappe.session.user.lower()}" if old_name in sidebar_items.keys(): diff --git a/frappe/public/js/frappe/ui/sidebar/sidebar.js b/frappe/public/js/frappe/ui/sidebar/sidebar.js index a101dfddf4..e4e3810216 100644 --- a/frappe/public/js/frappe/ui/sidebar/sidebar.js +++ b/frappe/public/js/frappe/ui/sidebar/sidebar.js @@ -161,11 +161,11 @@ frappe.ui.Sidebar = class Sidebar { let match = false; const that = this; $(".item-anchor").each(function () { - let href = $(this).attr("href")?.split("?")[0]; + let href = decodeURIComponent($(this).attr("href")?.split("?")[0]); const path = decodeURIComponent(window.location.pathname); // Match only if path equals href or starts with it followed by "/" or end of string - const isActive = new RegExp(`^${href}(?:/|$)`).test(path); + const isActive = href === path; if (href && isActive) { match = true; if (that.active_item) that.active_item.removeClass("active-sidebar"); @@ -445,7 +445,7 @@ frappe.ui.Sidebar = class Sidebar { if (route.length == 2) { workspace_title = this.get_correct_workspace_sidebars(route[1]); } else { - workspace_title = this.get_correct_workspace_sidebars(route); + workspace_title = this.get_correct_workspace_sidebars(route[0]); } let module_name = workspace_title[0]; if (module_name) { diff --git a/frappe/public/js/frappe/ui/sidebar/sidebar_item.js b/frappe/public/js/frappe/ui/sidebar/sidebar_item.js index bb88ec5f4a..67d76c4689 100644 --- a/frappe/public/js/frappe/ui/sidebar/sidebar_item.js +++ b/frappe/public/js/frappe/ui/sidebar/sidebar_item.js @@ -54,14 +54,16 @@ frappe.ui.sidebar_item.TypeLink = class SidebarItem { }); } } - return path; + if (path) { + return encodeURI(path); + } } prepare() {} make() { this.path = this.get_path(); this.set_suffix(); if (!this.item.icon && !(this.item.child && this.item.parent.indent)) { - this.item.icon = "list-alt"; + this.item.icon = "list"; } this.wrapper = $( frappe.render_template("sidebar_item", { diff --git a/frappe/utils/install.py b/frappe/utils/install.py index 60a393882e..2f885370c8 100644 --- a/frappe/utils/install.py +++ b/frappe/utils/install.py @@ -5,6 +5,7 @@ import getpass import frappe from frappe.geo.doctype.country.country import import_country_and_currency from frappe.utils import cint +from frappe.utils.caching import site_cache from frappe.utils.password import update_password @@ -221,3 +222,135 @@ def delete_desktop_icon_and_sidebar(app_name, dry_run=False): if dry_run: # Delete icons and sidebars frappe.db.commit() # nosemgrep + + +@site_cache() +def auto_generate_sidebar_from_module(): + """Auto generate sidebar from module""" + sidebars = [] + for module in frappe.get_all("Module Def", pluck="name"): + if not ( + frappe.db.exists("Workspace Sidebar", {"module": module}) + or frappe.db.exists("Workspace Sidebar", {"name": module}) + ): + module_info = get_module_info(module) + sidebar_items = create_sidebar_items(module_info) + sidebar = frappe.new_doc("Workspace Sidebar") + sidebar.title = module + sidebar.items = sidebar_items + sidebar.module = module + sidebar.header_icon = "hammer" + sidebar.app = frappe.local.module_app.get(frappe.scrub(module), None) + sidebars.append(sidebar) + return sidebars + + +def get_module_info(module_name): + entities = ["Workspace", "Dashboard", "DocType", "Report", "Page"] + module_info = {} + + for entity in entities: + module_info[entity] = {} + filters = [{"module": module_name}] + pluck = "name" + fieldnames = ["name"] + if entity.lower() == "doctype": + filters.append({"istable": 0}) + if entity.lower() == "page": + fieldnames.append("title") + pluck = None + module_info[entity] = frappe.get_all( + entity, filters=filters, fields=fieldnames, pluck=pluck, order_by="creation asc" + ) + + # if module info has no workspaces, then move doctypes to the front + if not module_info.get("Workspace"): + module_info = { + "DocType": module_info.get("DocType"), + "Workspace": module_info.get("Workspace"), + "Report": module_info.get("Report"), + "Dashboard": module_info.get("Dashboard"), + "Page": module_info.get("Page"), + } + top_doctypes = choose_top_doctypes(module_info.get("DocType")) + if top_doctypes: + module_info["DocType"] = choose_top_doctypes(module_info.get("DocType")) + return module_info + + +def choose_top_doctypes(doctype_names): + doctype_limit = 3 + if len(doctype_names) > doctype_limit: + try: + doctype_count_map = {} + for doctype in doctype_names: + doctype_count_map[doctype] = frappe.db.count(doctype) + top_doctypes = [ + name + for name, count in sorted(doctype_count_map.items(), key=lambda x: x[1], reverse=True)[ + :doctype_limit + ] + ] + return top_doctypes + except frappe.db.ProgrammingError: + # catches table not found errors + return None + + +def create_sidebar_items(module_info): + sidebar_items = [] + idx = 1 + + section_entities = {"report": "Reports", "dashboard": "Dashboards", "page": "Pages"} + + for entity, items in module_info.items(): + section_break_added = False + entity_lower = entity.lower() + + if entity_lower in section_entities: + if entity_lower == "report": + section_break = add_section_breaks("Reports", idx) + elif entity_lower in ("dashboard", "page") and len(items) > 1: + section_break = add_section_breaks(section_entities[entity_lower], idx) + section_break_added = True + sidebar_items.append(section_break) + idx += 1 + + for item in items: + print(entity, item) + item_info = {"label": item, "type": "Link", "link_type": entity, "link_to": item, "idx": idx} + + if entity_lower == "report": + item_info["child"] = 1 + item_info["icon"] = "table" + + if entity_lower == "page": + item_info["label"] = item.get("title") + item_info["link_to"] = item.get("name") + + if entity_lower == "workspace": + item_info["icon"] = "home" + item_info["icon"] = "wallpaper" + + if entity_lower == "page": + item_info["icon"] = "panel-top" + + if entity_lower == "doctype" and "settings" in item.lower(): + item_info["icon"] = "settings" + + if section_break_added: + item_info["child"] = 1 + + sidebar_item = frappe.new_doc("Workspace Sidebar Item") + sidebar_item.update(item_info) + sidebar_items.append(sidebar_item) + + idx += 1 + + return sidebar_items + + +def add_section_breaks(label, idx): + section_break = frappe.new_doc("Workspace Sidebar Item") + section_break.update({"label": label, "type": "Section Break", "idx": idx}) + return section_break