diff --git a/frappe/boot.py b/frappe/boot.py index 94c34abce3..240924291b 100644 --- a/frappe/boot.py +++ b/frappe/boot.py @@ -143,7 +143,7 @@ def load_conf_settings(bootinfo): def load_desktop_data(bootinfo): from frappe.desk.desktop import get_workspace_sidebar_items - bootinfo.allowed_workspaces = get_workspace_sidebar_items().get("pages") + bootinfo.sidebar_pages = get_workspace_sidebar_items() bootinfo.module_wise_workspaces = get_controller("Workspace").get_module_wise_workspaces() bootinfo.dashboards = frappe.get_all("Dashboard") diff --git a/frappe/desk/doctype/workspace/workspace.py b/frappe/desk/doctype/workspace/workspace.py index 1ea99d33f0..a4094dee96 100644 --- a/frappe/desk/doctype/workspace/workspace.py +++ b/frappe/desk/doctype/workspace/workspace.py @@ -341,32 +341,6 @@ def update_page(name, title, icon, indicator_color, parent, public): return {"name": title, "public": public, "label": new_name} -def hide_unhide_page(page_name: str, is_hidden: bool): - page = frappe.get_doc("Workspace", page_name) - - if page.get("public") and not is_workspace_manager(): - frappe.throw( - _("Need Workspace Manager role to hide/unhide public workspaces"), frappe.PermissionError - ) - - if not page.get("public") and page.get("for_user") != frappe.session.user and not is_workspace_manager(): - frappe.throw(_("Cannot update private workspace of other users"), frappe.PermissionError) - - page.is_hidden = int(is_hidden) - page.save(ignore_permissions=True) - return True - - -@frappe.whitelist() -def hide_page(page_name: str): - return hide_unhide_page(page_name, 1) - - -@frappe.whitelist() -def unhide_page(page_name: str): - return hide_unhide_page(page_name, 0) - - @frappe.whitelist() def duplicate_page(page_name, new_page): if not loads(new_page): @@ -426,40 +400,6 @@ def delete_page(page): return {"name": page.get("name"), "public": page.get("public"), "title": page.get("title")} -@frappe.whitelist() -def sort_pages(sb_public_items, sb_private_items): - if not loads(sb_public_items) and not loads(sb_private_items): - return - - sb_public_items = loads(sb_public_items) - sb_private_items = loads(sb_private_items) - - workspace_public_pages = get_page_list(["name", "title"], {"public": 1}) - workspace_private_pages = get_page_list(["name", "title"], {"for_user": frappe.session.user}) - - if sb_private_items: - return sort_page(workspace_private_pages, sb_private_items) - - if sb_public_items and is_workspace_manager(): - return sort_page(workspace_public_pages, sb_public_items) - - return False - - -def sort_page(workspace_pages, pages): - for seq, d in enumerate(pages): - for page in workspace_pages: - if page.title == d.get("title"): - doc = frappe.get_doc("Workspace", page.name) - doc.sequence_id = seq + 1 - doc.parent_page = d.get("parent_page") or "" - doc.flags.ignore_links = True - doc.save(ignore_permissions=True) - break - - return True - - def last_sequence_id(doc): doc_exists = frappe.db.exists({"doctype": "Workspace", "public": doc.public, "for_user": doc.for_user}) diff --git a/frappe/desk/doctype/workspace_settings/workspace_settings.js b/frappe/desk/doctype/workspace_settings/workspace_settings.js index d1ba550205..06915000c8 100644 --- a/frappe/desk/doctype/workspace_settings/workspace_settings.js +++ b/frappe/desk/doctype/workspace_settings/workspace_settings.js @@ -5,33 +5,35 @@ frappe.ui.form.on("Workspace Settings", { setup(frm) { frm.hide_full_form_button = true; frm.docfields = []; + frm.workspace_map = {}; let workspace_visibilty = JSON.parse(frm.doc.workspace_visibility_json || "{}"); // build fields from workspaces let cnt = 0, column_added = false; - for (let w of frappe.boot.allowed_workspaces) { - if (w.public) { + for (let page of frappe.boot.allowed_workspaces) { + if (page.public) { + frm.workspace_map[page.name] = page; cnt++; frm.docfields.push({ fieldtype: "Check", - fieldname: w.name, - label: w.title, - initial_value: workspace_visibilty[w.name] !== 0, // not set is also visible + fieldname: page.name, + label: page.title + (page.parent_page ? ` (${page.parent_page})` : ""), + initial_value: workspace_visibilty[page.name] !== 0, // not set is also visible }); } - - if (cnt >= frappe.boot.allowed_workspaces.length / 2 && !column_added) { - // add column break to split into 2 columns - frm.docfields.push({ fieldtype: "Column Break" }); - column_added = true; - } } frappe.temp = frm; }, validate(frm) { frm.doc.workspace_visibility_json = JSON.stringify(frm.dialog.get_values()); + frm.doc.workspace_sequence = JSON.stringify( + frm.wrapper + .find(".frappe-control") + .get() + .map((e) => e.fieldobj.df.fieldname) + ); frm.doc.workspace_setup_completed = 1; }, after_save(frm) { @@ -39,6 +41,50 @@ frappe.ui.form.on("Workspace Settings", { window.location.reload(); }, refresh(frm) { - frm.dialog.set_alert(__("Select modules you want to see in the sidebar")); + let get_page = (e) => frm.workspace_map[e.fieldobj.df.fieldname]; + + frm.dialog.set_alert(__("Select, sort modules you want to see in the sidebar")); + if (!frm.workspace_sortable) { + frm.wrapper.find(".frappe-control").css({ "margin-bottom": "0.5rem" }); + + let forms = frm.wrapper.find("form"); + frm.workspace_sortable = Sortable.create(forms.get(0), { + group: "workspace_settings", + animation: 150, + onEnd: (o) => { + // re-order so that child items are below parent items + for (let e of frm.wrapper.find(".frappe-control").get()) { + let page = get_page(e); + if (page.parent_page) { + // insert as the last child of the parent element + let parent_element = frm.wrapper + .find(`[data-fieldname="${page.parent_page}"`) + .closest(".frappe-control"); + let parent_page = page.parent_page; + + // find the last child + while (parent_element) { + let next_element = parent_element.next(".frappe-control"); + + if (!next_element.length) { + // end of list + $(e).insertAfter(parent_element); + break; + } else { + let page = get_page(next_element.get(0)); + if (page.parent_page != parent_page) { + // different parent, last child found + $(e).insertAfter(parent_element); + break; + } + } + + parent_element = next_element; + } + } + } + }, + }); + } }, }); diff --git a/frappe/desk/doctype/workspace_settings/workspace_settings.py b/frappe/desk/doctype/workspace_settings/workspace_settings.py index 7909a37c1e..df5773e820 100644 --- a/frappe/desk/doctype/workspace_settings/workspace_settings.py +++ b/frappe/desk/doctype/workspace_settings/workspace_settings.py @@ -1,7 +1,9 @@ # Copyright (c) 2024, Frappe Technologies and contributors # For license information, please see license.txt -# import frappe +import json + +import frappe from frappe.model.document import Document @@ -19,3 +21,12 @@ class WorkspaceSettings(Document): # end: auto-generated types pass + + def validate(self): + cnt = 1 + for page_name in json.loads(self.workspace_sequence): + frappe.db.set_value("Workspace", page_name, "sequence_id", cnt) + cnt += 1 + + def on_update(self): + frappe.clear_cache() diff --git a/frappe/public/js/frappe/desk.js b/frappe/public/js/frappe/desk.js index 1be36c978f..b16e409397 100644 --- a/frappe/public/js/frappe/desk.js +++ b/frappe/public/js/frappe/desk.js @@ -37,6 +37,7 @@ frappe.Application = class Application { this.load_bootinfo(); this.load_user_permissions(); this.make_nav_bar(); + this.make_sidebar(); this.set_favicon(); this.set_fullwidth_if_enabled(); this.add_browser_class(); @@ -46,6 +47,51 @@ frappe.Application = class Application { frappe.ui.keys.setup(); + this.setup_theme(); + + // page container + this.make_page_container(); + this.setup_tours(); + this.set_route(); + + // trigger app startup + $(document).trigger("startup"); + $(document).trigger("app_ready"); + + this.show_notices(); + this.show_notes(); + + if (frappe.ui.startup_setup_dialog && !frappe.boot.setup_complete) { + frappe.ui.startup_setup_dialog.pre_show(); + frappe.ui.startup_setup_dialog.show(); + } + + // listen to build errors + this.setup_build_events(); + + if (frappe.sys_defaults.email_user_password) { + var email_list = frappe.sys_defaults.email_user_password.split(","); + for (var u in email_list) { + if (email_list[u] === frappe.user.name) { + this.set_password(email_list[u]); + } + } + } + + // REDESIGN-TODO: Fix preview popovers + this.link_preview = new frappe.ui.LinkPreview(); + + frappe.broadcast.emit("boot", { + csrf_token: frappe.csrf_token, + user: frappe.session.user, + }); + } + + make_sidebar() { + this.sidebar = new frappe.ui.Sidebar({}); + } + + setup_theme() { frappe.ui.keys.add_shortcut({ shortcut: "shift+ctrl+g", description: __("Switch Theme"), @@ -71,9 +117,9 @@ frappe.Application = class Application { }); frappe.ui.set_theme(); + } - // page container - this.make_page_container(); + setup_tours() { if ( !window.Cypress && frappe.boot.onboarding_tours && @@ -90,13 +136,9 @@ frappe.Application = class Application { }); } } - this.set_route(); - - // trigger app startup - $(document).trigger("startup"); - - $(document).trigger("app_ready"); + } + show_notices() { if (frappe.boot.messages) { frappe.msgprint(frappe.boot.messages); } @@ -116,13 +158,6 @@ frappe.Application = class Application { console.log(`%c${console_security_message}`, "font-size: large"); } - this.show_notes(); - - if (frappe.ui.startup_setup_dialog && !frappe.boot.setup_complete) { - frappe.ui.startup_setup_dialog.pre_show(); - frappe.ui.startup_setup_dialog.show(); - } - frappe.realtime.on("version-update", function () { var dialog = frappe.msgprint({ message: __( @@ -136,26 +171,6 @@ frappe.Application = class Application { }); dialog.get_close_btn().toggle(false); }); - - // listen to build errors - this.setup_build_events(); - - if (frappe.sys_defaults.email_user_password) { - var email_list = frappe.sys_defaults.email_user_password.split(","); - for (var u in email_list) { - if (email_list[u] === frappe.user.name) { - this.set_password(email_list[u]); - } - } - } - - // REDESIGN-TODO: Fix preview popovers - this.link_preview = new frappe.ui.LinkPreview(); - - frappe.broadcast.emit("boot", { - csrf_token: frappe.csrf_token, - user: frappe.session.user, - }); } set_route() { @@ -275,6 +290,8 @@ frappe.Application = class Application { setup_workspaces() { frappe.modules = {}; frappe.workspaces = {}; + frappe.boot.allowed_workspaces = frappe.boot.sidebar_pages.pages; + for (let page of frappe.boot.allowed_workspaces || []) { frappe.modules[page.module] = page; frappe.workspaces[frappe.router.slug(page.name)] = page; diff --git a/frappe/public/js/frappe/form/layout.js b/frappe/public/js/frappe/form/layout.js index 038e3c55ef..ce72887537 100644 --- a/frappe/public/js/frappe/form/layout.js +++ b/frappe/public/js/frappe/form/layout.js @@ -503,7 +503,7 @@ frappe.ui.form.Layout = class Layout { let tabs_content = this.tabs_content[0]; if (!tabs_list.length) return; - $(window).scroll( + $(".main-section").scroll( frappe.utils.throttle(() => { let current_scroll = document.documentElement.scrollTop; if (current_scroll > 0 && last_scroll <= current_scroll) { diff --git a/frappe/public/js/frappe/form/sidebar/attachments.js b/frappe/public/js/frappe/form/sidebar/attachments.js index 4c8f37186e..e60b32dbea 100644 --- a/frappe/public/js/frappe/form/sidebar/attachments.js +++ b/frappe/public/js/frappe/form/sidebar/attachments.js @@ -124,7 +124,7 @@ frappe.ui.form.Attachments = class Attachments { let file_label = ` ${file_name} `; @@ -151,7 +151,7 @@ frappe.ui.form.Attachments = class Attachments { ${frappe.utils.icon(attachment.is_private ? "es-line-lock" : "es-line-unlock", "sm ml-0")} `; - $(`