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) { %}
+
+ {% } 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) { %}
+
+ {% } %}
+
\ 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;
}