Merge branch 'develop' into get-docs
This commit is contained in:
commit
1907293ba7
127 changed files with 25988 additions and 20848 deletions
2
.github/workflows/server-tests.yml
vendored
2
.github/workflows/server-tests.yml
vendored
|
|
@ -72,7 +72,7 @@ jobs:
|
|||
- name: Clone
|
||||
uses: actions/checkout@v6
|
||||
- name: Download artifacts
|
||||
uses: actions/download-artifact@v8.0.0
|
||||
uses: actions/download-artifact@v8.0.1
|
||||
- name: Upload coverage data
|
||||
uses: codecov/codecov-action@v5
|
||||
with:
|
||||
|
|
|
|||
2
.github/workflows/ui-tests.yml
vendored
2
.github/workflows/ui-tests.yml
vendored
|
|
@ -57,7 +57,7 @@ jobs:
|
|||
- name: Clone
|
||||
uses: actions/checkout@v6
|
||||
- name: Download artifacts
|
||||
uses: actions/download-artifact@v8.0.0
|
||||
uses: actions/download-artifact@v8.0.1
|
||||
- name: Upload python coverage data
|
||||
uses: codecov/codecov-action@v5
|
||||
with:
|
||||
|
|
|
|||
|
|
@ -151,9 +151,10 @@ def get_letter_heads():
|
|||
|
||||
|
||||
def load_conf_settings(bootinfo):
|
||||
from frappe.core.api.file import get_max_file_size
|
||||
from frappe.core.api.file import get_file_chunk_size, get_max_file_size
|
||||
|
||||
bootinfo.max_file_size = get_max_file_size()
|
||||
bootinfo.file_chunk_size = get_file_chunk_size()
|
||||
for key in ("developer_mode", "socketio_port", "file_watcher_port"):
|
||||
if key in frappe.conf:
|
||||
bootinfo[key] = frappe.conf.get(key)
|
||||
|
|
|
|||
|
|
@ -465,14 +465,14 @@ def validate_link_and_fetch(
|
|||
)
|
||||
|
||||
if not search_result:
|
||||
return {} # does not exist or filtered out
|
||||
return {} # Either the record does not exist or was excluded by link_filters
|
||||
|
||||
values = None
|
||||
is_virtual_dt = bool(meta.get("is_virtual"))
|
||||
if is_virtual_dt:
|
||||
try:
|
||||
doc = frappe.get_doc(doctype, docname)
|
||||
doc.check_permission("select" if frappe.only_has_select_perm(doctype) else "read")
|
||||
doc.check_permission("select")
|
||||
values = {"name": doc.name}
|
||||
|
||||
except frappe.DoesNotExistError:
|
||||
|
|
|
|||
|
|
@ -90,6 +90,10 @@ def get_max_file_size() -> int:
|
|||
)
|
||||
|
||||
|
||||
def get_file_chunk_size() -> int:
|
||||
return cint(frappe.conf.get("file_chunk_size")) or 25 * 1024 * 1024
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def create_new_folder(file_name: str, folder: str) -> File:
|
||||
"""create new folder under current parent folder"""
|
||||
|
|
|
|||
|
|
@ -18,7 +18,10 @@ from frappe.utils import (
|
|||
get_imaginary_pixel_response,
|
||||
get_string_between,
|
||||
list_to_str,
|
||||
now_datetime,
|
||||
parse_addr,
|
||||
split_emails,
|
||||
time_diff_in_seconds,
|
||||
validate_email_address,
|
||||
)
|
||||
|
||||
|
|
@ -328,3 +331,63 @@ def update_communication_as_read(name):
|
|||
name,
|
||||
{"read_by_recipient": 1, "delivery_status": "Read", "read_by_recipient_on": get_datetime()},
|
||||
)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def undo_email_send(communication_name: str):
|
||||
communication = frappe.get_doc("Communication", communication_name)
|
||||
|
||||
if communication.owner != frappe.session.user:
|
||||
frappe.throw(_("You are not authorized to undo this email"))
|
||||
|
||||
if communication.sent_or_received != "Sent" or communication.communication_medium != "Email":
|
||||
frappe.throw(_("Failed to delete communication"))
|
||||
|
||||
time_elapsed_in_seconds = time_diff_in_seconds(now_datetime(), communication.creation)
|
||||
if time_elapsed_in_seconds > 10:
|
||||
frappe.msgprint(
|
||||
_("Email undo window is over. Cannot undo email."), alert=True, indicator="red", raise_exception=1
|
||||
)
|
||||
|
||||
email_queue_records = frappe.get_all(
|
||||
"Email Queue", filters={"communication": communication_name}, fields=["name", "status"]
|
||||
)
|
||||
|
||||
for queue in email_queue_records:
|
||||
if queue.status != "Not Sent":
|
||||
frappe.msgprint(
|
||||
_("It is too late to undo this email. It is already being sent."),
|
||||
alert=True,
|
||||
indicator="red",
|
||||
raise_exception=1,
|
||||
)
|
||||
|
||||
for queue in email_queue_records:
|
||||
frappe.delete_doc("Email Queue", queue.name, ignore_permissions=True)
|
||||
|
||||
communication_data = {
|
||||
"subject": communication.subject,
|
||||
"content": communication.content,
|
||||
"recipients": communication.recipients,
|
||||
"cc": communication.cc,
|
||||
"bcc": communication.bcc,
|
||||
"doc": {"doctype": communication.reference_doctype, "name": communication.reference_name},
|
||||
"sender": communication.sender,
|
||||
"send_read_receipt": communication.read_receipt,
|
||||
}
|
||||
|
||||
linked_files = frappe.get_all(
|
||||
"File",
|
||||
filters={"attached_to_doctype": "Communication", "attached_to_name": communication_name},
|
||||
pluck="name",
|
||||
)
|
||||
|
||||
if linked_files:
|
||||
for file_name in linked_files:
|
||||
frappe.db.set_value("File", file_name, {"attached_to_doctype": None, "attached_to_name": None})
|
||||
|
||||
communication_data["attachments"] = linked_files
|
||||
|
||||
communication.delete(ignore_permissions=True)
|
||||
|
||||
return communication_data
|
||||
|
|
|
|||
|
|
@ -1,12 +1,14 @@
|
|||
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# License: MIT. See LICENSE
|
||||
from datetime import timedelta
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import frappe
|
||||
from frappe.core.doctype.communication.communication import Communication, get_emails, parse_email
|
||||
from frappe.core.doctype.communication.email import add_attachments, make
|
||||
from frappe.core.doctype.communication.email import add_attachments, make, undo_email_send
|
||||
from frappe.email.doctype.email_queue.email_queue import EmailQueue
|
||||
from frappe.tests import IntegrationTestCase
|
||||
from frappe.utils import add_to_date, now_datetime
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from frappe.contacts.doctype.contact.contact import Contact
|
||||
|
|
@ -438,6 +440,79 @@ class TestCommunicationEmailMixin(IntegrationTestCase):
|
|||
self.assertEqual(attached_file.file_name, file_name)
|
||||
self.assertEqual(attached_file.get_content(), file_content)
|
||||
|
||||
def test_undo_email_send(self):
|
||||
"""Undo should delete Communication and Email Queue, and return original data."""
|
||||
comm = self.new_communication(recipients=["to@test.com"])
|
||||
comm.sent_or_received = "Sent"
|
||||
comm.save(ignore_permissions=True)
|
||||
|
||||
eq = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Email Queue",
|
||||
"sender": "Test <test@example.com>",
|
||||
"message": "Test message",
|
||||
"status": "Not Sent",
|
||||
"priority": 1,
|
||||
"communication": comm.name,
|
||||
"recipients": [{"recipient": "to@test.com", "status": "Not Sent"}],
|
||||
}
|
||||
).insert(ignore_permissions=True)
|
||||
|
||||
result = undo_email_send(comm.name)
|
||||
|
||||
self.assertFalse(frappe.db.exists("Communication", comm.name))
|
||||
self.assertFalse(frappe.db.exists("Email Queue", eq.name))
|
||||
self.assertFalse(frappe.db.exists("Email Queue Recipient", {"parent": eq.name}))
|
||||
self.assertEqual(result["subject"], comm.subject)
|
||||
self.assertEqual(result["recipients"], comm.recipients)
|
||||
|
||||
def test_undo_email_send_fails_for_different_user(self):
|
||||
"""Undo should fail if the current user is not the owner."""
|
||||
comm = self.new_communication(recipients=["to@test.com"])
|
||||
comm.sent_or_received = "Sent"
|
||||
comm.save(ignore_permissions=True)
|
||||
frappe.db.set_value("Communication", comm.name, "owner", "other@test.com")
|
||||
|
||||
with self.assertRaises(frappe.exceptions.ValidationError):
|
||||
undo_email_send(comm.name)
|
||||
|
||||
self.assertTrue(frappe.db.exists("Communication", comm.name))
|
||||
|
||||
def test_undo_email_send_fails_after_time_window(self):
|
||||
"""Undo should fail if the 10-second window has passed."""
|
||||
comm = self.new_communication(recipients=["to@test.com"])
|
||||
comm.sent_or_received = "Sent"
|
||||
comm.save(ignore_permissions=True)
|
||||
|
||||
with self.freeze_time(add_to_date(now_datetime(), seconds=12)):
|
||||
with self.assertRaises(frappe.exceptions.ValidationError):
|
||||
undo_email_send(comm.name)
|
||||
|
||||
self.assertTrue(frappe.db.exists("Communication", comm.name))
|
||||
|
||||
def test_undo_email_send_fails_if_already_sent(self):
|
||||
"""Undo should fail if Email Queue status is not 'Not Sent'."""
|
||||
comm = self.new_communication(recipients=["to@test.com"])
|
||||
comm.sent_or_received = "Sent"
|
||||
comm.save(ignore_permissions=True)
|
||||
|
||||
frappe.get_doc(
|
||||
{
|
||||
"doctype": "Email Queue",
|
||||
"sender": "Test <test@example.com>",
|
||||
"message": "Test message",
|
||||
"status": "Sent",
|
||||
"priority": 1,
|
||||
"communication": comm.name,
|
||||
"recipients": [{"recipient": "to@test.com", "status": "Sent"}],
|
||||
}
|
||||
).insert(ignore_permissions=True)
|
||||
|
||||
with self.assertRaises(frappe.exceptions.ValidationError):
|
||||
undo_email_send(comm.name)
|
||||
|
||||
self.assertTrue(frappe.db.exists("Communication", comm.name))
|
||||
|
||||
|
||||
def create_email_account() -> "EmailAccount":
|
||||
frappe.delete_doc_if_exists("Email Account", "_Test Comm Account 1")
|
||||
|
|
|
|||
|
|
@ -187,8 +187,14 @@ def stop_data_import(doc_name: str):
|
|||
def start_import(data_import):
|
||||
"""This method runs in background job"""
|
||||
data_import = frappe.get_doc("Data Import", data_import)
|
||||
# Apply same delimiter/sniffer settings as preview so CSV is parsed correctly (e.g. EU ";" delimiter)
|
||||
data_import.set_delimiters_flag()
|
||||
try:
|
||||
i = Importer(data_import.reference_doctype, data_import=data_import)
|
||||
i = Importer(
|
||||
data_import.reference_doctype,
|
||||
data_import=data_import,
|
||||
use_sniffer=data_import.use_csv_sniffer,
|
||||
)
|
||||
i.import_data()
|
||||
except JobTimeoutException:
|
||||
frappe.db.rollback()
|
||||
|
|
|
|||
|
|
@ -338,7 +338,7 @@
|
|||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval: parent.is_submittable",
|
||||
"depends_on": "eval: parent.is_submittable || parent.istable",
|
||||
"fieldname": "allow_on_submit",
|
||||
"fieldtype": "Check",
|
||||
"label": "Allow on Submit",
|
||||
|
|
@ -647,7 +647,7 @@
|
|||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2026-02-06 15:13:03.688027",
|
||||
"modified": "2026-03-10 21:39:58.400441",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Core",
|
||||
"name": "DocField",
|
||||
|
|
|
|||
|
|
@ -751,7 +751,7 @@ class File(Document):
|
|||
return self.save_file_on_filesystem()
|
||||
|
||||
def save_file_on_filesystem(self):
|
||||
safe_file_name = re.sub(r"[/\\%?#]", "_", self.file_name)
|
||||
safe_file_name = get_safe_file_name(self.file_name)
|
||||
if self.is_private:
|
||||
self.file_url = f"/private/files/{safe_file_name}"
|
||||
else:
|
||||
|
|
|
|||
|
|
@ -476,3 +476,7 @@ def find_file_by_url(path: str, name: str | None = None) -> "File" | None:
|
|||
file: File = frappe.get_doc(doctype="File", **file_data)
|
||||
if file.is_downloadable():
|
||||
return file
|
||||
|
||||
|
||||
def get_safe_file_name(file_name: str) -> str:
|
||||
return re.sub(r"[/\\%?#]", "_", file_name)
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@
|
|||
"action",
|
||||
"hidden",
|
||||
"is_standard",
|
||||
"icon",
|
||||
"column_break_dtwu",
|
||||
"condition"
|
||||
],
|
||||
|
|
@ -72,19 +73,25 @@
|
|||
{
|
||||
"fieldname": "column_break_dtwu",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "icon",
|
||||
"fieldtype": "Icon",
|
||||
"label": "Icon"
|
||||
}
|
||||
],
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-11-15 14:12:19.803995",
|
||||
"modified": "2026-03-11 12:23:02.473404",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Core",
|
||||
"name": "Navbar Item",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"quick_entry": 1,
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -125,6 +125,19 @@ def generate_report(prepared_report):
|
|||
create_json_gz_file(result, instance.doctype, instance.name, instance.report_name)
|
||||
|
||||
instance.status = "Completed"
|
||||
|
||||
frappe.get_doc(
|
||||
{
|
||||
"doctype": "Notification Log",
|
||||
"subject": f"{instance.report_name} report is ready.",
|
||||
"for_user": frappe.session.user,
|
||||
"type": "Alert",
|
||||
"document_type": "Report",
|
||||
"document_name": report.name,
|
||||
"link": f"/desk/query-report/{report.name}?prepared_report_name={instance.name}",
|
||||
}
|
||||
).insert(ignore_permissions=True)
|
||||
|
||||
except Exception:
|
||||
# we need to ensure that error gets stored
|
||||
_save_error(instance, error=frappe.get_traceback(with_context=True))
|
||||
|
|
|
|||
|
|
@ -92,6 +92,13 @@ class TestTranslation(IntegrationTestCase):
|
|||
|
||||
self.assertTrue(_(source), target)
|
||||
|
||||
def test_html_message_translations(self):
|
||||
"""Test fallback for messages w/ HTML Tags"""
|
||||
message = "Hide descendant records of <b>For Value</b>."
|
||||
translated_message = "隐藏下层节点<b>值</b>"
|
||||
create_translation("zh", message, translated_message)
|
||||
self.assertEqual(_(message, lang="zh"), translated_message)
|
||||
|
||||
|
||||
def get_translation_data():
|
||||
html_source_data = """<font color="#848484" face="arial, tahoma, verdana, sans-serif">
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ from frappe.utils import (
|
|||
)
|
||||
from frappe.utils.data import sha256_hash
|
||||
from frappe.utils.html_utils import sanitize_html
|
||||
from frappe.utils.password import check_password, get_password_reset_limit
|
||||
from frappe.utils.password import check_password, get_password_reset_limit, is_password_reused
|
||||
from frappe.utils.password import update_password as _update_password
|
||||
from frappe.utils.user import get_system_managers
|
||||
from frappe.website.utils import get_home_page, is_signup_disabled
|
||||
|
|
@ -361,7 +361,7 @@ class User(Document):
|
|||
self.set(field, sanitize_html(field_value, always_sanitize=True))
|
||||
|
||||
def set_full_name(self):
|
||||
self.full_name = " ".join(filter(None, [self.first_name, self.last_name]))
|
||||
self.full_name = " ".join(p for p in [self.first_name, self.middle_name, self.last_name] if p)
|
||||
|
||||
def check_enable_disable(self):
|
||||
# do not allow disabling administrator/guest
|
||||
|
|
@ -927,6 +927,14 @@ def update_password(
|
|||
else:
|
||||
user = res["user"]
|
||||
|
||||
if is_password_reused(user, new_password):
|
||||
frappe.throw(
|
||||
_(
|
||||
"New password cannot be the same as your current password. Please choose a different password."
|
||||
),
|
||||
title=_("Invalid Password"),
|
||||
)
|
||||
|
||||
logout_all_sessions = cint(logout_all_sessions) or frappe.get_system_settings("logout_on_password_reset")
|
||||
_update_password(user, new_password, logout_all_sessions=cint(logout_all_sessions))
|
||||
|
||||
|
|
|
|||
|
|
@ -139,8 +139,8 @@ frappe.PermissionEngine = class PermissionEngine {
|
|||
data.message.forEach((d) => {
|
||||
let custom_rights = this.options.doctype_ptype_map[doctype] || [];
|
||||
d.rights = this.rights
|
||||
.filter((r) => d[r])
|
||||
.concat(custom_rights)
|
||||
.filter((r) => d[r])
|
||||
.map((r) => {
|
||||
return __(toTitle(frappe.unscrub(r)));
|
||||
})
|
||||
|
|
|
|||
|
|
@ -107,13 +107,15 @@ frappe.ui.form.on("Customize Form", {
|
|||
frm.page.set_title(__("Customize Form - {0}", [__(frm.doc.doc_type)]));
|
||||
frappe.customize_form.set_primary_action(frm);
|
||||
|
||||
frm.add_custom_button(
|
||||
__("Go to {0} List", [__(frm.doc.doc_type)]),
|
||||
function () {
|
||||
frappe.set_route("List", frm.doc.doc_type);
|
||||
},
|
||||
__("Actions")
|
||||
);
|
||||
if (!frappe.get_meta(frm.doc.doc_type).istable) {
|
||||
frm.add_custom_button(
|
||||
__("Go to {0} List", [__(frm.doc.doc_type)]),
|
||||
() => {
|
||||
frappe.set_route("List", frm.doc.doc_type);
|
||||
},
|
||||
__("Actions")
|
||||
);
|
||||
}
|
||||
|
||||
frm.add_custom_button(
|
||||
__("Set Permissions"),
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ from frappe.database.utils import NestedSetHierarchy
|
|||
from frappe.model.db_query import get_timespan_date_range
|
||||
from frappe.query_builder import Field
|
||||
from frappe.query_builder.functions import Coalesce
|
||||
from frappe.utils import cstr
|
||||
|
||||
|
||||
def like(key: Field, value: str) -> frappe.qb:
|
||||
|
|
@ -107,7 +108,7 @@ def func_between(key: Field, value: list | tuple) -> frappe.qb:
|
|||
def func_is(key, value):
|
||||
"Wrapper for IS"
|
||||
|
||||
match value.lower():
|
||||
match cstr(value).lower():
|
||||
case "set":
|
||||
return key != ""
|
||||
case "not set":
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ from frappe.database.utils import (
|
|||
get_doctype_name,
|
||||
get_doctype_sort_info,
|
||||
)
|
||||
from frappe.model import CORE_DOCTYPES as PERMITTED_CORE_DOCTYPES
|
||||
from frappe.model import OPTIONAL_FIELDS, get_permitted_fields
|
||||
from frappe.model.base_document import DOCTYPES_FOR_DOCTYPE
|
||||
from frappe.model.document import Document
|
||||
|
|
@ -83,7 +84,7 @@ def _apply_date_field_filter_conversion(value, operator: str, doctype: str, fiel
|
|||
elif isinstance(value, datetime.datetime):
|
||||
return value.date()
|
||||
|
||||
except (AttributeError, TypeError, KeyError):
|
||||
except AttributeError, TypeError, KeyError:
|
||||
pass
|
||||
|
||||
return value
|
||||
|
|
@ -273,7 +274,7 @@ class Engine:
|
|||
self.table = qb.DocType(table)
|
||||
|
||||
if self.apply_permissions:
|
||||
self.check_read_permission()
|
||||
self.check_select_permission()
|
||||
self.permission_doctype = parent_doctype or self.doctype
|
||||
self.permission_table = (
|
||||
qb.DocType(self.permission_doctype) if self.permission_doctype != self.doctype else self.table
|
||||
|
|
@ -357,13 +358,19 @@ class Engine:
|
|||
self.fields = [self.table.name]
|
||||
|
||||
self.query._child_queries = []
|
||||
has_select_field = False
|
||||
for field in self.fields:
|
||||
if isinstance(field, DynamicTableField):
|
||||
self.query = field.apply_select(self.query, engine=self)
|
||||
has_select_field = True
|
||||
elif isinstance(field, ChildQuery):
|
||||
self.query._child_queries.append(field)
|
||||
else:
|
||||
self.query = self.query.select(field)
|
||||
has_select_field = True
|
||||
|
||||
if not has_select_field:
|
||||
self.query = self.query.select(self.table.name)
|
||||
|
||||
def apply_filters(
|
||||
self,
|
||||
|
|
@ -669,7 +676,7 @@ class Engine:
|
|||
else:
|
||||
try:
|
||||
fallback_value = int(fallback_sql)
|
||||
except (ValueError, TypeError):
|
||||
except ValueError, TypeError:
|
||||
fallback_value = fallback_sql
|
||||
|
||||
return operator_fn(_field, ValueWrapper(fallback_value))
|
||||
|
|
@ -698,7 +705,7 @@ class Engine:
|
|||
else:
|
||||
try:
|
||||
fallback_value = int(fallback_sql)
|
||||
except (ValueError, TypeError):
|
||||
except ValueError, TypeError:
|
||||
fallback_value = fallback_sql
|
||||
|
||||
if fallback_value == _value:
|
||||
|
|
@ -1040,8 +1047,8 @@ class Engine:
|
|||
# for select permission on parent doctype, allow all permlevel 0 fields in filters
|
||||
cache_key = (doctype, None, "_filterable_select")
|
||||
if cache_key not in self.permitted_fields_cache:
|
||||
if doctype in CORE_DOCTYPES:
|
||||
# core doctypes have no restrictions - return all valid columns
|
||||
if doctype in PERMITTED_CORE_DOCTYPES:
|
||||
# no restrictions - return all valid columns
|
||||
self.permitted_fields_cache[cache_key] = set(meta.get_valid_columns())
|
||||
else:
|
||||
permlevel_0_fields = set(meta.default_fields) | OPTIONAL_FIELDS
|
||||
|
|
@ -1397,18 +1404,11 @@ class Engine:
|
|||
|
||||
return parsed_order_fields
|
||||
|
||||
def check_read_permission(self):
|
||||
"""Check if user has read permission on the doctype"""
|
||||
|
||||
def has_permission(ptype):
|
||||
return frappe.has_permission(
|
||||
self.doctype,
|
||||
ptype,
|
||||
user=self.user,
|
||||
parent_doctype=self.parent_doctype,
|
||||
)
|
||||
|
||||
if not has_permission("select") and not has_permission("read"):
|
||||
def check_select_permission(self):
|
||||
"""Check if user has select (or read) permission on the doctype"""
|
||||
if not frappe.has_permission(
|
||||
self.doctype, "select", user=self.user, parent_doctype=self.parent_doctype
|
||||
):
|
||||
self._raise_permission_error()
|
||||
|
||||
def _raise_permission_error(self, doctype=None):
|
||||
|
|
@ -1432,6 +1432,15 @@ class Engine:
|
|||
# Skip child table fields if parent permission is only 'select'
|
||||
continue
|
||||
|
||||
if field.parent_fieldname:
|
||||
parent_meta = frappe.get_meta(self.doctype)
|
||||
if parent_meta.get_field(
|
||||
field.parent_fieldname
|
||||
).permlevel not in parent_meta.get_permlevel_access(
|
||||
parent_permission_type, user=self.user
|
||||
):
|
||||
continue
|
||||
|
||||
# Cache permitted fields for child doctypes if accessed multiple times
|
||||
permitted_child_fields_set = self._get_cached_permitted_fields(
|
||||
field.doctype,
|
||||
|
|
@ -1462,6 +1471,12 @@ class Engine:
|
|||
# Skip child queries if parent permission is only 'select'
|
||||
continue
|
||||
|
||||
parent_meta = frappe.get_meta(self.doctype)
|
||||
if parent_meta.get_field(field.fieldname).permlevel not in parent_meta.get_permlevel_access(
|
||||
parent_permission_type, user=self.user
|
||||
):
|
||||
continue
|
||||
|
||||
# Cache permitted fields for the child doctype of the query
|
||||
permitted_child_fields_set = self._get_cached_permitted_fields(
|
||||
field.doctype,
|
||||
|
|
|
|||
|
|
@ -79,11 +79,11 @@ class V17FrappeDeprecationWarning(PendingFrappeDeprecationWarning):
|
|||
|
||||
|
||||
def __get_deprecation_class(graduation: str | None = None, class_name: str | None = None) -> type:
|
||||
current_module = sys.modules[__name__]
|
||||
if graduation:
|
||||
# Scrub the graduation string to ensure it's a valid class name
|
||||
cleaned_graduation = re.sub(r"\W|^(?=\d)", "_", graduation.upper())
|
||||
class_name = f"{cleaned_graduation}FrappeDeprecationWarning"
|
||||
current_module = sys.modules[__name__]
|
||||
try:
|
||||
return getattr(current_module, class_name)
|
||||
except AttributeError:
|
||||
|
|
|
|||
|
|
@ -13,6 +13,15 @@ class TestWorkspace(IntegrationTestCase):
|
|||
frappe.db.delete("DocType", {"module": "Test Module"})
|
||||
frappe.delete_doc("Module Def", "Test Module")
|
||||
|
||||
def test_workspace_conflicts_with_existing_doctype(self):
|
||||
"""Workspace name should not conflict with existing DocType names."""
|
||||
|
||||
create_doctype("Test", "Test Module")
|
||||
workspace = create_workspace(name="Test", label="Test", public=1, title="Test")
|
||||
|
||||
with self.assertRaises(frappe.NameError):
|
||||
workspace.insert()
|
||||
|
||||
# TODO: FIX ME - flaky test!!!
|
||||
# def test_workspace_with_cards_specific_to_a_country(self):
|
||||
# workspace = create_workspace()
|
||||
|
|
@ -65,6 +74,8 @@ def create_workspace(**args):
|
|||
workspace.category = args.category or "Modules"
|
||||
workspace.is_standard = args.is_standard or 1
|
||||
workspace.module = "Test Module"
|
||||
workspace.public = args.public or 0
|
||||
workspace.title = args.title or "Test Workspace"
|
||||
|
||||
return workspace
|
||||
|
||||
|
|
|
|||
|
|
@ -286,7 +286,6 @@ def auto_generate_sidebar_from_module():
|
|||
sidebar.items = sidebar_items
|
||||
sidebar.module = module
|
||||
sidebar.header_icon = "hammer"
|
||||
sidebar.app = frappe.local.module_app.get(frappe.scrub(module), None)
|
||||
sidebars.append(sidebar)
|
||||
return sidebars
|
||||
|
||||
|
|
|
|||
|
|
@ -1207,7 +1207,7 @@ class IconsPane {
|
|||
return;
|
||||
}
|
||||
this.wrapper.append(
|
||||
"<span style='margin-top: 10px; margin-bottom: 20px'>Removed Icons</span>"
|
||||
`<span style='margin-top: 10px; margin-bottom: 20px'>${__("Removed Icons")}</span>`
|
||||
);
|
||||
this.grid = new DesktopIconGrid({
|
||||
name: "hidden-icons-grid",
|
||||
|
|
|
|||
|
|
@ -158,6 +158,13 @@ frappe.setup.SetupWizard = class SetupWizard extends frappe.ui.Slides {
|
|||
this.in_refresh_slides = true;
|
||||
|
||||
this.update_values();
|
||||
const welcome_slide = frappe.setup.slides_settings.find((s) => s.name === "welcome");
|
||||
if (welcome_slide && this.values.language) {
|
||||
const lang_field = welcome_slide.fields.find((f) => f.fieldname === "language");
|
||||
if (lang_field) {
|
||||
lang_field.default = this.values.language;
|
||||
}
|
||||
}
|
||||
frappe.setup.slides = [];
|
||||
frappe.setup.run_event("before_load");
|
||||
|
||||
|
|
@ -444,14 +451,21 @@ frappe.setup.slides_settings = [
|
|||
} else {
|
||||
frappe.setup.utils.load_regional_data(slide, setup_fields);
|
||||
}
|
||||
let current_selection = frappe.wizard.values.language;
|
||||
if (!slide.get_value("language")) {
|
||||
let session_language =
|
||||
current_selection ||
|
||||
frappe.setup.utils.get_language_name_from_code(
|
||||
frappe.boot.lang || navigator.language
|
||||
) || "English";
|
||||
) ||
|
||||
"English";
|
||||
let language_field = slide.get_field("language");
|
||||
language_field.df.default = session_language;
|
||||
|
||||
language_field.set_input(session_language);
|
||||
if (language_field.awesomplete) {
|
||||
language_field.awesomplete.evaluate();
|
||||
}
|
||||
if (!frappe.setup._from_load_messages) {
|
||||
language_field.$input.trigger("change");
|
||||
}
|
||||
|
|
@ -539,7 +553,8 @@ frappe.setup.utils = {
|
|||
frappe.wizard.values.currency = r.message.currency;
|
||||
frappe.wizard.values.country = r.message.country;
|
||||
frappe.wizard.values.timezone = r.message.time_zone;
|
||||
frappe.wizard.values.language = r.message.language;
|
||||
frappe.wizard.values.language =
|
||||
frappe.wizard.values.language || r.message.language;
|
||||
|
||||
frappe.db.get_value(
|
||||
"User",
|
||||
|
|
@ -582,6 +597,9 @@ frappe.setup.utils = {
|
|||
setup_language_field: function (slide) {
|
||||
var language_field = slide.get_field("language");
|
||||
language_field.df.options = frappe.setup.data.lang.languages;
|
||||
if (frappe.wizard.values.language) {
|
||||
language_field.df.default = frappe.wizard.values.language;
|
||||
}
|
||||
language_field.set_options();
|
||||
},
|
||||
|
||||
|
|
@ -647,6 +665,7 @@ frappe.setup.utils = {
|
|||
language: lang,
|
||||
},
|
||||
callback: function () {
|
||||
frappe.wizard.values.language = lang;
|
||||
frappe.setup._from_load_messages = true;
|
||||
frappe.wizard.refresh_slides();
|
||||
},
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ from frappe.model.utils import render_include
|
|||
from frappe.modules import get_module_path, scrub
|
||||
from frappe.monitor import add_data_to_monitor
|
||||
from frappe.permissions import get_role_permissions, get_roles, has_permission
|
||||
from frappe.utils import cint, cstr, flt, format_duration, get_html_format, sbool
|
||||
from frappe.utils import cint, cstr, flt, format_datetime, format_duration, formatdate, get_html_format, sbool
|
||||
from frappe.utils.caching import request_cache
|
||||
|
||||
|
||||
|
|
@ -469,13 +469,27 @@ def format_fields(data: frappe._dict) -> None:
|
|||
if col.get("fieldtype") == "Duration":
|
||||
for row in data.result:
|
||||
index = col.get("fieldname") if isinstance(row, dict) else i
|
||||
if row[index]:
|
||||
row[index] = format_duration(row[index])
|
||||
val = row.get(index) if isinstance(row, dict) else row[index]
|
||||
if val:
|
||||
row[index] = format_duration(val)
|
||||
elif col.get("fieldtype") == "Currency" and col.get("precision"):
|
||||
for row in data.result:
|
||||
index = col.get("fieldname") if isinstance(row, dict) else i
|
||||
if row[index]:
|
||||
row[index] = round(row[index], col.get("precision"))
|
||||
val = row.get(index) if isinstance(row, dict) else row[index]
|
||||
if val:
|
||||
row[index] = round(val, col.get("precision"))
|
||||
elif col.get("fieldtype") == "Date":
|
||||
for row in data.result:
|
||||
index = col.get("fieldname") if isinstance(row, dict) else i
|
||||
val = row.get(index) if isinstance(row, dict) else row[index]
|
||||
if val:
|
||||
row[index] = formatdate(val)
|
||||
elif col.get("fieldtype") == "Datetime":
|
||||
for row in data.result:
|
||||
index = col.get("fieldname") if isinstance(row, dict) else i
|
||||
val = row.get(index) if isinstance(row, dict) else row[index]
|
||||
if val:
|
||||
row[index] = format_datetime(val)
|
||||
|
||||
|
||||
def build_xlsx_data(
|
||||
|
|
|
|||
|
|
@ -809,7 +809,7 @@ def get_match_cond(doctype, as_condition=True):
|
|||
if not as_condition:
|
||||
return cond
|
||||
|
||||
return ((" and " + cond) if cond else "").replace("%", "%%")
|
||||
return ((" and (" + cond + ")") if cond else "").replace("%", "%%")
|
||||
|
||||
|
||||
def build_match_conditions(doctype, user=None, as_condition=True):
|
||||
|
|
|
|||
|
|
@ -256,8 +256,16 @@ def validate_ignore_user_permissions(form_doctype, link_fieldname, link_doctype)
|
|||
frappe.throw(message, title=_('Error validating "Ignore User Permissions"'))
|
||||
|
||||
meta = frappe.get_meta(form_doctype)
|
||||
link_field = meta.get_field(link_fieldname)
|
||||
|
||||
# special early exit - link_fieldname is not being considered here
|
||||
# to avoid cases like bulk edit which have link_fieldname as "value" from failing
|
||||
if any(
|
||||
(field.fieldtype == "Link" and field.options == link_doctype and field.ignore_user_permissions)
|
||||
for field in meta.fields
|
||||
):
|
||||
return
|
||||
|
||||
link_field = meta.get_field(link_fieldname)
|
||||
if not link_field:
|
||||
_throw(
|
||||
_("Field <code>{0}</code> not found in {1}").format(
|
||||
|
|
@ -268,9 +276,6 @@ def validate_ignore_user_permissions(form_doctype, link_fieldname, link_doctype)
|
|||
ignore_user_permissions = link_field.ignore_user_permissions
|
||||
found_doctype = None
|
||||
|
||||
if link_field.fieldtype == "Link":
|
||||
found_doctype = link_field.options
|
||||
|
||||
if link_field.fieldtype == "Table MultiSelect":
|
||||
child_meta = frappe.get_meta(link_field.options)
|
||||
child_link_field = next((field for field in child_meta.fields if field.fieldtype == "Link"), None)
|
||||
|
|
@ -297,6 +302,11 @@ def validate_ignore_user_permissions(form_doctype, link_fieldname, link_doctype)
|
|||
if link_field.fieldtype == "Dynamic Link":
|
||||
return # skip doctype check for Dynamic Link fields
|
||||
|
||||
# all cases of valid Link fields are already covered in the early exit above
|
||||
# the following block only serves to show appropriate error message
|
||||
if link_field.fieldtype == "Link":
|
||||
found_doctype = link_field.options
|
||||
|
||||
if found_doctype != link_doctype:
|
||||
_throw(
|
||||
_("The field {0} in {1} links to {2} and not {3}").format(
|
||||
|
|
|
|||
|
|
@ -13,11 +13,15 @@ def get_all_nodes(doctype: str, label: str, parent: str, tree_method: str | None
|
|||
filters.pop("cmd", None)
|
||||
filters.pop("data", None)
|
||||
|
||||
tree_method = frappe.get_attr(tree_method)
|
||||
try:
|
||||
tree_method = frappe.override_whitelisted_method(tree_method)
|
||||
callable_tree_method = frappe.get_attr(tree_method)
|
||||
except Exception as e:
|
||||
frappe.throw(_("Failed to get method for command {0} with {1}").format(tree_method, str(e)))
|
||||
|
||||
frappe.is_whitelisted(tree_method)
|
||||
frappe.is_whitelisted(callable_tree_method)
|
||||
|
||||
data = tree_method(doctype, parent, **filters)
|
||||
data = callable_tree_method(doctype, parent, **filters)
|
||||
out = [dict(parent=label, data=data)]
|
||||
|
||||
filters.pop("is_root", None)
|
||||
|
|
@ -25,7 +29,7 @@ def get_all_nodes(doctype: str, label: str, parent: str, tree_method: str | None
|
|||
|
||||
while to_check:
|
||||
parent = to_check.pop()
|
||||
data = tree_method(doctype, parent, is_root=False, **filters)
|
||||
data = callable_tree_method(doctype, parent, is_root=False, **filters)
|
||||
out.append(dict(parent=parent, data=data))
|
||||
for d in data:
|
||||
if d.get("expandable"):
|
||||
|
|
|
|||
|
|
@ -17,7 +17,11 @@ def validate_route_conflict(doctype, name):
|
|||
all_names = []
|
||||
for _doctype in ["Page", "Workspace", "DocType"]:
|
||||
all_names.extend(
|
||||
[slug(d) for d in frappe.get_all(_doctype, pluck="name") if (doctype != _doctype and d != name)]
|
||||
[
|
||||
slug(d)
|
||||
for d in frappe.get_all(_doctype, pluck="name")
|
||||
if not (doctype == _doctype and d == name)
|
||||
]
|
||||
)
|
||||
|
||||
if slug(name) in all_names:
|
||||
|
|
|
|||
|
|
@ -188,6 +188,15 @@ frappe.ui.form.on("Auto Email Report", {
|
|||
},
|
||||
});
|
||||
dialog.show();
|
||||
|
||||
// add filters defined in onload event of report
|
||||
if (reference_report.onload) {
|
||||
frappe.query_report = new frappe.views.QueryReport({
|
||||
filters: dialog.fields_list,
|
||||
});
|
||||
reference_report.onload(frappe.query_report);
|
||||
}
|
||||
|
||||
dialog.set_values(filters);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ from datetime import timedelta
|
|||
import frappe
|
||||
from frappe import _, msgprint
|
||||
from frappe.utils import cint, cstr, get_url, now_datetime
|
||||
from frappe.utils.data import getdate
|
||||
from frappe.utils.data import add_to_date, getdate
|
||||
from frappe.utils.verified_command import get_signed_params, verify_request
|
||||
|
||||
# After this percent of failures in every batch, entire batch is aborted.
|
||||
|
|
@ -163,19 +163,21 @@ def flush():
|
|||
|
||||
def get_queue():
|
||||
batch_size = cint(frappe.conf.email_queue_batch_size) or 500
|
||||
undo_window = add_to_date(now_datetime(), seconds=-10)
|
||||
|
||||
return frappe.db.sql(
|
||||
f"""select
|
||||
"""select
|
||||
name, sender
|
||||
from
|
||||
`tabEmail Queue`
|
||||
where
|
||||
(status='Not Sent' or status='Partially Sent') and
|
||||
(send_after is null or send_after < %(now)s)
|
||||
(send_after is null or send_after < %(now)s) and
|
||||
(creation < %(undo_window)s)
|
||||
order
|
||||
by priority desc, retry asc, creation asc
|
||||
limit {batch_size}""",
|
||||
{"now": now_datetime()},
|
||||
limit %(batch_size)s""",
|
||||
{"now": now_datetime(), "undo_window": undo_window, "batch_size": batch_size},
|
||||
as_dict=True,
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -59,6 +59,7 @@ no,Norsk,0
|
|||
pl,Polski,0
|
||||
ps,پښتو,0
|
||||
pt,Português,0
|
||||
pt-BR,Português Brasileiro,0
|
||||
ro,Română,0
|
||||
ru,Русский,0
|
||||
rw,Kinyarwanda,0
|
||||
|
|
|
|||
|
|
|
@ -185,27 +185,40 @@ def parse_template_string(
|
|||
:param options: a dictionary of additional options (optional)
|
||||
:param lineno: starting line number (optional)
|
||||
"""
|
||||
from babel.messages.jslexer import line_re
|
||||
|
||||
prev_character = None
|
||||
current_lineno = lineno
|
||||
level = 0
|
||||
inside_str = False
|
||||
inside_expression_str = False
|
||||
expression_lineno = lineno
|
||||
expression_contents = ""
|
||||
for character in template_string[1:-1]:
|
||||
if not inside_str and character in ('"', "'", "`"):
|
||||
inside_str = character
|
||||
elif inside_str == character and prev_character != r"\\":
|
||||
inside_str = False
|
||||
if level:
|
||||
expression_contents += character
|
||||
if not inside_str:
|
||||
if not level:
|
||||
if character == "{" and prev_character == "$":
|
||||
expression_lineno = current_lineno
|
||||
level += 1
|
||||
elif level and character == "}":
|
||||
level -= 1
|
||||
if level == 0 and expression_contents:
|
||||
expression_contents = expression_contents[:-1]
|
||||
yield from extract_javascript(expression_contents, keywords, options, lineno)
|
||||
lineno += len(line_re.findall(expression_contents))
|
||||
expression_contents = ""
|
||||
else:
|
||||
expression_contents += character
|
||||
|
||||
if inside_expression_str:
|
||||
if inside_expression_str == character and prev_character != r"\\":
|
||||
inside_expression_str = False
|
||||
else:
|
||||
if character in ('"', "'", "`"):
|
||||
inside_expression_str = character
|
||||
elif character == "{":
|
||||
level += 1
|
||||
elif character == "}":
|
||||
level -= 1
|
||||
if level == 0 and expression_contents:
|
||||
expression_contents = expression_contents[:-1]
|
||||
yield from extract_javascript(
|
||||
expression_contents,
|
||||
keywords,
|
||||
options,
|
||||
expression_lineno,
|
||||
)
|
||||
expression_contents = ""
|
||||
inside_expression_str = False
|
||||
if character == "\n":
|
||||
current_lineno += 1
|
||||
prev_character = character
|
||||
|
|
|
|||
|
|
@ -15,3 +15,17 @@ class TestJavaScript(IntegrationTestCase):
|
|||
next(extract_javascript(code)),
|
||||
(1, "__", ("Test", None, "Context")),
|
||||
)
|
||||
|
||||
def test_extract_javascript_from_template_literal_attribute(self):
|
||||
code = "let test = `<button title=\"${__('In attribute')}\">${__('In text')}</button>`;"
|
||||
self.assertEqual(
|
||||
list(extract_javascript(code)),
|
||||
[(1, "__", "In attribute"), (1, "__", "In text")],
|
||||
)
|
||||
|
||||
def test_extract_javascript_template_literal_multiline_line_numbers(self):
|
||||
code = "let test = `\n<button title=\"${__('In attribute')}\">\n ${__('In text')}\n</button>\n`;"
|
||||
self.assertEqual(
|
||||
list(extract_javascript(code)),
|
||||
[(2, "__", "In attribute"), (3, "__", "In text")],
|
||||
)
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
|
||||
import os
|
||||
from mimetypes import guess_type
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from werkzeug.wrappers import Response
|
||||
|
|
@ -11,11 +12,11 @@ import frappe
|
|||
import frappe.sessions
|
||||
import frappe.utils
|
||||
from frappe import _, is_whitelisted, ping
|
||||
from frappe.core.doctype.file.utils import find_file_by_url
|
||||
from frappe.core.doctype.file.utils import find_file_by_url, get_safe_file_name
|
||||
from frappe.core.doctype.server_script.server_script_utils import get_server_script_map
|
||||
from frappe.monitor import add_data_to_monitor
|
||||
from frappe.permissions import check_doctype_permission
|
||||
from frappe.utils import cint
|
||||
from frappe.utils import cint, get_files_path
|
||||
from frappe.utils.csvutils import build_csv_response
|
||||
from frappe.utils.deprecations import deprecated
|
||||
from frappe.utils.image import optimize_image
|
||||
|
|
@ -162,9 +163,27 @@ def upload_file():
|
|||
|
||||
if "file" in files:
|
||||
file = files["file"]
|
||||
content = file.stream.read()
|
||||
filename = file.filename
|
||||
|
||||
if frappe.form_dict.chunk_index:
|
||||
current_chunk = int(frappe.form_dict.chunk_index)
|
||||
total_chunks = int(frappe.form_dict.total_chunk_count)
|
||||
offset = int(frappe.form_dict.chunk_byte_offset)
|
||||
else:
|
||||
offset = 0
|
||||
current_chunk = 0
|
||||
total_chunks = 1
|
||||
|
||||
temp_path = Path(get_files_path(".temp-" + get_safe_file_name(filename), is_private=is_private))
|
||||
with temp_path.open("ab" if current_chunk > 0 else "wb") as f:
|
||||
total_file_size = frappe.form_dict.total_file_size or 0
|
||||
f.seek(offset)
|
||||
f.write(file.stream.read())
|
||||
if not f.tell() >= int(total_file_size) or current_chunk != total_chunks - 1:
|
||||
return
|
||||
|
||||
content = temp_path.read_bytes()
|
||||
temp_path.unlink()
|
||||
content_type = guess_type(filename)[0]
|
||||
if optimize and content_type and content_type.startswith("image/"):
|
||||
args = {"content": content, "content_type": content_type}
|
||||
|
|
|
|||
|
|
@ -210,6 +210,7 @@ scheduler_events = {
|
|||
# 5 minutes
|
||||
"0/5 * * * *": [
|
||||
"frappe.email.doctype.notification.notification.trigger_offset_alerts",
|
||||
"frappe.search.sqlite_search.index_docs_in_queue",
|
||||
],
|
||||
# 15 minutes
|
||||
"0/15 * * * *": [
|
||||
|
|
|
|||
|
|
@ -44,6 +44,11 @@ def get_headers():
|
|||
def current_site_info():
|
||||
from frappe.utils import cint
|
||||
|
||||
cache_key = f"fc_current_site_info:{frappe.local.site}"
|
||||
cached_data = frappe.cache().get_value(cache_key)
|
||||
if cached_data:
|
||||
return cached_data
|
||||
|
||||
res = {}
|
||||
request = requests.post(f"{get_base_url()}/api/method/press.saas.api.site.info", headers=get_headers())
|
||||
if request.status_code == 200:
|
||||
|
|
@ -51,13 +56,17 @@ def current_site_info():
|
|||
if not res or not isinstance(res, dict):
|
||||
return None
|
||||
|
||||
return {
|
||||
site_info = {
|
||||
**res,
|
||||
"site_name": get_site_name(),
|
||||
"base_url": get_base_url(),
|
||||
"setup_complete": cint(frappe.get_system_settings("setup_complete")),
|
||||
}
|
||||
|
||||
frappe.cache().set_value(cache_key, site_info, expires_in_sec=600)
|
||||
|
||||
return site_info
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def api(method: str, data: str | dict[str, Any] | None = None):
|
||||
|
|
|
|||
1578
frappe/locale/ar.po
1578
frappe/locale/ar.po
File diff suppressed because it is too large
Load diff
1580
frappe/locale/bs.po
1580
frappe/locale/bs.po
File diff suppressed because it is too large
Load diff
1576
frappe/locale/cs.po
1576
frappe/locale/cs.po
File diff suppressed because it is too large
Load diff
1576
frappe/locale/da.po
1576
frappe/locale/da.po
File diff suppressed because it is too large
Load diff
1578
frappe/locale/de.po
1578
frappe/locale/de.po
File diff suppressed because it is too large
Load diff
1580
frappe/locale/eo.po
1580
frappe/locale/eo.po
File diff suppressed because it is too large
Load diff
1578
frappe/locale/es.po
1578
frappe/locale/es.po
File diff suppressed because it is too large
Load diff
1576
frappe/locale/fa.po
1576
frappe/locale/fa.po
File diff suppressed because it is too large
Load diff
1576
frappe/locale/fr.po
1576
frappe/locale/fr.po
File diff suppressed because it is too large
Load diff
1580
frappe/locale/hr.po
1580
frappe/locale/hr.po
File diff suppressed because it is too large
Load diff
1580
frappe/locale/hu.po
1580
frappe/locale/hu.po
File diff suppressed because it is too large
Load diff
1576
frappe/locale/id.po
1576
frappe/locale/id.po
File diff suppressed because it is too large
Load diff
1578
frappe/locale/it.po
1578
frappe/locale/it.po
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
1576
frappe/locale/my.po
1576
frappe/locale/my.po
File diff suppressed because it is too large
Load diff
1578
frappe/locale/nb.po
1578
frappe/locale/nb.po
File diff suppressed because it is too large
Load diff
1578
frappe/locale/nl.po
1578
frappe/locale/nl.po
File diff suppressed because it is too large
Load diff
1576
frappe/locale/pl.po
1576
frappe/locale/pl.po
File diff suppressed because it is too large
Load diff
1576
frappe/locale/pt.po
1576
frappe/locale/pt.po
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
1578
frappe/locale/ru.po
1578
frappe/locale/ru.po
File diff suppressed because it is too large
Load diff
1576
frappe/locale/sl.po
1576
frappe/locale/sl.po
File diff suppressed because it is too large
Load diff
1580
frappe/locale/sr.po
1580
frappe/locale/sr.po
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
1582
frappe/locale/sv.po
1582
frappe/locale/sv.po
File diff suppressed because it is too large
Load diff
1578
frappe/locale/th.po
1578
frappe/locale/th.po
File diff suppressed because it is too large
Load diff
1578
frappe/locale/tr.po
1578
frappe/locale/tr.po
File diff suppressed because it is too large
Load diff
1578
frappe/locale/vi.po
1578
frappe/locale/vi.po
File diff suppressed because it is too large
Load diff
1578
frappe/locale/zh.po
1578
frappe/locale/zh.po
File diff suppressed because it is too large
Load diff
|
|
@ -224,6 +224,7 @@ def get_permitted_fields(
|
|||
meta = frappe.get_meta(doctype)
|
||||
valid_columns = meta.get_valid_columns()
|
||||
|
||||
# note: any change here should also be made in _get_filterable_fields in query.py
|
||||
if doctype in CORE_DOCTYPES:
|
||||
return valid_columns
|
||||
|
||||
|
|
|
|||
|
|
@ -1224,7 +1224,7 @@ class Document(BaseDocument):
|
|||
if not self.meta.issingle and self._action != "discard":
|
||||
self.check_docstatus_transition(previous.docstatus)
|
||||
|
||||
def check_docstatus_transition(self, to_docstatus):
|
||||
def check_docstatus_transition(self, from_docstatus):
|
||||
"""Ensures valid `docstatus` transition.
|
||||
Valid transitions are (number in brackets is `docstatus`):
|
||||
|
||||
|
|
@ -1234,34 +1234,43 @@ class Document(BaseDocument):
|
|||
- Submit (1) > Cancel (2)
|
||||
|
||||
"""
|
||||
if to_docstatus == DocStatus.DRAFT:
|
||||
if self.docstatus.is_draft():
|
||||
if self.flags.skip_docstatus_validation:
|
||||
return
|
||||
|
||||
to_docstatus = self.docstatus
|
||||
if from_docstatus == DocStatus.DRAFT:
|
||||
if to_docstatus.is_draft():
|
||||
self._action = "save"
|
||||
elif self.docstatus.is_submitted():
|
||||
elif to_docstatus.is_submitted():
|
||||
if not getattr(self.meta, "is_submittable", False):
|
||||
frappe.throw(
|
||||
_("Cannot change docstatus of non submittable doctype {0}").format(self.doctype),
|
||||
frappe.DocstatusTransitionError,
|
||||
)
|
||||
self._action = "submit"
|
||||
self.check_permission("submit")
|
||||
elif self.docstatus.is_cancelled():
|
||||
elif to_docstatus.is_cancelled():
|
||||
raise frappe.DocstatusTransitionError(
|
||||
_("Cannot change docstatus from 0 (Draft) to 2 (Cancelled)")
|
||||
)
|
||||
else:
|
||||
raise frappe.ValidationError(_("Invalid docstatus"), self.docstatus)
|
||||
raise frappe.ValidationError(_("Invalid docstatus"), to_docstatus)
|
||||
|
||||
elif to_docstatus == DocStatus.SUBMITTED:
|
||||
if self.docstatus.is_submitted():
|
||||
elif from_docstatus == DocStatus.SUBMITTED:
|
||||
if to_docstatus.is_submitted():
|
||||
self._action = "update_after_submit"
|
||||
self.check_permission("submit")
|
||||
elif self.docstatus.is_cancelled():
|
||||
elif to_docstatus.is_cancelled():
|
||||
self._action = "cancel"
|
||||
self.check_permission("cancel")
|
||||
elif self.docstatus.is_draft():
|
||||
elif to_docstatus.is_draft():
|
||||
raise frappe.DocstatusTransitionError(
|
||||
_("Cannot change docstatus from 1 (Submitted) to 0 (Draft)")
|
||||
)
|
||||
else:
|
||||
raise frappe.ValidationError(_("Invalid docstatus"), self.docstatus)
|
||||
raise frappe.ValidationError(_("Invalid docstatus"), to_docstatus)
|
||||
|
||||
elif to_docstatus == DocStatus.CANCELLED:
|
||||
elif from_docstatus == DocStatus.CANCELLED:
|
||||
raise frappe.ValidationError(_("Cannot edit cancelled document"))
|
||||
|
||||
def set_parent_in_children(self):
|
||||
|
|
|
|||
|
|
@ -705,7 +705,8 @@ class Meta(Document):
|
|||
)
|
||||
|
||||
if 0 not in permlevel_access and permission_type in ("read", "select"):
|
||||
if frappe.share.get_shared(self.name, user, rights=["read"], limit=1):
|
||||
check_doctype = parenttype if self.istable and parenttype else self.name
|
||||
if frappe.share.get_shared(check_doctype, user, rights=["read"], limit=1):
|
||||
permlevel_access.add(0)
|
||||
|
||||
permitted_fieldnames.extend(
|
||||
|
|
|
|||
|
|
@ -208,6 +208,19 @@ def has_permission(
|
|||
debug and _debug_log("Checking if document/doctype is explicitly shared with user")
|
||||
perm = false_if_not_shared()
|
||||
|
||||
# select permission is implied by read permission
|
||||
if not perm and ptype == "select":
|
||||
perm = has_permission(
|
||||
doctype,
|
||||
ptype="read",
|
||||
doc=doc,
|
||||
user=user,
|
||||
parent_doctype=parent_doctype,
|
||||
print_logs=print_logs,
|
||||
debug=debug,
|
||||
ignore_share_permissions=ignore_share_permissions,
|
||||
)
|
||||
|
||||
return bool(perm)
|
||||
|
||||
|
||||
|
|
@ -855,7 +868,11 @@ def has_child_permission(
|
|||
return False
|
||||
|
||||
permlevel = parent_meta.get_field(parentfield).permlevel
|
||||
accessible_permlevels = parent_meta.get_permlevel_access(ptype, user=user)
|
||||
# checking for select == checking for "select or read"
|
||||
# select does not support access of higher permlevel child tables, but read does
|
||||
accessible_permlevels = parent_meta.get_permlevel_access(
|
||||
"read" if ptype == "select" else ptype, user=user
|
||||
)
|
||||
if permlevel > 0 and permlevel not in accessible_permlevels:
|
||||
push_perm_check_log(
|
||||
_("Insufficient Permission Level for {0}").format(frappe.bold(parent_doctype)), debug=debug
|
||||
|
|
|
|||
|
|
@ -206,16 +206,6 @@ frappe.ui.form.PrintView = class {
|
|||
this.set_breadcrumbs();
|
||||
this.setup_customize_dialog();
|
||||
|
||||
// print designer link
|
||||
if (!cint(frappe.boot.sysdefaults.disable_product_suggestion)) {
|
||||
if (!Object.keys(frappe.boot.versions).includes("print_designer")) {
|
||||
this.page.add_inner_message(`
|
||||
<a style="line-height: 2.4" href="https://frappecloud.com/marketplace/apps/print_designer?utm_source=framework-desk&utm_medium=print-view&utm_campaign=try-link">
|
||||
${__("Try the new Print Designer")}
|
||||
</a>
|
||||
`);
|
||||
}
|
||||
}
|
||||
let tasks = [
|
||||
this.set_default_print_format,
|
||||
this.set_default_print_language,
|
||||
|
|
|
|||
|
|
@ -33,14 +33,10 @@ $(document).ready(function () {
|
|||
!frappe.is_mobile() &&
|
||||
frappe.user.has_role("System Manager");
|
||||
if (visiblity_condition && isFCUser) {
|
||||
frappe.router.on("change", function () {
|
||||
if (frappe.get_route()[0] == "") {
|
||||
addChatBubble();
|
||||
toggleChatBubble(true);
|
||||
} else {
|
||||
toggleChatBubble(false);
|
||||
}
|
||||
});
|
||||
if (site_info.trial_end_date && trial_end_date > new Date()) {
|
||||
addChatBubble();
|
||||
toggleChatBubble(true);
|
||||
}
|
||||
}
|
||||
if (isFCUser) {
|
||||
$.extend(card_args, {
|
||||
|
|
@ -100,30 +96,24 @@ function addChatBubble() {
|
|||
const desk_apps = ["erpnext", "hrms"];
|
||||
|
||||
const apps_allowed = desk_apps.some((app) => all_apps.includes(app));
|
||||
if (checkBusinessHours() && apps_allowed) {
|
||||
if (apps_allowed) {
|
||||
let chat_banner = document.createElement("script");
|
||||
chat_banner.setAttribute("id", "chat_widget_trigger");
|
||||
chat_banner.innerHTML =
|
||||
'(function(d,t){var BASE_URL="https://chat.frappe.cloud";var g=d.createElement(t),s=d.getElementsByTagName(t)[0];g.src=BASE_URL+"/packs/js/sdk.js";g.async=true;s.parentNode.insertBefore(g,s);g.onload=function(){window.chatwootSDK.run({websiteToken:"LdmfJzftdJGEcFjoTqk8CrSq",baseUrl:BASE_URL})}})(document,"script");';
|
||||
'window.chatwootSettings = {"position":"right","launcherTitle":"Chat with us", darkMode: "auto"}; (function(d,t){var BASE_URL="https://chat.frappe.cloud";var g=d.createElement(t),s=d.getElementsByTagName(t)[0];g.src=BASE_URL+"/packs/js/sdk.js";g.async=true;s.parentNode.insertBefore(g,s);g.onload=function(){window.chatwootSDK.run({websiteToken:"LdmfJzftdJGEcFjoTqk8CrSq",baseUrl:BASE_URL})}})(document,"script");';
|
||||
document.body.append(chat_banner);
|
||||
const root = document.documentElement;
|
||||
root.style.setProperty("--s-700", "var(--gray-500)");
|
||||
|
||||
// Add padding to the main section to avoid overlapping with the chat bubble
|
||||
const main_section = document.getElementsByClassName("main-section");
|
||||
|
||||
if (main_section) {
|
||||
main_section[0].style.paddingBottom = "90px";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function checkBusinessHours() {
|
||||
let current_time = new Date();
|
||||
const ist_time = new Date(current_time.toLocaleString("en-US", { timeZone: "Asia/Kolkata" }));
|
||||
|
||||
const hours = ist_time.getHours();
|
||||
const day = ist_time.getDay();
|
||||
|
||||
const is_weekend = day === 0 || day === 6;
|
||||
const is_business_hour = hours >= 11 && hours < 18;
|
||||
|
||||
return !is_weekend && is_business_hour;
|
||||
}
|
||||
|
||||
function toggleChatBubble(toggle) {
|
||||
if (toggle) {
|
||||
$(".woot-widget-holder").show();
|
||||
|
|
|
|||
|
|
@ -214,9 +214,6 @@ export function evaluate_depends_on_value(expression, doc) {
|
|||
} else if (expression.substr(0, 5) == "eval:") {
|
||||
try {
|
||||
out = frappe.utils.eval(expression.substr(5), { doc, parent });
|
||||
if (parent && parent.istable && expression.includes("is_submittable")) {
|
||||
out = true;
|
||||
}
|
||||
} catch (e) {
|
||||
frappe.throw(__('Invalid "depends_on" expression'));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -377,15 +377,17 @@ frappe.Application = class Application {
|
|||
logout() {
|
||||
var me = this;
|
||||
me.logged_out = true;
|
||||
return frappe.call({
|
||||
method: "logout",
|
||||
callback: function (r) {
|
||||
if (r.exc) {
|
||||
return;
|
||||
}
|
||||
frappe.confirm(__("Are you sure you want to log out?"), function () {
|
||||
return frappe.call({
|
||||
method: "logout",
|
||||
callback: function (r) {
|
||||
if (r.exc) {
|
||||
return;
|
||||
}
|
||||
|
||||
me.redirect_to_login();
|
||||
},
|
||||
me.redirect_to_login();
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
handle_session_expired() {
|
||||
|
|
|
|||
|
|
@ -568,57 +568,66 @@ function return_as_dataurl() {
|
|||
close_dialog.value = true;
|
||||
return Promise.all(promises);
|
||||
}
|
||||
function upload_file(file, i) {
|
||||
async function upload_file(file, i) {
|
||||
currently_uploading.value = i;
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
let xhr = new XMLHttpRequest();
|
||||
xhr.upload.addEventListener("loadstart", (e) => {
|
||||
file.uploading = true;
|
||||
});
|
||||
xhr.upload.addEventListener("progress", (e) => {
|
||||
if (e.lengthComputable) {
|
||||
file.progress = e.loaded;
|
||||
file.total = e.total;
|
||||
}
|
||||
});
|
||||
xhr.upload.addEventListener("load", (e) => {
|
||||
file.uploading = false;
|
||||
});
|
||||
xhr.addEventListener("error", (e) => {
|
||||
file.failed = true;
|
||||
reject();
|
||||
});
|
||||
xhr.onreadystatechange = () => {
|
||||
if (xhr.readyState == XMLHttpRequest.DONE) {
|
||||
const CHUNK_SIZE = frappe.boot.file_chunk_size;
|
||||
|
||||
const use_chunks = file.file_obj && file.file_obj.size > CHUNK_SIZE;
|
||||
const total_chunks = use_chunks ? Math.ceil(file.file_obj.size / CHUNK_SIZE) : 1;
|
||||
|
||||
const send_chunk = (chunk_blob, chunk_index, chunk_byte_offset) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
let xhr = new XMLHttpRequest();
|
||||
|
||||
xhr.upload.addEventListener("loadstart", () => {
|
||||
file.uploading = true;
|
||||
});
|
||||
xhr.upload.addEventListener("progress", (e) => {
|
||||
if (e.lengthComputable) {
|
||||
file.progress = chunk_byte_offset + e.loaded;
|
||||
file.total = file.file_obj?.size || e.total;
|
||||
}
|
||||
});
|
||||
xhr.upload.addEventListener("load", () => {
|
||||
if (chunk_index === total_chunks - 1) {
|
||||
file.uploading = false;
|
||||
}
|
||||
});
|
||||
xhr.addEventListener("error", () => {
|
||||
file.failed = true;
|
||||
reject();
|
||||
});
|
||||
xhr.onreadystatechange = () => {
|
||||
if (xhr.readyState !== XMLHttpRequest.DONE) return;
|
||||
|
||||
if (xhr.status === 200) {
|
||||
resolve();
|
||||
file.request_succeeded = true;
|
||||
let r = null;
|
||||
let file_doc = null;
|
||||
try {
|
||||
r = JSON.parse(xhr.responseText);
|
||||
if (r.message.doctype === "File") {
|
||||
file_doc = r.message;
|
||||
// Only the last chunk returns a meaningful response
|
||||
if (chunk_index === total_chunks - 1) {
|
||||
file.request_succeeded = true;
|
||||
let r = null;
|
||||
let file_doc = null;
|
||||
try {
|
||||
r = JSON.parse(xhr.responseText);
|
||||
if (r.message?.doctype === "File") {
|
||||
file_doc = r.message;
|
||||
}
|
||||
} catch (e) {
|
||||
r = xhr.responseText;
|
||||
}
|
||||
} catch (e) {
|
||||
r = xhr.responseText;
|
||||
}
|
||||
|
||||
file.doc = file_doc;
|
||||
|
||||
if (props.on_success) {
|
||||
props.on_success(file_doc, r);
|
||||
}
|
||||
|
||||
if (
|
||||
i == files.value.length - 1 &&
|
||||
files.value.every((file) => file.request_succeeded)
|
||||
) {
|
||||
close_dialog.value = true;
|
||||
}
|
||||
if (show_web_link.value && file.file_url) {
|
||||
close_dialog.value = true;
|
||||
file.doc = file_doc;
|
||||
if (props.on_success) {
|
||||
props.on_success(file_doc, r);
|
||||
}
|
||||
if (
|
||||
(i == files.value.length - 1 &&
|
||||
files.value.every((f) => f.request_succeeded)) ||
|
||||
(show_web_link.value && file.file_url)
|
||||
) {
|
||||
close_dialog.value = true;
|
||||
}
|
||||
}
|
||||
} else if (xhr.status === 403) {
|
||||
reject();
|
||||
|
|
@ -669,60 +678,58 @@ function upload_file(file, i) {
|
|||
}
|
||||
frappe.request.cleanup({}, error);
|
||||
}
|
||||
};
|
||||
|
||||
xhr.open("POST", "/api/method/upload_file", true);
|
||||
xhr.setRequestHeader("Accept", "application/json");
|
||||
xhr.setRequestHeader("X-Frappe-CSRF-Token", frappe.csrf_token);
|
||||
|
||||
let form_data = new FormData();
|
||||
|
||||
if (chunk_blob) {
|
||||
form_data.append("file", chunk_blob, file.name);
|
||||
}
|
||||
};
|
||||
xhr.open("POST", "/api/method/upload_file", true);
|
||||
xhr.setRequestHeader("Accept", "application/json");
|
||||
xhr.setRequestHeader("X-Frappe-CSRF-Token", frappe.csrf_token);
|
||||
|
||||
let form_data = new FormData();
|
||||
if (file.file_obj) {
|
||||
form_data.append("file", file.file_obj, file.name);
|
||||
}
|
||||
form_data.append("is_private", +file.private);
|
||||
form_data.append("folder", props.folder);
|
||||
form_data.append("is_private", +file.private);
|
||||
form_data.append("folder", props.folder);
|
||||
form_data.append("total_file_size", file.file_obj?.size ?? 0);
|
||||
|
||||
if (file.file_url) {
|
||||
form_data.append("file_url", file.file_url);
|
||||
}
|
||||
if (file.file_size) {
|
||||
form_data.append("file_size", file.file_size);
|
||||
}
|
||||
if (file.file_name) {
|
||||
form_data.append("file_name", file.file_name);
|
||||
}
|
||||
if (file.library_file_name) {
|
||||
form_data.append("library_file_name", file.library_file_name);
|
||||
}
|
||||
if (use_chunks) {
|
||||
form_data.append("chunk_index", chunk_index);
|
||||
form_data.append("total_chunk_count", total_chunks);
|
||||
form_data.append("chunk_byte_offset", chunk_byte_offset);
|
||||
}
|
||||
|
||||
if (props.doctype) {
|
||||
form_data.append("doctype", props.doctype);
|
||||
}
|
||||
if (file.file_url) form_data.append("file_url", file.file_url);
|
||||
if (file.file_size) form_data.append("file_size", file.file_size);
|
||||
if (file.file_name) form_data.append("file_name", file.file_name);
|
||||
if (file.library_file_name)
|
||||
form_data.append("library_file_name", file.library_file_name);
|
||||
if (props.doctype) form_data.append("doctype", props.doctype);
|
||||
if (props.docname) form_data.append("docname", props.docname);
|
||||
if (props.fieldname) form_data.append("fieldname", props.fieldname);
|
||||
if (props.method) form_data.append("method", props.method);
|
||||
if (file.optimize) form_data.append("optimize", true);
|
||||
if (props.attach_doc_image) {
|
||||
form_data.append("max_width", 200);
|
||||
form_data.append("max_height", 200);
|
||||
}
|
||||
|
||||
if (props.docname) {
|
||||
form_data.append("docname", props.docname);
|
||||
}
|
||||
xhr.send(form_data);
|
||||
});
|
||||
};
|
||||
|
||||
if (props.fieldname) {
|
||||
form_data.append("fieldname", props.fieldname);
|
||||
}
|
||||
|
||||
if (props.method) {
|
||||
form_data.append("method", props.method);
|
||||
}
|
||||
|
||||
if (file.optimize) {
|
||||
form_data.append("optimize", true);
|
||||
}
|
||||
|
||||
if (props.attach_doc_image) {
|
||||
form_data.append("max_width", 200);
|
||||
form_data.append("max_height", 200);
|
||||
}
|
||||
|
||||
xhr.send(form_data);
|
||||
});
|
||||
// Slice and send chunks sequentially
|
||||
let chunk_byte_offset = 0;
|
||||
for (let chunk_index = 0; chunk_index < total_chunks; chunk_index++) {
|
||||
const chunk_blob = file.file_obj
|
||||
? file.file_obj.slice(chunk_byte_offset, chunk_byte_offset + CHUNK_SIZE)
|
||||
: null;
|
||||
await send_chunk(chunk_blob, chunk_index, chunk_byte_offset);
|
||||
chunk_byte_offset += CHUNK_SIZE;
|
||||
}
|
||||
}
|
||||
|
||||
function parse_error_response(response_text) {
|
||||
let error_message = "";
|
||||
let server_messages = [];
|
||||
|
|
|
|||
|
|
@ -37,8 +37,8 @@ export default class Column {
|
|||
}
|
||||
|
||||
resize_all_columns() {
|
||||
// distribute visible columns equally
|
||||
let all_columns = this.section.wrapper.find(".form-column");
|
||||
// distribute visible columns equally, scoped to this section's direct children only
|
||||
let all_columns = this.section.body.children(".form-column");
|
||||
let visible_columns = all_columns.filter(":not(.hide-control)");
|
||||
let columns = visible_columns.length || all_columns.length;
|
||||
let colspan = cint(12 / columns);
|
||||
|
|
|
|||
|
|
@ -938,6 +938,17 @@ frappe.ui.form.ControlLink = class ControlLink extends frappe.ui.form.ControlDat
|
|||
.then((response) => {
|
||||
if (!response) return;
|
||||
|
||||
const has_filters = !!(args.filters && Object.keys(args.filters).length);
|
||||
if (!response.name && has_filters) {
|
||||
frappe.show_alert({
|
||||
message: __("{0}: {1} did not match any results.", [
|
||||
__(this.df.label || this.df.fieldname),
|
||||
value,
|
||||
]),
|
||||
indicator: "red",
|
||||
});
|
||||
}
|
||||
|
||||
update_dependant_fields(response);
|
||||
return response.name;
|
||||
});
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ class Mention {
|
|||
return `${item.value}`;
|
||||
},
|
||||
mentionDenotationChars: ["@"],
|
||||
allowedChars: /^[a-zA-Z0-9_]*$/,
|
||||
allowedChars: /^[\p{L}0-9_]*$/u,
|
||||
minChars: 0,
|
||||
maxChars: 31,
|
||||
offsetTop: 2,
|
||||
|
|
|
|||
|
|
@ -254,7 +254,7 @@ frappe.ui.form.ControlTextEditor = class ControlTextEditor extends frappe.ui.for
|
|||
let me = this;
|
||||
|
||||
return {
|
||||
allowedChars: /^[A-Za-z0-9_]*$/,
|
||||
allowedChars: /^[\p{L}0-9_]*$/u,
|
||||
mentionDenotationChars: ["@"],
|
||||
isolateCharacter: true,
|
||||
|
||||
|
|
|
|||
|
|
@ -12,13 +12,14 @@ class FormTimeline extends BaseTimeline {
|
|||
super.make();
|
||||
this.setup_timeline_actions();
|
||||
this.render_timeline_items();
|
||||
this.setup_activity_toggle();
|
||||
}
|
||||
|
||||
refresh() {
|
||||
super.refresh();
|
||||
this.frm.trigger("timeline_refresh");
|
||||
this.setup_activity_toggle();
|
||||
this.setup_document_email_link();
|
||||
this.toggle_show_all_activity();
|
||||
}
|
||||
|
||||
setup_timeline_actions() {
|
||||
|
|
@ -48,11 +49,17 @@ class FormTimeline extends BaseTimeline {
|
|||
}
|
||||
}
|
||||
|
||||
setup_activity_toggle() {
|
||||
const doc_info = this.doc_info || this.frm.get_docinfo();
|
||||
const has_communications = () =>
|
||||
doc_info.communications?.length || doc_info.comments?.length;
|
||||
toggle_show_all_activity() {
|
||||
const btn = this.timeline_wrapper.find(".timeline-item .show-all-activity");
|
||||
btn.toggle(!!this.has_communications());
|
||||
}
|
||||
|
||||
has_communications() {
|
||||
const doc_info = this.doc_info || this.frm.get_docinfo();
|
||||
return doc_info.communications?.length || doc_info.comments?.length;
|
||||
}
|
||||
|
||||
setup_activity_toggle() {
|
||||
this.timeline_wrapper.remove(this.timeline_actions_wrapper);
|
||||
this.timeline_wrapper.find(".timeline-item.activity-title").remove();
|
||||
this.timeline_wrapper.prepend(`
|
||||
|
|
@ -62,28 +69,26 @@ class FormTimeline extends BaseTimeline {
|
|||
`);
|
||||
|
||||
const me = this;
|
||||
if (has_communications()) {
|
||||
this.timeline_wrapper
|
||||
.find(".timeline-item.activity-title")
|
||||
.append(
|
||||
`
|
||||
<div class="d-flex align-items-center show-all-activity">
|
||||
<span style="color: var(--text-light); margin:0px 6px;">${__("Show all activity")}</span>
|
||||
<label class="switch">
|
||||
<input type="checkbox">
|
||||
<span class="slider round"></span>
|
||||
</label>
|
||||
</div>
|
||||
this.timeline_wrapper
|
||||
.find(".timeline-item.activity-title")
|
||||
.append(
|
||||
`
|
||||
)
|
||||
.find("input[type=checkbox]")
|
||||
.prop("checked", !me.only_communication)
|
||||
.on("click", function (e) {
|
||||
me.only_communication = !this.checked;
|
||||
me.render_timeline_items();
|
||||
$(this).tab("show");
|
||||
});
|
||||
}
|
||||
<div class="flex align-items-center show-all-activity">
|
||||
<span style="color: var(--text-light); margin:0px 6px;">${__("Show all activity")}</span>
|
||||
<label class="switch">
|
||||
<input type="checkbox">
|
||||
<span class="slider round"></span>
|
||||
</label>
|
||||
</div>
|
||||
`
|
||||
)
|
||||
.find("input[type=checkbox]")
|
||||
.prop("checked", !me.only_communication)
|
||||
.on("click", function (e) {
|
||||
me.only_communication = !this.checked;
|
||||
me.render_timeline_items();
|
||||
$(this).tab("show");
|
||||
});
|
||||
this.timeline_wrapper
|
||||
.find(".timeline-item.activity-title")
|
||||
.append(this.timeline_actions_wrapper);
|
||||
|
|
|
|||
|
|
@ -1355,7 +1355,11 @@ frappe.ui.form.Form = class FrappeForm {
|
|||
frappe.re_route[frappe.router.get_sub_path()] = `${encodeURIComponent(
|
||||
frappe.router.slug(this.doctype)
|
||||
)}/${encodeURIComponent(name)}`;
|
||||
!frappe._from_link && frappe.set_route("Form", this.doctype, name);
|
||||
|
||||
// Skip routing only when the document is created from a Form view's Link field
|
||||
if (!frappe._from_link?.field_obj?.frm) {
|
||||
frappe.set_route("Form", this.doctype, name);
|
||||
}
|
||||
}
|
||||
|
||||
// ACTIONS
|
||||
|
|
|
|||
|
|
@ -331,6 +331,10 @@ export default class GridRow {
|
|||
// remove row
|
||||
if (!this.open_form_button) {
|
||||
this.open_form_button = $('<div class="col"></div>').appendTo(this.row);
|
||||
this.open_form_button.on("click", function (e) {
|
||||
me.toggle_view();
|
||||
return false;
|
||||
});
|
||||
|
||||
if (!this.configure_columns) {
|
||||
const edit_msg = __("Edit", "", "Edit grid row");
|
||||
|
|
@ -338,12 +342,8 @@ export default class GridRow {
|
|||
<div class="btn-open-row" data-toggle="tooltip" data-placement="right" title="${edit_msg}">
|
||||
<a>${frappe.utils.icon("edit", "xs")}</a>
|
||||
</div>
|
||||
`)
|
||||
.appendTo(this.open_form_button)
|
||||
.on("click", function () {
|
||||
me.toggle_view();
|
||||
return false;
|
||||
});
|
||||
`).appendTo(this.open_form_button);
|
||||
|
||||
$(this.open_form_button)
|
||||
.parent()
|
||||
.on("keydown", function (ev) {
|
||||
|
|
@ -836,9 +836,6 @@ export default class GridRow {
|
|||
} else if (expression.substr(0, 5) == "eval:") {
|
||||
try {
|
||||
out = frappe.utils.eval(expression.substr(5), { doc, parent });
|
||||
if (parent && parent.istable && expression.includes("is_submittable")) {
|
||||
out = true;
|
||||
}
|
||||
} catch (e) {
|
||||
frappe.throw(__('Invalid "depends_on" expression'));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -59,9 +59,7 @@ export default class GridRowForm {
|
|||
${__("Insert Above")}</button>
|
||||
<button class="btn btn-secondary btn-sm pull-right grid-insert-row-below hidden-xs">
|
||||
${__("Insert Below")}</button>
|
||||
<button class="btn btn-danger btn-sm pull-right grid-delete-row">
|
||||
${frappe.utils.icon("trash-2")} ${__("Delete")}
|
||||
</button>
|
||||
<button class="btn btn-danger btn-sm pull-right grid-delete-row">${__("Delete")}</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -802,9 +802,6 @@ frappe.ui.form.Layout = class Layout {
|
|||
} else if (expression.substr(0, 5) == "eval:") {
|
||||
try {
|
||||
out = frappe.utils.eval(expression.substr(5), { doc, parent });
|
||||
if (parent && parent.istable && expression.includes("is_submittable")) {
|
||||
out = true;
|
||||
}
|
||||
} catch (e) {
|
||||
frappe.throw(__('Invalid "depends_on" expression'));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -554,7 +554,7 @@ frappe.ui.form.MultiSelectDialog = class MultiSelectDialog {
|
|||
}
|
||||
} else {
|
||||
Object.keys(this.setters).forEach(function (setter) {
|
||||
var value = me.dialog.fields_dict[setter].get_value();
|
||||
var value = me.dialog.fields_dict[setter].get_value() || me.setters[setter];
|
||||
if (me.dialog.fields_dict[setter].df.fieldtype == "Data" && value) {
|
||||
filters[setter] = ["like", "%" + value + "%"];
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -198,36 +198,34 @@ frappe.ui.form.check_mandatory = function (frm) {
|
|||
return !has_errors;
|
||||
|
||||
function is_docfield_mandatory(doc, df) {
|
||||
if (df.reqd) return true;
|
||||
if (!df.mandatory_depends_on || !doc) return;
|
||||
if (df.mandatory_depends_on && doc) {
|
||||
let out = null;
|
||||
let expression = df.mandatory_depends_on;
|
||||
let parent = frm.doc;
|
||||
|
||||
let out = null;
|
||||
let expression = df.mandatory_depends_on;
|
||||
let parent = frappe.get_meta(df.parent);
|
||||
|
||||
if (typeof expression === "boolean") {
|
||||
out = expression;
|
||||
} else if (typeof expression === "function") {
|
||||
out = expression(doc);
|
||||
} else if (expression.substr(0, 5) == "eval:") {
|
||||
try {
|
||||
out = frappe.utils.eval(expression.substr(5), { doc, parent });
|
||||
if (parent && parent.istable && expression.includes("is_submittable")) {
|
||||
out = true;
|
||||
if (typeof expression === "boolean") {
|
||||
out = expression;
|
||||
} else if (typeof expression === "function") {
|
||||
out = expression(doc);
|
||||
} else if (expression.substr(0, 5) == "eval:") {
|
||||
try {
|
||||
out = frappe.utils.eval(expression.substr(5), { doc, parent });
|
||||
} catch (e) {
|
||||
frappe.throw(__('Invalid "mandatory_depends_on" expression'));
|
||||
}
|
||||
} catch (e) {
|
||||
frappe.throw(__('Invalid "mandatory_depends_on" expression'));
|
||||
}
|
||||
} else {
|
||||
var value = doc[expression];
|
||||
if ($.isArray(value)) {
|
||||
out = !!value.length;
|
||||
} else {
|
||||
out = !!value;
|
||||
var value = doc[expression];
|
||||
if ($.isArray(value)) {
|
||||
out = !!value.length;
|
||||
} else {
|
||||
out = !!value;
|
||||
}
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
return out;
|
||||
return !!df.reqd;
|
||||
}
|
||||
|
||||
function scroll_to(fieldname) {
|
||||
|
|
|
|||
|
|
@ -35,7 +35,6 @@ class Picker {
|
|||
}
|
||||
}
|
||||
setup_emojis() {
|
||||
console.log("Making emojis");
|
||||
// setup tab
|
||||
this.setup_tab();
|
||||
// setup emoji container
|
||||
|
|
|
|||
|
|
@ -1318,7 +1318,10 @@ class FilterArea {
|
|||
field.set_value(value.replace(/^%+|%+$/g, ""));
|
||||
}
|
||||
|
||||
this.debounced_refresh_list_view();
|
||||
// Only trigger refresh if field has a value
|
||||
if (value) {
|
||||
this.debounced_refresh_list_view();
|
||||
}
|
||||
});
|
||||
}, 100);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -391,12 +391,14 @@ export default class BulkOperations {
|
|||
show_help_text();
|
||||
|
||||
function set_value_field(dialogObj) {
|
||||
const new_df = Object.assign({}, field_mappings[dialogObj.get_value("field")]);
|
||||
const field_value = dialogObj.get_value("field");
|
||||
if (!field_value || !field_mappings[field_value]) return;
|
||||
const new_df = Object.assign({}, field_mappings[field_value]);
|
||||
/* if the field label has status in it and
|
||||
if it has select fieldtype with no default value then
|
||||
set a default value from the available option. */
|
||||
if (
|
||||
new_df.label.match(status_regex) &&
|
||||
new_df.label?.match(status_regex) &&
|
||||
new_df.fieldtype === "Select" &&
|
||||
!new_df.default
|
||||
) {
|
||||
|
|
|
|||
|
|
@ -182,18 +182,12 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList {
|
|||
const match_rules_list = frappe.perm.get_match_rules(this.doctype);
|
||||
if (match_rules_list.length) {
|
||||
this.restricted_list = $(
|
||||
`<button class="btn btn-xs restricted-button flex align-center ${
|
||||
frappe.is_mobile() ? "ml-2" : ""
|
||||
}">
|
||||
`<button class="btn btn-xs restricted-button flex align-center">
|
||||
${frappe.utils.icon("restriction", "xs")}
|
||||
</button>`
|
||||
)
|
||||
.click(() => this.show_restrictions(match_rules_list))
|
||||
.appendTo(
|
||||
frappe.is_mobile()
|
||||
? this.page.page_form.find(".filter-section")
|
||||
: this.page.page_form
|
||||
);
|
||||
.appendTo(this.page.page_form.find(".filter-section"));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1098,7 +1092,7 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList {
|
|||
}
|
||||
}
|
||||
|
||||
get_tags_html(user_tags, limit, colored = false) {
|
||||
get_tags_html(user_tags, limit = null, colored = false) {
|
||||
let get_tag_html = (tag) => {
|
||||
let color = "",
|
||||
style = "";
|
||||
|
|
@ -1111,11 +1105,12 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList {
|
|||
return `<div class="tag-pill ellipsis" title="${tag}" style="${style}">${tag}</div>`;
|
||||
}
|
||||
};
|
||||
return user_tags
|
||||
.split(",")
|
||||
.slice(1, limit + 1)
|
||||
.map(get_tag_html)
|
||||
.join("");
|
||||
user_tags = (user_tags || "").split(",");
|
||||
if (limit !== null) {
|
||||
// if there is a limit apply it
|
||||
user_tags = user_tags.slice(0, limit);
|
||||
}
|
||||
return user_tags.map(get_tag_html).join("");
|
||||
}
|
||||
|
||||
get_meta_html(doc) {
|
||||
|
|
|
|||
|
|
@ -52,12 +52,11 @@ frappe.ui.menu = class ContextMenu {
|
|||
function () {
|
||||
return true;
|
||||
};
|
||||
console.log(typeof item.condition);
|
||||
let render = false;
|
||||
if (typeof item.condition == "function") {
|
||||
render = item.condition();
|
||||
} else {
|
||||
render = frappe.utils.eval_expression(item.condition);
|
||||
render = frappe.utils.eval(item.condition);
|
||||
}
|
||||
if (render) {
|
||||
this.add_menu_item(item);
|
||||
|
|
|
|||
|
|
@ -53,8 +53,8 @@
|
|||
</a>
|
||||
<div class="nav-item dropdown dropdown-navbar-user dropdown-mobile mt-3">
|
||||
<a
|
||||
class="align-center btn-reset flex nav-link"
|
||||
style="width: 100%; height: 40px;"
|
||||
class="align-center btn-reset flex nav-link sidebar-user-button"
|
||||
style="width: 100%; min-height: 40px;"
|
||||
onclick="return frappe.ui.toolbar.route_to_user()"
|
||||
aria-label="{{ __("User Menu") }}"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -65,6 +65,16 @@ frappe.ui.Sidebar = class Sidebar {
|
|||
frappe.current_app = app;
|
||||
this.app_logo_url = app.app_logo_url;
|
||||
return;
|
||||
} else {
|
||||
let app_name = frappe.boot.module_app[this.workspace_title];
|
||||
if (app_name) {
|
||||
let app_title = frappe.boot.app_data.find((f) => {
|
||||
return f.app_name == app_name;
|
||||
}).app_title;
|
||||
this.header_subtitle = app_title;
|
||||
} else {
|
||||
this.header_subtitle = frappe.session.user;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -24,6 +24,17 @@ frappe.ui.SidebarHeader = class SidebarHeader {
|
|||
},
|
||||
items: this.sibling_workspaces,
|
||||
},
|
||||
{
|
||||
name: "website",
|
||||
label: __("Website"),
|
||||
icon: "web",
|
||||
onClick: function () {
|
||||
window.open(window.location.origin);
|
||||
},
|
||||
},
|
||||
{
|
||||
is_divider: true,
|
||||
},
|
||||
{
|
||||
name: "edit-sidebar",
|
||||
label: __("Edit Sidebar"),
|
||||
|
|
@ -36,11 +47,9 @@ frappe.ui.SidebarHeader = class SidebarHeader {
|
|||
},
|
||||
},
|
||||
{
|
||||
name: "website",
|
||||
label: __("Website"),
|
||||
icon: "web",
|
||||
onClick: function () {
|
||||
window.open(window.location.origin);
|
||||
is_divider: true,
|
||||
condition: function () {
|
||||
return frappe.boot.developer_mode;
|
||||
},
|
||||
},
|
||||
];
|
||||
|
|
@ -77,6 +86,9 @@ frappe.ui.SidebarHeader = class SidebarHeader {
|
|||
icon: "info",
|
||||
items: this.get_help_siblings(),
|
||||
},
|
||||
{
|
||||
is_divider: true,
|
||||
},
|
||||
{
|
||||
name: "logout",
|
||||
label: "Logout",
|
||||
|
|
|
|||
|
|
@ -18,6 +18,12 @@ frappe.ui.TagEditor = class TagEditor {
|
|||
this.initialized = true;
|
||||
this.refresh(this.user_tags);
|
||||
}
|
||||
update_user_tags(tags_string) {
|
||||
this.user_tags = tags_string;
|
||||
frappe.model.set_value(this.frm.doctype, this.frm.docname, "_user_tags", this.user_tags);
|
||||
this.on_change && this.on_change(this.user_tags);
|
||||
frappe.tags.utils.fetch_tags();
|
||||
}
|
||||
setup_tags() {
|
||||
var me = this;
|
||||
|
||||
|
|
@ -40,9 +46,7 @@ frappe.ui.TagEditor = class TagEditor {
|
|||
callback: function (r) {
|
||||
var user_tags = me.user_tags ? me.user_tags.split(",") : [];
|
||||
user_tags.push(tag);
|
||||
me.user_tags = user_tags.join(",");
|
||||
me.on_change && me.on_change(me.user_tags);
|
||||
frappe.tags.utils.fetch_tags();
|
||||
me.update_user_tags(user_tags.join(","));
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
@ -55,9 +59,7 @@ frappe.ui.TagEditor = class TagEditor {
|
|||
callback: function (r) {
|
||||
var user_tags = me.user_tags.split(",");
|
||||
user_tags.splice(user_tags.indexOf(tag), 1);
|
||||
me.user_tags = user_tags.join(",");
|
||||
me.on_change && me.on_change(me.user_tags);
|
||||
frappe.tags.utils.fetch_tags();
|
||||
me.update_user_tags(user_tags.join(","));
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -85,9 +85,6 @@ function resetAll(skips) {
|
|||
}
|
||||
|
||||
function handleAction(step) {
|
||||
if (step.is_complete) return;
|
||||
if (step.is_skipped) return;
|
||||
|
||||
if (step.route_options && typeof step.route_options === "string") {
|
||||
frappe.route_options = JSON.parse(step.route_options);
|
||||
}
|
||||
|
|
@ -284,7 +281,7 @@ function markReset(step) {
|
|||
style="align-items: center"
|
||||
:class="
|
||||
step.is_complete
|
||||
? 'text-extra-muted onb-cursor-disabled'
|
||||
? 'text-extra-muted onb-select-cursor'
|
||||
: 'text-ink-gray-8 onb-select-cursor'
|
||||
"
|
||||
>
|
||||
|
|
@ -316,15 +313,14 @@ function markReset(step) {
|
|||
|
||||
<div v-if="!step.is_complete">
|
||||
<div v-if="!step.is_skipped">
|
||||
<div
|
||||
class="ml-auto text-base onb-show-on-hover text-sm w-12 text-right text-ink-gray-8"
|
||||
>
|
||||
<div class="ml-auto onb-show-on-hover text-sm w-12 text-right">
|
||||
<span
|
||||
style="
|
||||
font-size: 12px;
|
||||
vertical-align: text-top;
|
||||
margin-right: 10px;
|
||||
margin-right: 0px;
|
||||
"
|
||||
class="text-ink-gray-7"
|
||||
@click="markSkip(step)"
|
||||
>
|
||||
{{ __("Skip") }}
|
||||
|
|
@ -332,15 +328,14 @@ function markReset(step) {
|
|||
</div>
|
||||
</div>
|
||||
<div v-if="step.is_skipped">
|
||||
<div
|
||||
class="ml-auto text-base onb-show-on-hover text-sm w-12 text-right text-ink-gray-8"
|
||||
>
|
||||
<div class="ml-auto onb-show-on-hover text-sm w-12 text-right">
|
||||
<span
|
||||
style="
|
||||
font-size: 12px;
|
||||
vertical-align: text-top;
|
||||
margin-right: 10px;
|
||||
margin-right: 0px;
|
||||
"
|
||||
class="text-ink-gray-7"
|
||||
@click="markReset(step)"
|
||||
>
|
||||
{{ __("Reset") }}
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ class UserOnboarding {
|
|||
title: title,
|
||||
steps: steps.value,
|
||||
minimizeIcon: frappe.utils.icon("minimize-2", "sm"),
|
||||
closeIcon: frappe.utils.icon("close", "sm"),
|
||||
closeIcon: frappe.utils.icon("x", "sm"),
|
||||
headerIcon: header_icon,
|
||||
checklistIcon: frappe.utils.icon("circle-check", "sm"),
|
||||
completeChecklistIcon: frappe.utils.icon(
|
||||
|
|
@ -59,18 +59,25 @@ class UserOnboarding {
|
|||
function addStyles() {
|
||||
if (document.getElementById("user-onboarding-styles")) return;
|
||||
|
||||
const main_section = document.getElementsByClassName("main-section");
|
||||
|
||||
if (main_section) {
|
||||
main_section[0].style.paddingBottom = "90px";
|
||||
}
|
||||
|
||||
const style = document.createElement("style");
|
||||
style.id = "user-onboarding-styles";
|
||||
|
||||
style.innerHTML = `
|
||||
|
||||
.onb-panel {
|
||||
position: fixed;
|
||||
right: 24px;
|
||||
left: 236px;
|
||||
bottom: 24px;
|
||||
width: 310px;
|
||||
max-height: 80vh;
|
||||
background: #fff;
|
||||
border-radius: 16px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 12px 40px rgba(0,0,0,0.15);
|
||||
padding: 16px;
|
||||
z-index: 9999;
|
||||
|
|
@ -82,7 +89,6 @@ function addStyles() {
|
|||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.onb-header-actions button {
|
||||
|
|
@ -135,21 +141,21 @@ function addStyles() {
|
|||
|
||||
.onb-steps {
|
||||
margin-top: 16px;
|
||||
padding: 0;
|
||||
padding: 0px;
|
||||
list-style: none;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
gap: 4px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.onb-group:hover {
|
||||
color: #111827;
|
||||
background: #f5f5f5;
|
||||
.onb-group {
|
||||
padding: 4px 8px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.onb-cursor-disabled {
|
||||
cursor: not-allowed;
|
||||
.onb-group:hover {
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
.onb-select-cursor {
|
||||
|
|
@ -240,7 +246,7 @@ function addStyles() {
|
|||
}
|
||||
|
||||
[data-theme="dark"] .onb-group:hover {
|
||||
background: #374151;
|
||||
background: #1C1C1C;
|
||||
color: #f3f4f6;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -233,7 +233,7 @@ frappe.breadcrumbs = {
|
|||
is_new_doc = true;
|
||||
} else {
|
||||
let title = frappe.model.get_doc_title(doc);
|
||||
docname_title = title || doc.name;
|
||||
docname_title = __(title) || __(doc.name);
|
||||
if (frappe.utils.is_html(docname_title)) {
|
||||
docname_title = strip_html(docname_title);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -896,6 +896,8 @@ frappe.views.CommunicationComposer = class {
|
|||
if (!r.exc) {
|
||||
frappe.utils.play_sound("email");
|
||||
|
||||
const communication_name = r.message["name"];
|
||||
|
||||
if (r.message["emails_not_sent_to"]) {
|
||||
frappe.msgprint(
|
||||
__("Email not sent to {0} (unsubscribed / disabled)", [
|
||||
|
|
@ -910,6 +912,54 @@ frappe.views.CommunicationComposer = class {
|
|||
me.frm.reload_doc();
|
||||
}
|
||||
|
||||
let undo_alert = frappe.show_alert(
|
||||
{
|
||||
message: `<span>${__(
|
||||
"Email Sent"
|
||||
)}</span><span class="cursor-pointer ml-4" data-action="undo" style="font-weight: 500; text-decoration: underline;">${__(
|
||||
"Undo"
|
||||
)}</span>`,
|
||||
indicator: "green",
|
||||
},
|
||||
10,
|
||||
{
|
||||
undo: () => {
|
||||
if (undo_alert) {
|
||||
undo_alert.find(".close").click();
|
||||
}
|
||||
frappe
|
||||
.xcall(
|
||||
"frappe.core.doctype.communication.email.undo_email_send",
|
||||
{ communication_name: communication_name }
|
||||
)
|
||||
.then((d) => {
|
||||
if (me.frm) {
|
||||
me.frm.reload_doc();
|
||||
}
|
||||
|
||||
// Reopen the composer with the recovered data
|
||||
new frappe.views.CommunicationComposer({
|
||||
doc: d.doc,
|
||||
subject: d.subject,
|
||||
recipients: d.recipients,
|
||||
cc: d.cc,
|
||||
bcc: d.bcc,
|
||||
message: d.content,
|
||||
sender: d.sender,
|
||||
read_receipt: d.send_read_receipt,
|
||||
attachments: d.attachments,
|
||||
frm: me.frm,
|
||||
});
|
||||
|
||||
frappe.show_alert({
|
||||
message: __("Email sending undone"),
|
||||
indicator: "blue",
|
||||
});
|
||||
});
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
// try the success callback if it exists
|
||||
if (me.success) {
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -589,8 +589,6 @@ frappe.provide("frappe.views");
|
|||
group: "cards",
|
||||
animation: 150,
|
||||
dataIdAttr: "data-name",
|
||||
forceFallback: true,
|
||||
fallbackTolerance: 20,
|
||||
onStart: function () {
|
||||
wrapper.find(".kanban-card.add-card").fadeOut(200, function () {
|
||||
wrapper.find(".kanban-cards").height("100vh");
|
||||
|
|
@ -599,7 +597,6 @@ frappe.provide("frappe.views");
|
|||
onEnd: function (e) {
|
||||
wrapper.find(".kanban-card.add-card").fadeIn(100);
|
||||
wrapper.find(".kanban-cards").height("auto");
|
||||
// update order
|
||||
const args = {
|
||||
name: decodeURIComponent($(e.item).attr("data-name")),
|
||||
from_colname: $(e.from)
|
||||
|
|
@ -611,7 +608,6 @@ frappe.provide("frappe.views");
|
|||
};
|
||||
store.dispatch("update_order_for_single_card", args);
|
||||
},
|
||||
onAdd: function () {},
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -756,11 +752,26 @@ frappe.provide("frappe.views");
|
|||
}
|
||||
|
||||
function get_tags_html(card) {
|
||||
return card.tags
|
||||
? `<div class="kanban-tags">
|
||||
${cur_list.get_tags_html(card.tags, 3, true)}
|
||||
</div>`
|
||||
: "";
|
||||
if (!card.tags) return "";
|
||||
const tags_array = card.tags.split(",");
|
||||
const limit = 3; // cap. at 3 tags
|
||||
const visible_tags = tags_array.slice(0, limit).join(",");
|
||||
const hidden_tags = tags_array.slice(limit).join(",");
|
||||
const hidden_tags_html = cur_list.get_tags_html(hidden_tags, null, true);
|
||||
const hidden_count = tags_array.length - limit;
|
||||
let html = `<div class="kanban-tags">
|
||||
${cur_list.get_tags_html(visible_tags, null, true)}`;
|
||||
|
||||
if (hidden_count > 0) {
|
||||
html += `
|
||||
<span class="tag-pill more-tags">
|
||||
+${hidden_count}
|
||||
<span class="hidden-tags">${hidden_tags_html}</span>
|
||||
</span>`;
|
||||
}
|
||||
|
||||
html += `</div>`;
|
||||
return html;
|
||||
}
|
||||
|
||||
function render_card_meta() {
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue