From 8dbc2832c517063a2e2bbe65b64fb77ec6ecfb69 Mon Sep 17 00:00:00 2001 From: Rushabh Mehta Date: Mon, 2 Sep 2024 21:55:13 +0530 Subject: [PATCH] feat: App switcher --- .github/frappe-framework-logo.svg | 7 +- frappe/boot.py | 26 ++++ .../workspace_settings/workspace_settings.js | 5 +- frappe/hooks.py | 3 +- .../public/images/frappe-framework-logo.svg | 7 +- frappe/public/js/frappe/ui/sidebar.js | 141 +++++++++++++++--- frappe/public/js/frappe/ui/toolbar/toolbar.js | 3 - frappe/public/scss/desk/sidebar.scss | 21 +++ 8 files changed, 176 insertions(+), 37 deletions(-) diff --git a/.github/frappe-framework-logo.svg b/.github/frappe-framework-logo.svg index 12dc55b736..b708320598 100644 --- a/.github/frappe-framework-logo.svg +++ b/.github/frappe-framework-logo.svg @@ -1,5 +1,4 @@ - - - - + + + diff --git a/frappe/boot.py b/frappe/boot.py index 240924291b..b6339a4af8 100644 --- a/frappe/boot.py +++ b/frappe/boot.py @@ -146,6 +146,32 @@ def load_desktop_data(bootinfo): bootinfo.sidebar_pages = get_workspace_sidebar_items() bootinfo.module_wise_workspaces = get_controller("Workspace").get_module_wise_workspaces() bootinfo.dashboards = frappe.get_all("Dashboard") + bootinfo.app_data = [] + + Workspace = frappe.qb.DocType("Workspace") + Module = frappe.qb.DocType("Module Def") + + for app_name in frappe.get_installed_apps(): + bootinfo.app_data.append( + dict( + app_name=app_name, + app_title=frappe.get_hooks("app_title", app_name=app_name), + app_home=frappe.get_hooks("app_home", app_name=app_name), + app_logo_url=frappe.get_hooks("app_logo_url", app_name=app_name), + modules=[m.name for m in frappe.get_all("Module Def", dict(app_name=app_name))], + workspaces=[ + r[0] + for r in ( + frappe.qb.from_(Workspace) + .inner_join(Module) + .on(Workspace.module == Module.name) + .select(Workspace.name) + .where(Module.app_name == app_name) + .run() + ) + ], + ) + ) def get_allowed_pages(cache=False): diff --git a/frappe/desk/doctype/workspace_settings/workspace_settings.js b/frappe/desk/doctype/workspace_settings/workspace_settings.js index 06915000c8..a3ae0763d0 100644 --- a/frappe/desk/doctype/workspace_settings/workspace_settings.js +++ b/frappe/desk/doctype/workspace_settings/workspace_settings.js @@ -18,6 +18,9 @@ frappe.ui.form.on("Workspace Settings", { frm.docfields.push({ fieldtype: "Check", fieldname: page.name, + hidden: !frappe.boot.app_data_map[frappe.current_app].workspaces.includes( + page.title + ), label: page.title + (page.parent_page ? ` (${page.parent_page})` : ""), initial_value: workspace_visibilty[page.name] !== 0, // not set is also visible }); @@ -38,7 +41,7 @@ frappe.ui.form.on("Workspace Settings", { }, after_save(frm) { // reload page to show latest sidebar - window.location.reload(); + frappe.app.sidebar.reload(); }, refresh(frm) { let get_page = (e) => frm.workspace_map[e.fieldobj.df.fieldname]; diff --git a/frappe/hooks.py b/frappe/hooks.py index 3222dabe3d..87a754cb98 100644 --- a/frappe/hooks.py +++ b/frappe/hooks.py @@ -3,12 +3,13 @@ import os from . import __version__ as app_version app_name = "frappe" -app_title = "Frappe Framework" +app_title = "Framework" app_publisher = "Frappe Technologies" app_description = "Full stack web framework with Python, Javascript, MariaDB, Redis, Node" app_license = "MIT" app_logo_url = "/assets/frappe/images/frappe-framework-logo.svg" develop_version = "15.x.x-develop" +app_home = "/app/build" app_email = "developers@frappe.io" diff --git a/frappe/public/images/frappe-framework-logo.svg b/frappe/public/images/frappe-framework-logo.svg index 1c3e2d821b..b708320598 100644 --- a/frappe/public/images/frappe-framework-logo.svg +++ b/frappe/public/images/frappe-framework-logo.svg @@ -1,5 +1,4 @@ - - - - + + + diff --git a/frappe/public/js/frappe/ui/sidebar.js b/frappe/public/js/frappe/ui/sidebar.js index 5c36250e99..6aa5840fd6 100644 --- a/frappe/public/js/frappe/ui/sidebar.js +++ b/frappe/public/js/frappe/ui/sidebar.js @@ -33,29 +33,46 @@ frappe.ui.Sidebar = class Sidebar { } make_dom() { + this.set_default_app(); this.wrapper = $(`
-
- -
- - `).prependTo("body"); @@ -65,6 +82,70 @@ frappe.ui.Sidebar = class Sidebar { this.wrapper.find(".body-sidebar .edit-sidebar-link").on("click", () => { frappe.quick_edit("Workspace Settings"); }); + + this.setup_app_switcher(); + } + + set_default_app() { + // sort apps based on # of workspaces + frappe.boot.app_data.sort((a, b) => (a.workspaces.length < b.workspaces.length ? 1 : -1)); + frappe.current_app = frappe.boot.app_data[0].app_name; + } + + setup_app_switcher() { + let app_switcher_menu = $(".app-switcher-menu"); + + $(".app-switcher-dropdown").on("click", () => { + app_switcher_menu.toggleClass("hidden"); + }); + + // hover out of the sidebar + this.wrapper.find(".body-sidebar").on("mouseleave", () => { + app_switcher_menu.addClass("hidden"); + + // hide any expanded menus as they leave a blank space in the sidebar + this.wrapper.find(".drop-icon[data-state='opened'").click(); + }); + + frappe.boot.app_data_map = {}; + for (var app of frappe.boot.app_data) { + frappe.boot.app_data_map[app.app_name] = app; + if (app.workspaces?.length) { + $(``).appendTo(app_switcher_menu); + } + } + + app_switcher_menu.find(".app-item").on("click", (e) => { + let item = $(e.delegateTarget); + frappe.current_app = item.attr("data-app-name"); + frappe.set_route(item.attr("data-app-home")); + + this.wrapper + .find(".app-switcher-dropdown .sidebar-item-icon img") + .attr("src", frappe.boot.app_data_map[frappe.current_app].app_logo_url); + this.wrapper + .find(".app-switcher-dropdown .sidebar-item-label") + .html(frappe.boot.app_data_map[frappe.current_app].app_title); + + // hide menu + app_switcher_menu.toggleClass("hidden"); + + // re-render the sidebar + this.make_sidebar(); + }); } setup_pages() { @@ -97,10 +178,12 @@ frappe.ui.Sidebar = class Sidebar { this.wrapper.find(".standard-sidebar-section").remove(); } + let app_workspaces = frappe.boot.app_data_map[frappe.current_app || "frappe"].workspaces; + let parent_pages = this.all_pages.filter((p) => !p.parent_page).uniqBy((p) => p.title); parent_pages = [ - ...parent_pages.filter((p) => !p.public), - ...parent_pages.filter((p) => p.public), + ...parent_pages.filter((p) => !p.public && app_workspaces.includes(p.title)), + ...parent_pages.filter((p) => p.public && app_workspaces.includes(p.title)), ]; this.build_sidebar_section("All", parent_pages); @@ -238,12 +321,22 @@ frappe.ui.Sidebar = class Sidebar { $drop_icon.removeClass("hidden"); } $drop_icon.on("click", () => { - let icon = - $drop_icon.find("use").attr("href") === "#es-line-down" - ? "#es-line-up" - : "#es-line-down"; - $drop_icon.find("use").attr("href", icon); + let opened = $drop_icon.find("use").attr("href") === "#es-line-down"; + + if (!opened) { + $drop_icon.attr("data-state", "closed").find("use").attr("href", "#es-line-down"); + } else { + $drop_icon.attr("data-state", "opened").find("use").attr("href", "#es-line-up"); + } + ``; $child_item_section.toggleClass("hidden"); }); } + + reload() { + return frappe.workspace.get_pages().then((r) => { + frappe.boot.sidebar_pages = r; + this.setup_pages(); + }); + } }; diff --git a/frappe/public/js/frappe/ui/toolbar/toolbar.js b/frappe/public/js/frappe/ui/toolbar/toolbar.js index ca7da66e72..c2981e037d 100644 --- a/frappe/public/js/frappe/ui/toolbar/toolbar.js +++ b/frappe/public/js/frappe/ui/toolbar/toolbar.js @@ -43,9 +43,6 @@ frappe.ui.toolbar.Toolbar = class { search_modal.find("#modal-search").focus(); }, 300); }); - $(".navbar-toggle-full-width").click(() => { - frappe.ui.toolbar.toggle_full_width(); - }); } setup_read_only_mode() { diff --git a/frappe/public/scss/desk/sidebar.scss b/frappe/public/scss/desk/sidebar.scss index 41cda4c101..d3498100c1 100644 --- a/frappe/public/scss/desk/sidebar.scss +++ b/frappe/public/scss/desk/sidebar.scss @@ -192,6 +192,27 @@ body { } } +.app-switcher-menu { + position: absolute; + top: 40px; + padding: var(--padding-xs); + box-shadow: var(--shadow-base); + background-color: var(--neutral); + min-width: 180px; + z-index: 1; + border-radius: var(--border-radius-tiny); +} + +.app-item { + padding: var(--padding-xs); + + a { + text-decoration: none; + display: flex; + vertical-align: middle; + } +} + // form sidebar .form-sidebar { .sidebar-section {