diff --git a/frappe/boot.py b/frappe/boot.py index 7b3a473d48..bdd175456e 100644 --- a/frappe/boot.py +++ b/frappe/boot.py @@ -529,26 +529,27 @@ def get_sidebar_items(): for s in sidebars: w = frappe.get_doc("Workspace Sidebar", s) - desktop_icon = frappe.db.get_value("Desktop Icon", w.desktop_icon, "label").lower() - sidebar_items[desktop_icon] = [] + if frappe.db.get_value("Desktop Icon", w.desktop_icon, "label"): + desktop_icon = frappe.db.get_value("Desktop Icon", w.desktop_icon, "label").lower() + sidebar_items[desktop_icon] = [] - for si in w.items: - workspace_sidebar = { - "label": si.label, - "link_to": si.link_to, - "link_type": si.link_type, - "type": si.type, - "icon": si.icon, - "child": si.child, - } - if si.link_type == "Report": - report_type, ref_doctype = frappe.db.get_value( - "Report", si.link_to, ["report_type", "ref_doctype"] - ) - workspace_sidebar["report"] = { - "report_type": report_type, - "ref_doctype": ref_doctype, + for si in w.items: + workspace_sidebar = { + "label": si.label, + "link_to": si.link_to, + "link_type": si.link_type, + "type": si.type, + "icon": si.icon, + "child": si.child, } - sidebar_items[desktop_icon].append(workspace_sidebar) + if si.link_type == "Report": + report_type, ref_doctype = frappe.db.get_value( + "Report", si.link_to, ["report_type", "ref_doctype"] + ) + workspace_sidebar["report"] = { + "report_type": report_type, + "ref_doctype": ref_doctype, + } + sidebar_items[desktop_icon].append(workspace_sidebar) return sidebar_items diff --git a/frappe/desk/desktop.py b/frappe/desk/desktop.py index ec57ef8e96..95d1c92556 100644 --- a/frappe/desk/desktop.py +++ b/frappe/desk/desktop.py @@ -707,3 +707,8 @@ def update_onboarding_step(name, field, value): frappe.db.set_value("Onboarding Step", name, field, value) capture(frappe.scrub(name), app="frappe_onboarding", properties={field: value}) + + +@frappe.whitelist() +def get_installed_apps(): + return frappe.get_installed_apps() diff --git a/frappe/desk/doctype/desktop_icon/desktop_icon.js b/frappe/desk/doctype/desktop_icon/desktop_icon.js index 1b57bfe6dc..3e187a9df8 100644 --- a/frappe/desk/doctype/desktop_icon/desktop_icon.js +++ b/frappe/desk/doctype/desktop_icon/desktop_icon.js @@ -2,7 +2,8 @@ // For license information, please see license.txt frappe.ui.form.on("Desktop Icon", { - refresh: function (frm) { + setup: function (frm) { + load_installed_apps(); frm.fields_dict.color.set_data(Object.keys(frappe.palette_map)); }, before_save: function (frm) { @@ -33,3 +34,14 @@ frappe.ui.form.on("Desktop Icon", { } }, }); + +async function load_installed_apps(frm) { + await frappe.call({ + method: "frappe.desk.desktop.get_installed_apps", + callback: function (r) { + if (r.message) { + cur_frm.fields_dict["app"].set_data(r.message); + } + }, + }); +} diff --git a/frappe/desk/doctype/desktop_icon/desktop_icon.json b/frappe/desk/doctype/desktop_icon/desktop_icon.json index aa76da3526..ebd4dac0f2 100644 --- a/frappe/desk/doctype/desktop_icon/desktop_icon.json +++ b/frappe/desk/doctype/desktop_icon/desktop_icon.json @@ -60,9 +60,8 @@ }, { "fieldname": "app", - "fieldtype": "Data", - "label": "App", - "read_only": 1 + "fieldtype": "Autocomplete", + "label": "App" }, { "fieldname": "description", @@ -166,7 +165,7 @@ } ], "links": [], - "modified": "2025-09-08 02:43:09.997790", + "modified": "2025-09-29 01:47:25.718356", "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 0288da4257..558295b745 100644 --- a/frappe/desk/doctype/desktop_icon/desktop_icon.py +++ b/frappe/desk/doctype/desktop_icon/desktop_icon.py @@ -2,11 +2,14 @@ # License: MIT. See LICENSE import json +import os import random import frappe from frappe import _ from frappe.model.document import Document +from frappe.modules.export_file import write_document_file +from frappe.modules.import_file import import_file_by_path class DesktopIcon(Document): @@ -20,7 +23,7 @@ class DesktopIcon(Document): _doctype: DF.Link | None _report: DF.Link | None - app: DF.Data | None + app: DF.Autocomplete | None blocked: DF.Check category: DF.Data | None color: DF.Autocomplete | None @@ -46,11 +49,49 @@ class DesktopIcon(Document): def on_trash(self): clear_desktop_icons_cache() + if frappe.conf.developer_mode: + if self.standard == 1 and self.app: + self.delete_desktop_icon_file() + + def on_update(self): + if frappe.conf.developer_mode: + if self.standard == 1 and self.app: + self.export_desktop_icon() + + def export_desktop_icon(self): + folder_path = create_directory_if_not_exists(self.app) + file_path = os.path.join(folder_path, f"{frappe.scrub(self.label)}.json") + doc_export = self.as_dict(no_nulls=True, no_private_properties=True) + + with open(file_path, "w+") as icon_file_doc: + icon_file_doc.write(frappe.as_json(doc_export) + "\n") + + def delete_desktop_icon_file(self): + folder_path = create_directory_if_not_exists(self.app) + file_path = os.path.join(folder_path, f"{frappe.scrub(self.label)}.json") + if not os.path.exists(file_path): + os.remove(file_path) def after_insert(self): clear_desktop_icons_cache() +def create_directory_if_not_exists(app_name): + app_path = frappe.get_app_path(app_name) + desktop_icon_path = os.path.join(app_path, "desktop_icon") + + if not os.path.exists(desktop_icon_path): + frappe.create_folder(desktop_icon_path) + + return desktop_icon_path + + +def get_desktop_icon_directory(app_name): + app_path = frappe.get_app_path(app_name) + desktop_icon_path = os.path.join(app_path, "desktop_icon") + return desktop_icon_path + + def after_doctype_insert(): frappe.db.add_unique("Desktop Icon", ("module_name", "owner", "standard")) @@ -401,7 +442,19 @@ def make_user_copy(module_name, user): def sync_desktop_icons(): """Sync desktop icons from all apps""" for app in frappe.get_installed_apps(): - sync_from_app(app) + sync_icons(app) + # sync_from_app(app) + + +def sync_icons(app_name): + icon_directory = get_desktop_icon_directory(app_name) + if os.path.exists(icon_directory): + icon_files = [os.path.join(icon_directory, filename) for filename in os.listdir(icon_directory)] + for doc_path in icon_files: + imported = import_file_by_path(doc_path) + if imported: + frappe.db.commit(chain=True) + # print(icon_directory) def sync_from_app(app): diff --git a/frappe/desk/doctype/workspace_sidebar/workspace_sidebar.json b/frappe/desk/doctype/workspace_sidebar/workspace_sidebar.json index bcbf495210..f400b61372 100644 --- a/frappe/desk/doctype/workspace_sidebar/workspace_sidebar.json +++ b/frappe/desk/doctype/workspace_sidebar/workspace_sidebar.json @@ -8,6 +8,7 @@ "field_order": [ "desktop_icon", "title", + "module", "items" ], "fields": [ @@ -30,12 +31,18 @@ "fieldtype": "Table", "label": "Items", "options": "Workspace Sidebar Item" + }, + { + "fieldname": "module", + "fieldtype": "Link", + "label": "Module", + "options": "Module Def" } ], "grid_page_length": 50, "index_web_pages_for_search": 1, "links": [], - "modified": "2025-08-12 12:48:17.192321", + "modified": "2025-09-26 02:16:29.097807", "modified_by": "Administrator", "module": "Desk", "name": "Workspace Sidebar", diff --git a/frappe/desk/doctype/workspace_sidebar/workspace_sidebar.py b/frappe/desk/doctype/workspace_sidebar/workspace_sidebar.py index d57fe94d18..8d259ede14 100644 --- a/frappe/desk/doctype/workspace_sidebar/workspace_sidebar.py +++ b/frappe/desk/doctype/workspace_sidebar/workspace_sidebar.py @@ -1,8 +1,11 @@ # Copyright (c) 2025, Frappe Technologies and contributors # For license information, please see license.txt -# import frappe +import frappe +from frappe import _ +from frappe.desk.doctype.workspace.workspace import is_workspace_manager from frappe.model.document import Document +from frappe.modules.export_file import delete_folder, export_to_files class WorkspaceSidebar(Document): @@ -17,7 +20,18 @@ class WorkspaceSidebar(Document): desktop_icon: DF.Link | None items: DF.Table[WorkspaceSidebarItem] + module: DF.Link | None title: DF.Data | None # end: auto-generated types - pass + def on_update(self): + if self.module and frappe.conf.developer_mode: + export_to_files(record_list=[["Workspace Sidebar", self.name]], record_module=self.module) + + def on_trash(self): + if not is_workspace_manager(): + frappe.throw(_("You need to be Workspace Manager to delete a public workspace.")) + + def after_delete(self): + if self.module and frappe.conf.developer_mode: + delete_folder(self.module, "Workspace Sidebar", self.name) diff --git a/frappe/desk/doctype/workspace_sidebar_item/workspace_sidebar_item.json b/frappe/desk/doctype/workspace_sidebar_item/workspace_sidebar_item.json index 31fccb6b5e..7428b2b59f 100644 --- a/frappe/desk/doctype/workspace_sidebar_item/workspace_sidebar_item.json +++ b/frappe/desk/doctype/workspace_sidebar_item/workspace_sidebar_item.json @@ -1,6 +1,5 @@ { "actions": [], - "allow_rename": 1, "creation": "2025-08-12 12:46:41.926121", "doctype": "DocType", "editable_grid": 1, @@ -61,7 +60,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2025-09-03 11:58:10.222194", + "modified": "2025-09-29 02:21:36.962559", "modified_by": "Administrator", "module": "Desk", "name": "Workspace Sidebar Item", diff --git a/frappe/desk/page/desktop/desktop.js b/frappe/desk/page/desktop/desktop.js index a7473e77ad..56e3d68bbd 100644 --- a/frappe/desk/page/desktop/desktop.js +++ b/frappe/desk/page/desktop/desktop.js @@ -85,7 +85,8 @@ function setup_navbar(navbar_style) { } frappe.router.on("change", function () { - if (frappe.get_route()[0] == "desktop") setup_navbar(); + let navbar_style = $("#icon-style").attr("data-navbar-style"); + if (frappe.get_route()[0] == "desktop") setup_navbar(navbar_style); else $(".navbar").show(); }); diff --git a/frappe/installer.py b/frappe/installer.py index f0827d5315..ea63e82abe 100644 --- a/frappe/installer.py +++ b/frappe/installer.py @@ -17,6 +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.utils import cint, is_git_url from frappe.utils.dashboard import sync_dashboards from frappe.utils.synchronization import filelock @@ -337,6 +338,7 @@ def install_app(name, verbose=False, set_as_patched=True, force=False): sync_fixtures(name) sync_customizations(name) sync_dashboards(name) + sync_desktop_icons() for after_sync in app_hooks.after_sync or []: frappe.get_attr(after_sync)() # diff --git a/frappe/migrate.py b/frappe/migrate.py index 623fa1bcbe..71bc49ed6e 100644 --- a/frappe/migrate.py +++ b/frappe/migrate.py @@ -21,6 +21,7 @@ from frappe.core.doctype.navbar_settings.navbar_settings import sync_standard_it from frappe.core.doctype.scheduled_job_type.scheduled_job_type import sync_jobs from frappe.database.schema import add_column from frappe.deferred_insert import save_to_db as flush_deferred_inserts +from frappe.desk.doctype.desktop_icon.desktop_icon import sync_desktop_icons from frappe.desk.notifications import clear_notifications from frappe.modules.patch_handler import PatchType from frappe.modules.utils import sync_customizations @@ -188,6 +189,9 @@ class SiteMigration: print("Updating installed applications...") frappe.get_single("Installed Applications").update_versions() + print("Syncing Desktop Icons...") + sync_desktop_icons() + print("Executing `after_migrate` hooks...") for app in frappe.get_installed_apps(): for fn in frappe.get_hooks("after_migrate", app_name=app): diff --git a/frappe/model/sync.py b/frappe/model/sync.py index 72d644cd0b..f8f155bb36 100644 --- a/frappe/model/sync.py +++ b/frappe/model/sync.py @@ -9,6 +9,7 @@ import os import frappe from frappe.cache_manager import clear_controller_cache +from frappe.desk.doctype.desktop_icon.desktop_icon import sync_desktop_icons from frappe.model.base_document import get_controller from frappe.modules.import_file import import_file_by_path from frappe.modules.patch_handler import _patch_mode @@ -27,6 +28,7 @@ IMPORTABLE_DOCTYPES = [ ("email", "notification"), ("printing", "print_style"), ("desk", "workspace"), + ("desk", "workspace_sidebar"), ("desk", "onboarding_step"), ("desk", "module_onboarding"), ("desk", "form_tour"), @@ -39,7 +41,6 @@ IMPORTABLE_DOCTYPES = [ def sync_all(force=0, reset_permissions=False): _patch_mode(True) - for app in frappe.get_installed_apps(): sync_for(app, force, reset_permissions=reset_permissions) @@ -93,6 +94,8 @@ def sync_for(app_name, force=0, reset_permissions=False): "workspace_number_card", "workspace_custom_block", "workspace", + "workspace_sidebar", + "workspace_sidebar_item", ]: files.append(os.path.join(FRAPPE_PATH, "desk", "doctype", desk_module, f"{desk_module}.json"))