Merge remote-tracking branch 'upstream/develop' into capture-client-events

This commit is contained in:
Saqib Ansari 2026-01-03 20:34:00 +05:30
commit f0c6c349e0
27 changed files with 402 additions and 140 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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)."

View file

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

View file

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

View file

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

View file

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

View file

@ -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 = `<i>${String(__(label, null, _doctype))}</i>`;
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) {

View file

@ -31,7 +31,7 @@
<div class="standard-actions flex">
<span class="page-icon-group hide hidden-xs hidden-sm"></span>
<div class="menu-btn-group hide">
<button type="button" class="btn btn-default icon-btn" data-toggle="dropdown" aria-expanded="false" aria-label="{{ __("Menu") }}">
<button type="button" class="btn btn-default icon-btn menu-more-button" data-toggle="dropdown" aria-expanded="false" aria-label="{{ __("Menu") }}">
<span>
<span class="menu-btn-group-label">
<svg class="icon icon-sm">

View file

@ -231,6 +231,10 @@ frappe.breadcrumbs = {
if (view === "form") {
let last_crumb = this.$breadcrumbs.find("li").last();
last_crumb.addClass("disabled");
if (frappe.is_mobile()) {
last_crumb.addClass("ellipsis");
last_crumb.find("a").addClass("ellipsis");
}
last_crumb.css("cursor", "copy");
last_crumb.click((event) => {
event.stopImmediatePropagation();

View file

@ -108,13 +108,6 @@ frappe.views.CommunicationComposer = class {
fieldtype: "Link",
options: "Email Template",
fieldname: "email_template",
get_query: () => {
return {
filters: {
use_html: me.dialog.get_value("use_html"),
},
};
},
},
{
fieldtype: "HTML",

View file

@ -321,18 +321,28 @@ body {
@media (max-width: 480px) {
body[data-route^="Form"] {
.navbar-breadcrumbs {
max-width: 165px;
}
.page-title {
width: 100%;
.title-area {
flex-direction: column;
align-items: baseline !important;
.indicator-pill {
margin-left: 5px;
}
.mobile-no-divider li a {
font-size: 14px;
padding-bottom: 5px;
}
}
}
.page-actions {
.standard-actions {
.menu-btn-group .menu-more-button {
margin-left: 0;
}
}
}
}
}

View file

@ -35,7 +35,7 @@
.title-area {
display: inline-flex;
align-items: center;
align-items: baseline;
.indicator-pill {
margin-left: var(--margin-sm);
}

View file

@ -89,7 +89,7 @@ class TestUtils(IntegrationTestCase):
)
def test_export_customizations_with_module_filter(self):
# create two customizations, one matching the module, one under a different module
with note_customizations() as (custom_field, property_setter):
with note_customizations() as (custom_field, property_setter, _doctype_link):
custom_field.db_set("module", "Custom")
property_setter.db_set("module", "Custom")
@ -135,7 +135,7 @@ class TestUtils(IntegrationTestCase):
os.access(frappe.get_app_path("frappe"), os.W_OK), "Only run if frappe app paths is writable"
)
def test_sync_customizations(self):
with note_customizations() as (custom_field, property_setter):
with note_customizations() as (custom_field, property_setter, doctype_link):
file_path = export_customizations(module="Custom", doctype="Note", sync_on_migrate=True)
custom_field.db_set("modified", now_datetime())
custom_field.reload()
@ -152,6 +152,7 @@ class TestUtils(IntegrationTestCase):
sync_customizations(app="frappe")
self.assertTrue(property_setter.doctype, property_setter.name)
self.assertTrue(custom_prop_setter.doctype, custom_prop_setter.name)
self.assertTrue(doctype_link.doctype, doctype_link.name)
self.assertTrue(file_path.endswith("/custom/custom/note.json"))
self.assertTrue(os.path.exists(file_path))
@ -232,9 +233,23 @@ def note_customizations():
property_setter = make_property_setter(
"Note", fieldname="content", property="bold", value="1", property_type="Check"
)
yield custom_field, property_setter
doctype_link = frappe.get_doc(
{
"doctype": "DocType Link",
"parent": "Note",
"parenttype": "DocType",
"parentfield": "links",
"link_doctype": "User",
"link_fieldname": "owner",
"group": "Test Group",
}
).insert()
yield custom_field, property_setter, doctype_link
finally:
custom_field.delete()
property_setter.delete()
doctype_link.delete()
trim_table("Note", dry_run=False)
delete_path(frappe.get_module_path("Desk", "Note"))