diff --git a/.releaserc b/.releaserc index 86f4f3cda0..ece1a68fa8 100644 --- a/.releaserc +++ b/.releaserc @@ -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" diff --git a/frappe/__init__.py b/frappe/__init__.py index 3cffd6ffda..11f48eaaa9 100644 --- a/frappe/__init__.py +++ b/frappe/__init__.py @@ -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: diff --git a/frappe/boot.py b/frappe/boot.py index c1f572f995..b409e36b99 100644 --- a/frappe/boot.py +++ b/frappe/boot.py @@ -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) diff --git a/frappe/core/doctype/doctype/boilerplate/controller_tree.js b/frappe/core/doctype/doctype/boilerplate/controller_tree.js new file mode 100644 index 0000000000..9fc78986e0 --- /dev/null +++ b/frappe/core/doctype/doctype/boilerplate/controller_tree.js @@ -0,0 +1,5 @@ +// Copyright (c) {year}, {app_publisher} and contributors +// For license information, please see license.txt + +// frappe.treeview_settings["{doctype}"] = {{ +// }}; \ No newline at end of file diff --git a/frappe/core/doctype/doctype/doctype.py b/frappe/core/doctype/doctype/doctype.py index dc3a785a37..1b9f6b50f8 100644 --- a/frappe/core/doctype/doctype/doctype.py +++ b/frappe/core/doctype/doctype/doctype.py @@ -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" diff --git a/frappe/core/doctype/version/test_version.py b/frappe/core/doctype/version/test_version.py index b99dc6f046..e7f8829ef6 100644 --- a/frappe/core/doctype/version/test_version.py +++ b/frappe/core/doctype/version/test_version.py @@ -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("alert", result) + self.assertNotIn("
injected", result) + # Escaped versions should be present + self.assertIn("<script>", result) + self.assertIn("<div>", 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(" 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) diff --git a/frappe/core/doctype/version/version.js b/frappe/core/doctype/version/version.js index 1e26e5f748..8fd83bc15f 100644 --- a/frappe/core/doctype/version/version.js +++ b/frappe/core/doctype/version/version.js @@ -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()); + }, }); diff --git a/frappe/core/doctype/version/version.py b/frappe/core/doctype/version/version.py index d3ee8ca22d..7ea6f2f039 100644 --- a/frappe/core/doctype/version/version.py +++ b/frappe/core/doctype/version/version.py @@ -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 "" diff --git a/frappe/core/doctype/version/version_view.html b/frappe/core/doctype/version/version_view.html index 7560118174..bbd63df849 100644 --- a/frappe/core/doctype/version/version_view.html +++ b/frappe/core/doctype/version/version_view.html @@ -1,3 +1,52 @@ +
{% if data.comment %}

{{ __("Comment") + " (" + data.comment_type }})

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

{{ __("Values Changed") }}

