From 087ee41ea1fbbfa7be376034e770c2a8a7790cb2 Mon Sep 17 00:00:00 2001 From: Rushabh Mehta Date: Thu, 22 Aug 2024 17:16:51 +0530 Subject: [PATCH] feat(design): espresso style sidebar --- frappe/boot.py | 2 +- frappe/desk/doctype/workspace/workspace.py | 60 -- .../workspace_settings/workspace_settings.js | 70 +- .../workspace_settings/workspace_settings.py | 13 +- frappe/public/js/frappe/desk.js | 87 +- frappe/public/js/frappe/form/layout.js | 2 +- .../js/frappe/form/sidebar/attachments.js | 4 +- .../js/frappe/form/sidebar/form_sidebar.js | 1 + .../frappe/form/templates/form_sidebar.html | 127 +-- frappe/public/js/frappe/list/base_list.js | 2 +- frappe/public/js/frappe/list/list_filter.js | 6 +- .../public/js/frappe/list/list_sidebar.html | 166 ++-- frappe/public/js/frappe/list/list_sidebar.js | 5 +- .../js/frappe/list/list_sidebar_group_by.js | 26 +- frappe/public/js/frappe/logtypes.js | 2 +- frappe/public/js/frappe/ui/page.js | 14 +- frappe/public/js/frappe/ui/sidebar.js | 274 ++++-- frappe/public/js/frappe/ui/tags.js | 4 +- .../public/js/frappe/ui/toolbar/navbar.html | 4 +- .../js/frappe/views/kanban/kanban_view.js | 2 + .../js/frappe/views/reports/query_report.js | 2 +- .../js/frappe/views/workspace/workspace.js | 806 ++---------------- frappe/public/scss/common/awesomeplete.scss | 2 + frappe/public/scss/common/css_variables.scss | 4 +- frappe/public/scss/common/global.scss | 2 +- frappe/public/scss/common/icon_picker.scss | 9 +- frappe/public/scss/common/modal.scss | 4 + frappe/public/scss/desk/breadcrumb.scss | 1 - frappe/public/scss/desk/css_variables.scss | 2 +- frappe/public/scss/desk/desktop.scss | 220 +---- frappe/public/scss/desk/form.scss | 19 +- frappe/public/scss/desk/global.scss | 17 +- frappe/public/scss/desk/list.scss | 13 +- frappe/public/scss/desk/mobile.scss | 69 +- frappe/public/scss/desk/navbar.scss | 2 +- frappe/public/scss/desk/page.scss | 33 +- frappe/public/scss/desk/report.scss | 1 + frappe/public/scss/desk/sidebar.scss | 208 ++++- 38 files changed, 832 insertions(+), 1453 deletions(-) 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")} `; - $(`
  • `) + $(`
    `) .append(frappe.get_data_pill(file_label, fileid, remove_action, icon)) .insertAfter(this.add_attachment_wrapper); } diff --git a/frappe/public/js/frappe/form/sidebar/form_sidebar.js b/frappe/public/js/frappe/form/sidebar/form_sidebar.js index d335803a21..4d62c62766 100644 --- a/frappe/public/js/frappe/form/sidebar/form_sidebar.js +++ b/frappe/public/js/frappe/form/sidebar/form_sidebar.js @@ -145,6 +145,7 @@ frappe.ui.form.Sidebar = class { callback: function (res) { me.sidebar .find(".auto-repeat-status") + .removeClass("hidden") .html(__("Repeats {0}", [__(res.message.frequency)])); me.sidebar.find(".auto-repeat-status").on("click", function () { frappe.set_route("Form", "Auto Repeat", me.frm.doc.auto_repeat); diff --git a/frappe/public/js/frappe/form/templates/form_sidebar.html b/frappe/public/js/frappe/form/templates/form_sidebar.html index a651d9a62b..00d94d9c35 100644 --- a/frappe/public/js/frappe/form/templates/form_sidebar.html +++ b/frappe/public/js/frappe/form/templates/form_sidebar.html @@ -1,6 +1,6 @@ - - + + {% if frm.meta.beta %} {% endif %} - - - - - - - - - - + + + + {% if(frappe.get_form_sidebar_extension) { %} {{ frappe.get_form_sidebar_extension() }} {% } %} diff --git a/frappe/public/js/frappe/list/base_list.js b/frappe/public/js/frappe/list/base_list.js index 890c89ef0d..c7ff5d49cf 100644 --- a/frappe/public/js/frappe/list/base_list.js +++ b/frappe/public/js/frappe/list/base_list.js @@ -169,7 +169,7 @@ frappe.views.BaseList = class BaseList { setup_page() { this.page = this.parent.page; this.$page = $(this.parent); - !this.hide_card_layout && this.page.main.addClass("frappe-card"); + this.page.main.addClass("layout-main-list"); this.page.page_form.removeClass("row").addClass("flex"); this.hide_page_form && this.page.page_form.hide(); this.hide_sidebar && this.$page.addClass("no-list-sidebar"); diff --git a/frappe/public/js/frappe/list/list_filter.js b/frappe/public/js/frappe/list/list_filter.js index 2f7b1ba9bc..4047761ca9 100644 --- a/frappe/public/js/frappe/list/list_filter.js +++ b/frappe/public/js/frappe/list/list_filter.js @@ -13,10 +13,10 @@ export default class ListFilter { make() { // init dom this.wrapper.html(` -
  • - +
    `); diff --git a/frappe/public/js/frappe/list/list_sidebar.html b/frappe/public/js/frappe/list/list_sidebar.html index 0437f4af20..de420c98f5 100644 --- a/frappe/public/js/frappe/list/list_sidebar.html +++ b/frappe/public/js/frappe/list/list_sidebar.html @@ -1,91 +1,91 @@ - - +
    + + +
    + + + diff --git a/frappe/public/js/frappe/list/list_sidebar.js b/frappe/public/js/frappe/list/list_sidebar.js index 236d3ae9a8..804a39605e 100644 --- a/frappe/public/js/frappe/list/list_sidebar.js +++ b/frappe/public/js/frappe/list/list_sidebar.js @@ -174,6 +174,7 @@ frappe.views.ListSidebar = class ListSidebar { let sections = [ ["tags-section", "list-tags"], ["save-filter-section", "list-filters"], + ["filter-section", "list-group-by"], ]; for (let s of sections) { @@ -237,11 +238,11 @@ frappe.views.ListSidebar = class ListSidebar { } set_loading_state(dropdown) { - dropdown.html(`
  • + dropdown.html(`
    ${__("Loading...")}
    -
  • `); + `); } render_stat(stats) { diff --git a/frappe/public/js/frappe/list/list_sidebar_group_by.js b/frappe/public/js/frappe/list/list_sidebar_group_by.js index 53f5406f4b..5ce58abfd9 100644 --- a/frappe/public/js/frappe/list/list_sidebar_group_by.js +++ b/frappe/public/js/frappe/list/list_sidebar_group_by.js @@ -55,11 +55,11 @@ frappe.views.ListGroupBy = class ListGroupBy { let html = `
    - + `; this.$wrapper.html(html); } @@ -80,17 +80,17 @@ frappe.views.ListGroupBy = class ListGroupBy { fieldtype = docfield.fieldtype; } - return ``; + `; }; let html = this.group_by_fields.map(get_item_html).join(""); this.$wrapper.find(".list-group-by-fields").html(html); @@ -230,13 +230,13 @@ frappe.views.ListGroupBy = class ListGroupBy { let applied_html = applied ? ` ${frappe.utils.icon("tick", "xs")} ` : ""; - return `
  • + return `
  • `; + `; } setup_filter_by() { diff --git a/frappe/public/js/frappe/logtypes.js b/frappe/public/js/frappe/logtypes.js index 1e65de48a7..0116aab6c1 100644 --- a/frappe/public/js/frappe/logtypes.js +++ b/frappe/public/js/frappe/logtypes.js @@ -11,7 +11,7 @@ frappe.utils.logtypes.show_log_retention_message = (doctype) => { } const add_sidebar_message = (message) => { - let sidebar_entry = $('').appendTo( + let sidebar_entry = $('