Merge branch 'frappe:develop' into form_control_alignment
This commit is contained in:
commit
7df7cf1878
37 changed files with 371 additions and 90 deletions
11
.releaserc
11
.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"
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,5 @@
|
|||
// Copyright (c) {year}, {app_publisher} and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
// frappe.treeview_settings["{doctype}"] = {{
|
||||
// }};
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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("<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("<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)
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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 ""
|
||||
|
|
|
|||
|
|
@ -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 %}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>");
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@
|
|||
color: var(--text-color);
|
||||
min-height: 75px;
|
||||
background-color: var(--subtle-accent);
|
||||
overflow-y: hidden;
|
||||
}
|
||||
|
||||
.form-grid.error {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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"""
|
||||
|
|
|
|||
|
|
@ -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})
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue