diff --git a/cypress/integration/awesome_bar.js b/cypress/integration/awesome_bar.js index f82fe19c24..e4b2febe3f 100644 --- a/cypress/integration/awesome_bar.js +++ b/cypress/integration/awesome_bar.js @@ -11,10 +11,7 @@ context("Awesome Bar", () => { beforeEach(() => { cy.get("body").click(0, 0); // Click on some blank space to avoid any modals. - let txt = `Search or type a command (${ - window.navigator.platform === "MacIntel" ? "⌘" : "Ctrl" - } + K)`; - cy.contains(txt).as("awesome_bar_search"); + cy.get("#navbar-modal-search").as("awesome_bar_search"); cy.get("@awesome_bar_search").click(); cy.get("#navbar-search").as("awesome_bar"); cy.get("#navbar-search").type("{selectall}"); diff --git a/frappe/client.py b/frappe/client.py index 13755531cc..6088f8a36a 100644 --- a/frappe/client.py +++ b/frappe/client.py @@ -416,8 +416,8 @@ def validate_link(doctype: str, docname: str, fields=None): if not isinstance(docname, str): frappe.throw(_("Document Name must be a string")) + parent_doctype = None if doctype != "DocType": - parent_doctype = None if frappe.get_meta(doctype).istable: # needed for links to child rows parent_doctype = frappe.db.get_value(doctype, docname, "parenttype") if not ( @@ -453,7 +453,7 @@ def validate_link(doctype: str, docname: str, fields=None): return values try: - values.update(get_value(doctype, fields, docname)) + values.update(get_value(doctype, fields, docname, parent=parent_doctype)) except frappe.PermissionError: frappe.clear_last_message() frappe.msgprint( diff --git a/frappe/core/doctype/communication/mixins.py b/frappe/core/doctype/communication/mixins.py index 9fe388cbb9..4318a451e5 100644 --- a/frappe/core/doctype/communication/mixins.py +++ b/frappe/core/doctype/communication/mixins.py @@ -271,7 +271,7 @@ class CommunicationEmailMixin: ) bcc = self.get_mail_bcc_with_displayname(is_inbound_mail_communcation=is_inbound_mail_communcation) - if not (recipients or cc): + if not (recipients or cc or bcc): return {} final_attachments = self.mail_attachments( diff --git a/frappe/core/doctype/data_import/data_import.py b/frappe/core/doctype/data_import/data_import.py index c6bfe0b66d..33921155dd 100644 --- a/frappe/core/doctype/data_import/data_import.py +++ b/frappe/core/doctype/data_import/data_import.py @@ -93,6 +93,7 @@ class DataImport(Document): if not self.google_sheets_url: return validate_google_sheets_url(self.google_sheets_url) + self.get_importer() def set_payload_count(self, importer: Importer | None = None): if self.import_file: diff --git a/frappe/core/doctype/data_import/importer.py b/frappe/core/doctype/data_import/importer.py index a880bf2aa6..828e44743b 100644 --- a/frappe/core/doctype/data_import/importer.py +++ b/frappe/core/doctype/data_import/importer.py @@ -483,6 +483,35 @@ class ImportFile: title=_("Template Error"), ) + def validate_columns_of_import_file(self, data): + mandatory_fields = self.get_mandatory_fields() + headers = data[0] if data else [] + + if len(headers) == 1 and ";" in headers[0]: + return + + if not len(headers): + frappe.throw(_("Import template should contain a Header row."), title=_("Template Error")) + + for field in mandatory_fields: + if field not in headers: + frappe.throw( + _( + "Mandatory field {0} is missing in the import template for {1}. Please correct the template and try again." + ).format(frappe.bold(field), frappe.bold(self.doctype)), + title=_("Template Error"), + ) + + def get_mandatory_fields(self): + meta = frappe.get_meta(self.doctype) + mandatory_fields = [] + + for df in meta.fields: + if df.reqd and df.fieldtype not in no_value_fields: + mandatory_fields.append(df.label) + + return mandatory_fields + def get_data_for_import_preview(self): """Adds a serial number column as the first column""" @@ -618,6 +647,7 @@ class ImportFile: elif extension == "xls": data = read_xls_file_from_attached_file(content) + self.validate_columns_of_import_file(data) return data diff --git a/frappe/core/doctype/system_settings/system_settings.json b/frappe/core/doctype/system_settings/system_settings.json index 7eeed0415a..3663c9df6c 100644 --- a/frappe/core/doctype/system_settings/system_settings.json +++ b/frappe/core/doctype/system_settings/system_settings.json @@ -95,6 +95,7 @@ "system_updates_section", "disable_system_update_notification", "disable_change_log_notification", + "column_break_ewhs", "hide_empty_read_only_fields", "disable_product_suggestion", "backups_tab", @@ -777,12 +778,16 @@ "fieldname": "disable_product_suggestion", "fieldtype": "Check", "label": "Disable Product Suggestion" + }, + { + "fieldname": "column_break_ewhs", + "fieldtype": "Column Break" } ], "icon": "fa fa-cog", "issingle": 1, "links": [], - "modified": "2025-11-23 13:17:57.577690", + "modified": "2025-12-01 00:12:17.823242", "modified_by": "Administrator", "module": "Core", "name": "System Settings", diff --git a/frappe/desk/page/desktop/desktop.css b/frappe/desk/page/desktop/desktop.css index 8def3c5658..2e5df23fc3 100644 --- a/frappe/desk/page/desktop/desktop.css +++ b/frappe/desk/page/desktop/desktop.css @@ -32,24 +32,22 @@ .desktop-search-wrapper{ flex: 1; - max-width: 396px; position: relative; } -#navbar-modal-search{ - padding-left: 32px; +.desktop-search-wrapper span { + color: var(--text-light); } -.desktop-search-icon{ - position: absolute; - left: 10px; - top: 4px; + +#navbar-modal-search{ + background-color: var(--control-bg); } #brand-logo{ width: auto; } .desktop-search-icon > .icon { - stroke: var(--ink-gray-4); stroke-width: 1px; + margin-bottom: 2px; } .desktop-container{ diff --git a/frappe/desk/page/desktop/desktop.html b/frappe/desk/page/desktop/desktop.html index 131cec9751..e2e541447c 100644 --- a/frappe/desk/page/desktop/desktop.html +++ b/frappe/desk/page/desktop/desktop.html @@ -8,17 +8,24 @@ alt="{{ _("App Logo") |e }}" > - ${item.label} + + `); if (!item.url) { item_wrapper.on("click", function () { - item.onClick(); - me.opts.onItemClick && me.opts.onItemClick(me.opts.parent); - me.hide(); + item.onClick && item.onClick(); + if (!(item.items && item.items.length)) { + me.opts.onItemClick && me.opts.onItemClick(me.opts.parent); + me.hide(); + } }); + } else if (item.items) { + $(); } else { $(item_wrapper).find("a").attr("href", item.url); } item_wrapper.appendTo(this.template); + if (item.items) { + this.handle_nested_menu(item_wrapper, item); + } + } + handle_nested_menu(item_wrapper, item) { + frappe.ui.create_menu({ + parent: item_wrapper, + menu_items: item.items, + nested: true, + parent_menu: this.name, + }); } show(parent) { - this.close_all_other_menu(); + // this.close_all_other_menu(); this.make(); @@ -58,12 +78,25 @@ frappe.ui.menu = class ContextMenu { const height = $(parent).outerHeight(); this.left_offset = 0; this.gap = 4; - this.template.css({ - display: "block", - position: "absolute", - top: offset.top + height + this.gap + "px", - left: offset.left, - }); + if (this.opts.nested && this.opts.parent_menu) { + let dropdown = frappe.menu_map[this.opts.parent_menu].template; + let width = dropdown.outerWidth(); + let offset = $(dropdown).offset(); + this.template.css({ + display: "block", + position: "absolute", + top: offset.top + "px", + left: offset.left + width + this.gap + "px", + }); + } else { + this.template.css({ + display: "block", + position: "absolute", + top: offset.top + height + this.gap + "px", + left: offset.left, + }); + } + if (this.open_on_left) { this.left_offset = parent.getBoundingClientRect().width; this.template.css({ @@ -151,12 +184,14 @@ frappe.ui.create_menu = function (opts) { $(document).on("click", function () { if (frappe.menu_map[context_menu.name].visible) { frappe.menu_map[context_menu.name].hide(); + opts.onHide && opts.onHide(opts.parent); } }); $(document).on("keydown", function (e) { if (e.key === "Escape" && frappe.menu_map[context_menu.name].visible) { frappe.menu_map[context_menu.name].hide(); + opts.onHide && opts.onHide(opts.parent); } }); }; diff --git a/frappe/public/js/frappe/ui/sidebar/sidebar.js b/frappe/public/js/frappe/ui/sidebar/sidebar.js index 34eaa85098..62326f455b 100644 --- a/frappe/public/js/frappe/ui/sidebar/sidebar.js +++ b/frappe/public/js/frappe/ui/sidebar/sidebar.js @@ -52,6 +52,7 @@ frappe.ui.Sidebar = class Sidebar { for (const app of frappe.boot.app_data) { if (app.workspaces.includes(this.workspace_title)) { this.header_subtitle = app.app_title; + frappe.current_app = app; this.app_logo_url = app.app_logo_url; return; } diff --git a/frappe/public/js/frappe/ui/sidebar/sidebar_header.js b/frappe/public/js/frappe/ui/sidebar/sidebar_header.js index 777ae50aa0..43f7198356 100644 --- a/frappe/public/js/frappe/ui/sidebar/sidebar_header.js +++ b/frappe/public/js/frappe/ui/sidebar/sidebar_header.js @@ -5,7 +5,14 @@ frappe.ui.SidebarHeader = class SidebarHeader { this.drop_down_expanded = false; this.workspace_title = this.sidebar.workspace_title; const me = this; + this.fetch; this.dropdown_items = [ + { + name: "workspaces", + label: "Workspaces", + icon: "wallpaper", + items: this.fetch_sibling_workspaces(), + }, { name: "desktop", label: __("Desktop"), @@ -37,7 +44,21 @@ frappe.ui.SidebarHeader = class SidebarHeader { this.populate_dropdown_menu(); this.setup_select_options(); } - + fetch_sibling_workspaces() { + let sibling_workspaces = []; + let workspaces = frappe.current_app.workspaces; + workspaces.splice(workspaces.indexOf(this.workspace_title), 1); + workspaces.forEach((w) => { + let item = { + name: w.toLowerCase(), + label: w, + icon: "wallpaper", + url: frappe.utils.generate_route({ type: "Workspace", route: w.toLowerCase() }), + }; + sibling_workspaces.push(item); + }); + return sibling_workspaces; + } make() { $(".sidebar-header").remove(); $(".sidebar-header-menu").remove(); diff --git a/frappe/public/js/frappe/ui/sidebar/sidebar_item.js b/frappe/public/js/frappe/ui/sidebar/sidebar_item.js index 352c006322..034b7962df 100644 --- a/frappe/public/js/frappe/ui/sidebar/sidebar_item.js +++ b/frappe/public/js/frappe/ui/sidebar/sidebar_item.js @@ -246,6 +246,10 @@ frappe.ui.sidebar_item.TypeSectionBreak = class SectionBreakSidebarItem extends if (e.originalEvent.isTrusted) { me.save_section_break_state(); } + if (!frappe.app.sidebar.sidebar_expanded) { + frappe.app.sidebar.open(); + this.open(); + } }); } save_section_break_state() { diff --git a/frappe/public/js/frappe/ui/toolbar/awesome_bar.js b/frappe/public/js/frappe/ui/toolbar/awesome_bar.js index 4273c8cdfb..f04bd6a8fe 100644 --- a/frappe/public/js/frappe/ui/toolbar/awesome_bar.js +++ b/frappe/public/js/frappe/ui/toolbar/awesome_bar.js @@ -238,6 +238,11 @@ frappe.search.AwesomeBar = class AwesomeBar { __("module name...") + "\ " + + __("Open in new tab") + + "" + + (frappe.utils.is_mac() ? "⌘ + Enter" : "Ctrl + Enter") + + "\ + " + __("Calculate") + "" + __("e.g. (55 + 434) / 4 or =Math.sin(Math.PI/2)...") + diff --git a/frappe/public/js/frappe/ui/toolbar/navbar.html b/frappe/public/js/frappe/ui/toolbar/navbar.html index 9410c876b5..c70a11e283 100644 --- a/frappe/public/js/frappe/ui/toolbar/navbar.html +++ b/frappe/public/js/frappe/ui/toolbar/navbar.html @@ -10,31 +10,17 @@