+{% for item in data.changed %} + {% if htmlDiffs[item[0]] %} +
+
{{ frappe.meta.get_label(doc.ref_doctype, item[0]) }}
+
{{ htmlDiffs[item[0]] }}
+
+ {% endif %} +{% endfor %} +{% var hasSimpleChanges = data.changed.some(item => !htmlDiffs[item[0]]) %} +{% if hasSimpleChanges %} @@ -17,15 +77,18 @@ {% for item in data.changed %} + {% if !htmlDiffs[item[0]] %} + {% endif %} {% endfor %}
{{ frappe.meta.get_label(doc.ref_doctype, item[0]) }} {{ getEscapedValue(item[1]) }} {{ getEscapedValue(item[2]) }}
{% endif %} +{% endif %} {% var _keys = ["added", "removed"]; %} {% for key in _keys %} diff --git a/frappe/desk/doctype/dashboard_chart/dashboard_chart.py b/frappe/desk/doctype/dashboard_chart/dashboard_chart.py index 1152d9215f..64328dfc43 100644 --- a/frappe/desk/doctype/dashboard_chart/dashboard_chart.py +++ b/frappe/desk/doctype/dashboard_chart/dashboard_chart.py @@ -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 diff --git a/frappe/desk/doctype/workspace_sidebar/workspace_sidebar.py b/frappe/desk/doctype/workspace_sidebar/workspace_sidebar.py index d6390ca036..d21d79030a 100644 --- a/frappe/desk/doctype/workspace_sidebar/workspace_sidebar.py +++ b/frappe/desk/doctype/workspace_sidebar/workspace_sidebar.py @@ -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 diff --git a/frappe/desk/page/desktop/desktop.css b/frappe/desk/page/desktop/desktop.css index 005f98a956..a28dbe26e6 100644 --- a/frappe/desk/page/desktop/desktop.css +++ b/frappe/desk/page/desktop/desktop.css @@ -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); } \ No newline at end of file diff --git a/frappe/desk/search.py b/frappe/desk/search.py index d18ce76748..71483ff13c 100644 --- a/frappe/desk/search.py +++ b/frappe/desk/search.py @@ -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 diff --git a/frappe/hooks.py b/frappe/hooks.py index 62801f33bb..a977f39529 100644 --- a/frappe/hooks.py +++ b/frappe/hooks.py @@ -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" diff --git a/frappe/locale/hu.po b/frappe/locale/hu.po index 2497e3c854..ffd24279ff 100644 --- a/frappe/locale/hu.po +++ b/frappe/locale/hu.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: 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' diff --git a/frappe/public/js/frappe/form/controls/base_input.js b/frappe/public/js/frappe/form/controls/base_input.js index bf8d2b0e32..4d6b641e2b 100644 --- a/frappe/public/js/frappe/form/controls/base_input.js +++ b/frappe/public/js/frappe/form/controls/base_input.js @@ -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) { diff --git a/frappe/public/js/frappe/form/controls/link.js b/frappe/public/js/frappe/form/controls/link.js index 3439a290f7..eb3748a9c0 100644 --- a/frappe/public/js/frappe/form/controls/link.js +++ b/frappe/public/js/frappe/form/controls/link.js @@ -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); diff --git a/frappe/public/js/frappe/form/form.js b/frappe/public/js/frappe/form/form.js index 2eadf88417..1ce2a9be0f 100644 --- a/frappe/public/js/frappe/form/form.js +++ b/frappe/public/js/frappe/form/form.js @@ -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, diff --git a/frappe/public/js/frappe/form/formatters.js b/frappe/public/js/frappe/form/formatters.js index b2e6b3a22a..e2984e952a 100644 --- a/frappe/public/js/frappe/form/formatters.js +++ b/frappe/public/js/frappe/form/formatters.js @@ -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); diff --git a/frappe/public/js/frappe/form/grid.js b/frappe/public/js/frappe/form/grid.js index 87c62aee55..c8b473de6a 100644 --- a/frappe/public/js/frappe/form/grid.js +++ b/frappe/public/js/frappe/form/grid.js @@ -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, diff --git a/frappe/public/js/frappe/form/multi_select_dialog.js b/frappe/public/js/frappe/form/multi_select_dialog.js index 1a3eb1359f..b83c405cbf 100644 --- a/frappe/public/js/frappe/form/multi_select_dialog.js +++ b/frappe/public/js/frappe/form/multi_select_dialog.js @@ -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, })); } diff --git a/frappe/public/js/frappe/microtemplate.js b/frappe/public/js/frappe/microtemplate.js index f3041c677a..7a1558c949 100644 --- a/frappe/public/js/frappe/microtemplate.js +++ b/frappe/public/js/frappe/microtemplate.js @@ -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); } diff --git a/frappe/public/js/frappe/ui/notifications/notifications.js b/frappe/public/js/frappe/ui/notifications/notifications.js index 808b48b592..f2ca85decb 100644 --- a/frappe/public/js/frappe/ui/notifications/notifications.js +++ b/frappe/public/js/frappe/ui/notifications/notifications.js @@ -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")} `) .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"); } } diff --git a/frappe/public/js/frappe/ui/sidebar/sidebar.js b/frappe/public/js/frappe/ui/sidebar/sidebar.js index 3a1ebcaf36..205f0766ef 100644 --- a/frappe/public/js/frappe/ui/sidebar/sidebar.js +++ b/frappe/public/js/frappe/ui/sidebar/sidebar.js @@ -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); diff --git a/frappe/public/js/frappe/ui/sidebar/sidebar_editor.js b/frappe/public/js/frappe/ui/sidebar/sidebar_editor.js index 9b781f7b9f..f952727da7 100644 --- a/frappe/public/js/frappe/ui/sidebar/sidebar_editor.js +++ b/frappe/public/js/frappe/ui/sidebar/sidebar_editor.js @@ -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; diff --git a/frappe/public/js/frappe/ui/sidebar/sidebar_item.js b/frappe/public/js/frappe/ui/sidebar/sidebar_item.js index 19e12a04be..f669d658ff 100644 --- a/frappe/public/js/frappe/ui/sidebar/sidebar_item.js +++ b/frappe/public/js/frappe/ui/sidebar/sidebar_item.js @@ -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); diff --git a/frappe/public/js/frappe/views/calendar/calendar.js b/frappe/public/js/frappe/views/calendar/calendar.js index cd1f4c281c..cef8b8a9c9 100644 --- a/frappe/public/js/frappe/views/calendar/calendar.js +++ b/frappe/public/js/frappe/views/calendar/calendar.js @@ -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, diff --git a/frappe/public/js/frappe/views/dashboard/dashboard_view.js b/frappe/public/js/frappe/views/dashboard/dashboard_view.js index 6b5b36a2ea..2adaee49fb 100644 --- a/frappe/public/js/frappe/views/dashboard/dashboard_view.js +++ b/frappe/public/js/frappe/views/dashboard/dashboard_view.js @@ -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, }); } diff --git a/frappe/public/js/frappe/views/reports/report_view.js b/frappe/public/js/frappe/views/reports/report_view.js index a34249c1d8..78ab8f59a9 100644 --- a/frappe/public/js/frappe/views/reports/report_view.js +++ b/frappe/public/js/frappe/views/reports/report_view.js @@ -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 diff --git a/frappe/public/js/frappe/widgets/widget_dialog.js b/frappe/public/js/frappe/widgets/widget_dialog.js index 0a5b4a24f3..6884cfef1e 100644 --- a/frappe/public/js/frappe/widgets/widget_dialog.js +++ b/frappe/public/js/frappe/widgets/widget_dialog.js @@ -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; } } diff --git a/frappe/public/scss/common/css_variables.scss b/frappe/public/scss/common/css_variables.scss index 1ae33c1a01..0fd3fb902c 100644 --- a/frappe/public/scss/common/css_variables.scss +++ b/frappe/public/scss/common/css_variables.scss @@ -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, "); --left-arrow-svg: url("data: image/svg+xml;utf8, "); diff --git a/frappe/public/scss/common/grid.scss b/frappe/public/scss/common/grid.scss index d7fed36002..c9cca2dd86 100644 --- a/frappe/public/scss/common/grid.scss +++ b/frappe/public/scss/common/grid.scss @@ -8,6 +8,7 @@ color: var(--text-color); min-height: 75px; background-color: var(--subtle-accent); + overflow-y: hidden; } .form-grid.error { diff --git a/frappe/public/scss/desk/dark.scss b/frappe/public/scss/desk/dark.scss index bb0cf9c2e5..aa4aabf48c 100644 --- a/frappe/public/scss/desk/dark.scss +++ b/frappe/public/scss/desk/dark.scss @@ -116,6 +116,7 @@ $check-icon-dark: url("data:image/svg+xml,