Merge branch 'frappe:develop' into tabs_on_grid_row_form

This commit is contained in:
avc 2025-12-01 07:55:55 +01:00 committed by GitHub
commit 5df1c3c211
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
49 changed files with 10680 additions and 9511 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -8,17 +8,24 @@
alt="{{ _("App Logo") |e }}"
>
</div>
<div class="desktop-search-wrapper input-group search-bar text-muted ">
<div id="navbar-modal-search">
Search or type a command
</div>
<div class="desktop-search-wrapper input-group search-bar">
<button
id="navbar-modal-search"
class="btn-reset flex justify-between"
title="Search"
>
<span class="desktop-search-icon">
<svg class="icon icon-sm"><use href="#icon-search"></use></svg>
Search
</span>
<span>
{{ "⌘ K" if is_mac else "Ctrl K" }}
</span>
</button>
</div>
<span class="desktop-avatar" style="margin-left: -10px;">
<div class="desktop-avatar" style="margin-left: -10px;">
</span>
</div>
</header>
<div class="desktop-container">
</div>

View file

@ -1,3 +1,5 @@
import sys
import frappe
from frappe.desk.doctype.desktop_icon.desktop_icon import get_desktop_icons
@ -13,4 +15,6 @@ def get_context(context):
context.brand_logo = brand_logo
context.desktop_icons = get_desktop_icons()
context.current_user = frappe.session.user
# check if system is mac or not
context.is_mac = sys.platform == "darwin"
return context

View file

