diff --git a/.eslintrc b/.eslintrc
index c55acc5bac..a80d2910fa 100644
--- a/.eslintrc
+++ b/.eslintrc
@@ -80,6 +80,7 @@
"validate_email": true,
"validate_name": true,
"validate_phone": true,
+ "validate_url": true,
"get_number_format": true,
"format_number": true,
"format_currency": true,
diff --git a/cypress/fixtures/data_field_validation_doctype.js b/cypress/fixtures/data_field_validation_doctype.js
new file mode 100644
index 0000000000..da091af7e5
--- /dev/null
+++ b/cypress/fixtures/data_field_validation_doctype.js
@@ -0,0 +1,65 @@
+export default {
+ name: 'Validation Test',
+ custom: 1,
+ actions: [],
+ creation: '2019-03-15 06:29:07.215072',
+ doctype: 'DocType',
+ editable_grid: 1,
+ engine: 'InnoDB',
+ fields: [
+ {
+ fieldname: 'email',
+ fieldtype: 'Data',
+ label: 'Email',
+ options: 'Email'
+ },
+ {
+ fieldname: 'URL',
+ fieldtype: 'Data',
+ label: 'URL',
+ options: 'URL'
+ },
+ {
+ fieldname: 'Phone',
+ fieldtype: 'Data',
+ label: 'Phone',
+ options: 'Phone'
+ },
+ {
+ fieldname: 'person_name',
+ fieldtype: 'Data',
+ label: 'Person Name',
+ options: 'Name'
+ },
+ {
+ fieldname: 'read_only_url',
+ fieldtype: 'Data',
+ label: 'Read Only URL',
+ options: 'URL',
+ read_only: '1',
+ default: 'https://frappe.io'
+ }
+ ],
+ issingle: 1,
+ links: [],
+ modified: '2021-04-19 14:40:53.127615',
+ modified_by: 'Administrator',
+ module: 'Custom',
+ owner: 'Administrator',
+ permissions: [
+ {
+ create: 1,
+ delete: 1,
+ email: 1,
+ print: 1,
+ read: 1,
+ role: 'System Manager',
+ share: 1,
+ write: 1
+ }
+ ],
+ quick_entry: 1,
+ sort_field: 'modified',
+ sort_order: 'ASC',
+ track_changes: 1
+};
diff --git a/cypress/integration/data_field_form_validation.js b/cypress/integration/data_field_form_validation.js
new file mode 100644
index 0000000000..c6feea5550
--- /dev/null
+++ b/cypress/integration/data_field_form_validation.js
@@ -0,0 +1,43 @@
+import data_field_validation_doctype from '../fixtures/data_field_validation_doctype';
+const doctype_name = data_field_validation_doctype.name;
+
+
+context('Data Field Input Validation in New Form', () => {
+ before(() => {
+ cy.login();
+ cy.visit('/app/website');
+ return cy.insert_doc('DocType', data_field_validation_doctype, true);
+ });
+
+ function validateField(fieldname, invalid_value, valid_value) {
+ // Invalid, should have has-error class
+ cy.get_field(fieldname).clear().type(invalid_value).blur();
+ cy.get(`.frappe-control[data-fieldname="${fieldname}"]`).should('have.class', 'has-error');
+ // Valid value, should not have has-error class
+ cy.get_field(fieldname).clear().type(valid_value);
+ cy.get(`.frappe-control[data-fieldname="${fieldname}"]`).should('not.have.class', 'has-error');
+ }
+
+ describe('Data Field Options', () => {
+ it('should validate email address', () => {
+ cy.new_form(doctype_name);
+ validateField('email', 'captian', 'hello@test.com');
+ });
+
+ it('should validate URL', () => {
+ validateField('url', 'jkl', 'https://frappe.io');
+ validateField('url', 'abcd.com', 'http://google.com/home');
+ validateField('url', '&&http://google.uae', 'gopher://frappe.io');
+ validateField('url', 'ftt2:://google.in?q=news', 'ftps2://frappe.io/__/#home');
+ validateField('url', 'ftt2://', 'ntps://localhost'); // For intranet URLs
+ });
+
+ it('should validate phone number', () => {
+ validateField('phone', 'america', '89787878');
+ });
+
+ it('should validate name', () => {
+ validateField('person_name', ' 777Hello', 'James Bond');
+ });
+ });
+});
\ No newline at end of file
diff --git a/cypress/integration/url_data_field.js b/cypress/integration/url_data_field.js
new file mode 100644
index 0000000000..cf22c62363
--- /dev/null
+++ b/cypress/integration/url_data_field.js
@@ -0,0 +1,43 @@
+import data_field_validation_doctype from '../fixtures/data_field_validation_doctype';
+
+const doctype_name = data_field_validation_doctype.name;
+
+context('URL Data Field Input', () => {
+ before(() => {
+ cy.login();
+ cy.visit('/app/website');
+ return cy.insert_doc('DocType', data_field_validation_doctype, true);
+ });
+
+
+ describe('URL Data Field Input ', () => {
+ it('should not show URL link button without focus', () => {
+ cy.new_form(doctype_name);
+ cy.get_field('url').clear().type('https://frappe.io');
+ cy.get_field('url').blur().wait(500);
+ cy.get('.link-btn').should('not.be.visible');
+ });
+
+ it('should show URL link button on focus', () => {
+ cy.get_field('url').focus().wait(500);
+ cy.get('.link-btn').should('be.visible');
+ });
+
+ it('should not show URL link button for invalid URL', () => {
+ cy.get_field('url').clear().type('fuzzbuzz');
+ cy.get('.link-btn').should('not.be.visible');
+ });
+
+ it('should have valid URL link with target _blank', () => {
+ cy.get_field('url').clear().type('https://frappe.io');
+ cy.get('.link-btn .btn-open').should('have.attr', 'href', 'https://frappe.io');
+ cy.get('.link-btn .btn-open').should('have.attr', 'target', '_blank');
+ });
+
+ it('should inject anchor tag in read-only URL data field', () => {
+ cy.get('[data-fieldname="read_only_url"]')
+ .find('a')
+ .should('have.attr', 'target', '_blank');
+ });
+ });
+});
\ No newline at end of file
diff --git a/frappe/app.py b/frappe/app.py
index c9e993a853..794d0f18af 100644
--- a/frappe/app.py
+++ b/frappe/app.py
@@ -201,12 +201,20 @@ def handle_exception(e):
response = None
http_status_code = getattr(e, "http_status_code", 500)
return_as_message = False
+ accept_header = frappe.get_request_header("Accept") or ""
+ respond_as_json = (
+ frappe.get_request_header('Accept')
+ and (frappe.local.is_ajax or 'application/json' in accept_header)
+ or (
+ frappe.local.request.path.startswith("/api/") and not accept_header.startswith("text")
+ )
+ )
if frappe.conf.get('developer_mode'):
# don't fail silently
print(frappe.get_traceback())
- if frappe.get_request_header('Accept') and (frappe.local.is_ajax or 'application/json' in frappe.get_request_header('Accept')):
+ if respond_as_json:
# handle ajax responses first
# if the request is ajax, send back the trace or error message
response = frappe.utils.response.report_error(http_status_code)
diff --git a/frappe/boot.py b/frappe/boot.py
index 65a07b15e5..0dfcb8d1b4 100644
--- a/frappe/boot.py
+++ b/frappe/boot.py
@@ -42,8 +42,6 @@ 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/change_log/v13/v13_2_0.md b/frappe/change_log/v13/v13_2_0.md
new file mode 100644
index 0000000000..6fc3eec5e3
--- /dev/null
+++ b/frappe/change_log/v13/v13_2_0.md
@@ -0,0 +1,32 @@
+# Version 13.2.0 Release Notes
+
+### Features & Enhancements
+
+- Add option to mention a group of users ([#12844](https://github.com/frappe/frappe/pull/12844))
+- Copy DocType / documents across sites ([#12872](https://github.com/frappe/frappe/pull/12872))
+- Scheduler log in notifications ([#1135](https://github.com/frappe/frappe/pull/1135))
+- Add Enable/Disable Webhook via Check Field ([#12842](https://github.com/frappe/frappe/pull/12842))
+- Allow query/custom reports to save custom data in the json field ([#12534](https://github.com/frappe/frappe/pull/12534))
+
+### Fixes
+
+- Load server translations in boot (backport #12848) ([#12852](https://github.com/frappe/frappe/pull/12852))
+- Allow to override dashboard chart properties type/color ([#12846](https://github.com/frappe/frappe/pull/12846))
+- Multi-column paste in grid ([#12861](https://github.com/frappe/frappe/pull/12861))
+- Add log_error and FrappeClient to restricted python ([#12857](https://github.com/frappe/frappe/pull/12857))
+- Redirect Web Form user directly to success URL, if no amount is due ([#12661](https://github.com/frappe/frappe/pull/12661))
+- Attachment pill lock icon redirects to File ([#12864](https://github.com/frappe/frappe/pull/12864))
+- Redirect Web Form user directly to success URL, if no amount is due (backport #12661) ([#12856](https://github.com/frappe/frappe/pull/12856))
+- Remove events to redraw charts ([#12973](https://github.com/frappe/frappe/pull/12973))
+- Don't allow user to remove/change data source file in data import ([#12827](https://github.com/frappe/frappe/pull/12827))
+- Load server translations in boot ([#12848](https://github.com/frappe/frappe/pull/12848))
+- Newly created Workspace not being accessible unless a shortcut u… ([#12866](https://github.com/frappe/frappe/pull/12866))
+- Currency labels in grids ([#12974](https://github.com/frappe/frappe/pull/12974))
+- Handle error while session start ([#12933](https://github.com/frappe/frappe/pull/12933))
+- Add field type check in custom field validation ([#12858](https://github.com/frappe/frappe/pull/12858))
+- Make language select optional and fix breakpoint issues ([#12860](https://github.com/frappe/frappe/pull/12860))
+- Form Dashboard reference link ([#12945](https://github.com/frappe/frappe/pull/12945))
+- Invalid HTML generated by the base template ([#12953](https://github.com/frappe/frappe/pull/12953))
+- Default values were not triggering change event ([#12975](https://github.com/frappe/frappe/pull/12975))
+- Make strings translatable ([#12877](https://github.com/frappe/frappe/pull/12877))
+- Added build-message-files command ([#12950](https://github.com/frappe/frappe/pull/12950))
\ No newline at end of file
diff --git a/frappe/commands/site.py b/frappe/commands/site.py
index 0102d3ac40..ebd2700c9c 100755
--- a/frappe/commands/site.py
+++ b/frappe/commands/site.py
@@ -1,6 +1,7 @@
# imports - standard imports
import os
import sys
+import shutil
# imports - third party imports
import click
@@ -202,10 +203,13 @@ def install_app(context, apps):
@click.command("list-apps")
+@click.option("--format", "-f", type=click.Choice(["text", "json"]), default="text")
@pass_context
-def list_apps(context):
+def list_apps(context, format):
"List apps in site"
+ summary_dict = {}
+
def fix_whitespaces(text):
if site == context.sites[-1]:
text = text.rstrip()
@@ -234,18 +238,25 @@ def list_apps(context):
]
applications_summary = "\n".join(installed_applications)
summary = f"{site_title}\n{applications_summary}\n"
+ summary_dict[site] = [app.app_name for app in apps]
else:
- applications_summary = "\n".join(frappe.get_installed_apps())
+ installed_applications = frappe.get_installed_apps()
+ applications_summary = "\n".join(installed_applications)
summary = f"{site_title}\n{applications_summary}\n"
+ summary_dict[site] = installed_applications
summary = fix_whitespaces(summary)
- if applications_summary and summary:
+ if format == "text" and applications_summary and summary:
print(summary)
frappe.destroy()
+ if format == "json":
+ import json
+
+ click.echo(json.dumps(summary_dict))
@click.command('add-system-manager')
@click.argument('email')
@@ -547,7 +558,7 @@ def move(dest_dir, site):
site_dump_exists = os.path.exists(final_new_path)
count = int(count or 0) + 1
- os.rename(old_path, final_new_path)
+ shutil.move(old_path, final_new_path)
frappe.destroy()
return final_new_path
diff --git a/frappe/commands/utils.py b/frappe/commands/utils.py
index 65557f62d3..dbca28a3a3 100644
--- a/frappe/commands/utils.py
+++ b/frappe/commands/utils.py
@@ -531,7 +531,7 @@ def run_tests(context, app=None, module=None, doctype=None, test=(), profile=Fal
# Generate coverage report only for app that is being tested
source_path = os.path.join(get_bench_path(), 'apps', app or 'frappe')
- cov = Coverage(source=[source_path], omit=[
+ omit=[
'*.html',
'*.js',
'*.xml',
@@ -541,7 +541,12 @@ def run_tests(context, app=None, module=None, doctype=None, test=(), profile=Fal
'*.vue',
'*/doctype/*/*_dashboard.py',
'*/patches/*'
- ])
+ ]
+
+ if not app or app == 'frappe':
+ omit.append('*/commands/*')
+
+ cov = Coverage(source=[source_path], omit=omit)
cov.start()
ret = frappe.test_runner.main(app, module, doctype, context.verbose, tests=tests,
diff --git a/frappe/core/doctype/doctype/doctype.json b/frappe/core/doctype/doctype/doctype.json
index fe5038b841..7f93d3130a 100644
--- a/frappe/core/doctype/doctype/doctype.json
+++ b/frappe/core/doctype/doctype/doctype.json
@@ -662,4 +662,4 @@
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
-}
\ No newline at end of file
+}
diff --git a/frappe/core/doctype/user/user.py b/frappe/core/doctype/user/user.py
index 0462de8643..a4d13a57e0 100644
--- a/frappe/core/doctype/user/user.py
+++ b/frappe/core/doctype/user/user.py
@@ -56,6 +56,7 @@ class User(Document):
def after_insert(self):
create_notification_settings(self.name)
+ frappe.cache().delete_key('users_for_mentions')
def validate(self):
self.check_demo()
@@ -129,6 +130,9 @@ class User(Document):
if self.time_zone:
frappe.defaults.set_default("time_zone", self.time_zone, self.name)
+ if self.has_value_changed('allow_in_mentions') or self.has_value_changed('user_type'):
+ frappe.cache().delete_key('users_for_mentions')
+
def has_website_permission(self, ptype, user, verbose=False):
"""Returns true if current user is the session user"""
return self.name == frappe.session.user
@@ -389,6 +393,9 @@ class User(Document):
# delete notification settings
frappe.delete_doc("Notification Settings", self.name, ignore_permissions=True)
+ if self.get('allow_in_mentions'):
+ frappe.cache().delete_key('users_for_mentions')
+
def before_rename(self, old_name, new_name, merge=False):
self.check_demo()
diff --git a/frappe/core/doctype/user_group/user_group.py b/frappe/core/doctype/user_group/user_group.py
index 64bffa06d0..b1d0fede4c 100644
--- a/frappe/core/doctype/user_group/user_group.py
+++ b/frappe/core/doctype/user_group/user_group.py
@@ -9,7 +9,7 @@ import frappe
class UserGroup(Document):
def after_insert(self):
- frappe.publish_realtime('user_group_added', self.name)
+ frappe.cache().delete_key('user_groups')
def on_trash(self):
- frappe.publish_realtime('user_group_deleted', self.name)
+ frappe.cache().delete_key('user_groups')
diff --git a/frappe/core/page/recorder/recorder.js b/frappe/core/page/recorder/recorder.js
index 3de0e50cb2..f1f74daf71 100644
--- a/frappe/core/page/recorder/recorder.js
+++ b/frappe/core/page/recorder/recorder.js
@@ -1,7 +1,7 @@
frappe.pages['recorder'].on_page_load = function(wrapper) {
frappe.ui.make_app_page({
parent: wrapper,
- title: 'Recorder',
+ title: __('Recorder'),
single_column: true,
card_layout: true
});
diff --git a/frappe/custom/doctype/customize_form/customize_form.json b/frappe/custom/doctype/customize_form/customize_form.json
index 442b8dbb31..1807678673 100644
--- a/frappe/custom/doctype/customize_form/customize_form.json
+++ b/frappe/custom/doctype/customize_form/customize_form.json
@@ -278,6 +278,7 @@
},
{
"collapsible": 1,
+ "depends_on": "doc_type",
"fieldname": "naming_section",
"fieldtype": "Section Break",
"label": "Naming"
@@ -287,6 +288,16 @@
"fieldname": "autoname",
"fieldtype": "Data",
"label": "Auto Name"
+ },
+ {
+ "fieldname": "default_email_template",
+ "fieldtype": "Link",
+ "label": "Default Email Template",
+ "options": "Email Template"
+ },
+ {
+ "fieldname": "column_break_26",
+ "fieldtype": "Column Break"
}
],
"hide_toolbar": 1,
@@ -295,7 +306,7 @@
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
- "modified": "2021-03-22 12:27:15.462727",
+ "modified": "2021-04-29 21:21:06.476372",
"modified_by": "Administrator",
"module": "Custom",
"name": "Customize Form",
@@ -316,4 +327,4 @@
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
-}
+}
\ No newline at end of file
diff --git a/frappe/desk/page/backups/backups.css b/frappe/desk/page/backups/backups.css
index 13f093e0b1..32ccb88c37 100644
--- a/frappe/desk/page/backups/backups.css
+++ b/frappe/desk/page/backups/backups.css
@@ -5,6 +5,7 @@
.download-backup-card {
display: block;
text-decoration: none;
+ margin-bottom: var(--margin-lg);
}
.download-backup-card:hover {
diff --git a/frappe/desk/page/backups/backups.js b/frappe/desk/page/backups/backups.js
index c82407c6bd..337ad33f43 100644
--- a/frappe/desk/page/backups/backups.js
+++ b/frappe/desk/page/backups/backups.js
@@ -1,7 +1,7 @@
frappe.pages['backups'].on_page_load = function(wrapper) {
var page = frappe.ui.make_app_page({
parent: wrapper,
- title: 'Download Backups',
+ title: __('Download Backups'),
single_column: true
});
diff --git a/frappe/desk/page/translation_tool/translation_tool.js b/frappe/desk/page/translation_tool/translation_tool.js
index b3f0c032e3..13f68e647a 100644
--- a/frappe/desk/page/translation_tool/translation_tool.js
+++ b/frappe/desk/page/translation_tool/translation_tool.js
@@ -1,7 +1,7 @@
frappe.pages['translation-tool'].on_page_load = function(wrapper) {
var page = frappe.ui.make_app_page({
parent: wrapper,
- title: 'Translation Tool',
+ title: __('Translation Tool'),
single_column: true,
card_layout: true,
});
diff --git a/frappe/desk/page/user_profile/user_profile.html b/frappe/desk/page/user_profile/user_profile.html
index 911ccc702d..f134441b74 100644
--- a/frappe/desk/page/user_profile/user_profile.html
+++ b/frappe/desk/page/user_profile/user_profile.html
@@ -8,7 +8,7 @@
@@ -19,7 +19,7 @@
@@ -30,7 +30,7 @@
@@ -41,4 +41,4 @@
-
\ No newline at end of file
+
diff --git a/frappe/desk/search.py b/frappe/desk/search.py
index 6181261fc2..3c9109eca9 100644
--- a/frappe/desk/search.py
+++ b/frappe/desk/search.py
@@ -221,3 +221,37 @@ def validate_and_sanitize_search_inputs(fn, instance, args, kwargs):
return []
return fn(**kwargs)
+
+
+@frappe.whitelist()
+def get_names_for_mentions(search_term):
+ users_for_mentions = frappe.cache().get_value('users_for_mentions', get_users_for_mentions)
+ user_groups = frappe.cache().get_value('user_groups', get_user_groups)
+
+ filtered_mentions = []
+ for mention_data in users_for_mentions + user_groups:
+ if search_term.lower() not in mention_data.value.lower():
+ continue
+
+ mention_data['link'] = frappe.utils.get_url_to_form(
+ 'User Group' if mention_data.get('is_group') else 'User Profile',
+ mention_data['id']
+ )
+
+ filtered_mentions.append(mention_data)
+
+ return sorted(filtered_mentions, key=lambda d: d['value'])
+
+def get_users_for_mentions():
+ return frappe.get_all('User',
+ fields=['name as id', 'full_name as value'],
+ filters={
+ 'name': ['not in', ('Administrator', 'Guest')],
+ 'allowed_in_mentions': True,
+ 'user_type': 'System User',
+ })
+
+def get_user_groups():
+ return frappe.get_all('User Group', fields=['name as id', 'name as value'], update={
+ 'is_group': True
+ })
diff --git a/frappe/event_streaming/doctype/event_producer/event_producer.py b/frappe/event_streaming/doctype/event_producer/event_producer.py
index 3d97583549..4836276734 100644
--- a/frappe/event_streaming/doctype/event_producer/event_producer.py
+++ b/frappe/event_streaming/doctype/event_producer/event_producer.py
@@ -55,8 +55,8 @@ class EventProducer(Document):
self.reload()
def check_url(self):
- if not frappe.utils.validate_url(self.producer_url):
- frappe.throw(_('Invalid URL'))
+ valid_url_schemes = ("http", "https")
+ frappe.utils.validate_url(self.producer_url, throw=True, valid_schemes=valid_url_schemes)
# remove '/' from the end of the url like http://test_site.com/
# to prevent mismatch in get_url() results
diff --git a/frappe/model/__init__.py b/frappe/model/__init__.py
index af06696621..205b451336 100644
--- a/frappe/model/__init__.py
+++ b/frappe/model/__init__.py
@@ -71,7 +71,8 @@ numeric_fieldtypes = (
data_field_options = (
'Email',
'Name',
- 'Phone'
+ 'Phone',
+ 'URL'
)
default_fields = (
diff --git a/frappe/model/base_document.py b/frappe/model/base_document.py
index 983511f7a4..05435482bd 100644
--- a/frappe/model/base_document.py
+++ b/frappe/model/base_document.py
@@ -666,6 +666,12 @@ class BaseDocument(object):
if data_field_options == "Phone":
frappe.utils.validate_phone_number(data, throw=True)
+ if data_field_options == "URL":
+ if not data:
+ continue
+
+ frappe.utils.validate_url(data, throw=True)
+
def _validate_constants(self):
if frappe.flags.in_import or self.is_new() or self.flags.ignore_validate_constants:
return
diff --git a/frappe/oauth.py b/frappe/oauth.py
index 3287bf7520..35f047a2b6 100644
--- a/frappe/oauth.py
+++ b/frappe/oauth.py
@@ -599,9 +599,10 @@ def get_client_scopes(client_id):
def get_userinfo(user):
picture = None
frappe_server_url = get_server_url()
+ valid_url_schemes = ("http", "https", "ftp", "ftps")
if user.user_image:
- if frappe.utils.validate_url(user.user_image):
+ if frappe.utils.validate_url(user.user_image, valid_schemes=valid_url_schemes):
picture = user.user_image
else:
picture = frappe_server_url + "/" + user.user_image
diff --git a/frappe/public/js/frappe/desk.js b/frappe/public/js/frappe/desk.js
index 4bd2d53083..207483a164 100644
--- a/frappe/public/js/frappe/desk.js
+++ b/frappe/public/js/frappe/desk.js
@@ -114,8 +114,6 @@ frappe.Application = class Application {
dialog.get_close_btn().toggle(false);
});
- this.setup_user_group_listeners();
-
// listen to build errors
this.setup_build_events();
@@ -591,15 +589,6 @@ frappe.Application = class Application {
}
}
- 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);
- });
- }
-
setup_energy_point_listeners() {
frappe.realtime.on('energy_point_alert', (message) => {
frappe.show_alert(message);
@@ -609,8 +598,7 @@ frappe.Application = class Application {
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 pasted_data = frappe.utils.get_clipboard_data(e);
let doc = JSON.parse(pasted_data);
if (doc.doctype) {
e.preventDefault();
@@ -625,6 +613,7 @@ frappe.Application = class Application {
let res = frappe.model.with_doctype(doc.doctype, () => {
let newdoc = frappe.model.copy_doc(doc);
newdoc.__newname = doc.name;
+ delete doc.name;
newdoc.idx = null;
newdoc.__run_link_triggers = false;
frappe.set_route('Form', newdoc.doctype, newdoc.name);
diff --git a/frappe/public/js/frappe/form/controls/color.js b/frappe/public/js/frappe/form/controls/color.js
index 802aad371a..7e8e25fac9 100644
--- a/frappe/public/js/frappe/form/controls/color.js
+++ b/frappe/public/js/frappe/form/controls/color.js
@@ -1,11 +1,13 @@
import Picker from '../../color_picker/color_picker';
frappe.ui.form.ControlColor = class ControlColor extends frappe.ui.form.ControlData {
- make_input () {
+ make_input() {
+ this.df.placeholder = this.df.placeholder || __('Choose a color');
super.make_input();
this.make_color_input();
}
- make_color_input () {
+
+ make_color_input() {
let picker_wrapper = $('');
this.picker = new Picker({
parent: picker_wrapper[0],
@@ -48,7 +50,16 @@ frappe.ui.form.ControlColor = class ControlColor extends frappe.ui.form.ControlD
$(window).off('hashchange.color-popover');
});
- this.$wrapper.find('.control-input').on('click', (e) => {
+ this.picker.on_change = (color) => {
+ this.set_value(color);
+ };
+
+ if (!this.selected_color) {
+ this.selected_color = $(`
`);
+ this.selected_color.insertAfter(this.$input);
+ }
+
+ this.$wrapper.find('.selected-color').parent().on('click', (e) => {
this.$wrapper.popover('toggle');
if (!this.get_color()) {
this.$input.val('');
@@ -63,16 +74,8 @@ frappe.ui.form.ControlColor = class ControlColor extends frappe.ui.form.ControlD
this.$wrapper.popover('hide');
});
});
-
- this.picker.on_change = (color) => {
- this.set_value(color);
- };
-
- if (!this.selected_color) {
- this.selected_color = $(`
`);
- this.selected_color.insertAfter(this.$input);
- }
}
+
refresh() {
super.refresh();
let color = this.get_color();
@@ -81,19 +84,21 @@ frappe.ui.form.ControlColor = class ControlColor extends frappe.ui.form.ControlD
this.picker.refresh();
}
}
+
set_formatted_input(value) {
super.set_formatted_input(value);
-
- this.$input.val(value || __('Choose a color'));
+ this.$input.val(value);
this.selected_color.css({
"background-color": value || 'transparent',
});
this.selected_color.toggleClass('no-value', !value);
}
+
get_color() {
return this.validate(this.get_value());
}
- validate (value) {
+
+ validate(value) {
if (value === '') {
return '';
}
diff --git a/frappe/public/js/frappe/form/controls/comment.js b/frappe/public/js/frappe/form/controls/comment.js
index 6d1664747f..7c10b61366 100644
--- a/frappe/public/js/frappe/form/controls/comment.js
+++ b/frappe/public/js/frappe/form/controls/comment.js
@@ -78,35 +78,25 @@ frappe.ui.form.ControlComment = class ControlComment extends frappe.ui.form.Cont
}
get_mention_options() {
- if (!(this.mentions && this.mentions.length)) {
+ if (!this.enable_mentions) {
return null;
}
-
- const at_values = this.mentions.slice();
-
+ let me = this;
return {
allowedChars: /^[A-Za-z0-9_]*$/,
mentionDenotationChars: ["@"],
isolateCharacter: true,
- source: function (searchTerm, renderList, mentionChar) {
- let values;
-
- if (mentionChar === "@") {
- values = at_values;
- }
-
- if (searchTerm.length === 0) {
- renderList(values, searchTerm);
- } else {
- const matches = [];
- for (let i = 0; i < values.length; i++) {
- if (~values[i].value.toLowerCase().indexOf(searchTerm.toLowerCase())) {
- matches.push(values[i]);
- }
- }
- renderList(matches, searchTerm);
- }
- },
+ source: frappe.utils.debounce(async function(search_term, renderList) {
+ let method = me.mention_search_method || 'frappe.desk.search.get_names_for_mentions';
+ let values = await frappe.xcall(method, {
+ search_term
+ });
+ renderList(values, search_term);
+ }, 300),
+ renderItem(item) {
+ let value = item.value;
+ return `${value} ${item.is_group ? frappe.utils.icon('users') : ''}`;
+ }
};
}
diff --git a/frappe/public/js/frappe/form/controls/currency.js b/frappe/public/js/frappe/form/controls/currency.js
index aa4df957db..0536a7403f 100644
--- a/frappe/public/js/frappe/form/controls/currency.js
+++ b/frappe/public/js/frappe/form/controls/currency.js
@@ -1,7 +1,7 @@
frappe.ui.form.ControlCurrency = class ControlCurrency extends frappe.ui.form.ControlFloat {
format_for_input(value) {
var formatted_value = format_number(value, this.get_number_format(), this.get_precision());
- return isNaN(parseFloat(value)) ? "" : formatted_value;
+ return isNaN(Number(value)) ? "" : formatted_value;
}
get_precision() {
diff --git a/frappe/public/js/frappe/form/controls/data.js b/frappe/public/js/frappe/form/controls/data.js
index dda0761694..772c8fb804 100644
--- a/frappe/public/js/frappe/form/controls/data.js
+++ b/frappe/public/js/frappe/form/controls/data.js
@@ -18,12 +18,99 @@ frappe.ui.form.ControlData = class ControlData extends frappe.ui.form.ControlInp
this.$input.attr("maxlength", this.df.length || 140);
}
+ this.$input.on('paste', (e) => {
+ let pasted_data = frappe.utils.get_clipboard_data(e);
+ let maxlength = this.$input.attr('maxlength');
+ if (maxlength && pasted_data.length > maxlength) {
+ let warning_message = __('The value you pasted was {0} characters long. Max allowed characters is {1}.', [
+ cstr(pasted_data.length).bold(),
+ cstr(maxlength).bold()
+ ]);
+
+ // Only show edit link to users who can update the doctype
+ if (this.frm && frappe.model.can_write(this.frm.doctype)) {
+ let doctype_edit_link = null;
+ if (this.frm.meta.custom) {
+ doctype_edit_link = frappe.utils.get_form_link(
+ 'DocType',
+ this.frm.doctype, true,
+ __('this form')
+ );
+ } else {
+ doctype_edit_link = frappe.utils.get_form_link('Customize Form', 'Customize Form', true, null, {
+ doc_type: this.frm.doctype
+ });
+ }
+ let edit_note = __('{0}: You can increase the limit for the field if required via {1}', [
+ __('Note').bold(),
+ doctype_edit_link
+ ]);
+ warning_message += `
${edit_note}`;
+ }
+
+ frappe.msgprint({
+ message: warning_message,
+ indicator: 'orange',
+ title: __('Data Clipped')
+ });
+ }
+ });
+
this.set_input_attributes();
this.input = this.$input.get(0);
this.has_input = true;
this.bind_change_event();
this.setup_autoname_check();
+
+ if (this.df.options == 'URL') {
+ this.setup_url_field();
+ }
}
+
+ setup_url_field() {
+ this.$wrapper.find('.control-input').append(
+ `
+
+ ${frappe.utils.icon('link-url', 'sm')}
+
+ `
+ );
+
+ this.$link = this.$wrapper.find('.link-btn');
+ this.$link_open = this.$link.find('.btn-open');
+
+ this.$input.on("focus", () => {
+ setTimeout(() => {
+ let inputValue = this.get_input_value();
+
+ if (inputValue && validate_url(inputValue)) {
+ this.$link.toggle(true);
+ this.$link_open.attr('href', this.get_input_value());
+ }
+ }, 500);
+ });
+
+
+ this.$input.bind("input", () => {
+ let inputValue = this.get_input_value();
+
+ if (inputValue && validate_url(inputValue)) {
+ this.$link.toggle(true);
+ this.$link_open.attr('href', this.get_input_value());
+ } else {
+ this.$link.toggle(false);
+ }
+ });
+
+ this.$input.on("blur", () => {
+ // if this disappears immediately, the user's click
+ // does not register, hence timeout
+ setTimeout(() => {
+ this.$link.toggle(false);
+ }, 500);
+ });
+ }
+
bind_change_event() {
const change_handler = e => {
if (this.change) this.change(e);
@@ -126,6 +213,9 @@ frappe.ui.form.ControlData = class ControlData extends frappe.ui.form.ControlInp
this.df.invalid = email_invalid;
return v;
}
+ } else if (this.df.options == 'URL') {
+ this.df.invalid = !validate_url(v);
+ return v;
} else {
return v;
}
diff --git a/frappe/public/js/frappe/form/controls/float.js b/frappe/public/js/frappe/form/controls/float.js
index f6a40bd6f8..89f8f23cc5 100644
--- a/frappe/public/js/frappe/form/controls/float.js
+++ b/frappe/public/js/frappe/form/controls/float.js
@@ -10,7 +10,7 @@ frappe.ui.form.ControlFloat = class ControlFloat extends frappe.ui.form.ControlI
number_format = this.get_number_format();
}
var formatted_value = format_number(value, number_format, this.get_precision());
- return isNaN(parseFloat(value)) ? "" : formatted_value;
+ return isNaN(Number(value)) ? "" : formatted_value;
}
get_number_format() {
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 d6907158f9..4a3c2d2eba 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
@@ -12,6 +12,7 @@ class MentionBlot extends Embed {
denotationChar.innerHTML = data.denotationChar;
node.appendChild(denotationChar);
node.innerHTML += data.value;
+ node.innerHTML += `${data.isGroup === 'true' ? frappe.utils.icon('users') : ''}`;
node.dataset.id = data.id;
node.dataset.value = data.value;
node.dataset.denotationChar = data.denotationChar;
diff --git a/frappe/public/js/frappe/form/controls/table.js b/frappe/public/js/frappe/form/controls/table.js
index 92c824f1e5..35849a71a5 100644
--- a/frappe/public/js/frappe/form/controls/table.js
+++ b/frappe/public/js/frappe/form/controls/table.js
@@ -26,8 +26,7 @@ frappe.ui.form.ControlTable = class ControlTable extends frappe.ui.form.Control
const row_docname = $(e.target).closest('.grid-row').data('name');
const in_grid_form = $(e.target).closest('.form-in-grid').length;
- let clipboard_data = e.clipboardData || window.clipboardData || e.originalEvent.clipboardData;
- let pasted_data = clipboard_data.getData('Text');
+ let pasted_data = frappe.utils.get_clipboard_data(e);
if (!pasted_data || in_grid_form) return;
diff --git a/frappe/public/js/frappe/form/footer/footer.js b/frappe/public/js/frappe/form/footer/footer.js
index aa29eea248..d6dfc227f0 100644
--- a/frappe/public/js/frappe/form/footer/footer.js
+++ b/frappe/public/js/frappe/form/footer/footer.js
@@ -24,7 +24,7 @@ frappe.ui.form.Footer = class FormFooter {
parent: this.wrapper.find(".comment-box"),
render_input: true,
only_input: true,
- mentions: frappe.utils.get_names_for_mentions(),
+ enable_mentions: true,
df: {
fieldtype: 'Comment',
fieldname: 'comment'
diff --git a/frappe/public/js/frappe/form/footer/form_timeline.js b/frappe/public/js/frappe/form/footer/form_timeline.js
index bd64c504ca..ab83ed2f71 100644
--- a/frappe/public/js/frappe/form/footer/form_timeline.js
+++ b/frappe/public/js/frappe/form/footer/form_timeline.js
@@ -492,7 +492,7 @@ class FormTimeline extends BaseTimeline {
fieldname: 'comment',
label: 'Comment'
},
- mentions: frappe.utils.get_names_for_mentions(),
+ enable_mentions: true,
render_input: true,
only_input: true,
no_wrapper: true
diff --git a/frappe/public/js/frappe/form/formatters.js b/frappe/public/js/frappe/form/formatters.js
index 0b396ad35e..89c34ed80c 100644
--- a/frappe/public/js/frappe/form/formatters.js
+++ b/frappe/public/js/frappe/form/formatters.js
@@ -15,7 +15,10 @@ frappe.form.formatters = {
return "
" + value + "
";
}
},
- Data: function(value) {
+ Data: function(value, df) {
+ if (df && df.options == "URL") {
+ return `
${value}`;
+ }
return value==null ? "" : value;
},
Select: function(value) {
@@ -156,7 +159,7 @@ frappe.form.formatters = {
return value || "";
},
DateRange: function(value) {
- if($.isArray(value)) {
+ if (Array.isArray(value)) {
return __("{0} to {1}", [frappe.datetime.str_to_user(value[0]), frappe.datetime.str_to_user(value[1])]);
} else {
return value || "";
@@ -292,12 +295,12 @@ frappe.form.formatters = {
return formatted_values.join(', ');
},
Color: (value) => {
- return `
` : '';
}
-}
+};
frappe.form.get_formatter = function(fieldtype) {
if(!fieldtype)
diff --git a/frappe/public/js/frappe/form/grid_row.js b/frappe/public/js/frappe/form/grid_row.js
index 9a689fabf4..f6da88df57 100644
--- a/frappe/public/js/frappe/form/grid_row.js
+++ b/frappe/public/js/frappe/form/grid_row.js
@@ -422,7 +422,7 @@ export default class GridRow {
field.$input
.addClass('input-sm')
.attr('data-col-idx', column.column_index)
- .attr('placeholder', __(df.label));
+ .attr('placeholder', __(df.placeholder || df.label));
// flag list input
if (this.columns_list && this.columns_list.slice(-1)[0]===column) {
field.$input.attr('data-last-input', 1);
diff --git a/frappe/public/js/frappe/form/toolbar.js b/frappe/public/js/frappe/form/toolbar.js
index 22787b70c1..c93466e024 100644
--- a/frappe/public/js/frappe/form/toolbar.js
+++ b/frappe/public/js/frappe/form/toolbar.js
@@ -32,7 +32,7 @@ frappe.ui.form.Toolbar = class Toolbar {
}
set_title() {
if (this.frm.is_new()) {
- var title = __('New {0}', [this.frm.meta.name]);
+ var title = __('New {0}', [__(this.frm.meta.name)]);
} else if (this.frm.meta.title_field) {
let title_field = (this.frm.doc[this.frm.meta.title_field] || "").toString().trim();
var title = strip_html(title_field || this.frm.docname);
@@ -551,7 +551,7 @@ frappe.ui.form.Toolbar = class Toolbar {
let fields = this.frm.fields
.filter(visible_fields_filter)
- .map(f => ({ label: f.df.label, value: f.df.fieldname }));
+ .map(f => ({ label: __(f.df.label), value: f.df.fieldname }));
let dialog = new frappe.ui.Dialog({
title: __('Jump to field'),
diff --git a/frappe/public/js/frappe/list/list_view_select.js b/frappe/public/js/frappe/list/list_view_select.js
index 9607be6e90..c89815d200 100644
--- a/frappe/public/js/frappe/list/list_view_select.js
+++ b/frappe/public/js/frappe/list/list_view_select.js
@@ -150,13 +150,13 @@ frappe.views.ListViewSelect = class ListViewSelect {
const views_wrapper = this.sidebar.sidebar.find(".views-section");
views_wrapper.find(".sidebar-label").html(`${__(view)}`);
const $dropdown = views_wrapper.find(".views-dropdown");
-
- let placeholder = `Select ${view}`;
+
+ let placeholder = `${__("Select {0}", [__(view)])}`;
let html = ``;
if (!items || !items.length) {
html = `
- ${__("No {} Found", [view])}
+ ${__("No {0} Found", [__(view)])}
`;
} else {
const page_name = this.get_page_name();
diff --git a/frappe/public/js/frappe/recorder/RecorderDetail.vue b/frappe/public/js/frappe/recorder/RecorderDetail.vue
index 57e63a0233..d17a8f0ec4 100644
--- a/frappe/public/js/frappe/recorder/RecorderDetail.vue
+++ b/frappe/public/js/frappe/recorder/RecorderDetail.vue
@@ -5,7 +5,7 @@
@@ -71,12 +71,12 @@
-
Recorder is Inactive
-
+
{{ __("Recorder is Inactive") }}
+
-
No Requests found
-
Go make some noise
+
{{ __("No Requests found") }}
+
{{ __("Go make some noise") }}
@@ -108,12 +108,12 @@ export default {
return {
requests: [],
columns: [
- {label: "Path", slug: "path"},
- {label: "Duration (ms)", slug: "duration", sortable: true, number: true},
- {label: "Time in Queries (ms)", slug: "time_queries", sortable: true, number: true},
- {label: "Queries", slug: "queries", sortable: true, number: true},
- {label: "Method", slug: "method"},
- {label: "Time", slug: "time", sortable: true},
+ {label: __("Path"), slug: "path"},
+ {label: __("Duration (ms)"), slug: "duration", sortable: true, number: true},
+ {label: __("Time in Queries (ms)"), slug: "time_queries", sortable: true, number: true},
+ {label: __("Queries"), slug: "queries", sortable: true, number: true},
+ {label: __("Method"), slug: "method"},
+ {label: __("Time"), slug: "time", sortable: true},
],
query: {
sort: "duration",
@@ -140,7 +140,7 @@ export default {
mounted() {
this.fetch_status();
this.refresh();
- this.$root.page.set_secondary_action("Clear", () => {
+ this.$root.page.set_secondary_action(__("Clear"), () => {
frappe.set_route("recorder");
this.clear();
});
@@ -151,11 +151,11 @@ export default {
const current_page = this.query.pagination.page;
const total_pages = this.query.pagination.total;
return [{
- label: "First",
+ label: __("First"),
number: 1,
status: (current_page == 1) ? "disabled" : "",
},{
- label: "Previous",
+ label: __("Previous"),
number: Math.max(current_page - 1, 1),
status: (current_page == 1) ? "disabled" : "",
}, {
@@ -163,11 +163,11 @@ export default {
number: current_page,
status: "btn-info",
}, {
- label: "Next",
+ label: __("Next"),
number: Math.min(current_page + 1, total_pages),
status: (current_page == total_pages) ? "disabled" : "",
}, {
- label: "Last",
+ label: __("Last"),
number: total_pages,
status: (current_page == total_pages) ? "disabled" : "",
}];
@@ -230,11 +230,11 @@ export default {
},
update_buttons: function() {
if(this.status.status == "Active") {
- this.$root.page.set_primary_action("Stop", () => {
+ this.$root.page.set_primary_action(__("Stop"), () => {
this.stop();
});
} else {
- this.$root.page.set_primary_action("Start", () => {
+ this.$root.page.set_primary_action(__("Start"), () => {
this.start();
});
}
diff --git a/frappe/public/js/frappe/recorder/RequestDetail.vue b/frappe/public/js/frappe/recorder/RequestDetail.vue
index cc056686d5..2e995bca39 100644
--- a/frappe/public/js/frappe/recorder/RequestDetail.vue
+++ b/frappe/public/js/frappe/recorder/RequestDetail.vue
@@ -16,7 +16,7 @@