diff --git a/frappe/boot.py b/frappe/boot.py index df7288370b..72888fb1a4 100644 --- a/frappe/boot.py +++ b/frappe/boot.py @@ -628,6 +628,9 @@ def add_user_specific_sidebar(sidebar_items): if f"-{frappe.session.user.lower()}" in sidebar: sidebars_to_remove.append(sidebar) for sidebar in sidebars_to_remove: - sidebar_name = sidebar.replace(f"-{frappe.session.user.lower()}", "") - sidebar_items[sidebar]["label"] = sidebar_items[sidebar_name]["label"] - sidebar_items[sidebar_name] = sidebar_items.pop(sidebar) + try: + sidebar_name = sidebar.replace(f"-{frappe.session.user.lower()}", "") + sidebar_items[sidebar]["label"] = sidebar_items[sidebar_name]["label"] + sidebar_items[sidebar_name] = sidebar_items.pop(sidebar) + except KeyError: + pass diff --git a/frappe/desk/doctype/desktop_icon/desktop_icon.json b/frappe/desk/doctype/desktop_icon/desktop_icon.json index 29ddf8aeb0..0b9b244579 100644 --- a/frappe/desk/doctype/desktop_icon/desktop_icon.json +++ b/frappe/desk/doctype/desktop_icon/desktop_icon.json @@ -7,27 +7,31 @@ "engine": "InnoDB", "field_order": [ "label", - "standard", "icon_type", "link_type", "link_to", "parent_icon", "sidebar", + "icon_image", "column_break_3", + "standard", "app", "icon", "logo_url", "idx", "link", "hidden", + "restrict_removal", "roles_tab", "roles" ], "fields": [ { + "allow_in_quick_entry": 1, "fieldname": "label", "fieldtype": "Data", "label": "Label", + "translatable": 1, "unique": 1 }, { @@ -42,9 +46,12 @@ "fieldtype": "Column Break" }, { + "allow_in_quick_entry": 1, + "depends_on": "eval: doc.link_type == \"External\"", "fieldname": "link", "fieldtype": "Small Text", - "label": "Link" + "label": "Link", + "max_height": "100px" }, { "fieldname": "icon", @@ -62,10 +69,11 @@ "label": "Logo URL" }, { + "allow_in_quick_entry": 1, "fieldname": "icon_type", "fieldtype": "Select", "label": "Icon Type", - "options": "Folder\nApp\nLink" + "options": "Link\nFolder\nApp" }, { "depends_on": "eval: doc.standard == 1", @@ -75,6 +83,8 @@ "options": "Installed Applications" }, { + "allow_in_quick_entry": 1, + "depends_on": "eval: doc.link_type != \"External\"", "fieldname": "link_to", "fieldtype": "Dynamic Link", "label": "Link To", @@ -93,12 +103,13 @@ "label": "Hidden" }, { + "allow_in_quick_entry": 1, "fieldname": "link_type", "fieldtype": "Select", "in_list_view": 1, "in_standard_filter": 1, "label": "Link Type", - "options": "DocType\nWorkspace\nExternal" + "options": "Workspace Sidebar\nExternal" }, { "fieldname": "roles_tab", @@ -116,10 +127,22 @@ "fieldtype": "Link", "label": "Sidebar", "options": "Workspace Sidebar" + }, + { + "allow_in_quick_entry": 1, + "fieldname": "icon_image", + "fieldtype": "Attach", + "label": "Icon Image" + }, + { + "default": "0", + "fieldname": "restrict_removal", + "fieldtype": "Check", + "label": "Restrict Removal" } ], "links": [], - "modified": "2025-11-15 22:10:10.463829", + "modified": "2026-01-01 19:41:40.557973", "modified_by": "Administrator", "module": "Desk", "name": "Desktop Icon", @@ -139,6 +162,7 @@ "write": 1 } ], + "quick_entry": 1, "read_only": 1, "row_format": "Dynamic", "sort_field": "creation", diff --git a/frappe/desk/doctype/desktop_icon/desktop_icon.py b/frappe/desk/doctype/desktop_icon/desktop_icon.py index 16ecff4f45..fcca383338 100644 --- a/frappe/desk/doctype/desktop_icon/desktop_icon.py +++ b/frappe/desk/doctype/desktop_icon/desktop_icon.py @@ -25,14 +25,16 @@ class DesktopIcon(Document): app: DF.Autocomplete | None hidden: DF.Check - icon_type: DF.Literal["Folder", "App", "Link"] + icon_image: DF.Attach | None + icon_type: DF.Literal["Link", "Folder", "App"] idx: DF.Int label: DF.Data | None link: DF.SmallText | None link_to: DF.DynamicLink | None - link_type: DF.Literal["DocType", "Workspace", "External"] + link_type: DF.Literal["Workspace Sidebar", "External"] logo_url: DF.Data | None parent_icon: DF.Link | None + restrict_removal: DF.Check roles: DF.Table[HasRole] sidebar: DF.Link | None standard: DF.Check @@ -72,22 +74,27 @@ class DesktopIcon(Document): os.remove(file_path) def is_permitted(self, bootinfo): - if frappe.session.user == "Administrator": - return True if self.icon_type == "Folder": return True - workspaces = get_workspace_names(bootinfo.workspaces) - if self.icon_type == "Link": - if self.link_type == "DocType": - return self.link_to in bootinfo.user.can_read - elif self.link_type == "Workspace": - return self.link_to in workspaces elif self.icon_type == "App": - return self.check_app_permission(self.label) + return self.check_app_permission() + else: + try: + items = bootinfo.workspace_sidebar_item[self.label.lower()]["items"] + # + if len(items) == 0: + return False - def check_app_permission(self, app_name): + if len(items) and all(item["type"] == "Section Break" for item in items): + return False + + return True + except KeyError: + return False + + def check_app_permission(self): for a in frappe.get_installed_apps(): - if frappe.get_hooks(app_name=a)["app_title"][0] == app_name or self.app == a: + if frappe.get_hooks(app_name=a)["app_title"][0] == self.label or self.app == a: permission_method = frappe.get_hooks(app_name=a)["add_to_apps_screen"][0].get( "has_permission", None ) diff --git a/frappe/desk/page/desktop/desktop.js b/frappe/desk/page/desktop/desktop.js index 4d363effce..aa57336819 100644 --- a/frappe/desk/page/desktop/desktop.js +++ b/frappe/desk/page/desktop/desktop.js @@ -43,20 +43,53 @@ function get_route(desktop_icon) { if (desktop_icon.link_type == "External" && desktop_icon.link) { route = window.location.origin + desktop_icon.link; } else { - if (desktop_icon.link_type == "Workspace") { - item = { - type: desktop_icon.link_type, - link: frappe.router.slug(desktop_icon.link_to), - }; - } else if (desktop_icon.link_type == "DocType" || desktop_icon.link_type == "list") { - item = { - type: desktop_icon.link_type, - name: desktop_icon.link_to, - }; - } - route = frappe.utils.generate_route(item); - } + let sidebar = frappe.boot.workspace_sidebar_item[desktop_icon.label.toLowerCase()]; + if (desktop_icon.link_type == "Workspace Sidebar" && sidebar) { + let first_link = sidebar.items.find((i) => i.type == "Link"); + if (first_link) { + if (first_link.link_type === "Report") { + let args = { + type: first_link.link_type, + name: first_link.link_to, + }; + if (first_link.report || !frappe.app.sidebar.editor.edit_mode) { + args.is_query_report = + first_link.report.report_type === "Query Report" || + first_link.report.report_type == "Script Report"; + args.report_ref_doctype = first_link.report.ref_doctype; + } + + route = frappe.utils.generate_route(args); + } else if (first_link.link_type == "Workspace") { + let workspaces = frappe.workspaces[frappe.router.slug(first_link.link_to)]; + if (workspaces.public) { + route = "/desk/" + frappe.router.slug(first_link.link_to); + } else { + route = "/desk/private/" + frappe.router.slug(workspaces.title); + } + + if (first_link.route) { + route = first_link.route; + } + } else if (first_link.link_type === "URL") { + route = first_link.url; + } else if (first_link.link_type == "Page" && first_link.route_options) { + route = frappe.utils.generate_route({ + type: first_link.link_type, + name: first_link.link_to, + route_options: JSON.parse(first_link.route_options), + }); + } else { + route = frappe.utils.generate_route({ + type: first_link.link_type, + name: first_link.link_to, + tab: first_link.tab, + }); + } + } + } + } return route; } diff --git a/frappe/desktop_icon/automation.json b/frappe/desktop_icon/automation.json index 7ad385d6f1..c9d6c5ce49 100644 --- a/frappe/desktop_icon/automation.json +++ b/frappe/desktop_icon/automation.json @@ -8,13 +8,14 @@ "icon_type": "Link", "idx": 0, "label": "Automation", - "link_to": "Assignment Rule", - "link_type": "DocType", - "modified": "2025-11-25 13:25:38.018090", + "link_to": "Automation", + "link_type": "Workspace Sidebar", + "modified": "2026-01-01 20:07:01.577056", "modified_by": "Administrator", "name": "Automation", "owner": "Administrator", "parent_icon": "Framework", + "restrict_removal": 0, "roles": [], "standard": 1 } diff --git a/frappe/desktop_icon/build.json b/frappe/desktop_icon/build.json index 7fbe48ef52..955f30c59f 100644 --- a/frappe/desktop_icon/build.json +++ b/frappe/desktop_icon/build.json @@ -8,13 +8,14 @@ "icon_type": "Link", "idx": 0, "label": "Build", - "link_to": "DocType", - "link_type": "DocType", - "modified": "2025-11-25 13:26:22.147009", + "link_to": "Build", + "link_type": "Workspace Sidebar", + "modified": "2026-01-01 20:07:01.609927", "modified_by": "Administrator", "name": "Build", "owner": "Administrator", "parent_icon": "Framework", + "restrict_removal": 0, "roles": [], "standard": 1 } diff --git a/frappe/desktop_icon/data.json b/frappe/desktop_icon/data.json index 42e4c9e984..c343c7fb07 100644 --- a/frappe/desktop_icon/data.json +++ b/frappe/desktop_icon/data.json @@ -8,13 +8,14 @@ "icon_type": "Link", "idx": 0, "label": "Data", - "link_to": "Data Import", - "link_type": "DocType", - "modified": "2025-11-25 13:25:18.769875", + "link_to": "Data", + "link_type": "Workspace Sidebar", + "modified": "2026-01-01 20:07:01.385516", "modified_by": "Administrator", "name": "Data", "owner": "Administrator", "parent_icon": "Framework", + "restrict_removal": 0, "roles": [ { "role": "Accounts User" diff --git a/frappe/desktop_icon/email.json b/frappe/desktop_icon/email.json index e2a273bca5..cc6edc9806 100644 --- a/frappe/desktop_icon/email.json +++ b/frappe/desktop_icon/email.json @@ -8,13 +8,14 @@ "icon_type": "Link", "idx": 0, "label": "Email", - "link_to": "Email Account", - "link_type": "DocType", - "modified": "2025-11-25 13:25:50.374006", + "link_to": "Email", + "link_type": "Workspace Sidebar", + "modified": "2026-01-01 20:07:01.584412", "modified_by": "Administrator", "name": "Email", "owner": "Administrator", "parent_icon": "Framework", + "restrict_removal": 0, "roles": [], "standard": 1 } diff --git a/frappe/desktop_icon/integrations.json b/frappe/desktop_icon/integrations.json index c2c2de7ce0..2e3b7e5acf 100644 --- a/frappe/desktop_icon/integrations.json +++ b/frappe/desktop_icon/integrations.json @@ -8,13 +8,14 @@ "icon_type": "Link", "idx": 0, "label": "Integrations", - "link_to": "Connected App", - "link_type": "DocType", - "modified": "2025-11-25 13:26:12.851783", + "link_to": "Integrations", + "link_type": "Workspace Sidebar", + "modified": "2026-01-01 20:07:01.603540", "modified_by": "Administrator", "name": "Integrations", "owner": "Administrator", "parent_icon": "Framework", + "restrict_removal": 0, "roles": [], "standard": 1 } diff --git a/frappe/desktop_icon/my_workspaces.json b/frappe/desktop_icon/my_workspaces.json index 1bce3c052c..a1bf416297 100644 --- a/frappe/desktop_icon/my_workspaces.json +++ b/frappe/desktop_icon/my_workspaces.json @@ -8,12 +8,13 @@ "icon_type": "Link", "idx": 101, "label": "My Workspaces", - "link_to": "", - "link_type": "DocType", - "modified": "2025-11-17 16:04:09.545862", + "link_to": "My Workspaces", + "link_type": "Workspace Sidebar", + "modified": "2026-01-01 20:07:01.338117", "modified_by": "Administrator", "name": "My Workspaces", "owner": "Administrator", + "restrict_removal": 1, "roles": [], "sidebar": "My Workspaces", "standard": 1 diff --git a/frappe/desktop_icon/printing.json b/frappe/desktop_icon/printing.json index cacde6368b..cf3e9acd4f 100644 --- a/frappe/desktop_icon/printing.json +++ b/frappe/desktop_icon/printing.json @@ -8,13 +8,14 @@ "icon_type": "Link", "idx": 0, "label": "Printing", - "link_to": "Print Format", - "link_type": "DocType", - "modified": "2025-11-25 13:25:33.114392", + "link_to": "Printing", + "link_type": "Workspace Sidebar", + "modified": "2026-01-01 20:07:01.569646", "modified_by": "Administrator", "name": "Printing", "owner": "Administrator", "parent_icon": "Framework", + "restrict_removal": 0, "roles": [ { "role": "System Manager" diff --git a/frappe/desktop_icon/productivity.json b/frappe/desktop_icon/productivity.json index 642371ea7f..41c104514c 100644 --- a/frappe/desktop_icon/productivity.json +++ b/frappe/desktop_icon/productivity.json @@ -8,13 +8,14 @@ "icon_type": "Link", "idx": 0, "label": "Productivity", - "link_to": "ToDo", - "link_type": "DocType", - "modified": "2025-11-25 13:27:47.019742", + "link_to": "Productivity", + "link_type": "Workspace Sidebar", + "modified": "2026-01-01 20:07:01.152305", "modified_by": "Administrator", "name": "Productivity", "owner": "Administrator", "parent_icon": "Framework", + "restrict_removal": 0, "roles": [], "standard": 1 } diff --git a/frappe/desktop_icon/system.json b/frappe/desktop_icon/system.json index 7a4a90c460..154ba9b2af 100644 --- a/frappe/desktop_icon/system.json +++ b/frappe/desktop_icon/system.json @@ -9,12 +9,13 @@ "idx": 0, "label": "System", "link_to": "System", - "link_type": "Workspace", - "modified": "2025-11-25 13:37:01.223244", + "link_type": "Workspace Sidebar", + "modified": "2026-01-01 20:07:01.161174", "modified_by": "Administrator", "name": "System", "owner": "Administrator", "parent_icon": "Framework", + "restrict_removal": 0, "roles": [], "standard": 1 } diff --git a/frappe/desktop_icon/users.json b/frappe/desktop_icon/users.json index 37a9930ded..9c46378b6c 100644 --- a/frappe/desktop_icon/users.json +++ b/frappe/desktop_icon/users.json @@ -9,12 +9,13 @@ "idx": 0, "label": "Users", "link_to": "Users", - "link_type": "Workspace", - "modified": "2025-11-25 13:26:04.757422", + "link_type": "Workspace Sidebar", + "modified": "2026-01-01 20:07:01.597388", "modified_by": "Administrator", "name": "Users", "owner": "Administrator", "parent_icon": "Framework", + "restrict_removal": 0, "roles": [], "standard": 1 } diff --git a/frappe/desktop_icon/website.json b/frappe/desktop_icon/website.json index ed16339762..1d4461a6b8 100644 --- a/frappe/desktop_icon/website.json +++ b/frappe/desktop_icon/website.json @@ -9,12 +9,13 @@ "idx": 0, "label": "Website", "link_to": "Website", - "link_type": "Workspace", - "modified": "2025-11-25 13:25:58.604796", + "link_type": "Workspace Sidebar", + "modified": "2026-01-01 20:07:01.591355", "modified_by": "Administrator", "name": "Website", "owner": "Administrator", "parent_icon": "Framework", + "restrict_removal": 0, "roles": [], "standard": 1 } diff --git a/frappe/locale/de.po b/frappe/locale/de.po index 83bc8174c5..01c665afe1 100644 --- a/frappe/locale/de.po +++ b/frappe/locale/de.po @@ -3,7 +3,7 @@ msgstr "" "Project-Id-Version: frappe\n" "Report-Msgid-Bugs-To: developers@frappe.io\n" "POT-Creation-Date: 2025-12-21 09:35+0000\n" -"PO-Revision-Date: 2025-12-24 20:23\n" +"PO-Revision-Date: 2026-01-02 22:29\n" "Last-Translator: developers@frappe.io\n" "Language-Team: German\n" "MIME-Version: 1.0\n" @@ -7784,7 +7784,7 @@ msgstr "DocType ist eine Tabelle / ein Formular in der Anwendung." #: frappe/integrations/doctype/webhook/webhook.py:83 msgid "DocType must be Submittable for the selected Doc Event" -msgstr "DocType muss für das ausgewählte Doc-Ereignis übermittelt werden" +msgstr "DocType muss für das ausgewählte Doc-Ereignis buchbar sein" #: frappe/client.py:406 msgid "DocType must be a string" @@ -8071,7 +8071,7 @@ msgstr "Der Dokumenttyp kann nicht importiert werden" #: frappe/permissions.py:148 msgid "Document Type is not submittable" -msgstr "Der Dokumenttyp kann nicht übermittelt werden" +msgstr "Der Dokumenttyp kann nicht gebucht werden" #. Label of the document_type (Link) field in DocType 'Milestone Tracker' #: frappe/automation/doctype/milestone_tracker/milestone_tracker.json @@ -14022,7 +14022,7 @@ msgstr "Ist Standard" #: frappe/core/doctype/doctype/doctype.json #: frappe/core/doctype/doctype/doctype_list.js:40 msgid "Is Submittable" -msgstr "Abschließbar" +msgstr "Kann gebucht werden" #. Label of the is_system_generated (Check) field in DocType 'Custom Field' #. Label of the is_system_generated (Check) field in DocType 'Customize Form @@ -18253,7 +18253,7 @@ msgstr "Onboarding abgeschlossen" #: frappe/core/doctype/doctype/doctype.json #: frappe/core/doctype/doctype/doctype_list.js:43 msgid "Once submitted, submittable documents cannot be changed. They can only be Cancelled and Amended." -msgstr "Einmal eingereichte Dokumente können nicht mehr geändert werden. Sie können nur storniert oder berichtigt werden." +msgstr "Einmal gebuchte Dokumente können nicht mehr geändert werden. Sie können nur storniert oder berichtigt werden." #: frappe/core/page/permission_manager/permission_manager_help.html:35 msgid "Once you have set this, the users will only be able access documents (eg. Blog Post) where the link exists (eg. Blogger)." diff --git a/frappe/locale/fa.po b/frappe/locale/fa.po index 202b0acc90..1a5f81a23d 100644 --- a/frappe/locale/fa.po +++ b/frappe/locale/fa.po @@ -3,7 +3,7 @@ msgstr "" "Project-Id-Version: frappe\n" "Report-Msgid-Bugs-To: developers@frappe.io\n" "POT-Creation-Date: 2025-12-21 09:35+0000\n" -"PO-Revision-Date: 2025-12-29 21:45\n" +"PO-Revision-Date: 2026-01-01 22:27\n" "Last-Translator: developers@frappe.io\n" "Language-Team: Persian\n" "MIME-Version: 1.0\n" @@ -1017,7 +1017,7 @@ msgstr "اکتیو دایرکتوری" #. Label of the active_domains (Table) field in DocType 'Domain Settings' #: frappe/core/doctype/domain_settings/domain_settings.json msgid "Active Domains" -msgstr "دامنه های فعال" +msgstr "دامنه‌های فعال" #. Label of the active_sessions (Table) field in DocType 'User' #. Label of the active_sessions (Int) field in DocType 'System Health Report' @@ -1660,7 +1660,7 @@ msgstr "تمامی فیلدها برای ارسال نظر ضروری است." #. Description of the 'Document States' (Table) field in DocType 'Workflow' #: frappe/workflow/doctype/workflow/workflow.json msgid "All possible Workflow States and roles of the workflow. Docstatus Options: 0 is \"Saved\", 1 is \"Submitted\" and 2 is \"Cancelled\"" -msgstr "همه حالت های گردش کار ممکن و نقش‌های گردش کار. گزینه‌های Docstatus: 0 \"ذخیره شده\"، 1 \"ارسال شده\" و 2 \"لغو شده\" است." +msgstr "تمام وضعیت‌ها و نقش‌های ممکن گردش کار. گزینه‌های وضعیت اسناد: ۰ به معنای «ذخیره شده»، ۱ به معنای «ارسال شده» و ۲ به معنای «لغو شده» است" #: frappe/utils/password_strength.py:183 msgid "All-uppercase is almost as easy to guess as all-lowercase." @@ -5026,7 +5026,7 @@ msgstr "گرد کردن تجاری" #. Label of the commit (Check) field in DocType 'System Console' #: frappe/desk/doctype/system_console/system_console.json msgid "Commit" -msgstr "مرتکب شدن" +msgstr "کامیت" #. Label of the committed (Check) field in DocType 'Console Log' #: frappe/desk/doctype/console_log/console_log.json @@ -5236,7 +5236,7 @@ msgstr "پیکربندی ستون‌ها" #: frappe/core/doctype/recorder/recorder_list.js:200 msgid "Configure Recorder" -msgstr "ضبط کننده را پیکربندی کنید" +msgstr "پیکربندی ضبط کننده" #: frappe/public/js/print_format_builder/Field.vue:103 msgid "Configure columns for {0}" @@ -5905,7 +5905,7 @@ msgstr "" #. Label of the current_job_id (Link) field in DocType 'RQ Worker' #: frappe/core/doctype/rq_worker/rq_worker.json msgid "Current Job ID" -msgstr "شناسه شغلی فعلی" +msgstr "شناسه کار فعلی" #. Label of the current_value (Int) field in DocType 'Document Naming Settings' #: frappe/core/doctype/document_naming_settings/document_naming_settings.json @@ -13988,17 +13988,17 @@ msgstr "شناسه کار" #. Label of the job_info_section (Section Break) field in DocType 'RQ Job' #: frappe/core/doctype/rq_job/rq_job.json msgid "Job Info" -msgstr "اطلاعات شغلی" +msgstr "اطلاعات کار" #. Label of the job_name (Data) field in DocType 'RQ Job' #: frappe/core/doctype/rq_job/rq_job.json msgid "Job Name" -msgstr "نام شغل" +msgstr "نام کار" #. Label of the job_status_section (Section Break) field in DocType 'RQ Job' #: frappe/core/doctype/rq_job/rq_job.json msgid "Job Status" -msgstr "وضعیت شغلی" +msgstr "وضعیت کار" #: frappe/core/doctype/data_import/data_import.js:191 #: frappe/core/doctype/rq_job/rq_job.js:24 @@ -18977,11 +18977,11 @@ msgstr "پچ" #. Name of a DocType #: frappe/core/doctype/patch_log/patch_log.json msgid "Patch Log" -msgstr "ثبت وصله" +msgstr "لاگ پچ" #: frappe/modules/patch_handler.py:136 msgid "Patch type {} not found in patches.txt" -msgstr "نوع وصله {} در patches.txt یافت نشد" +msgstr "نوع پچ {} در patches.txt یافت نشد" #. Label of the path (Data) field in DocType 'API Request Log' #. Label of the path (Small Text) field in DocType 'Package Release' @@ -20322,7 +20322,7 @@ msgstr "ویژگی" #: frappe/custom/doctype/customize_form_field/customize_form_field.json #: frappe/website/doctype/web_form_field/web_form_field.json msgid "Property Depends On" -msgstr "اموال بستگی دارد" +msgstr "ویژگی بستگی دارد به" #. Name of a DocType #: frappe/custom/doctype/property_setter/property_setter.json @@ -20647,7 +20647,7 @@ msgstr "در صف ارسال می‌توانید پیشرفت را در {0} دن #: frappe/desk/page/backups/backups.py:96 msgid "Queued for backup. You will receive an email with the download link" -msgstr "در صف پشتیبان گیری یک ایمیل با لینک دانلود دریافت خواهید کرد" +msgstr "در صف پشتیبان‌گیری قرار گرفت. ایمیلی حاوی لینک دانلود دریافت خواهید کرد" #. Label of the queues (Data) field in DocType 'System Health Report Workers' #: frappe/desk/doctype/system_health_report_workers/system_health_report_workers.json @@ -20694,7 +20694,7 @@ msgstr "لاگ اطلاعات خام" #. Name of a DocType #: frappe/core/doctype/rq_job/rq_job.json msgid "RQ Job" -msgstr "شغل RQ" +msgstr "کار RQ" #. Name of a DocType #: frappe/core/doctype/rq_worker/rq_worker.json @@ -23192,12 +23192,12 @@ msgstr "انتخاب محدوده تاریخ" #: frappe/public/js/frappe/doctype/index.js:178 #: frappe/website/doctype/web_form/web_form.json msgid "Select DocType" -msgstr "DocType را انتخاب کنید" +msgstr "انتخاب DocType" #. Label of the reference_doctype (Link) field in DocType 'Data Export' #: frappe/core/doctype/data_export/data_export.json msgid "Select Doctype" -msgstr "Doctype را انتخاب کنید" +msgstr "انتخاب DocType" #: frappe/printing/page/print_format_builder_beta/print_format_builder_beta.js:50 #: frappe/workflow/page/workflow_builder/workflow_builder.js:50 @@ -28366,7 +28366,7 @@ msgstr "از شناسه ایمیل متفاوت استفاده کنید" #. Description of the 'Detect CSV type' (Check) field in DocType 'Data Import' #: frappe/core/doctype/data_import/data_import.json msgid "Use if the default settings don't seem to detect your data correctly" -msgstr "استفاده کنید اگر تنظیمات پیش‌فرض به درستی داده‌های شما را شناسایی نمی‌کنند" +msgstr "اگر تنظیمات پیش‌فرض به درستی داده‌های شما را شناسایی نمی‌کنند، از این گزینه استفاده کنید" #: frappe/model/db_query.py:509 msgid "Use of sub-query or function is restricted" @@ -30761,7 +30761,7 @@ msgstr "نظر داد" #. Inspector' #: frappe/core/doctype/permission_inspector/permission_inspector.json msgid "create" -msgstr "ایجاد كردن" +msgstr "ایجاد کردن" #. Option for the 'Indicator Color' (Select) field in DocType 'Workspace' #: frappe/desk/doctype/workspace/workspace.json diff --git a/frappe/modules/utils.py b/frappe/modules/utils.py index 35a3f1f708..64d2463c2f 100644 --- a/frappe/modules/utils.py +++ b/frappe/modules/utils.py @@ -176,6 +176,24 @@ def sync_customizations_for_doctype(data: dict, folder: str, filename: str = "") custom_field.flags.ignore_validate = True custom_field.update(d) custom_field.db_update() + case "DocType Link": + for d in data[key]: + link = frappe.db.get_value( + "DocType Link", + { + "parent": doc_type, + "link_doctype": d.get("link_doctype"), + "link_fieldname": d.get("link_fieldname"), + }, + ) + if not link: + d["owner"] = "Administrator" + _insert(d) + else: + doc_link = frappe.get_doc("DocType Link", link) + doc_link.flags.ignore_validate = True + doc_link.update(d) + doc_link.db_update() case "Property Setter": # Property setter implement their own deduplication, we can just sync them as is for d in data[key]: @@ -205,6 +223,9 @@ def sync_customizations_for_doctype(data: dict, folder: str, filename: str = "") sync("custom_fields", "Custom Field", "dt") update_schema = True + if data.get("links", False): + sync("links", "DocType Link", "parent") + if data["property_setters"]: sync("property_setters", "Property Setter", "doc_type") diff --git a/frappe/patches.txt b/frappe/patches.txt index 5b076fab59..93cb29f3f9 100644 --- a/frappe/patches.txt +++ b/frappe/patches.txt @@ -252,3 +252,4 @@ frappe.patches.v16_0.auto_generate_desktop_icon_and_sidebar frappe.patches.v16_0.add_private_workspaces_to_sidebar frappe.core.doctype.communication_link.patches.copy_communication_date_to_link frappe.core.doctype.communication.patches.drop_ref_dt_dn_index +frappe.patches.v16_0.change_link_type_to_workspace_sidebar diff --git a/frappe/patches/v16_0/change_link_type_to_workspace_sidebar.py b/frappe/patches/v16_0/change_link_type_to_workspace_sidebar.py new file mode 100644 index 0000000000..32e2d2269f --- /dev/null +++ b/frappe/patches/v16_0/change_link_type_to_workspace_sidebar.py @@ -0,0 +1,20 @@ +import frappe + + +def execute(): + desktop_icons = frappe.get_all( + "Desktop Icon", + filters={ + "icon_type": "Link", + "link_type": ["in", ["Workspace", "DocType"]], + }, + ) + + for icon in desktop_icons: + icon_doc = frappe.get_doc("Desktop Icon", icon.name) + if frappe.db.exists("Workspace Sidebar", icon.name): + icon_doc.link_type = "Workspace Sidebar" + icon_doc.link_to = icon.name + icon_doc.save() + + frappe.db.commit() diff --git a/frappe/public/js/frappe/form/controls/link.js b/frappe/public/js/frappe/form/controls/link.js index 24c3a26eaf..d174c42cd5 100644 --- a/frappe/public/js/frappe/form/controls/link.js +++ b/frappe/public/js/frappe/form/controls/link.js @@ -431,7 +431,7 @@ frappe.ui.form.ControlLink = class ControlLink extends frappe.ui.form.ControlDat no_spinner: true, cache: use_get, args: args, - callback: (r) => { + callback: async (r) => { if (!window.Cypress && !this.$input.is(":focus")) { return; } @@ -441,7 +441,7 @@ frappe.ui.form.ControlLink = class ControlLink extends frappe.ui.form.ControlDat let filter_string = this.df.filter_description ? this.df.filter_description : args.filters - ? this.get_filter_description(args.filters) + ? await this.get_filter_description(args.filters) : null; if (filter_string) { r.message.push({ @@ -533,14 +533,9 @@ frappe.ui.form.ControlLink = class ControlLink extends frappe.ui.form.ControlDat } } - get_filter_description(filters) { - let doctype = this.get_options(); + async get_filter_description(filters) { + const doctype = this.get_options(); let filter_array = []; - let meta = null; - - frappe.model.with_doctype(doctype, () => { - meta = frappe.get_meta(doctype); - }); // convert object style to array if (!Array.isArray(filters)) { @@ -549,50 +544,175 @@ frappe.ui.form.ControlLink = class ControlLink extends frappe.ui.form.ControlDat if (!Array.isArray(value)) { value = ["=", value]; } - filter_array.push([fieldname, ...value]); // fieldname, operator, value + filter_array.push([doctype, fieldname, ...value]); // [doctype, fieldname, operator, value] } } else { - filter_array = filters; + filter_array = filters.slice(); // clone } - // add doctype if missing - filter_array = filter_array.map((filter) => { - if (filter.length === 3) { - return [doctype, ...filter]; // doctype, fieldname, operator, value - } - return filter; - }); - - function get_filter_description(filter) { - let doctype = filter[0]; - let fieldname = filter[1]; - let docfield = frappe.meta.get_docfield(doctype, fieldname); - let label = docfield ? docfield.label : frappe.model.unscrub(fieldname); + // add doctype if missing: [doctype, fieldname, operator, value] + filter_array = filter_array.map((f) => (f.length === 3 ? [doctype, ...f] : f)); + function formatValueForDisplay(docfield, val) { + // Check boolean fields -> show Yes/No (localized) + // Handles 0/1, true/false values if (docfield && docfield.fieldtype === "Check") { - filter[3] = filter[3] ? __("Yes") : __("No"); + return val == 1 || val === true ? __("Yes") : __("No"); } - if (filter[3] && Array.isArray(filter[3]) && filter[3].length > 5) { - filter[3] = filter[3].slice(0, 5); - filter[3].push("..."); + // Array values -> truncate to first 5, append "..." + if (Array.isArray(val)) { + const filtered = val.filter((v) => v != null && v !== ""); + const arr = filtered.slice(0, 5).map((v) => { + // Strings in quotes, numbers/dates not quoted + if (typeof v === "string") { + return `"${String(__(v))}"`; + } + // Numbers, dates, etc. - not translated, not quoted + return String(v); + }); + if (filtered.length > 5) arr.push("..."); + return arr.join(", "); } - let value; - if (filter[3] && Array.isArray(filter[3])) { - value = filter[3].map((v) => String(__(v)).bold()).join(", "); - } else if (filter[3] == null || filter[3] === "") { - value = __("empty").bold(); - } else { - value = String(__(filter[3])).bold(); + // Null / empty + if (val == null || val === "") { + return __("empty", null, "Comparison value is empty"); } - return [__(label).bold(), __(filter[2]), value].join(" "); + // Format based on type: strings in quotes, numbers/dates not quoted + if (typeof val === "string") { + return `"${String(__(val))}"`; + } + + // Numbers, dates, etc. - not translated, not quoted + return frappe.format(val, docfield || {}); } - let filter_string = filter_array.map(get_filter_description).join(", "); + async function describe_filter(filter) { + // expect [doctype, fieldname, operator, value] + const _doctype = filter[0]; + const fieldname = filter[1]; + const operator = filter[2]; + let value = filter[3]; - return __("Filters applied for {0}", [filter_string]); + // Ensure metadata is loaded for this doctype before accessing docfield + await frappe.model.with_doctype(_doctype, () => {}); + + const docfield = frappe.meta.get_docfield(_doctype, fieldname); + const label = docfield ? docfield.label : frappe.model.unscrub(fieldname); + const fieldtype = docfield ? docfield.fieldtype : null; + + const labelDisplay = `${String(__(label, null, _doctype))}`; + const valueDisplay = formatValueForDisplay(docfield, value); + const is_time_like = ["Date", "Datetime", "Time"].includes(fieldtype); + + // Handle all operators with translation and interpolation in one call + switch (operator) { + case "=": + if (fieldtype === "Check") { + if (fieldname === "enabled") { + return value == 1 + ? __("is enabled") // ["enabled", "=", 1] + : __("is disabled"); // ["enabled", "=", 0] + } + + if (fieldname === "disabled") { + return value == 1 + ? __("is disabled") // ["disabled", "=", 1] + : __("is enabled"); // ["disabled", "=", 0] + } + + return value == 1 + ? __("{0} is enabled", [labelDisplay]) + : __("{0} is disabled", [labelDisplay]); + } + return __("{0} equals {1}", [labelDisplay, valueDisplay]); + case "!=": + if (fieldtype === "Check") { + if (fieldname === "enabled") { + return value == 1 + ? __("is disabled") // ["enabled", "!=", 1] + : __("is enabled"); // ["enabled", "!=", 0] + } + + if (fieldname === "disabled") { + return value == 1 + ? __("is enabled") // ["disabled", "!=", 1] + : __("is disabled"); // ["disabled", "!=", 0] + } + + return value == 1 + ? __("{0} is disabled", [labelDisplay]) + : __("{0} is enabled", [labelDisplay]); + } + return __("{0} is not equal to {1}", [labelDisplay, valueDisplay]); + case "in": + return __("{0} is one of {1}", [labelDisplay, valueDisplay]); + case "not in": + return __("{0} is not one of {1}", [labelDisplay, valueDisplay]); + case "like": + return __("{0} contains {1}", [labelDisplay, valueDisplay]); + case "not like": + return __("{0} does not contain {1}", [labelDisplay, valueDisplay]); + case ">": + if (is_time_like) { + return __("{0} is after {1}", [labelDisplay, valueDisplay]); + } + return __("{0} is greater than {1}", [labelDisplay, valueDisplay]); + case "<": + if (is_time_like) { + return __("{0} is before {1}", [labelDisplay, valueDisplay]); + } + return __("{0} is less than {1}", [labelDisplay, valueDisplay]); + case ">=": + if (is_time_like) { + return __("{0} is on or after {1}", [labelDisplay, valueDisplay]); + } + return __("{0} is greater than or equal to {1}", [labelDisplay, valueDisplay]); + case "<=": + if (is_time_like) { + return __("{0} is on or before {1}", [labelDisplay, valueDisplay]); + } + return __("{0} is less than or equal to {1}", [labelDisplay, valueDisplay]); + case "is": + if (value == "set") { + return __("{0} is set", [labelDisplay]); + } + if (value == "not set") { + return __("{0} is not set", [labelDisplay]); + } + return __("{0} is {1}", [labelDisplay, valueDisplay]); + case "between": + if (Array.isArray(value) && value.length === 2) { + return __("{0} is between {1} and {2}", [ + labelDisplay, + formatValueForDisplay(docfield, value[0]), + formatValueForDisplay(docfield, value[1]), + ]); + } + return __("{0} is between {1}", [labelDisplay, valueDisplay]); + case "descendants of": + return __("{0} is a descendant of {1}", [labelDisplay, valueDisplay]); + case "ancestors of": + return __("{0} is an ancestor of {1}", [labelDisplay, valueDisplay]); + case "not descendants of": + return __("{0} is not a descendant of {1}", [labelDisplay, valueDisplay]); + case "not ancestors of": + return __("{0} is not an ancestor of {1}", [labelDisplay, valueDisplay]); + case "timespan": + return __("{0} is within {1}", [labelDisplay, valueDisplay]); + default: + // Fallback for unknown operators (no translatable text here) + return [labelDisplay, operator, valueDisplay].join(" "); + } + } + + const descriptions = await Promise.all( + filter_array.map((filter) => describe_filter(filter)) + ); + const filter_string = frappe.utils.comma_and(descriptions); + return __("Filtered by: {0}.", [filter_string]); } set_custom_query(args) { diff --git a/frappe/public/js/frappe/ui/page.html b/frappe/public/js/frappe/ui/page.html index 059f830de7..070103d0bd 100644 --- a/frappe/public/js/frappe/ui/page.html +++ b/frappe/public/js/frappe/ui/page.html @@ -31,7 +31,7 @@