Merge remote-tracking branch 'upstream/develop' into feat-py3-dependencies
This commit is contained in:
commit
a7794d9f6c
72 changed files with 1005 additions and 466 deletions
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -1,7 +1,4 @@
|
|||
context('Relative Timeframe', () => {
|
||||
beforeEach(() => {
|
||||
cy.login();
|
||||
});
|
||||
before(() => {
|
||||
cy.login();
|
||||
cy.visit('/app/website');
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
context('Table MultiSelect', () => {
|
||||
beforeEach(() => {
|
||||
before(() => {
|
||||
cy.login();
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -20,6 +20,6 @@ frappe.listview_settings['Communication'] = {
|
|||
},
|
||||
|
||||
primary_action: function() {
|
||||
new frappe.views.CommunicationComposer({ doc: {} });
|
||||
new frappe.views.CommunicationComposer();
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 = '''
|
||||
<div>
|
||||
Testing comment for
|
||||
<span class="mention" data-id="Team" data-value="Team" data-is-group="true" data-denotation-char="@">
|
||||
<span><span class="ql-mention-denotation-char">@</span>Team</span>
|
||||
</span>
|
||||
please check
|
||||
</div>
|
||||
'''
|
||||
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)
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
0
frappe/core/doctype/user_group/__init__.py
Normal file
0
frappe/core/doctype/user_group/__init__.py
Normal file
10
frappe/core/doctype/user_group/test_user_group.py
Normal file
10
frappe/core/doctype/user_group/test_user_group.py
Normal file
|
|
@ -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
|
||||
8
frappe/core/doctype/user_group/user_group.js
Normal file
8
frappe/core/doctype/user_group/user_group.js
Normal file
|
|
@ -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) {
|
||||
|
||||
// }
|
||||
});
|
||||
48
frappe/core/doctype/user_group/user_group.json
Normal file
48
frappe/core/doctype/user_group/user_group.json
Normal file
|
|
@ -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
|
||||
}
|
||||
15
frappe/core/doctype/user_group/user_group.py
Normal file
15
frappe/core/doctype/user_group/user_group.py
Normal file
|
|
@ -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)
|
||||
0
frappe/core/doctype/user_group_member/__init__.py
Normal file
0
frappe/core/doctype/user_group_member/__init__.py
Normal file
|
|
@ -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
|
||||
|
|
@ -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) {
|
||||
|
||||
// }
|
||||
});
|
||||
32
frappe/core/doctype/user_group_member/user_group_member.json
Normal file
32
frappe/core/doctype/user_group_member/user_group_member.json
Normal file
|
|
@ -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
|
||||
}
|
||||
10
frappe/core/doctype/user_group_member/user_group_member.py
Normal file
10
frappe/core/doctype/user_group_member/user_group_member.py
Normal file
|
|
@ -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
|
||||
|
|
@ -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"))
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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(' ', '_')
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
server.sess
|
||||
|
|
|
|||
|
|
@ -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 = {}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
22
frappe/patches/v13_0/queryreport_columns.py
Normal file
22
frappe/patches/v13_0/queryreport_columns.py
Normal file
|
|
@ -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)
|
||||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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 = `<div><svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.07685 1.45015H8.02155C7.13255 1.44199 6.2766 1.78689 5.64159 2.40919C5.00596 3.0321 4.64377 3.88196 4.63464 4.77188L4.63462 4.77188V4.77701V5.12157H3.75C2.64543 5.12157 1.75 6.017 1.75 7.12157V12.5132C1.75 13.6177 2.64543 14.5132 3.75 14.5132H12.2885C13.393 14.5132 14.2885 13.6177 14.2885 12.5132V7.12157C14.2885 6.017 13.393 5.12157 12.2885 5.12157H11.4037V4.83708C11.4119 3.94809 11.067 3.09213 10.4447 2.45713C9.82175 1.8215 8.97189 1.4593 8.08198 1.45018L8.08198 1.45015H8.07685ZM10.4037 5.12157V4.8347V4.82972L10.4037 4.82972C10.4099 4.20495 10.1678 3.60329 9.73045 3.15705C9.29371 2.7114 8.69805 2.4572 8.07417 2.45015H8.01916H8.01418L8.01419 2.45013C7.38942 2.44391 6.78776 2.68609 6.34152 3.12341C5.89586 3.56015 5.64166 4.15581 5.63462 4.77969V5.12157H10.4037ZM3.75 6.12157C3.19772 6.12157 2.75 6.56929 2.75 7.12157V12.5132C2.75 13.0655 3.19772 13.5132 3.75 13.5132H12.2885C12.8407 13.5132 13.2885 13.0655 13.2885 12.5132V7.12157C13.2885 6.56929 12.8407 6.12157 12.2885 6.12157H3.75ZM8.01936 10.3908C8.33605 10.3908 8.59279 10.134 8.59279 9.81734C8.59279 9.50064 8.33605 9.24391 8.01936 9.24391C7.70266 9.24391 7.44593 9.50064 7.44593 9.81734C7.44593 10.134 7.70266 10.3908 8.01936 10.3908ZM9.59279 9.81734C9.59279 10.6863 8.88834 11.3908 8.01936 11.3908C7.15038 11.3908 6.44593 10.6863 6.44593 9.81734C6.44593 8.94836 7.15038 8.24391 8.01936 8.24391C8.88834 8.24391 9.59279 8.94836 9.59279 9.81734Z" fill="currentColor"/>
|
||||
</svg></div>`;
|
||||
} else {
|
||||
icon = `<div><svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.07685 1.45014H8.02155C7.13255 1.44198 6.2766 1.78687 5.64159 2.40918C5.00596 3.03209 4.64377 3.88195 4.63464 4.77187L4.63462 4.77187V4.777V5.12156H3.75C2.64543 5.12156 1.75 6.01699 1.75 7.12156V12.5132C1.75 13.6177 2.64543 14.5132 3.75 14.5132H12.2885C13.393 14.5132 14.2885 13.6177 14.2885 12.5132V7.12156C14.2885 6.01699 13.393 5.12156 12.2885 5.12156H5.63462V4.77968C5.64166 4.1558 5.89586 3.56014 6.34152 3.12339C6.78776 2.68608 7.38942 2.4439 8.01419 2.45012L8.01418 2.45014H8.01916H8.07417C8.69805 2.45718 9.29371 2.71138 9.73045 3.15704C9.92373 3.35427 10.2403 3.35746 10.4375 3.16418C10.6347 2.9709 10.6379 2.65434 10.4447 2.45711C9.82175 1.82149 8.97189 1.45929 8.08198 1.45017L8.08198 1.45014H8.07685ZM3.75 6.12156C3.19772 6.12156 2.75 6.56927 2.75 7.12156V12.5132C2.75 13.0655 3.19772 13.5132 3.75 13.5132H12.2885C12.8407 13.5132 13.2885 13.0655 13.2885 12.5132V7.12156C13.2885 6.56927 12.8407 6.12156 12.2885 6.12156H3.75ZM8.01936 10.3908C8.33605 10.3908 8.59279 10.134 8.59279 9.81732C8.59279 9.50063 8.33605 9.2439 8.01936 9.2439C7.70266 9.2439 7.44593 9.50063 7.44593 9.81732C7.44593 10.134 7.70266 10.3908 8.01936 10.3908ZM9.59279 9.81732C9.59279 10.6863 8.88834 11.3908 8.01936 11.3908C7.15038 11.3908 6.44593 10.6863 6.44593 9.81732C6.44593 8.94835 7.15038 8.2439 8.01936 8.2439C8.88834 8.2439 9.59279 8.94835 9.59279 9.81732Z" fill="currentColor"/>
|
||||
</svg></div>`;
|
||||
}
|
||||
const icon = `<a href="/app/file/${fileid}">
|
||||
${frappe.utils.icon(attachment.is_private ? 'lock' : 'unlock', 'sm ml-0')}
|
||||
</a>`;
|
||||
|
||||
$(`<li class="attachment-row">`)
|
||||
.append(frappe.get_data_pill(
|
||||
|
|
|
|||
|
|
@ -1,10 +0,0 @@
|
|||
<li class="attachment-row flex align-center">
|
||||
<a class="close">×</a>
|
||||
<a href="{{ file_path }}">
|
||||
<i class="{{ icon }} fa-fw text-warning"></i>
|
||||
</a>
|
||||
<a href="{{ file_url }}" target="_blank" title="{{ file_name }}" class="ellipsis" style="max-width: calc(100% - 43px);">
|
||||
<span>{{ file_name }}</span>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -2,73 +2,55 @@
|
|||
// MIT License. See license.txt
|
||||
|
||||
frappe.last_edited_communication = {};
|
||||
frappe.standard_replies = {};
|
||||
frappe.separator_element = '<div>---</div>';
|
||||
const separator_element = '<div>---</div>';
|
||||
|
||||
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('<!-- salutation-ends -->');
|
||||
const content_field = me.dialog.fields_dict.content;
|
||||
const subject_field = me.dialog.fields_dict.subject;
|
||||
|
||||
if(parts.length===2) {
|
||||
content = [reply.message, "<br>", parts[1]];
|
||||
} else {
|
||||
content = [reply.message, "<br>", content];
|
||||
}
|
||||
|
||||
content_field.set_value(content.join(''));
|
||||
let content = content_field.get_value() || "";
|
||||
content = content.split('<!-- salutation-ends -->')[1] || content;
|
||||
|
||||
content_field.set_value(`${reply.message}<br>${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 $(`<p class="checkbox flex">
|
||||
|
|
@ -526,56 +521,55 @@ frappe.views.CommunicationComposer = Class.extend({
|
|||
${frappe.utils.icon('link-url')}
|
||||
</a>
|
||||
</p>`);
|
||||
},
|
||||
}
|
||||
|
||||
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 = "<!-- signature-included -->" + 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, "<br>");
|
||||
if (message) {
|
||||
this.content_set = true;
|
||||
}
|
||||
|
||||
if(this.txt) {
|
||||
this.message = this.txt + (this.message ? ("<br><br>" + 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 = '<p>'+__('Dear') +' '
|
||||
+ this.real_name + ",</p><!-- salutation-ends --><br>" + (this.message || "");
|
||||
}
|
||||
|
||||
if(this.message && signature && this.message.includes(signature)) {
|
||||
signature = "";
|
||||
}
|
||||
|
||||
let reply = (this.message || "") + (signature ? ("<br>" + 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("<!-- salutation-ends -->")) {
|
||||
message = `<p>${__('Dear')} ${this.real_name},</p>
|
||||
<!-- salutation-ends --><br>${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 <blockquote>
|
||||
last_email_content = this.html2text(last_email_content).replace(/\n/g, '<br>');
|
||||
|
||||
// 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 += '<div>' + __('Message clipped') + '</div>' + 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}
|
||||
<div><br></div>
|
||||
${frappe.separator_element || ''}
|
||||
<p>${__("On {0}, {1} wrote:", [frappe.datetime.global_date_format(communication_date) , last_email.sender])}</p>
|
||||
<blockquote>
|
||||
${last_email_content}
|
||||
</blockquote>
|
||||
`;
|
||||
} 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, "<br>");
|
||||
}
|
||||
|
||||
return "<br>" + 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 <blockquote>
|
||||
last_email_content = this.html2text(last_email_content).replace(/\n/g, '<br>');
|
||||
|
||||
// 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 += '<div>' + __('Message clipped') + '</div>' + 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 `
|
||||
<div><br></div>
|
||||
${separator_element || ''}
|
||||
<p>
|
||||
${__("On {0}, {1} wrote:", [communication_date, last_email.sender])}
|
||||
</p>
|
||||
<blockquote>
|
||||
${last_email_content}
|
||||
</blockquote>
|
||||
`;
|
||||
}
|
||||
|
||||
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, '<br></div>') // replace end of blocks
|
||||
.replace(/<\/p>/g, '<br></p>') // replace end of paragraphs
|
||||
.replace(/<br>/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');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
.mention[data-is-group="true"] {
|
||||
background-color: var(--group-mention-bg-color);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@
|
|||
@import "mobile";
|
||||
@import "form";
|
||||
@import "print_preview";
|
||||
@import "scrollbar";
|
||||
@import "navbar";
|
||||
@import "../common/modal";
|
||||
@import "slides";
|
||||
|
|
|
|||
29
frappe/public/scss/desk/scrollbar.scss
Normal file
29
frappe/public/scss/desk/scrollbar.scss
Normal file
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 }};
|
||||
</script>
|
||||
</head>
|
||||
<body frappe-session-status="{{ 'logged-in' if frappe.session.user != 'Guest' else 'logged-out'}}" data-path="{{ path | e }}" {%- if template and template.endswith('.md') %} frappe-content-type="markdown" {% endif -%} class="{{ body_class or ''}}">
|
||||
|
|
@ -110,39 +112,5 @@
|
|||
{%- endblock %}
|
||||
<!-- csrf_token -->
|
||||
{%- block body_include %}{{ body_include or "" }}{% endblock -%}
|
||||
<script>
|
||||
frappe.ready(() => {
|
||||
if (frappe.session.user === 'Guest') {
|
||||
frappe.call("frappe.translate.get_all_languages", {
|
||||
with_language_name: true
|
||||
}).then(res => {
|
||||
let language_list = res.message;
|
||||
let language = frappe.get_cookie('preferred_language');
|
||||
let language_codes = [];
|
||||
language_list.forEach(language_doc => {
|
||||
language_codes.push(language_doc.language_code)
|
||||
$("#language-switcher")
|
||||
.append(
|
||||
$("<option></option>")
|
||||
.attr("value", language_doc.language_code)
|
||||
.text(language_doc.language_name)
|
||||
);
|
||||
});
|
||||
$("#language-switcher").removeClass('hide');
|
||||
language = language || (language_codes.includes(navigator.language) ? navigator.language : 'en');
|
||||
$("#language-switcher").val(language);
|
||||
document.documentElement.lang = language;
|
||||
$("#language-switcher").change((e) => {
|
||||
let lang = $("#language-switcher").val();
|
||||
frappe.call("frappe.translate.set_preferred_language_cookie", {
|
||||
"preferred_language": lang
|
||||
}).then(() => {
|
||||
window.location.reload();
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
})
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -21,8 +21,8 @@
|
|||
<div class="collapse navbar-collapse" id="navbarSupportedContent">
|
||||
{% include "templates/includes/navbar/navbar_items.html" %}
|
||||
</div>
|
||||
<div class="form-group mb-0">
|
||||
<select class="form-control hide" id="language-switcher"></select>
|
||||
<div class="form-group mb-0 hide" id="language-switcher">
|
||||
<select class="form-control"></select>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
|
|
|||
|
|
@ -109,6 +109,13 @@ def get_dict(fortype, name=None):
|
|||
elif fortype=="jsfile":
|
||||
messages = get_messages_from_file(name)
|
||||
elif fortype=="boot":
|
||||
messages = []
|
||||
apps = frappe.get_all_apps(True)
|
||||
for app in apps:
|
||||
messages.extend(get_server_messages(app))
|
||||
messages = deduplicate_messages(messages)
|
||||
|
||||
messages += frappe.db.sql("""select "navbar", item_label from `tabNavbar Item` where item_label is not null""")
|
||||
messages = get_messages_from_include_files()
|
||||
messages += frappe.db.sql("select 'Print Format:', name from `tabPrint Format`")
|
||||
messages += frappe.db.sql("select 'DocType:', name from tabDocType")
|
||||
|
|
|
|||
|
|
@ -177,7 +177,7 @@ acceptable_attributes = [
|
|||
'data-value', 'role', 'frameborder', 'allowfullscreen', 'spellcheck',
|
||||
'data-mode', 'data-gramm', 'data-placeholder', 'data-comment',
|
||||
'data-id', 'data-denotation-char', 'itemprop', 'itemscope',
|
||||
'itemtype', 'itemid', 'itemref', 'datetime'
|
||||
'itemtype', 'itemid', 'itemref', 'datetime', 'data-is-group'
|
||||
]
|
||||
|
||||
mathml_attributes = [
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ from frappe.modules import scrub
|
|||
from frappe.www.printview import get_visible_columns
|
||||
import frappe.exceptions
|
||||
import frappe.integrations.utils
|
||||
from frappe.frappeclient import FrappeClient
|
||||
|
||||
class ServerScriptNotEnabled(frappe.PermissionError):
|
||||
pass
|
||||
|
|
@ -104,8 +105,10 @@ def get_safe_globals():
|
|||
make_post_request = frappe.integrations.utils.make_post_request,
|
||||
socketio_port=frappe.conf.socketio_port,
|
||||
get_hooks=frappe.get_hooks,
|
||||
sanitize_html=frappe.utils.sanitize_html
|
||||
sanitize_html=frappe.utils.sanitize_html,
|
||||
log_error=frappe.log_error
|
||||
),
|
||||
FrappeClient=FrappeClient,
|
||||
style=frappe._dict(
|
||||
border_color='#d1d8dd'
|
||||
),
|
||||
|
|
@ -297,4 +300,4 @@ VALID_UTILS = (
|
|||
"formatdate",
|
||||
"get_user_info_for_avatar",
|
||||
"get_abbr"
|
||||
)
|
||||
)
|
||||
|
|
|
|||
|
|
@ -215,6 +215,11 @@ def get_context(context):
|
|||
amount = self.amount
|
||||
if self.amount_based_on_field:
|
||||
amount = doc.get(self.amount_field)
|
||||
|
||||
from decimal import Decimal
|
||||
if amount is None or Decimal(amount) <= 0:
|
||||
return frappe.utils.get_url(self.success_url or self.route)
|
||||
|
||||
payment_details = {
|
||||
"amount": amount,
|
||||
"title": title,
|
||||
|
|
|
|||
|
|
@ -33,20 +33,12 @@ frappe.ui.form.on('Website Settings', {
|
|||
frm.fields_dict.top_bar_items.grid.update_docfield_property(
|
||||
'parent_label', 'options', frm.events.get_parent_options(frm, "top_bar_items")
|
||||
);
|
||||
|
||||
if ($(frm.fields_dict.top_bar_items.grid.wrapper).find(".grid-row-open")) {
|
||||
frm.fields_dict.top_bar_items.grid.refresh();
|
||||
}
|
||||
},
|
||||
|
||||
set_parent_label_options_footer: function(frm) {
|
||||
frm.fields_dict.footer_items.grid.update_docfield_property(
|
||||
'parent_label', 'options', frm.events.get_parent_options(frm, "top_bar_items")
|
||||
'parent_label', 'options', frm.events.get_parent_options(frm, "footer_items")
|
||||
);
|
||||
|
||||
if ($(frm.fields_dict.footer_items.grid.wrapper).find(".grid-row-open")) {
|
||||
frm.fields_dict.footer_items.grid.refresh();
|
||||
}
|
||||
},
|
||||
|
||||
authorize_api_indexing_access: function(frm) {
|
||||
|
|
@ -122,10 +114,18 @@ frappe.ui.form.on('Website Settings', {
|
|||
});
|
||||
|
||||
frappe.ui.form.on('Top Bar Item', {
|
||||
top_bar_items_delete(frm) {
|
||||
frm.events.set_parent_label_options(frm);
|
||||
},
|
||||
|
||||
footer_items_add(frm, cdt, cdn) {
|
||||
frappe.model.set_value(cdt, cdn, 'right', 0);
|
||||
},
|
||||
|
||||
footer_items_delete(frm) {
|
||||
frm.events.set_parent_label_options_footer(frm);
|
||||
},
|
||||
|
||||
parent_label: function(frm, doctype, name) {
|
||||
frm.events.set_parent_options(frm, doctype, name);
|
||||
},
|
||||
|
|
|
|||
|
|
@ -25,9 +25,11 @@
|
|||
"set_banner_from_image",
|
||||
"favicon",
|
||||
"top_bar",
|
||||
"navbar_search",
|
||||
"hide_login",
|
||||
"top_bar_items",
|
||||
"hide_login",
|
||||
"navbar_search",
|
||||
"show_language_picker",
|
||||
"navbar_template_section",
|
||||
"navbar_template",
|
||||
"navbar_template_values",
|
||||
"edit_navbar_template_values",
|
||||
|
|
@ -410,6 +412,19 @@
|
|||
"fieldname": "google_analytics_anonymize_ip",
|
||||
"fieldtype": "Check",
|
||||
"label": "Google Analytics Anonymize IP"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "show_language_picker",
|
||||
"fieldtype": "Check",
|
||||
"label": "Show Language Picker"
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"collapsible_depends_on": "navbar_template",
|
||||
"fieldname": "navbar_template_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Navbar Template"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-cog",
|
||||
|
|
@ -418,7 +433,7 @@
|
|||
"issingle": 1,
|
||||
"links": [],
|
||||
"max_attachments": 10,
|
||||
"modified": "2021-04-13 10:22:51.888788",
|
||||
"modified": "2021-04-14 17:39:56.609771",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Website",
|
||||
"name": "Website Settings",
|
||||
|
|
|
|||
|
|
@ -121,7 +121,8 @@ def get_website_settings(context=None):
|
|||
"facebook_share", "google_plus_one", "twitter_share", "linked_in_share",
|
||||
"disable_signup", "hide_footer_signup", "head_html", "title_prefix",
|
||||
"navbar_template", "footer_template", "navbar_search", "enable_view_tracking",
|
||||
"footer_logo", "call_to_action", "call_to_action_url"]:
|
||||
"footer_logo", "call_to_action", "call_to_action_url", "show_language_picker",
|
||||
"chat_enable"]:
|
||||
if hasattr(settings, k):
|
||||
context[k] = settings.get(k)
|
||||
|
||||
|
|
@ -178,7 +179,3 @@ def get_items(parentfield):
|
|||
t['child_items'].append(d)
|
||||
break
|
||||
return top_items
|
||||
|
||||
@frappe.whitelist(allow_guest=True)
|
||||
def is_chat_enabled():
|
||||
return bool(frappe.db.get_single_value('Website Settings', 'chat_enable'))
|
||||
|
|
|
|||
|
|
@ -376,6 +376,39 @@ $.extend(frappe, {
|
|||
// Start observing an element
|
||||
io.observe(el);
|
||||
});
|
||||
},
|
||||
show_language_picker() {
|
||||
if (frappe.session.user === 'Guest' && window.show_language_picker) {
|
||||
frappe.call("frappe.translate.get_all_languages", {
|
||||
with_language_name: true
|
||||
}).then(res => {
|
||||
let language_list = res.message;
|
||||
let language = frappe.get_cookie('preferred_language');
|
||||
let language_codes = [];
|
||||
let language_switcher = $("#language-switcher .form-control");
|
||||
language_list.forEach(language_doc => {
|
||||
language_codes.push(language_doc.language_code);
|
||||
language_switcher
|
||||
.append(
|
||||
$("<option></option>")
|
||||
.attr("value", language_doc.language_code)
|
||||
.text(language_doc.language_name)
|
||||
);
|
||||
});
|
||||
$("#language-switcher").removeClass('hide');
|
||||
language = language || (language_codes.includes(navigator.language) ? navigator.language : 'en');
|
||||
language_switcher.val(language);
|
||||
document.documentElement.lang = language;
|
||||
language_switcher.change(() => {
|
||||
let lang = language_switcher.val();
|
||||
frappe.call("frappe.translate.set_preferred_language_cookie", {
|
||||
"preferred_language": lang
|
||||
}).then(() => {
|
||||
window.location.reload();
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -599,17 +632,13 @@ $(document).on("page-change", function() {
|
|||
|
||||
|
||||
frappe.ready(function() {
|
||||
frappe.call({
|
||||
method: 'frappe.website.doctype.website_settings.website_settings.is_chat_enabled',
|
||||
callback: (r) => {
|
||||
if (r.message) {
|
||||
frappe.require(['/assets/js/moment-bundle.min.js', "/assets/css/frappe-chat-web.css", "/assets/frappe/js/lib/socket.io.min.js"], () => {
|
||||
frappe.require('/assets/js/chat.js', () => {
|
||||
frappe.chat.setup();
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
frappe.show_language_picker();
|
||||
if (window.is_chat_enabled) {
|
||||
frappe.require(['/assets/js/moment-bundle.min.js', "/assets/css/frappe-chat-web.css", "/assets/frappe/js/lib/socket.io.min.js"], () => {
|
||||
frappe.require('/assets/js/chat.js', () => {
|
||||
frappe.chat.setup();
|
||||
});
|
||||
});
|
||||
}
|
||||
frappe.socketio.init(window.socketio_port);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@
|
|||
"driver.js": "^0.9.8",
|
||||
"express": "^4.17.1",
|
||||
"fast-deep-equal": "^2.0.1",
|
||||
"frappe-charts": "^2.0.0-rc11",
|
||||
"frappe-charts": "^2.0.0-rc13",
|
||||
"frappe-datatable": "^1.15.3",
|
||||
"frappe-gantt": "^0.5.0",
|
||||
"fuse.js": "^3.4.6",
|
||||
|
|
|
|||
|
|
@ -2699,10 +2699,10 @@ fragment-cache@^0.2.1:
|
|||
dependencies:
|
||||
map-cache "^0.2.2"
|
||||
|
||||
frappe-charts@^2.0.0-rc11:
|
||||
version "2.0.0-rc11"
|
||||
resolved "https://registry.yarnpkg.com/frappe-charts/-/frappe-charts-2.0.0-rc11.tgz#0724fa0d43593362c075c3805ebbbe1a608fcef7"
|
||||
integrity sha512-DY3tThT1lNGcJlRMOtIhnILtSm5h1iKysWhZAyj7yrGiOnOWbZpYx/NZzXZYwtRrWwMlYiLX2ylV76qo31ONsg==
|
||||
frappe-charts@^2.0.0-rc13:
|
||||
version "2.0.0-rc13"
|
||||
resolved "https://registry.yarnpkg.com/frappe-charts/-/frappe-charts-2.0.0-rc13.tgz#fdb251d7ae311c41e38f90a3ae108070ec6b9072"
|
||||
integrity sha512-Bv7IfllIrjRbKWHn5b769dOSenqdBixAr6m5kurf8ZUOJSLOgK4HOXItJ7BA8n9PvviH9/k5DaloisjLM2Bm1w==
|
||||
|
||||
frappe-datatable@^1.15.3:
|
||||
version "1.15.3"
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue