Merge branch 'frappe:develop' into form_control_alignment

This commit is contained in:
avc 2026-01-18 22:53:57 +01:00 committed by GitHub
commit 7df7cf1878
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
37 changed files with 371 additions and 90 deletions

View file

@ -1,19 +1,22 @@
{
"branches": ["develop", {"name": "version-14-beta", "channel": "beta", "prerelease": true}],
"branches": ["version-17"],
"plugins": [
"@semantic-release/commit-analyzer", {
"preset": "angular"
"preset": "angular",
"releaseRules": [
{"breaking": true, "release": false}
]
},
"@semantic-release/release-notes-generator",
[
"@semantic-release/exec", {
"prepareCmd": 'sed -ir "s/[0-9]*\.[0-9]*\.[0-9]*/${nextRelease.version}/" frappe/__init__.py'
"prepareCmd": 'sed -ir -E "s/\"[0-9]+\.[0-9]+\.[0-9]+\"/\"${nextRelease.version}\"/" frappe/__init__.py'
}
],
[
"@semantic-release/git", {
"assets": ["frappe/__init__.py"],
"message": "chore(release): Bumped to Version ${nextRelease.version}"
"message": "chore(release): Bumped to Version ${nextRelease.version}\n\n${nextRelease.notes}"
}
],
"@semantic-release/github"

View file

@ -196,7 +196,7 @@ def init(site: str, sites_path: str = ".", new_site: bool = False, force: bool =
local.cache = {}
local.form_dict = _dict()
local.preload_assets = {"style": [], "script": [], "icons": []}
local.session = _dict(user="Guest")
local.session = _dict(user="Guest", data=_dict())
local.dev_server = _dev_server # only for backwards compatibility
local.qb = get_query_builder(local.conf.db_type)
if not cache or not client_cache:

View file

@ -161,8 +161,8 @@ def load_desktop_data(bootinfo):
from frappe.desk.desktop import get_workspace_sidebar_items
bootinfo.workspaces = get_workspace_sidebar_items()
bootinfo.workspace_sidebar_item = get_sidebar_items()
allowed_pages = [d.name for d in bootinfo.workspaces.get("pages")]
bootinfo.workspace_sidebar_item = get_sidebar_items(allowed_pages)
bootinfo.module_wise_workspaces = get_controller("Workspace").get_module_wise_workspaces()
bootinfo.dashboards = frappe.get_all("Dashboard")
bootinfo.app_data = []
@ -533,7 +533,7 @@ def get_sentry_dsn():
return os.getenv("FRAPPE_SENTRY_DSN")
def get_sidebar_items():
def get_sidebar_items(allowed_workspaces):
from frappe import _
from frappe.desk.doctype.workspace_sidebar.workspace_sidebar import auto_generate_sidebar_from_module
@ -585,7 +585,7 @@ def get_sidebar_items():
if (
"My Workspaces" in sidebar_title
or si.type == "Section Break"
or w.is_item_allowed(si.link_to, si.link_type)
or w.is_item_allowed(si.link_to, si.link_type, allowed_workspaces)
):
sidebar_items[sidebar_title.lower()]["items"].append(workspace_sidebar)
add_user_specific_sidebar(sidebar_items)

View file

@ -0,0 +1,5 @@
// Copyright (c) {year}, {app_publisher} and contributors
// For license information, please see license.txt
// frappe.treeview_settings["{doctype}"] = {{
// }};

View file

@ -877,6 +877,9 @@ class DocType(Document):
make_boilerplate("controller.js", self.as_dict())
# make_boilerplate("controller_list.js", self.as_dict())
if self.is_tree:
make_boilerplate("controller_tree.js", self.as_dict())
if self.has_web_view:
templates_path = frappe.get_module_path(
frappe.scrub(self.module), "doctype", frappe.scrub(self.name), "templates"

View file

@ -3,12 +3,137 @@
import copy
import frappe
from frappe.core.doctype.version.version import get_diff
from frappe.tests import IntegrationTestCase
from frappe.core.doctype.version.version import (
_as_string,
_generate_html_diff,
_should_generate_html_diff,
get_diff,
)
from frappe.tests import IntegrationTestCase, UnitTestCase
from frappe.tests.utils import make_test_objects
class TestHTMLDiff(UnitTestCase):
def test_generate_html_diff_produces_table(self):
"""Test HTML diff generates a table with content."""
result = _generate_html_diff("line1\nline2", "line1\nmodified")
self.assertIsNotNone(result)
self.assertIn("<table", result)
self.assertIn("line1", result)
def test_generate_html_diff_escapes_html(self):
"""Test HTML output is properly escaped and safe."""
old_value = "<script>alert('xss')</script>\nline2"
new_value = "<div>injected</div>\nline2"
result = _generate_html_diff(old_value, new_value)
self.assertIsNotNone(result)
# Raw script/div tags should be escaped, not executable
self.assertNotIn("<script>alert", result)
self.assertNotIn("<div>injected", result)
# Escaped versions should be present
self.assertIn("&lt;script&gt;", result)
self.assertIn("&lt;div&gt;", result)
def test_should_generate_html_diff_multiline(self):
"""Test should_generate_html_diff returns True for multiline text."""
self.assertTrue(_should_generate_html_diff("line1\nline2", "line1\nmodified"))
self.assertTrue(_should_generate_html_diff("single", "multi\nline"))
self.assertTrue(_should_generate_html_diff("multi\nline", "single"))
def test_should_generate_html_diff_long_text(self):
"""Test should_generate_html_diff returns True for text > 80 characters."""
self.assertTrue(_should_generate_html_diff("a" * 81, "b"))
self.assertTrue(_should_generate_html_diff("a", "b" * 81))
self.assertTrue(_should_generate_html_diff("a" * 81, "b" * 81))
def test_should_generate_html_diff_short_text(self):
"""Test should_generate_html_diff returns False for short single-line text."""
self.assertFalse(_should_generate_html_diff("short", "text"))
self.assertFalse(_should_generate_html_diff("a" * 80, "b" * 80)) # Exactly 80 chars
def test_should_generate_html_diff_empty_values(self):
"""Test should_generate_html_diff returns False when either value is empty."""
self.assertFalse(_should_generate_html_diff("", "short"))
self.assertFalse(_should_generate_html_diff("short", ""))
self.assertFalse(_should_generate_html_diff("", ""))
# Even long/multiline text returns False if the other value is empty
self.assertFalse(_should_generate_html_diff("", "a" * 81))
self.assertFalse(_should_generate_html_diff("multi\nline", ""))
def test_as_string_converts_values(self):
"""Test _as_string converts values to strings correctly."""
self.assertEqual(_as_string("text"), "text")
self.assertEqual(_as_string(None), "")
self.assertEqual(_as_string(""), "")
self.assertEqual(_as_string(0), "0")
class TestVersion(IntegrationTestCase):
def test_onload_generates_html_diffs_for_multiline(self):
"""Test onload generates HTML diffs for multiline changes."""
version = frappe.get_doc(
doctype="Version",
ref_doctype="ToDo",
docname="test-doc",
data=frappe.as_json({"changed": [["description", "line1\nline2", "line1\nmodified"]]}),
)
version.onload()
html_diffs = version.get_onload().get("html_diffs")
self.assertIsNotNone(html_diffs)
self.assertIn("description", html_diffs)
self.assertIn("<table", html_diffs["description"])
def test_onload_generates_html_diffs_for_long_text(self):
"""Test onload generates HTML diffs for text > 80 characters."""
version = frappe.get_doc(
doctype="Version",
ref_doctype="ToDo",
docname="test-doc",
data=frappe.as_json({"changed": [["notes", "x" * 81, "y" * 81]]}),
)
version.onload()
html_diffs = version.get_onload().get("html_diffs")
self.assertIsNotNone(html_diffs)
self.assertIn("notes", html_diffs)
def test_onload_no_html_diffs_for_simple_changes(self):
"""Test onload doesn't generate HTML diffs for simple short changes."""
version = frappe.get_doc(
doctype="Version",
ref_doctype="ToDo",
docname="test-doc",
data=frappe.as_json({"changed": [["status", "Open", "Closed"]]}),
)
version.onload()
html_diffs = version.get_onload().get("html_diffs")
self.assertIsNone(html_diffs)
def test_onload_handles_empty_data(self):
"""Test onload handles empty or missing data gracefully."""
version = frappe.get_doc(
doctype="Version",
ref_doctype="ToDo",
docname="test-doc",
data=None,
)
# Should not raise an error
version.onload()
self.assertIsNone(version.get_onload().get("html_diffs"))
version.data = frappe.as_json({"changed": []})
version.onload()
self.assertIsNone(version.get_onload().get("html_diffs"))
def test_get_diff(self):
frappe.set_user("Administrator")
test_records = make_test_objects("Event", reset=True)

View file

@ -1,12 +1,23 @@
frappe.ui.form.on("Version", "refresh", function (frm) {
$(
frappe.render_template("version_view", { doc: frm.doc, data: JSON.parse(frm.doc.data) })
).appendTo(frm.fields_dict.table_html.$wrapper.empty());
frm.add_custom_button(__("Show all Versions"), function () {
frappe.set_route("List", "Version", {
ref_doctype: frm.doc.ref_doctype,
docname: frm.doc.docname,
frappe.ui.form.on("Version", {
refresh: function (frm) {
frm.add_custom_button(__("Show all Versions"), function () {
frappe.set_route("List", "Version", {
ref_doctype: frm.doc.ref_doctype,
docname: frm.doc.docname,
});
});
});
frm.trigger("render_version_view");
},
render_version_view: async function (frm) {
await frappe.model.with_doctype(frm.doc.ref_doctype);
$(
frappe.render_template("version_view", {
doc: frm.doc,
data: JSON.parse(frm.doc.data),
})
).appendTo(frm.fields_dict.table_html.$wrapper.empty());
},
});

View file

@ -1,6 +1,7 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: MIT. See LICENSE
import difflib
import json
import frappe
@ -74,6 +75,29 @@ class Version(Document):
def get_data(self):
return json.loads(self.data)
def onload(self):
"""Generate HTML diffs for multiline changes on document load."""
if not self.data:
return
data = self.get_data()
changed = data.get("changed", [])
if not changed:
return
html_diffs = {}
for item in changed:
if len(item) >= 3:
fieldname, old_str, new_str = item[0], _as_string(item[1]), _as_string(item[2])
if not _should_generate_html_diff(old_str, new_str):
continue
html_diff = _generate_html_diff(old_str, new_str)
if html_diff:
html_diffs[fieldname] = html_diff
if html_diffs:
self.set_onload("html_diffs", html_diffs)
def get_diff(old, new, for_child=False, compare_cancelled=False):
"""Get diff between 2 document objects
@ -203,3 +227,32 @@ def get_diff(old, new, for_child=False, compare_cancelled=False):
def on_doctype_update():
frappe.db.add_index("Version", ["ref_doctype", "docname"])
def _generate_html_diff(old_str: str, new_str: str) -> str | None:
"""Generate HTML diff for the given old and new strings."""
old_lines = old_str.splitlines(keepends=True)
new_lines = new_str.splitlines(keepends=True)
differ = difflib.HtmlDiff(wrapcolumn=80)
html_diff = differ.make_table(
old_lines,
new_lines,
fromdesc=frappe._("Original"),
todesc=frappe._("New"),
context=True,
numlines=3,
)
return html_diff
def _should_generate_html_diff(old_str: str, new_str: str) -> bool:
"""Determine if HTML diff should be generated for the given values."""
return (
old_str and new_str and ("\n" in old_str or "\n" in new_str or len(old_str) > 80 or len(new_str) > 80)
)
def _as_string(value: str | None) -> str:
"""Convert the given value to a string."""
return cstr(value) if value is not None else ""

View file

@ -1,3 +1,52 @@
<style>
.version-diff-container {
margin-bottom: 1rem;
}
.version-diff-container h5 {
margin-bottom: 0.5rem;
}
.version-html-diff table.diff {
width: 100%;
border-collapse: collapse;
font-family: monospace;
font-size: 12px;
}
.version-html-diff table.diff td {
padding: 2px 6px;
border: 1px solid var(--border-color);
vertical-align: top;
}
.version-html-diff table.diff .diff_header {
background-color: var(--subtle-fg);
text-align: right;
padding: 2px 6px;
color: var(--text-muted);
font-weight: normal;
width: 40px;
}
.version-html-diff table.diff .diff_next {
background-color: var(--subtle-fg);
width: 10px;
}
.version-html-diff table.diff .diff_add {
background-color: var(--diff-added);
}
.version-html-diff table.diff .diff_chg {
background-color: var(--diff-changed);
}
.version-html-diff table.diff .diff_sub {
background-color: var(--diff-removed);
}
.version-html-diff table.diff th {
background-color: var(--subtle-fg);
padding: 6px;
border: 1px solid var(--border-color);
font-weight: 500;
}
.version-html-diff table.diff colgroup {
display: none;
}
</style>
<div class="version-info">
{% if data.comment %}
<h4>{{ __("Comment") + " (" + data.comment_type }})</h4>
@ -5,8 +54,19 @@
{% endif %}
{% const getEscapedValue = (v) => v === null ? "null" : frappe.utils.escape_html(v) %}
{% const htmlDiffs = (doc.__onload && doc.__onload.html_diffs) || {} %}
{% if data.changed && data.changed.length %}
<h4>{{ __("Values Changed") }}</h4>
{% for item in data.changed %}
{% if htmlDiffs[item[0]] %}
<div class="version-diff-container">
<h5>{{ frappe.meta.get_label(doc.ref_doctype, item[0]) }}</h5>
<div class="version-html-diff">{{ htmlDiffs[item[0]] }}</div>
</div>
{% endif %}
{% endfor %}
{% var hasSimpleChanges = data.changed.some(item => !htmlDiffs[item[0]]) %}
{% if hasSimpleChanges %}
<table class="table table-bordered">
<thead>
<tr>
@ -17,15 +77,18 @@
</thead>
<tbody>
{% for item in data.changed %}
{% if !htmlDiffs[item[0]] %}
<tr>
<td>{{ frappe.meta.get_label(doc.ref_doctype, item[0]) }}</td>
<td class="diff-remove">{{ getEscapedValue(item[1]) }}</td>
<td class="diff-add">{{ getEscapedValue(item[2]) }}</td>
</tr>
{% endif %}
{% endfor %}
</tbody>
</table>
{% endif %}
{% endif %}
{% var _keys = ["added", "removed"]; %}
{% for key in _keys %}

View file

@ -218,7 +218,7 @@ def get_chart_config(chart, filters, timespan, timegrain, from_date, to_date):
else get_period(r[0], timegrain)
for r in result
],
"datasets": [{"name": chart.name, "values": [r[1] for r in result]}],
"datasets": [{"name": _(chart.name), "values": [r[1] for r in result]}],
}
@ -292,7 +292,7 @@ def get_group_by_chart_config(chart, filters) -> dict | None:
if data:
return {
"labels": [item.get("name", "Not Specified") for item in data],
"datasets": [{"name": chart.name, "values": [item["count"] for item in data]}],
"datasets": [{"name": _(chart.name), "values": [item["count"] for item in data]}],
}
return None

View file

@ -77,7 +77,7 @@ class WorkspaceSidebar(Document):
else:
frappe.throw(_("You need to be Workspace Manager to delete a public workspace."))
def is_item_allowed(self, name, item_type):
def is_item_allowed(self, name, item_type, allowed_workspaces):
if frappe.session.user == "Administrator":
return True
@ -100,12 +100,7 @@ class WorkspaceSidebar(Document):
if item_type == "url":
return True
if item_type == "workspace":
try:
workspace = frappe.get_cached_doc("Workspace", name)
if workspace.module in self.allowed_modules:
return True
except frappe.DoesNotExistError:
return False
return name in allowed_workspaces
def get_cached(self, cache_key, fallback_fn):
value = frappe.cache.get_value(cache_key, user=frappe.session.user)
@ -339,7 +334,7 @@ def choose_top_doctypes(doctype_names):
try:
doctype_count_map = {}
for doctype in doctype_names:
if not is_single_doctype(doctype):
if not is_single_doctype(doctype) and not frappe.get_meta(doctype).is_virtual:
doctype_count_map[doctype] = frappe.db.count(doctype)
top_doctypes = [
name

View file

@ -234,7 +234,7 @@
}
.modal-body .icons{
margin-top: 0px;
place-self: start;
place-self: anchor-center;
& .desktop-icon{
height: 126px;
width: 127px;
@ -395,7 +395,9 @@
}
.desktop-modal-body {
width: 90vw;
width: calc(100vw - 20px);
padding-left: 0px !important;
padding-right: 0px !important;
> .icons-container {
width: 100%;
overflow: hidden !important;
@ -404,10 +406,8 @@
padding: 0;
> .icons {
position: relative;
right: 6%;
column-gap: 4px;
row-gap: 8px;
column-gap: 6px;
row-gap: 6px;
@media screen and (max-width: 380px) {
--desktop-icon-container: 100px;
@ -463,4 +463,7 @@
[data-theme="dark"] .desktop-edit:hover{
opacity: 0.8;
transition: opacity 0.3s;
}
.desktop-navbar-modal-search:hover{
outline: 1px solid var(--surface-gray-3);
}

View file

@ -109,28 +109,10 @@ def search_widget(
if filters is None:
filters = {}
are_filters_dict = isinstance(filters, dict)
include_disabled = False
if not query and are_filters_dict:
if "include_disabled" in filters:
if filters["include_disabled"] == 1:
include_disabled = True
filters.pop("include_disabled")
filters = [make_filter_tuple(doctype, key, value) for key, value in filters.items()]
are_filters_dict = False
if for_link_validation:
if are_filters_dict:
# we add filter if possible, otherwise rely on txt
if "name" not in filters:
filters["name"] = txt
else:
filters.append([doctype, "name", "=", txt])
as_dict = False
# for custom queries that don't respect filters but respect limit (rare)
# or for when we have to rely on txt
# for custom queries, we don't mutate filters
# we have to rely on txt
# we want to match "A" with "A" only and not "A1", "BA" etc.
page_length = PAGE_LENGTH_FOR_LINK_VALIDATION
@ -163,6 +145,19 @@ def search_widget(
return []
meta = frappe.get_meta(doctype)
include_disabled = False
if isinstance(filters, dict):
if "include_disabled" in filters:
if filters["include_disabled"] == 1:
include_disabled = True
filters.pop("include_disabled")
filters = [make_filter_tuple(doctype, key, value) for key, value in filters.items()]
if for_link_validation:
filters.append([doctype, "name", "=", txt])
or_filters = []
# build from doctype

View file

@ -8,7 +8,7 @@ app_publisher = "Frappe Technologies"
app_description = "Full stack web framework with Python, Javascript, MariaDB, Redis, Node"
app_license = "MIT"
app_logo_url = "/assets/frappe/images/frappe-framework-logo.svg"
develop_version = "15.x.x-develop"
develop_version = "17.x.x-develop"
app_home = "/app/build"
app_email = "developers@frappe.io"

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: 2026-01-05 23:50\n"
"PO-Revision-Date: 2026-01-14 01:41\n"
"Last-Translator: developers@frappe.io\n"
"Language-Team: Hungarian\n"
"MIME-Version: 1.0\n"
@ -32,7 +32,7 @@ msgstr "\"Cégtörténet\""
#: frappe/core/doctype/data_export/exporter.py:202
msgid "\"Parent\" signifies the parent table in which this row must be added"
msgstr "\"Szülő\" jelenti azt a szülő táblát, amelyhez ezt a sort hozzá kell adni"
msgstr "\"Szülő\" jelenti azt a fő táblát, amelyhez ezt a sort hozzá kell adni"
#. Description of the 'Team Members Heading' (Data) field in DocType 'About Us
#. Settings'

View file

@ -55,6 +55,18 @@ frappe.ui.form.ControlInput = class ControlInput extends frappe.ui.form.Control
// like links, currencies, HTMLs etc.
this.disp_area = this.$wrapper.find(".control-value").get(0);
}
this.setup_shortcut();
}
setup_shortcut() {
$(this.input_area).on("keydown", function (event) {
if (event.originalEvent.ctrlKey || event.originalEvent.metaKey) {
if (event.originalEvent.key === "k" || event.originalEvent.key === "K") {
$("#navbar-modal-search").click();
event.preventDefault();
return false;
}
}
});
}
set_max_width() {
if (this.constructor.horizontal) {

View file

@ -859,7 +859,7 @@ frappe.ui.form.ControlLink = class ControlLink extends frappe.ui.form.ControlDat
}
validate_link_and_fetch(value) {
const args = this.get_search_args(value);
if (!args.doctype) return;
if (!args) return;
const columns_to_fetch = Object.values(this.fetch_map);

View file

@ -1400,7 +1400,7 @@ frappe.ui.form.Form = class FrappeForm {
}
email_doc(message) {
new frappe.views.CommunicationComposer({
return new frappe.views.CommunicationComposer({
doc: this.doc,
frm: this,
subject: __(this.meta.name) + ": " + this.docname,

View file

@ -433,9 +433,7 @@ frappe.format = function (value, df, options, doc) {
df._options = doc ? doc[df.options] : null;
}
var formatter =
frappe.meta.get_docfield(doc?.doctype, df.fieldname)?.formatter ||
frappe.form.get_formatter(fieldtype);
var formatter = df.formatter || frappe.form.get_formatter(fieldtype);
var formatted = formatter(value, df, options, doc);

View file

@ -552,7 +552,7 @@ export default class Grid {
grid_row = new GridRow({
parent: $rows,
parent_df: this.df,
docfields: JSON.parse(JSON.stringify(this.docfields)),
docfields: this.docfields,
doc: d,
frm: this.frm,
grid: this,

View file

@ -191,7 +191,7 @@ frappe.ui.form.MultiSelectDialog = class MultiSelectDialog {
get_child_datatable_columns() {
const parent = this.doctype;
return [parent, ...this.child_columns].map((d) => ({
name: frappe.unscrub(d),
name: __(frappe.unscrub(d)),
editable: false,
}));
}

View file

@ -187,6 +187,9 @@ frappe.render_pdf = function (html, opts = {}) {
//Push the HTML content into an element
formData.append("html", html);
if (opts.doctype) {
formData.append("doctype", opts.doctype);
}
if (opts.orientation) {
formData.append("orientation", opts.orientation);
}

View file

@ -4,7 +4,8 @@ frappe.ui.Notifications = class Notifications {
constructor(opts) {
this.tabs = {};
this.notification_settings = frappe.boot.notification_settings;
this.full_height = opts?.full_height || true;
this.full_height = opts?.full_height || false;
this.wrapper = opts?.wrapper || $(".standard-items-sections");
this.make();
}
@ -52,8 +53,10 @@ frappe.ui.Notifications = class Notifications {
${frappe.utils.icon("x")}
</span>`)
.on("click", (e) => {
if (!this.full_height) {
if (this.full_height) {
this.dropdown.addClass("hidden");
} else {
this.dropdown_list.addClass("hidden");
}
})
.appendTo(this.header_actions)
@ -149,9 +152,8 @@ frappe.ui.Notifications = class Notifications {
const isInsideNotificationBtn =
$(e.target).closest(".standard-items-sections .sidebar-notification").length > 0;
const isInsideDropdown = $(e.target).closest(".notifications-list").length > 0;
if (!isInsideNotificationBtn && !isInsideDropdown) {
if (!full_height) {
if (full_height) {
dropdown.addClass("hidden");
}
}

View file

@ -332,7 +332,7 @@ frappe.ui.Sidebar = class Sidebar {
}
setup_notifications() {
if (frappe.boot.desk_settings.notifications && frappe.session.user !== "Guest") {
this.notifications = new frappe.ui.Notifications();
this.notifications = new frappe.ui.Notifications({ full_height: true });
}
}
add_item(container, item) {
@ -475,6 +475,8 @@ frappe.ui.Sidebar = class Sidebar {
let sidebar = this.get_workspace_for_module(module);
if (sidebars.includes(this.get_workspace_for_module(module))) {
frappe.app.sidebar.setup(sidebar);
} else {
frappe.app.sidebar.setup(module);
}
} else if (module) {
this.show_sidebar_for_module(module);

View file

@ -84,6 +84,7 @@ export class SidebarEditor {
}
prepare_data() {
this.new_sidebar_items.forEach((item) => {
if (!item.nested_items) return;
item.nested_items.forEach((nested_item) => {
if (nested_item.parent) {
delete nested_item.parent;

View file

@ -177,8 +177,8 @@ frappe.ui.sidebar_item.TypeSectionBreak = class SectionBreakSidebarItem extends
this.full_template = $(this.wrapper);
}
make() {
if (this.item.nested_items.length == 0) return;
super.make();
if (!this.item.nested_items || this.item.nested_items.length == 0) return;
this.add_items();
this.toggle_on_collapse();
this.enable_collapsible(this.item, this.full_template);

View file

@ -188,10 +188,10 @@ frappe.views.Calendar = class Calendar {
const me = this;
let btn_group = me.$wrapper.find(".fc-button-group");
btn_group.on("click", ".btn", function () {
let value = $(this).hasClass("fc-dayGridWeek-button")
? "dayGridWeek"
: $(this).hasClass("fc-dayGridDay-button")
? "dayGridDay"
let value = $(this).hasClass("fc-timeGridWeek-button")
? "timeGridWeek"
: $(this).hasClass("fc-timeGridDay-button")
? "timeGridDay"
: "dayGridMonth";
me.set_localStorage_option("cal_initialView", value);
});
@ -206,7 +206,7 @@ frappe.views.Calendar = class Calendar {
}
set_css() {
const viewButtons =
".fc-dayGridMonth-button, .fc-dayGridWeek-button, .fc-dayGridDay-button, .fc-today-button";
".fc-dayGridMonth-button, .fc-timeGridWeek-button, .fc-timeGridDay-button, .fc-today-button";
const fcViewButtonClasses = "fc-button fc-button-primary fc-button-active";
// remove fc-button styles
@ -259,7 +259,7 @@ frappe.views.Calendar = class Calendar {
headerToolbar: {
left: "prev,title,next",
center: "",
right: "today,dayGridMonth,dayGridWeek,dayGridDay",
right: "today,dayGridMonth,timeGridWeek,timeGridDay",
},
editable: true,
droppable: true,

View file

@ -124,7 +124,7 @@ frappe.views.DashboardView = class DashboardView extends frappe.views.ListView {
? JSON.parse(settings.chart_config)
: {};
this.charts.map((chart) => {
chart.label = chart.chart_name;
chart.label = __(chart.chart_name);
chart.chart_settings = this.dashboard_chart_settings[chart.chart_name] || {};
});
this.render_dashboard_charts();
@ -464,7 +464,7 @@ frappe.views.DashboardView = class DashboardView extends frappe.views.ListView {
} else {
this.chart_group.new_widget.on_create({
chart_name: chart.chart,
label: chart.chart,
label: __(chart.chart),
name: chart.chart,
});
}

View file

@ -1531,6 +1531,7 @@ frappe.views.ReportView = class ReportView extends frappe.views.ListView {
},
{
label: __("Print"),
condition: () => frappe.model.can_print(this.doctype),
action: () => {
// prepare rows in their current state, sorted and filtered
const rows_in_order = this.datatable.datamanager.rowViewOrder

View file

@ -147,7 +147,7 @@ class ChartDialog extends WidgetDialog {
}
process_data(data) {
data.label = data.label ? data.label : data.chart_name;
data.label = data.label ? data.label : __(data.chart_name);
return data;
}
}

View file

@ -150,8 +150,9 @@ $disabled-input-height: 22px;
--switch-bg: var(--gray-300);
// "diff" colors
--diff-added: var(--green-100);
--diff-removed: var(--red-100);
--diff-added: var(--green-200);
--diff-removed: var(--red-200);
--diff-changed: var(--blue-200);
--right-arrow-svg: url("data: image/svg+xml;utf8, <svg width='6' height='8' viewBox='0 0 6 8' fill='none' xmlns='http://www.w3.org/2000/svg'><path d='M1.25 7.5L4.75 4L1.25 0.5' stroke='%231F272E' stroke-linecap='round' stroke-linejoin='round'/></svg>");
--left-arrow-svg: url("data: image/svg+xml;utf8, <svg width='6' height='8' viewBox='0 0 6 8' fill='none' xmlns='http://www.w3.org/2000/svg'><path d='M7.5 9.5L4 6l3.5-3.5' stroke='%231F272E' stroke-linecap='round' stroke-linejoin='round'></path></svg>");

View file

@ -8,6 +8,7 @@
color: var(--text-color);
min-height: 75px;
background-color: var(--subtle-accent);
overflow-y: hidden;
}
.form-grid.error {

View file

@ -116,6 +116,7 @@ $check-icon-dark: url("data:image/svg+xml, <svg viewBox='0 0 8 7' fill='none' xm
// "diff" colors
--diff-added: var(--green-800);
--diff-removed: var(--red-800);
--diff-changed: var(--blue-800);
// sidebar toggle
.page-title .sidebar-toggle-btn {

View file

@ -252,7 +252,6 @@
display: none;
}
.section-break {
flex: 0 0 auto !important;
color: var(--ink-gray-5) !important;
margin-left: 7px;
gap: 0px !important;
@ -270,8 +269,7 @@
.standard-sidebar-item:hover {
& .sidebar-item-edit-controls {
visibility: visible;
display: flex;
gap: 6px;
width: auto;
}
}
.collapse-sidebar-link {
@ -283,6 +281,9 @@
}
.sidebar-item-edit-controls {
visibility: hidden;
display: flex;
gap: 6px;
width: 0;
}
.standard-sidebar-item[data-name="add-sidebar-item"] {
margin-top: 5px;

View file

@ -818,7 +818,7 @@ jobs:
patches_template = """[pre_model_sync]
# Patches added in this section will be executed before doctypes are migrated
# Read docs to understand patches: https://frappeframework.com/docs/v14/user/en/database-migrations
# Read docs to understand patches: https://docs.frappe.io/framework/user/en/database-migrations
[post_model_sync]
# Patches added in this section will be executed after doctypes are migrated"""

View file

@ -253,7 +253,9 @@ def download_pdf(
@frappe.whitelist()
def report_to_pdf(html, orientation="Landscape"):
def report_to_pdf(html, orientation="Landscape", doctype=None):
if doctype:
frappe.has_permission(doctype, "print", throw=True)
make_access_log(file_type="PDF", method="PDF", page=html)
frappe.local.response.filename = "report.pdf"
frappe.local.response.filecontent = get_pdf(html, {"orientation": orientation})

View file

@ -98,7 +98,7 @@ Repository = "https://github.com/frappe/frappe.git"
[project.optional-dependencies]
dev = [
"pyngrok~=6.0.0",
"pyngrok~=7.5.0",
"watchdog~=6.0.0",
"responses==0.23.1",
# typechecking
@ -148,7 +148,7 @@ skip_namespaces = [
[tool.bench.dev-dependencies]
coverage = "~=7.10.0"
Faker = "~=18.10.1"
pyngrok = "~=6.0.0"
pyngrok = "~=7.5.0"
unittest-xml-reporting = "~=3.2.0"
watchdog = "~=6.0.0"
hypothesis = "~=6.77.0"