@ -664,7 +664,7 @@ class QueueBuilder:
if self._unsubscribed_user_emails is not None:
return self._unsubscribed_user_emails
all_ids = list(set(self.recipients + self.cc))
all_ids = list(set(self.recipients + self.cc + self.bcc))
EmailUnsubscribe = DocType("Email Unsubscribe")
@ -698,6 +698,10 @@ class QueueBuilder:
unsubscribed_emails = self.get_unsubscribed_user_emails()
return [mail_id for mail_id in self.cc if mail_id not in unsubscribed_emails]
def final_bcc(self):
unsubscribed_emails = self.get_unsubscribed_user_emails()
return [mail_id for mail_id in self.bcc if mail_id not in unsubscribed_emails]
def get_attachments(self):
attachments = []
if self._attachments:
@ -725,7 +729,7 @@ class QueueBuilder:
attachments=self._attachments,
reply_to=self.reply_to,
cc=self.final_cc(),
bcc=self.bcc,
bcc=self.final_bcc(),
email_account=email_account,
expose_recipients=self.expose_recipients,
inline_images=self.inline_images,
@ -752,7 +756,7 @@ class QueueBuilder:
"""
final_recipients = self.final_recipients()
queue_separately = (final_recipients and self.queue_separately) or len(final_recipients) > 100
if not (final_recipients + self.final_cc()):
if not (final_recipients + self.final_cc() + self.final_bcc()):
return []
queue_data = self.as_dict(include_recipients=False)
@ -760,7 +764,7 @@ class QueueBuilder:
return []
if not queue_separately:
recipients = list(set(final_recipients + self.final_cc() + self.bcc))
recipients = list(set(final_recipients + self.final_cc() + self.final_bcc()))
q = EmailQueue.new({**queue_data, **{"recipients": recipients}}, ignore_permissions=True)
send_now and q.send()
return q
@ -786,7 +790,7 @@ class QueueBuilder:
frappe_mail_client = None
smtp_server_instance = None
for r in final_recipients:
recipients = list(set([r, *self.final_cc(), *self.bcc]))
recipients = list(set([r, *self.final_cc(), *self.final_bcc()]))
q = EmailQueue.new({**queue_data, **{"recipients": recipients}}, ignore_permissions=True)
if not frappe_mail_client and not smtp_server_instance:
email_account = q.get_email_account(raise_error=True)
@ -836,7 +840,7 @@ class QueueBuilder:
"communication": self.communication,
"send_after": self.send_after,
"show_as_cc": ",".join(self.final_cc()),
"show_as_bcc": ",".join(self.bcc),
"show_as_bcc": ",".join(self.final_bcc()),
"email_account": email_account_name or None,
"email_read_tracker_url": self.email_read_tracker_url,
}

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -160,7 +160,12 @@ function get_version_timeline_content(version_doc, frm) {
) {
parts.push(
__("{0} from {1} to {2} in row #{3}", [
frappe.meta.get_label(frm.fields_dict[row[0]].grid.doctype, p[0]),
__(
frappe.meta.get_label(
frm.fields_dict[row[0]].grid.doctype,
p[0]
)
),
format_content_for_timeline(p[1]),
format_content_for_timeline(p[2]),
row[1] + 1,

View file

@ -17,9 +17,10 @@ frappe.ui.menu = class ContextMenu {
this.add_menu_item(f);
});
if (!$.contains(document.body, this.template[0])) {
$(document.body).append(this.template);
}
// if (!$.contains(document.body, this.template[0])) {
// $(document.body).append(this.template);
// }
$(document.body).append(this.template);
}
add_menu_item(item) {
const me = this;
@ -36,21 +37,40 @@ frappe.ui.menu = class ContextMenu {
}
</div>
<span class="menu-item-title">${item.label}</span>
<div class="menu-item-icon" style="margin-left:auto">
${item.items && item.items.length ? frappe.utils.icon("chevron-right") : ""}
</div>
</a>
</div>`);
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);
}
});
};

View file

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

View file

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

View file

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

View file

@ -238,6 +238,11 @@ frappe.search.AwesomeBar = class AwesomeBar {
__("module name...") +
"</td></tr>\
<tr><td>" +
__("Open in new tab") +
"</td><td>" +
(frappe.utils.is_mac() ? "⌘ + Enter" : "Ctrl + Enter") +
"</td></tr>\
<tr><td>" +
__("Calculate") +
"</td><td>" +
__("e.g. (55 + 434) / 4 or =Math.sin(Math.PI/2)...") +

View file

@ -10,31 +10,17 @@
</a>
<ul class="nav navbar-nav d-none d-sm-flex" id="navbar-breadcrumbs"></ul>
<div class="collapse navbar-collapse justify-content-end">
<form class="form-inline fill-width justify-content-end" role="search" onsubmit="return false;">
{% if (frappe.boot.read_only) { %}
<span class="indicator-pill yellow no-indicator-dot read-only-banner" title="{%= __("Your site is undergoing maintenance or being updated.") %}">
{%= __("Read Only Mode") %}
</span>
{% } %}
{% if (frappe.boot.user.impersonated_by) { %}
<span class="indicator-pill red no-indicator-dot" title="{%= __("You are impersonating as another user.") %}">
{%= __("Impersonating {0}", [frappe.boot.user.name]) %}
</span>
{% } %}
<div class="input-group search-bar text-muted hidden">
<div
id="navbar-modal-search"
class=""
placeholder="Search for type a command"
>
{%= __('Search or type a command ({0})', [frappe.utils.is_mac() ? '⌘ + K' : 'Ctrl + K']) %}
</div>
<span class="search-icon">
<svg class="icon icon-sm"><use href="#icon-search"></use></svg>
</span>
</div>
</form>
<ul class="navbar-nav">
<li>
<button
id="navbar-modal-search"
class="btn-reset text-muted"
>
<span class="search-icon">
<svg class="icon icon-sm"><use href="#icon-search"></use></svg>
</span>
</button>
</li>
<li class="nav-item dropdown dropdown-notifications dropdown-mobile hidden">
<button
class="btn-reset nav-link notifications-icon text-muted"

View file

@ -519,9 +519,14 @@
max-width: var(--page-max-width);
}
// Adjust position when sidebar is expanded to avoid collision
.body-sidebar-container.expanded ~ .main-section & {
left: calc(50% + var(--sidebar-width, 220px) / 2);
}
.grid-form-body {
max-height: 80vh;
overflow: scroll;
overflow: auto;
}
}

View file

@ -48,7 +48,5 @@
#navbar-modal-search {
border-radius: var(--border-radius-sm);
padding: var(--input-padding);
background-color: var(--control-bg);
width: 100%;
padding-left: 40px;
}

View file

@ -174,6 +174,24 @@ class TestClient(IntegrationTestCase):
validate_link("User", "Guest", fields=["enabled"]), {"name": "Guest", "enabled": 1}
)
def test_validate_link_child_table(self):
"""
Test validate_link works for child table doctypes with field fetch.
"""
from frappe.client import validate_link
self.addCleanup(frappe.db.rollback)
user = frappe.get_doc("User", "Administrator")
user.append("block_modules", {"module": "Setup"})
user.save()
child_row = user.block_modules[-1]
result = validate_link("Block Module", child_row.name, fields=["module"])
self.assertEqual(result.get("name"), child_row.name)
self.assertEqual(result.get("module"), "Setup")
def test_client_insert(self):
from frappe.client import insert