diff --git a/frappe/boot.py b/frappe/boot.py index f58f3b91b7..1cd5d0fbbb 100644 --- a/frappe/boot.py +++ b/frappe/boot.py @@ -530,7 +530,7 @@ def get_sidebar_items(): for s in sidebars: w = frappe.get_doc("Workspace Sidebar", s) sidebar_items[s.lower()] = [] - + print(s) for si in w.items: workspace_sidebar = { "label": si.label, @@ -539,8 +539,8 @@ def get_sidebar_items(): "type": si.type, "icon": si.icon, "child": si.child, - "collapsible": si.collapsible, - "collapsed_by_default": si.collapsed_by_default, + # "collapsible": si.collapsible, + # "collapsed_by_default": si.collapsed_by_default, } if si.link_type == "Report": report_type, ref_doctype = frappe.db.get_value( diff --git a/frappe/desk/doctype/desktop_icon/desktop_icon.js b/frappe/desk/doctype/desktop_icon/desktop_icon.js index 3e187a9df8..a6d137783d 100644 --- a/frappe/desk/doctype/desktop_icon/desktop_icon.js +++ b/frappe/desk/doctype/desktop_icon/desktop_icon.js @@ -4,34 +4,6 @@ frappe.ui.form.on("Desktop Icon", { setup: function (frm) { load_installed_apps(); - frm.fields_dict.color.set_data(Object.keys(frappe.palette_map)); - }, - 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; - } else if (frm.doc.type == "list") { - frm.doc.route = `/app/${frappe.router.slug(frm.doc._doctype)}`; - } }, }); diff --git a/frappe/desk/doctype/desktop_icon/desktop_icon.json b/frappe/desk/doctype/desktop_icon/desktop_icon.json index ebd4dac0f2..d9ed082fe3 100644 --- a/frappe/desk/doctype/desktop_icon/desktop_icon.json +++ b/frappe/desk/doctype/desktop_icon/desktop_icon.json @@ -4,41 +4,26 @@ "doctype": "DocType", "engine": "InnoDB", "field_order": [ - "module_name", "label", "standard", - "custom", + "icon_type", "type", "workspace", - "route", + "link_to", + "parent_icon", "column_break_3", "app", - "description", - "category", - "hidden", - "blocked", - "force_show", - "section_break_7", - "_doctype", - "_report", - "link", - "column_break_10", - "color", "icon", "logo_url", - "reverse", - "idx" + "idx", + "link" ], "fields": [ - { - "fieldname": "module_name", - "fieldtype": "Data", - "label": "Module Name" - }, { "fieldname": "label", "fieldtype": "Data", - "label": "Label" + "label": "Label", + "unique": 1 }, { "default": "0", @@ -47,100 +32,28 @@ "in_list_view": 1, "label": "Standard" }, - { - "default": "0", - "fieldname": "custom", - "fieldtype": "Check", - "label": "Custom", - "read_only": 1 - }, { "fieldname": "column_break_3", "fieldtype": "Column Break" }, - { - "fieldname": "app", - "fieldtype": "Autocomplete", - "label": "App" - }, - { - "fieldname": "description", - "fieldtype": "Small Text", - "label": "Description" - }, - { - "fieldname": "category", - "fieldtype": "Data", - "label": "Category" - }, - { - "default": "0", - "fieldname": "hidden", - "fieldtype": "Check", - "label": "Hidden" - }, - { - "default": "0", - "fieldname": "blocked", - "fieldtype": "Check", - "label": "Blocked" - }, - { - "default": "0", - "fieldname": "force_show", - "fieldtype": "Check", - "label": "Force Show", - "read_only": 1 - }, - { - "fieldname": "section_break_7", - "fieldtype": "Section Break" - }, { "fieldname": "type", "fieldtype": "Select", "in_list_view": 1, "in_standard_filter": 1, - "label": "Type", - "options": "module\nlist\nlink\npage\nquery-report\nworkspace" - }, - { - "fieldname": "_doctype", - "fieldtype": "Link", - "label": "_doctype", - "options": "DocType" - }, - { - "fieldname": "_report", - "fieldtype": "Link", - "label": "_report", - "options": "Report" + "label": "Link Type", + "options": "DocType\nWorkspace\nExternal" }, { "fieldname": "link", "fieldtype": "Small Text", "label": "Link" }, - { - "fieldname": "column_break_10", - "fieldtype": "Column Break" - }, - { - "fieldname": "color", - "fieldtype": "Autocomplete", - "label": "Color" - }, { "fieldname": "icon", "fieldtype": "Icon", "label": "Icon" }, - { - "default": "0", - "fieldname": "reverse", - "fieldtype": "Check", - "label": "Reverse Icon Color" - }, { "fieldname": "idx", "fieldtype": "Int", @@ -152,20 +65,38 @@ "label": "Workspace", "options": "Workspace" }, - { - "fieldname": "route", - "fieldtype": "Data", - "hidden": 1, - "label": "Route" - }, { "fieldname": "logo_url", "fieldtype": "Data", "label": "Logo URL" + }, + { + "fieldname": "icon_type", + "fieldtype": "Select", + "label": "Icon Type", + "options": "Folder\nApp\nLink" + }, + { + "depends_on": "eval: doc.standard == 1", + "fieldname": "app", + "fieldtype": "Autocomplete", + "label": "App" + }, + { + "fieldname": "link_to", + "fieldtype": "Dynamic Link", + "label": "Link To", + "options": "type" + }, + { + "fieldname": "parent_icon", + "fieldtype": "Link", + "label": "Parent Icon", + "options": "Desktop Icon" } ], "links": [], - "modified": "2025-09-29 01:47:25.718356", + "modified": "2025-10-08 23:40:31.716144", "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 558295b745..e98991d863 100644 --- a/frappe/desk/doctype/desktop_icon/desktop_icon.py +++ b/frappe/desk/doctype/desktop_icon/desktop_icon.py @@ -21,25 +21,16 @@ class DesktopIcon(Document): if TYPE_CHECKING: from frappe.types import DF - _doctype: DF.Link | None - _report: DF.Link | None app: DF.Autocomplete | None - blocked: DF.Check - category: DF.Data | None - color: DF.Autocomplete | None - custom: DF.Check - description: DF.SmallText | None - force_show: DF.Check - hidden: DF.Check + icon_type: DF.Literal["Folder", "App", "Link"] idx: DF.Int label: DF.Data | None link: DF.SmallText | None + link_to: DF.DynamicLink | None logo_url: DF.Data | None - module_name: DF.Data | None - reverse: DF.Check - route: DF.Data | None + parent_icon: DF.Link | None standard: DF.Check - type: DF.Literal["module", "list", "link", "page", "query-report", "workspace"] + type: DF.Literal["DocType", "Workspace", "External"] workspace: DF.Link | None # end: auto-generated types @@ -93,7 +84,10 @@ def get_desktop_icon_directory(app_name): def after_doctype_insert(): - frappe.db.add_unique("Desktop Icon", ("module_name", "owner", "standard")) + pass + + +# frappe.db.add_unique("Desktop Icon", ("owner", "standard")) def get_desktop_icons(user=None): @@ -105,25 +99,16 @@ def get_desktop_icons(user=None): if not user_icons: fields = [ - "module_name", - "hidden", "label", "link", "type", + "icon_type", + "parent_icon", "icon", - "color", - "description", - "category", - "_doctype", - "_report", + "link_to", "idx", - "force_show", - "reverse", - "custom", "standard", - "blocked", "workspace", - "route", "logo_url", ] @@ -140,6 +125,7 @@ def get_desktop_icons(user=None): standard_icons = frappe.get_all("Desktop Icon", fields=fields, filters={"standard": 1}) standard_map = {} + for icon in standard_icons: if icon._doctype in blocked_doctypes: icon.blocked = 1 @@ -192,7 +178,11 @@ def get_desktop_icons(user=None): for d in user_icons: if d.label: d.label = _(d.label, context=d.parent) + # includes + for s in user_icons: + if s.parent_icon: + s.parent_icon = frappe.db.get_value("Desktop Icon", s.parent_icon, "label") frappe.cache.hset("desktop_icons", user, user_icons) return user_icons @@ -635,39 +625,75 @@ def hide(name, user=None): def create_desktop_icons_from_workspace(): - all_workspaces = frappe.get_all("Workspace", filters={"public": 1}, pluck="name") + from frappe.query_builder import DocType + + workspace = DocType("Workspace") + + all_workspaces = ( + frappe.qb.from_(workspace) + .select(workspace.name) + .where((workspace.public == 1) & (workspace.name != "Welcome Workspace")) + ).run(pluck=True) + 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) + if not frappe.db.exists("Desktop Icon", {"label": w}): + icon = frappe.new_doc("Desktop Icon") + icon.type = "Workspace" + icon.workspace = w + icon.route = "/app/" + w.lower() + icon.label = w + icon.icon_type = "Link" + icon.standard = 1 + icon.color = generate_color() + icon.icon = frappe.db.get_value("Workspace", w, "icon") + icon.app = frappe.db.get_value("Workspace", w, "app") + module = frappe.db.get_value("Workspace", w, "module") + + icon_app = frappe.db.get_value("Module Def", module, "app_name") + icon_app = frappe.get_hooks("app_title", app_name=icon_app) + + parent_icon = frappe.db.exists("Desktop Icon", {"label": icon_app[0]}) + if parent_icon: + icon.parent_icon = parent_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()) + colors = ["orange", "pink", "blue", "green", "dark", "red", "yellow", "purple", "gray"] + return random.choice(colors) def create_desktop_icons_from_installed_apps(): - from frappe.apps import is_desk_apps - apps = frappe.get_installed_apps() for a in apps: app_details = frappe.get_hooks("add_to_apps_screen", app_name=a) if len(app_details) != 0: - if not is_desk_apps(app_details): - icon = frappe.new_doc("Desktop Icon") - icon.route = app_details[0]["route"] - icon.label = app_details[0]["title"] - icon.type = "link" - icon.link = app_details[0]["route"] - icon.logo_url = app_details[0]["logo"] - icon.save() + icon = frappe.new_doc("Desktop Icon") + icon.route = app_details[0]["route"] + icon.label = app_details[0]["title"] + icon.type = "External" + icon.standard = 1 + icon.icon_type = "App" + icon.link = app_details[0]["route"] + icon.logo_url = app_details[0]["logo"] + icon.save() + + +@frappe.whitelist() +def set_sequence(desktop_icons): + cnt = 1 + for item in json.loads(desktop_icons): + frappe.db.set_value("Workspace", item.get("name"), "sequence_id", cnt) + frappe.db.set_value("Workspace", item.get("name"), "parent_page", item.get("parent") or "") + cnt += 1 + + frappe.clear_cache() + frappe.toast(frappe._("Updated")) + + +def create_desktop_icon(): + create_desktop_icons_from_installed_apps() + frappe.db.commit() + create_desktop_icons_from_workspace() + frappe.db.commit() diff --git a/frappe/desk/doctype/workspace_sidebar/workspace_sidebar.py b/frappe/desk/doctype/workspace_sidebar/workspace_sidebar.py index 2bc375a2f0..a174eb1bfc 100644 --- a/frappe/desk/doctype/workspace_sidebar/workspace_sidebar.py +++ b/frappe/desk/doctype/workspace_sidebar/workspace_sidebar.py @@ -33,3 +33,13 @@ class WorkspaceSidebar(Document): def after_delete(self): if self.module and frappe.conf.developer_mode: delete_folder(self.module, "Workspace Sidebar", self.name) + + +def create_workspace_sidebar_for_workspaces(): + all_workspaces = frappe.get_all("Workspace", pluck="name") + existing_sidebars = frappe.get_all("Workspace Sidebar", pluck="title") + for workspace in all_workspaces: + if workspace not in existing_sidebars: + sidebar = frappe.new_doc("Workspace Sidebar") + sidebar.title = workspace + sidebar.save() diff --git a/frappe/desk/page/desktop/desktop.css b/frappe/desk/page/desktop/desktop.css index bfe164df94..26f5cf2ea1 100644 --- a/frappe/desk/page/desktop/desktop.css +++ b/frappe/desk/page/desktop/desktop.css @@ -1,3 +1,8 @@ +:root{ + --desktop-blur: blur(10.2px); + --desktop-modal-width: 508px; + --desktop-modal-height: 448px; +} .desktop-wrapper{ max-width: 100%; width: 100%; @@ -5,6 +10,37 @@ margin-left: auto; padding: 0px; } + +.navbar-container{ + display: flex; + align-items: center; + justify-content: space-between; + width: 100%; + padding: 10px 20px 10px 20px; /* Add padding if needed */ + box-sizing: border-box; + height: 52px; +} + +.desktop-search-wrapper{ + flex: 1; + max-width: 396px; + position: relative; +} + +#navbar-search{ + padding-left: 32px; +} +.desktop-search-icon{ + position: absolute; + left: 10px; + top: 2px; +} + +.desktop-search-icon > .icon { + stroke: var(--ink-gray-4); + stroke-width: 1px; +} + .desktop-container{ display: flex; align-items: center; @@ -12,33 +48,7 @@ flex-direction: column; margin-top: 20px; } -#navbar-search{ - padding-left: 32px; -} -.desktop-search-icon{ - position: absolute; - left: 6px; - top: 2px; -} -.navbar-container{ - display: flex; - align-items: center; - width: 50%; - justify-content: space-between; - width: 100%; /* Allow it to span the full container */ - max-width: 600px; /* Match icon grid width if needed */ - padding: 0 15px; /* Add padding if needed */ - box-sizing: border-box; -} -.search-container{ - display: flex; - justify-content: center; -} -.desktop-search-wrapper{ - max-width: 300px; - margin: 0 var(--margin-md); - position: relative; -} + .icon-stroke{ stroke-width: 1.5px; } @@ -47,7 +57,8 @@ display: grid; justify-items: center; margin-top: 50px; - grid-template-columns: repeat(5, 1fr); + grid-template-columns: repeat(6, 1fr); + grid-template-rows: repeat(3, 1fr); } .desktop-icon{ display: flex; @@ -55,31 +66,49 @@ width: 100px; flex-direction: column; align-items: center; - + gap: 12px; } .icon-container:has(img) { padding: 0; + background-color: unset; } .icon-container img{ - width: 60px; - height: 60px; + width: 54px; + height: 54px; } .icon-container{ padding: 10px; - border: var(--gray-400) solid 1px; border-radius: 10px; display: flex; align-items: center; justify-content: center; + width: 54px; + height: 54px; + background-color: var(--surface-gray-3); +} +.icon-container .icon{ + width: 27px; + height: 27px; + stroke: var(--gray-900); } .icon-container:hover{ transform: scale(1.05); transition: transform 0.1s; } -.icon-label{ +.icon-caption{ text-align: center; text-wrap: nowrap; - margin-top: 5px; + display: flex; + flex-direction: column; +} +.icon-title{ + font-weight: var(--weight-semibold); + font-size: var(--text-sm); +} +.icon-subtitle{ + font-weight: var(--weight-regular); + font-size: var(--text-xs); + color: var(--ink-gray-5); } .timeless-style{ width: 100vw; @@ -95,4 +124,83 @@ } .small-margin{ margin-top: 30px; +} +.desktop-modal{ + backdrop-filter: var(--desktop-blur); + display: flex !important; + & .modal-dialog{ + & .modal-content { + top: 120px; + } + } +} + +.desktop-modal-body { + width: var(--desktop-modal-width); + height: var(--desktop-modal-height); + padding: 0px !important; + padding-top: 23px !important; + padding-bottom: 23px !important; + & .icons{ + gap: 20px 0px; + } +} +.modal-heading{ + display: flex; + align-items: center; + justify-content: center; + font-size: var(--text-2xl); + font-weight: var(--weight-semibold); + color: var(--neutral-white); +} +.desktop-modal-heading { + all: unset !important; + position: absolute !important; + top: -75px !important; + width: 100% !important; + & .title-section{ + display: flex; + align-items: center; + justify-content: center; + & .modal-title{ + color: var(--neutral-white); + font-size: var(--text-2xl); + } + } + & .modal-actions { + display: none !important; + } +} +.modal-body .icons{ + margin-top: 0px; +} +.desktop-context-menu{ + position: absolute; +} + +.folder-icon{ + background-color: var(--gray-50); + box-shadow: 0px 0px 1px 0px rgba(0, 0, 0, 0.14); + padding: 7px; + align-items: normal; + + & .icons{ + gap: 2.1px; + margin-top: 0px; + & .desktop-icon { + width: fit-content; + height: fit-content; + & .icon-container{ + height: 9px; + width: 9px; + padding: 0px; + border-radius: 2px; + & .icon{ + width: 5px; + height: 5px; + } + } + + } + } } \ No newline at end of file diff --git a/frappe/desk/page/desktop/desktop.html b/frappe/desk/page/desktop/desktop.html index 3f88963be0..70d791d3e1 100644 --- a/frappe/desk/page/desktop/desktop.html +++ b/frappe/desk/page/desktop/desktop.html @@ -1,91 +1,41 @@
- {% if navbar_style == "Brand Logo" or navbar_style == "Brand Logo with Search" %} - - {% endif %} - -
- {% if navbar_style == "macOS Launchpad" or navbar_style == "Timeless Launchpad" %} -
- - {% elif navbar_style == "Apps with Search" %} - - {% endif %} - diff --git a/frappe/desk/page/desktop/desktop.js b/frappe/desk/page/desktop/desktop.js index 56e3d68bbd..eb41652052 100644 --- a/frappe/desk/page/desktop/desktop.js +++ b/frappe/desk/page/desktop/desktop.js @@ -1,3 +1,18 @@ +frappe.desktop_utils = {}; +$.extend(frappe.desktop_utils, { + modal: null, + create_desktop_modal: function (icon, icon_title, icons_data, grid) { + if (!this.modal) { + this.modal = new DesktopModal(icon); + } + return this.modal; + }, + close_desktop_modal: function () { + if (this.modal) { + this.modal.hide(); + } + }, +}); frappe.pages["desktop"].on_page_load = function (wrapper) { var page = frappe.ui.make_app_page({ parent: wrapper, @@ -5,125 +20,436 @@ frappe.pages["desktop"].on_page_load = function (wrapper) { single_column: true, hide_sidebar: true, }); - page.page_head.hide(); - $(frappe.render_template("desktop")).appendTo(page.body); - setup(); + let desktop_page = new DesktopPage(page); + frappe.pages["desktop"].desktop_page = desktop_page; + // setup(); }; -frappe.pages["desktop"].on_page_show = function (wrapper) {}; -function setup() { - let desktop_icon_style = $("#icon-style").attr("data-icon-style"); - let navbar_style = $("#icon-style").attr("data-navbar-style"); - $(".desktop-icon").each((i, el) => { - let icon_name = $(el).attr("data-icon"); - let icon_container = $(el.children[0]); - - if ($(el).attr("data-logo") != "None") { - // create a img tag - const logo_url = $(el).attr("data-logo"); - const $img = $("").attr("src", logo_url); - icon_container.append($img); - icon_container.css("border", "none"); - } else { - const svg = frappe.utils.icon(icon_name, "xl icon-stroke"); - - if (svg) { - const $svg = $(svg); - - // Apply stroke via CSS - if (desktop_icon_style !== "Monochrome") { - let bg_color, text_color; - let color_scheme = - frappe.palette[frappe.palette_map[icon_container.attr("data-color")]]; - if (desktop_icon_style === "Subtle") { - bg_color = `var(${color_scheme[0]})`; - text_color = color_scheme[1]; - } else if (desktop_icon_style === "Subtle Reverse") { - bg_color = `var(${color_scheme[1]})`; - text_color = color_scheme[0]; - } else if (desktop_icon_style === "Subtle Reverse w Opacity") { - // #0289f7bd - var style = window.getComputedStyle(document.body); - console.log(style.getPropertyValue(color_scheme[1])); - bg_color = style.getPropertyValue(color_scheme[1]) + "e6"; - text_color = color_scheme[0]; - } - icon_container.css("background-color", `${bg_color}`); - $svg.find("*").css("stroke", `var(${text_color})`); - - // Apply to svg root - $svg.css("stroke", `var(${bg_color})`); - icon_container.css("border", "none"); - } - - icon_container.append($svg); - } - } - - // let color_name = icon_container.attr("data-color"); - // icon_container.css("background-color", color_name); +function get_workspaces_from_app_name(app_name) { + const app = frappe.boot.app_data.filter((a) => { + return a.app_title === app_name; }); - setup_navbar(navbar_style); + if (app.length > 0) return app[0].workspaces; } -function get_route(element) { +function get_route(desktop_icon) { let route; - if (element.attr("data-type") == "workspace") { - route = window.location.origin + element.attr("data-route"); + if (!desktop_icon) return; + let item = {}; + if (desktop_icon.type == "External" && desktop_icon.link) { + route = window.location.origin + desktop_icon.link; } else { - route = element.attr("data-route"); + if (desktop_icon.type == "Workspace") { + item = { + type: desktop_icon.type, + link: frappe.router.slug(desktop_icon.workspace), + }; + } else if (desktop_icon.type == "List") { + item = { + type: desktop_icon.type, + link: desktop_icon.__doctype, + }; + } + route = frappe.utils.generate_route(item); } + return route; } -function setup_navbar(navbar_style) { - if (navbar_style != "Awesomebar") { - $(".sticky-top > .navbar").hide(); - } else { - $(".navbar").show(); - } -} - -frappe.router.on("change", function () { - let navbar_style = $("#icon-style").attr("data-navbar-style"); - if (frappe.get_route()[0] == "desktop") setup_navbar(navbar_style); - else $(".navbar").show(); -}); - frappe.pages["desktop"].on_page_show = function () { - let desktop_icon_style = $("#icon-style").attr("data-icon-style"); - let navbar_style = $("#icon-style").attr("data-navbar-style"); - setup_avatar(); - if (navbar_style != "Awesomebar") { - if (navbar_style == "macOS Launchpad") - $(".desktop-container").css("align-items", "normal"); - setup_avatar(); - } - setup_search(); + frappe.pages["desktop"].desktop_page.setup(); }; -function setup_avatar() { - $(".desktop-avatar").html(frappe.avatar(frappe.session.user, "avatar-medium")); -} - -function setup_search() { - let all_icons = $(".icon-label"); - let icons_to_show = []; - $(".desktop-search-wrapper > #navbar-search").on("input", function (e) { - let search_query = $(e.target).val().toLowerCase(); - icons_to_show = []; - all_icons.each(function (index, element) { - $(element).parent().hide(); - let label = $(element).text().toLowerCase(); - if (label.includes(search_query)) { - icons_to_show.push(element); - } - }); - toggle_icons(icons_to_show); - }); -} function toggle_icons(icons) { icons.forEach((i) => { $(i).parent().show(); }); } + +class DesktopPage { + constructor(page) { + this.prepare(); + this.make(page); + this.setup(); + } + + prepare() { + this.apps_icons = frappe.boot.desktop_icons.filter((c) => { + return c.icon_type == "App" || c.icon_type == "Folder"; + }); + } + + make(page) { + page.page_head.hide(); + $(frappe.render_template("desktop")).appendTo(page.body); + this.wrapper = page.body.find(".desktop-container"); + this.icon_grid = new DesktopIconGrid(this.wrapper, this.apps_icons); + } + + setup() { + this.setup_avatar(); + this.setup_navbar(); + this.setup_icon_search(); + this.handke_route_change(); + } + setup_avatar() { + $(".desktop-avatar").html(frappe.avatar(frappe.session.user, "avatar-medium")); + } + setup_navbar() { + $(".sticky-top > .navbar").hide(); + } + + handke_route_change() { + const me = this; + frappe.router.on("change", function () { + if (frappe.get_route()[0] == "desktop") me.setup_navbar(); + else { + $(".navbar").show(); + frappe.desktop_utils.close_desktop_modal(); + } + }); + } + + setup_icon_search() { + let all_icons = $(".icon-title"); + let icons_to_show = []; + $(".desktop-search-wrapper > #navbar-search").on("input", function (e) { + let search_query = $(e.target).val().toLowerCase(); + console.log(search_query); + icons_to_show = []; + all_icons.each(function (index, element) { + $(element).parent().hide(); + let label = $(element).text().toLowerCase(); + if (label.includes(search_query)) { + icons_to_show.push(element); + } + }); + toggle_icons(icons_to_show); + }); + } +} + +class DesktopIconGrid { + constructor(wrapper, icons_data, row_size, in_folder, in_modal, parent_icon, no_dragging) { + this.wrapper = wrapper; + this.icons_data = icons_data; + this.row_size = row_size; + this.icons = []; + this.page_size = { + col: 4, + row: 3, + total: function () { + return this.col * this.row; + }, + }; + this.in_folder = in_folder; + this.in_modal = in_modal; + this.parent_icon_obj = parent_icon; + this.no_dragging = no_dragging; + this.grids = []; + this.prepare(); + this.make(); + } + prepare() { + this.total_pages = Math.ceil(this.icons_data.length / this.page_size.total()); + this.icons_data_by_page = this.split_data(this.icons_data, this.page_size.total()); + } + make() { + const me = this; + for (let i = 0; i < this.total_pages; i++) { + let template = `
`; + + if (this.row_size && this.in_modal) { + template = ``; + } + this.grids.push($(template).appendTo(this.wrapper)); + this.make_icons(this.icons_data_by_page[i], this.grids[i]); + if (!this.no_dragging) { + this.setup_reordering(this.grids[i]); + } + this.grids[i].on("wheel", function (event) { + if (event.originalEvent) { + event = event.originalEvent; // for jQuery or wrapped events + } + + if (Math.abs(event.deltaX) > Math.abs(event.deltaY)) { + event.preventDefault(); + if (event.deltaX > 0) { + if (me.current_page != me.total_pages - 1) me.current_page++; + me.change_to_page(me.current_page); + } else { + if (me.current_page != 0) me.current_page--; + me.change_to_page(me.current_page); + } + } + }); + } + this.setup_pagination(); + } + setup_pagination() { + this.current_page = 0; + this.change_to_page(this.current_page); + } + change_to_page(index) { + this.grids.forEach((g) => $(g).css("display", "none")); + this.grids[index].css("display", "grid"); + this.current_page = index; + } + split_data(icons, size) { + const result = []; + + for (let i = 0; i < icons.length; i += size) { + result.push(icons.slice(i, i + size)); + } + + return result; + } + make_icons(icons_data, grid) { + icons_data.forEach((icon) => { + let icon_html = new DesktopIcon(icon, this.in_folder).get_desktop_icon_html(); + this.icons.push(icon_html); + grid.append(icon_html); + }); + } + + setup_reordering(grid) { + const me = this; + let sortable = new Sortable($(grid).get(0), { + swapThreshold: 0.09, + group: { + name: "desktop", + put: true, + pull: true, + }, + onEnd: function (evt) { + let title = $(evt.item).find(".icon-title").text(); + if (me.parent_icon_obj) { + let icon = me.parent_icon_obj.child_icons.findIndex((f) => f.label == title); + me.parent_icon_obj.child_icons.splice(icon, 1); + if (me.parent_icon_obj) me.parent_icon_obj.render_folder_thumbnail(); + } + + // if (evt.to.parentElement.classList.contains("folder-icon")) { + // // open the folder + + // } + }, + }); + } +} +class DesktopIcon { + constructor(icon, in_folder) { + this.icon_data = icon; + this.icon_title = this.icon_data.label; + this.icon_subtitle = ""; + this.icon_type = this.icon_data.icon_type; + this.in_folder = in_folder; + this.type = this.icon_data.type; + if (this.icon_type != "Folder") { + this.icon_route = get_route(this.get_desktop_icon(this.icon_title)); + } + this.icon = $( + frappe.render_template("desktop_icon", { icon: this.icon_data, in_folder: in_folder }) + ); + this.icon_caption_area = $(this.icon.get(0).children[1]); + this.child_icons = this.get_child_icons_data(); + // this.child_icons = this.get_desktop_icon(this.icon_title).child_icons; + // this.child_icons_data = this.get_child_icons_data(); + this.parent_icon = this.icon_data.icon; + this.setup_click(); + this.setup_context_menu(); + this.render_folder_thumbnail(); + this.setup_dragging(); + this.child_icons = this.get_child_icons_data(); + } + get_child_icons_data() { + return frappe.boot.desktop_icons.filter((f) => { + return f.parent_icon == this.icon_title; + }); + } + get_desktop_icon_html() { + return this.icon; + } + get_desktop_icon(icon_label) { + return frappe.boot.desktop_icons.find((d) => { + return d.label == icon_label; + }); + } + setup_click() { + const me = this; + if (this.child_icons.length && (this.icon_type == "App" || this.icon_type == "Folder")) { + $(this.icon).on("click", () => { + let modal = frappe.desktop_utils.create_desktop_modal(me); + modal.setup(me.icon_title, me.child_icons, 4); + modal.show(); + }); + $($(this.icon_caption_area).children()[1]).html( + `${this.child_icons.length} Workspaces` + ); + } else { + this.icon.attr("href", this.icon_route); + } + } + + setup_context_menu() { + const me = this; + this.context_menu_items = { + icon: [ + { + label: "Add Children Icon", + icon: "add", + onClick: function () { + console.log("Open folder modal"); + }, + }, + ], + }; + this.context_menu = new ContextMenu(this.context_menu_items["icon"]); + $(this.icon).on("contextmenu", function (event) { + event.preventDefault(); + me.context_menu.show(event); + }); + } + + render_folder_thumbnail() { + if (this.icon_type == "Folder") { + if (!this.folder_wrapper) this.folder_wrapper = this.icon.find(".icon-container"); + this.folder_wrapper.html(""); + this.folder_grid = new DesktopIconGrid( + this.folder_wrapper, + this.child_icons, + 4, + true, + true, + null, + true + ); + } + } + + setup_dragging() { + this.icon.on("drag", (event) => { + const mouse_x = event.clientX; + const mouse_y = event.clientY; + if (frappe.desktop_utils.modal) { + let modal = frappe.desktop_utils.modal.modal + .find(".modal-content") + .get(0) + .getBoundingClientRect(); + if ( + mouse_x > modal.right || + mouse_x < modal.left || + mouse_y > modal.bottom || + mouse_y < modal.top + ) { + frappe.desktop_utils.close_desktop_modal(); + } + } + }); + } +} + +class DesktopModal { + constructor(icon) { + this.parent_icon_obj = icon; + } + setup(icon_title, child_icons_data, grid_row_size) { + const me = this; + this.modal = new frappe.get_modal(icon_title, ""); + this.modal.find(".modal-header").addClass("desktop-modal-heading"); + this.modal.addClass("desktop-modal"); + this.modal.attr("draggable", true); + this.modal.find(".modal-body").addClass("desktop-modal-body"); + this.$child_icons_wrapper = this.modal.find(".desktop-modal-body"); + + this.child_icon_grid = new DesktopIconGrid( + this.$child_icons_wrapper, + child_icons_data, + grid_row_size, + false, + true, + this.parent_icon_obj + ); + + this.modal.on("hidden.bs.modal", function () { + me.modal.remove(); + }); + } + show() { + this.modal.modal("show"); + } + hide() { + this.modal.modal("hide"); + } +} +class ContextMenu { + constructor(menu_items) { + this.template = $(``); + this.menu_items = menu_items; + this.make(); + } + make() { + this.template.appendTo(document.body); + this.menu_items.forEach((f) => { + this.add_menu_item(f); + }); + } + add_menu_item(item) { + $(``) + .on("click", function () { + item.onClick(); + this.template; + }) + .appendTo(this.template); + } + show(event) { + this.top = this.mouseY(event) + "px"; + this.left = this.mouseX(event) + "px"; + this.template.css("display", "block"); + this.template.css("top", this.top); + this.template.css("left", this.left); + } + mouseX(evt) { + if (evt.pageX) { + return evt.pageX; + } else if (evt.clientX) { + return ( + evt.clientX + + (document.documentElement.scrollLeft + ? document.documentElement.scrollLeft + : document.body.scrollLeft) + ); + } else { + return null; + } + } + + mouseY(evt) { + if (evt.pageY) { + return evt.pageY; + } else if (evt.clientY) { + return ( + evt.clientY + + (document.documentElement.scrollTop + ? document.documentElement.scrollTop + : document.body.scrollTop) + ); + } else { + return null; + } + } +} diff --git a/frappe/desk/page/desktop/desktop.json b/frappe/desk/page/desktop/desktop.json index 8128a50518..ea61c81769 100644 --- a/frappe/desk/page/desktop/desktop.json +++ b/frappe/desk/page/desktop/desktop.json @@ -5,7 +5,7 @@ "doctype": "Page", "icon": "", "idx": 0, - "modified": "2025-08-18 16:17:19.559412", + "modified": "2025-10-08 13:31:06.525425", "modified_by": "Administrator", "module": "Desk", "name": "desktop", @@ -13,7 +13,7 @@ "page_name": "desktop", "roles": [ { - "role": "System Manager" + "role": "All" } ], "script": null, diff --git a/frappe/desk/page/desktop/desktop.py b/frappe/desk/page/desktop/desktop.py index e710151ae1..4da005c55d 100644 --- a/frappe/desk/page/desktop/desktop.py +++ b/frappe/desk/page/desktop/desktop.py @@ -6,9 +6,6 @@ def get_context(context): if frappe.session.user == "Guest": frappe.local.flags.redirect_location = "/app" raise frappe.Redirect - context.desktop_icon_style = frappe.get_single_value("Desktop Settings", "icon_style") - context.navbar_style = frappe.get_single_value("Desktop Settings", "navbar_style") context.brand_logo = frappe.get_single_value("Navbar Settings", "app_logo") context.current_user = frappe.session.user - context.icons = get_desktop_icons() return context diff --git a/frappe/hooks.py b/frappe/hooks.py index b8c8901320..858d813636 100644 --- a/frappe/hooks.py +++ b/frappe/hooks.py @@ -416,6 +416,7 @@ ignore_links_on_delete = [ "Route History", "Access Log", "Permission Log", + "Desktop Icon", ] # Request Hooks @@ -581,3 +582,13 @@ user_invitation = { "System Manager": [], }, } + + +add_to_apps_screen = [ + { + "name": app_name, + "logo": app_logo_url, + "title": app_title, + "route": app_home, + } +] diff --git a/frappe/installer.py b/frappe/installer.py index ea63e82abe..0d1f9ae4fa 100644 --- a/frappe/installer.py +++ b/frappe/installer.py @@ -17,7 +17,7 @@ from semantic_version import Version import frappe from frappe.defaults import _clear_cache -from frappe.desk.doctype.desktop_icon.desktop_icon import sync_desktop_icons +from frappe.desk.doctype.desktop_icon.desktop_icon import create_desktop_icon, sync_desktop_icons from frappe.utils import cint, is_git_url from frappe.utils.dashboard import sync_dashboards from frappe.utils.synchronization import filelock diff --git a/frappe/public/js/desk.bundle.js b/frappe/public/js/desk.bundle.js index 8b57c94023..1378418b35 100644 --- a/frappe/public/js/desk.bundle.js +++ b/frappe/public/js/desk.bundle.js @@ -109,3 +109,4 @@ import "./frappe/ui/driver.js"; import "./frappe/scanner"; import "./frappe/ui/address_autocomplete/autocomplete_dialog.js"; +import "./frappe/ui/desktop_icon.html"; diff --git a/frappe/public/js/frappe/ui/desktop_icon.html b/frappe/public/js/frappe/ui/desktop_icon.html new file mode 100644 index 0000000000..041a158e1f --- /dev/null +++ b/frappe/public/js/frappe/ui/desktop_icon.html @@ -0,0 +1,19 @@ + + {% if (icon.logo_url) { %} +
{{ icon.label }}
+ {% } else if (icon.icon_type == "Folder") { %} +
+ +
+ {% } else { %} +
+ {%= frappe.utils.icon(icon.icon || "list-alt" , "lg", "", "", "text-ink-gray-7 current-color", true)%} +
+ {% } %} + {% if (!in_folder) { %} +
+
{{ icon.label }}
+
+
+ {% } %} +
\ No newline at end of file diff --git a/frappe/public/js/frappe/ui/sidebar/sidebar.js b/frappe/public/js/frappe/ui/sidebar/sidebar.js index a3492d6d6b..22c3ff44a8 100644 --- a/frappe/public/js/frappe/ui/sidebar/sidebar.js +++ b/frappe/public/js/frappe/ui/sidebar/sidebar.js @@ -35,7 +35,7 @@ frappe.ui.Sidebar = class Sidebar { } setup(workspace_title) { - this.workspace_title = frappe.utils.to_title_case(workspace_title); + this.workspace_title = workspace_title; this.sidebar_header = new frappe.ui.SidebarHeader(this); this.make_sidebar(); this.setup_complete = true; @@ -545,8 +545,6 @@ frappe.ui.Sidebar = class Sidebar { } else { frappe.app.sidebar.setup(sidebars[0] || "Build"); } - } else { - this.set_sidebar_for_page(); } this.set_active_workspace_item(); diff --git a/frappe/public/js/frappe/ui/sidebar/sidebar_header.js b/frappe/public/js/frappe/ui/sidebar/sidebar_header.js index 1932a5cf26..63575741a3 100644 --- a/frappe/public/js/frappe/ui/sidebar/sidebar_header.js +++ b/frappe/public/js/frappe/ui/sidebar/sidebar_header.js @@ -74,10 +74,6 @@ frappe.ui.SidebarHeader = class SidebarHeader { ); if (icon.length > 0) { this.header_icon = icon[0].icon; - this.header_logo_color = icon[0].color; - this.header_bg_color = frappe.palette[frappe.palette_map[this.header_logo_color]][0]; - this.header_stroke_color = - frappe.palette[frappe.palette_map[this.header_logo_color]][1]; } } setup_app_switcher() { diff --git a/frappe/public/scss/espresso/_colors.scss b/frappe/public/scss/espresso/_colors.scss index 564b7b33db..2fa0419bf3 100644 --- a/frappe/public/scss/espresso/_colors.scss +++ b/frappe/public/scss/espresso/_colors.scss @@ -202,4 +202,7 @@ $light-yellow: #fef4e2; rgba(205, 41, 41, 0.804) 99.87% ); --angular-blue: conic-gradient(rgba(0, 110, 219, 0) 72.38%, rgba(0, 110, 219, 0) 99.87%); + + // Surfaces + --surface-gray-3: #ededed; }