@@ -481,6 +481,7 @@ def clear_cache(user=None, doctype=None): | |||
:param user: If user is given, only user cache is cleared. | |||
:param doctype: If doctype is given, only DocType cache is cleared.""" | |||
import frappe.sessions | |||
from frappe.core.doctype.domain_settings.domain_settings import clear_domain_cache | |||
if doctype: | |||
import frappe.model.meta | |||
frappe.model.meta.clear_cache(doctype) | |||
@@ -492,7 +493,7 @@ def clear_cache(user=None, doctype=None): | |||
frappe.sessions.clear_cache() | |||
translate.clear_cache() | |||
reset_metadata_version() | |||
clear_domainification_cache() | |||
clear_domain_cache() | |||
local.cache = {} | |||
local.new_doc_templates = {} | |||
@@ -1358,37 +1359,11 @@ def safe_eval(code, eval_globals=None, eval_locals=None): | |||
return eval(code, eval_globals, eval_locals) | |||
def get_active_domains(): | |||
""" get the domains set in the Domain Settings as active domain """ | |||
active_domains = cache().hget("domains", "active_domains") or None | |||
if active_domains is None: | |||
domains = get_all("Has Domain", filters={ "parent": "Domain Settings" }, | |||
fields=["domain"], distinct=True) | |||
active_domains = [row.get("domain") for row in domains] | |||
active_domains.append("") | |||
cache().hset("domains", "active_domains", active_domains) | |||
return active_domains | |||
def get_active_modules(): | |||
""" get the active modules from Module Def""" | |||
active_modules = cache().hget("modules", "active_modules") or None | |||
if active_modules is None: | |||
domains = get_active_domains() | |||
modules = get_all("Module Def", filters={"restrict_to_domain": ("in", domains)}) | |||
active_modules = [module.name for module in modules] | |||
cache().hset("modules", "active_modules", active_modules) | |||
return active_modules | |||
def clear_domainification_cache(): | |||
_cache = cache() | |||
_cache.delete_key("domains", "active_domains") | |||
_cache.delete_key("modules", "active_modules") | |||
def get_system_settings(key): | |||
if not local.system_settings.has_key(key): | |||
local.system_settings.update({key: db.get_single_value('System Settings', key)}) | |||
return local.system_settings.get(key) | |||
def get_active_domains(): | |||
from frappe.core.doctype.domain_settings.domain_settings import get_active_domains | |||
return get_active_domains() |
@@ -360,9 +360,10 @@ def serve(context, port=None, profile=False, sites_path='.', site=None): | |||
frappe.app.serve(port=port, profile=profile, site=site, sites_path='.') | |||
@click.command('request') | |||
@click.argument('args') | |||
@click.option('--args', help='arguments like `?cmd=test&key=value` or `/api/request/method?..`') | |||
@click.option('--path', help='path to request JSON') | |||
@pass_context | |||
def request(context, args): | |||
def request(context, args=None, path=None): | |||
"Run a request as an admin" | |||
import frappe.handler | |||
import frappe.api | |||
@@ -370,13 +371,19 @@ def request(context, args): | |||
try: | |||
frappe.init(site=site) | |||
frappe.connect() | |||
if "?" in args: | |||
frappe.local.form_dict = frappe._dict([a.split("=") for a in args.split("?")[-1].split("&")]) | |||
else: | |||
frappe.local.form_dict = frappe._dict() | |||
if args.startswith("/api/method"): | |||
frappe.local.form_dict.cmd = args.split("?")[0].split("/")[-1] | |||
if args: | |||
if "?" in args: | |||
frappe.local.form_dict = frappe._dict([a.split("=") for a in args.split("?")[-1].split("&")]) | |||
else: | |||
frappe.local.form_dict = frappe._dict() | |||
if args.startswith("/api/method"): | |||
frappe.local.form_dict.cmd = args.split("?")[0].split("/")[-1] | |||
elif path: | |||
with open(os.path.join('..', path), 'r') as f: | |||
args = json.loads(f.read()) | |||
frappe.local.form_dict = frappe._dict(args) | |||
frappe.handler.execute_cmd(frappe.form_dict.cmd) | |||
@@ -22,6 +22,8 @@ class Contact(Document): | |||
break | |||
def validate(self): | |||
if self.email_id: | |||
self.email_id = self.email_id.strip() | |||
self.set_user() | |||
if self.email_id and not self.image: | |||
self.image = has_gravatar(self.email_id) | |||
@@ -407,7 +407,7 @@ def validate_fields(meta): | |||
validate_column_name(fieldname) | |||
def check_unique_fieldname(fieldname): | |||
duplicates = filter(None, map(lambda df: df.fieldname==fieldname and str(df.idx) or None, fields)) | |||
duplicates = list(filter(None, map(lambda df: df.fieldname==fieldname and str(df.idx) or None, fields))) | |||
if len(duplicates) > 1: | |||
frappe.throw(_("Fieldname {0} appears multiple times in rows {1}").format(fieldname, ", ".join(duplicates))) | |||
@@ -8,4 +8,31 @@ from frappe.model.document import Document | |||
class DomainSettings(Document): | |||
def on_update(self): | |||
frappe.clear_domainification_cache() | |||
clear_domain_cache() | |||
def get_active_domains(): | |||
""" get the domains set in the Domain Settings as active domain """ | |||
def _get_active_domains(): | |||
domains = frappe.get_all("Has Domain", filters={ "parent": "Domain Settings" }, | |||
fields=["domain"], distinct=True) | |||
active_domains = [row.get("domain") for row in domains] | |||
active_domains.append("") | |||
return active_domains | |||
return frappe.cache().get_value("active_domains", _get_active_domains) | |||
def get_active_modules(): | |||
""" get the active modules from Module Def""" | |||
def _get_active_modules(): | |||
active_modules = [] | |||
active_domains = get_active_domains() | |||
for m in frappe.get_all("Module Def", fields=['name', 'restrict_to_domain']): | |||
if (not m.restrict_to_domain) or (m.restrict_to_domain in active_domains): | |||
active_modules.append(m.name) | |||
return active_modules | |||
return frappe.cache().get_value('active_modules', _get_active_modules) | |||
def clear_domain_cache(): | |||
frappe.cache().delete_key(['active_domains', 'active_modules']) |
@@ -10,6 +10,7 @@ class ModuleDef(Document): | |||
def on_update(self): | |||
"""If in `developer_mode`, create folder for module and | |||
add in `modules.txt` of app if missing.""" | |||
frappe.clear_cache() | |||
if frappe.conf.get("developer_mode"): | |||
self.create_modules_folder() | |||
self.add_to_modules_txt() | |||
@@ -0,0 +1,23 @@ | |||
/* eslint-disable */ | |||
// rename this file from _test_[name] to test_[name] to activate | |||
// and remove above this line | |||
QUnit.test("test: User", function (assert) { | |||
let done = assert.async(); | |||
// number of asserts | |||
assert.expect(1); | |||
frappe.run_serially([ | |||
// insert a new User | |||
() => frappe.tests.make('User', [ | |||
// values to be set | |||
{key: 'value'} | |||
]), | |||
() => { | |||
assert.equal(cur_frm.doc.key, 'value'); | |||
}, | |||
() => done() | |||
]); | |||
}); |
@@ -60,11 +60,11 @@ frappe.ui.form.on('User', { | |||
"user": doc.name | |||
}; | |||
frappe.set_route('List', 'User Permission'); | |||
}, null, "btn-default") | |||
}, __("Permissions")) | |||
frm.add_custom_button(__('View Permitted Documents'), | |||
() => frappe.set_route('query-report', 'Permitted Documents For User', | |||
{user: frm.doc.name})); | |||
{user: frm.doc.name}), __("Permissions")); | |||
frm.toggle_display(['sb1', 'sb3', 'modules_access'], true); | |||
} | |||
@@ -76,7 +76,7 @@ frappe.ui.form.on('User', { | |||
"user": frm.doc.name | |||
} | |||
}) | |||
}) | |||
}, __("Password")); | |||
frm.add_custom_button(__("Reset OTP Secret"), function() { | |||
frappe.call({ | |||
@@ -85,7 +85,7 @@ frappe.ui.form.on('User', { | |||
"user": frm.doc.name | |||
} | |||
}) | |||
}) | |||
}, __("Password")); | |||
frm.trigger('enabled'); | |||
@@ -1043,6 +1043,36 @@ | |||
"set_only_once": 0, | |||
"unique": 0 | |||
}, | |||
{ | |||
"allow_bulk_edit": 0, | |||
"allow_on_submit": 0, | |||
"bold": 0, | |||
"collapsible": 0, | |||
"columns": 0, | |||
"fieldname": "send_me_a_copy", | |||
"fieldtype": "Check", | |||
"hidden": 0, | |||
"ignore_user_permissions": 0, | |||
"ignore_xss_filter": 0, | |||
"in_filter": 0, | |||
"in_global_search": 0, | |||
"in_list_view": 0, | |||
"in_standard_filter": 0, | |||
"label": "Send Me A Copy of Outgoing Emails", | |||
"length": 0, | |||
"no_copy": 0, | |||
"permlevel": 0, | |||
"precision": "", | |||
"print_hide": 0, | |||
"print_hide_if_no_value": 0, | |||
"read_only": 0, | |||
"remember_last_selected_value": 0, | |||
"report_hide": 0, | |||
"reqd": 0, | |||
"search_index": 0, | |||
"set_only_once": 0, | |||
"unique": 0 | |||
}, | |||
{ | |||
"allow_bulk_edit": 0, | |||
"allow_on_submit": 0, | |||
@@ -1971,7 +2001,7 @@ | |||
"istable": 0, | |||
"max_attachments": 5, | |||
"menu_index": 0, | |||
"modified": "2017-07-07 17:18:14.047969", | |||
"modified": "2017-08-23 10:34:11.944298", | |||
"modified_by": "Administrator", | |||
"module": "Core", | |||
"name": "User", | |||
@@ -952,7 +952,7 @@ def send_token_via_email(tmp_id,token=None): | |||
delayed=False, retry=3) | |||
return True | |||
@frappe.whitelist(allow_guest=True) | |||
def reset_otp_secret(user): | |||
otp_issuer = frappe.db.get_value('System Settings', 'System Settings', 'otp_issuer_name') | |||
@@ -964,7 +964,7 @@ def reset_otp_secret(user): | |||
'recipients':user_email, 'sender':None, 'subject':'OTP Secret Reset - {}'.format(otp_issuer or "Frappe Framework"), | |||
'message':'<p>Your OTP secret on {} has been reset. If you did not perform this reset and did not request it, please contact your System Administrator immediately.</p>'.format(otp_issuer or "Frappe Framework"), | |||
'delayed':False, | |||
'retry':3 | |||
'retry':3 | |||
} | |||
enqueue(method=frappe.sendmail, queue='short', timeout=300, event=None, async=True, job_name=None, now=False, **email_args) | |||
return frappe.msgprint(_("OTP Secret has been reset. Re-registration will be required on next login.")) | |||
@@ -46,13 +46,13 @@ frappe.PermissionEngine = Class.extend({ | |||
var me = this; | |||
this.doctype_select | |||
= this.wrapper.page.add_select(__("Document Types"), | |||
[{value: "", label: __("Select Document Type")+"..."}].concat(this.options.doctypes.sort())) | |||
[{value: "", label: __("Select Document Type")+"..."}].concat(this.options.doctypes)) | |||
.change(function() { | |||
frappe.set_route("permission-manager", $(this).val()); | |||
}); | |||
this.role_select | |||
= this.wrapper.page.add_select(__("Roles"), | |||
[__("Select Role")+"..."].concat(this.options.roles.sort())) | |||
[__("Select Role")+"..."].concat(this.options.roles)) | |||
.change(function() { | |||
me.refresh(); | |||
}); | |||
@@ -35,9 +35,12 @@ def get_roles_and_doctypes(): | |||
"restrict_to_domain": ("in", active_domains) | |||
}, fields=["name"]) | |||
doctypes_list = [ {"label":_(d.get("name")), "value":d.get("name")} for d in doctypes] | |||
roles_list = [ {"label":_(d.get("name")), "value":d.get("name")} for d in roles] | |||
return { | |||
"doctypes": [d.get("name") for d in doctypes], | |||
"roles": [d.get("name") for d in roles] | |||
"doctypes": sorted(doctypes_list, key=lambda d: d['label']), | |||
"roles": sorted(roles_list, key=lambda d: d['label']) | |||
} | |||
@frappe.whitelist() | |||
@@ -10,7 +10,7 @@ from frappe.geo.country_info import get_country_info | |||
from frappe.utils.file_manager import save_file | |||
from frappe.utils.password import update_password | |||
from werkzeug.useragents import UserAgent | |||
import install_fixtures | |||
from . import install_fixtures | |||
from six import string_types | |||
@frappe.whitelist() | |||
@@ -10,7 +10,7 @@ import frappe.permissions | |||
import MySQLdb | |||
from frappe.model.db_query import DatabaseQuery | |||
from frappe import _ | |||
from six import text_type, string_types | |||
from six import text_type, string_types, StringIO | |||
@frappe.whitelist() | |||
def get(): | |||
@@ -146,13 +146,14 @@ def export_query(): | |||
# convert to csv | |||
import csv | |||
from six import StringIO | |||
from frappe.utils.xlsxutils import handle_html | |||
f = StringIO() | |||
writer = csv.writer(f) | |||
for r in data: | |||
# encode only unicode type strings and not int, floats etc. | |||
writer.writerow(map(lambda v: isinstance(v, text_type) and v.encode('utf-8') or v, r)) | |||
writer.writerow(map(lambda v: isinstance(v, string_types) and | |||
handle_html(v.encode('utf-8')) or v, r)) | |||
f.seek(0) | |||
frappe.response['result'] = text_type(f.read(), 'utf-8') | |||
@@ -27,6 +27,7 @@ app_include_js = [ | |||
"assets/js/desk.min.js", | |||
"assets/js/list.min.js", | |||
"assets/js/form.min.js", | |||
"assets/js/control.min.js", | |||
"assets/js/report.min.js", | |||
"assets/js/d3.min.js", | |||
"assets/frappe/js/frappe/toolbar.js" | |||
@@ -22,16 +22,74 @@ | |||
"website/js/website.js", | |||
"public/js/frappe/misc/rating_icons.html" | |||
], | |||
"js/control.min.js": [ | |||
"public/js/frappe/form/controls/base_control.js", | |||
"public/js/frappe/form/controls/base_input.js", | |||
"public/js/frappe/form/controls/data.js", | |||
"public/js/frappe/form/controls/int.js", | |||
"public/js/frappe/form/controls/float.js", | |||
"public/js/frappe/form/controls/currency.js", | |||
"public/js/frappe/form/controls/date.js", | |||
"public/js/frappe/form/controls/time.js", | |||
"public/js/frappe/form/controls/datetime.js", | |||
"public/js/frappe/form/controls/date_range.js", | |||
"public/js/frappe/form/controls/select.js", | |||
"public/js/frappe/form/controls/link.js", | |||
"public/js/frappe/form/controls/dynamic_link.js", | |||
"public/js/frappe/form/controls/text.js", | |||
"public/js/frappe/form/controls/code.js", | |||
"public/js/frappe/form/controls/text_editor.js", | |||
"public/js/frappe/form/controls/check.js", | |||
"public/js/frappe/form/controls/image.js", | |||
"public/js/frappe/form/controls/attach.js", | |||
"public/js/frappe/form/controls/attach_image.js", | |||
"public/js/frappe/form/controls/table.js", | |||
"public/js/frappe/form/controls/color.js", | |||
"public/js/frappe/form/controls/signature.js", | |||
"public/js/frappe/form/controls/password.js", | |||
"public/js/frappe/form/controls/read_only.js", | |||
"public/js/frappe/form/controls/button.js", | |||
"public/js/frappe/form/controls/html.js", | |||
"public/js/frappe/form/controls/heading.js" | |||
], | |||
"js/dialog.min.js": [ | |||
"public/js/frappe/dom.js", | |||
"public/js/frappe/ui/modal.html", | |||
"public/js/frappe/form/formatters.js", | |||
"public/js/frappe/form/layout.js", | |||
"public/js/frappe/ui/field_group.js", | |||
"public/js/frappe/form/control.js", | |||
"public/js/frappe/form/link_selector.js", | |||
"public/js/frappe/form/multi_select_dialog.js", | |||
"public/js/frappe/ui/dialog.js" | |||
"public/js/frappe/ui/dialog.js", | |||
"public/js/frappe/form/controls/base_control.js", | |||
"public/js/frappe/form/controls/base_input.js", | |||
"public/js/frappe/form/controls/data.js", | |||
"public/js/frappe/form/controls/int.js", | |||
"public/js/frappe/form/controls/float.js", | |||
"public/js/frappe/form/controls/currency.js", | |||
"public/js/frappe/form/controls/date.js", | |||
"public/js/frappe/form/controls/time.js", | |||
"public/js/frappe/form/controls/datetime.js", | |||
"public/js/frappe/form/controls/date_range.js", | |||
"public/js/frappe/form/controls/select.js", | |||
"public/js/frappe/form/controls/link.js", | |||
"public/js/frappe/form/controls/dynamic_link.js", | |||
"public/js/frappe/form/controls/text.js", | |||
"public/js/frappe/form/controls/code.js", | |||
"public/js/frappe/form/controls/text_editor.js", | |||
"public/js/frappe/form/controls/check.js", | |||
"public/js/frappe/form/controls/image.js", | |||
"public/js/frappe/form/controls/attach.js", | |||
"public/js/frappe/form/controls/attach_image.js", | |||
"public/js/frappe/form/controls/table.js", | |||
"public/js/frappe/form/controls/color.js", | |||
"public/js/frappe/form/controls/signature.js", | |||
"public/js/frappe/form/controls/password.js", | |||
"public/js/frappe/form/controls/read_only.js", | |||
"public/js/frappe/form/controls/button.js", | |||
"public/js/frappe/form/controls/html.js", | |||
"public/js/frappe/form/controls/heading.js" | |||
], | |||
"css/desk.min.css": [ | |||
"public/js/lib/datepicker/datepicker.min.css", | |||
@@ -108,7 +166,6 @@ | |||
"public/js/frappe/ui/iconbar.js", | |||
"public/js/frappe/form/layout.js", | |||
"public/js/frappe/ui/field_group.js", | |||
"public/js/frappe/form/control.js", | |||
"public/js/frappe/form/link_selector.js", | |||
"public/js/frappe/form/multi_select_dialog.js", | |||
"public/js/frappe/ui/dialog.js", | |||
@@ -289,6 +346,7 @@ | |||
"public/js/frappe/views/reports/query_report.js", | |||
"public/js/frappe/views/reports/grid_report.js", | |||
"public/js/frappe/views/reports/print_grid.html", | |||
"public/js/frappe/views/reports/print_tree.html", | |||
"public/js/lib/slickgrid/jquery.event.drag.js", | |||
"public/js/lib/slickgrid/plugins/slick.cellrangedecorator.js", | |||
@@ -157,6 +157,12 @@ hr { | |||
.indicator.indicator-yellow { | |||
background-color: #FEEF72; | |||
} | |||
.screenshot { | |||
box-shadow: 0px 3px 6px rgba(0, 0, 0, 0.1); | |||
border: 1px solid #d1d8dd; | |||
margin: 8px 0; | |||
max-width: 100%; | |||
} | |||
/* auto email report */ | |||
.report-title { | |||
margin-bottom: 20px; | |||
@@ -224,7 +224,7 @@ frappe.get_modal = function(title, content) { | |||
(function($) { | |||
$.fn.add_options = function(options_list) { | |||
// create options | |||
for(var i=0; i<options_list.length; i++) { | |||
for(var i=0, j=options_list.length; i<j; i++) { | |||
var v = options_list[i]; | |||
if (is_null(v)) { | |||
var value = null; | |||
@@ -0,0 +1,187 @@ | |||
frappe.ui.form.ControlAttach = frappe.ui.form.ControlData.extend({ | |||
make_input: function() { | |||
var me = this; | |||
this.$input = $('<button class="btn btn-default btn-sm btn-attach">') | |||
.html(__("Attach")) | |||
.prependTo(me.input_area) | |||
.on("click", function() { | |||
me.onclick(); | |||
}); | |||
this.$value = $('<div style="margin-top: 5px;">\ | |||
<div class="ellipsis" style="display: inline-block; width: 90%;">\ | |||
<i class="fa fa-paper-clip"></i> \ | |||
<a class="attached-file" target="_blank"></a>\ | |||
</div>\ | |||
<a class="close">×</a></div>') | |||
.prependTo(me.input_area) | |||
.toggle(false); | |||
this.input = this.$input.get(0); | |||
this.set_input_attributes(); | |||
this.has_input = true; | |||
this.$value.find(".close").on("click", function() { | |||
me.clear_attachment(); | |||
}); | |||
}, | |||
clear_attachment: function() { | |||
var me = this; | |||
if(this.frm) { | |||
me.frm.attachments.remove_attachment_by_filename(me.value, function() { | |||
me.parse_validate_and_set_in_model(null); | |||
me.refresh(); | |||
me.frm.save(); | |||
}); | |||
} else { | |||
this.dataurl = null; | |||
this.fileobj = null; | |||
this.set_input(null); | |||
this.refresh(); | |||
} | |||
}, | |||
onclick: function() { | |||
var me = this; | |||
if(this.doc) { | |||
var doc = this.doc.parent && frappe.model.get_doc(this.doc.parenttype, this.doc.parent) || this.doc; | |||
if (doc.__islocal) { | |||
frappe.msgprint(__("Please save the document before uploading.")); | |||
return; | |||
} | |||
} | |||
if(!this.dialog) { | |||
this.dialog = new frappe.ui.Dialog({ | |||
title: __(this.df.label || __("Upload")), | |||
fields: [ | |||
{fieldtype:"HTML", fieldname:"upload_area"}, | |||
{fieldtype:"HTML", fieldname:"or_attach", options: __("Or")}, | |||
{fieldtype:"Select", fieldname:"select", label:__("Select from existing attachments") }, | |||
{fieldtype:"Button", fieldname:"clear", | |||
label:__("Clear Attachment"), click: function() { | |||
me.clear_attachment(); | |||
me.dialog.hide(); | |||
} | |||
}, | |||
] | |||
}); | |||
} | |||
this.dialog.show(); | |||
this.dialog.get_field("upload_area").$wrapper.empty(); | |||
// select from existing attachments | |||
var attachments = this.frm && this.frm.attachments.get_attachments() || []; | |||
var select = this.dialog.get_field("select"); | |||
if(attachments.length) { | |||
attachments = $.map(attachments, function(o) { return o.file_url; }); | |||
select.df.options = [""].concat(attachments); | |||
select.toggle(true); | |||
this.dialog.get_field("or_attach").toggle(true); | |||
select.refresh(); | |||
} else { | |||
this.dialog.get_field("or_attach").toggle(false); | |||
select.toggle(false); | |||
} | |||
select.$input.val(""); | |||
// show button if attachment exists | |||
this.dialog.get_field('clear').$wrapper.toggle(this.get_model_value() ? true : false); | |||
this.set_upload_options(); | |||
frappe.upload.make(this.upload_options); | |||
}, | |||
set_upload_options: function() { | |||
var me = this; | |||
this.upload_options = { | |||
parent: this.dialog.get_field("upload_area").$wrapper, | |||
args: {}, | |||
allow_multiple: 0, | |||
max_width: this.df.max_width, | |||
max_height: this.df.max_height, | |||
options: this.df.options, | |||
btn: this.dialog.set_primary_action(__("Upload")), | |||
on_no_attach: function() { | |||
// if no attachmemts, | |||
// check if something is selected | |||
var selected = me.dialog.get_field("select").get_value(); | |||
if(selected) { | |||
me.parse_validate_and_set_in_model(selected); | |||
me.dialog.hide(); | |||
me.frm.save(); | |||
} else { | |||
frappe.msgprint(__("Please attach a file or set a URL")); | |||
} | |||
}, | |||
callback: function(attachment) { | |||
me.on_upload_complete(attachment); | |||
me.dialog.hide(); | |||
}, | |||
onerror: function() { | |||
me.dialog.hide(); | |||
} | |||
}; | |||
if ("is_private" in this.df) { | |||
this.upload_options.is_private = this.df.is_private; | |||
} | |||
if(this.frm) { | |||
this.upload_options.args = { | |||
from_form: 1, | |||
doctype: this.frm.doctype, | |||
docname: this.frm.docname | |||
}; | |||
} else { | |||
this.upload_options.on_attach = function(fileobj, dataurl) { | |||
me.dialog.hide(); | |||
me.fileobj = fileobj; | |||
me.dataurl = dataurl; | |||
if(me.on_attach) { | |||
me.on_attach(); | |||
} | |||
if(me.df.on_attach) { | |||
me.df.on_attach(fileobj, dataurl); | |||
} | |||
me.on_upload_complete(); | |||
}; | |||
} | |||
}, | |||
set_input: function(value, dataurl) { | |||
this.value = value; | |||
if(this.value) { | |||
this.$input.toggle(false); | |||
if(this.value.indexOf(",")!==-1) { | |||
var parts = this.value.split(","); | |||
var filename = parts[0]; | |||
dataurl = parts[1]; | |||
} | |||
this.$value.toggle(true).find(".attached-file") | |||
.html(filename || this.value) | |||
.attr("href", dataurl || this.value); | |||
} else { | |||
this.$input.toggle(true); | |||
this.$value.toggle(false); | |||
} | |||
}, | |||
get_value: function() { | |||
if(this.frm) { | |||
return this.value; | |||
} else { | |||
return this.fileobj ? (this.fileobj.filename + "," + this.dataurl) : null; | |||
} | |||
}, | |||
on_upload_complete: function(attachment) { | |||
if(this.frm) { | |||
this.parse_validate_and_set_in_model(attachment.file_url); | |||
this.refresh(); | |||
this.frm.attachments.update_attachment(attachment); | |||
this.frm.save(); | |||
} else { | |||
this.value = this.get_value(); | |||
this.refresh(); | |||
} | |||
}, | |||
}); |
@@ -0,0 +1,66 @@ | |||
frappe.ui.form.ControlAttachImage = frappe.ui.form.ControlAttach.extend({ | |||
make: function() { | |||
var me = this; | |||
this._super(); | |||
this.container = $('<div class="control-container">').insertAfter($(this.wrapper)); | |||
$(this.wrapper).detach(); | |||
this.container.attr('data-fieldtype', this.df.fieldtype).append(this.wrapper); | |||
if(this.df.align === 'center') { | |||
this.container.addClass("flex-justify-center"); | |||
} else if (this.df.align === 'right') { | |||
this.container.addClass("flex-justify-end"); | |||
} | |||
this.img_wrapper = $('<div style="width: 100%; height: calc(100% - 40px); position: relative;">\ | |||
<div class="missing-image attach-missing-image"><i class="octicon octicon-device-camera"></i></div></div>') | |||
.appendTo(this.wrapper); | |||
this.img_container = $(`<div class='img-container'></div>`); | |||
this.img = $(`<img class='img-responsive attach-image-display'>`) | |||
.appendTo(this.img_container); | |||
this.img_overlay = $(`<div class='img-overlay'> | |||
<span class="overlay-text">Change</span> | |||
</div>`).appendTo(this.img_container); | |||
this.remove_image_link = $('<a style="font-size: 12px;color: #8D99A6;">Remove</a>'); | |||
this.img_wrapper.append(this.img_container).append(this.remove_image_link); | |||
// this.img.toggle(false); | |||
// this.img_overlay.toggle(false); | |||
this.img_container.toggle(false); | |||
this.remove_image_link.toggle(false); | |||
// propagate click to Attach button | |||
this.img_wrapper.find(".missing-image").on("click", function() { me.$input.click(); }); | |||
this.img_container.on("click", function() { me.$input.click(); }); | |||
this.remove_image_link.on("click", function() { me.$value.find(".close").click(); }); | |||
this.set_image(); | |||
}, | |||
refresh_input: function() { | |||
this._super(); | |||
$(this.wrapper).find('.btn-attach').addClass('hidden'); | |||
this.set_image(); | |||
if(this.get_status()=="Read") { | |||
$(this.disp_area).toggle(false); | |||
} | |||
}, | |||
set_image: function() { | |||
if(this.get_value()) { | |||
$(this.img_wrapper).find(".missing-image").toggle(false); | |||
// this.img.attr("src", this.dataurl ? this.dataurl : this.value).toggle(true); | |||
// this.img_overlay.toggle(true); | |||
this.img.attr("src", this.dataurl ? this.dataurl : this.value); | |||
this.img_container.toggle(true); | |||
this.remove_image_link.toggle(true); | |||
} else { | |||
$(this.img_wrapper).find(".missing-image").toggle(true); | |||
// this.img.toggle(false); | |||
// this.img_overlay.toggle(false); | |||
this.img_container.toggle(false); | |||
this.remove_image_link.toggle(false); | |||
} | |||
} | |||
}); |
@@ -0,0 +1,172 @@ | |||
frappe.ui.form.make_control = function (opts) { | |||
var control_class_name = "Control" + opts.df.fieldtype.replace(/ /g, ""); | |||
if(frappe.ui.form[control_class_name]) { | |||
return new frappe.ui.form[control_class_name](opts); | |||
} else { | |||
// eslint-disable-next-line | |||
console.log("Invalid Control Name: " + opts.df.fieldtype); | |||
} | |||
}; | |||
frappe.ui.form.Control = Class.extend({ | |||
init: function(opts) { | |||
$.extend(this, opts); | |||
this.make(); | |||
// if developer_mode=1, show fieldname as tooltip | |||
if(frappe.boot.user && frappe.boot.user.name==="Administrator" && | |||
frappe.boot.developer_mode===1 && this.$wrapper) { | |||
this.$wrapper.attr("title", __(this.df.fieldname)); | |||
} | |||
if(this.render_input) { | |||
this.refresh(); | |||
} | |||
}, | |||
make: function() { | |||
this.make_wrapper(); | |||
this.$wrapper | |||
.attr("data-fieldtype", this.df.fieldtype) | |||
.attr("data-fieldname", this.df.fieldname); | |||
this.wrapper = this.$wrapper.get(0); | |||
this.wrapper.fieldobj = this; // reference for event handlers | |||
}, | |||
make_wrapper: function() { | |||
this.$wrapper = $("<div class='frappe-control'></div>").appendTo(this.parent); | |||
// alias | |||
this.wrapper = this.$wrapper; | |||
}, | |||
toggle: function(show) { | |||
this.df.hidden = show ? 0 : 1; | |||
this.refresh(); | |||
}, | |||
// returns "Read", "Write" or "None" | |||
// as strings based on permissions | |||
get_status: function(explain) { | |||
if(!this.doctype && !this.docname) { | |||
// like in case of a dialog box | |||
if (cint(this.df.hidden)) { | |||
// eslint-disable-next-line | |||
if(explain) console.log("By Hidden: None"); | |||
return "None"; | |||
} else if (cint(this.df.hidden_due_to_dependency)) { | |||
// eslint-disable-next-line | |||
if(explain) console.log("By Hidden Dependency: None"); | |||
return "None"; | |||
} else if (cint(this.df.read_only)) { | |||
// eslint-disable-next-line | |||
if(explain) console.log("By Read Only: Read"); | |||
return "Read"; | |||
} | |||
return "Write"; | |||
} | |||
var status = frappe.perm.get_field_display_status(this.df, | |||
frappe.model.get_doc(this.doctype, this.docname), this.perm || (this.frm && this.frm.perm), explain); | |||
// hide if no value | |||
if (this.doctype && status==="Read" && !this.only_input | |||
&& is_null(frappe.model.get_value(this.doctype, this.docname, this.df.fieldname)) | |||
&& !in_list(["HTML", "Image"], this.df.fieldtype)) { | |||
// eslint-disable-next-line | |||
if(explain) console.log("By Hide Read-only, null fields: None"); | |||
status = "None"; | |||
} | |||
return status; | |||
}, | |||
refresh: function() { | |||
this.disp_status = this.get_status(); | |||
this.$wrapper | |||
&& this.$wrapper.toggleClass("hide-control", this.disp_status=="None") | |||
&& this.refresh_input | |||
&& this.refresh_input(); | |||
}, | |||
get_doc: function() { | |||
return this.doctype && this.docname | |||
&& locals[this.doctype] && locals[this.doctype][this.docname] || {}; | |||
}, | |||
get_model_value: function() { | |||
if(this.doc) { | |||
return this.doc[this.df.fieldname]; | |||
} | |||
}, | |||
set_value: function(value) { | |||
return this.validate_and_set_in_model(value); | |||
}, | |||
parse_validate_and_set_in_model: function(value, e) { | |||
if(this.parse) { | |||
value = this.parse(value); | |||
} | |||
return this.validate_and_set_in_model(value, e); | |||
}, | |||
validate_and_set_in_model: function(value, e) { | |||
var me = this; | |||
if(this.inside_change_event) { | |||
return Promise.resolve(); | |||
} | |||
this.inside_change_event = true; | |||
var set = function(value) { | |||
me.inside_change_event = false; | |||
return frappe.run_serially([ | |||
() => me.set_model_value(value), | |||
() => { | |||
me.set_mandatory && me.set_mandatory(value); | |||
if(me.df.change || me.df.onchange) { | |||
// onchange event specified in df | |||
return (me.df.change || me.df.onchange).apply(me, [e]); | |||
} | |||
} | |||
]); | |||
}; | |||
value = this.validate(value); | |||
if (value && value.then) { | |||
// got a promise | |||
return value.then((value) => set(value)); | |||
} else { | |||
// all clear | |||
return set(value); | |||
} | |||
}, | |||
get_value: function() { | |||
if(this.get_status()==='Write') { | |||
return this.get_input_value ? | |||
(this.parse ? this.parse(this.get_input_value()) : this.get_input_value()) : | |||
undefined; | |||
} else if(this.get_status()==='Read') { | |||
return this.value || undefined; | |||
} else { | |||
return undefined; | |||
} | |||
}, | |||
set_model_value: function(value) { | |||
if(this.doctype && this.docname) { | |||
this.last_value = value; | |||
return frappe.model.set_value(this.doctype, this.docname, this.df.fieldname, | |||
value, this.df.fieldtype); | |||
} else { | |||
if(this.doc) { | |||
this.doc[this.df.fieldname] = value; | |||
} | |||
this.set_input(value); | |||
return Promise.resolve(); | |||
} | |||
}, | |||
set_focus: function() { | |||
if(this.$input) { | |||
this.$input.get(0).focus(); | |||
return true; | |||
} | |||
} | |||
}); |
@@ -0,0 +1,180 @@ | |||
frappe.ui.form.ControlInput = frappe.ui.form.Control.extend({ | |||
horizontal: true, | |||
make: function() { | |||
// parent element | |||
this._super(); | |||
this.set_input_areas(); | |||
// set description | |||
this.set_max_width(); | |||
}, | |||
make_wrapper: function() { | |||
if(this.only_input) { | |||
this.$wrapper = $('<div class="form-group frappe-control">').appendTo(this.parent); | |||
} else { | |||
this.$wrapper = $('<div class="frappe-control">\ | |||
<div class="form-group">\ | |||
<div class="clearfix">\ | |||
<label class="control-label" style="padding-right: 0px;"></label>\ | |||
</div>\ | |||
<div class="control-input-wrapper">\ | |||
<div class="control-input"></div>\ | |||
<div class="control-value like-disabled-input" style="display: none;"></div>\ | |||
<p class="help-box small text-muted hidden-xs"></p>\ | |||
</div>\ | |||
</div>\ | |||
</div>').appendTo(this.parent); | |||
} | |||
}, | |||
toggle_label: function(show) { | |||
this.$wrapper.find(".control-label").toggleClass("hide", !show); | |||
}, | |||
toggle_description: function(show) { | |||
this.$wrapper.find(".help-box").toggleClass("hide", !show); | |||
}, | |||
set_input_areas: function() { | |||
if(this.only_input) { | |||
this.input_area = this.wrapper; | |||
} else { | |||
this.label_area = this.label_span = this.$wrapper.find("label").get(0); | |||
this.input_area = this.$wrapper.find(".control-input").get(0); | |||
// keep a separate display area to rendered formatted values | |||
// like links, currencies, HTMLs etc. | |||
this.disp_area = this.$wrapper.find(".control-value").get(0); | |||
} | |||
}, | |||
set_max_width: function() { | |||
if(this.horizontal) { | |||
this.$wrapper.addClass("input-max-width"); | |||
} | |||
}, | |||
// update input value, label, description | |||
// display (show/hide/read-only), | |||
// mandatory style on refresh | |||
refresh_input: function() { | |||
var me = this; | |||
var make_input = function() { | |||
if(!me.has_input) { | |||
me.make_input(); | |||
if(me.df.on_make) { | |||
me.df.on_make(me); | |||
} | |||
} | |||
}; | |||
var update_input = function() { | |||
if(me.doctype && me.docname) { | |||
me.set_input(me.value); | |||
} else { | |||
me.set_input(me.value || null); | |||
} | |||
}; | |||
if(me.disp_status != "None") { | |||
// refresh value | |||
if(me.doctype && me.docname) { | |||
me.value = frappe.model.get_value(me.doctype, me.docname, me.df.fieldname); | |||
} | |||
if(me.disp_status=="Write") { | |||
me.disp_area && $(me.disp_area).toggle(false); | |||
$(me.input_area).toggle(true); | |||
me.$input && me.$input.prop("disabled", false); | |||
make_input(); | |||
update_input(); | |||
} else { | |||
if(me.only_input) { | |||
make_input(); | |||
update_input(); | |||
} else { | |||
$(me.input_area).toggle(false); | |||
if (me.disp_area) { | |||
me.set_disp_area(me.value); | |||
$(me.disp_area).toggle(true); | |||
} | |||
} | |||
me.$input && me.$input.prop("disabled", true); | |||
} | |||
me.set_description(); | |||
me.set_label(); | |||
me.set_mandatory(me.value); | |||
me.set_bold(); | |||
} | |||
}, | |||
set_disp_area: function(value) { | |||
if(in_list(["Currency", "Int", "Float"], this.df.fieldtype) | |||
&& (this.value === 0 || value === 0)) { | |||
// to set the 0 value in readonly for currency, int, float field | |||
value = 0; | |||
} else { | |||
value = this.value || value; | |||
} | |||
this.disp_area && $(this.disp_area) | |||
.html(frappe.format(value, this.df, {no_icon:true, inline:true}, | |||
this.doc || (this.frm && this.frm.doc))); | |||
}, | |||
bind_change_event: function() { | |||
var me = this; | |||
this.$input && this.$input.on("change", this.change || function(e) { | |||
me.parse_validate_and_set_in_model(me.get_input_value(), e); | |||
}); | |||
}, | |||
bind_focusout: function() { | |||
// on touchscreen devices, scroll to top | |||
// so that static navbar and page head don't overlap the input | |||
if (frappe.dom.is_touchscreen()) { | |||
var me = this; | |||
this.$input && this.$input.on("focusout", function() { | |||
if (frappe.dom.is_touchscreen()) { | |||
frappe.utils.scroll_to(me.$wrapper); | |||
} | |||
}); | |||
} | |||
}, | |||
set_label: function(label) { | |||
if(label) this.df.label = label; | |||
if(this.only_input || this.df.label==this._label) | |||
return; | |||
var icon = ""; | |||
this.label_span.innerHTML = (icon ? '<i class="'+icon+'"></i> ' : "") + | |||
__(this.df.label) || " "; | |||
this._label = this.df.label; | |||
}, | |||
set_description: function(description) { | |||
if (description !== undefined) { | |||
this.df.description = description; | |||
} | |||
if (this.only_input || this.df.description===this._description) { | |||
return; | |||
} | |||
if (this.df.description) { | |||
this.$wrapper.find(".help-box").html(__(this.df.description)); | |||
} else { | |||
this.set_empty_description(); | |||
} | |||
this._description = this.df.description; | |||
}, | |||
set_new_description: function(description) { | |||
this.$wrapper.find(".help-box").html(description); | |||
}, | |||
set_empty_description: function() { | |||
this.$wrapper.find(".help-box").html(""); | |||
}, | |||
set_mandatory: function(value) { | |||
this.$wrapper.toggleClass("has-error", (this.df.reqd && is_null(value)) ? true : false); | |||
}, | |||
set_bold: function() { | |||
if(this.$input) { | |||
this.$input.toggleClass("bold", !!(this.df.bold || this.df.reqd)); | |||
} | |||
if(this.disp_area) { | |||
$(this.disp_area).toggleClass("bold", !!(this.df.bold || this.df.reqd)); | |||
} | |||
} | |||
}); |
@@ -0,0 +1,38 @@ | |||
frappe.ui.form.ControlButton = frappe.ui.form.ControlData.extend({ | |||
make_input: function() { | |||
var me = this; | |||
this.$input = $('<button class="btn btn-default btn-xs">') | |||
.prependTo(me.input_area) | |||
.on("click", function() { | |||
me.onclick(); | |||
}); | |||
this.input = this.$input.get(0); | |||
this.set_input_attributes(); | |||
this.has_input = true; | |||
this.toggle_label(false); | |||
}, | |||
onclick: function() { | |||
if(this.frm && this.frm.doc) { | |||
if(this.frm.script_manager.has_handlers(this.df.fieldname, this.doctype)) { | |||
this.frm.script_manager.trigger(this.df.fieldname, this.doctype, this.docname); | |||
} else { | |||
this.frm.runscript(this.df.options, this); | |||
} | |||
} | |||
else if(this.df.click) { | |||
this.df.click(); | |||
} | |||
}, | |||
set_input_areas: function() { | |||
this._super(); | |||
$(this.disp_area).removeClass().addClass("hide"); | |||
}, | |||
set_empty_description: function() { | |||
this.$wrapper.find(".help-box").empty().toggle(false); | |||
}, | |||
set_label: function() { | |||
$(this.label_span).html(" "); | |||
this.$input && this.$input.html((this.df.icon ? | |||
('<i class="'+this.df.icon+' fa-fw"></i> ') : "") + __(this.df.label)); | |||
} | |||
}); |
@@ -0,0 +1,38 @@ | |||
frappe.ui.form.ControlCheck = frappe.ui.form.ControlData.extend({ | |||
input_type: "checkbox", | |||
make_wrapper: function() { | |||
this.$wrapper = $('<div class="form-group frappe-control">\ | |||
<div class="checkbox">\ | |||
<label>\ | |||
<span class="input-area"></span>\ | |||
<span class="disp-area" style="display:none; margin-left: -20px;"></span>\ | |||
<span class="label-area small"></span>\ | |||
</label>\ | |||
<p class="help-box small text-muted"></p>\ | |||
</div>\ | |||
</div>').appendTo(this.parent); | |||
}, | |||
set_input_areas: function() { | |||
this.label_area = this.label_span = this.$wrapper.find(".label-area").get(0); | |||
this.input_area = this.$wrapper.find(".input-area").get(0); | |||
this.disp_area = this.$wrapper.find(".disp-area").get(0); | |||
}, | |||
make_input: function() { | |||
this._super(); | |||
this.$input.removeClass("form-control"); | |||
}, | |||
get_input_value: function() { | |||
return this.input && this.input.checked ? 1 : 0; | |||
}, | |||
validate: function(value) { | |||
return cint(value); | |||
}, | |||
set_input: function(value) { | |||
if(this.input) { | |||
this.input.checked = (value ? 1 : 0); | |||
} | |||
this.last_value = value; | |||
this.set_mandatory(value); | |||
this.set_disp_area(value); | |||
} | |||
}); |
@@ -0,0 +1,8 @@ | |||
frappe.ui.form.ControlCode = frappe.ui.form.ControlText.extend({ | |||
make_input: function() { | |||
this._super(); | |||
$(this.input_area).find("textarea") | |||
.allowTabs() | |||
.addClass('control-code'); | |||
} | |||
}); |
@@ -0,0 +1,83 @@ | |||
frappe.ui.form.ControlColor = frappe.ui.form.ControlData.extend({ | |||
make_input: function () { | |||
this._super(); | |||
this.colors = [ | |||
"#ffc4c4", "#ff8989", "#ff4d4d", "#a83333", | |||
"#ffe8cd", "#ffd19c", "#ffb868", "#a87945", | |||
"#ffd2c2", "#ffa685", "#ff7846", "#a85b5b", | |||
"#ffd7d7", "#ffb1b1", "#ff8989", "#a84f2e", | |||
"#fffacd", "#fff168", "#fff69c", "#a89f45", | |||
"#ebf8cc", "#d9f399", "#c5ec63", "#7b933d", | |||
"#cef6d1", "#9deca2", "#6be273", "#428b46", | |||
"#d2f8ed", "#a4f3dd", "#77ecca", "#49937e", | |||
"#d2f1ff", "#a6e4ff", "#78d6ff", "#4f8ea8", | |||
"#d2d2ff", "#a3a3ff", "#7575ff", "#4d4da8", | |||
"#dac7ff", "#b592ff", "#8e58ff", "#5e3aa8", | |||
"#f8d4f8", "#f3aaf0", "#ec7dea", "#934f92" | |||
]; | |||
this.make_color_input(); | |||
}, | |||
make_color_input: function () { | |||
this.$wrapper | |||
.find('.control-input-wrapper') | |||
.append(`<div class="color-picker"> | |||
<div class="color-picker-pallete"></div> | |||
</div>`); | |||
this.$color_pallete = this.$wrapper.find('.color-picker-pallete'); | |||
var color_html = this.colors.map(this.get_color_box).join(""); | |||
this.$color_pallete.append(color_html); | |||
this.$color_pallete.hide(); | |||
this.bind_events(); | |||
}, | |||
get_color_box: function (hex) { | |||
return `<div class="color-box" data-color="${hex}" style="background-color: ${hex}"></div>`; | |||
}, | |||
set_formatted_input: function(value) { | |||
this._super(value); | |||
if(!value) value = '#ffffff'; | |||
this.$input.css({ | |||
"background-color": value | |||
}); | |||
}, | |||
bind_events: function () { | |||
var mousedown_happened = false; | |||
this.$wrapper.on("click", ".color-box", (e) => { | |||
mousedown_happened = false; | |||
var color_val = $(e.target).data("color"); | |||
this.set_value(color_val); | |||
// set focus so that we can blur it later | |||
this.set_focus(); | |||
}); | |||
this.$wrapper.find(".color-box").mousedown(() => { | |||
mousedown_happened = true; | |||
}); | |||
this.$input.on("focus", () => { | |||
this.$color_pallete.show(); | |||
}); | |||
this.$input.on("blur", () => { | |||
if (mousedown_happened) { | |||
// cancel the blur event | |||
mousedown_happened = false; | |||
} else { | |||
// blur event is okay | |||
$(this.$color_pallete).hide(); | |||
} | |||
}); | |||
}, | |||
validate: function (value) { | |||
if(value === '') { | |||
return ''; | |||
} | |||
var is_valid = /^#[0-9A-F]{6}$/i.test(value); | |||
if(is_valid) { | |||
return value; | |||
} | |||
frappe.msgprint(__("{0} is not a valid hex color", [value])); | |||
return null; | |||
} | |||
}); |
@@ -0,0 +1,20 @@ | |||
frappe.ui.form.ControlCurrency = frappe.ui.form.ControlFloat.extend({ | |||
format_for_input: function(value) { | |||
var formatted_value = format_number(parseFloat(value), this.get_number_format(), this.get_precision()); | |||
return isNaN(parseFloat(value)) ? "" : formatted_value; | |||
}, | |||
get_precision: function() { | |||
// always round based on field precision or currency's precision | |||
// this method is also called in this.parse() | |||
if (!this.df.precision) { | |||
if(frappe.boot.sysdefaults.currency_precision) { | |||
this.df.precision = frappe.boot.sysdefaults.currency_precision; | |||
} else { | |||
this.df.precision = get_number_format_info(this.get_number_format()).precision; | |||
} | |||
} | |||
return this.df.precision; | |||
} | |||
}); |
@@ -0,0 +1,138 @@ | |||
frappe.ui.form.ControlData = frappe.ui.form.ControlInput.extend({ | |||
html_element: "input", | |||
input_type: "text", | |||
make_input: function() { | |||
if(this.$input) return; | |||
this.$input = $("<"+ this.html_element +">") | |||
.attr("type", this.input_type) | |||
.attr("autocomplete", "off") | |||
.addClass("input-with-feedback form-control") | |||
.prependTo(this.input_area); | |||
if (in_list(['Data', 'Link', 'Dynamic Link', 'Password', 'Select', 'Read Only', 'Attach', 'Attach Image'], | |||
this.df.fieldtype)) { | |||
this.$input.attr("maxlength", this.df.length || 140); | |||
} | |||
this.set_input_attributes(); | |||
this.input = this.$input.get(0); | |||
this.has_input = true; | |||
this.bind_change_event(); | |||
this.bind_focusout(); | |||
this.setup_autoname_check(); | |||
// somehow this event does not bubble up to document | |||
// after v7, if you can debug, remove this | |||
}, | |||
setup_autoname_check: function() { | |||
if (!this.df.parent) return; | |||
this.meta = frappe.get_meta(this.df.parent); | |||
if (this.meta && this.meta.autoname | |||
&& this.meta.autoname.substr(0, 6)==='field:') { | |||
this.$input.on('keyup', () => { | |||
this.set_description(''); | |||
if (this.doc && this.doc.__islocal) { | |||
// check after 1 sec | |||
let timeout = setTimeout(() => { | |||
// clear any pending calls | |||
if (this.last_check) clearTimeout(this.last_check); | |||
// check if name exists | |||
frappe.db.get_value(this.doctype, this.$input.val(), | |||
'name', (val) => { | |||
if (val) { | |||
this.set_description(__('{0} already exists. Select another name', [val.name])); | |||
} | |||
}); | |||
this.last_check = null; | |||
}, 1000); | |||
this.last_check = timeout; | |||
} | |||
}); | |||
} | |||
}, | |||
set_input_attributes: function() { | |||
this.$input | |||
.attr("data-fieldtype", this.df.fieldtype) | |||
.attr("data-fieldname", this.df.fieldname) | |||
.attr("placeholder", this.df.placeholder || ""); | |||
if(this.doctype) { | |||
this.$input.attr("data-doctype", this.doctype); | |||
} | |||
if(this.df.input_css) { | |||
this.$input.css(this.df.input_css); | |||
} | |||
if(this.df.input_class) { | |||
this.$input.addClass(this.df.input_class); | |||
} | |||
}, | |||
set_input: function(value) { | |||
this.last_value = this.value; | |||
this.value = value; | |||
this.set_formatted_input(value); | |||
this.set_disp_area(value); | |||
this.set_mandatory && this.set_mandatory(value); | |||
}, | |||
set_formatted_input: function(value) { | |||
this.$input && this.$input.val(this.format_for_input(value)); | |||
}, | |||
get_input_value: function() { | |||
return this.$input ? this.$input.val() : undefined; | |||
}, | |||
format_for_input: function(val) { | |||
return val==null ? "" : val; | |||
}, | |||
validate: function(v) { | |||
if(this.df.options == 'Phone') { | |||
if(v+''=='') { | |||
return ''; | |||
} | |||
var v1 = ''; | |||
// phone may start with + and must only have numbers later, '-' and ' ' are stripped | |||
v = v.replace(/ /g, '').replace(/-/g, '').replace(/\(/g, '').replace(/\)/g, ''); | |||
// allow initial +,0,00 | |||
if(v && v.substr(0,1)=='+') { | |||
v1 = '+'; v = v.substr(1); | |||
} | |||
if(v && v.substr(0,2)=='00') { | |||
v1 += '00'; v = v.substr(2); | |||
} | |||
if(v && v.substr(0,1)=='0') { | |||
v1 += '0'; v = v.substr(1); | |||
} | |||
v1 += cint(v) + ''; | |||
return v1; | |||
} else if(this.df.options == 'Email') { | |||
if(v+''=='') { | |||
return ''; | |||
} | |||
var email_list = frappe.utils.split_emails(v); | |||
if (!email_list) { | |||
// invalid email | |||
return ''; | |||
} else { | |||
var invalid_email = false; | |||
email_list.forEach(function(email) { | |||
if (!validate_email(email)) { | |||
frappe.msgprint(__("Invalid Email: {0}", [email])); | |||
invalid_email = true; | |||
} | |||
}); | |||
if (invalid_email) { | |||
// at least 1 invalid email | |||
return ''; | |||
} else { | |||
// all good | |||
return v; | |||
} | |||
} | |||
} else { | |||
return v; | |||
} | |||
} | |||
}); |
@@ -0,0 +1,106 @@ | |||
frappe.ui.form.ControlDate = frappe.ui.form.ControlData.extend({ | |||
make_input: function() { | |||
this._super(); | |||
this.set_date_options(); | |||
this.set_datepicker(); | |||
this.set_t_for_today(); | |||
}, | |||
set_formatted_input: function(value) { | |||
this._super(value); | |||
if(!value) return; | |||
let should_refresh = this.last_value && this.last_value !== value; | |||
if (!should_refresh) { | |||
if(this.datepicker.selectedDates.length > 0) { | |||
// if date is selected but different from value, refresh | |||
const selected_date = | |||
moment(this.datepicker.selectedDates[0]) | |||
.format(moment.defaultDateFormat); | |||
should_refresh = selected_date !== value; | |||
} else { | |||
// if datepicker has no selected date, refresh | |||
should_refresh = true; | |||
} | |||
} | |||
if(should_refresh) { | |||
this.datepicker.selectDate(frappe.datetime.str_to_obj(value)); | |||
} | |||
}, | |||
set_date_options: function() { | |||
var me = this; | |||
var lang = frappe.boot.user.language; | |||
if(!$.fn.datepicker.language[lang]) { | |||
lang = 'en'; | |||
} | |||
this.today_text = __("Today"); | |||
this.datepicker_options = { | |||
language: lang, | |||
autoClose: true, | |||
todayButton: frappe.datetime.now_date(true), | |||
dateFormat: (frappe.boot.sysdefaults.date_format || 'yyyy-mm-dd'), | |||
startDate: frappe.datetime.now_date(true), | |||
onSelect: () => { | |||
this.$input.trigger('change'); | |||
}, | |||
onShow: () => { | |||
this.datepicker.$datepicker | |||
.find('.datepicker--button:visible') | |||
.text(me.today_text); | |||
this.update_datepicker_position(); | |||
} | |||
}; | |||
}, | |||
update_datepicker_position: function() { | |||
if(!this.frm) return; | |||
// show datepicker above or below the input | |||
// based on scroll position | |||
var window_height = $(window).height(); | |||
var window_scroll_top = $(window).scrollTop(); | |||
var el_offset_top = this.$input.offset().top + 280; | |||
var position = 'top left'; | |||
if(window_height + window_scroll_top >= el_offset_top) { | |||
position = 'bottom left'; | |||
} | |||
this.datepicker.update('position', position); | |||
}, | |||
set_datepicker: function() { | |||
this.$input.datepicker(this.datepicker_options); | |||
this.datepicker = this.$input.data('datepicker'); | |||
}, | |||
set_t_for_today: function() { | |||
var me = this; | |||
this.$input.on("keydown", function(e) { | |||
if(e.which===84) { // 84 === t | |||
if(me.df.fieldtype=='Date') { | |||
me.set_value(frappe.datetime.nowdate()); | |||
} if(me.df.fieldtype=='Datetime') { | |||
me.set_value(frappe.datetime.now_datetime()); | |||
} if(me.df.fieldtype=='Time') { | |||
me.set_value(frappe.datetime.now_time()); | |||
} | |||
return false; | |||
} | |||
}); | |||
}, | |||
parse: function(value) { | |||
if(value) { | |||
return frappe.datetime.user_to_str(value); | |||
} | |||
}, | |||
format_for_input: function(value) { | |||
if(value) { | |||
return frappe.datetime.str_to_user(value); | |||
} | |||
return ""; | |||
}, | |||
validate: function(value) { | |||
if(value && !frappe.datetime.validate(value)) { | |||
frappe.msgprint(__("Date must be in format: {0}", [frappe.sys_defaults.date_format || "yyyy-mm-dd"])); | |||
return ''; | |||
} | |||
return value; | |||
} | |||
}); |
@@ -0,0 +1,62 @@ | |||
frappe.ui.form.ControlDateRange = frappe.ui.form.ControlData.extend({ | |||
make_input: function() { | |||
this._super(); | |||
this.set_date_options(); | |||
this.set_datepicker(); | |||
this.refresh(); | |||
}, | |||
set_date_options: function() { | |||
var me = this; | |||
this.datepicker_options = { | |||
language: "en", | |||
range: true, | |||
autoClose: true, | |||
toggleSelected: false | |||
}; | |||
this.datepicker_options.dateFormat = | |||
(frappe.boot.sysdefaults.date_format || 'yyyy-mm-dd'); | |||
this.datepicker_options.onSelect = function() { | |||
me.$input.trigger('change'); | |||
}; | |||
}, | |||
set_datepicker: function() { | |||
this.$input.datepicker(this.datepicker_options); | |||
this.datepicker = this.$input.data('datepicker'); | |||
}, | |||
set_input: function(value, value2) { | |||
this.last_value = this.value; | |||
if (value && value2) { | |||
this.value = [value, value2]; | |||
} else { | |||
this.value = value; | |||
} | |||
if (this.value) { | |||
let formatted = this.format_for_input(this.value[0], this.value[1]); | |||
this.$input && this.$input.val(formatted); | |||
} else { | |||
this.$input && this.$input.val(""); | |||
} | |||
this.set_disp_area(value || ''); | |||
this.set_mandatory && this.set_mandatory(value); | |||
}, | |||
parse: function(value) { | |||
// replace the separator (which can be in user language) with comma | |||
const to = __('{0} to {1}').replace('{0}', '').replace('{1}', ''); | |||
value = value.replace(to, ','); | |||
if(value && value.includes(',')) { | |||
var vals = value.split(','); | |||
var from_date = moment(frappe.datetime.user_to_obj(vals[0])).format('YYYY-MM-DD'); | |||
var to_date = moment(frappe.datetime.user_to_obj(vals[vals.length-1])).format('YYYY-MM-DD'); | |||
return [from_date, to_date]; | |||
} | |||
}, | |||
format_for_input: function(value1, value2) { | |||
if(value1 && value2) { | |||
value1 = frappe.datetime.str_to_user(value1); | |||
value2 = frappe.datetime.str_to_user(value2); | |||
return __("{0} to {1}").format([value1, value2]); | |||
} | |||
return ""; | |||
} | |||
}); |
@@ -0,0 +1,23 @@ | |||
frappe.ui.form.ControlDatetime = frappe.ui.form.ControlDate.extend({ | |||
set_date_options: function() { | |||
this._super(); | |||
this.today_text = __("Now"); | |||
$.extend(this.datepicker_options, { | |||
timepicker: true, | |||
timeFormat: "hh:ii:ss", | |||
todayButton: frappe.datetime.now_datetime(true) | |||
}); | |||
}, | |||
set_description: function() { | |||
const { description } = this.df; | |||
const { time_zone } = frappe.sys_defaults; | |||
if (!frappe.datetime.is_timezone_same()) { | |||
if (!description) { | |||
this.df.description = time_zone; | |||
} else if (!description.includes(time_zone)) { | |||
this.df.description += '<br>' + time_zone; | |||
} | |||
} | |||
this._super(); | |||
} | |||
}); |
@@ -0,0 +1,21 @@ | |||
frappe.ui.form.ControlDynamicLink = frappe.ui.form.ControlLink.extend({ | |||
get_options: function() { | |||
if(this.df.get_options) { | |||
return this.df.get_options(); | |||
} | |||
if (this.docname==null && cur_dialog) { | |||
//for dialog box | |||
return cur_dialog.get_value(this.df.options); | |||
} | |||
if (cur_frm==null && cur_list){ | |||
//for list page | |||
return cur_list.wrapper.find("input[data-fieldname*="+this.df.options+"]").val(); | |||
} | |||
var options = frappe.model.get_value(this.df.parent, this.docname, this.df.options); | |||
// if(!options) { | |||
// frappe.msgprint(__("Please set {0} first", | |||
// [frappe.meta.get_docfield(this.df.parent, this.df.options, this.docname).label])); | |||
// } | |||
return options; | |||
}, | |||
}); |
@@ -0,0 +1,27 @@ | |||
frappe.ui.form.ControlFloat = frappe.ui.form.ControlInt.extend({ | |||
parse: function(value) { | |||
return isNaN(parseFloat(value)) ? null : flt(value, this.get_precision()); | |||
}, | |||
format_for_input: function(value) { | |||
var number_format; | |||
if (this.df.fieldtype==="Float" && this.df.options && this.df.options.trim()) { | |||
number_format = this.get_number_format(); | |||
} | |||
var formatted_value = format_number(parseFloat(value), number_format, this.get_precision()); | |||
return isNaN(parseFloat(value)) ? "" : formatted_value; | |||
}, | |||
// even a float field can be formatted based on currency format instead of float format | |||
get_number_format: function() { | |||
var currency = frappe.meta.get_field_currency(this.df, this.get_doc()); | |||
return get_number_format(currency); | |||
}, | |||
get_precision: function() { | |||
// round based on field precision or float precision, else don't round | |||
return this.df.precision || cint(frappe.boot.sysdefaults.float_precision, null); | |||
} | |||
}); | |||
frappe.ui.form.ControlPercent = frappe.ui.form.ControlFloat; |
@@ -0,0 +1,5 @@ | |||
frappe.ui.form.ControlHeading = frappe.ui.form.ControlHTML.extend({ | |||
get_content: function() { | |||
return "<h4>" + __(this.df.label) + "</h4>"; | |||
} | |||
}); |
@@ -0,0 +1,26 @@ | |||
frappe.ui.form.ControlHTML = frappe.ui.form.Control.extend({ | |||
make: function() { | |||
this._super(); | |||
this.disp_area = this.wrapper; | |||
}, | |||
refresh_input: function() { | |||
var content = this.get_content(); | |||
if(content) this.$wrapper.html(content); | |||
}, | |||
get_content: function() { | |||
return this.df.options || ""; | |||
}, | |||
html: function(html) { | |||
this.$wrapper.html(html || this.get_content()); | |||
}, | |||
set_value: function(html) { | |||
if(html.appendTo) { | |||
// jquery object | |||
html.appendTo(this.$wrapper.empty()); | |||
} else { | |||
// html | |||
this.df.options = html; | |||
this.html(html); | |||
} | |||
} | |||
}); |
@@ -0,0 +1,22 @@ | |||
frappe.ui.form.ControlImage = frappe.ui.form.Control.extend({ | |||
make: function() { | |||
this._super(); | |||
this.$wrapper.css({"margin": "0px"}); | |||
this.$body = $("<div></div>").appendTo(this.$wrapper) | |||
.css({"margin-bottom": "10px"}); | |||
$('<div class="clearfix"></div>').appendTo(this.$wrapper); | |||
}, | |||
refresh_input: function() { | |||
this.$body.empty(); | |||
var doc = this.get_doc(); | |||
if(doc && this.df.options && doc[this.df.options]) { | |||
this.$img = $("<img src='"+doc[this.df.options]+"' class='img-responsive'>") | |||
.appendTo(this.$body); | |||
} else { | |||
this.$buffer = $("<div class='missing-image'><i class='octicon octicon-circle-slash'></i></div>") | |||
.appendTo(this.$body); | |||
} | |||
return false; | |||
} | |||
}); |
@@ -0,0 +1,25 @@ | |||
frappe.ui.form.ControlInt = frappe.ui.form.ControlData.extend({ | |||
make: function() { | |||
this._super(); | |||
// $(this.label_area).addClass('pull-right'); | |||
// $(this.disp_area).addClass('text-right'); | |||
}, | |||
make_input: function() { | |||
var me = this; | |||
this._super(); | |||
this.$input | |||
// .addClass("text-right") | |||
.on("focus", function() { | |||
setTimeout(function() { | |||
if(!document.activeElement) return; | |||
document.activeElement.value | |||
= me.validate(document.activeElement.value); | |||
document.activeElement.select(); | |||
}, 100); | |||
return false; | |||
}); | |||
}, | |||
parse: function(value) { | |||
return cint(value, null); | |||
} | |||
}); |
@@ -0,0 +1,382 @@ | |||
// special features for link | |||
// buttons | |||
// autocomplete | |||
// link validation | |||
// custom queries | |||
// add_fetches | |||
frappe.ui.form.ControlLink = frappe.ui.form.ControlData.extend({ | |||
make_input: function() { | |||
var me = this; | |||
// line-height: 1 is for Mozilla 51, shows extra padding otherwise | |||
$('<div class="link-field ui-front" style="position: relative; line-height: 1;">\ | |||
<input type="text" class="input-with-feedback form-control">\ | |||
<span class="link-btn">\ | |||
<a class="btn-open no-decoration" title="' + __("Open Link") + '">\ | |||
<i class="octicon octicon-arrow-right"></i></a>\ | |||
</span>\ | |||
</div>').prependTo(this.input_area); | |||
this.$input_area = $(this.input_area); | |||
this.$input = this.$input_area.find('input'); | |||
this.$link = this.$input_area.find('.link-btn'); | |||
this.$link_open = this.$link.find('.btn-open'); | |||
this.set_input_attributes(); | |||
this.$input.on("focus", function() { | |||
setTimeout(function() { | |||
if(me.$input.val() && me.get_options()) { | |||
me.$link.toggle(true); | |||
me.$link_open.attr('href', '#Form/' + me.get_options() + '/' + me.$input.val()); | |||
} | |||
if(!me.$input.val()) { | |||
me.$input.val("").trigger("input"); | |||
} | |||
}, 500); | |||
}); | |||
this.$input.on("blur", function() { | |||
// if this disappears immediately, the user's click | |||
// does not register, hence timeout | |||
setTimeout(function() { | |||
me.$link.toggle(false); | |||
}, 500); | |||
}); | |||
this.input = this.$input.get(0); | |||
this.has_input = true; | |||
this.translate_values = true; | |||
this.setup_buttons(); | |||
this.setup_awesomeplete(); | |||
if(this.df.change) { | |||
this.$input.on("change", function() { | |||
me.df.change.apply(this); | |||
}); | |||
} | |||
}, | |||
get_options: function() { | |||
return this.df.options; | |||
}, | |||
setup_buttons: function() { | |||
if(this.only_input && !this.with_link_btn) { | |||
this.$input_area.find(".link-btn").remove(); | |||
} | |||
}, | |||
open_advanced_search: function() { | |||
var doctype = this.get_options(); | |||
if(!doctype) return; | |||
new frappe.ui.form.LinkSelector({ | |||
doctype: doctype, | |||
target: this, | |||
txt: this.get_input_value() | |||
}); | |||
return false; | |||
}, | |||
new_doc: function() { | |||
var doctype = this.get_options(); | |||
var me = this; | |||
if(!doctype) return; | |||
// set values to fill in the new document | |||
if(this.df.get_route_options_for_new_doc) { | |||
frappe.route_options = this.df.get_route_options_for_new_doc(this); | |||
} else { | |||
frappe.route_options = {}; | |||
} | |||
// partially entered name field | |||
frappe.route_options.name_field = this.get_value(); | |||
// reference to calling link | |||
frappe._from_link = this; | |||
frappe._from_link_scrollY = $(document).scrollTop(); | |||
frappe.ui.form.make_quick_entry(doctype, (doc) => { | |||
return me.set_value(doc.name); | |||
}); | |||
return false; | |||
}, | |||
setup_awesomeplete: function() { | |||
var me = this; | |||
this.$input.cache = {}; | |||
this.awesomplete = new Awesomplete(me.input, { | |||
minChars: 0, | |||
maxItems: 99, | |||
autoFirst: true, | |||
list: [], | |||
data: function (item) { | |||
return { | |||
label: item.label || item.value, | |||
value: item.value | |||
}; | |||
}, | |||
filter: function() { | |||
return true; | |||
}, | |||
item: function (item) { | |||
var d = this.get_item(item.value); | |||
if(!d.label) { d.label = d.value; } | |||
var _label = (me.translate_values) ? __(d.label) : d.label; | |||
var html = "<strong>" + _label + "</strong>"; | |||
if(d.description && d.value!==d.description) { | |||
html += '<br><span class="small">' + __(d.description) + '</span>'; | |||
} | |||
return $('<li></li>') | |||
.data('item.autocomplete', d) | |||
.prop('aria-selected', 'false') | |||
.html('<a><p>' + html + '</p></a>') | |||
.get(0); | |||
}, | |||
sort: function() { | |||
return 0; | |||
} | |||
}); | |||
this.$input.on("input", function(e) { | |||
var doctype = me.get_options(); | |||
if(!doctype) return; | |||
if (!me.$input.cache[doctype]) { | |||
me.$input.cache[doctype] = {}; | |||
} | |||
var term = e.target.value; | |||
if (me.$input.cache[doctype][term]!=null) { | |||
// immediately show from cache | |||
me.awesomplete.list = me.$input.cache[doctype][term]; | |||
} | |||
var args = { | |||
'txt': term, | |||
'doctype': doctype, | |||
}; | |||
me.set_custom_query(args); | |||
frappe.call({ | |||
type: "GET", | |||
method:'frappe.desk.search.search_link', | |||
no_spinner: true, | |||
args: args, | |||
callback: function(r) { | |||
if(!me.$input.is(":focus")) { | |||
return; | |||
} | |||
if(!me.df.only_select) { | |||
if(frappe.model.can_create(doctype) | |||
&& me.df.fieldtype !== "Dynamic Link") { | |||
// new item | |||
r.results.push({ | |||
label: "<span class='text-primary link-option'>" | |||
+ "<i class='fa fa-plus' style='margin-right: 5px;'></i> " | |||
+ __("Create a new {0}", [__(me.df.options)]) | |||
+ "</span>", | |||
value: "create_new__link_option", | |||
action: me.new_doc | |||
}); | |||
} | |||
// advanced search | |||
r.results.push({ | |||
label: "<span class='text-primary link-option'>" | |||
+ "<i class='fa fa-search' style='margin-right: 5px;'></i> " | |||
+ __("Advanced Search") | |||
+ "</span>", | |||
value: "advanced_search__link_option", | |||
action: me.open_advanced_search | |||
}); | |||
} | |||
me.$input.cache[doctype][term] = r.results; | |||
me.awesomplete.list = me.$input.cache[doctype][term]; | |||
} | |||
}); | |||
}); | |||
this.$input.on("blur", function() { | |||
if(me.selected) { | |||
me.selected = false; | |||
return; | |||
} | |||
var value = me.get_input_value(); | |||
if(value!==me.last_value) { | |||
me.parse_validate_and_set_in_model(value); | |||
} | |||
}); | |||
this.$input.on("awesomplete-open", function() { | |||
me.$wrapper.css({"z-index": 100}); | |||
me.$wrapper.find('ul').css({"z-index": 100}); | |||
me.autocomplete_open = true; | |||
}); | |||
this.$input.on("awesomplete-close", function() { | |||
me.$wrapper.css({"z-index": 1}); | |||
me.autocomplete_open = false; | |||
}); | |||
this.$input.on("awesomplete-select", function(e) { | |||
var o = e.originalEvent; | |||
var item = me.awesomplete.get_item(o.text.value); | |||
me.autocomplete_open = false; | |||
// prevent selection on tab | |||
var TABKEY = 9; | |||
if(e.keyCode === TABKEY) { | |||
e.preventDefault(); | |||
me.awesomplete.close(); | |||
return false; | |||
} | |||
if(item.action) { | |||
item.value = ""; | |||
item.action.apply(me); | |||
} | |||
// if remember_last_selected is checked in the doctype against the field, | |||
// then add this value | |||
// to defaults so you do not need to set it again | |||
// unless it is changed. | |||
if(me.df.remember_last_selected_value) { | |||
frappe.boot.user.last_selected_values[me.df.options] = item.value; | |||
} | |||
me.parse_validate_and_set_in_model(item.value); | |||
}); | |||
this.$input.on("awesomplete-selectcomplete", function(e) { | |||
var o = e.originalEvent; | |||
if(o.text.value.indexOf("__link_option") !== -1) { | |||
me.$input.val(""); | |||
} | |||
}); | |||
}, | |||
set_custom_query: function(args) { | |||
var set_nulls = function(obj) { | |||
$.each(obj, function(key, value) { | |||
if(value!==undefined) { | |||
obj[key] = value; | |||
} | |||
}); | |||
return obj; | |||
}; | |||
if(this.get_query || this.df.get_query) { | |||
var get_query = this.get_query || this.df.get_query; | |||
if($.isPlainObject(get_query)) { | |||
var filters = null; | |||
if(get_query.filters) { | |||
// passed as {'filters': {'key':'value'}} | |||
filters = get_query.filters; | |||
} else if(get_query.query) { | |||
// passed as {'query': 'path.to.method'} | |||
args.query = get_query; | |||
} else { | |||
// dict is filters | |||
filters = get_query; | |||
} | |||
if (filters) { | |||
filters = set_nulls(filters); | |||
// extend args for custom functions | |||
$.extend(args, filters); | |||
// add "filters" for standard query (search.py) | |||
args.filters = filters; | |||
} | |||
} else if(typeof(get_query)==="string") { | |||
args.query = get_query; | |||
} else { | |||
// get_query by function | |||
var q = (get_query)(this.frm && this.frm.doc || this.doc, this.doctype, this.docname); | |||
if (typeof(q)==="string") { | |||
// returns a string | |||
args.query = q; | |||
} else if($.isPlainObject(q)) { | |||
// returns a plain object with filters | |||
if(q.filters) { | |||
set_nulls(q.filters); | |||
} | |||
// turn off value translation | |||
if(q.translate_values !== undefined) { | |||
this.translate_values = q.translate_values; | |||
} | |||
// extend args for custom functions | |||
$.extend(args, q); | |||
// add "filters" for standard query (search.py) | |||
args.filters = q.filters; | |||
} | |||
} | |||
} | |||
if(this.df.filters) { | |||
set_nulls(this.df.filters); | |||
if(!args.filters) args.filters = {}; | |||
$.extend(args.filters, this.df.filters); | |||
} | |||
}, | |||
validate: function(value) { | |||
// validate the value just entered | |||
if(this.df.options=="[Select]" || this.df.ignore_link_validation) { | |||
return value; | |||
} | |||
return this.validate_link_and_fetch(this.df, this.get_options(), | |||
this.docname, value); | |||
}, | |||
validate_link_and_fetch: function(df, doctype, docname, value) { | |||
var me = this; | |||
if(value) { | |||
return new Promise((resolve) => { | |||
var fetch = ''; | |||
if(this.frm && this.frm.fetch_dict[df.fieldname]) { | |||
fetch = this.frm.fetch_dict[df.fieldname].columns.join(', '); | |||
} | |||
return frappe.call({ | |||
method:'frappe.desk.form.utils.validate_link', | |||
type: "GET", | |||
args: { | |||
'value': value, | |||
'options': doctype, | |||
'fetch': fetch | |||
}, | |||
no_spinner: true, | |||
callback: function(r) { | |||
if(r.message=='Ok') { | |||
if(r.fetch_values && docname) { | |||
me.set_fetch_values(df, docname, r.fetch_values); | |||
} | |||
resolve(r.valid_value); | |||
} else { | |||
resolve(""); | |||
} | |||
} | |||
}); | |||
}); | |||
} | |||
}, | |||
set_fetch_values: function(df, docname, fetch_values) { | |||
var fl = this.frm.fetch_dict[df.fieldname].fields; | |||
for(var i=0; i < fl.length; i++) { | |||
frappe.model.set_value(df.parent, docname, fl[i], fetch_values[i], df.fieldtype); | |||
} | |||
} | |||
}); | |||
if(Awesomplete) { | |||
Awesomplete.prototype.get_item = function(value) { | |||
return this._list.find(function(item) { | |||
return item.value === value; | |||
}); | |||
}; | |||
} | |||
@@ -0,0 +1,52 @@ | |||
frappe.ui.form.ControlPassword = frappe.ui.form.ControlData.extend({ | |||
input_type: "password", | |||
make: function() { | |||
this._super(); | |||
}, | |||
make_input: function() { | |||
var me = this; | |||
this._super(); | |||
this.$input.parent().append($('<span class="password-strength-indicator indicator"></span>')); | |||
this.$wrapper.find('.control-input-wrapper').append($('<p class="password-strength-message text-muted small hidden"></p>')); | |||
this.indicator = this.$wrapper.find('.password-strength-indicator'); | |||
this.message = this.$wrapper.find('.help-box'); | |||
this.$input.on('input', () => { | |||
var $this = $(this); | |||
clearTimeout($this.data('timeout')); | |||
$this.data('timeout', setTimeout(() => { | |||
var txt = me.$input.val(); | |||
me.get_password_strength(txt); | |||
}), 300); | |||
}); | |||
}, | |||
get_password_strength: function(value) { | |||
var me = this; | |||
frappe.call({ | |||
type: 'GET', | |||
method: 'frappe.core.doctype.user.user.test_password_strength', | |||
args: { | |||
new_password: value || '' | |||
}, | |||
callback: function(r) { | |||
if (r.message && r.message.entropy) { | |||
var score = r.message.score, | |||
feedback = r.message.feedback; | |||
feedback.crack_time_display = r.message.crack_time_display; | |||
var indicators = ['grey', 'red', 'orange', 'yellow', 'green']; | |||
me.set_strength_indicator(indicators[score]); | |||
} | |||
} | |||
}); | |||
}, | |||
set_strength_indicator: function(color) { | |||
var message = __("Include symbols, numbers and capital letters in the password"); | |||
this.indicator.removeClass().addClass('password-strength-indicator indicator ' + color); | |||
this.message.html(message).removeClass('hidden'); | |||
} | |||
}); |
@@ -0,0 +1,8 @@ | |||
frappe.ui.form.ControlReadOnly = frappe.ui.form.ControlData.extend({ | |||
get_status: function(explain) { | |||
var status = this._super(explain); | |||
if(status==="Write") | |||
status = "Read"; | |||
return; | |||
}, | |||
}); |
@@ -0,0 +1,64 @@ | |||
frappe.ui.form.ControlSelect = frappe.ui.form.ControlData.extend({ | |||
html_element: "select", | |||
make_input: function() { | |||
this._super(); | |||
this.set_options(); | |||
}, | |||
set_formatted_input: function(value) { | |||
// refresh options first - (new ones??) | |||
if(value==null) value = ''; | |||
this.set_options(value); | |||
// set in the input element | |||
this._super(value); | |||
// check if the value to be set is selected | |||
var input_value = ''; | |||
if(this.$input) { | |||
input_value = this.$input.val(); | |||
} | |||
if(value && input_value && value !== input_value) { | |||
// trying to set a non-existant value | |||
// model value must be same as whatever the input is | |||
this.set_model_value(input_value); | |||
} | |||
}, | |||
set_options: function(value) { | |||
// reset options, if something new is set | |||
var options = this.df.options || []; | |||
if(typeof this.df.options==="string") { | |||
options = this.df.options.split("\n"); | |||
} | |||
// nothing changed | |||
if(options.toString() === this.last_options) { | |||
return; | |||
} | |||
this.last_options = options.toString(); | |||
if(this.$input) { | |||
var selected = this.$input.find(":selected").val(); | |||
this.$input.empty().add_options(options || []); | |||
if(value===undefined && selected) { | |||
this.$input.val(selected); | |||
} | |||
} | |||
}, | |||
get_file_attachment_list: function() { | |||
if(!this.frm) return; | |||
var fl = frappe.model.docinfo[this.frm.doctype][this.frm.docname]; | |||
if(fl && fl.attachments) { | |||
this.set_description(""); | |||
var options = [""]; | |||
$.each(fl.attachments, function(i, f) { | |||
options.push(f.file_url); | |||
}); | |||
return options; | |||
} else { | |||
this.set_description(__("Please attach a file first.")); | |||
return [""]; | |||
} | |||
} | |||
}); |
@@ -0,0 +1,120 @@ | |||
frappe.ui.form.ControlSignature = frappe.ui.form.ControlData.extend({ | |||
saving: false, | |||
loading: false, | |||
make: function() { | |||
var me = this; | |||
this._super(); | |||
// make jSignature field | |||
this.body = $('<div class="signature-field"></div>').appendTo(me.wrapper); | |||
this.make_pad(); | |||
this.img_wrapper = $(`<div class="signature-display"> | |||
<div class="missing-image attach-missing-image"> | |||
<i class="octicon octicon-circle-slash"></i> | |||
</div></div>`) | |||
.appendTo(this.wrapper); | |||
this.img = $("<img class='img-responsive attach-image-display'>") | |||
.appendTo(this.img_wrapper).toggle(false); | |||
}, | |||
make_pad: function() { | |||
let width = this.body.width(); | |||
if (width > 0 && !this.$pad) { | |||
this.$pad = this.body.jSignature({ | |||
height: 300, | |||
width: this.body.width(), | |||
lineWidth: 0.8 | |||
}).on('change', | |||
this.on_save_sign.bind(this)); | |||
this.load_pad(); | |||
this.$reset_button_wrapper = $(`<div class="signature-btn-row"> | |||
<a href="#" type="button" class="signature-reset btn btn-default"> | |||
<i class="glyphicon glyphicon-repeat"></i></a>`) | |||
.appendTo(this.$pad) | |||
.on("click", '.signature-reset', () => { | |||
this.on_reset_sign(); | |||
return false; | |||
}); | |||
} | |||
}, | |||
refresh_input: function(e) { | |||
// prevent to load the second time | |||
this.make_pad(); | |||
this.$wrapper.find(".control-input").toggle(false); | |||
this.set_editable(this.get_status()=="Write"); | |||
this.load_pad(); | |||
if(this.get_status()=="Read") { | |||
$(this.disp_area).toggle(false); | |||
} | |||
}, | |||
set_image: function(value) { | |||
if(value) { | |||
$(this.img_wrapper).find(".missing-image").toggle(false); | |||
this.img.attr("src", value).toggle(true); | |||
} else { | |||
$(this.img_wrapper).find(".missing-image").toggle(true); | |||
this.img.toggle(false); | |||
} | |||
}, | |||
load_pad: function() { | |||
// make sure not triggered during saving | |||
if (this.saving) return; | |||
// get value | |||
var value = this.get_value(); | |||
// import data for pad | |||
if (this.$pad) { | |||
this.loading = true; | |||
// reset in all cases | |||
this.$pad.jSignature('reset'); | |||
if (value) { | |||
// load the image to find out the size, because scaling will affect | |||
// stroke width | |||
try { | |||
this.$pad.jSignature('setData', value); | |||
this.set_image(value); | |||
} | |||
catch (e){ | |||
console.log("Cannot set data for signature", value, e); | |||
} | |||
} | |||
this.loading = false; | |||
} | |||
}, | |||
set_editable: function(editable) { | |||
this.$pad && this.$pad.toggle(editable); | |||
this.img_wrapper.toggle(!editable); | |||
if (this.$reset_button_wrapper) { | |||
this.$reset_button_wrapper.toggle(editable); | |||
if (editable) { | |||
this.$reset_button_wrapper.addClass('editing'); | |||
} | |||
else { | |||
this.$reset_button_wrapper.removeClass('editing'); | |||
} | |||
} | |||
}, | |||
set_my_value: function(value) { | |||
if (this.saving || this.loading) return; | |||
this.saving = true; | |||
this.set_value(value); | |||
this.saving = false; | |||
}, | |||
get_value: function() { | |||
return this.value ? this.value: this.get_model_value(); | |||
}, | |||
// reset signature canvas | |||
on_reset_sign: function() { | |||
this.$pad.jSignature("reset"); | |||
this.set_my_value(""); | |||
}, | |||
// save signature value to model and display | |||
on_save_sign: function() { | |||
if (this.saving || this.loading) return; | |||
var base64_img = this.$pad.jSignature("getData"); | |||
this.set_my_value(base64_img); | |||
this.set_image(this.get_value()); | |||
} | |||
}); |
@@ -0,0 +1,30 @@ | |||
frappe.ui.form.ControlTable = frappe.ui.form.Control.extend({ | |||
make: function() { | |||
this._super(); | |||
// add title if prev field is not column / section heading or html | |||
this.grid = new frappe.ui.form.Grid({ | |||
frm: this.frm, | |||
df: this.df, | |||
perm: this.perm || (this.frm && this.frm.perm) || this.df.perm, | |||
parent: this.wrapper | |||
}); | |||
if(this.frm) { | |||
this.frm.grids[this.frm.grids.length] = this; | |||
} | |||
// description | |||
if(this.df.description) { | |||
$('<p class="text-muted small">' + __(this.df.description) + '</p>') | |||
.appendTo(this.wrapper); | |||
} | |||
}, | |||
refresh_input: function() { | |||
this.grid.refresh(); | |||
}, | |||
get_value: function() { | |||
if(this.grid) { | |||
return this.grid.get_data(); | |||
} | |||
} | |||
}); |
@@ -0,0 +1,20 @@ | |||
frappe.ui.form.ControlText = frappe.ui.form.ControlData.extend({ | |||
html_element: "textarea", | |||
horizontal: false, | |||
make_wrapper: function() { | |||
this._super(); | |||
this.$wrapper.find(".like-disabled-input").addClass("for-description"); | |||
}, | |||
make_input: function() { | |||
this._super(); | |||
this.$input.css({'height': '300px'}); | |||
} | |||
}); | |||
frappe.ui.form.ControlLongText = frappe.ui.form.ControlText; | |||
frappe.ui.form.ControlSmallText = frappe.ui.form.ControlText.extend({ | |||
make_input: function() { | |||
this._super(); | |||
this.$input.css({'height': '150px'}); | |||
} | |||
}); |
@@ -0,0 +1,297 @@ | |||
frappe.ui.form.ControlTextEditor = frappe.ui.form.ControlCode.extend({ | |||
make_input: function() { | |||
this.has_input = true; | |||
this.make_editor(); | |||
this.hide_elements_on_mobile(); | |||
this.setup_drag_drop(); | |||
this.setup_image_dialog(); | |||
this.setting_count = 0; | |||
}, | |||
make_editor: function() { | |||
var me = this; | |||
this.editor = $("<div>").appendTo(this.input_area); | |||
// Note: while updating summernote, please make sure all 'p' blocks | |||
// in the summernote source code are replaced by 'div' blocks. | |||
// by default summernote, adds <p> blocks for new paragraphs, which adds | |||
// unexpected whitespaces, esp for email replies. | |||
this.editor.summernote({ | |||
minHeight: 400, | |||
toolbar: [ | |||
['magic', ['style']], | |||
['style', ['bold', 'italic', 'underline', 'clear']], | |||
['fontsize', ['fontsize']], | |||
['color', ['color']], | |||
['para', ['ul', 'ol', 'paragraph', 'hr']], | |||
//['height', ['height']], | |||
['media', ['link', 'picture', 'video', 'table']], | |||
['misc', ['fullscreen', 'codeview']] | |||
], | |||
keyMap: { | |||
pc: { | |||
'CTRL+ENTER': '' | |||
}, | |||
mac: { | |||
'CMD+ENTER': '' | |||
} | |||
}, | |||
prettifyHtml: true, | |||
dialogsInBody: true, | |||
callbacks: { | |||
onInit: function() { | |||
// firefox hack that puts the caret in the wrong position | |||
// when div is empty. To fix, seed with a <br>. | |||
// See https://bugzilla.mozilla.org/show_bug.cgi?id=550434 | |||
// this function is executed only once | |||
$(".note-editable[contenteditable='true']").one('focus', function() { | |||
var $this = $(this); | |||
$this.html($this.html() + '<br>'); | |||
}); | |||
}, | |||
onChange: function(value) { | |||
me.parse_validate_and_set_in_model(value); | |||
}, | |||
onKeydown: function(e) { | |||
me._last_change_on = new Date(); | |||
var key = frappe.ui.keys.get_key(e); | |||
// prevent 'New DocType (Ctrl + B)' shortcut in editor | |||
if(['ctrl+b', 'meta+b'].indexOf(key) !== -1) { | |||
e.stopPropagation(); | |||
} | |||
if(key.indexOf('escape') !== -1) { | |||
if(me.note_editor.hasClass('fullscreen')) { | |||
// exit fullscreen on escape key | |||
me.note_editor | |||
.find('.note-btn.btn-fullscreen') | |||
.trigger('click'); | |||
} | |||
} | |||
}, | |||
}, | |||
icons: { | |||
'align': 'fa fa-align', | |||
'alignCenter': 'fa fa-align-center', | |||
'alignJustify': 'fa fa-align-justify', | |||
'alignLeft': 'fa fa-align-left', | |||
'alignRight': 'fa fa-align-right', | |||
'indent': 'fa fa-indent', | |||
'outdent': 'fa fa-outdent', | |||
'arrowsAlt': 'fa fa-arrows-alt', | |||
'bold': 'fa fa-bold', | |||
'caret': 'caret', | |||
'circle': 'fa fa-circle', | |||
'close': 'fa fa-close', | |||
'code': 'fa fa-code', | |||
'eraser': 'fa fa-eraser', | |||
'font': 'fa fa-font', | |||
'frame': 'fa fa-frame', | |||
'italic': 'fa fa-italic', | |||
'link': 'fa fa-link', | |||
'unlink': 'fa fa-chain-broken', | |||
'magic': 'fa fa-magic', | |||
'menuCheck': 'fa fa-check', | |||
'minus': 'fa fa-minus', | |||
'orderedlist': 'fa fa-list-ol', | |||
'pencil': 'fa fa-pencil', | |||
'picture': 'fa fa-image', | |||
'question': 'fa fa-question', | |||
'redo': 'fa fa-redo', | |||
'square': 'fa fa-square', | |||
'strikethrough': 'fa fa-strikethrough', | |||
'subscript': 'fa fa-subscript', | |||
'superscript': 'fa fa-superscript', | |||
'table': 'fa fa-table', | |||
'textHeight': 'fa fa-text-height', | |||
'trash': 'fa fa-trash', | |||
'underline': 'fa fa-underline', | |||
'undo': 'fa fa-undo', | |||
'unorderedlist': 'fa fa-list-ul', | |||
'video': 'fa fa-video-camera' | |||
} | |||
}); | |||
this.note_editor = $(this.input_area).find('.note-editor'); | |||
// to fix <p> on enter | |||
//this.set_formatted_input('<div><br></div>'); | |||
}, | |||
setup_drag_drop: function() { | |||
var me = this; | |||
this.note_editor.on('dragenter dragover', false) | |||
.on('drop', function(e) { | |||
var dataTransfer = e.originalEvent.dataTransfer; | |||
if (dataTransfer && dataTransfer.files && dataTransfer.files.length) { | |||
me.note_editor.focus(); | |||
var files = [].slice.call(dataTransfer.files); | |||
files.forEach(file => { | |||
me.get_image(file, (url) => { | |||
me.editor.summernote('insertImage', url, file.name); | |||
}); | |||
}); | |||
} | |||
e.preventDefault(); | |||
e.stopPropagation(); | |||
}); | |||
}, | |||
get_image: function (fileobj, callback) { | |||
var freader = new FileReader(); | |||
freader.onload = function() { | |||
var dataurl = freader.result; | |||
// add filename to dataurl | |||
var parts = dataurl.split(","); | |||
parts[0] += ";filename=" + fileobj.name; | |||
dataurl = parts[0] + ',' + parts[1]; | |||
callback(dataurl); | |||
}; | |||
freader.readAsDataURL(fileobj); | |||
}, | |||
hide_elements_on_mobile: function() { | |||
this.note_editor.find('.note-btn-underline,\ | |||
.note-btn-italic, .note-fontsize,\ | |||
.note-color, .note-height, .btn-codeview') | |||
.addClass('hidden-xs'); | |||
if($('.toggle-sidebar').is(':visible')) { | |||
// disable tooltips on mobile | |||
this.note_editor.find('.note-btn') | |||
.attr('data-original-title', ''); | |||
} | |||
}, | |||
get_input_value: function() { | |||
return this.editor? this.editor.summernote('code'): ''; | |||
}, | |||
parse: function(value) { | |||
if(value == null) value = ""; | |||
return frappe.dom.remove_script_and_style(value); | |||
}, | |||
set_formatted_input: function(value) { | |||
if(value !== this.get_input_value()) { | |||
this.set_in_editor(value); | |||
} | |||
}, | |||
set_in_editor: function(value) { | |||
// set values in editor only if | |||
// 1. value not be set in the last 500ms | |||
// 2. user has not typed anything in the last 3seconds | |||
// --- | |||
// we will attempt to cleanup the user's DOM, hence if this happens | |||
// in the middle of the user is typing, it creates a lot of issues | |||
// also firefox tends to reset the cursor for some reason if the values | |||
// are reset | |||
if(this.setting_count > 2) { | |||
// we don't understand how the internal triggers work, | |||
// so if someone is setting the value third time, then quit | |||
return; | |||
} | |||
this.setting_count += 1; | |||
let time_since_last_keystroke = moment() - moment(this._last_change_on); | |||
if(!this._last_change_on || (time_since_last_keystroke > 3000)) { | |||
setTimeout(() => this.setting_count = 0, 500); | |||
this.editor.summernote('code', value || ''); | |||
} else { | |||
this._setting_value = setInterval(() => { | |||
if(time_since_last_keystroke > 3000) { | |||
if(this.last_value !== this.get_input_value()) { | |||
// if not already in sync, reset | |||
this.editor.summernote('code', this.last_value || ''); | |||
} | |||
clearInterval(this._setting_value); | |||
this._setting_value = null; | |||
this.setting_count = 0; | |||
} | |||
}, 1000); | |||
} | |||
}, | |||
set_focus: function() { | |||
return this.editor.summernote('focus'); | |||
}, | |||
set_upload_options: function() { | |||
var me = this; | |||
this.upload_options = { | |||
parent: this.image_dialog.get_field("upload_area").$wrapper, | |||
args: {}, | |||
max_width: this.df.max_width, | |||
max_height: this.df.max_height, | |||
options: "Image", | |||
btn: this.image_dialog.set_primary_action(__("Insert")), | |||
on_no_attach: function() { | |||
// if no attachmemts, | |||
// check if something is selected | |||
var selected = me.image_dialog.get_field("select").get_value(); | |||
if(selected) { | |||
me.editor.summernote('insertImage', selected); | |||
me.image_dialog.hide(); | |||
} else { | |||
frappe.msgprint(__("Please attach a file or set a URL")); | |||
} | |||
}, | |||
callback: function(attachment) { | |||
me.editor.summernote('insertImage', attachment.file_url, attachment.file_name); | |||
me.image_dialog.hide(); | |||
}, | |||
onerror: function() { | |||
me.image_dialog.hide(); | |||
} | |||
}; | |||
if ("is_private" in this.df) { | |||
this.upload_options.is_private = this.df.is_private; | |||
} | |||
if(this.frm) { | |||
this.upload_options.args = { | |||
from_form: 1, | |||
doctype: this.frm.doctype, | |||
docname: this.frm.docname | |||
}; | |||
} else { | |||
this.upload_options.on_attach = function(fileobj, dataurl) { | |||
me.editor.summernote('insertImage', dataurl); | |||
me.image_dialog.hide(); | |||
frappe.hide_progress(); | |||
}; | |||
} | |||
}, | |||
setup_image_dialog: function() { | |||
this.note_editor.find('[data-original-title="Image"]').on('click', () => { | |||
if(!this.image_dialog) { | |||
this.image_dialog = new frappe.ui.Dialog({ | |||
title: __("Image"), | |||
fields: [ | |||
{fieldtype:"HTML", fieldname:"upload_area"}, | |||
{fieldtype:"HTML", fieldname:"or_attach", options: __("Or")}, | |||
{fieldtype:"Select", fieldname:"select", label:__("Select from existing attachments") }, | |||
] | |||
}); | |||
} | |||
this.image_dialog.show(); | |||
this.image_dialog.get_field("upload_area").$wrapper.empty(); | |||
// select from existing attachments | |||
var attachments = this.frm && this.frm.attachments.get_attachments() || []; | |||
var select = this.image_dialog.get_field("select"); | |||
if(attachments.length) { | |||
attachments = $.map(attachments, function(o) { return o.file_url; }); | |||
select.df.options = [""].concat(attachments); | |||
select.toggle(true); | |||
this.image_dialog.get_field("or_attach").toggle(true); | |||
select.refresh(); | |||
} else { | |||
this.image_dialog.get_field("or_attach").toggle(false); | |||
select.toggle(false); | |||
} | |||
select.$input.val(""); | |||
this.set_upload_options(); | |||
frappe.upload.make(this.upload_options); | |||
}); | |||
} | |||
}); |
@@ -0,0 +1,44 @@ | |||
frappe.ui.form.ControlTime = frappe.ui.form.ControlData.extend({ | |||
make_input: function() { | |||
var me = this; | |||
this._super(); | |||
this.$input.datepicker({ | |||
language: "en", | |||
timepicker: true, | |||
onlyTimepicker: true, | |||
timeFormat: "hh:ii:ss", | |||
startDate: frappe.datetime.now_time(true), | |||
onSelect: function() { | |||
me.$input.trigger('change'); | |||
}, | |||
onShow: function() { | |||
$('.datepicker--button:visible').text(__('Now')); | |||
}, | |||
todayButton: frappe.datetime.now_time(true) | |||
}); | |||
this.datepicker = this.$input.data('datepicker'); | |||
this.refresh(); | |||
}, | |||
set_input: function(value) { | |||
this._super(value); | |||
if(value | |||
&& ((this.last_value && this.last_value !== this.value) | |||
|| (!this.datepicker.selectedDates.length))) { | |||
var date_obj = frappe.datetime.moment_to_date_obj(moment(value, 'hh:mm:ss')); | |||
this.datepicker.selectDate(date_obj); | |||
} | |||
}, | |||
set_description: function() { | |||
const { description } = this.df; | |||
const { time_zone } = frappe.sys_defaults; | |||
if (!frappe.datetime.is_timezone_same()) { | |||
if (!description) { | |||
this.df.description = time_zone; | |||
} else if (!description.includes(time_zone)) { | |||
this.df.description += '<br>' + time_zone; | |||
} | |||
} | |||
this._super(); | |||
} | |||
}); |
@@ -254,6 +254,9 @@ frappe.ui.get_upload_dialog = function(opts){ | |||
"reqd" : false, | |||
"filters": { | |||
'related_doctype': opts.args.doctype | |||
}, | |||
onchange: function(){ | |||
opts.args.gs_template = this.get_value(); | |||
} | |||
}, | |||
], | |||
@@ -264,12 +267,6 @@ frappe.ui.get_upload_dialog = function(opts){ | |||
dialog.show(); | |||
var upload_area = $('<div style="padding-bottom: 25px;"></div>').prependTo(dialog.body); | |||
var fd = dialog.fields_dict; | |||
$(fd.gs_template.input).change(function() { | |||
opts.args.gs_template = fd.gs_template.get_value(); | |||
}); | |||
frappe.upload.make({ | |||
parent: upload_area, | |||
args: opts.args, | |||
@@ -636,7 +636,7 @@ frappe.ui.form.Timeline = Class.extend({ | |||
communications = this.frm.get_docinfo().communications, | |||
email = this.get_recipient(); | |||
$.each(communications.sort(function(a, b) { return a.creation > b.creation ? -1 : 1 }), function(i, c) { | |||
$.each(communications && communications.sort(function(a, b) { return a.creation > b.creation ? -1 : 1 }), function(i, c) { | |||
if(c.communication_type=='Communication' && c.communication_medium=="Email") { | |||
if(from_recipient) { | |||
if(c.sender.indexOf(email)!==-1) { | |||
@@ -41,7 +41,7 @@ | |||
{{ data.user_info.abbr }}</div> | |||
{% } %} | |||
</span> | |||
<div class="asset-details"> | |||
<div class="asset-details" data-communication-type = "{{ data.communication_type }}"> | |||
<span class="author-wrap"> | |||
<i class="{%= data.icon %} hidden-xs fa-fw"></i> | |||
<span title="{%= data.comment_by %}">{%= data.fullname %}</span> | |||
@@ -28,6 +28,9 @@ frappe.ui.form.save = function (frm, action, callback, btn) { | |||
$(document).trigger("save", [frm.doc]); | |||
callback(r); | |||
}, | |||
error: function (r) { | |||
callback(r); | |||
}, | |||
btn: btn, | |||
freeze_message: freeze_message | |||
}); | |||
@@ -188,6 +191,7 @@ frappe.ui.form.save = function (frm, action, callback, btn) { | |||
callback: function (r) { | |||
opts.callback && opts.callback(r); | |||
}, | |||
error: opts.error, | |||
always: function (r) { | |||
$(btn).prop("disabled", false); | |||
frappe.ui.form.is_saving = false; | |||
@@ -152,6 +152,13 @@ frappe.ui.form.Toolbar = Class.extend({ | |||
this.page.add_menu_item(__("Reload"), function() { | |||
me.frm.reload_doc();}, true); | |||
// add to desktop | |||
if(me.frm.meta.issingle) { | |||
this.page.add_menu_item(__('Add to Desktop'), function () { | |||
frappe.add_to_desktop(me.frm.doctype, me.frm.doctype); | |||
}, true); | |||
} | |||
// delete | |||
if((cint(me.frm.doc.docstatus) != 1) && !me.frm.doc.__islocal | |||
&& frappe.model.can_delete(me.frm.doctype)) { | |||
@@ -431,8 +431,9 @@ frappe.views.ListRenderer = Class.extend({ | |||
return `<span class='indicator ${indicator[1]}' title='${__(indicator[0])}'></span>`; | |||
}, | |||
prepare_data: function (data) { | |||
if (data.modified) | |||
if (data.modified) { | |||
this.prepare_when(data, data.modified); | |||
} | |||
// nulls as strings | |||
for (var key in data) { | |||
@@ -452,8 +453,24 @@ frappe.views.ListRenderer = Class.extend({ | |||
var title_field = this.meta.title_field || 'name'; | |||
data._title = strip_html(data[title_field] || data.name); | |||
data._full_title = data._title; | |||
// check for duplicates | |||
// add suffix like (1), (2) etc | |||
if (data.name && this.values_map) { | |||
if (this.values_map[data.name]!==undefined) { | |||
if (this.values_map[data.name]===1) { | |||
// update first row! | |||
this.set_title_with_row_number(this.rows_map[data.name], 1); | |||
} | |||
this.values_map[data.name]++; | |||
this.set_title_with_row_number(data, this.values_map[data.name]); | |||
} else { | |||
this.values_map[data.name] = 1; | |||
this.rows_map[data.name] = data; | |||
} | |||
} | |||
data._full_title = data._title; | |||
data._workflow = null; | |||
if (this.workflow_state_fieldname) { | |||
@@ -493,6 +510,11 @@ frappe.views.ListRenderer = Class.extend({ | |||
return data; | |||
}, | |||
set_title_with_row_number: function (data, id) { | |||
data._title = data._title + ` (${__("Row")} ${id})`; | |||
data._full_title = data._title; | |||
}, | |||
prepare_when: function (data, date_str) { | |||
if (!date_str) date_str = data.modified; | |||
// when | |||
@@ -520,10 +542,12 @@ frappe.views.ListRenderer = Class.extend({ | |||
|| ($.isArray(this.required_libs) && this.required_libs.length); | |||
this.render_view = function (values) { | |||
me.values_map = {}; | |||
me.rows_map = {}; | |||
// prepare data before rendering view | |||
values = values.map(me.prepare_data.bind(this)); | |||
// remove duplicates | |||
values = values.uniqBy(value => value.name); | |||
// values = values.uniqBy(value => value.name); | |||
if (lib_exists) { | |||
me.load_lib(function () { | |||
@@ -66,7 +66,7 @@ frappe.tools.to_csv = function(data) { | |||
var res = []; | |||
$.each(data, function(i, row) { | |||
row = $.map(row, function(col) { | |||
return typeof(col)==="string" ? ('"' + col.replace(/"/g, '""') + '"') : col; | |||
return typeof(col)==="string" ? ('"' + $('<i>').html(col.replace(/"/g, '""')).text() + '"') : col; | |||
}); | |||
res.push(row.join(",")); | |||
}); | |||
@@ -369,7 +369,7 @@ frappe.ui.Page = Class.extend({ | |||
.appendTo(this.page_form); | |||
}, | |||
add_select: function(label, options) { | |||
var field = this.add_field({label:label, fieldtype:"Select"}) | |||
var field = this.add_field({label:label, fieldtype:"Select"}); | |||
return field.$wrapper.find("select").empty().add_options(options); | |||
}, | |||
add_data: function(label) { | |||
@@ -251,7 +251,13 @@ frappe.search.utils = { | |||
var level = me.fuzzy_search(keywords, item); | |||
if(level > 0) { | |||
var module = frappe.modules[item]; | |||
if(module._doctype) return; | |||
if (module._doctype) return; | |||
// disallow restricted modules | |||
if (frappe.boot.user.allow_modules && | |||
!frappe.boot.user.allow_modules.includes(module.module_name)) { | |||
return; | |||
} | |||
var ret = { | |||
type: "Module", | |||
label: __("Open {0}", [me.bolden_match_part(__(item), keywords)]), | |||
@@ -63,7 +63,7 @@ frappe.views.CommunicationComposer = Class.extend({ | |||
{label:__("Send As Email"), fieldtype:"Check", | |||
fieldname:"send_email"}, | |||
{label:__("Send me a copy"), fieldtype:"Check", | |||
fieldname:"send_me_a_copy"}, | |||
fieldname:"send_me_a_copy", 'default': frappe.boot.user.send_me_a_copy}, | |||
{label:__("Send Read Receipt"), fieldtype:"Check", | |||
fieldname:"send_read_receipt"}, | |||
{label:__("Communication Medium"), fieldtype:"Select", | |||
@@ -375,7 +375,14 @@ frappe.views.CommunicationComposer = Class.extend({ | |||
$(fields.select_print_format.wrapper).toggle(true); | |||
} | |||
$(fields.send_email.input).prop("checked", true) | |||
$(fields.send_email.input).prop("checked", 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(); | |||
frappe.db.set_value('User', frappe.session.user, 'send_me_a_copy', val); | |||
frappe.boot.user.send_me_a_copy = val; | |||
}); | |||
// toggle print format | |||
$(fields.send_email.input).click(function() { | |||
@@ -0,0 +1,91 @@ | |||
<!DOCTYPE html> | |||
<html lang="en"> | |||
<head> | |||
<meta charset="utf-8"> | |||
<meta http-equiv="X-UA-Compatible" content="IE=edge"> | |||
<meta name="viewport" content="width=device-width, initial-scale=1"> | |||
<meta name="description" content=""> | |||
<meta name="author" content=""> | |||
<title>{{ title }}</title> | |||
<link href="{{ base_url }}/assets/frappe/css/bootstrap.css" rel="stylesheet"> | |||
<link type="text/css" rel="stylesheet" | |||
href="{{ base_url }}/assets/frappe/css/font-awesome.css"> | |||
<link rel="stylesheet" type="text/css" href="{{ base_url }}/assets/frappe/css/tree.css"> | |||
<style> | |||
{{ print_css }} | |||
</style> | |||
<style> | |||
.tree.opened::before, | |||
.tree-node.opened::before, | |||
.tree:last-child::after, | |||
.tree-node:last-child::after { | |||
z-index: 1; | |||
border-left: 1px solid #d1d8dd; | |||
background: none; | |||
} | |||
.tree a, | |||
.tree-link { | |||
text-decoration: none; | |||
cursor: default; | |||
} | |||
.tree.opened > .tree-children > .tree-node > .tree-link::before, | |||
.tree-node.opened > .tree-children > .tree-node > .tree-link::before { | |||
border-top: 1px solid #d1d8dd; | |||
z-index: 1; | |||
background: none; | |||
} | |||
i.fa.fa-fw.fa-folder { | |||
z-index: 2; | |||
position: relative; | |||
} | |||
.tree:last-child::after, .tree-node:last-child::after { | |||
display: none; | |||
} | |||
.tree-node-toolbar { | |||
display: none; | |||
} | |||
i.octicon.octicon-primitive-dot.text-extra-muted { | |||
width: 7px; | |||
height: 7px; | |||
border-radius: 50%; | |||
background: #d1d8dd; | |||
display: inline-block; | |||
position: relative; | |||
z-index: 2; | |||
} | |||
@media (max-width: 767px) { | |||
ul.tree-children { | |||
padding-left: 20px; | |||
} | |||
} | |||
</style> | |||
</head> | |||
<body> | |||
<div class="print-format-gutter"> | |||
{% if print_settings.repeat_header_footer %} | |||
<div id="footer-html" class="visible-pdf"> | |||
{% if print_settings.letter_head && print_settings.letter_head.footer %} | |||
<div class="letter-head-footer"> | |||
{{ print_settings.letter_head.footer }} | |||
</div> | |||
{% endif %} | |||
<p class="text-center small page-number visible-pdf"> | |||
{{ __("Page {0} of {1}", [`<span class="page"></span>`, `<span class="topage"></span>`]) }} | |||
</p> | |||
</div> | |||
{% endif %} | |||
<div class="print-format {% if landscape %} landscape {% endif %}"> | |||
{% if print_settings.letter_head %} | |||
<div {% if print_settings.repeat_header_footer %} id="header-html" class="hidden-pdf" {% endif %}> | |||
<div class="letter-head">{{ print_settings.letter_head.header }}</div> | |||
</div> | |||
{% endif %} | |||
<div class="tree opened"> | |||
{{ tree }} | |||
</div> | |||
</div> | |||
</div> | |||
</body> | |||
</html> |
@@ -281,6 +281,18 @@ frappe.views.TreeView = Class.extend({ | |||
} | |||
}) | |||
}, | |||
print_tree: function() { | |||
if(!frappe.model.can_print(this.doctype)) { | |||
frappe.msgprint(__("You are not allowed to print this report")); | |||
return false; | |||
} | |||
var tree = $(".tree:visible").html(); | |||
var me = this; | |||
frappe.ui.get_print_settings(false, function(print_settings) { | |||
var title = __(me.docname || me.doctype); | |||
frappe.render_tree({title: title, tree: tree, print_settings:print_settings}); | |||
}); | |||
}, | |||
set_primary_action: function(){ | |||
var me = this; | |||
if (!this.opts.disable_add_node && this.can_create) { | |||
@@ -299,13 +311,20 @@ frappe.views.TreeView = Class.extend({ | |||
frappe.set_route('List', me.doctype); | |||
} | |||
}, | |||
{ | |||
label: __('Print'), | |||
action: function() { | |||
me.print_tree(); | |||
} | |||
}, | |||
{ | |||
label: __('Refresh'), | |||
action: function() { | |||
me.make_tree(); | |||
} | |||
}, | |||
] | |||
]; | |||
if (me.opts.menu_items) { | |||
me.menu_items.push.apply(me.menu_items, me.opts.menu_items) | |||
@@ -757,13 +757,18 @@ _f.Frm.prototype.savesubmit = function(btn, callback, on_error) { | |||
frappe.validated = true; | |||
me.script_manager.trigger("before_submit").then(function() { | |||
if(!frappe.validated) { | |||
if(on_error) | |||
if(on_error) { | |||
on_error(); | |||
} | |||
return; | |||
} | |||
return me.save('Submit', function(r) { | |||
if(!r.exc) { | |||
if(r.exc) { | |||
if (on_error) { | |||
on_error(); | |||
} | |||
} else { | |||
frappe.utils.play_sound("submit"); | |||
callback && callback(); | |||
me.script_manager.trigger("on_submit"); | |||
@@ -780,19 +785,22 @@ _f.Frm.prototype.savecancel = function(btn, callback, on_error) { | |||
frappe.validated = true; | |||
me.script_manager.trigger("before_cancel").then(function() { | |||
if(!frappe.validated) { | |||
if(on_error) | |||
if(on_error) { | |||
on_error(); | |||
} | |||
return; | |||
} | |||
var after_cancel = function(r) { | |||
if(!r.exc) { | |||
if(r.exc) { | |||
if (on_error) { | |||
on_error(); | |||
} | |||
} else { | |||
frappe.utils.play_sound("cancel"); | |||
me.refresh(); | |||
callback && callback(); | |||
me.script_manager.trigger("after_cancel"); | |||
} else { | |||
on_error(); | |||
} | |||
}; | |||
frappe.ui.form.save(me, "cancel", after_cancel, btn); | |||
@@ -1,7 +1,7 @@ | |||
/*! | |||
* FullCalendar v3.0.1 Print Stylesheet | |||
* Docs & License: http://fullcalendar.io/ | |||
* (c) 2016 Adam Shaw | |||
* FullCalendar v3.4.0 Print Stylesheet | |||
* Docs & License: https://fullcalendar.io/ | |||
* (c) 2017 Adam Shaw | |||
*/ | |||
/* | |||
@@ -1,9 +1,10 @@ | |||
/*! | |||
* FullCalendar v3.0.1 Google Calendar Plugin | |||
* Docs & License: http://fullcalendar.io/ | |||
* (c) 2016 Adam Shaw | |||
* FullCalendar v3.4.0 Google Calendar Plugin | |||
* Docs & License: https://fullcalendar.io/ | |||
* (c) 2017 Adam Shaw | |||
*/ | |||
(function(factory) { | |||
if (typeof define === 'function' && define.amd) { | |||
define([ 'jquery' ], factory); | |||
@@ -73,7 +74,7 @@ FC.sourceFetchers.push(function(sourceOptions, start, end, timezone) { | |||
function transformOptions(sourceOptions, start, end, timezone, calendar) { | |||
var url = API_BASE + '/' + encodeURIComponent(sourceOptions.googleCalendarId) + '/events?callback=?'; // jsonp | |||
var apiKey = sourceOptions.googleCalendarApiKey || calendar.options.googleCalendarApiKey; | |||
var apiKey = sourceOptions.googleCalendarApiKey || calendar.opt('googleCalendarApiKey'); | |||
var success = sourceOptions.success; | |||
var data; | |||
var timezoneArg; // populated when a specific timezone. escaped to Google's liking | |||
@@ -83,7 +84,7 @@ function transformOptions(sourceOptions, start, end, timezone, calendar) { | |||
// call error handlers | |||
(sourceOptions.googleCalendarError || $.noop).apply(calendar, errorObjs); | |||
(calendar.options.googleCalendarError || $.noop).apply(calendar, errorObjs); | |||
(calendar.opt('googleCalendarError') || $.noop).apply(calendar, errorObjs); | |||
// print error to debug console | |||
FC.warn.apply(null, [ message ].concat(apiErrorObjs || [])); | |||
@@ -122,4 +122,18 @@ frappe.render_grid = function(opts) { | |||
w.document.write(html); | |||
w.document.close(); | |||
}, | |||
frappe.render_tree = function(opts) { | |||
opts.base_url = frappe.urllib.get_base_url(); | |||
opts.landscape = false; | |||
opts.print_css = frappe.boot.print_css; | |||
var tree = frappe.render_template("print_tree", opts); | |||
var w = window.open(); | |||
if(!w) { | |||
frappe.msgprint(__("Please enable pop-ups in your browser")) | |||
} | |||
w.document.write(tree); | |||
w.document.close(); | |||
} |
@@ -194,6 +194,13 @@ hr { | |||
} | |||
} | |||
.screenshot { | |||
box-shadow: 0px 3px 6px rgba(0, 0, 0, 0.1); | |||
border: 1px solid @border-color; | |||
margin: 8px 0; | |||
max-width: 100%; | |||
} | |||
/* auto email report */ | |||
.report-title { | |||
margin-bottom: 20px; | |||
@@ -33,7 +33,7 @@ def clear_cache(user=None): | |||
cache = frappe.cache() | |||
groups = ("bootinfo", "user_recent", "roles", "user_doc", "lang", | |||
"defaults", "user_permissions", "roles", "home_page", "linked_with", | |||
"defaults", "user_permissions", "home_page", "linked_with", | |||
"desktop_icons", 'portal_menu_items') | |||
if user: | |||
@@ -7,6 +7,8 @@ from frappe.core.page.permission_manager.permission_manager import get_roles_and | |||
from frappe.desk.doctype.desktop_icon.desktop_icon import (get_desktop_icons, add_user_icon, | |||
clear_desktop_icons_cache) | |||
from frappe.core.doctype.domain_settings.domain_settings import get_active_modules | |||
class TestDomainification(unittest.TestCase): | |||
def setUp(self): | |||
# create test domain | |||
@@ -33,7 +35,7 @@ class TestDomainification(unittest.TestCase): | |||
def remove_from_active_domains(self, domain=None, remove_all=False): | |||
""" remove domain from domain settings """ | |||
if not domain: | |||
if not (domain or remove_all): | |||
return | |||
domain_settings = frappe.get_doc("Domain Settings", "Domain Settings") | |||
@@ -90,8 +92,8 @@ class TestDomainification(unittest.TestCase): | |||
# doctype should be hidden in desktop icon, role permissions | |||
results = get_roles_and_doctypes() | |||
self.assertTrue("Test Domainification" in results.get("doctypes")) | |||
self.assertTrue("_Test Role" in results.get("roles")) | |||
self.assertTrue("Test Domainification" in [d.get("value") for d in results.get("doctypes")]) | |||
self.assertTrue("_Test Role" in [d.get("value") for d in results.get("roles")]) | |||
self.add_active_domain("_Test Domain 2") | |||
test_doctype.restrict_to_domain = "_Test Domain 2" | |||
@@ -101,18 +103,18 @@ class TestDomainification(unittest.TestCase): | |||
test_role.save() | |||
results = get_roles_and_doctypes() | |||
self.assertTrue("Test Domainification" in results.get("doctypes")) | |||
self.assertTrue("_Test Role" in results.get("roles")) | |||
self.assertTrue("Test Domainification" in [d.get("value") for d in results.get("doctypes")]) | |||
self.assertTrue("_Test Role" in [d.get("value") for d in results.get("roles")]) | |||
self.remove_from_active_domains("_Test Domain 2") | |||
results = get_roles_and_doctypes() | |||
self.assertTrue("Test Domainification" not in results.get("doctypes")) | |||
self.assertTrue("_Test Role" not in results.get("roles")) | |||
self.assertTrue("Test Domainification" not in [d.get("value") for d in results.get("doctypes")]) | |||
self.assertTrue("_Test Role" not in [d.get("value") for d in results.get("roles")]) | |||
def test_desktop_icon_for_domainification(self): | |||
""" desktop icon should be hidden if doctype's restrict to domain is not in active domains """ | |||
test_doctype = self.new_doctype("Test Domainification") | |||
test_doctype.restrict_to_domain = "_Test Domain 2" | |||
test_doctype.insert() | |||
@@ -144,10 +146,14 @@ class TestDomainification(unittest.TestCase): | |||
self.add_active_domain("_Test Domain 2") | |||
modules = frappe.get_active_modules() | |||
modules = get_active_modules() | |||
self.assertTrue("Contacts" in modules) | |||
# doctype should be hidden from the desk | |||
self.remove_from_active_domains("_Test Domain 2") | |||
modules = frappe.get_active_modules() | |||
self.assertTrue("Test Module" not in modules) | |||
modules = get_active_modules() | |||
self.assertTrue("Contacts" not in modules) | |||
test_module_def = frappe.get_doc("Module Def", "Contacts") | |||
test_module_def.restrict_to_domain = "" | |||
test_module_def.save() |
@@ -11,3 +11,5 @@ frappe/core/doctype/report/test_query_report.js | |||
frappe/tests/ui/test_linked_with.js | |||
frappe/custom/doctype/customize_form/test_customize_form.js | |||
frappe/desk/doctype/event/test_event.js | |||
frappe/workflow/doctype/workflow/tests/test_workflow_create.js | |||
frappe/workflow/doctype/workflow/tests/test_workflow_test.js |
@@ -9,6 +9,7 @@ import frappe.share | |||
from frappe.utils import cint | |||
from frappe.boot import get_allowed_reports | |||
from frappe.permissions import get_roles, get_valid_perms | |||
from frappe.core.doctype.domain_settings.domain_settings import get_active_modules | |||
class UserPermissions: | |||
""" | |||
@@ -63,10 +64,13 @@ class UserPermissions: | |||
def build_doctype_map(self): | |||
"""build map of special doctype properties""" | |||
active_domains = frappe.get_active_domains() | |||
self.doctype_map = {} | |||
for r in frappe.db.sql("""select name, in_create, issingle, istable, | |||
read_only, module from tabDocType""", as_dict=1): | |||
self.doctype_map[r['name']] = r | |||
read_only, restrict_to_domain, module from tabDocType""", as_dict=1): | |||
if (not r.restrict_to_domain) or (r.restrict_to_domain in active_domains): | |||
self.doctype_map[r['name']] = r | |||
def build_perm_map(self): | |||
"""build map of permissions at level 0""" | |||
@@ -91,7 +95,7 @@ class UserPermissions: | |||
self.build_perm_map() | |||
user_shared = frappe.share.get_shared_doctypes() | |||
no_list_view_link = [] | |||
active_modules = frappe.get_active_modules() or [] | |||
active_modules = get_active_modules() or [] | |||
for dt in self.doctype_map: | |||
dtp = self.doctype_map[dt] | |||
@@ -193,8 +197,8 @@ class UserPermissions: | |||
def load_user(self): | |||
d = frappe.db.sql("""select email, first_name, last_name, creation, | |||
email_signature, user_type, language, background_image, background_style, mute_sounds | |||
from tabUser where name = %s""", (self.name,), as_dict=1)[0] | |||
email_signature, user_type, language, background_image, background_style, | |||
mute_sounds, send_me_a_copy from tabUser where name = %s""", (self.name,), as_dict=1)[0] | |||
if not self.can_read: | |||
self.build_permissions() | |||
@@ -12,7 +12,7 @@ $.extend(frappe, { | |||
_assets_loaded: [], | |||
require: function(url) { | |||
if(frappe._assets_loaded.indexOf(url)!==-1) return; | |||
$.ajax({ | |||
return $.ajax({ | |||
url: url, | |||
async: false, | |||
dataType: "text", | |||
@@ -0,0 +1,59 @@ | |||
QUnit.module('setup'); | |||
QUnit.test("Test Workflow", function(assert) { | |||
assert.expect(1); | |||
let done = assert.async(); | |||
frappe.run_serially([ | |||
() => { | |||
return frappe.tests.make('Workflow', [ | |||
{workflow_name: "Test User Workflow"}, | |||
{document_type: "User"}, | |||
{is_active: 1}, | |||
{override_status: 1}, | |||
{states: [ | |||
[ | |||
{state: 'Pending'}, | |||
{doc_status: 0}, | |||
{allow_edit: 'Administrator'} | |||
], | |||
[ | |||
{state: 'Approved'}, | |||
{doc_status: 1}, | |||
{allow_edit: 'Administrator'} | |||
], | |||
[ | |||
{state: 'Rejected'}, | |||
{doc_status: 2}, | |||
{allow_edit: 'Administrator'} | |||
] | |||
]}, | |||
{transitions: [ | |||
[ | |||
{state: 'Pending'}, | |||
{action: 'Review'}, | |||
{next_state: 'Pending'}, | |||
{allowed: 'Administrator'} | |||
], | |||
[ | |||
{state: 'Pending'}, | |||
{action: 'Approve'}, | |||
{next_state: 'Approved'}, | |||
{allowed: 'Administrator'} | |||
], | |||
[ | |||
{state: 'Approved'}, | |||
{action: 'Reject'}, | |||
{next_state: 'Rejected'}, | |||
{allowed: 'Administrator'} | |||
], | |||
]}, | |||
{workflow_state_field: 'workflow_state'} | |||
]); | |||
}, | |||
() => frappe.timeout(1), | |||
() => {assert.equal($('.msgprint').text(), "Created Custom Field workflow_state in User", "Workflow created");}, | |||
() => frappe.tests.click_button('Close'), | |||
() => done() | |||
]); | |||
}); |
@@ -0,0 +1,53 @@ | |||
QUnit.module('setup'); | |||
QUnit.test("Test Workflow", function(assert) { | |||
assert.expect(5); | |||
let done = assert.async(); | |||
frappe.run_serially([ | |||
() => frappe.set_route('Form', 'User', 'New User 1'), | |||
() => frappe.timeout(1), | |||
() => { | |||
cur_frm.set_value('email', 'test1@testmail.com'); | |||
cur_frm.set_value('first_name', 'Test Name'); | |||
cur_frm.set_value('send_welcome_email', 0); | |||
cur_frm.save(); | |||
}, | |||
() => frappe.timeout(2), | |||
() => frappe.tests.click_button('Actions'), | |||
() => frappe.timeout(0.5), | |||
() => { | |||
let review = $(`.dropdown-menu li:contains("Review"):visible`).size(); | |||
let approve = $(`.dropdown-menu li:contains("Approve"):visible`).size(); | |||
assert.equal(review, 1, "Review Action exists"); | |||
assert.equal(approve, 1, "Approve Action exists"); | |||
}, | |||
() => frappe.tests.click_dropdown_item('Approve'), | |||
() => frappe.timeout(1), | |||
() => frappe.tests.click_button('Yes'), | |||
() => frappe.timeout(1), | |||
() => { | |||
assert.equal($('.msgprint').text(), "Did not saveInsufficient Permission for User", "Approve action working"); | |||
frappe.tests.click_button('Close'); | |||
}, | |||
() => frappe.timeout(1), | |||
() => { | |||
$('.user-role input:eq(5)').click(); | |||
cur_frm.save(); | |||
}, | |||
() => frappe.timeout(0.5), | |||
() => frappe.tests.click_button('Actions'), | |||
() => frappe.timeout(0.5), | |||
() => { | |||
let reject = $(`.dropdown-menu li:contains("Reject"):visible`).size(); | |||
assert.equal(reject, 1, "Review Action exists"); | |||
}, | |||
() => frappe.tests.click_dropdown_item('Reject'), | |||
() => frappe.timeout(0.5), | |||
() => { | |||
if(frappe.tests.click_button('Close')) | |||
assert.equal(1, 1, "Reject action works"); | |||
}, | |||
() => done() | |||
]); | |||
}); |