diff --git a/cypress/integration/form.js b/cypress/integration/form.js
index 5302ed0964..20ed7a61cd 100644
--- a/cypress/integration/form.js
+++ b/cypress/integration/form.js
@@ -8,7 +8,7 @@ context('Form', () => {
});
it('create a new form', () => {
cy.visit('/app/todo/new');
- cy.fill_field('description', 'this is a test todo', 'Text Editor').blur();
+ cy.fill_field('description', 'this is a test todo', 'Text Editor');
cy.wait(300);
cy.get('.page-title').should('contain', 'Not Saved');
cy.intercept({
diff --git a/cypress/integration/relative_time_filters.js b/cypress/integration/relative_time_filters.js
index 80e6387d99..cbb0524c24 100644
--- a/cypress/integration/relative_time_filters.js
+++ b/cypress/integration/relative_time_filters.js
@@ -1,7 +1,4 @@
context('Relative Timeframe', () => {
- beforeEach(() => {
- cy.login();
- });
before(() => {
cy.login();
cy.visit('/app/website');
diff --git a/cypress/integration/table_multiselect.js b/cypress/integration/table_multiselect.js
index faa72d63a5..25cab78ba2 100644
--- a/cypress/integration/table_multiselect.js
+++ b/cypress/integration/table_multiselect.js
@@ -1,5 +1,5 @@
context('Table MultiSelect', () => {
- beforeEach(() => {
+ before(() => {
cy.login();
});
diff --git a/frappe/__init__.py b/frappe/__init__.py
index 4e7017d8fe..cab9b0da76 100644
--- a/frappe/__init__.py
+++ b/frappe/__init__.py
@@ -15,6 +15,7 @@ from __future__ import unicode_literals, print_function
from six import iteritems, binary_type, text_type, string_types, PY2
from werkzeug.local import Local, release_local
import os, sys, importlib, inspect, json
+import typing
from past.builtins import cmp
import click
@@ -134,6 +135,14 @@ message_log = local("message_log")
lang = local("lang")
+# This if block is never executed when running the code. It is only used for
+# telling static code analyzer where to find dynamically defined attributes.
+if typing.TYPE_CHECKING:
+ from frappe.database.mariadb.database import MariaDBDatabase
+ from frappe.database.postgres.database import PostgresDatabase
+ db: typing.Union[MariaDBDatabase, PostgresDatabase]
+# end: static analysis hack
+
def init(site, sites_path=None, new_site=False):
"""Initialize frappe for the current site. Reset thread locals `frappe.local`"""
if getattr(local, "initialised", None):
diff --git a/frappe/boot.py b/frappe/boot.py
index 0dfcb8d1b4..65a07b15e5 100644
--- a/frappe/boot.py
+++ b/frappe/boot.py
@@ -42,6 +42,8 @@ def get_bootinfo():
bootinfo.user_info = get_user_info()
bootinfo.sid = frappe.session['sid']
+ bootinfo.user_groups = frappe.get_all('User Group', pluck="name")
+
bootinfo.modules = {}
bootinfo.module_list = []
load_desktop_data(bootinfo)
diff --git a/frappe/commands/utils.py b/frappe/commands/utils.py
index 5ff66171fc..a203c8c6d9 100644
--- a/frappe/commands/utils.py
+++ b/frappe/commands/utils.py
@@ -11,7 +11,7 @@ import click
import frappe
from frappe.commands import get_site, pass_context
from frappe.exceptions import SiteNotSpecifiedError
-from frappe.utils import get_bench_path, update_progress_bar
+from frappe.utils import get_bench_path, update_progress_bar, cint
@click.command('build')
@@ -567,11 +567,14 @@ def run_ui_tests(context, app, headless=False):
node_bin = subprocess.getoutput("npm bin")
cypress_path = "{0}/cypress".format(node_bin)
- plugin_path = "{0}/cypress-file-upload".format(node_bin)
+ plugin_path = "{0}/../cypress-file-upload".format(node_bin)
# check if cypress in path...if not, install it.
- if not (os.path.exists(cypress_path) or os.path.exists(plugin_path)) \
- or not subprocess.getoutput("npm view cypress version").startswith("6."):
+ if not (
+ os.path.exists(cypress_path)
+ and os.path.exists(plugin_path)
+ and cint(subprocess.getoutput("npm view cypress version")[:1]) >= 6
+ ):
# install cypress
click.secho("Installing Cypress...", fg="yellow")
frappe.commands.popen("yarn add cypress@^6 cypress-file-upload@^5 --no-lockfile")
diff --git a/frappe/core/doctype/communication/communication_list.js b/frappe/core/doctype/communication/communication_list.js
index 454897b865..315b74a39c 100644
--- a/frappe/core/doctype/communication/communication_list.js
+++ b/frappe/core/doctype/communication/communication_list.js
@@ -20,6 +20,6 @@ frappe.listview_settings['Communication'] = {
},
primary_action: function() {
- new frappe.views.CommunicationComposer({ doc: {} });
+ new frappe.views.CommunicationComposer();
}
};
diff --git a/frappe/core/doctype/data_import/data_import.json b/frappe/core/doctype/data_import/data_import.json
index 8b1b6c4e07..fe6fb90481 100644
--- a/frappe/core/doctype/data_import/data_import.json
+++ b/frappe/core/doctype/data_import/data_import.json
@@ -53,7 +53,8 @@
"fieldname": "import_file",
"fieldtype": "Attach",
"in_list_view": 1,
- "label": "Import File"
+ "label": "Import File",
+ "read_only_depends_on": "eval: ['Success', 'Partial Success'].includes(doc.status)"
},
{
"fieldname": "import_preview",
@@ -156,10 +157,11 @@
"description": "Must be a publicly accessible Google Sheets URL",
"fieldname": "google_sheets_url",
"fieldtype": "Data",
- "label": "Import from Google Sheets"
+ "label": "Import from Google Sheets",
+ "read_only_depends_on": "eval: ['Success', 'Partial Success'].includes(doc.status)"
},
{
- "depends_on": "eval:doc.google_sheets_url && !doc.__unsaved",
+ "depends_on": "eval:doc.google_sheets_url && !doc.__unsaved && ['Success', 'Partial Success'].includes(doc.status)",
"fieldname": "refresh_google_sheet",
"fieldtype": "Button",
"label": "Refresh Google Sheet"
@@ -167,7 +169,7 @@
],
"hide_toolbar": 1,
"links": [],
- "modified": "2020-06-24 14:33:03.173876",
+ "modified": "2021-04-11 01:50:42.074623",
"modified_by": "Administrator",
"module": "Core",
"name": "Data Import",
diff --git a/frappe/core/doctype/doctype/doctype.json b/frappe/core/doctype/doctype/doctype.json
index 276ce7bee7..fe5038b841 100644
--- a/frappe/core/doctype/doctype/doctype.json
+++ b/frappe/core/doctype/doctype/doctype.json
@@ -56,6 +56,8 @@
"show_preview_popup",
"show_name_in_global_search",
"email_settings_sb",
+ "default_email_template",
+ "column_break_51",
"email_append_to",
"sender_field",
"subject_field",
@@ -535,6 +537,16 @@
"fieldname": "is_virtual",
"fieldtype": "Check",
"label": "Is Virtual"
+ },
+ {
+ "fieldname": "default_email_template",
+ "fieldtype": "Link",
+ "label": "Default Email Template",
+ "options": "Email Template"
+ },
+ {
+ "fieldname": "column_break_51",
+ "fieldtype": "Column Break"
}
],
"icon": "fa fa-bolt",
@@ -616,7 +628,7 @@
"link_fieldname": "reference_doctype"
}
],
- "modified": "2021-02-17 20:18:06.212232",
+ "modified": "2021-04-16 12:26:41.031135",
"modified_by": "Administrator",
"module": "Core",
"name": "DocType",
diff --git a/frappe/core/doctype/prepared_report/prepared_report.py b/frappe/core/doctype/prepared_report/prepared_report.py
index e947cee8ed..c27853f460 100644
--- a/frappe/core/doctype/prepared_report/prepared_report.py
+++ b/frappe/core/doctype/prepared_report/prepared_report.py
@@ -37,7 +37,10 @@ def run_background(prepared_report):
custom_report_doc = report
reference_report = custom_report_doc.reference_report
report = frappe.get_doc("Report", reference_report)
- report.custom_columns = custom_report_doc.json
+ if custom_report_doc.json:
+ data = json.loads(custom_report_doc.json)
+ if data:
+ report.custom_columns = data["columns"]
result = generate_report_result(
report=report,
diff --git a/frappe/core/doctype/user/test_user.py b/frappe/core/doctype/user/test_user.py
index 5b16c72775..5bea767934 100644
--- a/frappe/core/doctype/user/test_user.py
+++ b/frappe/core/doctype/user/test_user.py
@@ -229,6 +229,28 @@ class TestUser(unittest.TestCase):
self.assertEqual(extract_mentions(comment)[0], "test_user@example.com")
self.assertEqual(extract_mentions(comment)[1], "test.again@example1.com")
+ doc = frappe.get_doc({
+ 'doctype': 'User Group',
+ 'name': 'Team',
+ 'user_group_members': [{
+ 'user': 'test@example.com'
+ }, {
+ 'user': 'test1@example.com'
+ }]
+ })
+ doc.insert(ignore_if_duplicate=True)
+
+ comment = '''
+
+ Testing comment for
+
+ @Team
+
+ please check
+
+ '''
+ self.assertListEqual(extract_mentions(comment), ['test@example.com', 'test1@example.com'])
+
def test_rate_limiting_for_reset_password(self):
# Allow only one reset request for a day
frappe.db.set_value("System Settings", "System Settings", "password_reset_limit", 1)
diff --git a/frappe/core/doctype/user/user.py b/frappe/core/doctype/user/user.py
index 04d087e82a..0462de8643 100644
--- a/frappe/core/doctype/user/user.py
+++ b/frappe/core/doctype/user/user.py
@@ -1018,8 +1018,16 @@ def extract_mentions(txt):
soup = BeautifulSoup(txt, 'html.parser')
emails = []
for mention in soup.find_all(class_='mention'):
+ if mention.get('data-is-group') == 'true':
+ try:
+ user_group = frappe.get_cached_doc('User Group', mention['data-id'])
+ emails += [d.user for d in user_group.user_group_members]
+ except frappe.DoesNotExistError:
+ pass
+ continue
email = mention['data-id']
emails.append(email)
+
return emails
def handle_password_test_fail(result):
diff --git a/frappe/core/doctype/user_group/__init__.py b/frappe/core/doctype/user_group/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/frappe/core/doctype/user_group/test_user_group.py b/frappe/core/doctype/user_group/test_user_group.py
new file mode 100644
index 0000000000..c7e28f3d31
--- /dev/null
+++ b/frappe/core/doctype/user_group/test_user_group.py
@@ -0,0 +1,10 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2021, Frappe Technologies and Contributors
+# See license.txt
+from __future__ import unicode_literals
+
+# import frappe
+import unittest
+
+class TestUserGroup(unittest.TestCase):
+ pass
diff --git a/frappe/core/doctype/user_group/user_group.js b/frappe/core/doctype/user_group/user_group.js
new file mode 100644
index 0000000000..2aa9b68658
--- /dev/null
+++ b/frappe/core/doctype/user_group/user_group.js
@@ -0,0 +1,8 @@
+// Copyright (c) 2021, Frappe Technologies and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on('User Group', {
+ // refresh: function(frm) {
+
+ // }
+});
diff --git a/frappe/core/doctype/user_group/user_group.json b/frappe/core/doctype/user_group/user_group.json
new file mode 100644
index 0000000000..e807372061
--- /dev/null
+++ b/frappe/core/doctype/user_group/user_group.json
@@ -0,0 +1,48 @@
+{
+ "actions": [],
+ "autoname": "Prompt",
+ "creation": "2021-04-12 15:17:24.751710",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "user_group_members"
+ ],
+ "fields": [
+ {
+ "fieldname": "user_group_members",
+ "fieldtype": "Table MultiSelect",
+ "label": "User Group Members",
+ "options": "User Group Member",
+ "reqd": 1
+ }
+ ],
+ "index_web_pages_for_search": 1,
+ "links": [],
+ "modified": "2021-04-15 16:12:31.455401",
+ "modified_by": "Administrator",
+ "module": "Core",
+ "name": "User Group",
+ "owner": "Administrator",
+ "permissions": [
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "System Manager",
+ "share": 1,
+ "write": 1
+ },
+ {
+ "read": 1,
+ "role": "All"
+ }
+ ],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/frappe/core/doctype/user_group/user_group.py b/frappe/core/doctype/user_group/user_group.py
new file mode 100644
index 0000000000..64bffa06d0
--- /dev/null
+++ b/frappe/core/doctype/user_group/user_group.py
@@ -0,0 +1,15 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2021, Frappe Technologies and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+# import frappe
+from frappe.model.document import Document
+import frappe
+
+class UserGroup(Document):
+ def after_insert(self):
+ frappe.publish_realtime('user_group_added', self.name)
+
+ def on_trash(self):
+ frappe.publish_realtime('user_group_deleted', self.name)
diff --git a/frappe/core/doctype/user_group_member/__init__.py b/frappe/core/doctype/user_group_member/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/frappe/core/doctype/user_group_member/test_user_group_member.py b/frappe/core/doctype/user_group_member/test_user_group_member.py
new file mode 100644
index 0000000000..38aade4608
--- /dev/null
+++ b/frappe/core/doctype/user_group_member/test_user_group_member.py
@@ -0,0 +1,10 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2021, Frappe Technologies and Contributors
+# See license.txt
+from __future__ import unicode_literals
+
+# import frappe
+import unittest
+
+class TestUserGroupMember(unittest.TestCase):
+ pass
diff --git a/frappe/core/doctype/user_group_member/user_group_member.js b/frappe/core/doctype/user_group_member/user_group_member.js
new file mode 100644
index 0000000000..0b2dbe0d46
--- /dev/null
+++ b/frappe/core/doctype/user_group_member/user_group_member.js
@@ -0,0 +1,8 @@
+// Copyright (c) 2021, Frappe Technologies and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on('User Group Member', {
+ // refresh: function(frm) {
+
+ // }
+});
diff --git a/frappe/core/doctype/user_group_member/user_group_member.json b/frappe/core/doctype/user_group_member/user_group_member.json
new file mode 100644
index 0000000000..d2ff149366
--- /dev/null
+++ b/frappe/core/doctype/user_group_member/user_group_member.json
@@ -0,0 +1,32 @@
+{
+ "actions": [],
+ "creation": "2021-04-12 15:16:29.279107",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "user"
+ ],
+ "fields": [
+ {
+ "fieldname": "user",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "User",
+ "options": "User",
+ "reqd": 1
+ }
+ ],
+ "index_web_pages_for_search": 1,
+ "istable": 1,
+ "links": [],
+ "modified": "2021-04-12 15:17:18.773046",
+ "modified_by": "Administrator",
+ "module": "Core",
+ "name": "User Group Member",
+ "owner": "Administrator",
+ "permissions": [],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/frappe/core/doctype/user_group_member/user_group_member.py b/frappe/core/doctype/user_group_member/user_group_member.py
new file mode 100644
index 0000000000..4d0656913d
--- /dev/null
+++ b/frappe/core/doctype/user_group_member/user_group_member.py
@@ -0,0 +1,10 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2021, Frappe Technologies and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+# import frappe
+from frappe.model.document import Document
+
+class UserGroupMember(Document):
+ pass
diff --git a/frappe/custom/doctype/custom_field/custom_field.py b/frappe/custom/doctype/custom_field/custom_field.py
index ee6e3b9c61..3126326636 100644
--- a/frappe/custom/doctype/custom_field/custom_field.py
+++ b/frappe/custom/doctype/custom_field/custom_field.py
@@ -40,6 +40,8 @@ class CustomField(Document):
frappe.throw(_("A field with the name '{}' already exists in doctype {}.").format(self.fieldname, self.dt))
def validate(self):
+ from frappe.custom.doctype.customize_form.customize_form import CustomizeForm
+
meta = frappe.get_meta(self.dt, cached=False)
fieldnames = [df.fieldname for df in meta.get("fields")]
@@ -49,7 +51,11 @@ class CustomField(Document):
if self.insert_after and self.insert_after in fieldnames:
self.idx = fieldnames.index(self.insert_after) + 1
- self._old_fieldtype = self.db_get('fieldtype')
+ old_fieldtype = self.db_get('fieldtype')
+ is_fieldtype_changed = (not self.is_new()) and (old_fieldtype != self.fieldtype)
+
+ if is_fieldtype_changed and not CustomizeForm.allow_fieldtype_change(old_fieldtype, self.fieldtype):
+ frappe.throw(_("Fieldtype cannot be changed from {0} to {1}").format(old_fieldtype, self.fieldtype))
if not self.fieldname:
frappe.throw(_("Fieldname not set for Custom Field"))
diff --git a/frappe/custom/doctype/customize_form/customize_form.json b/frappe/custom/doctype/customize_form/customize_form.json
index 77f62b3ec3..442b8dbb31 100644
--- a/frappe/custom/doctype/customize_form/customize_form.json
+++ b/frappe/custom/doctype/customize_form/customize_form.json
@@ -33,6 +33,8 @@
"show_preview_popup",
"image_view",
"email_settings_section",
+ "default_email_template",
+ "column_break_26",
"email_append_to",
"sender_field",
"subject_field",
@@ -264,6 +266,16 @@
"label": "Actions",
"options": "DocType Action"
},
+ {
+ "fieldname": "default_email_template",
+ "fieldtype": "Link",
+ "label": "Default Email Template",
+ "options": "Email Template"
+ },
+ {
+ "fieldname": "column_break_26",
+ "fieldtype": "Column Break"
+ },
{
"collapsible": 1,
"fieldname": "naming_section",
@@ -283,7 +295,7 @@
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
- "modified": "2021-02-16 15:22:11.108256",
+ "modified": "2021-03-22 12:27:15.462727",
"modified_by": "Administrator",
"module": "Custom",
"name": "Customize Form",
@@ -304,4 +316,4 @@
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
-}
\ No newline at end of file
+}
diff --git a/frappe/custom/doctype/customize_form/customize_form.py b/frappe/custom/doctype/customize_form/customize_form.py
index c79c965aae..be0dded99c 100644
--- a/frappe/custom/doctype/customize_form/customize_form.py
+++ b/frappe/custom/doctype/customize_form/customize_form.py
@@ -401,22 +401,18 @@ class CustomizeForm(Document):
return property_value
def validate_fieldtype_change(self, df, old_value, new_value):
- allowed = False
- self.check_length_for_fieldtypes = []
- for allowed_changes in ALLOWED_FIELDTYPE_CHANGE:
- if (old_value in allowed_changes and new_value in allowed_changes):
- allowed = True
- old_value_length = cint(frappe.db.type_map.get(old_value)[1])
- new_value_length = cint(frappe.db.type_map.get(new_value)[1])
+ allowed = self.allow_fieldtype_change(old_value, new_value)
+ if allowed:
+ old_value_length = cint(frappe.db.type_map.get(old_value)[1])
+ new_value_length = cint(frappe.db.type_map.get(new_value)[1])
- # Ignore fieldtype check validation if new field type has unspecified maxlength
- # Changes like DATA to TEXT, where new_value_lenth equals 0 will not be validated
- if new_value_length and (old_value_length > new_value_length):
- self.check_length_for_fieldtypes.append({'df': df, 'old_value': old_value})
- self.validate_fieldtype_length()
- else:
- self.flags.update_db = True
- break
+ # Ignore fieldtype check validation if new field type has unspecified maxlength
+ # Changes like DATA to TEXT, where new_value_lenth equals 0 will not be validated
+ if new_value_length and (old_value_length > new_value_length):
+ self.check_length_for_fieldtypes.append({'df': df, 'old_value': old_value})
+ self.validate_fieldtype_length()
+ else:
+ self.flags.update_db = True
if not allowed:
frappe.throw(_("Fieldtype cannot be changed from {0} to {1} in row {2}").format(old_value, new_value, df.idx))
@@ -458,6 +454,14 @@ class CustomizeForm(Document):
reset_customization(self.doc_type)
self.fetch_to_customize()
+ @classmethod
+ def allow_fieldtype_change(self, old_type: str, new_type: str) -> bool:
+ """ allow type change, if both old_type and new_type are in same field group.
+ field groups are defined in ALLOWED_FIELDTYPE_CHANGE variables.
+ """
+ in_field_group = lambda group: (old_type in group) and (new_type in group)
+ return any(map(in_field_group, ALLOWED_FIELDTYPE_CHANGE))
+
def reset_customization(doctype):
setters = frappe.get_all("Property Setter", filters={
'doc_type': doctype,
@@ -487,6 +491,7 @@ doctype_properties = {
'allow_auto_repeat': 'Check',
'allow_import': 'Check',
'show_preview_popup': 'Check',
+ 'default_email_template': 'Data',
'email_append_to': 'Check',
'subject_field': 'Data',
'sender_field': 'Data',
diff --git a/frappe/desk/desktop.py b/frappe/desk/desktop.py
index 5b6e2fdd21..d1b5e27a2f 100644
--- a/frappe/desk/desktop.py
+++ b/frappe/desk/desktop.py
@@ -63,7 +63,7 @@ class Workspace:
for section in cards:
links = loads(section.get('links')) if isinstance(section.get('links'), string_types) else section.get('links')
for item in links:
- if self.is_item_allowed(item.get('name'), item.get('type')):
+ if self.is_item_allowed(item.get('link_to'), item.get('link_type')):
return True
def _in_active_domains(item):
diff --git a/frappe/desk/doctype/notification_settings/notification_settings.js b/frappe/desk/doctype/notification_settings/notification_settings.js
index 88dc145be2..cc2fd95204 100644
--- a/frappe/desk/doctype/notification_settings/notification_settings.js
+++ b/frappe/desk/doctype/notification_settings/notification_settings.js
@@ -19,7 +19,7 @@ frappe.ui.form.on('Notification Settings', {
refresh: (frm) => {
if (frappe.user.has_role('System Manager')) {
- frm.add_custom_button('Go to Notification Settings List', () => {
+ frm.add_custom_button(__('Go to Notification Settings List'), () => {
frappe.set_route('List', 'Notification Settings');
});
}
diff --git a/frappe/desk/query_report.py b/frappe/desk/query_report.py
index 22d47d1120..9589507ca6 100644
--- a/frappe/desk/query_report.py
+++ b/frappe/desk/query_report.py
@@ -36,7 +36,10 @@ def get_report_doc(report_name):
reference_report = custom_report_doc.reference_report
doc = frappe.get_doc("Report", reference_report)
doc.custom_report = report_name
- doc.custom_columns = custom_report_doc.json
+ if custom_report_doc.json:
+ data = json.loads(custom_report_doc.json)
+ if data:
+ doc.custom_columns = data["columns"]
doc.is_custom_report = True
if not doc.is_permitted():
@@ -83,7 +86,7 @@ def generate_report_result(report, filters=None, user=None, custom_columns=None)
if report.custom_columns:
# saved columns (with custom columns / with different column order)
- columns = json.loads(report.custom_columns)
+ columns = report.custom_columns
# unsaved custom_columns
if custom_columns:
@@ -524,9 +527,12 @@ def save_report(reference_report, report_name, columns):
"report_type": "Custom Report",
},
)
+
if docname:
report = frappe.get_doc("Report", docname)
- report.update({"json": columns})
+ existing_jd = json.loads(report.json)
+ existing_jd["columns"] = json.loads(columns)
+ report.update({"json": json.dumps(existing_jd, separators=(',', ':'))})
report.save()
frappe.msgprint(_("Report updated successfully"))
@@ -536,7 +542,7 @@ def save_report(reference_report, report_name, columns):
{
"doctype": "Report",
"report_name": report_name,
- "json": columns,
+ "json": f'{{"columns":{columns}}}',
"ref_doctype": report_doc.ref_doctype,
"is_standard": "No",
"report_type": "Custom Report",
diff --git a/frappe/desk/treeview.py b/frappe/desk/treeview.py
index 12fdb0dadc..d479b71b52 100644
--- a/frappe/desk/treeview.py
+++ b/frappe/desk/treeview.py
@@ -66,7 +66,7 @@ def add_node():
doc.save()
def make_tree_args(**kwarg):
- del kwarg['cmd']
+ kwarg.pop('cmd', None)
doctype = kwarg['doctype']
parent_field = 'parent_' + doctype.lower().replace(' ', '_')
diff --git a/frappe/email/doctype/auto_email_report/auto_email_report.py b/frappe/email/doctype/auto_email_report/auto_email_report.py
index d82caa7bd4..6f1cd8eebd 100644
--- a/frappe/email/doctype/auto_email_report/auto_email_report.py
+++ b/frappe/email/doctype/auto_email_report/auto_email_report.py
@@ -252,7 +252,7 @@ def make_links(columns, data):
elif col.fieldtype == "Dynamic Link":
if col.options and row.get(col.fieldname) and row.get(col.options):
row[col.fieldname] = get_link_to_form(row[col.options], row[col.fieldname])
- elif col.fieldtype == "Currency":
+ elif col.fieldtype == "Currency" and row.get(col.fieldname):
row[col.fieldname] = frappe.format_value(row[col.fieldname], col)
return columns, data
diff --git a/frappe/email/test_smtp.py b/frappe/email/test_smtp.py
index 869d708430..0b11c559a2 100644
--- a/frappe/email/test_smtp.py
+++ b/frappe/email/test_smtp.py
@@ -2,7 +2,9 @@
# License: The MIT License
import unittest
+import frappe
from frappe.email.smtp import SMTPServer
+from frappe.email.smtp import get_outgoing_email_account
class TestSMTP(unittest.TestCase):
def test_smtp_ssl_session(self):
@@ -13,6 +15,57 @@ class TestSMTP(unittest.TestCase):
for port in [None, 0, 587, "587"]:
make_server(port, 0, 1)
+ def test_get_email_account(self):
+ existing_email_accounts = frappe.get_all("Email Account", fields = ["name", "enable_outgoing", "default_outgoing", "append_to"])
+ unset_details = {
+ "enable_outgoing": 0,
+ "default_outgoing": 0,
+ "append_to": None
+ }
+ for email_account in existing_email_accounts:
+ frappe.db.set_value('Email Account', email_account['name'], unset_details)
+
+ # remove mail_server config so that test@example.com is not created
+ mail_server = frappe.conf.get('mail_server')
+ del frappe.conf['mail_server']
+
+ frappe.local.outgoing_email_account = {}
+
+ frappe.local.outgoing_email_account = {}
+ # lowest preference given to email account with default incoming enabled
+ create_email_account(email_id="default_outgoing_enabled@gmail.com", password="***", enable_outgoing = 1, default_outgoing=1)
+ self.assertEqual(get_outgoing_email_account().email_id, "default_outgoing_enabled@gmail.com")
+
+ frappe.local.outgoing_email_account = {}
+ # highest preference given to email account with append_to matching
+ create_email_account(email_id="append_to@gmail.com", password="***", enable_outgoing = 1, default_outgoing=1, append_to="Blog Post")
+ self.assertEqual(get_outgoing_email_account(append_to="Blog Post").email_id, "append_to@gmail.com")
+
+ # add back the mail_server
+ frappe.conf['mail_server'] = mail_server
+ for email_account in existing_email_accounts:
+ set_details = {
+ "enable_outgoing": email_account['enable_outgoing'],
+ "default_outgoing": email_account['default_outgoing'],
+ "append_to": email_account['append_to']
+ }
+ frappe.db.set_value('Email Account', email_account['name'], set_details)
+
+def create_email_account(email_id, password, enable_outgoing, default_outgoing=0, append_to=None):
+ email_dict = {
+ "email_id": email_id,
+ "passsword": password,
+ "enable_outgoing":enable_outgoing ,
+ "default_outgoing":default_outgoing ,
+ "enable_incoming": 1,
+ "append_to":append_to,
+ "is_dummy_password": 1,
+ "smtp_server": "localhost"
+ }
+
+ email_account = frappe.new_doc('Email Account')
+ email_account.update(email_dict)
+ email_account.save()
def make_server(port, ssl, tls):
server = SMTPServer(
@@ -22,4 +75,4 @@ def make_server(port, ssl, tls):
use_tls = tls
)
- server.sess
\ No newline at end of file
+ server.sess
diff --git a/frappe/integrations/doctype/webhook/__init__.py b/frappe/integrations/doctype/webhook/__init__.py
index 8b08db5f68..19233bd175 100644
--- a/frappe/integrations/doctype/webhook/__init__.py
+++ b/frappe/integrations/doctype/webhook/__init__.py
@@ -21,7 +21,9 @@ def run_webhooks(doc, method):
if webhooks is None:
# query webhooks
webhooks_list = frappe.get_all('Webhook',
- fields=["name", "`condition`", "webhook_docevent", "webhook_doctype"])
+ fields=["name", "`condition`", "webhook_docevent", "webhook_doctype"],
+ filters={"enabled": True}
+ )
# make webhooks map for cache
webhooks = {}
diff --git a/frappe/integrations/doctype/webhook/test_webhook.py b/frappe/integrations/doctype/webhook/test_webhook.py
index fa7f9534e1..acf2f609e7 100644
--- a/frappe/integrations/doctype/webhook/test_webhook.py
+++ b/frappe/integrations/doctype/webhook/test_webhook.py
@@ -10,6 +10,44 @@ from frappe.integrations.doctype.webhook.webhook import get_webhook_headers, get
class TestWebhook(unittest.TestCase):
+ @classmethod
+ def setUpClass(cls):
+ # delete any existing webhooks
+ frappe.db.sql("DELETE FROM tabWebhook")
+ # create test webhooks
+ cls.create_sample_webhooks()
+
+ @classmethod
+ def create_sample_webhooks(cls):
+ samples_webhooks_data = [
+ {
+ "webhook_doctype": "User",
+ "webhook_docevent": "after_insert",
+ "request_url": "https://httpbin.org/post",
+ "condition": "doc.email",
+ "enabled": True
+ },
+ {
+ "webhook_doctype": "User",
+ "webhook_docevent": "after_insert",
+ "request_url": "https://httpbin.org/post",
+ "condition": "doc.first_name",
+ "enabled": False
+ }
+ ]
+
+ cls.sample_webhooks = []
+ for wh_fields in samples_webhooks_data:
+ wh = frappe.new_doc("Webhook")
+ wh.update(wh_fields)
+ wh.insert()
+ cls.sample_webhooks.append(wh)
+
+ @classmethod
+ def tearDownClass(cls):
+ # delete any existing webhooks
+ frappe.db.sql("DELETE FROM tabWebhook")
+
def setUp(self):
# retrieve or create a User webhook for `after_insert`
webhook_fields = {
@@ -30,10 +68,37 @@ class TestWebhook(unittest.TestCase):
self.user.email = frappe.mock("email")
self.user.save()
+ # Create another test user specific to this test
+ self.test_user = frappe.new_doc("User")
+ self.test_user.email = "user1@integration.webhooks.test.com"
+ self.test_user.first_name = "user1"
+
def tearDown(self) -> None:
self.user.delete()
+ self.test_user.delete()
super().tearDown()
+ def test_webhook_trigger_with_enabled_webhooks(self):
+ """Test webhook trigger for enabled webhooks"""
+
+ frappe.cache().delete_value('webhooks')
+ frappe.flags.webhooks = None
+
+ # Insert the user to db
+ self.test_user.insert()
+
+ self.assertTrue("User" in frappe.flags.webhooks)
+ # only 1 hook (enabled) must be queued
+ self.assertEqual(
+ len(frappe.flags.webhooks.get("User")),
+ 1
+ )
+ self.assertTrue(self.test_user.email in frappe.flags.webhooks_executed)
+ self.assertEqual(
+ frappe.flags.webhooks_executed.get(self.test_user.email)[0],
+ self.sample_webhooks[0].name
+ )
+
def test_validate_doc_events(self):
"Test creating a submit-related webhook for a non-submittable DocType"
diff --git a/frappe/integrations/doctype/webhook/webhook.json b/frappe/integrations/doctype/webhook/webhook.json
index 9f979099c9..85895c052c 100644
--- a/frappe/integrations/doctype/webhook/webhook.json
+++ b/frappe/integrations/doctype/webhook/webhook.json
@@ -11,6 +11,7 @@
"webhook_doctype",
"cb_doc_events",
"webhook_docevent",
+ "enabled",
"sb_condition",
"condition",
"cb_condition",
@@ -147,10 +148,16 @@
"fieldname": "webhook_secret",
"fieldtype": "Password",
"label": "Webhook Secret"
+ },
+ {
+ "default": "1",
+ "fieldname": "enabled",
+ "fieldtype": "Check",
+ "label": "Enabled"
}
],
"links": [],
- "modified": "2020-01-13 01:53:04.459968",
+ "modified": "2021-04-14 05:35:28.532049",
"modified_by": "Administrator",
"module": "Integrations",
"name": "Webhook",
diff --git a/frappe/patches.txt b/frappe/patches.txt
index 5251b3da30..516ddb6094 100644
--- a/frappe/patches.txt
+++ b/frappe/patches.txt
@@ -334,3 +334,4 @@ frappe.patches.v13_0.delete_package_publish_tool
frappe.patches.v13_0.rename_list_view_setting_to_list_view_settings
frappe.patches.v13_0.remove_twilio_settings
frappe.patches.v12_0.rename_uploaded_files_with_proper_name
+frappe.patches.v13_0.queryreport_columns
diff --git a/frappe/patches/v13_0/queryreport_columns.py b/frappe/patches/v13_0/queryreport_columns.py
new file mode 100644
index 0000000000..6c2a1b1219
--- /dev/null
+++ b/frappe/patches/v13_0/queryreport_columns.py
@@ -0,0 +1,22 @@
+# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
+# MIT License. See license.txt
+
+from __future__ import unicode_literals
+import frappe
+import json
+
+def execute():
+ """Convert Query Report json to support other content"""
+ records = frappe.get_all('Report',
+ filters={
+ "json": ["!=", ""]
+ },
+ fields=["name", "json"]
+ )
+ for record in records:
+ jstr = record["json"]
+ data = json.loads(jstr)
+ if isinstance(data, list):
+ # double escape braces
+ jstr = f'{{"columns":{jstr}}}'
+ frappe.db.update('Report', record["name"], "json", jstr)
diff --git a/frappe/public/js/frappe/desk.js b/frappe/public/js/frappe/desk.js
index 250d308b7e..216ec967a4 100644
--- a/frappe/public/js/frappe/desk.js
+++ b/frappe/public/js/frappe/desk.js
@@ -51,6 +51,7 @@ frappe.Application = Class.extend({
this.set_fullwidth_if_enabled();
this.add_browser_class();
this.setup_energy_point_listeners();
+ this.setup_copy_doc_listener();
frappe.ui.keys.setup();
@@ -113,7 +114,7 @@ frappe.Application = Class.extend({
dialog.get_close_btn().toggle(false);
});
- this.setup_social_listeners();
+ this.setup_user_group_listeners();
// listen to build errors
this.setup_build_error_listener();
@@ -592,11 +593,12 @@ frappe.Application = Class.extend({
}
},
- setup_social_listeners() {
- frappe.realtime.on('mention', (message) => {
- if (frappe.get_route()[0] !== 'social') {
- frappe.show_alert(message);
- }
+ setup_user_group_listeners() {
+ frappe.realtime.on('user_group_added', (user_group) => {
+ frappe.boot.user_groups && frappe.boot.user_groups.push(user_group);
+ });
+ frappe.realtime.on('user_group_deleted', (user_group) => {
+ frappe.boot.user_groups = (frappe.boot.user_groups || []).filter(el => el !== user_group);
});
},
@@ -605,6 +607,39 @@ frappe.Application = Class.extend({
frappe.show_alert(message);
});
},
+
+ setup_copy_doc_listener() {
+ $('body').on('paste', (e) => {
+ try {
+ let clipboard_data = e.clipboardData || window.clipboardData || e.originalEvent.clipboardData;
+ let pasted_data = clipboard_data.getData('Text');
+ let doc = JSON.parse(pasted_data);
+ if (doc.doctype) {
+ e.preventDefault();
+ let sleep = (time) => {
+ return new Promise((resolve) => setTimeout(resolve, time));
+ };
+
+ frappe.dom.freeze(__('Creating {0}', [doc.doctype]) + '...');
+ // to avoid abrupt UX
+ // wait for activity feedback
+ sleep(500).then(() => {
+ let res = frappe.model.with_doctype(doc.doctype, () => {
+ let newdoc = frappe.model.copy_doc(doc);
+ newdoc.__newname = doc.name;
+ newdoc.idx = null;
+ newdoc.__run_link_triggers = false;
+ frappe.set_route('Form', newdoc.doctype, newdoc.name);
+ frappe.dom.unfreeze();
+ });
+ res && res.fail(frappe.dom.unfreeze);
+ });
+ }
+ } catch (e) {
+ //
+ }
+ });
+ }
});
frappe.get_module = function(m, default_module) {
diff --git a/frappe/public/js/frappe/form/controls/multiselect.js b/frappe/public/js/frappe/form/controls/multiselect.js
index 64ca4fc83d..bbd7aef822 100644
--- a/frappe/public/js/frappe/form/controls/multiselect.js
+++ b/frappe/public/js/frappe/form/controls/multiselect.js
@@ -68,7 +68,7 @@ frappe.ui.form.ControlMultiSelect = frappe.ui.form.ControlAutocomplete.extend({
let data;
if(this.df.get_data) {
data = this.df.get_data();
- this.set_data(data);
+ if (data) this.set_data(data);
} else {
data = this._super();
}
diff --git a/frappe/public/js/frappe/form/controls/quill-mention/blots/mention.js b/frappe/public/js/frappe/form/controls/quill-mention/blots/mention.js
index 1c5787f854..d6907158f9 100644
--- a/frappe/public/js/frappe/form/controls/quill-mention/blots/mention.js
+++ b/frappe/public/js/frappe/form/controls/quill-mention/blots/mention.js
@@ -15,6 +15,7 @@ class MentionBlot extends Embed {
node.dataset.id = data.id;
node.dataset.value = data.value;
node.dataset.denotationChar = data.denotationChar;
+ node.dataset.isGroup = data.isGroup;
if (data.link) {
node.dataset.link = data.link;
}
@@ -27,6 +28,7 @@ class MentionBlot extends Embed {
value: domNode.dataset.value,
link: domNode.dataset.link || null,
denotationChar: domNode.dataset.denotationChar,
+ isGroup: domNode.dataset.isGroup,
};
}
}
diff --git a/frappe/public/js/frappe/form/controls/quill-mention/quill.mention.js b/frappe/public/js/frappe/form/controls/quill-mention/quill.mention.js
index ac1b9697f0..4b5326271e 100644
--- a/frappe/public/js/frappe/form/controls/quill-mention/quill.mention.js
+++ b/frappe/public/js/frappe/form/controls/quill-mention/quill.mention.js
@@ -149,6 +149,7 @@ class Mention {
this.mentionList.childNodes[this.itemIndex].dataset.value,
link: itemLink || null,
denotationChar: this.mentionList.childNodes[this.itemIndex].dataset.denotationChar,
+ isGroup: this.mentionList.childNodes[this.itemIndex].dataset.isGroup,
};
}
@@ -197,6 +198,7 @@ class Mention {
li.dataset.index = i;
li.dataset.id = data[i].id;
li.dataset.value = data[i].value;
+ li.dataset.isGroup = Boolean(data[i].is_group);
li.dataset.denotationChar = mentionChar;
if (data[i].link) {
li.dataset.link = data[i].link;
diff --git a/frappe/public/js/frappe/form/controls/table.js b/frappe/public/js/frappe/form/controls/table.js
index 075608aa8c..c40f471939 100644
--- a/frappe/public/js/frappe/form/controls/table.js
+++ b/frappe/public/js/frappe/form/controls/table.js
@@ -45,9 +45,12 @@ frappe.ui.form.ControlTable = frappe.ui.form.Control.extend({
} else {
// no column header, map to the existing visible columns
const visible_columns = grid_rows[0].get_visible_columns();
+ let target_column_matched = false;
visible_columns.forEach(column => {
- if (column.fieldname === $(e.target).data('fieldname')) {
+ // consider all columns after the target column.
+ if (target_column_matched || column.fieldname === $(e.target).data('fieldname')) {
fieldnames.push(column.fieldname);
+ target_column_matched = true;
}
});
}
diff --git a/frappe/public/js/frappe/form/form_viewers.js b/frappe/public/js/frappe/form/form_viewers.js
index 3d488e4729..964576ef8a 100644
--- a/frappe/public/js/frappe/form/form_viewers.js
+++ b/frappe/public/js/frappe/form/form_viewers.js
@@ -7,6 +7,11 @@ frappe.ui.form.FormViewers = class FormViewers {
refresh() {
let users = this.frm.get_docinfo()['viewers'];
+ if (!users || !users.current || !users.current.length) {
+ this.parent.empty();
+ return;
+ }
+
let currently_viewing = users.current.filter(user => user != frappe.session.user);
let avatar_group = frappe.avatar_group(currently_viewing, 5, {'align': 'left', 'overlap': true});
this.parent.empty().append(avatar_group);
diff --git a/frappe/public/js/frappe/form/grid.js b/frappe/public/js/frappe/form/grid.js
index b211476e63..86feefed7a 100644
--- a/frappe/public/js/frappe/form/grid.js
+++ b/frappe/public/js/frappe/form/grid.js
@@ -194,7 +194,10 @@ export default class Grid {
}
tasks.push(() => {
- if (dirty) this.refresh();
+ if (dirty) {
+ this.refresh();
+ this.frm.script_manager.trigger(this.df.fieldname + "_delete", this.doctype);
+ }
});
frappe.run_serially(tasks);
@@ -210,6 +213,7 @@ export default class Grid {
this.frm.doc[this.df.fieldname] = [];
$(this.parent).find('.rows').empty();
this.grid_rows = [];
+ this.frm.script_manager.trigger(this.df.fieldname + "_delete", this.doctype);
this.wrapper.find('.grid-heading-row .grid-row-check:checked:first').prop('checked', 0);
this.refresh();
diff --git a/frappe/public/js/frappe/form/sidebar/attachments.js b/frappe/public/js/frappe/form/sidebar/attachments.js
index 9e1ea30c6e..ffd0b513a2 100644
--- a/frappe/public/js/frappe/form/sidebar/attachments.js
+++ b/frappe/public/js/frappe/form/sidebar/attachments.js
@@ -1,8 +1,6 @@
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
// MIT License. See license.txt
-
-
frappe.ui.form.Attachments = Class.extend({
init: function(opts) {
$.extend(this, opts);
@@ -84,17 +82,9 @@ frappe.ui.form.Attachments = Class.extend({
};
}
- let icon;
- // REDESIGN-TODO: set icon using frappe.utils.icon
- if (attachment.is_private) {
- icon = ``;
- } else {
- icon = ``;
- }
+ const icon = `
+ ${frappe.utils.icon(attachment.is_private ? 'lock' : 'unlock', 'sm ml-0')}
+ `;
$(``)
.append(frappe.get_data_pill(
diff --git a/frappe/public/js/frappe/form/templates/attachment.html b/frappe/public/js/frappe/form/templates/attachment.html
deleted file mode 100644
index c1fe3f3c85..0000000000
--- a/frappe/public/js/frappe/form/templates/attachment.html
+++ /dev/null
@@ -1,10 +0,0 @@
-
- ×
-
-
-
-
- {{ file_name }}
-
-
-
diff --git a/frappe/public/js/frappe/form/toolbar.js b/frappe/public/js/frappe/form/toolbar.js
index 2f5b84fb1a..145b8d3eed 100644
--- a/frappe/public/js/frappe/form/toolbar.js
+++ b/frappe/public/js/frappe/form/toolbar.js
@@ -278,13 +278,18 @@ frappe.ui.form.Toolbar = class Toolbar {
}, true)
}
- // copy
+ // duplicate
if(in_list(frappe.boot.user.can_create, me.frm.doctype) && !me.frm.meta.allow_copy) {
this.page.add_menu_item(__("Duplicate"), function() {
me.frm.copy_doc();
}, true);
}
+ // copy doc to clipboard
+ this.page.add_menu_item(__("Copy to Clipboard"), function() {
+ frappe.utils.copy_to_clipboard(JSON.stringify(me.frm.doc));
+ }, true);
+
// rename
if(this.can_rename()) {
this.page.add_menu_item(__("Rename"), function() {
diff --git a/frappe/public/js/frappe/utils/utils.js b/frappe/public/js/frappe/utils/utils.js
index 302ebceeda..7ce30a525c 100644
--- a/frappe/public/js/frappe/utils/utils.js
+++ b/frappe/public/js/frappe/utils/utils.js
@@ -1285,6 +1285,16 @@ Object.assign(frappe.utils, {
value: frappe.boot.user_info[user].fullname,
};
});
+
+ frappe.boot.user_groups && frappe.boot.user_groups.map(group => {
+ names_for_mentions.push({
+ id: group,
+ value: group,
+ is_group: true,
+ link: frappe.utils.get_form_link('User Group', group)
+ });
+ });
+
return names_for_mentions;
},
print(doctype, docname, print_format, letterhead, lang_code) {
diff --git a/frappe/public/js/frappe/views/communication.js b/frappe/public/js/frappe/views/communication.js
index 3a4da2a0b4..6501018c88 100755
--- a/frappe/public/js/frappe/views/communication.js
+++ b/frappe/public/js/frappe/views/communication.js
@@ -2,73 +2,55 @@
// MIT License. See license.txt
frappe.last_edited_communication = {};
-frappe.standard_replies = {};
-frappe.separator_element = '---
';
+const separator_element = '---
';
-frappe.views.CommunicationComposer = Class.extend({
- init: function(opts) {
+frappe.views.CommunicationComposer = class {
+ constructor(opts) {
$.extend(this, opts);
+ if (!this.doc) {
+ this.doc = this.frm && this.frm.doc || {};
+ }
+
this.make();
- },
- make: function() {
- var me = this;
+ }
+
+ make() {
+ const me = this;
this.dialog = new frappe.ui.Dialog({
title: (this.title || this.subject || __("New Email")),
no_submit_on_enter: true,
fields: this.get_fields(),
primary_action_label: __("Send"),
- size: 'large',
- primary_action: function() {
- me.delete_saved_draft();
+ primary_action() {
me.send_action();
},
+ secondary_action_label: __("Discard"),
+ secondary_action() {
+ me.dialog.hide();
+ me.clear_cache();
+ },
+ size: 'large',
minimizable: true
});
this.dialog.sections[0].wrapper.addClass('to_section');
- ['recipients', 'cc', 'bcc'].forEach(field => {
- this.dialog.fields_dict[field].get_data = function() {
- const data = me.dialog.fields_dict[field].get_value();
- const txt = data.match(/[^,\s*]*$/)[0] || '';
- let options = [];
-
- frappe.call({
- method: "frappe.email.get_contact_list",
- args: {
- txt: txt,
- },
- callback: (r) => {
- options = r.message;
- me.dialog.fields_dict[field].set_data(options);
- }
- });
- return options;
- }
- });
-
this.prepare();
this.dialog.show();
if (this.frm) {
$(document).trigger('form-typing', [this.frm]);
}
+ }
- if (this.cc || this.bcc) {
- this.toggle_more_options(true);
- }
- },
-
- get_fields: function() {
- let contactList = [];
- let fields = [
+ get_fields() {
+ const fields = [
{
label: __("To"),
fieldtype: "MultiSelect",
reqd: 0,
fieldname: "recipients",
- options: contactList
},
{
fieldtype: "Button",
@@ -87,13 +69,11 @@ frappe.views.CommunicationComposer = Class.extend({
label: __("CC"),
fieldtype: "MultiSelect",
fieldname: "cc",
- options: contactList
},
{
label: __("BCC"),
fieldtype: "MultiSelect",
fieldname: "bcc",
- options: contactList
},
{
label: __("Email Template"),
@@ -163,78 +143,83 @@ frappe.views.CommunicationComposer = Class.extend({
);
});
- if (frappe.boot.email_accounts && email_accounts.length > 1) {
- fields = [
- {
- label: __("From"),
- fieldtype: "Select",
- reqd: 1,
- fieldname: "sender",
- options: email_accounts.map(function(e) {
- return e.email_id;
- })
- }
- ].concat(fields);
+ if (email_accounts.length > 1) {
+ fields.unshift({
+ label: __("From"),
+ fieldtype: "Select",
+ reqd: 1,
+ fieldname: "sender",
+ options: email_accounts.map(function(e) {
+ return e.email_id;
+ })
+ });
}
return fields;
- },
+ }
toggle_more_options(show_options) {
show_options = show_options || this.dialog.fields_dict.more_options.df.hidden;
this.dialog.set_df_property('more_options', 'hidden', !show_options);
- let label = frappe.utils.icon(show_options ? 'up-line': 'down');
- this.dialog.get_field('option_toggle_button').set_label(label);
- },
- prepare: function() {
+ const label = frappe.utils.icon(show_options ? 'up-line': 'down');
+ this.dialog.get_field('option_toggle_button').set_label(label);
+ }
+
+ prepare() {
+ this.setup_multiselect_queries();
this.setup_subject_and_recipients();
this.setup_print_language();
this.setup_print();
this.setup_attach();
this.setup_email();
- this.setup_last_edited_communication();
this.setup_email_template();
+ this.setup_last_edited_communication();
+ this.set_values();
+ }
- this.dialog.set_value("recipients", this.recipients || '');
- this.dialog.set_value("cc", this.cc || '');
- this.dialog.set_value("bcc", this.bcc || '');
+ setup_multiselect_queries() {
+ ['recipients', 'cc', 'bcc'].forEach(field => {
+ this.dialog.fields_dict[field].get_data = () => {
+ const data = this.dialog.fields_dict[field].get_value();
+ const txt = data.match(/[^,\s*]*$/)[0] || '';
- if(this.dialog.fields_dict.sender) {
- this.dialog.fields_dict.sender.set_value(this.sender || '');
- }
- this.dialog.fields_dict.subject.set_value(
- frappe.utils.html2text(this.subject) || ''
- );
+ frappe.call({
+ method: "frappe.email.get_contact_list",
+ args: {txt},
+ callback: (r) => {
+ this.dialog.fields_dict[field].set_data(r.message);
+ }
+ });
+ };
+ });
+ }
- this.setup_earlier_reply();
- },
-
- setup_subject_and_recipients: function() {
+ setup_subject_and_recipients() {
this.subject = this.subject || "";
- if(!this.forward && !this.recipients && this.last_email) {
+ if (!this.forward && !this.recipients && this.last_email) {
this.recipients = this.last_email.sender;
this.cc = this.last_email.cc;
this.bcc = this.last_email.bcc;
}
- if(!this.forward && !this.recipients) {
+ if (!this.forward && !this.recipients) {
this.recipients = this.frm && this.frm.timeline.get_recipient();
}
- if(!this.subject && this.frm) {
+ if (!this.subject && this.frm) {
// get subject from last communication
- var last = this.frm.timeline.get_last_email();
+ const last = this.frm.timeline.get_last_email();
- if(last) {
+ if (last) {
this.subject = last.subject;
- if(!this.recipients) {
+ if (!this.recipients) {
this.recipients = last.sender;
}
// prepend "Re:"
- if(strip(this.subject.toLowerCase().split(":")[0])!="re") {
+ if (strip(this.subject.toLowerCase().split(":")[0])!="re") {
this.subject = __("Re: {0}", [this.subject]);
}
}
@@ -251,7 +236,7 @@ frappe.views.CommunicationComposer = Class.extend({
// always add an identifier to catch a reply
// some email clients (outlook) may not send the message id to identify
// the thread. So as a backup we use the name of the document as identifier
- let identifier = `#${this.frm.doc.name}`;
+ const identifier = `#${this.frm.doc.name}`;
if (!this.subject.includes(identifier)) {
this.subject = `${this.subject} (${identifier})`;
}
@@ -260,33 +245,25 @@ frappe.views.CommunicationComposer = Class.extend({
if (this.frm && !this.recipients) {
this.recipients = this.frm.doc[this.frm.email_field];
}
- },
+ }
- setup_email_template: function() {
- var me = this;
+ setup_email_template() {
+ const me = this;
this.dialog.fields_dict["email_template"].df.onchange = () => {
- var email_template = me.dialog.fields_dict.email_template.get_value();
+ const email_template = me.dialog.fields_dict.email_template.get_value();
+ if (!email_template) return;
- var prepend_reply = function(reply) {
- if(me.reply_added===email_template) {
- return;
- }
- var content_field = me.dialog.fields_dict.content;
- var subject_field = me.dialog.fields_dict.subject;
- var content = content_field.get_value() || "";
- var subject = subject_field.get_value() || "";
+ function prepend_reply(reply) {
+ if (me.reply_added === email_template) return;
- var parts = content.split('');
+ const content_field = me.dialog.fields_dict.content;
+ const subject_field = me.dialog.fields_dict.subject;
- if(parts.length===2) {
- content = [reply.message, "
", parts[1]];
- } else {
- content = [reply.message, "
", content];
- }
-
- content_field.set_value(content.join(''));
+ let content = content_field.get_value() || "";
+ content = content.split('')[1] || content;
+ content_field.set_value(`${reply.message}
${content}`);
subject_field.set_value(reply.subject);
me.reply_added = email_template;
@@ -296,86 +273,107 @@ frappe.views.CommunicationComposer = Class.extend({
method: 'frappe.email.doctype.email_template.email_template.get_email_template',
args: {
template_name: email_template,
- doc: me.frm.doc,
+ doc: me.doc,
_lang: me.dialog.get_value("language_sel")
},
- callback: function(r) {
+ callback(r) {
prepend_reply(r.message);
},
});
- }
- },
+ };
+ }
- setup_last_edited_communication: function() {
- var me = this;
- if (!this.doc){
- if (cur_frm){
- this.doc = cur_frm.doctype;
- }else{
- this.doc = "Inbox";
- }
- }
- if (cur_frm && cur_frm.docname) {
- this.key = cur_frm.docname;
+ setup_last_edited_communication() {
+ if (this.frm) {
+ this.doctype = this.frm.doctype;
+ this.key = this.frm.docname;
} else {
- this.key = "Inbox";
+ this.doctype = this.key = "Inbox";
}
- if(this.last_email) {
+
+ if (this.last_email) {
this.key = this.key + ":" + this.last_email.name;
}
- if(this.subject){
+
+ if (this.subject) {
this.key = this.key + ":" + this.subject;
}
- this.dialog.onhide = function() {
- var last_edited_communication = me.get_last_edited_communication();
- $.extend(last_edited_communication, {
- sender: me.dialog.get_value("sender"),
- recipients: me.dialog.get_value("recipients"),
- cc: me.dialog.get_value("cc"),
- bcc: me.dialog.get_value("bcc"),
- subject: me.dialog.get_value("subject"),
- content: me.dialog.get_value("content"),
- });
- if (me.frm) {
- $(document).trigger("form-stopped-typing", [me.frm]);
+ this.dialog.on_hide = () => {
+ $.extend(
+ this.get_last_edited_communication(true),
+ this.dialog.get_values(true)
+ );
+
+ if (this.frm) {
+ $(document).trigger("form-stopped-typing", [this.frm]);
+ }
+ };
+ }
+
+ get_last_edited_communication(clear) {
+ if (!frappe.last_edited_communication[this.doctype]) {
+ frappe.last_edited_communication[this.doctype] = {};
+ }
+
+ if (clear || !frappe.last_edited_communication[this.doctype][this.key]) {
+ frappe.last_edited_communication[this.doctype][this.key] = {};
+ }
+
+ return frappe.last_edited_communication[this.doctype][this.key];
+ }
+
+ async set_values() {
+ for (const fieldname of ["recipients", "cc", "bcc", "sender"]) {
+ await this.dialog.set_value(fieldname, this[fieldname] || "");
+ }
+
+ const subject = frappe.utils.html2text(this.subject) || '';
+ await this.dialog.set_value("subject", subject);
+
+ await this.set_values_from_last_edited_communication();
+ await this.set_content();
+
+ // set default email template for the first email in a document
+ if (this.frm && !this.is_a_reply && !this.content_set) {
+ const email_template = this.frm.meta.default_email_template || '';
+ await this.dialog.set_value("email_template", email_template);
+ }
+
+ for (const fieldname of ['email_template', 'cc', 'bcc']) {
+ if (this.dialog.get_value(fieldname)) {
+ this.toggle_more_options(true);
+ break;
}
}
+ }
- this.dialog.on_page_show = function() {
- if (!me.txt) {
- var last_edited_communication = me.get_last_edited_communication();
- if(last_edited_communication.content) {
- me.dialog.set_value("sender", last_edited_communication.sender || "");
- me.dialog.set_value("subject", last_edited_communication.subject || "");
- me.dialog.set_value("recipients", last_edited_communication.recipients || "");
- me.dialog.set_value("cc", last_edited_communication.cc || "");
- me.dialog.set_value("bcc", last_edited_communication.bcc || "");
- me.dialog.set_value("content", last_edited_communication.content || "");
- }
- }
+ async set_values_from_last_edited_communication() {
+ if (this.txt) return;
+ const last_edited = this.get_last_edited_communication();
+ if (!last_edited.content) return;
+
+ // prevent re-triggering of email template
+ if (last_edited.email_template) {
+ const template_field = this.dialog.fields_dict.email_template;
+ await template_field.set_model_value(last_edited.email_template);
+ delete last_edited.email_template;
}
- },
+ await this.dialog.set_values(last_edited);
+ this.content_set = true;
+ }
- get_last_edited_communication: function() {
- if (!frappe.last_edited_communication[this.doc]) {
- frappe.last_edited_communication[this.doc] = {};
- }
+ selected_format() {
+ return (
+ this.dialog.fields_dict.select_print_format.input.value
+ || this.frm && this.frm.meta.default_print_format
+ || "Standard"
+ );
+ }
- if(!frappe.last_edited_communication[this.doc][this.key]) {
- frappe.last_edited_communication[this.doc][this.key] = {};
- }
-
- return frappe.last_edited_communication[this.doc][this.key];
- },
-
- selected_format: function() {
- return this.dialog.fields_dict.select_print_format.input.value || (this.frm && this.frm.meta.default_print_format) || "Standard";
- },
-
- get_print_format: function(format) {
+ get_print_format(format) {
if (!format) {
format = this.selected_format();
}
@@ -385,21 +383,18 @@ frappe.views.CommunicationComposer = Class.extend({
} else {
return {};
}
- },
+ }
- setup_print_language: function() {
- var doc = this.doc || cur_frm.doc;
- var fields = this.dialog.fields_dict;
+ setup_print_language() {
+ const fields = this.dialog.fields_dict;
//Load default print language from doctype
- this.lang_code = doc.language
-
- if (!this.lang_code && this.get_print_format().default_print_language) {
- this.lang_code = this.get_print_format().default_print_language;
- }
+ this.lang_code = this.doc.language
+ || this.get_print_format().default_print_language
+ || frappe.boot.lang;
//On selection of language retrieve language code
- var me = this;
+ const me = this;
$(fields.language_sel.input).change(function(){
me.lang_code = this.value
})
@@ -412,11 +407,11 @@ frappe.views.CommunicationComposer = Class.extend({
if (this.lang_code) {
$(fields.language_sel.input).val(this.lang_code);
}
- },
+ }
- setup_print: function() {
+ setup_print() {
// print formats
- var fields = this.dialog.fields_dict;
+ const fields = this.dialog.fields_dict;
// toggle print format
$(fields.attach_document_print.input).click(function() {
@@ -426,8 +421,8 @@ frappe.views.CommunicationComposer = Class.extend({
// select print format
$(fields.select_print_format.wrapper).toggle(false);
- if (cur_frm) {
- const print_formats = frappe.meta.get_print_formats(cur_frm.meta.name);
+ if (this.frm) {
+ const print_formats = frappe.meta.get_print_formats(this.frm.meta.name);
$(fields.select_print_format.input)
.empty()
.add_options(print_formats)
@@ -436,11 +431,11 @@ frappe.views.CommunicationComposer = Class.extend({
$(fields.attach_document_print.wrapper).toggle(false);
}
- },
+ }
- setup_attach: function() {
- var fields = this.dialog.fields_dict;
- var attach = $(fields.select_attachments.wrapper);
+ setup_attach() {
+ const fields = this.dialog.fields_dict;
+ const attach = $(fields.select_attachments.wrapper);
if (!this.attachments) {
this.attachments = [];
@@ -483,9 +478,9 @@ frappe.views.CommunicationComposer = Class.extend({
.find(".add-more-attachments button")
.on('click', () => new frappe.ui.FileUploader(args));
this.render_attachment_rows();
- },
+ }
- render_attachment_rows: function(attachment) {
+ render_attachment_rows(attachment) {
const select_attachments = this.dialog.fields_dict.select_attachments;
const attachment_rows = $(select_attachments.wrapper).find(".attach-list");
if (attachment) {
@@ -509,7 +504,7 @@ frappe.views.CommunicationComposer = Class.extend({
});
}
}
- },
+ }
get_attachment_row(attachment, checked) {
return $(`
@@ -526,56 +521,55 @@ frappe.views.CommunicationComposer = Class.extend({
${frappe.utils.icon('link-url')}
`);
- },
+ }
- setup_email: function() {
+ setup_email() {
// email
- var fields = this.dialog.fields_dict;
+ const fields = this.dialog.fields_dict;
- if(this.attach_document_print) {
+ if (this.attach_document_print) {
$(fields.attach_document_print.input).click();
$(fields.select_print_format.wrapper).toggle(true);
}
$(fields.send_me_a_copy.input).on('click', () => {
// update send me a copy (make it sticky)
- let val = fields.send_me_a_copy.get_value();
+ const val = fields.send_me_a_copy.get_value();
frappe.db.set_value('User', frappe.session.user, 'send_me_a_copy', val);
frappe.boot.user.send_me_a_copy = val;
});
- },
+ }
- send_action: function() {
- var me = this;
- var btn = me.dialog.get_primary_btn();
+ send_action() {
+ const me = this;
+ const btn = me.dialog.get_primary_btn();
+ const form_values = this.get_values();
+ if (!form_values) return;
- var form_values = this.get_values();
- if(!form_values) return;
-
- var selected_attachments =
+ const selected_attachments =
$.map($(me.dialog.wrapper).find("[data-file-name]:checked"), function (element) {
return $(element).attr("data-file-name");
});
- if(form_values.attach_document_print) {
+ if (form_values.attach_document_print) {
me.send_email(btn, form_values, selected_attachments, null, form_values.select_print_format || "");
} else {
me.send_email(btn, form_values, selected_attachments);
}
- },
+ }
- get_values: function() {
- var form_values = this.dialog.get_values();
+ get_values() {
+ const form_values = this.dialog.get_values();
// cc
- for ( var i=0, l=this.dialog.fields.length; i < l; i++ ) {
- var df = this.dialog.fields[i];
+ for (let i = 0, l = this.dialog.fields.length; i < l; i++) {
+ const df = this.dialog.fields[i];
- if ( df.is_cc_checkbox ) {
+ if (df.is_cc_checkbox) {
// concat in cc
- if ( form_values[df.fieldname] ) {
+ if (form_values[df.fieldname]) {
form_values.cc = ( form_values.cc ? (form_values.cc + ", ") : "" ) + df.fieldname;
form_values.bcc = ( form_values.bcc ? (form_values.bcc + ", ") : "" ) + df.fieldname;
}
@@ -585,22 +579,27 @@ frappe.views.CommunicationComposer = Class.extend({
}
return form_values;
- },
+ }
- save_as_draft: function() {
+ save_as_draft() {
if (this.dialog && this.frm) {
let message = this.dialog.get_value('content');
- message = message.split(frappe.separator_element)[0];
+ message = message.split(separator_element)[0];
localforage.setItem(this.frm.doctype + this.frm.docname, message).catch(e => {
if (e) {
// silently fail
console.log(e); // eslint-disable-line
- console.warn('[Communication] localStorage is full. Cannot save message as draft'); // eslint-disable-line
+ console.warn('[Communication] IndexedDB is full. Cannot save message as draft'); // eslint-disable-line
}
});
}
- },
+ }
+
+ clear_cache() {
+ this.delete_saved_draft();
+ this.get_last_edited_communication(true);
+ }
delete_saved_draft() {
if (this.dialog && this.frm) {
@@ -608,28 +607,28 @@ frappe.views.CommunicationComposer = Class.extend({
if (e) {
// silently fail
console.log(e); // eslint-disable-line
- console.warn('[Communication] localStorage is full. Cannot save message as draft'); // eslint-disable-line
+ console.warn('[Communication] IndexedDB is full. Cannot save message as draft'); // eslint-disable-line
}
});
}
- },
+ }
- send_email: function(btn, form_values, selected_attachments, print_html, print_format) {
- var me = this;
- me.dialog.hide();
+ send_email(btn, form_values, selected_attachments, print_html, print_format) {
+ const me = this;
+ this.dialog.hide();
- if(!form_values.recipients) {
+ if (!form_values.recipients) {
frappe.msgprint(__("Enter Email Recipient(s)"));
return;
}
- if(!form_values.attach_document_print) {
+ if (!form_values.attach_document_print) {
print_html = null;
print_format = null;
}
- if(cur_frm && !frappe.model.can_email(me.doc.doctype, cur_frm)) {
+ if (this.frm && !frappe.model.can_email(this.doc.doctype, this.frm)) {
frappe.msgprint(__("You are not allowed to send emails related to this document"));
return;
}
@@ -650,28 +649,29 @@ frappe.views.CommunicationComposer = Class.extend({
send_me_a_copy: form_values.send_me_a_copy,
print_format: print_format,
sender: form_values.sender,
- sender_full_name: form_values.sender?frappe.user.full_name():undefined,
+ sender_full_name: form_values.sender
+ ? frappe.user.full_name()
+ : undefined,
email_template: form_values.email_template,
attachments: selected_attachments,
_lang : me.lang_code,
read_receipt:form_values.send_read_receipt,
print_letterhead: me.is_print_letterhead_checked(),
},
- btn: btn,
- callback: function(r) {
- if(!r.exc) {
+ btn,
+ callback(r) {
+ if (!r.exc) {
frappe.utils.play_sound("email");
- if(r.message["emails_not_sent_to"]) {
+ if (r.message["emails_not_sent_to"]) {
frappe.msgprint(__("Email not sent to {0} (unsubscribed / disabled)",
[ frappe.utils.escape_html(r.message["emails_not_sent_to"]) ]) );
}
- if ((frappe.last_edited_communication[me.doc] || {})[me.key]) {
- delete frappe.last_edited_communication[me.doc][me.key];
- }
- if (cur_frm) {
- cur_frm.reload_doc();
+ me.clear_cache();
+
+ if (me.frm) {
+ me.frm.reload_doc();
}
// try the success callback if it exists
@@ -679,7 +679,7 @@ frappe.views.CommunicationComposer = Class.extend({
try {
me.success(r);
} catch (e) {
- console.log(e);
+ console.log(e); // eslint-disable-line
}
}
@@ -691,113 +691,115 @@ frappe.views.CommunicationComposer = Class.extend({
try {
me.error(r);
} catch (e) {
- console.log(e);
+ console.log(e); // eslint-disable-line
}
}
}
}
});
- },
+ }
- is_print_letterhead_checked: function() {
+ is_print_letterhead_checked() {
if (this.frm && $(this.frm.wrapper).find('.form-print-wrapper').is(':visible')){
return $(this.frm.wrapper).find('.print-letterhead').prop('checked') ? 1 : 0;
} else {
return (frappe.model.get_doc(":Print Settings", "Print Settings") ||
{ with_letterhead: 1 }).with_letterhead ? 1 : 0;
}
- },
+ }
- get_default_outgoing_email_account_signature: function() {
- return frappe.db.get_value('Email Account', { 'default_outgoing': 1, 'add_signature': 1 }, 'signature');
- },
+ async set_content() {
+ if (this.content_set) return;
- setup_earlier_reply: async function() {
- let fields = this.dialog.fields_dict;
- let signature = frappe.boot.user.email_signature || "";
-
- if (!signature) {
- const res = await this.get_default_outgoing_email_account_signature();
- signature = "" + res.message.signature;
+ let message = this.txt || "";
+ if (!message && this.frm) {
+ const { doctype, docname } = this.frm;
+ message = await localforage.getItem(doctype + docname) || "";
}
- if (signature && !frappe.utils.is_html(signature)) {
- signature = signature.replace(/\n/g, "
");
+ if (message) {
+ this.content_set = true;
}
- if(this.txt) {
- this.message = this.txt + (this.message ? ("
" + this.message) : "");
- } else {
- // saved draft in localStorage
- const { doctype, docname } = this.frm || {};
- if (doctype && docname) {
- this.message = await localforage.getItem(doctype + docname) || '';
- }
- }
-
- if(this.real_name) {
- this.message = ''+__('Dear') +' '
- + this.real_name + ",
" + (this.message || "");
- }
-
- if(this.message && signature && this.message.includes(signature)) {
- signature = "";
- }
-
- let reply = (this.message || "") + (signature ? ("
" + signature) : "");
- let content = '';
-
- if (this.is_a_reply === 'undefined') {
- this.is_a_reply = true;
+ message += await this.get_signature();
+ if (this.real_name && !message.includes("")) {
+ message = `${__('Dear')} ${this.real_name},
+
${message}`;
}
if (this.is_a_reply) {
- let last_email = this.last_email;
-
- if (!last_email) {
- last_email = this.frm && this.frm.timeline.get_last_email(true);
- }
-
- if (!last_email) return;
-
- let last_email_content = last_email.original_comment || last_email.content;
-
- // convert the email context to text as we are enclosing
- // this inside
- last_email_content = this.html2text(last_email_content).replace(/\n/g, '
');
-
- // clip last email for a maximum of 20k characters
- // to prevent the email content from getting too large
- if (last_email_content.length > 20 * 1024) {
- last_email_content += '' + __('Message clipped') + '
' + last_email_content;
- last_email_content = last_email_content.slice(0, 20 * 1024);
- }
-
- let communication_date = last_email.communication_date || last_email.creation;
- content = `
- ${reply}
-
- ${frappe.separator_element || ''}
- ${__("On {0}, {1} wrote:", [frappe.datetime.global_date_format(communication_date) , last_email.sender])}
-
- ${last_email_content}
-
- `;
- } else {
- content = reply;
+ message += this.get_earlier_reply();
}
- fields.content.set_value(content);
- },
- html2text: function(html) {
+ await this.dialog.set_value("content", message);
+ }
+
+ async get_signature() {
+ let signature = frappe.boot.user.email_signature;
+
+ if (!signature) {
+ const response = await frappe.db.get_value(
+ 'Email Account',
+ {'default_outgoing': 1, 'add_signature': 1},
+ 'signature'
+ );
+
+ signature = response.message.signature;
+ }
+
+ if (!signature) return "";
+
+ if (!frappe.utils.is_html(signature)) {
+ signature = signature.replace(/\n/g, "
");
+ }
+
+ return "
" + signature;
+ }
+
+ get_earlier_reply() {
+ const last_email = (
+ this.last_email
+ || this.frm && this.frm.timeline.get_last_email(true)
+ );
+
+ if (!last_email) return "";
+ let last_email_content = last_email.original_comment || last_email.content;
+
+ // convert the email context to text as we are enclosing
+ // this inside
+ last_email_content = this.html2text(last_email_content).replace(/\n/g, '
');
+
+ // clip last email for a maximum of 20k characters
+ // to prevent the email content from getting too large
+ if (last_email_content.length > 20 * 1024) {
+ last_email_content += '' + __('Message clipped') + '
' + last_email_content;
+ last_email_content = last_email_content.slice(0, 20 * 1024);
+ }
+
+ const communication_date = frappe.datetime.global_date_format(
+ last_email.communication_date || last_email.creation
+ );
+
+ return `
+
+ ${separator_element || ''}
+
+ ${__("On {0}, {1} wrote:", [communication_date, last_email.sender])}
+
+
+ ${last_email_content}
+
+ `;
+ }
+
+ html2text(html) {
// convert HTML to text and try and preserve whitespace
- var d = document.createElement( 'div' );
+ const d = document.createElement( 'div' );
d.innerHTML = html.replace(/<\/div>/g, '
') // replace end of blocks
.replace(/<\/p>/g, '
') // replace end of paragraphs
.replace(/
/g, '\n');
- let text = d.textContent;
// replace multiple empty lines with just one
- return text.replace(/\n{3,}/g, '\n\n');
+ return d.textContent.replace(/\n{3,}/g, '\n\n');
}
-});
+};
diff --git a/frappe/public/js/frappe/views/inbox/inbox_view.js b/frappe/public/js/frappe/views/inbox/inbox_view.js
index 1085e93e6c..8b53bd49a9 100644
--- a/frappe/public/js/frappe/views/inbox/inbox_view.js
+++ b/frappe/public/js/frappe/views/inbox/inbox_view.js
@@ -204,9 +204,7 @@ frappe.views.InboxView = class InboxView extends frappe.views.ListView {
};
frappe.new_doc('Email Account');
} else {
- new frappe.views.CommunicationComposer({
- doc: {}
- });
+ new frappe.views.CommunicationComposer();
}
}
};
diff --git a/frappe/public/js/frappe/views/kanban/kanban_board.js b/frappe/public/js/frappe/views/kanban/kanban_board.js
index f563f64cb4..bbc2051e4c 100644
--- a/frappe/public/js/frappe/views/kanban/kanban_board.js
+++ b/frappe/public/js/frappe/views/kanban/kanban_board.js
@@ -306,6 +306,7 @@ frappe.provide("frappe.views");
store.on('change:cur_list', setup_restore_columns);
store.on('change:columns', setup_restore_columns);
store.on('change:empty_state', show_empty_state);
+ fluxify.doAction('update_order');
}
function prepare() {
diff --git a/frappe/public/scss/common/css_variables.scss b/frappe/public/scss/common/css_variables.scss
index 701a0d09e9..8f4af36389 100644
--- a/frappe/public/scss/common/css_variables.scss
+++ b/frappe/public/scss/common/css_variables.scss
@@ -169,6 +169,9 @@
// Other Colors
--sidebar-select-color: var(--gray-200);
+ --scrollbar-thumb-color: var(--gray-400);
+ --scrollbar-track-color: var(--gray-200);
+
--shadow-inset: inset 0px -1px 0px var(--gray-300);
--border-color: var(--gray-100);
--dark-border-color: var(--gray-300);
diff --git a/frappe/public/scss/common/quill.scss b/frappe/public/scss/common/quill.scss
index e5303be5cf..d15ca7e036 100644
--- a/frappe/public/scss/common/quill.scss
+++ b/frappe/public/scss/common/quill.scss
@@ -119,7 +119,10 @@
border: 1px solid var(--border-color);
padding: 2px 5px;
font-size: var(--text-sm);
- background-color: var(--fg-color);
+ background-color: var(--user-mention-bg-color);
+ a[href] {
+ text-decoration: none;
+ }
}
// table
@@ -174,7 +177,7 @@
.ql-editor.read-mode {
padding: 0;
.mention {
- background-color: var(--control-bg);
+ --user-mention-bg-color: var(--control-bg);
}
}
@@ -190,4 +193,8 @@
.mention>span {
margin: 0 3px;
-}
\ No newline at end of file
+}
+
+.mention[data-is-group="true"] {
+ background-color: var(--group-mention-bg-color);
+}
diff --git a/frappe/public/scss/desk/css_variables.scss b/frappe/public/scss/desk/css_variables.scss
index 21b4ac6c1d..5aca23a0b0 100644
--- a/frappe/public/scss/desk/css_variables.scss
+++ b/frappe/public/scss/desk/css_variables.scss
@@ -59,6 +59,10 @@ $input-height: 28px !default;
--timeline-content-max-width: 700px;
--timeline-left-padding: calc(var(--padding-xl) + var(--timeline-item-icon-size) / 2);
+ // mentions
+ --user-mention-bg-color: var(--fg-color);
+ --group-mention-bg-color: var(--bg-purple);
+
// skeleton
--skeleton-bg: var(--gray-100);
diff --git a/frappe/public/scss/desk/dark.scss b/frappe/public/scss/desk/dark.scss
index 743107af47..4e83f4db47 100644
--- a/frappe/public/scss/desk/dark.scss
+++ b/frappe/public/scss/desk/dark.scss
@@ -65,6 +65,9 @@
--sidebar-select-color: var(--gray-800);
+ --scrollbar-thumb-color: var(--gray-600);
+ --scrollbar-track-color: var(--gray-700);
+
--shadow-inset: var(--fg-color);
--border-color: var(--gray-700);
--dark-border-color: var(--gray-600);
@@ -75,6 +78,8 @@
// input
--input-disabled-bg: none;
+ color-scheme: dark;
+
.frappe-card {
.btn-default {
background-color: var(--bg-color);
@@ -99,7 +104,7 @@
.ql-editor {
color: var(--text-on-gray);
&.read-mode {
- span,
+ span:not(.mention),
p,
u,
strong {
diff --git a/frappe/public/scss/desk/desktop.scss b/frappe/public/scss/desk/desktop.scss
index 1bb91090e6..ac3b1a4f7c 100644
--- a/frappe/public/scss/desk/desktop.scss
+++ b/frappe/public/scss/desk/desktop.scss
@@ -754,7 +754,28 @@ body {
.layout-side-section, .layout-main-section-wrapper {
height: 100%;
overflow-y: auto;
+ padding-right: 25px;
+ scrollbar-color: var(--gray-200) transparent;
+ [data-theme="dark"] & {
+ scrollbar-color: var(--gray-800) transparent;
+ }
+
+ &::-webkit-scrollbar-track {
+ background: transparent;
+ }
+
+ &::-webkit-scrollbar-thumb {
+ background: var(--gray-200);
+ [data-theme="dark"] & {
+ background: var(--gray-800);
+ }
+ }
}
+
+ .layout-side-section {
+ margin-right: 20px;
+ }
+
.desk-sidebar {
margin-bottom: var(--margin-2xl);
}
diff --git a/frappe/public/scss/desk/index.scss b/frappe/public/scss/desk/index.scss
index 31eae63776..d0d968df63 100644
--- a/frappe/public/scss/desk/index.scss
+++ b/frappe/public/scss/desk/index.scss
@@ -10,6 +10,7 @@
@import "mobile";
@import "form";
@import "print_preview";
+@import "scrollbar";
@import "navbar";
@import "../common/modal";
@import "slides";
diff --git a/frappe/public/scss/desk/scrollbar.scss b/frappe/public/scss/desk/scrollbar.scss
new file mode 100644
index 0000000000..806ffd13eb
--- /dev/null
+++ b/frappe/public/scss/desk/scrollbar.scss
@@ -0,0 +1,29 @@
+/* Works on Firefox */
+* {
+ scrollbar-width: thin;
+ scrollbar-color: var(--scrollbar-thumb-color) var(--scrollbar-track-color);
+}
+
+html {
+ scrollbar-width: auto;
+}
+
+/* Works on Chrome, Edge, and Safari */
+*::-webkit-scrollbar {
+ width: 6px;
+ height: 6px;
+}
+
+*::-webkit-scrollbar-thumb {
+ background: var(--scrollbar-thumb-color);
+}
+
+*::-webkit-scrollbar-track,
+*::-webkit-scrollbar-corner {
+ background: var(--scrollbar-track-color);
+}
+
+body::-webkit-scrollbar {
+ width: unset;
+ height: unset;
+}
diff --git a/frappe/public/scss/desk/timeline.scss b/frappe/public/scss/desk/timeline.scss
index 4bb3cbec78..a7e5d3dd9c 100644
--- a/frappe/public/scss/desk/timeline.scss
+++ b/frappe/public/scss/desk/timeline.scss
@@ -77,6 +77,7 @@ $threshold: 34;
}
}
.document-email-link-container {
+ @extend .ellipsis;
position: relative;
padding: var(--padding-sm);
font-size: var(--text-sm);
@@ -141,4 +142,4 @@ $threshold: 34;
--icon-stroke: var(--text-color);
}
}
-}
\ No newline at end of file
+}
diff --git a/frappe/public/scss/website/index.scss b/frappe/public/scss/website/index.scss
index 1fb5badc6c..823ec9b08a 100644
--- a/frappe/public/scss/website/index.scss
+++ b/frappe/public/scss/website/index.scss
@@ -90,6 +90,13 @@
margin: 2rem 0;
}
+@media (max-width: map-get($grid-breakpoints, "lg")) {
+ .page-content-wrapper .container {
+ padding-left: 1rem;
+ padding-right: 1rem;
+ }
+}
+
.breadcrumb-container {
margin-top: 1rem;
padding-top: 0.25rem;
diff --git a/frappe/public/scss/website/navbar.scss b/frappe/public/scss/website/navbar.scss
index 4d2ccfece9..3496a8907c 100644
--- a/frappe/public/scss/website/navbar.scss
+++ b/frappe/public/scss/website/navbar.scss
@@ -1,3 +1,15 @@
+.navbar {
+ padding-left: 0;
+ padding-right: 0;
+}
+
+@media (max-width: map-get($grid-breakpoints, "lg")) {
+ .navbar {
+ padding-left: 1rem;
+ padding-right: 1rem;
+ }
+}
+
.navbar-light {
border-bottom: 1px solid $border-color;
background: $navbar-bg;
@@ -96,4 +108,4 @@
@extend .ellipsis;
max-width: 100%;
vertical-align: middle;
-}
\ No newline at end of file
+}
diff --git a/frappe/templates/base.html b/frappe/templates/base.html
index 18c9e9d99a..78aa573c99 100644
--- a/frappe/templates/base.html
+++ b/frappe/templates/base.html
@@ -56,6 +56,8 @@
}
window.dev_server = {{ dev_server }};
window.socketio_port = {{ (frappe.socketio_port or 'null') }};
+ window.show_language_picker = {{ show_language_picker }};
+ window.is_chat_enabled = {{ chat_enable }};
@@ -110,39 +112,5 @@
{%- endblock %}
{%- block body_include %}{{ body_include or "" }}{% endblock -%}
-