feat: desktop screen wth new design

This commit is contained in:
sokumon 2025-10-09 18:18:47 +05:30
parent 4f64cb1613
commit 7cdfb60797
17 changed files with 756 additions and 408 deletions

View file

@ -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(

View file

@ -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)}`;
}
},
});

View file

@ -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",

View file

@ -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()

View file

@ -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()

View file

@ -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;
}
}
}
}
}

View file

@ -1,91 +1,41 @@
<!-- jinja -->
<div class="desktop-wrapper">
{% if navbar_style == "Brand Logo" or navbar_style == "Brand Logo with Search" %}
<header class="navbar navbar-expand" role="navigation" style="justify-content: center;">
<div class="navbar-container">
<a class="navbar-home" href="/app">
<img
class="brand-logo"
src="{{ brand_logo }}"
alt="{{ _("App Logo") }}"
>
</a>
{% if navbar_style == "Brand Logo with Search" %}
<div style="display: flex;">
<div class="search-container">
<div class="desktop-search-wrapper text-muted">
<input
id="navbar-search"
type="text"
class="form-control"
aria-haspopup="true"
placeholder="Search Desktop Icons"
>
<span class="desktop-search-icon">
<svg class="icon icon-sm"><use href="#icon-search"></use></svg>
</span>
</div>
</div>
<span class="desktop-avatar" style="margin-left: -10px;">
</span>
</div>
{% endif %}
{% if navbar_style == "Brand Logo" %}
<span class="desktop-avatar">
</span>
{% endif %}
</div>
</header>
{% endif %}
<div id="icon-style" style="display: none;" data-icon-style="{{ desktop_icon_style }}" data-navbar-style="{{ navbar_style }}"></div>
<div class="desktop-container">
{% if navbar_style == "macOS Launchpad" or navbar_style == "Timeless Launchpad" %}
<div class="search-container">
<div class="desktop-search-wrapper text-muted
{% if navbar_style == 'Timeless Launchpad' %} timeless-style
{% endif %}">
<header class="navbar navbar-expand navbar-container" role="navigation">
<a class="navbar-home" href="/app">
<img
class="brand-logo"
src="{{ brand_logo }}"
alt="{{ _("App Logo") }}"
>
</a>
<div class="desktop-search-wrapper text-muted ">
<input
id="navbar-search"
type="text"
class="form-control"
aria-haspopup="true"
placeholder="Search Desktop Icons"
placeholder="Search apps, workspaces"
>
<span class="desktop-search-icon">
<svg class="icon icon-sm"><use href="#icon-search"></use></svg>
</span>
</div>
</div>
{% elif navbar_style == "Apps with Search" %}
<div class="search-container timeless-style apps-search">
<h4 style="margin: 0px; font-size: 30px;">Apps</h4>
<div class="desktop-search-wrapper text-muted">
<input
id="navbar-search"
type="text"
class="form-control"
aria-haspopup="true"
placeholder="Search Apps"
>
<span class="desktop-search-icon">
<svg class="icon icon-sm"><use href="#icon-search"></use></svg>
</span>
</div>
</div>
{% endif %}
<div class="icons
{% if navbar_style == 'Apps with Search' %} small-margin
{% endif %}">
<span class="desktop-avatar" style="margin-left: -10px;">
</span>
</header>
<div class="desktop-container">
<!-- <div class="icons">
{% for icon in icons %}
<a class="desktop-icon" data-logo="{{ icon.logo_url }}" data-icon="{{ icon.icon }}" data-type="{{ icon.type }}" href="{{ icon.route }}" style="text-decoration:none">
<div class="icon-container" data-color="{{ icon.color }}"> </div>
<div class="icon-label">{{ icon.label }}</div>
<a class="desktop-icon" data-logo="{{ icon.logo_url }}" data-icon="{{ icon.icon }}" data-type="{{ icon.type }}" style="text-decoration:none">
<div class="icon-container"> </div>
<div class="icon-caption">
<div class="icon-title">{{ icon.label }}</div>
<div class="icon-subtitle"></div>
</div>
</a>
{% endfor %}
</div>
</div> -->
</div>
</div>

View file

@ -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 = $("<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 = `<div class="icons"></div>`;
if (this.row_size && this.in_modal) {
template = `<div class="icons" style="display: none; grid-template-columns: repeat(${this.row_size}, 1fr)"></div>`;
}
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 = $(`<div class="dropdown-menu desktop-context-menu" role="menu"></div>`);
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) {
$(`<div class="dropdown-menu-item">
<a>
<div class="sidebar-item-icon">
${
item.icon
? frappe.utils.icon(item.icon)
: `<img
class="logo"
src="${item.icon_url}"
>`
}
</div>
<span class="menu-item-title">${item.label}</span>
</a>
</div>`)
.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;
}
}
}

View file

@ -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,

View file

@ -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

View file

@ -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,
}
]

View file

@ -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

View file

@ -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";

View file

@ -0,0 +1,19 @@
<a class="desktop-icon" data-logo="{{ icon.logo_url }}" data-icon="{{ icon.icon }}" data-type="{{ icon.type }}" style="text-decoration:none">
{% if (icon.logo_url) { %}
<div class="icon-container"> <img src="{{ icon.logo_url }}" alt="{{ icon.label }}" /></div>
{% } else if (icon.icon_type == "Folder") { %}
<div class="icon-container folder-icon">
</div>
{% } else { %}
<div class="icon-container">
{%= frappe.utils.icon(icon.icon || "list-alt" , "lg", "", "", "text-ink-gray-7 current-color", true)%}
</div>
{% } %}
{% if (!in_folder) { %}
<div class="icon-caption">
<div class="icon-title">{{ icon.label }}</div>
<div class="icon-subtitle"></div>
</div>
{% } %}
</a>

View file

@ -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();

View file

@ -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() {

View file

@ -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;
}