From b4f5a63ebd3d36c1d97c1a977cc62102a644dac0 Mon Sep 17 00:00:00 2001 From: sokumon Date: Thu, 7 Aug 2025 01:26:37 +0530 Subject: [PATCH 001/127] fix: allow /desktop --- frappe/hooks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/hooks.py b/frappe/hooks.py index b8c8901320..08d561e172 100644 --- a/frappe/hooks.py +++ b/frappe/hooks.py @@ -60,7 +60,7 @@ website_route_rules = [ ] website_redirects = [ - {"source": r"/desk(.*)", "target": r"/app\1"}, + {"source": r"/desk", "target": r"/app\1"}, ] base_template = "templates/base.html" From 2af99c60097e7e63a128f60003fc99f738f8dd2e Mon Sep 17 00:00:00 2001 From: sokumon Date: Thu, 7 Aug 2025 20:41:27 +0530 Subject: [PATCH 002/127] feat: reIntroduce Desktop Icon --- .../doctype/desktop_icon/desktop_icon.json | 10 ++--- .../desk/doctype/desktop_icon/desktop_icon.py | 3 +- .../doctype/desktop_icon/test_desktop_icon.py | 20 ++++++++++ frappe/www/desktop.css | 28 ++++++++++++++ frappe/www/desktop.html | 38 +++++++++++++++++++ frappe/www/desktop.py | 11 ++++++ 6 files changed, 103 insertions(+), 7 deletions(-) create mode 100644 frappe/desk/doctype/desktop_icon/test_desktop_icon.py create mode 100644 frappe/www/desktop.css create mode 100644 frappe/www/desktop.html create mode 100644 frappe/www/desktop.py diff --git a/frappe/desk/doctype/desktop_icon/desktop_icon.json b/frappe/desk/doctype/desktop_icon/desktop_icon.json index 1e42a6d468..31e23dea0f 100644 --- a/frappe/desk/doctype/desktop_icon/desktop_icon.json +++ b/frappe/desk/doctype/desktop_icon/desktop_icon.json @@ -125,12 +125,12 @@ }, { "fieldname": "color", - "fieldtype": "Data", + "fieldtype": "Color", "label": "Color" }, { "fieldname": "icon", - "fieldtype": "Data", + "fieldtype": "Icon", "label": "Icon" }, { @@ -145,9 +145,8 @@ "label": "Idx" } ], - "in_create": 1, "links": [], - "modified": "2024-03-23 16:02:17.847139", + "modified": "2025-08-07 13:11:45.701693", "modified_by": "Administrator", "module": "Desk", "name": "Desktop Icon", @@ -167,9 +166,10 @@ } ], "read_only": 1, + "row_format": "Dynamic", "sort_field": "creation", "sort_order": "DESC", "states": [], "title_field": "module_name", "track_changes": 1 -} \ No newline at end of file +} diff --git a/frappe/desk/doctype/desktop_icon/desktop_icon.py b/frappe/desk/doctype/desktop_icon/desktop_icon.py index 77b7f01570..3d63c498fe 100644 --- a/frappe/desk/doctype/desktop_icon/desktop_icon.py +++ b/frappe/desk/doctype/desktop_icon/desktop_icon.py @@ -24,12 +24,11 @@ class DesktopIcon(Document): app: DF.Data | None blocked: DF.Check category: DF.Data | None - color: DF.Data | None + color: DF.Color | None custom: DF.Check description: DF.SmallText | None force_show: DF.Check hidden: DF.Check - icon: DF.Data | None idx: DF.Int label: DF.Data | None link: DF.SmallText | None diff --git a/frappe/desk/doctype/desktop_icon/test_desktop_icon.py b/frappe/desk/doctype/desktop_icon/test_desktop_icon.py new file mode 100644 index 0000000000..d83eb7c9ed --- /dev/null +++ b/frappe/desk/doctype/desktop_icon/test_desktop_icon.py @@ -0,0 +1,20 @@ +# Copyright (c) 2025, Frappe Technologies and Contributors +# See license.txt + +# import frappe +from frappe.tests import IntegrationTestCase + +# On IntegrationTestCase, the doctype test records and all +# link-field test record dependencies are recursively loaded +# Use these module variables to add/remove to/from that list +EXTRA_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"] +IGNORE_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"] + + +class IntegrationTestDesktopIcon(IntegrationTestCase): + """ + Integration tests for DesktopIcon. + Use this class for testing interactions between multiple components. + """ + + pass diff --git a/frappe/www/desktop.css b/frappe/www/desktop.css new file mode 100644 index 0000000000..acf26f3008 --- /dev/null +++ b/frappe/www/desktop.css @@ -0,0 +1,28 @@ +.desktop-container{ + max-width: 100px; +} +.icons{ + gap: 30px; + display: grid; + justify-items: center; + grid-template-columns: repeat(3, 1fr); + margin-top: 50px; +} +.d-icon{ + stroke: white; +} +.desktop-icon{ + display: flex; + height: 100px; + width: 100px; + flex-direction: column; + align-items: center; +} +.icon-container{ + padding: 10px; + border: black solid 0px; +} +.icon-container:hover{ + transform: scale(1.05); + transition: transform 0.1s; +} diff --git a/frappe/www/desktop.html b/frappe/www/desktop.html new file mode 100644 index 0000000000..003583f411 --- /dev/null +++ b/frappe/www/desktop.html @@ -0,0 +1,38 @@ +--- +base_template: "templates/base.html" +no_cache: 1 +--- + +{%- block navbar -%} {% from "frappe/templates/includes/avatar_macro.html" import avatar %} {%- +endblock -%} {%- block footer -%} {%- endblock -%} {% block content %} +
+
+
+ {% for icon in icons %} +
+
+ {{ icon.label }} +
+ + {% endfor %} +
+
+ +
+{% endblock %} {% block script %} + +{% endblock %} diff --git a/frappe/www/desktop.py b/frappe/www/desktop.py new file mode 100644 index 0000000000..0a84300b4b --- /dev/null +++ b/frappe/www/desktop.py @@ -0,0 +1,11 @@ +import frappe +from frappe.desk.doctype.desktop_icon.desktop_icon import get_desktop_icons + + +def get_context(context): + if frappe.session.user == "Guest": + frappe.local.flags.redirect_location = "/app" + raise frappe.Redirect + + context.icons = get_desktop_icons() + return context From 2a47687690f9c0d308eddec5a8aee57ff226d31e Mon Sep 17 00:00:00 2001 From: sokumon Date: Tue, 12 Aug 2025 12:05:12 +0530 Subject: [PATCH 003/127] fix: make the desktop page functional --- .../desk/doctype/desktop_icon/desktop_icon.js | 25 +++++++++++++++ .../doctype/desktop_icon/desktop_icon.json | 20 ++++++++++-- .../desk/doctype/desktop_icon/desktop_icon.py | 31 ++++++++++++++++++- frappe/public/js/frappe/form/grid_row.js | 1 - frappe/www/desktop.css | 11 ++++--- frappe/www/desktop.html | 17 ++++++++-- frappe/www/desktop.py | 1 - 7 files changed, 93 insertions(+), 13 deletions(-) diff --git a/frappe/desk/doctype/desktop_icon/desktop_icon.js b/frappe/desk/doctype/desktop_icon/desktop_icon.js index 72ef1f7a12..4437bec0c6 100644 --- a/frappe/desk/doctype/desktop_icon/desktop_icon.js +++ b/frappe/desk/doctype/desktop_icon/desktop_icon.js @@ -3,4 +3,29 @@ frappe.ui.form.on("Desktop Icon", { refresh: function (frm) {}, + before_save: function (frm) { + if (frm.doc.type == "workspace") { + frappe.call({ + method: "frappe.client.get", + args: { + doctype: "Workspace", // e.g., "User" + name: frm.doc.workspace, + }, + callback: function (r) { + if (r.message) { + // Access attributes like r.message.another_field + let doc = r.message; + let url = `/app/${ + doc.public + ? frappe.router.slug(doc.title) + : "private/" + frappe.router.slug(doc.title) + }`; + frm.doc.route = url; + } + }, + }); + } else if (frm.doc.type == "link") { + frm.doc.route = frm.doc.link; + } + }, }); diff --git a/frappe/desk/doctype/desktop_icon/desktop_icon.json b/frappe/desk/doctype/desktop_icon/desktop_icon.json index 31e23dea0f..d72520622a 100644 --- a/frappe/desk/doctype/desktop_icon/desktop_icon.json +++ b/frappe/desk/doctype/desktop_icon/desktop_icon.json @@ -8,6 +8,9 @@ "label", "standard", "custom", + "type", + "workspace", + "route", "column_break_3", "app", "description", @@ -16,7 +19,6 @@ "blocked", "force_show", "section_break_7", - "type", "_doctype", "_report", "link", @@ -100,7 +102,7 @@ "in_list_view": 1, "in_standard_filter": 1, "label": "Type", - "options": "module\nlist\nlink\npage\nquery-report" + "options": "module\nlist\nlink\npage\nquery-report\nworkspace" }, { "fieldname": "_doctype", @@ -143,10 +145,22 @@ "fieldname": "idx", "fieldtype": "Int", "label": "Idx" + }, + { + "fieldname": "workspace", + "fieldtype": "Link", + "label": "Workspace", + "options": "Workspace" + }, + { + "fieldname": "route", + "fieldtype": "Data", + "hidden": 1, + "label": "Route" } ], "links": [], - "modified": "2025-08-07 13:11:45.701693", + "modified": "2025-08-08 02:36:28.942332", "modified_by": "Administrator", "module": "Desk", "name": "Desktop Icon", diff --git a/frappe/desk/doctype/desktop_icon/desktop_icon.py b/frappe/desk/doctype/desktop_icon/desktop_icon.py index 3d63c498fe..935b58241d 100644 --- a/frappe/desk/doctype/desktop_icon/desktop_icon.py +++ b/frappe/desk/doctype/desktop_icon/desktop_icon.py @@ -34,8 +34,10 @@ class DesktopIcon(Document): link: DF.SmallText | None module_name: DF.Data | None reverse: DF.Check + route: DF.Data | None standard: DF.Check - type: DF.Literal["module", "list", "link", "page", "query-report"] + type: DF.Literal["module", "list", "link", "page", "query-report", "workspace"] + workspace: DF.Link | None # end: auto-generated types def validate(self): @@ -45,6 +47,9 @@ class DesktopIcon(Document): def on_trash(self): clear_desktop_icons_cache() + def after_insert(self): + clear_desktop_icons_cache() + def after_doctype_insert(): frappe.db.add_unique("Desktop Icon", ("module_name", "owner", "standard")) @@ -76,6 +81,8 @@ def get_desktop_icons(user=None): "custom", "standard", "blocked", + "workspace", + "route", ] active_domains = frappe.get_active_domains() @@ -569,3 +576,25 @@ def hide(name, user=None): return False return True + + +def create_desktop_icons_from_workspace(): + all_workspaces = frappe.get_all("Workspace", filters={"public": 1}, pluck="name") + for w in all_workspaces: + icon = frappe.new_doc("Desktop Icon") + icon.type = "workspace" + icon.workspace = w + icon.route = "/app/" + w.lower() + icon.label = w + icon.color = generate_color() + icon.icon = frappe.db.get_value("Workspace", w, "icon") + icon.insert(ignore_if_duplicate=True) + + +def generate_color(): + import random + + def hex(): + return random.randint(0, 255) + + return "#%02X%02X%02X" % (hex(), hex(), hex()) diff --git a/frappe/public/js/frappe/form/grid_row.js b/frappe/public/js/frappe/form/grid_row.js index 29b3f239f5..25b1087070 100644 --- a/frappe/public/js/frappe/form/grid_row.js +++ b/frappe/public/js/frappe/form/grid_row.js @@ -384,7 +384,6 @@ export default class GridRow { add_column_configure_button() { if (this.grid.df.in_place_edit && !this.frm) return; - if (this.configure_columns && this.frm) { this.configure_columns_button = $(`
diff --git a/frappe/www/desktop.css b/frappe/www/desktop.css index acf26f3008..5ce2ccf7c8 100644 --- a/frappe/www/desktop.css +++ b/frappe/www/desktop.css @@ -1,12 +1,9 @@ -.desktop-container{ - max-width: 100px; -} .icons{ gap: 30px; display: grid; justify-items: center; - grid-template-columns: repeat(3, 1fr); margin-top: 50px; + grid-template-columns: repeat(5, 1fr); } .d-icon{ stroke: white; @@ -21,8 +18,14 @@ .icon-container{ padding: 10px; border: black solid 0px; + border-radius: 10px; } .icon-container:hover{ transform: scale(1.05); transition: transform 0.1s; } +.icon-label{ + text-align: center; + text-wrap: nowrap; + margin-top: 5px; +} diff --git a/frappe/www/desktop.html b/frappe/www/desktop.html index 003583f411..cc2a9ddea4 100644 --- a/frappe/www/desktop.html +++ b/frappe/www/desktop.html @@ -9,9 +9,9 @@ endblock -%} {%- block footer -%} {%- endblock -%} {% block content %}
{% for icon in icons %} -
+
- {{ icon.label }} +
{{ icon.label }}
{% endfor %} @@ -21,7 +21,6 @@ endblock -%} {%- block footer -%} {%- endblock -%} {% block content %}
{% endblock %} {% block script %} {% endblock %} diff --git a/frappe/www/desktop.py b/frappe/www/desktop.py index 0a84300b4b..1a51ac7896 100644 --- a/frappe/www/desktop.py +++ b/frappe/www/desktop.py @@ -6,6 +6,5 @@ def get_context(context): if frappe.session.user == "Guest": frappe.local.flags.redirect_location = "/app" raise frappe.Redirect - context.icons = get_desktop_icons() return context From e1ea6c58f283277d7d3f45b88b6bce37d7da3fec Mon Sep 17 00:00:00 2001 From: sokumon Date: Wed, 13 Aug 2025 02:49:24 +0530 Subject: [PATCH 004/127] feat: Workspace Sidebar --- .../doctype/desktop_icon/desktop_icon.json | 5 +- .../desk/doctype/desktop_icon/desktop_icon.py | 3 +- .../doctype/workspace_sidebar/__init__.py | 0 .../test_workspace_sidebar.py | 20 ++++++ .../workspace_sidebar/workspace_sidebar.js | 8 +++ .../workspace_sidebar/workspace_sidebar.json | 62 +++++++++++++++++++ .../workspace_sidebar/workspace_sidebar.py | 23 +++++++ .../workspace_sidebar_item/__init__.py | 0 .../workspace_sidebar_item.json | 58 +++++++++++++++++ .../workspace_sidebar_item.py | 26 ++++++++ frappe/public/js/desk.bundle.js | 4 +- frappe/public/js/frappe/ui/sidebar.js | 2 +- ...apps_switcher.html => sidebar_header.html} | 0 .../{apps_switcher.js => sidebar_header.js} | 5 +- frappe/public/js/frappe/views/breadcrumbs.js | 2 +- 15 files changed, 208 insertions(+), 10 deletions(-) create mode 100644 frappe/desk/doctype/workspace_sidebar/__init__.py create mode 100644 frappe/desk/doctype/workspace_sidebar/test_workspace_sidebar.py create mode 100644 frappe/desk/doctype/workspace_sidebar/workspace_sidebar.js create mode 100644 frappe/desk/doctype/workspace_sidebar/workspace_sidebar.json create mode 100644 frappe/desk/doctype/workspace_sidebar/workspace_sidebar.py create mode 100644 frappe/desk/doctype/workspace_sidebar_item/__init__.py create mode 100644 frappe/desk/doctype/workspace_sidebar_item/workspace_sidebar_item.json create mode 100644 frappe/desk/doctype/workspace_sidebar_item/workspace_sidebar_item.py rename frappe/public/js/frappe/ui/{apps_switcher.html => sidebar_header.html} (100%) rename frappe/public/js/frappe/ui/{apps_switcher.js => sidebar_header.js} (98%) diff --git a/frappe/desk/doctype/desktop_icon/desktop_icon.json b/frappe/desk/doctype/desktop_icon/desktop_icon.json index d72520622a..5bb7cfd6df 100644 --- a/frappe/desk/doctype/desktop_icon/desktop_icon.json +++ b/frappe/desk/doctype/desktop_icon/desktop_icon.json @@ -160,7 +160,7 @@ } ], "links": [], - "modified": "2025-08-08 02:36:28.942332", + "modified": "2025-08-12 12:22:16.735640", "modified_by": "Administrator", "module": "Desk", "name": "Desktop Icon", @@ -181,9 +181,10 @@ ], "read_only": 1, "row_format": "Dynamic", + "show_title_field_in_link": 1, "sort_field": "creation", "sort_order": "DESC", "states": [], - "title_field": "module_name", + "title_field": "label", "track_changes": 1 } diff --git a/frappe/desk/doctype/desktop_icon/desktop_icon.py b/frappe/desk/doctype/desktop_icon/desktop_icon.py index 935b58241d..b44911a2ff 100644 --- a/frappe/desk/doctype/desktop_icon/desktop_icon.py +++ b/frappe/desk/doctype/desktop_icon/desktop_icon.py @@ -7,7 +7,6 @@ import random import frappe from frappe import _ from frappe.model.document import Document -from frappe.utils.user import UserPermissions class DesktopIcon(Document): @@ -483,6 +482,8 @@ def get_module_icons(user=None): def get_user_icons(user): """Get user icons for module setup page""" + from frappe.utils.user import UserPermissions + user_perms = UserPermissions(user) user_perms.build_permissions() diff --git a/frappe/desk/doctype/workspace_sidebar/__init__.py b/frappe/desk/doctype/workspace_sidebar/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frappe/desk/doctype/workspace_sidebar/test_workspace_sidebar.py b/frappe/desk/doctype/workspace_sidebar/test_workspace_sidebar.py new file mode 100644 index 0000000000..4482200436 --- /dev/null +++ b/frappe/desk/doctype/workspace_sidebar/test_workspace_sidebar.py @@ -0,0 +1,20 @@ +# Copyright (c) 2025, Frappe Technologies and Contributors +# See license.txt + +# import frappe +from frappe.tests import IntegrationTestCase + +# On IntegrationTestCase, the doctype test records and all +# link-field test record dependencies are recursively loaded +# Use these module variables to add/remove to/from that list +EXTRA_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"] +IGNORE_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"] + + +class IntegrationTestWorkspaceSidebar(IntegrationTestCase): + """ + Integration tests for WorkspaceSidebar. + Use this class for testing interactions between multiple components. + """ + + pass diff --git a/frappe/desk/doctype/workspace_sidebar/workspace_sidebar.js b/frappe/desk/doctype/workspace_sidebar/workspace_sidebar.js new file mode 100644 index 0000000000..f964300fa9 --- /dev/null +++ b/frappe/desk/doctype/workspace_sidebar/workspace_sidebar.js @@ -0,0 +1,8 @@ +// Copyright (c) 2025, Frappe Technologies and contributors +// For license information, please see license.txt + +// frappe.ui.form.on("Workspace Sidebar", { +// refresh(frm) { + +// }, +// }); diff --git a/frappe/desk/doctype/workspace_sidebar/workspace_sidebar.json b/frappe/desk/doctype/workspace_sidebar/workspace_sidebar.json new file mode 100644 index 0000000000..bcbf495210 --- /dev/null +++ b/frappe/desk/doctype/workspace_sidebar/workspace_sidebar.json @@ -0,0 +1,62 @@ +{ + "actions": [], + "allow_rename": 1, + "autoname": "field:title", + "creation": "2025-08-12 12:06:45.016314", + "doctype": "DocType", + "engine": "InnoDB", + "field_order": [ + "desktop_icon", + "title", + "items" + ], + "fields": [ + { + "fieldname": "desktop_icon", + "fieldtype": "Link", + "label": "Desktop Icon", + "options": "Desktop Icon", + "unique": 1 + }, + { + "fetch_from": "desktop_icon.label", + "fieldname": "title", + "fieldtype": "Data", + "label": "Title", + "unique": 1 + }, + { + "fieldname": "items", + "fieldtype": "Table", + "label": "Items", + "options": "Workspace Sidebar Item" + } + ], + "grid_page_length": 50, + "index_web_pages_for_search": 1, + "links": [], + "modified": "2025-08-12 12:48:17.192321", + "modified_by": "Administrator", + "module": "Desk", + "name": "Workspace Sidebar", + "naming_rule": "By fieldname", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "row_format": "Dynamic", + "sort_field": "creation", + "sort_order": "DESC", + "states": [] +} diff --git a/frappe/desk/doctype/workspace_sidebar/workspace_sidebar.py b/frappe/desk/doctype/workspace_sidebar/workspace_sidebar.py new file mode 100644 index 0000000000..d57fe94d18 --- /dev/null +++ b/frappe/desk/doctype/workspace_sidebar/workspace_sidebar.py @@ -0,0 +1,23 @@ +# Copyright (c) 2025, Frappe Technologies and contributors +# For license information, please see license.txt + +# import frappe +from frappe.model.document import Document + + +class WorkspaceSidebar(Document): + # begin: auto-generated types + # This code is auto-generated. Do not modify anything in this block. + + from typing import TYPE_CHECKING + + if TYPE_CHECKING: + from frappe.desk.doctype.workspace_sidebar_item.workspace_sidebar_item import WorkspaceSidebarItem + from frappe.types import DF + + desktop_icon: DF.Link | None + items: DF.Table[WorkspaceSidebarItem] + title: DF.Data | None + # end: auto-generated types + + pass diff --git a/frappe/desk/doctype/workspace_sidebar_item/__init__.py b/frappe/desk/doctype/workspace_sidebar_item/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frappe/desk/doctype/workspace_sidebar_item/workspace_sidebar_item.json b/frappe/desk/doctype/workspace_sidebar_item/workspace_sidebar_item.json new file mode 100644 index 0000000000..6a0fb51580 --- /dev/null +++ b/frappe/desk/doctype/workspace_sidebar_item/workspace_sidebar_item.json @@ -0,0 +1,58 @@ +{ + "actions": [], + "allow_rename": 1, + "creation": "2025-08-12 12:46:41.926121", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "label", + "type", + "link_type", + "link_to" + ], + "fields": [ + { + "fieldname": "type", + "fieldtype": "Select", + "in_list_view": 1, + "label": "Type", + "options": "Link\nSection Break\nSpacer" + }, + { + "fieldname": "label", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Label" + }, + { + "depends_on": "eval: doc.type == 'Link'", + "fieldname": "link_type", + "fieldtype": "Select", + "in_list_view": 1, + "label": "Link Type", + "options": "Page\nDocType\nReport" + }, + { + "fieldname": "link_to", + "fieldtype": "Dynamic Link", + "in_list_view": 1, + "label": "Link To", + "options": "link_type" + } + ], + "grid_page_length": 50, + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2025-08-12 12:55:03.654000", + "modified_by": "Administrator", + "module": "Desk", + "name": "Workspace Sidebar Item", + "owner": "Administrator", + "permissions": [], + "row_format": "Dynamic", + "sort_field": "creation", + "sort_order": "DESC", + "states": [] +} diff --git a/frappe/desk/doctype/workspace_sidebar_item/workspace_sidebar_item.py b/frappe/desk/doctype/workspace_sidebar_item/workspace_sidebar_item.py new file mode 100644 index 0000000000..b6e7a58680 --- /dev/null +++ b/frappe/desk/doctype/workspace_sidebar_item/workspace_sidebar_item.py @@ -0,0 +1,26 @@ +# Copyright (c) 2025, Frappe Technologies and contributors +# For license information, please see license.txt + +# import frappe +from frappe.model.document import Document + + +class WorkspaceSidebarItem(Document): + # begin: auto-generated types + # This code is auto-generated. Do not modify anything in this block. + + from typing import TYPE_CHECKING + + if TYPE_CHECKING: + from frappe.types import DF + + label: DF.Data | None + link_to: DF.DynamicLink | None + link_type: DF.Literal["Page", "DocType", "Report"] + parent: DF.Data + parentfield: DF.Data + parenttype: DF.Data + type: DF.Literal["Link", "Section Break", "Spacer"] + # end: auto-generated types + + pass diff --git a/frappe/public/js/desk.bundle.js b/frappe/public/js/desk.bundle.js index cd7653fba3..e6cfc228e6 100644 --- a/frappe/public/js/desk.bundle.js +++ b/frappe/public/js/desk.bundle.js @@ -11,8 +11,8 @@ import "./frappe/ui/keyboard.js"; import "./frappe/ui/colors.js"; import "./frappe/ui/sidebar.html"; import "./frappe/ui/sidebar.js"; -import "./frappe/ui/apps_switcher.js"; -import "./frappe/ui/apps_switcher.html"; +import "./frappe/ui/sidebar_header.js"; +import "./frappe/ui/sidebar_header.html"; import "./frappe/ui/link_preview.js"; import "./frappe/request.js"; diff --git a/frappe/public/js/frappe/ui/sidebar.js b/frappe/public/js/frappe/ui/sidebar.js index 372072a76b..6c8d7fd08b 100644 --- a/frappe/public/js/frappe/ui/sidebar.js +++ b/frappe/public/js/frappe/ui/sidebar.js @@ -48,7 +48,7 @@ frappe.ui.Sidebar = class Sidebar { this.wrapper.find(".overlay").on("click", () => { this.close_sidebar(); }); - this.apps_switcher = new frappe.ui.AppsSwitcher(this); + this.apps_switcher = new frappe.ui.SidebarHeader(this); this.apps_switcher.create_app_data_map(); } diff --git a/frappe/public/js/frappe/ui/apps_switcher.html b/frappe/public/js/frappe/ui/sidebar_header.html similarity index 100% rename from frappe/public/js/frappe/ui/apps_switcher.html rename to frappe/public/js/frappe/ui/sidebar_header.html diff --git a/frappe/public/js/frappe/ui/apps_switcher.js b/frappe/public/js/frappe/ui/sidebar_header.js similarity index 98% rename from frappe/public/js/frappe/ui/apps_switcher.js rename to frappe/public/js/frappe/ui/sidebar_header.js index 643e47cd07..a4d567d4c6 100644 --- a/frappe/public/js/frappe/ui/apps_switcher.js +++ b/frappe/public/js/frappe/ui/sidebar_header.js @@ -1,4 +1,4 @@ -frappe.ui.AppsSwitcher = class AppsSwitcher { +frappe.ui.SidebarHeader = class SidebarHeader { constructor(sidebar) { this.sidebar = sidebar; this.sidebar_wrapper = $(this.sidebar.wrapper.find(".body-sidebar")); @@ -10,14 +10,13 @@ frappe.ui.AppsSwitcher = class AppsSwitcher { make() { this.wrapper = $( - frappe.render_template("apps_switcher", { + frappe.render_template("sidebar_header", { app_logo_url: frappe.boot.app_data[0].app_logo_url, app_title: __(frappe.boot.app_data[0].app_title), }) ).prependTo(this.sidebar_wrapper); this.app_switcher_dropdown = $(".app-switcher-dropdown"); } - setup_app_switcher() { this.app_switcher_menu = $(".app-switcher-menu"); $(".app-switcher-dropdown").on("click", (e) => { diff --git a/frappe/public/js/frappe/views/breadcrumbs.js b/frappe/public/js/frappe/views/breadcrumbs.js index e7ac3851d0..ad70e949f7 100644 --- a/frappe/public/js/frappe/views/breadcrumbs.js +++ b/frappe/public/js/frappe/views/breadcrumbs.js @@ -80,7 +80,7 @@ frappe.breadcrumbs = { frappe.workspace_map[breadcrumbs.workspace]?.app != frappe.current_app ) { let app = frappe.workspace_map[breadcrumbs.workspace].app; - frappe.app.sidebar.apps_switcher.set_current_app(app); + frappe.app.sidebar.sidebar_header.set_current_app(app); } this.toggle(true); From a51218f511b66616084c575c6f19f2aa3285a2fa Mon Sep 17 00:00:00 2001 From: sokumon Date: Mon, 18 Aug 2025 16:16:01 +0530 Subject: [PATCH 005/127] feat: create sidebar from workspace sidebar items --- frappe/boot.py | 31 ++- .../workspace_sidebar_item.json | 19 +- .../workspace_sidebar_item.py | 1 + frappe/public/js/desk.bundle.js | 1 + frappe/public/js/frappe/desk.js | 2 +- frappe/public/js/frappe/ui/sidebar.js | 187 ++++++++---------- .../public/js/frappe/ui/sidebar_header.html | 7 +- frappe/public/js/frappe/ui/sidebar_header.js | 17 +- frappe/public/js/frappe/ui/sidebar_item.html | 26 +++ frappe/public/js/frappe/views/breadcrumbs.js | 10 - .../js/frappe/views/workspace/workspace.js | 47 +++-- frappe/public/scss/desk/sidebar.scss | 4 +- 12 files changed, 198 insertions(+), 154 deletions(-) create mode 100644 frappe/public/js/frappe/ui/sidebar_item.html diff --git a/frappe/boot.py b/frappe/boot.py index 6d8781c550..c312ea6eb8 100644 --- a/frappe/boot.py +++ b/frappe/boot.py @@ -14,6 +14,7 @@ from frappe.core.doctype.installed_applications.installed_applications import ( ) from frappe.core.doctype.navbar_settings.navbar_settings import get_app_logo, get_navbar_settings from frappe.desk.doctype.changelog_feed.changelog_feed import get_changelog_feed_items +from frappe.desk.doctype.desktop_icon.desktop_icon import get_desktop_icons from frappe.desk.doctype.form_tour.form_tour import get_onboarding_ui_tours from frappe.desk.doctype.route_history.route_history import frequently_visited_links from frappe.desk.form.load import get_meta_bundle @@ -148,8 +149,11 @@ def load_conf_settings(bootinfo): def load_desktop_data(bootinfo): from frappe.desk.desktop import get_workspace_sidebar_items - bootinfo.sidebar_pages = get_workspace_sidebar_items() - allowed_pages = [d.name for d in bootinfo.sidebar_pages.get("pages")] + bootinfo.desktop_icons = get_desktop_icons() + bootinfo.workspaces = get_workspace_sidebar_items() + bootinfo.workspace_sidebar_item = get_sidebar_items() + print(bootinfo.workspace_sidebar_item) + allowed_pages = [d.name for d in bootinfo.workspaces.get("pages")] bootinfo.module_wise_workspaces = get_controller("Workspace").get_module_wise_workspaces() bootinfo.dashboards = frappe.get_all("Dashboard") bootinfo.app_data = [] @@ -518,3 +522,26 @@ def get_sentry_dsn(): return return os.getenv("FRAPPE_SENTRY_DSN") + + +def get_sidebar_items(): + sidebars = frappe.get_all("Workspace Sidebar", pluck="name") + sidebar_items = {} + + for s in sidebars: + w = frappe.get_doc("Workspace Sidebar", s) + desktop_icon = frappe.db.get_value("Desktop Icon", w.desktop_icon, "label").lower() + sidebar_items[desktop_icon] = [] + + for si in w.items: + workspace_sidebar = { + "label": si.label, + "link_to": si.link_to, + "link_type": si.link_type, + "type": si.type, + "icon": si.icon, + "child": si.child, + } + sidebar_items[desktop_icon].append(workspace_sidebar) + + return sidebar_items diff --git a/frappe/desk/doctype/workspace_sidebar_item/workspace_sidebar_item.json b/frappe/desk/doctype/workspace_sidebar_item/workspace_sidebar_item.json index 6a0fb51580..b301d77357 100644 --- a/frappe/desk/doctype/workspace_sidebar_item/workspace_sidebar_item.json +++ b/frappe/desk/doctype/workspace_sidebar_item/workspace_sidebar_item.json @@ -9,7 +9,9 @@ "label", "type", "link_type", - "link_to" + "link_to", + "icon", + "child" ], "fields": [ { @@ -39,13 +41,26 @@ "in_list_view": 1, "label": "Link To", "options": "link_type" + }, + { + "fieldname": "icon", + "fieldtype": "Icon", + "in_list_view": 1, + "label": "Icon" + }, + { + "default": "0", + "fieldname": "child", + "fieldtype": "Check", + "in_list_view": 1, + "label": "Child Item" } ], "grid_page_length": 50, "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2025-08-12 12:55:03.654000", + "modified": "2025-08-18 03:41:56.405534", "modified_by": "Administrator", "module": "Desk", "name": "Workspace Sidebar Item", diff --git a/frappe/desk/doctype/workspace_sidebar_item/workspace_sidebar_item.py b/frappe/desk/doctype/workspace_sidebar_item/workspace_sidebar_item.py index b6e7a58680..ba42715839 100644 --- a/frappe/desk/doctype/workspace_sidebar_item/workspace_sidebar_item.py +++ b/frappe/desk/doctype/workspace_sidebar_item/workspace_sidebar_item.py @@ -14,6 +14,7 @@ class WorkspaceSidebarItem(Document): if TYPE_CHECKING: from frappe.types import DF + child: DF.Check label: DF.Data | None link_to: DF.DynamicLink | None link_type: DF.Literal["Page", "DocType", "Report"] diff --git a/frappe/public/js/desk.bundle.js b/frappe/public/js/desk.bundle.js index e6cfc228e6..83b2942463 100644 --- a/frappe/public/js/desk.bundle.js +++ b/frappe/public/js/desk.bundle.js @@ -10,6 +10,7 @@ import "./frappe/ui/messages.js"; import "./frappe/ui/keyboard.js"; import "./frappe/ui/colors.js"; import "./frappe/ui/sidebar.html"; +import "./frappe/ui/sidebar_item.html"; import "./frappe/ui/sidebar.js"; import "./frappe/ui/sidebar_header.js"; import "./frappe/ui/sidebar_header.html"; diff --git a/frappe/public/js/frappe/desk.js b/frappe/public/js/frappe/desk.js index 9b5f095144..3e7bf70ccc 100644 --- a/frappe/public/js/frappe/desk.js +++ b/frappe/public/js/frappe/desk.js @@ -292,7 +292,7 @@ frappe.Application = class Application { setup_workspaces() { frappe.modules = {}; frappe.workspaces = {}; - frappe.boot.allowed_workspaces = frappe.boot.sidebar_pages.pages; + frappe.boot.allowed_workspaces = frappe.boot.workspaces.pages; for (let page of frappe.boot.allowed_workspaces || []) { frappe.modules[page.module] = page; diff --git a/frappe/public/js/frappe/ui/sidebar.js b/frappe/public/js/frappe/ui/sidebar.js index 6c8d7fd08b..b170388e07 100644 --- a/frappe/public/js/frappe/ui/sidebar.js +++ b/frappe/public/js/frappe/ui/sidebar.js @@ -3,14 +3,14 @@ frappe.ui.Sidebar = class Sidebar { this.items = {}; this.parent_items = []; this.sidebar_expanded = false; - + this.workspace_sidebar_items = []; if (!frappe.boot.setup_complete) { // no sidebar if setup is not complete return; } this.set_all_pages(); - this.make_dom(); + // this.make_dom(); this.sidebar_items = { public: {}, private: {}, @@ -31,12 +31,14 @@ frappe.ui.Sidebar = class Sidebar { ]; this.setup_pages(); - this.apps_switcher.populate_apps_menu(); this.handle_outside_click(); } - + setup(workspace_title) { + this.make_dom(); + this.apps_switcher = new frappe.ui.SidebarHeader(this, workspace_title); + this.make_sidebar(workspace_title.toLowerCase()); + } make_dom() { - this.set_default_app(); this.wrapper = $(frappe.render_template("sidebar")).prependTo("body"); this.$sidebar = this.wrapper.find(".sidebar-items"); @@ -64,10 +66,7 @@ frappe.ui.Sidebar = class Sidebar { } set_all_pages() { - this.sidebar_pages = frappe.boot.sidebar_pages; - this.all_pages = this.sidebar_pages.pages; - this.has_access = this.sidebar_pages.has_access; - this.has_create_access = this.sidebar_pages.has_create_access; + this.sidebar_items = frappe.boot.workspace_sidebar_item; } set_default_app() { @@ -78,21 +77,7 @@ frappe.ui.Sidebar = class Sidebar { } set_active_workspace_item() { - if (!frappe.get_route()) return; - let current_route = frappe.get_route(); - let current_route_str = frappe.get_route_str(); - let current_item; - if (current_route[0] == "Workspaces") { - current_item = current_route[1]; - } else if (frappe.breadcrumbs) { - if (Object.keys(frappe.breadcrumbs.all).length == 0) return; - if (frappe.breadcrumbs.all[current_route_str]) { - current_item = - frappe.breadcrumbs.all[current_route_str].workspace || - frappe.breadcrumbs.all[current_route_str].module; - } - } - if (this.is_route_in_sidebar(current_item)) { + if (this.is_route_in_sidebar()) { this.active_item.addClass("active-sidebar"); } if (this.active_item) { @@ -135,11 +120,12 @@ frappe.ui.Sidebar = class Sidebar { }); return sidebar_item; } + is_route_in_sidebar(active_module) { let match = false; const that = this; $(".item-anchor").each(function () { - if ($(this).attr("title") == active_module) { + if ($(this).attr("href") == window.location.pathname) { match = true; if (that.active_item) that.active_item.removeClass("active-sidebar"); that.active_item = $(this).parent(); @@ -152,13 +138,6 @@ frappe.ui.Sidebar = class Sidebar { setup_pages() { this.set_all_pages(); - this.all_pages.forEach((page) => { - page.is_editable = !page.public || this.has_access; - if (typeof page.content == "string") { - page.content = JSON.parse(page.content); - } - }); - if (this.all_pages) { frappe.workspaces = {}; frappe.workspace_list = []; @@ -176,8 +155,8 @@ frappe.ui.Sidebar = class Sidebar { } this.make_sidebar(); } - this.set_hover(); - this.set_sidebar_state(); + // this.set_hover(); + // this.set_sidebar_state(); } set_sidebar_state() { this.sidebar_expanded = true; @@ -189,21 +168,31 @@ frappe.ui.Sidebar = class Sidebar { } this.expand_sidebar(); } - make_sidebar() { + make_sidebar(workspace_title) { if (this.wrapper.find(".standard-sidebar-section")[0]) { 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.name); - if (frappe.current_app === "private") { - parent_pages = parent_pages.filter((p) => !p.public); + this.workspace_sidebar_items = frappe.boot.workspace_sidebar_item[workspace_title]; + let parent_pages = this.workspace_sidebar_items; + if (this.workspace_sidebar_items && this.workspace_sidebar_items.length > 0) { + this.workspace_sidebar_items.unshift({ + label: "Home", + icon: "home", + type: "Workspace", + route: `/app/${workspace_title}`, + }); } else { - parent_pages = parent_pages.filter((p) => p.public && app_workspaces.includes(p.name)); + this.workspace_sidebar_items = []; + this.workspace_sidebar_items[0] = { + label: "Home", + icon: "home", + type: "Workspace", + route: `/app/${workspace_title}`, + }; } - this.build_sidebar_section("All", parent_pages); + // this.build_sidebar_section("All", parent_pages); + this.create_sidebar(); // Scroll sidebar to selected page if it is not in viewport. this.wrapper.find(".selected").length && @@ -213,8 +202,21 @@ frappe.ui.Sidebar = class Sidebar { this.setup_sorting(); this.set_active_workspace_item(); this.set_hover(); + this.set_sidebar_state(); + } + create_sidebar() { + if (this.workspace_sidebar_items && this.workspace_sidebar_items.length > 0) { + let parent_links = this.workspace_sidebar_items.filter((f) => f.child !== 1); + parent_links.forEach((w) => { + this.append_item(w, this.wrapper.find(".sidebar-items")); + }); + } else { + let no_items_message = $( + "
No Sidebar Items
" + ); + this.wrapper.find(".sidebar-items").append(no_items_message); + } } - build_sidebar_section(title, root_pages) { let sidebar_section = $( `
` @@ -251,13 +253,14 @@ frappe.ui.Sidebar = class Sidebar { } // visibility not explicitly set to 0 - if (item.visibility !== 0) { + if (item.child !== 0) { this.append_item(item, child_container); } last_item = item; } child_container.appendTo(item_container); } + toggle_sidebar() { if (!this.sidebar_expanded) { this.open_sidebar(); @@ -265,6 +268,7 @@ frappe.ui.Sidebar = class Sidebar { this.close_sidebar(); } } + expand_sidebar() { let direction; if (this.sidebar_expanded) { @@ -295,28 +299,28 @@ frappe.ui.Sidebar = class Sidebar { let $item_container = this.sidebar_item_container(item); let sidebar_control = $item_container.find(".sidebar-item-control"); - let child_items = this.all_pages.filter( - (page) => page.parent_page == item.name || page.parent_page == item.title - ); - if (child_items.length > 0) { - let child_container = $item_container.find(".sidebar-child-item"); - child_container.addClass("hidden"); - this.prepare_sidebar(child_items, child_container, $item_container); - this.parent_items.push($item_container); + if (item.type == "Section Break") { + let current_index = this.workspace_sidebar_items.indexOf(item); + let child_items = this.workspace_sidebar_items + .slice(current_index) + .filter((page) => page.child == 1); + if (child_items.length > 0) { + let child_container = $item_container.find(".sidebar-child-item"); + child_container.addClass("hidden"); + this.prepare_sidebar(child_items, child_container, $item_container); + this.parent_items.push($item_container); + $item_container.find(".drop-icon").first().addClass("show-in-edit-mode"); + } } $item_container.appendTo(container); - this.sidebar_items[item.public ? "public" : "private"][item.name] = $item_container; + // this.sidebar_items[item.public ? "public" : "private"][item.name] = $item_container; - if ($item_container.parent().hasClass("hidden") && is_current_page) { + if ($item_container.parent().hasClass("hidden")) { $item_container.parent().toggleClass("hidden"); } this.add_toggle_children(item, sidebar_control, $item_container); - - if (child_items.length > 0) { - $item_container.find(".drop-icon").first().addClass("show-in-edit-mode"); - } } sidebar_item_container(item) { @@ -336,66 +340,32 @@ frappe.ui.Sidebar = class Sidebar { } } else if (item.type === "URL") { path = item.external_link; - } else { - if (item.public) { - path = "/app/" + frappe.router.slug(item.name); - } else { - path = "/app/private/" + frappe.router.slug(item.name.split("-")[0]); + } else if (item.type == "Workspace") { + path = "/app/" + frappe.router.slug(item.label); + if (item.route) { + path = item.route; } } - - return $(` - - `); + console.log(item); + return $( + frappe.render_template("sidebar_item", { + item: item, + path: path, + }) + ); } add_toggle_children(item, sidebar_control, item_container) { - let drop_icon = "es-line-down"; - if ( - this.current_page && - item_container.find(`[item-name="${this.current_page.name}"]`).length - ) { + let $child_item_section = item_container.find(".sidebar-child-item"); + let drop_icon = "es-line-up"; + if ($child_item_section.children() > 0) { drop_icon = "small-up"; } - - let $child_item_section = item_container.find(".sidebar-child-item"); let $drop_icon = $(` + + + \ No newline at end of file diff --git a/frappe/public/js/frappe/ui/sidebar/sidebar_header.js b/frappe/public/js/frappe/ui/sidebar/sidebar_header.js new file mode 100644 index 0000000000..26843e5109 --- /dev/null +++ b/frappe/public/js/frappe/ui/sidebar/sidebar_header.js @@ -0,0 +1,151 @@ +frappe.ui.SidebarHeader = class SidebarHeader { + constructor(sidebar, workspace_title) { + this.sidebar = sidebar; + this.sidebar_wrapper = $(".body-sidebar"); + this.drop_down_expanded = false; + this.workspace_title = workspace_title; + const me = this; + this.dropdown_items = [ + { + name: "desktop", + label: __("Desktop"), + icon: "layout-grid", + onClick: function (el) { + frappe.set_route("desktop"); + }, + }, + { + name: "edit-sidebar", + label: __("Edit Sidebar"), + icon: "edit", + onClick: function () { + if ( + Object.keys(frappe.boot.workspace_sidebar_item).includes( + me.workspace_title.toLowerCase() + ) + ) { + frappe.set_route("Form", "Workspace Sidebar", me.workspace_title); + } else { + frappe.set_route("List", "Workspace Sidebar"); + } + }, + }, + { + name: "website", + label: __("Website"), + route: "/", + icon_url: "/assets/frappe/images/web.svg", + }, + ]; + this.make(); + this.setup_app_switcher(); + this.populate_apps_menu(); + this.setup_select_options(); + } + + make() { + $( + frappe.render_template("sidebar_header", { + workspace_title: this.workspace_title, + }) + ).prependTo(this.sidebar_wrapper); + this.wrapper = $(".sidebar-header"); + this.dropdown_menu = this.wrapper.find(".sidebar-header-menu"); + this.$header_title = this.wrapper.find(".header-title"); + this.$drop_icon = this.wrapper.find(".drop-icon"); + } + + setup_app_switcher() { + this.dropdown_menu = $(".sidebar-header-menu"); + $(".sidebar-header").on("click", (e) => { + this.toggle_app_menu(); + e.stopImmediatePropagation(); + }); + } + + toggle_app_menu() { + this.toggle_active(); + this.dropdown_menu.toggleClass("hidden"); + } + + populate_apps_menu() { + const me = this; + this.dropdown_items.forEach((d) => { + me.add_app_item(d); + }); + } + + add_app_item(item) { + $(``).appendTo(this.dropdown_menu); + } + + setup_select_options() { + this.dropdown_menu.find(".dropdown-menu-item").on("click", (e) => { + let item = $(e.delegateTarget); + let name = item.attr("data-name"); + let current_item = this.dropdown_items.find((f) => f.name == name); + this.dropdown_menu.toggleClass("hidden"); + this.toggle_active(); + if (current_item.route) [window.open(current_item.route)]; + current_item.onClick(item); + }); + } + + toggle_active() { + this.toggle_dropdown(); + this.wrapper.toggleClass("active-sidebar"); + if (!this.sidebar.sidebar_expanded) { + this.wrapper.removeClass("active-sidebar"); + } + } + + toggle_dropdown() { + if (this.drop_down_expanded) { + this.drop_down_expanded = false; + } else { + this.drop_down_expanded = true; + } + } + + setup_hover() { + $(".sidebar-header").on("mouseover", function (event) { + if ($(this).parent().hasClass("active-sidebar")) return; + $(this).addClass("hover"); + }); + + $(".sidebar-header").on("mouseleave", function () { + $(this).removeClass("hover"); + }); + } + + toggle_width(expand) { + let class_name = "collapse-header"; + if (!expand) { + $(this.wrapper[0]).css("width", "auto"); + this.$drop_icon.addClass(class_name); + this.$header_title.addClass(class_name); + $(this.wrapper[0]).off("mouseleave"); + $(this.wrapper[0]).off("mouseover"); + } else { + $(this.wrapper[0]).css("width", "100%"); + this.$drop_icon.removeClass(class_name); + this.$header_title.removeClass(class_name); + this.setup_hover(); + } + } +}; diff --git a/frappe/public/js/frappe/ui/sidebar_item.html b/frappe/public/js/frappe/ui/sidebar/sidebar_item.html similarity index 100% rename from frappe/public/js/frappe/ui/sidebar_item.html rename to frappe/public/js/frappe/ui/sidebar/sidebar_item.html diff --git a/frappe/public/js/frappe/ui/sidebar_header.html b/frappe/public/js/frappe/ui/sidebar_header.html deleted file mode 100644 index 6aba065bab..0000000000 --- a/frappe/public/js/frappe/ui/sidebar_header.html +++ /dev/null @@ -1,22 +0,0 @@ - -
-
- - -
- -
-
- - \ No newline at end of file diff --git a/frappe/public/js/frappe/ui/sidebar_header.js b/frappe/public/js/frappe/ui/sidebar_header.js deleted file mode 100644 index 6043df7c4e..0000000000 --- a/frappe/public/js/frappe/ui/sidebar_header.js +++ /dev/null @@ -1,227 +0,0 @@ -frappe.ui.SidebarHeader = class SidebarHeader { - constructor(sidebar, workspace_title) { - this.sidebar = sidebar; - this.sidebar_wrapper = $(this.sidebar.wrapper.find(".body-sidebar")); - this.drop_down_expanded = false; - this.workspace_title = workspace_title; - - this.make(); - this.setup_app_switcher(); - this.set_hover(); - this.populate_apps_menu(); - this.setup_select_app(); - } - - make() { - this.desktop_icon = this.get_desktop_icon(); - this.wrapper = $( - frappe.render_template("sidebar_header", { - workspace_title: this.workspace_title, - icon: frappe.utils.icon(this.desktop_icon.icon, "lg"), - }) - ).prependTo(this.sidebar_wrapper); - this.app_switcher_dropdown = $(".app-switcher-dropdown"); - } - get_desktop_icon() { - return frappe.boot.desktop_icons.filter((f) => f.label == this.workspace_title)[0]; - } - setup_app_switcher() { - this.app_switcher_menu = $(".app-switcher-menu"); - $(".app-switcher-dropdown").on("click", (e) => { - this.toggle_app_menu(); - e.stopImmediatePropagation(); - }); - } - toggle_app_menu() { - this.toggle_active(); - this.app_switcher_menu.toggleClass("hidden"); - } - create_app_data_map() { - 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) { - this.add_app_item(app); - } - } - } - populate_apps_menu() { - this.add_desktop(); - this.add_edit_sidebar(); - this.add_website_select(); - } - - add_app_item(app) { - $(``).appendTo(this.app_switcher_menu); - } - - add_private_app() { - let private_pages = this.sidebar.all_pages.filter((p) => p.public === 0); - if (private_pages.length === 0) return; - - const app = { - app_name: "private", - app_title: __("My Workspaces"), - app_route: "/app/private", - app_logo_url: "/assets/frappe/images/frappe-framework-logo.svg", - workspaces: private_pages, - }; - - frappe.boot.app_data_map["private"] = app; - $(`
`).prependTo(this.app_switcher_menu); - $(``).prependTo(this.app_switcher_menu); - } - - setup_select_app() { - this.app_switcher_menu.find(".app-item").on("click", (e) => { - let item = $(e.delegateTarget); - let route = item.attr("data-app-route"); - this.app_switcher_menu.toggleClass("hidden"); - this.toggle_active(); - - if (item.attr("data-app-name") == "desktop") { - frappe.set_route("desktop"); - return; - } - if (item.attr("data-app-name") == "settings") { - frappe.quick_edit("Workspace Settings"); - return; - } - if (item.attr("data-app-name") == "edit-sidebar") { - frappe.set_route("Form", "Workspace Sidebar", this.workspace_title); - return; - } - if (route.startsWith("/app/private")) { - this.set_current_app("private"); - let ws = Object.values(frappe.workspace_map).find((ws) => ws.public === 0); - route += "/" + frappe.router.slug(ws.title); - frappe.set_route(route); - } else if (route.startsWith("/app")) { - frappe.set_route(route); - this.set_current_app(item.attr("data-app-name")); - } else { - // new page - window.open(route); - } - }); - } - // refactor them into one single function - add_website_select() { - this.add_app_item( - { - app_name: "website", - app_title: __("Website"), - app_route: "/", - app_logo_url: "/assets/frappe/images/web.svg", - }, - this.app_switcher_menu - ); - } - - add_settings_select() { - $(`
`).appendTo(this.app_switcher_menu); - this.add_app_item({ - app_name: "settings", - app_title: __("Settings"), - app_logo_url: "/assets/frappe/images/settings-gear.svg", - }); - } - add_edit_sidebar() { - if (frappe.boot.workspaces.has_create_access) { - this.add_app_item({ - app_name: "edit-sidebar", - app_title: __("Edit Sidebar"), - app_logo_icon: "edit", - }); - } - } - add_desktop() { - this.add_app_item({ - app_name: "desktop", - app_title: __("Desktop"), - app_logo_icon: "layout-grid", - }); - } - set_current_app(app) { - if (!app) { - console.warn("set_current_app: app not defined"); - return; - } - let app_data = frappe.boot.app_data_map[app] || frappe.boot.app_data_map["frappe"]; - - this.sidebar_wrapper - .find(".app-switcher-dropdown .sidebar-item-icon img") - .attr("src", app_data.app_logo_url); - this.sidebar_wrapper - .find(".app-switcher-dropdown .sidebar-item-label") - .html(app_data.app_title); - - frappe.frappe_toolbar.set_app_logo(app_data.app_logo_url); - - if (frappe.current_app === app) return; - frappe.current_app = app; - - // re-render the sidebar - frappe.app.sidebar.make_sidebar(); - } - - set_hover() { - const me = this; - - this.app_switcher_dropdown.on("mouseover", function () { - if ($(this).hasClass("active-sidebar")) return; - $(this).addClass("hover"); - - if (!me.sidebar.sidebar_expanded) { - $(this).removeClass("hover"); - } - }); - - this.app_switcher_dropdown.on("mouseleave", function () { - $(this).removeClass("hover"); - }); - } - - toggle_active() { - this.toggle_dropdown(); - this.app_switcher_dropdown.toggleClass("active-sidebar"); - if (!this.sidebar.sidebar_expanded) { - this.app_switcher_dropdown.removeClass("active-sidebar"); - } - } - toggle_dropdown() { - if (this.drop_down_expanded) { - this.drop_down_expanded = false; - } else { - this.drop_down_expanded = true; - } - } -}; diff --git a/frappe/public/scss/desk/index.scss b/frappe/public/scss/desk/index.scss index 48622f3f6f..c6a533a8ff 100644 --- a/frappe/public/scss/desk/index.scss +++ b/frappe/public/scss/desk/index.scss @@ -51,3 +51,4 @@ @import "../common/quill"; @import "plyr"; @import "version"; +@import "sidebar_header"; diff --git a/frappe/public/scss/desk/sidebar.scss b/frappe/public/scss/desk/sidebar.scss index 4ca47efad9..d7e090bd88 100644 --- a/frappe/public/scss/desk/sidebar.scss +++ b/frappe/public/scss/desk/sidebar.scss @@ -115,14 +115,6 @@ body { // position: relative; // top:10px; } - .app-title { - font-weight: 500; - line-height: 16.1px; - } - .app-logo { - width: 32px; - height: 32px; - } .divider { margin: var(--margin-xs) 0; @@ -244,11 +236,6 @@ body { // make it an overlay on hover position: absolute; width: var(--left-sidebar-width); - .app-switcher-dropdown { - width: 100%; - left: 0px; - padding: 3px; - } .body-sidebar-top { width: 100%; overflow-y: hidden; @@ -328,72 +315,6 @@ body { } } -// Sidebar Header -.app-switcher-dropdown { - position: relative; - text-decoration: none; - width: 38px; - height: 38px; - padding: 3px; - margin-left: -2px; - .standard-sidebar-item { - padding-top: 1px; - padding-bottom: 1px; - .d-flex { - width: 161px; - } - gap: 30px; - } - .sidebar-item-control { - margin: 2px; - margin-right: 0px; - } -} - -.app-switcher-menu { - position: absolute; - top: 50px; - left: 9px; - width: 208px; - padding: 6px; - border-radius: var(--border-radius-lg); - background: var(--surface-modal); - box-shadow: var(--shadow-xl); - z-index: 1; -} - -.app-item { - // padding: var(--padding-xs); - border-radius: var(--border-radius-tiny); - opacity: 0px; - &:hover { - background-color: var(--sidebar-hover-color); - } - - a { - width: 208px; - height: 28px; - padding: 6px 8px 6px 8px; - gap: 8px; - text-decoration: none; - display: flex; - align-items: center; - gap: var(--margin-sm); - .sidebar-item-icon { - line-height: 0px; - .app-logo { - width: 16px; - height: 16px; - } - } - } - - .app-item-title { - text-overflow: ellipsis; - text-wrap: nowrap; - overflow: hidden; - } -} // sidebar-item states @mixin hover-mixin { background-color: var(--sidebar-hover-color); diff --git a/frappe/public/scss/desk/sidebar_header.scss b/frappe/public/scss/desk/sidebar_header.scss new file mode 100644 index 0000000000..4ac56f8667 --- /dev/null +++ b/frappe/public/scss/desk/sidebar_header.scss @@ -0,0 +1,77 @@ +// Sidebar Header +.sidebar-header { + width: 100%; + position: relative; + height: 38px; + padding: 3px 0px 3px 3px; + display: flex; + align-items: center; + button { + padding: 6px; + margin-left: 12px; + } +} + +.header-logo > * { + background-color: var(--gray-200); + color: var(--gray-500); + border-radius: 20% !important; +} + +.title-container { + flex: 1 1 0%; + margin-left: 8px; +} +.header-title { + font-weight: 500; +} +.collapse-header { + overflow: hidden; + width: 0px; + margin-left: 0px !important; + opacity: 0; +} +.sidebar-header-menu { + position: absolute; + top: 50px; + left: 9px; + width: 208px; + padding: 6px; + border-radius: var(--border-radius-lg); + background: var(--surface-modal); + box-shadow: var(--shadow-xl); + z-index: 1; +} + +.dropdown-menu-item { + // padding: var(--padding-xs); + border-radius: var(--border-radius-tiny); + opacity: 0px; + &:hover { + background-color: var(--sidebar-hover-color); + } + + a { + width: 208px; + height: 28px; + padding: 6px 8px 6px 8px; + gap: 8px; + text-decoration: none; + display: flex; + align-items: center; + gap: var(--margin-sm); + .sidebar-item-icon { + line-height: 0px; + .app-logo { + width: 16px; + height: 16px; + } + } + } + + .menu-item-title { + text-overflow: ellipsis; + text-wrap: nowrap; + overflow: hidden; + } +} From 6bf7d45e6ef1a480f99bdf4e72066b154208dc18 Mon Sep 17 00:00:00 2001 From: sokumon Date: Tue, 2 Sep 2025 14:06:07 +0530 Subject: [PATCH 018/127] refactor: Sidebar --- .../workspace_sidebar_item.json | 5 +- .../workspace_sidebar_item.py | 2 +- frappe/public/js/frappe/router.js | 3 +- .../public/js/frappe/ui/sidebar/sidebar.html | 5 +- frappe/public/js/frappe/ui/sidebar/sidebar.js | 181 ++++++------- .../js/frappe/ui/sidebar/sidebar_header.html | 25 +- .../js/frappe/ui/sidebar/sidebar_header.js | 24 +- .../js/frappe/ui/sidebar/sidebar_item.html | 17 +- frappe/public/js/frappe/utils/utils.js | 13 +- frappe/public/scss/common/icons.scss | 4 + frappe/public/scss/desk/form_sidebar.scss | 5 +- frappe/public/scss/desk/index.scss | 1 + frappe/public/scss/desk/main.scss | 44 ++++ frappe/public/scss/desk/sidebar.scss | 240 ++++++------------ frappe/public/scss/desk/sidebar_header.scss | 25 +- frappe/public/scss/espresso/_typography.scss | 6 + 16 files changed, 283 insertions(+), 317 deletions(-) create mode 100644 frappe/public/scss/desk/main.scss diff --git a/frappe/desk/doctype/workspace_sidebar_item/workspace_sidebar_item.json b/frappe/desk/doctype/workspace_sidebar_item/workspace_sidebar_item.json index b301d77357..3c72ae30f5 100644 --- a/frappe/desk/doctype/workspace_sidebar_item/workspace_sidebar_item.json +++ b/frappe/desk/doctype/workspace_sidebar_item/workspace_sidebar_item.json @@ -33,7 +33,7 @@ "fieldtype": "Select", "in_list_view": 1, "label": "Link Type", - "options": "Page\nDocType\nReport" + "options": "DocType\nPage\nReport\nWorkspace" }, { "fieldname": "link_to", @@ -43,6 +43,7 @@ "options": "link_type" }, { + "default": "list-alt", "fieldname": "icon", "fieldtype": "Icon", "in_list_view": 1, @@ -60,7 +61,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2025-08-18 03:41:56.405534", + "modified": "2025-08-29 12:49:51.155661", "modified_by": "Administrator", "module": "Desk", "name": "Workspace Sidebar Item", diff --git a/frappe/desk/doctype/workspace_sidebar_item/workspace_sidebar_item.py b/frappe/desk/doctype/workspace_sidebar_item/workspace_sidebar_item.py index ba42715839..af0a14a898 100644 --- a/frappe/desk/doctype/workspace_sidebar_item/workspace_sidebar_item.py +++ b/frappe/desk/doctype/workspace_sidebar_item/workspace_sidebar_item.py @@ -17,7 +17,7 @@ class WorkspaceSidebarItem(Document): child: DF.Check label: DF.Data | None link_to: DF.DynamicLink | None - link_type: DF.Literal["Page", "DocType", "Report"] + link_type: DF.Literal["DocType", "Page", "Report", "Workspace"] parent: DF.Data parentfield: DF.Data parenttype: DF.Data diff --git a/frappe/public/js/frappe/router.js b/frappe/public/js/frappe/router.js index f69fad3bbf..97967268e8 100644 --- a/frappe/public/js/frappe/router.js +++ b/frappe/public/js/frappe/router.js @@ -148,7 +148,7 @@ frappe.router = { this.set_history(sub_path); this.render(); this.set_title(sub_path); - this.trigger("change"); + this.trigger("change", this); }, async parse(route) { @@ -202,7 +202,6 @@ frappe.router = { return frappe.model.with_doctype(doctype_route.doctype).then(() => { // doctype route let meta = frappe.get_meta(doctype_route.doctype); - if (route[1] && route[1] === "view" && route[2]) { route = this.get_standard_route_for_list( route, diff --git a/frappe/public/js/frappe/ui/sidebar/sidebar.html b/frappe/public/js/frappe/ui/sidebar/sidebar.html index 1f427d438f..cf67b404a1 100644 --- a/frappe/public/js/frappe/ui/sidebar/sidebar.html +++ b/frappe/public/js/frappe/ui/sidebar/sidebar.html @@ -7,8 +7,9 @@
- - {%= __("Collapse") %} + {%= frappe.utils.icon("arrow-left-to-line" , "sm", "", "", "text-ink-gray-7 current-color", true)%} + {%= __("Collapse") %} +
diff --git a/frappe/public/js/frappe/ui/sidebar/sidebar.js b/frappe/public/js/frappe/ui/sidebar/sidebar.js index 5481d354c0..be6f430d14 100644 --- a/frappe/public/js/frappe/ui/sidebar/sidebar.js +++ b/frappe/public/js/frappe/ui/sidebar/sidebar.js @@ -1,10 +1,10 @@ frappe.ui.Sidebar = class Sidebar { constructor() { this.items = {}; - this.parent_items = []; + this.section_breaks = []; + this.section_breaks_content = []; this.sidebar_expanded = false; this.workspace_sidebar_items = []; - this.setup_complete = false; if (!frappe.boot.setup_complete) { // no sidebar if setup is not complete return; @@ -35,15 +35,16 @@ frappe.ui.Sidebar = class Sidebar { } setup(workspace_title) { - if (!this.setup_complete) { - this.workspace_title = workspace_title; - this.apps_switcher = new frappe.ui.SidebarHeader(this, workspace_title); - this.make_sidebar(workspace_title.toLowerCase()); - this.setup_complete = true; - } + this.workspace_title = workspace_title; + this.sidebar_header = new frappe.ui.SidebarHeader(this, workspace_title); + this.make_sidebar(workspace_title.toLowerCase()); + this.setup_complete = true; } setup_events() { const me = this; + frappe.router.on("change", function (router) { + frappe.app.sidebar.set_workspace_sidebar(); + }); $(document).on("page-change", function () { frappe.app.sidebar.toggle(); }); @@ -57,7 +58,6 @@ frappe.ui.Sidebar = class Sidebar { if (frappe.container.page.page.hide_sidebar) { this.wrapper.hide(); } else { - frappe.app.sidebar.set_workspace_sidebar(); this.wrapper.show(); } } @@ -76,14 +76,20 @@ frappe.ui.Sidebar = class Sidebar { } set_hover() { - $(".standard-sidebar-item > .item-anchor").on("mouseover", function (event) { - if ($(this).parent().hasClass("active-sidebar")) return; - $(this).parent().addClass("hover"); - }); + $(".standard-sidebar-item > .item-anchor:not(.section-break)").on( + "mouseover", + function (event) { + if ($(this).parent().hasClass("active-sidebar")) return; + $(this).parent().addClass("hover"); + } + ); - $(".standard-sidebar-item > .item-anchor").on("mouseleave", function () { - $(this).parent().removeClass("hover"); - }); + $(".standard-sidebar-item > .item-anchor:not(.section-break)").on( + "mouseleave", + function () { + $(this).parent().removeClass("hover"); + } + ); } set_all_pages() { @@ -101,49 +107,29 @@ frappe.ui.Sidebar = class Sidebar { if (this.is_route_in_sidebar()) { this.active_item.addClass("active-sidebar"); } - if (this.active_item) { - if (this.is_nested_item(this.active_item.parent())) { - let current_item = this.active_item.parent(); - this.expand_parent_item(current_item); - } - } - if (!this.sidebar_expanded) this.close_children_item(); } - - expand_parent_item(item) { - let parent_title = item.attr("item-parent"); - if (!parent_title) return; - - let parent = this.get_sidebar_item(parent_title); - if (parent) { - let $drop_icon = $(parent).find(".drop-icon"); - if ($($(parent).children()[1]).hasClass("hidden")) { - $drop_icon[0].click(); - if (this.is_nested_item($(parent))) { - this.expand_parent_item($(parent)); - } - } - } - } - is_nested_item(item) { - if (item.attr("item-parent")) { - return true; - } else { - return false; - } - } - - get_sidebar_item(name) { - let sidebar_item = ""; - $(".sidebar-item-container").each(function () { - if ($(this).attr("item-name") == name) { - sidebar_item = this; + toggle_section_break() { + this.section_breaks.forEach((f, i) => { + $(f[0]).html(""); + if (this.sidebar_expanded) { + $(f[0]).html(this.section_breaks_content[i]); + this.setup_event_listner($($(f[0]).parent())); + } else { + $(f[0]).html("
"); } }); - return sidebar_item; } - is_route_in_sidebar(active_module) { + open_all_section_breaks() { + this.section_breaks.forEach((f) => { + f.find(".drop-icon").click(); + if (f.find(".nested-container").hasClass("hidden")) { + f.find(".drop-icon").click(); + } + }); + } + + is_route_in_sidebar() { let match = false; const that = this; $(".item-anchor").each(function () { @@ -189,29 +175,13 @@ frappe.ui.Sidebar = class Sidebar { this.sidebar_expanded = false; } this.expand_sidebar(); - this.apps_switcher; + this.sidebar_header; } make_sidebar(workspace_title) { - if (this.wrapper.find(".standard-sidebar-section")[0]) { - this.wrapper.find(".standard-sidebar-section").remove(); + if (this.wrapper.find(".sidebar-items")[0]) { + this.wrapper.find(".sidebar-items").html(""); } this.workspace_sidebar_items = frappe.boot.workspace_sidebar_item[workspace_title]; - if (this.workspace_sidebar_items && this.workspace_sidebar_items.length > 0) { - this.workspace_sidebar_items.unshift({ - label: "Home", - icon: "home", - type: "Workspace", - route: `/app/${workspace_title}`, - }); - } else { - this.workspace_sidebar_items = []; - this.workspace_sidebar_items[0] = { - label: "Home", - icon: "home", - type: "Workspace", - route: `/app/${workspace_title}`, - }; - } // this.build_sidebar_section("All", parent_pages); this.create_sidebar(); @@ -306,7 +276,9 @@ frappe.ui.Sidebar = class Sidebar { .find(".body-sidebar .collapse-sidebar-link") .find("use") .attr("href", `#icon-arrow-${direction}-to-line`); - this.apps_switcher.toggle_width(this.sidebar_expanded); + this.sidebar_header.toggle_width(this.sidebar_expanded); + this.open_all_section_breaks(); + this.toggle_section_break(); } append_item(item, container) { @@ -335,19 +307,18 @@ frappe.ui.Sidebar = class Sidebar { let child_container = $item_container.find(".sidebar-child-item"); child_container.addClass("hidden"); this.prepare_sidebar(child_items, child_container, $item_container); - this.parent_items.push($item_container); + this.section_breaks.push($item_container.find(".standard-sidebar-item")); + $item_container.find(".drop-icon").first().addClass("show-in-edit-mode"); + this.add_toggle_children(item, sidebar_control, $item_container); + this.section_breaks_content.push( + $($item_container.find(".standard-sidebar-item")[0]).html() + ); } } $item_container.appendTo(container); // this.sidebar_items[item.public ? "public" : "private"][item.name] = $item_container; - - if ($item_container.parent().hasClass("hidden")) { - $item_container.parent().toggleClass("hidden"); - } - - this.add_toggle_children(item, sidebar_control, $item_container); } sidebar_item_container(item) { @@ -359,19 +330,23 @@ frappe.ui.Sidebar = class Sidebar { path = frappe.utils.generate_route({ type: item.link_type, name: item.link_to, - is_query_report: item.report.report_type === "Query Report", + is_query_report: + item.report.report_type === "Query Report" || + item.report.report_type == "Script Report", report_ref_doctype: item.report.ref_doctype, }); + } else if (item.link_type == "Workspace") { + let label = item.label; + if (label == "Home") label = this.workspace_title.toLowerCase(); + path = "/app/" + frappe.router.slug(label); + if (item.route) { + path = item.route; + } } else { path = frappe.utils.generate_route({ type: item.link_type, name: item.link_to }); } } else if (item.type === "URL") { path = item.external_link; - } else if (item.type == "Workspace") { - path = "/app/" + frappe.router.slug(item.label); - if (item.route) { - path = item.route; - } } return $( frappe.render_template("sidebar_item", { @@ -383,7 +358,7 @@ frappe.ui.Sidebar = class Sidebar { add_toggle_children(item, sidebar_control, item_container) { let $child_item_section = item_container.find(".sidebar-child-item"); - let drop_icon = "es-line-up"; + let drop_icon = "chevron-right"; if ($child_item_section.children() > 0) { drop_icon = "small-up"; } @@ -394,13 +369,24 @@ frappe.ui.Sidebar = class Sidebar { if (item.type == "Section Break") { $drop_icon.removeClass("hidden"); } + this.setup_event_listner(item_container); + } + setup_event_listner(item_container) { + let $child_item_section = item_container.find(".sidebar-child-item"); + let $drop_icon = item_container.find(".drop-icon"); $drop_icon.on("click", () => { - let opened = $drop_icon.find("use").attr("href") === "#es-line-down"; + let opened = $drop_icon.find("use").attr("href") === "#icon-chevron-down"; if (!opened) { - $drop_icon.attr("data-state", "closed").find("use").attr("href", "#es-line-down"); + $drop_icon + .attr("data-state", "closed") + .find("use") + .attr("href", "#icon-chevron-down"); } else { - $drop_icon.attr("data-state", "opened").find("use").attr("href", "#es-line-up"); + $drop_icon + .attr("data-state", "opened") + .find("use") + .attr("href", "#icon-chevron-right"); } $child_item_section.toggleClass("hidden"); }); @@ -450,7 +436,6 @@ frappe.ui.Sidebar = class Sidebar { close_sidebar() { this.sidebar_expanded = false; this.expand_sidebar(); - this.close_children_item(); if (frappe.is_mobile()) frappe.app.sidebar.prevent_scroll(); } open_sidebar() { @@ -459,12 +444,6 @@ frappe.ui.Sidebar = class Sidebar { this.set_active_workspace_item(); } - close_children_item() { - this.parent_items.forEach((i) => { - if (!$($(i).children()[1]).hasClass("hidden")) $(i).find(".drop-icon").click(); - }); - } - reload() { return frappe.workspace.get_pages().then((r) => { frappe.boot.sidebar_pages = r; @@ -479,9 +458,9 @@ frappe.ui.Sidebar = class Sidebar { handle_outside_click() { document.addEventListener("click", (e) => { - if (this.apps_switcher.drop_down_expanded) { - if (!e.composedPath().includes(this.apps_switcher.app_switcher_dropdown)) { - this.apps_switcher.toggle_app_menu(); + if (this.sidebar_header.drop_down_expanded) { + if (!e.composedPath().includes(this.sidebar_header.app_switcher_dropdown)) { + this.sidebar_header.toggle_dropdown_menu(); } } }); diff --git a/frappe/public/js/frappe/ui/sidebar/sidebar_header.html b/frappe/public/js/frappe/ui/sidebar/sidebar_header.html index 756850f1a3..dd14158fd2 100644 --- a/frappe/public/js/frappe/ui/sidebar/sidebar_header.html +++ b/frappe/public/js/frappe/ui/sidebar/sidebar_header.html @@ -1,17 +1,18 @@ + -