@@ -945,7 +945,11 @@ def get_installed_apps(sort=False, frappe_last=False): | |||
connect() | |||
if not local.all_apps: | |||
local.all_apps = get_all_apps(True) | |||
local.all_apps = cache().get_value('all_apps', get_all_apps) | |||
#cache bench apps | |||
if not cache().get_value('all_apps'): | |||
cache().set_value('all_apps', local.all_apps) | |||
installed = json.loads(db.get_global("installed_apps") or "[]") | |||
@@ -44,6 +44,20 @@ frappe.ui.form.on('Auto Repeat', { | |||
// auto repeat schedule | |||
frappe.auto_repeat.render_schedule(frm); | |||
frm.trigger('toggle_submit_on_creation'); | |||
}, | |||
reference_doctype: function(frm) { | |||
frm.trigger('toggle_submit_on_creation'); | |||
}, | |||
toggle_submit_on_creation: function(frm) { | |||
// submit on creation checkbox | |||
frappe.model.with_doctype(frm.doc.reference_doctype, () => { | |||
let meta = frappe.get_meta(frm.doc.reference_doctype); | |||
frm.toggle_display('submit_on_creation', meta.is_submittable); | |||
}); | |||
}, | |||
template: function(frm) { | |||
@@ -1,4 +1,5 @@ | |||
{ | |||
"actions": [], | |||
"allow_import": 1, | |||
"allow_rename": 1, | |||
"autoname": "format:AUT-AR-{#####}", | |||
@@ -12,6 +13,7 @@ | |||
"section_break_3", | |||
"reference_doctype", | |||
"reference_document", | |||
"submit_on_creation", | |||
"column_break_5", | |||
"start_date", | |||
"end_date", | |||
@@ -186,9 +188,16 @@ | |||
"fieldname": "repeat_on_last_day", | |||
"fieldtype": "Check", | |||
"label": "Repeat on Last Day of the Month" | |||
}, | |||
{ | |||
"default": "0", | |||
"fieldname": "submit_on_creation", | |||
"fieldtype": "Check", | |||
"label": "Submit on Creation" | |||
} | |||
], | |||
"modified": "2019-07-17 11:30:51.412317", | |||
"links": [], | |||
"modified": "2020-12-10 10:43:13.449172", | |||
"modified_by": "Administrator", | |||
"module": "Automation", | |||
"name": "Auto Repeat", | |||
@@ -21,6 +21,7 @@ class AutoRepeat(Document): | |||
def validate(self): | |||
self.update_status() | |||
self.validate_reference_doctype() | |||
self.validate_submit_on_creation() | |||
self.validate_dates() | |||
self.validate_email_id() | |||
self.set_dates() | |||
@@ -60,6 +61,11 @@ class AutoRepeat(Document): | |||
if not frappe.get_meta(self.reference_doctype).allow_auto_repeat: | |||
frappe.throw(_("Enable Allow Auto Repeat for the doctype {0} in Customize Form").format(self.reference_doctype)) | |||
def validate_submit_on_creation(self): | |||
if self.submit_on_creation and not frappe.get_meta(self.reference_doctype).is_submittable: | |||
frappe.throw(_('Cannot enable {0} for a non-submittable doctype').format( | |||
frappe.bold('Submit on Creation'))) | |||
def validate_dates(self): | |||
if frappe.flags.in_patch: | |||
return | |||
@@ -150,6 +156,9 @@ class AutoRepeat(Document): | |||
self.update_doc(new_doc, reference_doc) | |||
new_doc.insert(ignore_permissions = True) | |||
if self.submit_on_creation: | |||
new_doc.submit() | |||
return new_doc | |||
def update_doc(self, new_doc, reference_doc): | |||
@@ -160,7 +169,7 @@ class AutoRepeat(Document): | |||
if new_doc.meta.get_field('auto_repeat'): | |||
new_doc.set('auto_repeat', self.name) | |||
for fieldname in ['naming_series', 'ignore_pricing_rule', 'posting_time', 'select_print_heading', 'remarks', 'owner']: | |||
for fieldname in ['naming_series', 'ignore_pricing_rule', 'posting_time', 'select_print_heading', 'user_remark', 'remarks', 'owner']: | |||
if new_doc.meta.get_field(fieldname): | |||
new_doc.set(fieldname, reference_doc.get(fieldname)) | |||
@@ -111,6 +111,25 @@ class TestAutoRepeat(unittest.TestCase): | |||
doc = make_auto_repeat(frequency='Daily', reference_document=todo.name, start_date=add_days(today(), -2)) | |||
self.assertEqual(getdate(doc.next_schedule_date), current_date) | |||
def test_submit_on_creation(self): | |||
doctype = 'Test Submittable DocType' | |||
create_submittable_doctype(doctype) | |||
current_date = getdate() | |||
submittable_doc = frappe.get_doc(dict(doctype=doctype, test='test submit on creation')).insert() | |||
submittable_doc.submit() | |||
doc = make_auto_repeat(frequency='Daily', reference_doctype=doctype, reference_document=submittable_doc.name, | |||
start_date=add_days(current_date, -1), submit_on_creation=1) | |||
data = get_auto_repeat_entries(current_date) | |||
create_repeated_entries(data) | |||
docnames = frappe.db.get_all(doc.reference_doctype, | |||
filters={'auto_repeat': doc.name}, | |||
fields=['docstatus'], | |||
limit=1 | |||
) | |||
self.assertEquals(docnames[0].docstatus, 1) | |||
def make_auto_repeat(**args): | |||
args = frappe._dict(args) | |||
@@ -118,6 +137,7 @@ def make_auto_repeat(**args): | |||
'doctype': 'Auto Repeat', | |||
'reference_doctype': args.reference_doctype or 'ToDo', | |||
'reference_document': args.reference_document or frappe.db.get_value('ToDo', 'name'), | |||
'submit_on_creation': args.submit_on_creation or 0, | |||
'frequency': args.frequency or 'Daily', | |||
'start_date': args.start_date or add_days(today(), -1), | |||
'end_date': args.end_date or "", | |||
@@ -128,3 +148,34 @@ def make_auto_repeat(**args): | |||
}).insert(ignore_permissions=True) | |||
return doc | |||
def create_submittable_doctype(doctype): | |||
if frappe.db.exists('DocType', doctype): | |||
return | |||
else: | |||
doc = frappe.get_doc({ | |||
'doctype': 'DocType', | |||
'__newname': doctype, | |||
'module': 'Custom', | |||
'custom': 1, | |||
'is_submittable': 1, | |||
'fields': [{ | |||
'fieldname': 'test', | |||
'label': 'Test', | |||
'fieldtype': 'Data' | |||
}], | |||
'permissions': [{ | |||
'role': 'System Manager', | |||
'read': 1, | |||
'write': 1, | |||
'create': 1, | |||
'delete': 1, | |||
'submit': 1, | |||
'cancel': 1, | |||
'amend': 1 | |||
}] | |||
}).insert() | |||
doc.allow_auto_repeat = 1 | |||
doc.save() |
@@ -48,6 +48,7 @@ def get_bootinfo(): | |||
bootinfo.letter_heads = get_letter_heads() | |||
bootinfo.active_domains = frappe.get_active_domains() | |||
bootinfo.all_domains = [d.get("name") for d in frappe.get_all("Domain")] | |||
add_layouts(bootinfo) | |||
bootinfo.module_app = frappe.local.module_app | |||
bootinfo.single_types = [d.name for d in frappe.get_all('DocType', {'issingle': 1})] | |||
@@ -307,6 +308,10 @@ def get_additional_filters_from_hooks(): | |||
return filter_config | |||
def add_layouts(bootinfo): | |||
# add routes for readable doctypes | |||
bootinfo.doctype_layouts = frappe.get_all('DocType Layout', ['name', 'route', 'document_type']) | |||
def get_desk_settings(): | |||
role_list = frappe.get_all('Role', fields=['*'], filters=dict( | |||
name=['in', frappe.get_roles()] | |||
@@ -18,7 +18,7 @@ global_cache_keys = ("app_hooks", "installed_apps", | |||
'scheduler_events', 'time_zone', 'webhooks', 'active_domains', | |||
'active_modules', 'assignment_rule', 'server_script_map', 'wkhtmltopdf_version', | |||
'domain_restricted_doctypes', 'domain_restricted_pages', 'information_schema:counts', | |||
'sitemap_routes', 'db_tables', 'doctype_name_map') + doctype_map_keys | |||
'sitemap_routes', 'db_tables') + doctype_map_keys | |||
user_cache_keys = ("bootinfo", "user_recent", "roles", "user_doc", "lang", | |||
"defaults", "user_permissions", "home_page", "linked_with", | |||
@@ -73,7 +73,7 @@ def clear_doctype_cache(doctype=None): | |||
if getattr(frappe.local, 'meta_cache') and (doctype in frappe.local.meta_cache): | |||
del frappe.local.meta_cache[doctype] | |||
for key in ('is_table', 'doctype_modules', 'doctype_name_map', 'document_cache'): | |||
for key in ('is_table', 'doctype_modules', 'document_cache'): | |||
cache.delete_value(key) | |||
frappe.local.document_cache = {} | |||
@@ -100,13 +100,11 @@ def restore(context, sql_file_path, mariadb_root_username=None, mariadb_root_pas | |||
# Extract public and/or private files to the restored site, if user has given the path | |||
if with_public_files: | |||
with_public_files = os.path.join(base_path, with_public_files) | |||
public = extract_files(site, with_public_files, 'public') | |||
public = extract_files(site, with_public_files) | |||
os.remove(public) | |||
if with_private_files: | |||
with_private_files = os.path.join(base_path, with_private_files) | |||
private = extract_files(site, with_private_files, 'private') | |||
private = extract_files(site, with_private_files) | |||
os.remove(private) | |||
# Removing temporarily created file | |||
@@ -32,7 +32,7 @@ frappe.ui.form.on('Data Import Legacy', { | |||
frm.reload_doc(); | |||
} | |||
if (data.progress) { | |||
let progress_bar = $(frm.dashboard.progress_area).find(".progress-bar"); | |||
let progress_bar = $(frm.dashboard.progress_area.body).find(".progress-bar"); | |||
if (progress_bar) { | |||
$(progress_bar).removeClass("progress-bar-danger").addClass("progress-bar-success progress-bar-striped"); | |||
$(progress_bar).css("width", data.progress + "%"); | |||
@@ -132,7 +132,7 @@ | |||
"label": "Editable Grid" | |||
}, | |||
{ | |||
"default": "1", | |||
"default": "0", | |||
"depends_on": "eval:!doc.istable && !doc.issingle", | |||
"description": "Open a dialog with mandatory fields to create a new record quickly", | |||
"fieldname": "quick_entry", | |||
@@ -427,7 +427,7 @@ | |||
"label": "Allow Guest to View" | |||
}, | |||
{ | |||
"depends_on": "has_web_view", | |||
"depends_on": "eval:!doc.istable", | |||
"fieldname": "route", | |||
"fieldtype": "Data", | |||
"label": "Route" | |||
@@ -609,7 +609,7 @@ | |||
"link_fieldname": "reference_doctype" | |||
} | |||
], | |||
"modified": "2020-09-24 13:13:58.227153", | |||
"modified": "2020-12-10 15:10:09.227205", | |||
"modified_by": "Administrator", | |||
"module": "Core", | |||
"name": "DocType", | |||
@@ -637,6 +637,7 @@ | |||
"write": 1 | |||
} | |||
], | |||
"route": "doctype", | |||
"search_fields": "module", | |||
"show_name_in_global_search": 1, | |||
"sort_field": "modified", | |||
@@ -26,6 +26,7 @@ from frappe.database.schema import validate_column_name, validate_column_length | |||
from frappe.model.docfield import supports_translation | |||
from frappe.modules.import_file import get_file_path | |||
from frappe.model.meta import Meta | |||
from frappe.desk.utils import get_doctype_route | |||
class InvalidFieldNameError(frappe.ValidationError): pass | |||
@@ -63,15 +64,7 @@ class DocType(Document): | |||
self.validate_name() | |||
if self.issingle: | |||
self.allow_import = 0 | |||
self.is_submittable = 0 | |||
self.istable = 0 | |||
elif self.istable: | |||
self.allow_import = 0 | |||
self.permissions = [] | |||
self.set_defaults_for_single_and_table() | |||
self.scrub_field_names() | |||
self.set_default_in_list_view() | |||
self.set_default_translatable() | |||
@@ -79,10 +72,7 @@ class DocType(Document): | |||
self.validate_document_type() | |||
validate_fields(self) | |||
if self.istable: | |||
# no permission records for child table | |||
self.permissions = [] | |||
else: | |||
if not self.istable: | |||
validate_permissions(self) | |||
self.make_amendable() | |||
@@ -93,8 +83,6 @@ class DocType(Document): | |||
if not self.is_new(): | |||
self.before_update = frappe.get_doc('DocType', self.name) | |||
if not self.is_new(): | |||
self.setup_fields_to_fetch() | |||
check_email_append_to(self) | |||
@@ -102,14 +90,20 @@ class DocType(Document): | |||
if self.default_print_format and not self.custom: | |||
frappe.throw(_('Standard DocType cannot have default print format, use Customize Form')) | |||
if frappe.conf.get('developer_mode'): | |||
self.owner = 'Administrator' | |||
self.modified_by = 'Administrator' | |||
def after_insert(self): | |||
# clear user cache so that on the next reload this doctype is included in boot | |||
clear_user_cache(frappe.session.user) | |||
def set_defaults_for_single_and_table(self): | |||
if self.issingle: | |||
self.allow_import = 0 | |||
self.is_submittable = 0 | |||
self.istable = 0 | |||
elif self.istable: | |||
self.allow_import = 0 | |||
self.permissions = [] | |||
def set_default_in_list_view(self): | |||
'''Set default in-list-view for first 4 mandatory fields''' | |||
if not [d.fieldname for d in self.fields if d.in_list_view]: | |||
@@ -134,6 +128,10 @@ class DocType(Document): | |||
if not frappe.conf.get("developer_mode") and not self.custom: | |||
frappe.throw(_("Not in Developer Mode! Set in site_config.json or make 'Custom' DocType."), CannotCreateStandardDoctypeError) | |||
if frappe.conf.get('developer_mode'): | |||
self.owner = 'Administrator' | |||
self.modified_by = 'Administrator' | |||
def setup_fields_to_fetch(self): | |||
'''Setup query to update values for newly set fetch values''' | |||
try: | |||
@@ -192,6 +190,12 @@ class DocType(Document): | |||
def validate_website(self): | |||
"""Ensure that website generator has field 'route'""" | |||
if not self.istable and not self.route: | |||
self.route = get_doctype_route(self.name) | |||
if self.route: | |||
self.route = self.route.strip('/') | |||
if self.has_web_view: | |||
# route field must be present | |||
if not 'route' in [d.fieldname for d in self.fields]: | |||
@@ -0,0 +1,7 @@ | |||
import frappe | |||
from frappe.desk.utils import get_doctype_route | |||
def execute(): | |||
for doctype in frappe.get_all('DocType', ['name', 'route'], dict(istable=0)): | |||
if not doctype.route: | |||
frappe.db.set_value('DocType', doctype.name, 'route', get_doctype_route(doctype.name), update_modified = False) |
@@ -23,7 +23,7 @@ class NavbarSettings(Document): | |||
if not frappe.flags.in_patch and (len(before_save_items) > len(after_save_items)): | |||
frappe.throw(_("Please hide the standard navbar items instead of deleting them")) | |||
@frappe.whitelist() | |||
@frappe.whitelist(allow_guest=True) | |||
def get_app_logo(): | |||
app_logo = frappe.db.get_single_value('Navbar Settings', 'app_logo') | |||
if not app_logo: | |||
@@ -16,7 +16,7 @@ | |||
"two_factor_auth", | |||
"navigation_settings_section", | |||
"search_bar", | |||
"notification", | |||
"notifications", | |||
"chat", | |||
"list_settings_section", | |||
"list_sidebar", | |||
@@ -84,12 +84,6 @@ | |||
"fieldtype": "Check", | |||
"label": "Search Bar" | |||
}, | |||
{ | |||
"default": "1", | |||
"fieldname": "notification", | |||
"fieldtype": "Check", | |||
"label": "Notification" | |||
}, | |||
{ | |||
"default": "1", | |||
"fieldname": "chat", | |||
@@ -141,13 +135,19 @@ | |||
"fieldname": "view_switcher", | |||
"fieldtype": "Check", | |||
"label": "View Switcher" | |||
}, | |||
{ | |||
"default": "1", | |||
"fieldname": "notifications", | |||
"fieldtype": "Check", | |||
"label": "Notifications" | |||
} | |||
], | |||
"icon": "fa fa-bookmark", | |||
"idx": 1, | |||
"index_web_pages_for_search": 1, | |||
"links": [], | |||
"modified": "2020-11-11 17:29:13.149522", | |||
"modified": "2020-12-03 14:08:38.181035", | |||
"modified_by": "Administrator", | |||
"module": "Core", | |||
"name": "Role", | |||
@@ -6,7 +6,7 @@ import frappe | |||
from frappe.model.document import Document | |||
desk_properties = ("search_bar", "notification", "chat", "list_sidebar", | |||
desk_properties = ("search_bar", "notifications", "chat", "list_sidebar", | |||
"bulk_actions", "view_switcher", "form_sidebar", "timeline", "dashboard") | |||
class Role(Document): | |||
@@ -48,29 +48,33 @@ frappe.ui.form.on('Server Script', { | |||
setup_help(frm) { | |||
frm.get_field('help_html').html(` | |||
<h3>Examples</h3> | |||
<h4>DocType Event</h4> | |||
<pre><code> | |||
<p>Add logic for standard doctype events like Before Insert, After Submit, etc.</p> | |||
<pre> | |||
<code> | |||
# set property | |||
if "test" in doc.description: | |||
doc.status = 'Closed' | |||
doc.status = 'Closed' | |||
# validate | |||
if "validate" in doc.description: | |||
raise frappe.ValidationError | |||
raise frappe.ValidationError | |||
# auto create another document | |||
if doc.allocted_to: | |||
frappe.get_doc(dict( | |||
doctype = 'ToDo' | |||
owner = doc.allocated_to, | |||
description = doc.subject | |||
)).insert() | |||
</code></pre> | |||
if doc.allocated_to: | |||
frappe.get_doc(dict( | |||
doctype = 'ToDo' | |||
owner = doc.allocated_to, | |||
description = doc.subject | |||
)).insert() | |||
</code> | |||
</pre> | |||
<hr> | |||
<h4>API Call</h4> | |||
<p>Respond to <code>/api/method/<method-name></code> calls, just like whitelisted methods</p> | |||
<pre><code> | |||
# respond to API | |||
@@ -79,6 +83,21 @@ if frappe.form_dict.message == "ping": | |||
else: | |||
frappe.response['message'] = "ok" | |||
</code></pre> | |||
<hr> | |||
<h4>Permission Query</h4> | |||
<p>Add conditions to the where clause of list queries.</p> | |||
<pre><code> | |||
# generate dynamic conditions and set it in the conditions variable | |||
tenant_id = frappe.db.get_value(...) | |||
conditions = 'tenant_id = {}'.format(tenant_id) | |||
# resulting select query | |||
select name from \`tabPerson\` | |||
where tenant_id = 2 | |||
order by creation desc | |||
</code></pre> | |||
`); | |||
} | |||
@@ -24,7 +24,7 @@ | |||
"fieldtype": "Select", | |||
"in_list_view": 1, | |||
"label": "Script Type", | |||
"options": "DocType Event\nScheduler Event\nAPI", | |||
"options": "DocType Event\nScheduler Event\nPermission Query\nAPI", | |||
"reqd": 1 | |||
}, | |||
{ | |||
@@ -35,7 +35,7 @@ | |||
"reqd": 1 | |||
}, | |||
{ | |||
"depends_on": "eval:doc.script_type==='DocType Event'", | |||
"depends_on": "eval:['DocType Event', 'Permission Query'].includes(doc.script_type)", | |||
"fieldname": "reference_doctype", | |||
"fieldtype": "Link", | |||
"in_list_view": 1, | |||
@@ -88,7 +88,7 @@ | |||
], | |||
"index_web_pages_for_search": 1, | |||
"links": [], | |||
"modified": "2020-11-11 12:39:41.391052", | |||
"modified": "2020-12-03 22:42:02.708148", | |||
"modified_by": "Administrator", | |||
"module": "Core", | |||
"name": "Server Script", | |||
@@ -4,6 +4,8 @@ | |||
from __future__ import unicode_literals | |||
import ast | |||
import frappe | |||
from frappe.model.document import Document | |||
from frappe.utils.safe_exec import safe_exec | |||
@@ -11,9 +13,9 @@ from frappe import _ | |||
class ServerScript(Document): | |||
@staticmethod | |||
def validate(): | |||
def validate(self): | |||
frappe.only_for('Script Manager', True) | |||
ast.parse(self.script) | |||
@staticmethod | |||
def on_update(): | |||
@@ -41,6 +43,12 @@ class ServerScript(Document): | |||
# wrong report type! | |||
raise frappe.DoesNotExistError | |||
def get_permission_query_conditions(self, user): | |||
locals = {"user": user, "conditions": ""} | |||
safe_exec(self.script, None, locals) | |||
if locals["conditions"]: | |||
return locals["conditions"] | |||
@frappe.whitelist() | |||
def setup_scheduler_events(script_name, frequency): | |||
method = frappe.scrub('{0}-{1}'.format(script_name, frequency)) | |||
@@ -50,6 +50,9 @@ def get_server_script_map(): | |||
# }, | |||
# '_api': { | |||
# '[path]': '[server script]' | |||
# }, | |||
# 'permission_query': { | |||
# 'DocType': '[server script]' | |||
# } | |||
# } | |||
if frappe.flags.in_patch and not frappe.db.table_exists('Server Script'): | |||
@@ -57,16 +60,20 @@ def get_server_script_map(): | |||
script_map = frappe.cache().get_value('server_script_map') | |||
if script_map is None: | |||
script_map = {} | |||
script_map = { | |||
'permission_query': {} | |||
} | |||
enabled_server_scripts = frappe.get_all('Server Script', | |||
fields=('name', 'reference_doctype', 'doctype_event','api_method', 'script_type'), | |||
filters={'disabled': 0}) | |||
for script in enabled_server_scripts: | |||
if script.script_type == 'DocType Event': | |||
script_map.setdefault(script.reference_doctype, {}).setdefault(script.doctype_event, []).append(script.name) | |||
elif script.script_type == 'Permission Query': | |||
script_map['permission_query'][script.reference_doctype] = script.name | |||
else: | |||
script_map.setdefault('_api', {})[script.api_method] = script.name | |||
frappe.cache().set_value('server_script_map', script_map) | |||
return script_map | |||
return script_map |
@@ -45,6 +45,22 @@ frappe.response['message'] = 'hello' | |||
allow_guest = 1, | |||
script = ''' | |||
frappe.flags = 'hello' | |||
''' | |||
), | |||
dict( | |||
name='test_permission_query', | |||
script_type = 'Permission Query', | |||
reference_doctype = 'ToDo', | |||
script = ''' | |||
conditions = '1 = 1' | |||
'''), | |||
dict( | |||
name='test_invalid_namespace_method', | |||
script_type = 'DocType Event', | |||
doctype_event = 'Before Insert', | |||
reference_doctype = 'Note', | |||
script = ''' | |||
frappe.method_that_doesnt_exist("do some magic") | |||
''' | |||
) | |||
] | |||
@@ -85,3 +101,12 @@ class TestServerScript(unittest.TestCase): | |||
def test_api_return(self): | |||
self.assertEqual(frappe.get_doc('Server Script', 'test_return_value').execute_method(), 'hello') | |||
def test_permission_query(self): | |||
self.assertTrue('where (1 = 1)' in frappe.db.get_list('ToDo', return_query=1)) | |||
self.assertTrue(isinstance(frappe.db.get_list('ToDo'), list)) | |||
def test_attribute_error(self): | |||
"""Raise AttributeError if method not found in Namespace""" | |||
note = frappe.get_doc({"doctype": "Note", "title": "Test Note: Server Script"}) | |||
self.assertRaises(AttributeError, note.insert) |
@@ -1,37 +1,36 @@ | |||
frappe.ui.form.on("System Settings", "refresh", function(frm) { | |||
frappe.call({ | |||
method: "frappe.core.doctype.system_settings.system_settings.load", | |||
callback: function(data) { | |||
frappe.all_timezones = data.message.timezones; | |||
frm.set_df_property("time_zone", "options", frappe.all_timezones); | |||
frappe.ui.form.on("System Settings", { | |||
refresh: function(frm) { | |||
frappe.call({ | |||
method: "frappe.core.doctype.system_settings.system_settings.load", | |||
callback: function(data) { | |||
frappe.all_timezones = data.message.timezones; | |||
frm.set_df_property("time_zone", "options", frappe.all_timezones); | |||
$.each(data.message.defaults, function(key, val) { | |||
frm.set_value(key, val); | |||
frappe.sys_defaults[key] = val; | |||
}) | |||
$.each(data.message.defaults, function(key, val) { | |||
frm.set_value(key, val); | |||
frappe.sys_defaults[key] = val; | |||
}); | |||
} | |||
}); | |||
}, | |||
enable_password_policy: function(frm) { | |||
if (frm.doc.enable_password_policy == 0) { | |||
frm.set_value("minimum_password_score", ""); | |||
} else { | |||
frm.set_value("minimum_password_score", "2"); | |||
} | |||
}); | |||
}); | |||
frappe.ui.form.on("System Settings", "enable_password_policy", function(frm) { | |||
if(frm.doc.enable_password_policy == 0){ | |||
frm.set_value("minimum_password_score", ""); | |||
} else { | |||
frm.set_value("minimum_password_score", "2"); | |||
} | |||
}); | |||
frappe.ui.form.on("System Settings", "enable_two_factor_auth", function(frm) { | |||
if(frm.doc.enable_two_factor_auth == 0){ | |||
frm.set_value("bypass_2fa_for_retricted_ip_users", 0); | |||
frm.set_value("bypass_restrict_ip_check_if_2fa_enabled", 0); | |||
} | |||
}); | |||
frappe.ui.form.on("System Settings", "enable_prepared_report_auto_deletion", function(frm) { | |||
if (frm.doc.enable_prepared_report_auto_deletion) { | |||
if (!frm.doc.prepared_report_expiry_period) { | |||
frm.set_value('prepared_report_expiry_period', 7); | |||
}, | |||
enable_two_factor_auth: function(frm) { | |||
if (frm.doc.enable_two_factor_auth == 0) { | |||
frm.set_value("bypass_2fa_for_retricted_ip_users", 0); | |||
frm.set_value("bypass_restrict_ip_check_if_2fa_enabled", 0); | |||
} | |||
}, | |||
enable_prepared_report_auto_deletion: function(frm) { | |||
if (frm.doc.enable_prepared_report_auto_deletion) { | |||
if (!frm.doc.prepared_report_expiry_period) { | |||
frm.set_value('prepared_report_expiry_period', 7); | |||
} | |||
} | |||
} | |||
}); |
@@ -55,7 +55,7 @@ class UserPermission(Document): | |||
ref_link = frappe.get_desk_link(self.doctype, overlap_exists[0].name) | |||
frappe.throw(_("{0} has already assigned default value for {1}.").format(ref_link, self.allow)) | |||
@frappe.whitelist() | |||
@frappe.whitelist(allow_guest=True) | |||
def get_user_permissions(user=None): | |||
'''Get all users permissions for the user as a dict of doctype''' | |||
# if this is called from client-side, | |||
@@ -66,7 +66,7 @@ def get_user_permissions(user=None): | |||
if not user: | |||
user = frappe.session.user | |||
if not user or user == "Administrator": | |||
if not user or user in ("Administrator", "Guest"): | |||
return {} | |||
cached_user_permissions = frappe.cache().hget("user_permissions", user) | |||
@@ -81,6 +81,11 @@ frappe.ui.form.on("Customize Form", { | |||
} else { | |||
f._sortable = false; | |||
} | |||
if (f.fieldtype == "Table") { | |||
frm.add_custom_button(f.options, function() { | |||
frm.set_value('doc_type', f.options); | |||
}, __('Customize Child Table')); | |||
} | |||
}); | |||
frm.fields_dict.fields.grid.refresh(); | |||
}, | |||
@@ -8,6 +8,7 @@ | |||
"engine": "InnoDB", | |||
"field_order": [ | |||
"document_type", | |||
"route", | |||
"fields", | |||
"client_script" | |||
], | |||
@@ -31,11 +32,17 @@ | |||
"fieldname": "client_script", | |||
"fieldtype": "Code", | |||
"label": "Client Script" | |||
}, | |||
{ | |||
"fieldname": "route", | |||
"fieldtype": "Data", | |||
"label": "Route", | |||
"reqd": 1 | |||
} | |||
], | |||
"index_web_pages_for_search": 1, | |||
"links": [], | |||
"modified": "2020-11-17 15:49:49.669291", | |||
"modified": "2020-12-10 15:01:04.352184", | |||
"modified_by": "Administrator", | |||
"module": "Custom", | |||
"name": "DocType Layout", | |||
@@ -52,8 +59,13 @@ | |||
"role": "System Manager", | |||
"share": 1, | |||
"write": 1 | |||
}, | |||
{ | |||
"read": 1, | |||
"role": "Guest" | |||
} | |||
], | |||
"route": "doctype-layout", | |||
"sort_field": "modified", | |||
"sort_order": "DESC", | |||
"track_changes": 1 |
@@ -7,6 +7,9 @@ from __future__ import unicode_literals | |||
import frappe | |||
from frappe.model.document import Document | |||
from frappe.desk.utils import get_doctype_route | |||
class DocTypeLayout(Document): | |||
def validate(self): | |||
frappe.cache().delete_value('doctype_name_map') | |||
if not self.route: | |||
self.route = get_doctype_route(self.name) |
@@ -0,0 +1,13 @@ | |||
import frappe | |||
def execute(): | |||
for web_form_name in frappe.db.get_all('Web Form', pluck='name'): | |||
web_form = frappe.get_doc('Web Form', web_form_name) | |||
doctype_layout = frappe.get_doc(dict( | |||
doctype = 'DocType Layout', | |||
document_type = web_form.doc_type, | |||
name = web_form.title, | |||
route = web_form.route, | |||
fields = [dict(fieldname = d.fieldname, label=d.label) for d in web_form.web_form_fields if d.fieldname] | |||
)).insert() | |||
print(doctype_layout.name) |
@@ -233,7 +233,7 @@ CREATE TABLE `tabDocType` ( | |||
DROP TABLE IF EXISTS `tabSeries`; | |||
CREATE TABLE `tabSeries` ( | |||
`name` varchar(100) DEFAULT NULL, | |||
`name` varchar(100), | |||
`current` int(10) NOT NULL DEFAULT 0, | |||
PRIMARY KEY(`name`) | |||
) ENGINE=InnoDB ROW_FORMAT=COMPRESSED CHARACTER SET=utf8mb4 COLLATE=utf8mb4_unicode_ci; | |||
@@ -162,7 +162,7 @@ class Workspace: | |||
item_type = item_type.lower() | |||
if item_type == "doctype": | |||
return (name in self.can_read and name in self.restricted_doctypes) | |||
return (name in self.can_read or [] and name in self.restricted_doctypes or []) | |||
if item_type == "page": | |||
return (name in self.allowed_pages and name in self.restricted_pages) | |||
if item_type == "report": | |||
@@ -5,6 +5,7 @@ | |||
from __future__ import unicode_literals | |||
from frappe.model.document import Document | |||
from frappe.modules.export_file import export_to_files | |||
from frappe.config import get_modules_from_all_apps_for_user | |||
import frappe | |||
from frappe import _ | |||
import json | |||
@@ -42,6 +43,24 @@ class Dashboard(Document): | |||
except ValueError as error: | |||
frappe.throw(_("Invalid json added in the custom options: {0}").format(error)) | |||
def get_permission_query_conditions(user): | |||
if not user: | |||
user = frappe.session.user | |||
if user == 'Administrator': | |||
return | |||
roles = frappe.get_roles(user) | |||
if "System Manager" in roles: | |||
return None | |||
allowed_modules = [frappe.db.escape(module.get('module_name')) for module in get_modules_from_all_apps_for_user()] | |||
module_condition = '`tabDashboard`.`module` in ({allowed_modules}) or `tabDashboard`.`module` is NULL'.format( | |||
allowed_modules=','.join(allowed_modules)) | |||
return module_condition | |||
@frappe.whitelist() | |||
def get_permitted_charts(dashboard_name): | |||
permitted_charts = [] | |||
@@ -13,12 +13,12 @@ from frappe.utils.dateutils import\ | |||
get_period, get_period_beginning, get_from_date_from_timespan, get_dates_from_timegrain | |||
from frappe.model.naming import append_number_if_name_exists | |||
from frappe.boot import get_allowed_reports | |||
from frappe.config import get_modules_from_all_apps_for_user | |||
from frappe.model.document import Document | |||
from frappe.modules.export_file import export_to_files | |||
def get_permission_query_conditions(user): | |||
if not user: | |||
user = frappe.session.user | |||
@@ -31,9 +31,11 @@ def get_permission_query_conditions(user): | |||
doctype_condition = False | |||
report_condition = False | |||
module_condition = False | |||
allowed_doctypes = [frappe.db.escape(doctype) for doctype in frappe.permissions.get_doctypes_with_read()] | |||
allowed_reports = [frappe.db.escape(key) if type(key) == str else key.encode('UTF8') for key in get_allowed_reports()] | |||
allowed_modules = [frappe.db.escape(module.get('module_name')) for module in get_modules_from_all_apps_for_user()] | |||
if allowed_doctypes: | |||
doctype_condition = '`tabDashboard Chart`.`document_type` in ({allowed_doctypes})'.format( | |||
@@ -41,18 +43,24 @@ def get_permission_query_conditions(user): | |||
if allowed_reports: | |||
report_condition = '`tabDashboard Chart`.`report_name` in ({allowed_reports})'.format( | |||
allowed_reports=','.join(allowed_reports)) | |||
if allowed_modules: | |||
module_condition = '''`tabDashboard Chart`.`module` in ({allowed_modules}) | |||
or `tabDashboard Chart`.`module` is NULL'''.format( | |||
allowed_modules=','.join(allowed_modules)) | |||
return ''' | |||
(`tabDashboard Chart`.`chart_type` in ('Count', 'Sum', 'Average') | |||
and {doctype_condition}) | |||
or | |||
(`tabDashboard Chart`.`chart_type` = 'Report' | |||
and {report_condition}) | |||
'''.format( | |||
doctype_condition=doctype_condition, | |||
report_condition=report_condition | |||
) | |||
((`tabDashboard Chart`.`chart_type` in ('Count', 'Sum', 'Average') | |||
and {doctype_condition}) | |||
or | |||
(`tabDashboard Chart`.`chart_type` = 'Report' | |||
and {report_condition})) | |||
and | |||
({module_condition}) | |||
'''.format( | |||
doctype_condition=doctype_condition, | |||
report_condition=report_condition, | |||
module_condition=module_condition | |||
) | |||
def has_permission(doc, ptype, user): | |||
roles = frappe.get_roles(user) | |||
@@ -8,6 +8,7 @@ from frappe.model.document import Document | |||
from frappe.utils import cint | |||
from frappe.model.naming import append_number_if_name_exists | |||
from frappe.modules.export_file import export_to_files | |||
from frappe.config import get_modules_from_all_apps_for_user | |||
class NumberCard(Document): | |||
def autoname(self): | |||
@@ -33,16 +34,24 @@ def get_permission_query_conditions(user=None): | |||
return None | |||
doctype_condition = False | |||
module_condition = False | |||
allowed_doctypes = [frappe.db.escape(doctype) for doctype in frappe.permissions.get_doctypes_with_read()] | |||
allowed_modules = [frappe.db.escape(module.get('module_name')) for module in get_modules_from_all_apps_for_user()] | |||
if allowed_doctypes: | |||
doctype_condition = '`tabNumber Card`.`document_type` in ({allowed_doctypes})'.format( | |||
allowed_doctypes=','.join(allowed_doctypes)) | |||
if allowed_modules: | |||
module_condition = '''`tabNumber Card`.`module` in ({allowed_modules}) | |||
or `tabNumber Card`.`module` is NULL'''.format( | |||
allowed_modules=','.join(allowed_modules)) | |||
return ''' | |||
{doctype_condition} | |||
'''.format(doctype_condition=doctype_condition) | |||
{doctype_condition} | |||
and | |||
{module_condition} | |||
'''.format(doctype_condition=doctype_condition, module_condition=module_condition) | |||
def has_permission(doc, ptype, user): | |||
roles = frappe.get_roles(user) | |||
@@ -13,7 +13,7 @@ from frappe.desk.form.document_follow import is_document_followed | |||
from frappe import _ | |||
from six.moves.urllib.parse import quote | |||
@frappe.whitelist() | |||
@frappe.whitelist(allow_guest=True) | |||
def getdoc(doctype, name, user=None): | |||
""" | |||
Loads a doclist for a given document. This method is called directly from the client. | |||
@@ -52,7 +52,7 @@ def getdoc(doctype, name, user=None): | |||
frappe.response.docs.append(doc) | |||
@frappe.whitelist() | |||
@frappe.whitelist(allow_guest=True) | |||
def getdoctype(doctype, with_parent=False, cached_timestamp=None): | |||
"""load doctype""" | |||
@@ -202,13 +202,17 @@ class FormMeta(Meta): | |||
self.load_kanban_column_fields() | |||
def load_kanban_column_fields(self): | |||
values = frappe.get_list( | |||
'Kanban Board', fields=['field_name'], | |||
filters={'reference_doctype': self.name}) | |||
fields = [x['field_name'] for x in values] | |||
fields = list(set(fields)) | |||
self.set("__kanban_column_fields", fields, as_value=True) | |||
try: | |||
values = frappe.get_list( | |||
'Kanban Board', fields=['field_name'], | |||
filters={'reference_doctype': self.name}) | |||
fields = [x['field_name'] for x in values] | |||
fields = list(set(fields)) | |||
self.set("__kanban_column_fields", fields, as_value=True) | |||
except frappe.PermissionError: | |||
# no access to kanban board | |||
pass | |||
def get_code_files_via_hooks(hook, name): | |||
code_files = [] | |||
@@ -4,7 +4,7 @@ from __future__ import unicode_literals | |||
import frappe | |||
@frappe.whitelist() | |||
@frappe.whitelist(allow_guest=True) | |||
def get_list_settings(doctype): | |||
try: | |||
return frappe.get_cached_doc("List View Settings", doctype) | |||
@@ -0,0 +1,30 @@ | |||
.recent-activity .new-timeline { | |||
padding-top: 0; | |||
} | |||
.recent-activity .new-timeline:before { | |||
top: 25px; | |||
} | |||
.recent-activity-title { | |||
font-weight: 700; | |||
font-size: var(--text-xl); | |||
color: var(--text-color); | |||
} | |||
.recent-activity .recent-activity-footer { | |||
margin-left: calc(var(--timeline-left-padding) + var(--timeline-item-left-margin)); | |||
max-width: var(--timeline-content-max-width); | |||
} | |||
.recent-activity .show-more-activity-btn { | |||
display: block; | |||
margin: auto; | |||
width: max-content; | |||
margin-top: 35px; | |||
font-size: var(--text-md); | |||
} | |||
.recent-activity { | |||
padding-bottom: 60px; | |||
} |
@@ -36,10 +36,9 @@ | |||
</div> | |||
</div> | |||
<div class="recent-activity"> | |||
<p class="recent-activity-title h6 uppercase">{%=__("Recent Activity") %}</p> | |||
<div class="recent-activity-list py-2"> | |||
</div> | |||
<div class="show-more-activity"><a class="text-muted">{%=__("Show More Activity") %}</a></div> | |||
<div class="recent-activity-title">{%=__("Recent Activity") %}</div> | |||
<div class="recent-activity-list"></div> | |||
<div class="recent-activity-footer"></div> | |||
</div> | |||
</div> | |||
</div> |
@@ -1,9 +1,8 @@ | |||
frappe.pages['user-profile'].on_page_load = function (wrapper) { | |||
frappe.ui.make_app_page({ | |||
parent: wrapper, | |||
title: __('User Profile'), | |||
}); | |||
frappe.require('assets/js/user_profile_controller.min.js', () => { | |||
frappe.ui.make_app_page({ | |||
parent: wrapper, | |||
}); | |||
let user_profile = new frappe.ui.UserProfile(wrapper); | |||
user_profile.show(); | |||
}); |
@@ -15,7 +15,9 @@ class UserProfile { | |||
//validate if user | |||
if (route.length > 1) { | |||
frappe.dom.freeze(__('Loading user profile') + '...'); | |||
frappe.db.exists('User', this.user_id).then(exists => { | |||
frappe.dom.unfreeze(); | |||
if (exists) { | |||
this.make_user_profile(); | |||
} else { | |||
@@ -42,8 +44,7 @@ class UserProfile { | |||
this.render_line_chart(); | |||
this.render_percentage_chart('type', 'Type Distribution'); | |||
this.create_percentage_chart_filters(); | |||
this.setup_show_more_activity(); | |||
this.render_user_activity(); | |||
this.setup_user_activity_timeline(); | |||
} | |||
setup_user_search() { | |||
@@ -374,46 +375,71 @@ class UserProfile { | |||
frappe.set_route('Form', 'User', this.user_id); | |||
} | |||
render_user_activity() { | |||
this.$recent_activity_list = this.wrapper.find('.recent-activity-list'); | |||
frappe.xcall('frappe.desk.page.user_profile.user_profile.get_energy_points_list', { | |||
start: this.activity_start, | |||
limit: this.activity_end, | |||
setup_user_activity_timeline() { | |||
this.user_activity_timeline = new UserProfileTimeline({ | |||
parent: this.wrapper.find('.recent-activity-list'), | |||
footer: this.wrapper.find('.recent-activity-footer'), | |||
user: this.user_id | |||
}).then(list => { | |||
if (!list.length) return; | |||
this.activities_timeline = new BaseTimeline({ | |||
parent: this.$recent_activity_list | |||
}); | |||
this.activities_timeline.prepare_timeline_contents = () => { | |||
this.activities_timeline.timeline_items = list.map((data) => { | |||
return { | |||
creation: data.creation, | |||
card: true, | |||
content: frappe.energy_points.format_history_log(data), | |||
}; | |||
}); | |||
}; | |||
this.activities_timeline.refresh(); | |||
}); | |||
this.user_activity_timeline.refresh(); | |||
} | |||
} | |||
class UserProfileTimeline extends BaseTimeline { | |||
make() { | |||
super.make(); | |||
this.activity_start = 0; | |||
this.activity_limit = 20; | |||
this.setup_show_more_activity(); | |||
} | |||
prepare_timeline_contents() { | |||
return this.get_user_activity_data().then((activities) => { | |||
if (!activities.length) { | |||
this.show_more_button.hide(); | |||
this.timeline_wrapper.html(`<div>${__('No activities to show')}</div>`); | |||
return; | |||
} | |||
this.show_more_button.toggle(activities.length === this.activity_limit); | |||
this.timeline_items = activities.map((activity) => this.get_activity_timeline_item(activity)); | |||
}); | |||
} | |||
get_user_activity_data() { | |||
return frappe.xcall('frappe.desk.page.user_profile.user_profile.get_energy_points_list', { | |||
start: this.activity_start, | |||
limit: this.activity_limit, | |||
user: this.user | |||
}); | |||
} | |||
get_activity_timeline_item(data) { | |||
let icon = data.type == 'Appreciation' ? 'clap': data.type == 'Criticism' ? 'criticize': null; | |||
return { | |||
icon: icon, | |||
creation: data.creation, | |||
is_card: true, | |||
content: frappe.energy_points.format_history_log(data), | |||
}; | |||
} | |||
setup_show_more_activity() { | |||
//Show 10 items at a time | |||
this.activity_start = 0; | |||
this.activity_end = 11; | |||
this.wrapper.find('.show-more-activity').on('click', () => this.show_more_activity()); | |||
this.show_more_button = $(`<a class="show-more-activity-btn">${__('Show More Activity')}</a>`); | |||
this.show_more_button.hide(); | |||
this.footer.append(this.show_more_button); | |||
this.show_more_button.on('click', () => this.show_more_activity()); | |||
} | |||
show_more_activity() { | |||
this.activity_start = this.activity_end; | |||
this.activity_end += 11; | |||
this.render_user_activity(); | |||
this.activity_start += this.activity_limit; | |||
this.get_user_activity_data().then(activities => { | |||
if (!activities.length || activities.length < this.activity_limit) { | |||
this.show_more_button.hide(); | |||
} | |||
let timeline_items = activities.map((activity) => this.get_activity_timeline_item(activity)); | |||
timeline_items.map((item) => this.add_timeline_item(item, true)); | |||
}); | |||
} | |||
} | |||
frappe.provide('frappe.ui'); |
@@ -14,7 +14,7 @@ from frappe.core.doctype.access_log.access_log import make_access_log | |||
from frappe.utils import cstr, format_duration | |||
@frappe.whitelist() | |||
@frappe.whitelist(allow_guest=True) | |||
@frappe.read_only() | |||
def get(): | |||
args = get_form_params() | |||
@@ -3,26 +3,5 @@ | |||
import frappe | |||
@frappe.whitelist(allow_guest=True) | |||
def get_doctype_name(name): | |||
# translates the doctype name from url to name `sales-order` to `Sales Order` | |||
# also supports document type layouts | |||
# if with_layout is set: return the layout object too | |||
def get_name_map(): | |||
name_map = {} | |||
for d in frappe.get_all('DocType'): | |||
name_map[d.name.lower().replace(' ', '-')] = frappe._dict(doctype = d.name) | |||
for d in frappe.get_all('DocType Layout', fields = ['name', 'document_type']): | |||
name_map[d.name.lower().replace(' ', '-')] = frappe._dict(doctype = d.document_type, doctype_layout = d.name) | |||
return name_map | |||
data = frappe._dict(name_map = frappe.cache().get_value('doctype_name_map', get_name_map).get(name, dict(doctype = name))) | |||
if data.name_map.get('doctype_layout'): | |||
# return the layout object | |||
frappe.response.docs.append(frappe.get_doc('DocType Layout', data.name_map.get('doctype_layout')).as_dict()) | |||
return data | |||
def get_doctype_route(name): | |||
return name.lower().replace(' ', '-') |
@@ -1,4 +1,5 @@ | |||
{ | |||
"actions": [], | |||
"allow_import": 1, | |||
"allow_rename": 1, | |||
"autoname": "Prompt", | |||
@@ -8,6 +9,8 @@ | |||
"engine": "InnoDB", | |||
"field_order": [ | |||
"subject", | |||
"use_html", | |||
"response_html", | |||
"response", | |||
"owner", | |||
"section_break_4", | |||
@@ -22,11 +25,12 @@ | |||
"reqd": 1 | |||
}, | |||
{ | |||
"depends_on": "eval:!doc.use_html", | |||
"fieldname": "response", | |||
"fieldtype": "Text Editor", | |||
"in_list_view": 1, | |||
"label": "Response", | |||
"reqd": 1 | |||
"mandatory_depends_on": "eval:!doc.use_html" | |||
}, | |||
{ | |||
"default": "user", | |||
@@ -45,10 +49,24 @@ | |||
"fieldtype": "HTML", | |||
"label": "Email Reply Help", | |||
"options": "<h4>Email Reply Example</h4>\n\n<pre>Order Overdue\n\nTransaction {{ name }} has exceeded Due Date. Please take necessary action.\n\nDetails\n\n- Customer: {{ customer }}\n- Amount: {{ grand_total }}\n</pre>\n\n<h4>How to get fieldnames</h4>\n\n<p>The fieldnames you can use in your email template are the fields in the document from which you are sending the email. You can find out the fields of any documents via Setup > Customize Form View and selecting the document type (e.g. Sales Invoice)</p>\n\n<h4>Templating</h4>\n\n<p>Templates are compiled using the Jinja Templating Language. To learn more about Jinja, <a class=\"strong\" href=\"http://jinja.pocoo.org/docs/dev/templates/\">read this documentation.</a></p>\n" | |||
}, | |||
{ | |||
"default": "0", | |||
"fieldname": "use_html", | |||
"fieldtype": "Check", | |||
"label": "Use HTML" | |||
}, | |||
{ | |||
"depends_on": "eval:doc.use_html", | |||
"fieldname": "response_html", | |||
"fieldtype": "Code", | |||
"label": "Response ", | |||
"options": "HTML" | |||
} | |||
], | |||
"icon": "fa fa-comment", | |||
"modified": "2019-10-30 14:15:00.956347", | |||
"links": [], | |||
"modified": "2020-11-30 14:12:50.321633", | |||
"modified_by": "Administrator", | |||
"module": "Email", | |||
"name": "Email Template", | |||
@@ -9,7 +9,29 @@ from six import string_types | |||
class EmailTemplate(Document): | |||
def validate(self): | |||
validate_template(self.response) | |||
if self.use_html: | |||
validate_template(self.response_html) | |||
else: | |||
validate_template(self.response) | |||
def get_formatted_subject(self, doc): | |||
return frappe.render_template(self.subject, doc) | |||
def get_formatted_response(self, doc): | |||
if self.use_html: | |||
return frappe.render_template(self.response_html, doc) | |||
return frappe.render_template(self.response, doc) | |||
def get_formatted_email(self, doc): | |||
if isinstance(doc, string_types): | |||
doc = json.loads(doc) | |||
return { | |||
"subject" : self.get_formatted_subject(doc), | |||
"message" : self.get_formatted_response(doc) | |||
} | |||
@frappe.whitelist() | |||
def get_email_template(template_name, doc): | |||
@@ -18,5 +40,4 @@ def get_email_template(template_name, doc): | |||
doc = json.loads(doc) | |||
email_template = frappe.get_doc("Email Template", template_name) | |||
return {"subject" : frappe.render_template(email_template.subject, doc), | |||
"message" : frappe.render_template(email_template.response, doc)} | |||
return email_template.get_formatted_email(doc) |
@@ -85,11 +85,11 @@ class Newsletter(WebsiteGenerator): | |||
self.db_set("scheduled_to_send", len(self.recipients)) | |||
def get_message(self): | |||
if self.content_type == "HTML": | |||
return frappe.render_template(self.message_html, {"doc": self.as_dict()}) | |||
return { | |||
'Rich Text': self.message, | |||
'Markdown': markdown(self.message_md), | |||
'HTML': self.message_html | |||
'Markdown': markdown(self.message_md) | |||
}[self.content_type or 'Rich Text'] | |||
def get_recipients(self): | |||
@@ -93,6 +93,7 @@ permission_query_conditions = { | |||
"User": "frappe.core.doctype.user.user.get_permission_query_conditions", | |||
"Dashboard Settings": "frappe.desk.doctype.dashboard_settings.dashboard_settings.get_permission_query_conditions", | |||
"Notification Log": "frappe.desk.doctype.notification_log.notification_log.get_permission_query_conditions", | |||
"Dashboard": "frappe.desk.doctype.dashboard.dashboard.get_permission_query_conditions", | |||
"Dashboard Chart": "frappe.desk.doctype.dashboard_chart.dashboard_chart.get_permission_query_conditions", | |||
"Number Card": "frappe.desk.doctype.number_card.number_card.get_permission_query_conditions", | |||
"Notification Settings": "frappe.desk.doctype.notification_settings.notification_settings.get_permission_query_conditions", | |||
@@ -440,20 +440,11 @@ def extract_sql_from_archive(sql_file_path): | |||
Returns: | |||
str: Path of the decompressed SQL file | |||
""" | |||
from frappe.utils import get_bench_relative_path | |||
sql_file_path = get_bench_relative_path(sql_file_path) | |||
# Extract the gzip file if user has passed *.sql.gz file instead of *.sql file | |||
if not os.path.exists(sql_file_path): | |||
base_path = '..' | |||
sql_file_path = os.path.join(base_path, sql_file_path) | |||
if not os.path.exists(sql_file_path): | |||
print('Invalid path {0}'.format(sql_file_path[3:])) | |||
sys.exit(1) | |||
elif sql_file_path.startswith(os.sep): | |||
base_path = os.sep | |||
else: | |||
base_path = '.' | |||
if sql_file_path.endswith('sql.gz'): | |||
decompressed_file_name = extract_sql_gzip(os.path.abspath(sql_file_path)) | |||
decompressed_file_name = extract_sql_gzip(sql_file_path) | |||
else: | |||
decompressed_file_name = sql_file_path | |||
@@ -475,9 +466,12 @@ def extract_sql_gzip(sql_gz_path): | |||
return decompressed_file | |||
def extract_files(site_name, file_path, folder_name): | |||
def extract_files(site_name, file_path): | |||
import shutil | |||
import subprocess | |||
from frappe.utils import get_bench_relative_path | |||
file_path = get_bench_relative_path(file_path) | |||
# Need to do frappe.init to maintain the site locals | |||
frappe.init(site=site_name) | |||
@@ -18,12 +18,9 @@ | |||
"bucket", | |||
"endpoint_url", | |||
"column_break_13", | |||
"region", | |||
"backup_details_section", | |||
"frequency", | |||
"backup_files", | |||
"column_break_18", | |||
"backup_limit" | |||
"backup_files" | |||
], | |||
"fields": [ | |||
{ | |||
@@ -42,7 +39,7 @@ | |||
}, | |||
{ | |||
"default": "1", | |||
"description": "Note: By default emails for failed backups are sent.", | |||
"description": "By default, emails are only sent for failed backups.", | |||
"fieldname": "send_email_for_successful_backup", | |||
"fieldtype": "Check", | |||
"label": "Send Email for Successful Backup" | |||
@@ -73,14 +70,7 @@ | |||
"reqd": 1 | |||
}, | |||
{ | |||
"default": "us-east-1", | |||
"description": "See https://docs.aws.amazon.com/general/latest/gr/s3.html for details.", | |||
"fieldname": "region", | |||
"fieldtype": "Select", | |||
"label": "Region", | |||
"options": "us-east-1\nus-east-2\nus-west-1\nus-west-2\naf-south-1\nap-east-1\nap-south-1\nap-southeast-1\nap-southeast-2\nap-northeast-1\nap-northeast-2\nap-northeast-3\nca-central-1\ncn-north-1\ncn-northwest-1\neu-central-1\neu-west-1\neu-west-2\neu-west-3\neu-south-1\neu-north-1\nme-south-1\nsa-east-1" | |||
}, | |||
{ | |||
"default": "https://s3.amazonaws.com", | |||
"fieldname": "endpoint_url", | |||
"fieldtype": "Data", | |||
"label": "Endpoint URL" | |||
@@ -92,14 +82,6 @@ | |||
"mandatory_depends_on": "enabled", | |||
"reqd": 1 | |||
}, | |||
{ | |||
"description": "Set to 0 for no limit on the number of backups taken", | |||
"fieldname": "backup_limit", | |||
"fieldtype": "Int", | |||
"label": "Backup Limit", | |||
"mandatory_depends_on": "enabled", | |||
"reqd": 1 | |||
}, | |||
{ | |||
"depends_on": "enabled", | |||
"fieldname": "api_access_section", | |||
@@ -142,16 +124,12 @@ | |||
"fieldname": "backup_files", | |||
"fieldtype": "Check", | |||
"label": "Backup Files" | |||
}, | |||
{ | |||
"fieldname": "column_break_18", | |||
"fieldtype": "Column Break" | |||
} | |||
], | |||
"hide_toolbar": 1, | |||
"issingle": 1, | |||
"links": [], | |||
"modified": "2020-07-27 17:27:21.400000", | |||
"modified": "2020-12-07 15:30:55.047689", | |||
"modified_by": "Administrator", | |||
"module": "Integrations", | |||
"name": "S3 Backup Settings", | |||
@@ -172,4 +150,4 @@ | |||
"sort_field": "modified", | |||
"sort_order": "DESC", | |||
"track_changes": 1 | |||
} | |||
} |
@@ -24,6 +24,7 @@ class S3BackupSettings(Document): | |||
if not self.endpoint_url: | |||
self.endpoint_url = 'https://s3.amazonaws.com' | |||
conn = boto3.client( | |||
's3', | |||
aws_access_key_id=self.access_key_id, | |||
@@ -31,25 +32,21 @@ class S3BackupSettings(Document): | |||
endpoint_url=self.endpoint_url | |||
) | |||
bucket_lower = str(self.bucket) | |||
try: | |||
conn.list_buckets() | |||
except ClientError: | |||
frappe.throw(_("Invalid Access Key ID or Secret Access Key.")) | |||
try: | |||
# Head_bucket returns a 200 OK if the bucket exists and have access to it. | |||
conn.head_bucket(Bucket=bucket_lower) | |||
# Requires ListBucket permission | |||
conn.head_bucket(Bucket=self.bucket) | |||
except ClientError as e: | |||
error_code = e.response['Error']['Code'] | |||
bucket_name = frappe.bold(self.bucket) | |||
if error_code == '403': | |||
frappe.throw(_("Do not have permission to access {0} bucket.").format(bucket_lower)) | |||
else: # '400'-Bad request or '404'-Not Found return | |||
# try to create bucket | |||
conn.create_bucket(Bucket=bucket_lower, CreateBucketConfiguration={ | |||
'LocationConstraint': self.region}) | |||
msg = _("Do not have permission to access bucket {0}.").format(bucket_name) | |||
elif error_code == '404': | |||
msg = _("Bucket {0} not found.").format(bucket_name) | |||
else: | |||
msg = e.args[0] | |||
frappe.throw(msg) | |||
@frappe.whitelist() | |||
@@ -70,11 +67,13 @@ def take_backups_weekly(): | |||
def take_backups_monthly(): | |||
take_backups_if("Monthly") | |||
def take_backups_if(freq): | |||
if cint(frappe.db.get_value("S3 Backup Settings", None, "enabled")): | |||
if frappe.db.get_value("S3 Backup Settings", None, "frequency") == freq: | |||
take_backups_s3() | |||
@frappe.whitelist() | |||
def take_backups_s3(retry_count=0): | |||
try: | |||
@@ -146,42 +145,13 @@ def backup_to_s3(): | |||
if files_filename: | |||
upload_file_to_s3(files_filename, folder, conn, bucket) | |||
delete_old_backups(doc.backup_limit, bucket) | |||
def upload_file_to_s3(filename, folder, conn, bucket): | |||
destpath = os.path.join(folder, os.path.basename(filename)) | |||
try: | |||
print("Uploading file:", filename) | |||
conn.upload_file(filename, bucket, destpath) | |||
conn.upload_file(filename, bucket, destpath) # Requires PutObject permission | |||
except Exception as e: | |||
frappe.log_error() | |||
print("Error uploading: %s" % (e)) | |||
def delete_old_backups(limit, bucket): | |||
all_backups = [] | |||
doc = frappe.get_single("S3 Backup Settings") | |||
backup_limit = int(limit) | |||
s3 = boto3.resource( | |||
's3', | |||
aws_access_key_id=doc.access_key_id, | |||
aws_secret_access_key=doc.get_password('secret_access_key'), | |||
endpoint_url=doc.endpoint_url or 'https://s3.amazonaws.com' | |||
) | |||
bucket = s3.Bucket(bucket) | |||
objects = bucket.meta.client.list_objects_v2(Bucket=bucket.name, Delimiter='/') | |||
if objects: | |||
for obj in objects.get('CommonPrefixes'): | |||
all_backups.append(obj.get('Prefix')) | |||
oldest_backup = sorted(all_backups)[0] if all_backups else '' | |||
if len(all_backups) > backup_limit: | |||
print("Deleting Backup: {0}".format(oldest_backup)) | |||
for obj in bucket.objects.filter(Prefix=oldest_backup): | |||
# delete all keys that are inside the oldest_backup | |||
s3.Object(bucket.name, obj.key).delete() |
@@ -802,12 +802,12 @@ class BaseDocument(object): | |||
if translated: | |||
val = _(val) | |||
if absolute_value and isinstance(val, (int, float)): | |||
val = abs(self.get(fieldname)) | |||
if not doc: | |||
doc = getattr(self, "parent_doc", None) or self | |||
if (absolute_value or doc.get('absolute_value')) and isinstance(val, (int, float)): | |||
val = abs(self.get(fieldname)) | |||
return format_value(val, df=df, doc=doc, currency=currency) | |||
def is_print_hide(self, fieldname, df=None, for_print=True): | |||
@@ -18,6 +18,7 @@ from frappe.client import check_parent_permission | |||
from frappe.model.utils.user_settings import get_user_settings, update_user_settings | |||
from frappe.utils import flt, cint, get_time, make_filter_tuple, get_filter, add_to_date, cstr, get_timespan_date_range | |||
from frappe.model.meta import get_table_columns | |||
from frappe.core.doctype.server_script.server_script_utils import get_server_script_map | |||
class DatabaseQuery(object): | |||
def __init__(self, doctype, user=None): | |||
@@ -683,15 +684,23 @@ class DatabaseQuery(object): | |||
self.match_filters.append(match_filters) | |||
def get_permission_query_conditions(self): | |||
conditions = [] | |||
condition_methods = frappe.get_hooks("permission_query_conditions", {}).get(self.doctype, []) | |||
if condition_methods: | |||
conditions = [] | |||
for method in condition_methods: | |||
c = frappe.call(frappe.get_attr(method), self.user) | |||
if c: | |||
conditions.append(c) | |||
return " and ".join(conditions) if conditions else None | |||
permision_script_name = get_server_script_map().get("permission_query", {}).get(self.doctype) | |||
if permision_script_name: | |||
script = frappe.get_doc("Server Script", permision_script_name) | |||
condition = script.get_permission_query_conditions(self.user) | |||
if condition: | |||
conditions.append(condition) | |||
return " and ".join(conditions) if conditions else "" | |||
def run_custom_query(self, query): | |||
if '%(key)s' in query: | |||
@@ -209,7 +209,8 @@ class Meta(Document): | |||
'owner': _('Created By'), | |||
'modified_by': _('Modified By'), | |||
'creation': _('Created On'), | |||
'modified': _('Last Modified On') | |||
'modified': _('Last Modified On'), | |||
'_assign': _('Assigned To') | |||
}.get(fieldname) or _('No Label') | |||
return label | |||
@@ -22,6 +22,7 @@ execute:frappe.reload_doc('email', 'doctype', 'document_follow') | |||
execute:frappe.reload_doc('core', 'doctype', 'communication_link') #2019-10-02 | |||
execute:frappe.reload_doc('core', 'doctype', 'has_role') | |||
execute:frappe.reload_doc('core', 'doctype', 'communication') #2019-10-02 | |||
execute:frappe.reload_doc('core', 'doctype', 'server_script') | |||
frappe.patches.v11_0.replicate_old_user_permissions | |||
frappe.patches.v11_0.reload_and_rename_view_log #2019-01-03 | |||
frappe.patches.v7_1.rename_scheduler_log_to_error_log | |||
@@ -297,7 +298,7 @@ frappe.patches.v13_0.update_duration_options | |||
frappe.patches.v13_0.replace_old_data_import # 2020-06-24 | |||
frappe.patches.v13_0.create_custom_dashboards_cards_and_charts | |||
frappe.patches.v13_0.rename_is_custom_field_in_dashboard_chart | |||
frappe.patches.v13_0.add_standard_navbar_items | |||
frappe.patches.v13_0.add_standard_navbar_items # 2020-12-15 | |||
frappe.patches.v13_0.generate_theme_files_in_public_folder | |||
frappe.patches.v13_0.increase_password_length | |||
frappe.patches.v12_0.fix_email_id_formatting | |||
@@ -317,5 +318,7 @@ frappe.patches.v13_0.remove_custom_link | |||
execute:frappe.delete_doc("DocType", "Footer Item") | |||
frappe.patches.v13_0.replace_field_target_with_open_in_new_tab | |||
frappe.core.doctype.role.patches.v13_set_default_desk_properties | |||
frappe.patches.v13_0.add_switch_theme_to_navbar_settings | |||
frappe.patches.v13_0.update_icons_in_customized_desk_pages | |||
frappe.patches.v13_0.rename_desk_page_to_workspace | |||
frappe.patches.v13_0.cleanup_desk_cards | |||
frappe.patches.v13_0.cleanup_desk_cards |
@@ -0,0 +1,21 @@ | |||
from __future__ import unicode_literals | |||
import frappe | |||
def execute(): | |||
navbar_settings = frappe.get_single("Navbar Settings") | |||
if frappe.db.exists('Navbar Item', {'item_label': 'Toggle Theme'}): | |||
return | |||
for navbar_item in navbar_settings.settings_dropdown[6:]: | |||
navbar_item.idx = navbar_item.idx + 1 | |||
navbar_settings.append('settings_dropdown', { | |||
'item_label': 'Toggle Theme', | |||
'item_type': 'Action', | |||
'action': 'new frappe.ui.ThemeSwitcher().show()', | |||
'is_standard': 1, | |||
'idx': 7 | |||
}) | |||
navbar_settings.save() |
@@ -0,0 +1,13 @@ | |||
from __future__ import unicode_literals | |||
import frappe | |||
def execute(): | |||
pages = frappe.get_all("Desk Page", filters={ "is_standard": False }, fields=["name", "extends", "for_user"]) | |||
default_icon = {} | |||
for page in pages: | |||
if page.extends and page.for_user: | |||
if not default_icon.get(page.extends): | |||
default_icon[page.extends] = frappe.db.get_value("Desk Page", page.extends, "icon") | |||
icon = default_icon.get(page.extends) | |||
frappe.db.set_value("Desk Page", page.name, "icon", icon) |
@@ -19,6 +19,7 @@ frappe.ui.form.on("Print Format", { | |||
} | |||
frm.trigger('render_buttons'); | |||
frm.toggle_display('standard', frappe.boot.developer_mode); | |||
frm.trigger('hide_absolute_value_field'); | |||
}, | |||
render_buttons: function (frm) { | |||
frm.page.clear_inner_toolbar(); | |||
@@ -58,5 +59,20 @@ frappe.ui.form.on("Print Format", { | |||
frm.set_value('show_section_headings', value); | |||
frm.set_value('line_breaks', value); | |||
frm.trigger('render_buttons'); | |||
}, | |||
doc_type: function (frm) { | |||
frm.trigger('hide_absolute_value_field'); | |||
}, | |||
hide_absolute_value_field: function (frm) { | |||
// TODO: make it work with frm.doc.doc_type | |||
// Problem: frm isn't updated in some random cases | |||
const doctype = locals[frm.doc.doctype][frm.doc.name]; | |||
if (doctype) { | |||
frappe.model.with_doctype(doctype, () => { | |||
const meta = frappe.get_meta(doctype); | |||
const has_int_float_currency_field = meta.fields.filter(df => in_list(['Int', 'Float', 'Currency'], df.fieldtype)); | |||
frm.toggle_display('absolute_value', has_int_float_currency_field.length); | |||
}); | |||
} | |||
} | |||
}) | |||
}); |
@@ -22,6 +22,7 @@ | |||
"align_labels_right", | |||
"show_section_headings", | |||
"line_breaks", | |||
"absolute_value", | |||
"column_break_11", | |||
"font", | |||
"css_section", | |||
@@ -196,13 +197,21 @@ | |||
"fieldtype": "Check", | |||
"hidden": 1, | |||
"label": "Print Format Builder" | |||
}, | |||
{ | |||
"default": "0", | |||
"depends_on": "doc_type", | |||
"description": "If checked, negative numberic values of Currency, Quantity or Count would be shown as positive", | |||
"fieldname": "absolute_value", | |||
"fieldtype": "Check", | |||
"label": "Show absolute values" | |||
} | |||
], | |||
"icon": "fa fa-print", | |||
"idx": 1, | |||
"index_web_pages_for_search": 1, | |||
"links": [], | |||
"modified": "2020-10-27 18:27:58.307070", | |||
"modified": "2020-12-10 18:58:55.598269", | |||
"modified_by": "Administrator", | |||
"module": "Printing", | |||
"name": "Print Format", | |||
@@ -453,6 +453,7 @@ frappe.ui.form.PrintView = class { | |||
display: block !important; | |||
order: 1; | |||
margin-top: auto; | |||
padding-top: var(--padding-xl) | |||
` | |||
); | |||
} | |||
@@ -208,8 +208,8 @@ frappe.PrintFormatBuilder = Class.extend({ | |||
if(!this.print_heading_template) { | |||
// default print heading template | |||
this.print_heading_template = '<div class="print-heading">\ | |||
<h2>'+__(this.print_format.doc_type) | |||
+'<br><small class="sub-heading">{{ doc.name }}</small>\ | |||
<h2><div>'+__(this.print_format.doc_type) | |||
+'</div><br><small class="sub-heading">{{ doc.name }}</small>\ | |||
</h2></div>'; | |||
} | |||
@@ -1,11 +1,11 @@ | |||
{ | |||
"creation": "2020-10-22 00:00:08.161999", | |||
"css": ".print-format {\n font-size: 13px;\n background: white;\n}\n\n.print-heading {\n border-bottom: 1px solid #E2E6E9;\n padding-bottom: 10px;\n margin-bottom: 20px;\n}\n\n.print-heading h2 {\n font-size: 24px;\n}\n\n.print-heading h2 div {\n margin-bottom: 10px;\n text-transform: uppercase;\n}\n\n.print-heading small {\n font-size: 14px !important;\n font-weight: normal;\n color: #74808B;\n}\n\n.print-format .letter-head {\n margin-bottom: 30px;\n}\n\n.print-format label {\n font-weight: normal;\n font-size: 13px;\n color: #4C5A67;\n}\n\n.print-format .data-field {\n margin-top: 0;\n margin-bottom: 0;\n}\n\n.print-format .value {\n color: #192734;\n}\n\n.print-format .section-break:not(:last-child) {\n margin-bottom: 15px;\n}\n\n.print-format .row {\n line-height: 2;\n margin-top: 5px !important;\n}\n\n.print-format .important .value {\n font-size: 13px;\n font-weight: bold;\n}\n\n.print-format th {\n color: #74808b;\n font-weight: normal;\n border-bottom-width: 1px !important;\n}\n\n.print-format .table-bordered td, .print-format .table-bordered th {\n border: 1px solid #E2E6E9;\n}\n\n.print-format .table-bordered {\n border: 1px solid #E2E6E9;\n}\n\n.print-format td, .print-format th {\n padding: 10px !important;\n}\n\n.print-format .primary.compact-item {\n font-weight: normal;\n}\n", | |||
"css": ".print-format {\n font-size: 13px;\n background: white;\n}\n\n.print-heading {\n border-bottom: 1px solid #f4f5f6;\n padding-bottom: 5px;\n margin-bottom: 10px;\n}\n\n.print-heading h2 {\n font-size: 24px;\n}\n\n.print-heading h2 div {\n font-weight: 600;\n}\n\n.print-heading small {\n font-size: 13px !important;\n font-weight: normal;\n line-height: 2.5;\n color: #4c5a67;\n}\n\n.print-format .letter-head {\n margin-bottom: 30px;\n}\n\n.print-format label {\n font-weight: normal;\n font-size: 13px;\n color: #4C5A67;\n margin-bottom: 0;\n}\n\n.print-format .data-field {\n margin-top: 0;\n margin-bottom: 0;\n}\n\n.print-format .value {\n color: #192734;\n line-height: 1.8;\n}\n\n.print-format .section-break:not(:last-child) {\n margin-bottom: 0;\n}\n\n.print-format .row:not(.section-break) {\n line-height: 1.6;\n margin-top: 15px !important;\n}\n\n.print-format .important .value {\n font-size: 13px;\n font-weight: 600;\n}\n\n.print-format th {\n color: #74808b;\n font-weight: normal;\n border-bottom-width: 1px !important;\n}\n\n.print-format .table-bordered td, .print-format .table-bordered th {\n border: 1px solid #f4f5f6;\n}\n\n.print-format .table-bordered {\n border: 1px solid #f4f5f6;\n}\n\n.print-format td, .print-format th {\n padding: 10px !important;\n}\n\n.print-format .primary.compact-item {\n font-weight: normal;\n}\n\n.print-format table td .value {\n font-size: 12px;\n line-height: 1.8;\n}\n", | |||
"disabled": 0, | |||
"docstatus": 0, | |||
"doctype": "Print Style", | |||
"idx": 0, | |||
"modified": "2020-11-24 12:28:13.229178", | |||
"modified": "2020-12-14 17:56:37.421390", | |||
"modified_by": "Administrator", | |||
"name": "Redesign", | |||
"owner": "Administrator", | |||
@@ -79,7 +79,6 @@ | |||
"public/css/octicons/octicons.css", | |||
"public/less/desk.less", | |||
"public/less/module.less", | |||
"public/less/flex.less", | |||
"public/less/link_preview.less", | |||
"public/less/form.less", | |||
"public/less/mobile.less", | |||
@@ -153,6 +152,7 @@ | |||
"public/js/frappe/ui/capture.js", | |||
"public/js/frappe/ui/app_icon.js", | |||
"public/js/frappe/ui/dropzone.js", | |||
"public/js/frappe/ui/theme_switcher.js", | |||
"public/js/frappe/model/model.js", | |||
"public/js/frappe/db.js", | |||
@@ -203,7 +203,6 @@ | |||
"public/js/frappe/views/translation_manager.js", | |||
"public/js/frappe/views/workspace/workspace.js", | |||
"public/js/frappe/widgets/utils.js", | |||
"public/js/frappe/widgets/widget_group.js", | |||
"public/js/frappe/ui/sort_selector.html", | |||
@@ -1,315 +0,0 @@ | |||
/* the element that this class is applied to, should have a max width for this to work*/ | |||
.page-container { | |||
margin-top: 40px; | |||
} | |||
.page-head { | |||
border-bottom: 1px solid #d1d8dd; | |||
height: 70px; | |||
position: fixed; | |||
left: 0; | |||
right: 0; | |||
top: 41px; | |||
margin: auto; | |||
background-color: #fff; | |||
z-index: 101; | |||
} | |||
.sub-heading { | |||
display: inline-block; | |||
margin-right: 10px; | |||
max-width: 50%; | |||
vertical-align: middle; | |||
} | |||
@media (min-width: 767px) { | |||
.page-body { | |||
overflow-x: hidden; | |||
min-height: calc(100vh - 40px); | |||
} | |||
} | |||
.page-title { | |||
position: relative; | |||
} | |||
.page-title h6 { | |||
margin-top: -8px; | |||
} | |||
.page-title h1 { | |||
margin-top: 17px; | |||
} | |||
.page-title .indicator { | |||
vertical-align: middle; | |||
} | |||
.page-title .title-text { | |||
margin-right: 7px; | |||
max-width: 75%; | |||
display: inline-block; | |||
vertical-align: middle; | |||
} | |||
.page-title .title-image { | |||
width: 46px; | |||
height: 0; | |||
padding: 23px 0; | |||
background-size: contain; | |||
background-repeat: no-repeat; | |||
background-position: center center; | |||
border-radius: 4px; | |||
color: #fff; | |||
text-align: center; | |||
line-height: 0; | |||
float: left; | |||
margin-right: 10px; | |||
} | |||
.editable-title .title-text { | |||
cursor: pointer; | |||
} | |||
.editable-title .title-text:hover { | |||
background-color: #fffce7; | |||
} | |||
.page-actions { | |||
padding-top: 17px; | |||
padding-bottom: 17px; | |||
} | |||
.page-actions .sub-heading { | |||
max-width: 50%; | |||
overflow: hidden; | |||
} | |||
.page-content { | |||
margin-top: 70px; | |||
} | |||
/* show menu aligned to the right border of the dropdown */ | |||
.page-actions .dropdown-menu, | |||
.form-inner-toolbar .dropdown-menu { | |||
right: 0px; | |||
left: auto; | |||
} | |||
.layout-main-section { | |||
border: 1px solid #d1d8dd; | |||
border-top: 0px; | |||
} | |||
.layout-main-section-wrapper { | |||
margin-bottom: 60px; | |||
} | |||
.layout-footer { | |||
border: 1px solid #d1d8dd; | |||
border-top: 0px; | |||
padding: 3px 15px; | |||
font-size: 12px; | |||
} | |||
.page-form { | |||
margin: 0px; | |||
padding-right: 15px; | |||
padding-top: 10px; | |||
display: flex; | |||
flex-wrap: wrap; | |||
border-bottom: 1px solid #d1d8dd; | |||
background-color: #F7FAFC; | |||
} | |||
.page-form .form-group { | |||
padding-right: 0px; | |||
margin-bottom: 10px; | |||
} | |||
.page-form .checkbox { | |||
margin-top: 4px; | |||
margin-bottom: 4px; | |||
} | |||
.page-form .checkbox .help-box { | |||
display: none; | |||
} | |||
select.input-sm { | |||
line-height: 1.2em !important; | |||
} | |||
.message-page { | |||
padding-top: 10rem; | |||
} | |||
.message-page .message-page-icon { | |||
font-size: 10rem; | |||
margin-bottom: 1rem; | |||
} | |||
.message-page .message-page-image { | |||
margin-bottom: 1rem; | |||
} | |||
.message-page .btn-home { | |||
margin-top: 1rem; | |||
} | |||
@media (max-width: 991px) { | |||
.page-head .page-title h1 { | |||
font-size: 22px; | |||
margin-top: 22px; | |||
} | |||
} | |||
@media (max-width: 767px) { | |||
.page-actions { | |||
max-width: 150px; | |||
float: right; | |||
} | |||
.page-title { | |||
position: absolute; | |||
left: 0; | |||
right: 101px; | |||
width: 100%; | |||
} | |||
.page-title h1 { | |||
padding-right: 170px; | |||
} | |||
.page-head .page-title h1 { | |||
font-size: 18px; | |||
} | |||
} | |||
#page-setup-wizard { | |||
margin-top: 30px; | |||
} | |||
@media (min-width: 768px) { | |||
.setup-wizard-slide { | |||
max-width: 500px; | |||
} | |||
} | |||
.setup-wizard-slide { | |||
margin: 60px auto; | |||
padding: 10px 50px; | |||
border: 1px solid #d1d8dd; | |||
box-shadow: 0px 3px 5px rgba(0, 0, 0, 0.1); | |||
} | |||
.setup-wizard-slide .slides-progress { | |||
margin-top: 20px; | |||
} | |||
.setup-wizard-slide .lead { | |||
margin: 30px; | |||
color: #777777; | |||
text-align: center; | |||
font-size: 24px; | |||
} | |||
.setup-wizard-slide .col-sm-12 { | |||
padding: 0px; | |||
} | |||
.setup-wizard-slide .section-body .col-sm-6:first-child { | |||
padding-left: 0px; | |||
} | |||
.setup-wizard-slide .section-body .col-sm-6:last-child { | |||
padding-right: 0px; | |||
} | |||
.setup-wizard-slide .form-control { | |||
font-weight: 500; | |||
} | |||
.setup-wizard-slide .form-control.bold { | |||
background-color: #fff; | |||
} | |||
.setup-wizard-slide .add-more { | |||
margin: 0px; | |||
} | |||
.setup-wizard-slide .footer { | |||
padding: 30px 7px; | |||
} | |||
.setup-wizard-slide a.next-btn.disabled { | |||
background-color: #b1bdca; | |||
color: #fff; | |||
border-color: #b1bdca; | |||
} | |||
.setup-wizard-slide a.complete-btn.disabled { | |||
background-color: #b1bdca; | |||
color: #fff; | |||
border-color: #b1bdca; | |||
} | |||
.setup-wizard-slide .fa-fw { | |||
vertical-align: middle; | |||
font-size: 10px; | |||
} | |||
.setup-wizard-slide .fa-fw.active { | |||
color: #5e64ff; | |||
} | |||
.setup-wizard-slide .icon-circle-blank { | |||
font-size: 7px; | |||
} | |||
.setup-wizard-slide .icon-circle { | |||
font-size: 10px; | |||
} | |||
.setup-wizard-slide .frappe-control[data-fieldtype="Attach Image"] { | |||
width: 140px; | |||
height: 180px; | |||
margin-top: 20px; | |||
} | |||
.setup-wizard-slide .frappe-control[data-fieldtype="Attach Image"] .form-group { | |||
display: none; | |||
} | |||
.setup-wizard-slide .frappe-control[data-fieldtype="Attach Image"] .clearfix { | |||
display: none; | |||
} | |||
.setup-wizard-slide .missing-image { | |||
display: block; | |||
position: relative; | |||
border-radius: 4px; | |||
border: 1px solid #d1d8dd; | |||
border-radius: 6px; | |||
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); | |||
} | |||
.setup-wizard-slide .missing-image .octicon { | |||
position: relative; | |||
top: 50%; | |||
transform: translate(0px, -50%); | |||
-webkit-transform: translate(0px, -50%); | |||
} | |||
.setup-wizard-slide .attach-image-display { | |||
display: block; | |||
position: relative; | |||
border-radius: 4px; | |||
} | |||
.setup-wizard-slide .img-container { | |||
height: 100%; | |||
width: 100%; | |||
padding: 2px; | |||
display: flex; | |||
align-items: center; | |||
justify-content: center; | |||
position: relative; | |||
border: 1px solid #d1d8dd; | |||
border-radius: 6px; | |||
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); | |||
} | |||
.setup-wizard-slide .img-overlay { | |||
display: flex; | |||
align-items: center; | |||
justify-content: center; | |||
position: absolute; | |||
width: 100%; | |||
height: 100%; | |||
color: #777777; | |||
background-color: rgba(255, 255, 255, 0.7); | |||
opacity: 0; | |||
} | |||
.setup-wizard-slide .img-overlay:hover { | |||
opacity: 1; | |||
cursor: pointer; | |||
} | |||
.setup-wizard-slide .progress-bar { | |||
background-color: #5e64ff; | |||
} | |||
.page-card-container { | |||
padding: 70px; | |||
} | |||
.page-card { | |||
max-width: 360px; | |||
margin: 70px auto; | |||
padding: 15px; | |||
border: 1px solid #d1d8dd; | |||
border-radius: 4px; | |||
background-color: #fff; | |||
box-shadow: 0px 3px 6px rgba(0, 0, 0, 0.1); | |||
} | |||
.page-card .page-card-head { | |||
padding: 10px 15px; | |||
margin: -15px; | |||
margin-bottom: 15px; | |||
border-bottom: 1px solid #d1d8dd; | |||
} | |||
.page-card .btn { | |||
margin-top: 30px; | |||
} | |||
.state-icon-container { | |||
display: flex; | |||
justify-content: center; | |||
} | |||
.state-icon { | |||
position: relative; | |||
width: 100px !important; | |||
height: 100px !important; | |||
display: flex; | |||
justify-content: center; | |||
align-items: center; | |||
} |
@@ -35,13 +35,9 @@ | |||
<path fill-rule="evenodd" clip-rule="evenodd" d="M11.717 7c2.29 0 4.211 1.859 4.494 4.266 1.272.152 2.289 1.31 2.289 2.742 0 1.523-1.13 2.742-2.543 2.742H8.043c-1.413 0-2.543-1.219-2.543-2.742 0-1.188.707-2.224 1.724-2.59C7.422 8.92 9.372 7 11.717 7zm.148 2.37a.499.499 0 0 0-.363.156l-1.556 1.555a.5.5 0 1 0 .708.707l.71-.711v3.097a.5.5 0 0 0 1 0v-3.098l.713.712a.5.5 0 1 0 .707-.707l-1.565-1.565a.498.498 0 0 0-.354-.146z" | |||
fill="#fff"></path> | |||
</symbol> | |||
<symbol viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg" id="icon-upload"> | |||
<path d="M10.2427 2.3999V11.7332" stroke="var(--icon-stroke)" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/> | |||
<path d="M7.29004 5.35228L10.2424 2.3999L13.1948 5.35228" stroke="var(--icon-stroke)" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/> | |||
<mask id="path-3-inside-1" fill="white"> | |||
<path d="M13.9334 8.30469H16.8858V15.8999C16.8858 17.0045 15.9904 17.8999 14.8858 17.8999H5.6001C4.49553 17.8999 3.6001 17.0045 3.6001 15.8999V8.30469H6.55248"/> | |||
</mask> | |||
<path d="M16.8858 8.30469H17.8858C17.8858 7.7524 17.4381 7.30469 16.8858 7.30469V8.30469ZM3.6001 8.30469V7.30469C3.04781 7.30469 2.6001 7.7524 2.6001 8.30469H3.6001ZM13.9334 9.30469H16.8858V7.30469H13.9334V9.30469ZM15.8858 8.30469V15.8999H17.8858V8.30469H15.8858ZM14.8858 16.8999H5.6001V18.8999H14.8858V16.8999ZM4.6001 15.8999V8.30469H2.6001V15.8999H4.6001ZM3.6001 9.30469H6.55248V7.30469H3.6001V9.30469ZM5.6001 16.8999C5.04781 16.8999 4.6001 16.4522 4.6001 15.8999H2.6001C2.6001 17.5568 3.94325 18.8999 5.6001 18.8999V16.8999ZM15.8858 15.8999C15.8858 16.4522 15.4381 16.8999 14.8858 16.8999V18.8999C16.5427 18.8999 17.8858 17.5568 17.8858 15.8999H15.8858Z" fill="white" mask="url(#path-3-inside-1)"/> | |||
<symbol viewBox="0 0 20 20" id="icon-upload" fill="currentColor" xmlns="http://www.w3.org/2000/svg"> | |||
<path d="M10.596 2.046a.5.5 0 0 0-.707 0L6.937 5a.5.5 0 0 0 .707.707l2.099-2.099v8.126a.5.5 0 1 0 1 0V3.607l2.098 2.099a.5.5 0 0 0 .708-.707l-2.953-2.953z"/> | |||
<path d="M6.552 8.305v1H4.6V15.9a1 1 0 0 0 1 1h9.286a1 1 0 0 0 1-1V9.305h-1.953v-1h2.953V15.9a2 2 0 0 1-2 2H5.6a2 2 0 0 1-2-2V8.305h2.952z"/> | |||
</symbol> | |||
<symbol viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" id="icon-tag"> | |||
<path d="M12.6401 10.2571L10.107 12.7901C9.52125 13.3759 8.5718 13.3756 7.98601 12.7898L2.49654 7.30037C2.40278 7.2066 2.3501 7.07942 2.3501 6.94682L2.3501 3C2.3501 2.72386 2.57396 2.5 2.8501 2.5L6.79691 2.5C6.92952 2.5 7.0567 2.55268 7.15047 2.64645L12.6399 8.13591C13.2257 8.7217 13.2259 9.67131 12.6401 10.2571Z" stroke="var(--icon-stroke)" stroke-linecap="round" stroke-linejoin="round"/> | |||
@@ -105,26 +101,12 @@ | |||
<path d="M8 2a10.534 10.534 0 0 1-6 2.8s0 6.8 6 9.2c6-2.4 6-9.2 6-9.2A10.534 10.534 0 0 1 8 2z" stroke="var(--icon-stroke)" stroke-linecap="round" stroke-linejoin="round"></path> | |||
<path d="M5.75 8l1.5 1.5 3-3" stroke="var(--icon-stroke)" stroke-linecap="round" stroke-linejoin="round"></path> | |||
</symbol> | |||
<symbol viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" id="icon-printer"> | |||
<symbol viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" id="icon-printer"> | |||
<g id="Icon / Printer"> | |||
<g id="Group 308"> | |||
<g id="Vector"> | |||
<mask id="path-2-inside-1" fill="white"> | |||
<path d="M4 5.5V3C4 2.17157 4.67157 1.5 5.5 1.5H10.5C11.3284 1.5 12 2.17157 12 3V5.5"/> | |||
</mask> | |||
<path d="M4 5C3.72386 5 3.5 5.22386 3.5 5.5C3.5 5.77614 3.72386 6 4 6V5ZM12 6C12.2761 6 12.5 5.77614 12.5 5.5C12.5 5.22386 12.2761 5 12 5V6ZM12 5H4V6H12V5ZM3 5.5C3 6.05228 3.44772 6.5 4 6.5C4.55228 6.5 5 6.05228 5 5.5H3ZM11 5.5C11 6.05228 11.4477 6.5 12 6.5C12.5523 6.5 13 6.05228 13 5.5H11ZM5 5.5V3H3V5.5H5ZM5.5 2.5H10.5V0.5H5.5V2.5ZM11 3V5.5H13V3H11ZM10.5 2.5C10.7761 2.5 11 2.72386 11 3H13C13 1.61929 11.8807 0.5 10.5 0.5V2.5ZM5 3C5 2.72386 5.22386 2.5 5.5 2.5V0.5C4.11929 0.5 3 1.61929 3 3H5Z" fill="var(--icon-stroke)" stroke-width="0" mask="url(#path-2-inside-1)"/> | |||
</g> | |||
<g id="Vector_2"> | |||
<mask id="path-4-inside-2" fill="white"> | |||
<path d="M3.7 11.5H4.9H11.8H13.3C13.9627 11.5 14.5 10.9628 14.5 10.3V7.2C14.5 5.98497 13.515 5 12.3 5H3.7C2.48497 5 1.5 5.98497 1.5 7.2V10.3C1.5 10.9628 2.03726 11.5 2.7 11.5H3.7Z"/> | |||
</mask> | |||
<path d="M4.9 10.5H3.7V12.5H4.9V10.5ZM2.5 10.3V7.2H0.5V10.3H2.5ZM3.7 6H12.3V4H3.7V6ZM13.5 7.2V10.3H15.5V7.2H13.5ZM13.3 10.5H11.8V12.5H13.3V10.5ZM2.7 12.5H3.7V10.5H2.7V12.5ZM11.8 10.5H4.9V12.5H11.8V10.5ZM0.5 10.3C0.5 11.515 1.48497 12.5 2.7 12.5V10.5C2.58954 10.5 2.5 10.4105 2.5 10.3H0.5ZM13.5 10.3C13.5 10.4105 13.4105 10.5 13.3 10.5V12.5C14.515 12.5 15.5 11.515 15.5 10.3H13.5ZM12.3 6C12.9627 6 13.5 6.53726 13.5 7.2H15.5C15.5 5.43269 14.0673 4 12.3 4V6ZM2.5 7.2C2.5 6.53726 3.03726 6 3.7 6V4C1.93269 4 0.5 5.43269 0.5 7.2H2.5Z" fill="var(--icon-stroke)" mask="url(#path-4-inside-2)" stroke-width="0"/> | |||
</g> | |||
<path id="Vector_3" d="M4.5 9.5L11.5 9.5V12C11.5 12.5523 11.0523 13 10.5 13H5.5C4.94772 13 4.5 12.5523 4.5 12V9.5Z" fill="var(--icon-fill-bg)" stroke-linecap="round" stroke-linejoin="round"/> | |||
</g> | |||
<circle id="Ellipse 30" cx="12" cy="7.5" r="0.5" fill="var(--icon-stroke)" stroke-width="0"/> | |||
<path id="Union" fill-rule="evenodd" clip-rule="evenodd" d="M11 3.5V5.5H5V3.5C5 3.22386 5.22386 3 5.5 3H10.5C10.7761 3 11 3.22386 11 3.5ZM4 5.5V3.5C4 2.67157 4.67157 2 5.5 2H10.5C11.3284 2 12 2.67157 12 3.5V5.5H12.3C13.515 5.5 14.5 6.48497 14.5 7.7V10.8C14.5 11.4628 13.9627 12 13.3 12L12 12V12.5C12 13.3284 11.3284 14 10.5 14H5.5C4.67157 14 4 13.3284 4 12.5V12L3.7 12H2.7C2.03726 12 1.5 11.4628 1.5 10.8V7.7C1.5 6.48497 2.48497 5.5 3.7 5.5H4ZM5 12V11.5V10.5H6H6.5H8.5H10H11V11.5V12V12.5C11 12.7761 10.7761 13 10.5 13H5.5C5.22386 13 5 12.7761 5 12.5V12ZM4 10.5V11L3.7 11H3.69999H2.7C2.58954 11 2.5 10.9105 2.5 10.8V7.7C2.5 7.03726 3.03726 6.5 3.7 6.5H12.3C12.9627 6.5 13.5 7.03726 13.5 7.7V10.8C13.5 10.9105 13.4105 11 13.3 11L12 11V10.5C12 9.94772 11.5523 9.5 11 9.5H8.5H6.5H5C4.44772 9.5 4 9.94772 4 10.5ZM12 8.5C12.2761 8.5 12.5 8.27614 12.5 8C12.5 7.72386 12.2761 7.5 12 7.5C11.7239 7.5 11.5 7.72386 11.5 8C11.5 8.27614 11.7239 8.5 12 8.5Z" fill="var(--icon-stroke)" stroke="var(--icon-fill)"/> | |||
</g> | |||
</symbol> | |||
<symbol id="icon-notification-with-indicator" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg" > | |||
<path d="M12.4663 15.0275H16.5872L15.4292 13.8695C15.2737 13.714 15.1504 13.5293 15.0662 13.3261C14.9821 13.1229 14.9388 12.9051 14.9389 12.6852V9.09341C14.939 8.07055 14.622 7.07281 14.0316 6.23755C13.4412 5.40228 12.6064 4.77057 11.6421 4.4294V4.14835C11.6421 3.71118 11.4685 3.29192 11.1594 2.98279C10.8502 2.67367 10.431 2.5 9.9938 2.5C9.55663 2.5 9.13736 2.67367 8.82824 2.98279C8.51911 3.29192 8.34545 3.71118 8.34545 4.14835V4.4294C6.42512 5.10852 5.04874 6.94066 5.04874 9.09341V12.686C5.04874 13.1294 4.87237 13.5555 4.55836 13.8695L3.40039 15.0275H7.52127M12.4663 15.0275H7.52127M12.4663 15.0275C12.4663 15.6832 12.2058 16.3121 11.7421 16.7758C11.2785 17.2395 10.6496 17.5 9.9938 17.5C9.33804 17.5 8.70914 17.2395 8.24546 16.7758C7.78177 16.3121 7.52127 15.6832 7.52127 15.0275" stroke="var(--icon-stroke)" stroke-linecap="round" stroke-linejoin="round"/> | |||
<path d="M14 6.75C15.5188 6.75 16.75 5.51878 16.75 4C16.75 2.48122 15.5188 1.25 14 1.25C12.4812 1.25 11.25 2.48122 11.25 4C11.25 5.51878 12.4812 6.75 14 6.75Z" fill="#FF5858" stroke="white" stroke-width="1.5"/> | |||
@@ -152,12 +134,11 @@ | |||
<path d="M4.4 5h5.215M4.4 7.4h2.607" stroke-linecap="round"></path> | |||
</g> | |||
</symbol> | |||
<symbol viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" id="icon-menu"> | |||
<path fill-rule="evenodd" clip-rule="evenodd" d="M2 4a.5.5 0 0 1 .5-.5h10.998a.5.5 0 0 1 0 1H2.5A.5.5 0 0 1 2 4zm0 4a.5.5 0 0 1 .5-.5h10.998a.5.5 0 0 1 0 1H2.5A.5.5 0 0 1 2 8zm.5 3.5a.5.5 0 0 0 0 1h10.998a.5.5 0 0 0 0-1H2.5z" fill="var(--icon-stroke)"></path> | |||
<symbol viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" id="icon-menu"> | |||
<path fill-rule="evenodd" clip-rule="evenodd" d="M3.25 6C3.25 5.72386 3.47386 5.5 3.75 5.5H20.2474C20.5236 5.5 20.7474 5.72386 20.7474 6C20.7474 6.27614 20.5236 6.5 20.2474 6.5H3.75C3.47386 6.5 3.25 6.27614 3.25 6ZM3.25 12C3.25 11.7239 3.47386 11.5 3.75 11.5H20.2474C20.5236 11.5 20.7474 11.7239 20.7474 12C20.7474 12.2761 20.5236 12.5 20.2474 12.5H3.75C3.47386 12.5 3.25 12.2761 3.25 12ZM3.75 17.5C3.47386 17.5 3.25 17.7239 3.25 18C3.25 18.2761 3.47386 18.5 3.75 18.5H20.2474C20.5236 18.5 20.7474 18.2761 20.7474 18C20.7474 17.7239 20.5236 17.5 20.2474 17.5H3.75Z" fill="var(--icon-stroke)"/> | |||
</symbol> | |||
<symbol viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" id="icon-lock"> | |||
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.077 1.45h-.055a3.356 3.356 0 0 0-3.387 3.322v.35H3.75a2 2 0 0 0-2 2v5.391a2 2 0 0 0 2 2h8.539a2 2 0 0 0 2-2V7.122a2 2 0 0 0-2-2h-.885v-.285A3.356 3.356 0 0 0 8.082 1.45h-.005zm2.327 3.672V4.83a2.356 2.356 0 0 0-2.33-2.38h-.06a2.356 2.356 0 0 0-2.38 2.33v.342h4.77zm-6.654 1a1 1 0 0 0-1 1v5.391a1 1 0 0 0 1 1h8.539a1 1 0 0 0 1-1V7.122a1 1 0 0 0-1-1H3.75zm4.27 4.269a.573.573 0 1 0 0-1.147.573.573 0 0 0 0 1.147zm1.573-.574a1.573 1.573 0 1 1-3.147 0 1.573 1.573 0 0 1 3.147 0z" | |||
fill="#12283A"></path> | |||
<symbol fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" id="icon-lock"> | |||
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.077 1.45h-.055a3.356 3.356 0 00-3.387 3.322v.35H3.75a2 2 0 00-2 2v5.391a2 2 0 002 2h8.539a2 2 0 002-2V7.122a2 2 0 00-2-2h-.885v-.285A3.356 3.356 0 008.082 1.45h-.005zm2.327 3.672V4.83a2.356 2.356 0 00-2.33-2.38h-.06a2.356 2.356 0 00-2.38 2.33v.342h4.77zm-6.654 1a1 1 0 00-1 1v5.391a1 1 0 001 1h8.539a1 1 0 001-1V7.122a1 1 0 00-1-1H3.75zm4.27 4.269a.573.573 0 100-1.147.573.573 0 000 1.147zm1.573-.574a1.573 1.573 0 11-3.147 0 1.573 1.573 0 013.147 0z"></path> | |||
</symbol> | |||
<symbol viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" id="icon-list_alt"> | |||
<path fill-rule="evenodd" clip-rule="evenodd" d="M2.5 3.722c0-.454.316-.722.59-.722h9.82c.274 0 .59.268.59.722v8.556c0 .454-.316.722-.59.722H3.09c-.274 0-.59-.268-.59-.722V3.722zM3.09 2c-.93 0-1.59.826-1.59 1.722v8.556c0 .896.66 1.722 1.59 1.722h9.82c.93 0 1.59-.826 1.59-1.722V3.722C14.5 2.826 13.84 2 12.91 2H3.09zM5 4.5a.5.5 0 0 0 0 1h4.002a.5.5 0 1 0 0-1H5zM5 7a.5.5 0 0 0 0 1h5.002a.5.5 0 1 0 0-1H5zm-.5 3a.5.5 0 0 1 .5-.5h2.27a.5.5 0 0 1 0 1H5a.5.5 0 0 1-.5-.5z" | |||
@@ -657,7 +638,29 @@ | |||
<path fill-rule="evenodd" clip-rule="evenodd" d="M19 0a1 1 0 011 1v10a3 3 0 003 3h12a2 2 0 012 2v26a4 4 0 01-4 4H4a4 4 0 01-4-4V3a3 3 0 013-3h16zM8 37a1 1 0 100 2h21a1 1 0 100-2H8zm-1-7a1 1 0 011-1h21a1 1 0 110 2H8a1 1 0 01-1-1zm1-9a1 1 0 100 2h6a1 1 0 100-2H8z"></path> | |||
</g> | |||
</symbol> | |||
<svg viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" id="icon-comment"> | |||
<symbol viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" id="icon-comment"> | |||
<path d="M14.2222 7.17042C14.2222 4.19205 11.4363 1.77783 7.99999 1.77783C4.56367 1.77783 1.77777 4.19205 1.77777 7.17042C1.77777 10.1488 4.56367 12.563 7.99999 12.563C8.43555 12.563 8.86032 12.5232 9.27099 12.4494L12.563 14.2223V10.8283C13.59 9.86672 14.2222 8.58411 14.2222 7.17042" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/> | |||
</svg> | |||
</symbol> | |||
<symbol viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg" id="icon-clap"> | |||
<path d="M12.5842 4.63692L10.046 2.0995C9.96005 2.01355 9.85773 1.94571 9.7451 1.89999C9.63247 1.85428 9.51182 1.83161 9.39028 1.83334C9.26874 1.83507 9.14878 1.86116 9.03749 1.91006C8.92621 1.95896 8.82586 2.02968 8.74239 2.11804V2.11804C8.57986 2.29008 8.49084 2.51872 8.49423 2.75536C8.49763 2.992 8.59318 3.21799 8.76057 3.3853L11.542 6.16672" stroke="var(--icon-stroke)" stroke-miterlimit="10"/> | |||
<path d="M9.14288 3.76929L7.72472 2.36131C7.63876 2.27536 7.53645 2.20752 7.42382 2.1618C7.31119 2.11609 7.19053 2.09343 7.06899 2.09515C6.94745 2.09688 6.82749 2.12297 6.7162 2.17187C6.60492 2.22077 6.50457 2.29149 6.4211 2.37985V2.37985C6.25857 2.55189 6.16955 2.78053 6.17295 3.01717C6.17634 3.25381 6.27189 3.4798 6.43928 3.64711" stroke="var(--icon-stroke)" stroke-miterlimit="10"/> | |||
<path d="M17.034 11.2878C17.517 10.1522 17.568 8.8791 17.1772 7.70853C16.6445 6.11619 15.8874 4.32021 15.5245 2.69041C15.5019 2.55824 15.453 2.43196 15.3807 2.31907C15.3083 2.20618 15.214 2.10898 15.1034 2.03326C14.9927 1.95754 14.868 1.90483 14.7366 1.87827C14.6051 1.85172 14.4697 1.85186 14.3383 1.87868C14.207 1.9055 14.0823 1.95846 13.9718 2.0344C13.8613 2.11035 13.7672 2.20773 13.6951 2.32077C13.623 2.43381 13.5743 2.56019 13.552 2.6924C13.5297 2.82462 13.5342 2.95997 13.5653 3.09041C13.7122 4.08612 13.7838 5.09151 13.7794 6.09801" stroke="var(--icon-stroke)" stroke-miterlimit="10"/> | |||
<path d="M4.0558 9.64014C3.87166 9.45977 3.62471 9.35791 3.36695 9.35601C3.1092 9.35411 2.86078 9.45232 2.674 9.62995V9.62995C2.57804 9.72053 2.50124 9.82943 2.44814 9.95022C2.39503 10.071 2.36671 10.2012 2.36484 10.3332C2.36298 10.4651 2.38761 10.5961 2.43728 10.7183C2.48695 10.8406 2.56064 10.9516 2.654 11.0448L8.22812 16.6186C9.00617 17.3963 10.0612 17.8333 11.1614 17.8333C12.2615 17.8333 13.3165 17.3963 14.0946 16.6186V16.6186C14.8197 15.8958 15.329 14.9852 15.5654 13.989C15.8017 12.9929 15.7558 11.9505 15.4328 10.979C14.855 9.25069 14.0331 7.30125 13.639 5.53037C13.6127 5.38837 13.5585 5.25302 13.4793 5.13225C13.4002 5.01148 13.2977 4.90772 13.178 4.82703C13.0582 4.74634 12.9236 4.69034 12.7819 4.66232C12.6403 4.6343 12.4944 4.63481 12.353 4.66383C12.2115 4.69285 12.0773 4.7498 11.9581 4.83133C11.8389 4.91287 11.7372 5.01735 11.6589 5.13868C11.5806 5.26 11.5273 5.39573 11.5021 5.53791C11.4769 5.68009 11.4803 5.82587 11.5121 5.96672C11.5223 6.04745 11.7426 7.69252 11.9808 8.9216C11.9848 8.94271 11.9819 8.96455 11.9726 8.98391C11.9633 9.00327 11.948 9.01912 11.929 9.02913C11.91 9.03915 11.8882 9.0428 11.867 9.03954C11.8458 9.03629 11.8261 9.0263 11.811 9.01105L7.69176 4.89001C7.59846 4.79674 7.48741 4.72313 7.36517 4.67352C7.24294 4.62391 7.11199 4.59931 6.98009 4.60117C6.84818 4.60304 6.71799 4.63133 6.5972 4.68437C6.47641 4.73742 6.36749 4.81414 6.27687 4.91001C6.10045 5.09676 6.00381 5.34493 6.00748 5.60181C6.01114 5.85868 6.11483 6.10399 6.2965 6.28563L9.78083 9.76995" stroke="var(--icon-stroke)" stroke-miterlimit="10" stroke-linecap="square"/> | |||
<path d="M4.05713 6.85146C3.8756 6.66973 3.63032 6.56599 3.37348 6.56233C3.11665 6.55866 2.86851 6.65535 2.68187 6.83182V6.83182C2.58592 6.92243 2.50912 7.03136 2.45601 7.15216C2.4029 7.27297 2.37456 7.4032 2.37266 7.53515C2.37076 7.6671 2.39535 7.79809 2.44496 7.92038C2.49457 8.04266 2.56821 8.15375 2.6615 8.24708L6.98327 12.5674" stroke="var(--icon-stroke)" stroke-miterlimit="10" stroke-linecap="square"/> | |||
<path d="M6.37845 6.36782L5.17337 5.17438C5.08009 5.08101 4.96903 5.00731 4.84675 4.95764C4.72448 4.90797 4.59348 4.88333 4.46151 4.8852C4.32955 4.88706 4.1993 4.91539 4.07848 4.9685C3.95766 5.0216 3.84872 5.09842 3.75811 5.19438V5.19438C3.58179 5.38117 3.4852 5.62933 3.48887 5.88618C3.49254 6.14302 3.59616 6.38832 3.77775 6.57L8.37988 11.1707" stroke="var(--icon-stroke)" stroke-miterlimit="10" stroke-linecap="square"/> | |||
</symbol> | |||
<symbol viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg" id="icon-criticize"> | |||
<path d="M10.7275 6.10907L8.54574 6.10907" stroke="var(--icon-stroke)" stroke-miterlimit="10" stroke-linecap="square"/> | |||
<path d="M11.8341 14.9294L12.909 9.01814L16.4675 9.01814C17.1919 9.01814 17.8639 8.52288 17.9802 7.80797C18.1279 6.89671 17.429 6.10908 16.5453 6.10908L10.7272 6.10908L10.7272 4.65454C10.7272 3.85092 10.0763 3.20001 9.27266 3.20001L7.16723 3.20001C6.17669 3.20001 5.21525 3.53746 4.44143 4.15637L2 6.10908L2 15.5635L9.35557 16.6952C10.5243 16.8748 11.6232 16.0915 11.8341 14.9294Z" stroke="var(--icon-stroke)" stroke-miterlimit="10" stroke-linecap="square"/> | |||
<path d="M8.54541 9.74542L4.90908 9.74542" stroke="var(--icon-stroke)" stroke-miterlimit="10" stroke-linecap="square"/> | |||
<path d="M8.54541 12.6545L4.90908 12.6545" stroke="var(--icon-stroke)" stroke-miterlimit="10" stroke-linecap="square"/> | |||
</symbol> | |||
<symbol viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" id="icon-sidebar-collapse"> | |||
<path d="M12 6L6 12L12 18" stroke="var(--icon-stroke)" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> | |||
<path d="M18 6L12 12L18 18" stroke="var(--icon-stroke)" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> | |||
</symbol> | |||
<symbol viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" id="icon-sidebar-expand"> | |||
<path d="M12 18L18 12L12 6" stroke="var(--icon-stroke)" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> | |||
<path d="M6 18L12 12L6 6" stroke="var(--icon-stroke)" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> | |||
</symbol> | |||
</svg> |
@@ -2670,20 +2670,6 @@ frappe.chat.render = (render = true, force = false) => | |||
// With the assumption, that there's only one navbar. | |||
const $placeholder = $('.navbar .frappe-chat-dropdown') | |||
// Render if frappe-chat-toggle doesn't exist. | |||
if ( frappe.utils.is_empty($placeholder.has('.frappe-chat-toggle')) ) { | |||
const $template = $(` | |||
<a class="dropdown-toggle frappe-chat-toggle" data-toggle="dropdown"> | |||
<div> | |||
<i class="octicon octicon-comment-discussion"/> | |||
</div> | |||
</a> | |||
`) | |||
$placeholder.addClass('dropdown hidden') | |||
$placeholder.html($template) | |||
} | |||
if ( render ) { | |||
$placeholder.removeClass('hidden') | |||
} else { | |||
@@ -101,6 +101,7 @@ frappe.data_import.ImportPreview = class ImportPreview { | |||
.replace('%H', 'HH') | |||
.replace('%M', 'mm') | |||
.replace('%S', 'ss') | |||
.replace('%b', 'Mon') | |||
: null; | |||
let column_title = `<span class="indicator green"> | |||
@@ -61,13 +61,8 @@ frappe.Application = Class.extend({ | |||
shortcut: 'shift+ctrl+g', | |||
description: __('Switch Theme'), | |||
action: () => { | |||
let new_theme = document.body.dataset.theme == "dark" ? "Light" : "Dark"; | |||
frappe.call('frappe.core.doctype.user.user.switch_theme', { | |||
theme: new_theme | |||
}).then(() => { | |||
document.body.dataset.theme = new_theme.toLowerCase(); | |||
frappe.show_alert("Theme Changed", 3); | |||
}) | |||
frappe.theme_switcher = new frappe.ui.ThemeSwitcher(); | |||
frappe.theme_switcher.show(); | |||
} | |||
}) | |||
@@ -153,7 +148,6 @@ frappe.Application = Class.extend({ | |||
user: frappe.session.user | |||
}, | |||
callback: function(r) { | |||
console.log(r); | |||
if (r.message.show_alert) { | |||
frappe.show_alert({ | |||
indicator: 'red', | |||
@@ -165,8 +159,6 @@ frappe.Application = Class.extend({ | |||
}, 600000); // check every 10 minutes | |||
} | |||
} | |||
this.fetch_tags(); | |||
}, | |||
set_route() { | |||
@@ -175,7 +167,7 @@ frappe.Application = Class.extend({ | |||
localStorage.removeItem("session_last_route"); | |||
} else { | |||
// route to home page | |||
frappe.route(); | |||
frappe.router.route(); | |||
} | |||
}, | |||
@@ -268,6 +260,7 @@ frappe.Application = Class.extend({ | |||
this.check_metadata_cache_status(); | |||
this.set_globals(); | |||
this.sync_pages(); | |||
frappe.router.setup(); | |||
moment.locale("en"); | |||
moment.user_utc_offset = moment().utcOffset(); | |||
if(frappe.boot.timezone_info) { | |||
@@ -277,6 +270,7 @@ frappe.Application = Class.extend({ | |||
frappe.dom.set_style(frappe.boot.print_css, "print-style"); | |||
} | |||
frappe.user.name = frappe.boot.user.name; | |||
frappe.router.setup(); | |||
} else { | |||
this.set_as_guest(); | |||
} | |||
@@ -299,6 +293,7 @@ frappe.Application = Class.extend({ | |||
set_globals: function() { | |||
frappe.session.user = frappe.boot.user.name; | |||
frappe.session.logged_in_user = frappe.boot.user.name; | |||
frappe.session.user_email = frappe.boot.user.email; | |||
frappe.session.user_fullname = frappe.user_info().fullname; | |||
@@ -547,13 +542,7 @@ frappe.Application = Class.extend({ | |||
}, | |||
add_browser_class() { | |||
let browsers = ['Chrome', 'Firefox', 'Safari']; | |||
for (let browser of browsers) { | |||
if (navigator.userAgent.includes(browser)) { | |||
$('html').addClass(browser.toLowerCase()); | |||
return; | |||
} | |||
} | |||
$('html').addClass(frappe.utils.get_browser().name.toLowerCase()); | |||
}, | |||
set_fullwidth_if_enabled() { | |||
@@ -610,10 +599,6 @@ frappe.Application = Class.extend({ | |||
frappe.show_alert(message); | |||
}); | |||
}, | |||
fetch_tags() { | |||
frappe.tags.utils.fetch_tags(); | |||
} | |||
}); | |||
frappe.get_module = function(m, default_module) { | |||
@@ -293,10 +293,13 @@ frappe.unscrub = function(txt) { | |||
return frappe.model.unscrub(txt); | |||
}; | |||
frappe.get_data_pill = (label, target_id=null, remove_action=null) => { | |||
frappe.get_data_pill = (label, target_id=null, remove_action=null, image=null) => { | |||
let data_pill_wrapper = $(` | |||
<button class="data-pill btn"> | |||
<span class="pill-label ellipsis">${label}</span> | |||
<div class="flex align-center ellipsis"> | |||
${image ? image : ''} | |||
<span class="pill-label ${image ? "ml-2" : ""}">${label}</span> | |||
</div> | |||
</button> | |||
`); | |||
@@ -88,6 +88,22 @@ frappe.ui.form.ControlAutocomplete = frappe.ui.form.ControlData.extend({ | |||
} | |||
}); | |||
this.$input.on("awesomplete-open", () => { | |||
let modal = this.$input.parents('.modal-dialog')[0]; | |||
if (modal) { | |||
$(modal).removeClass("modal-dialog-scrollable"); | |||
} | |||
this.autocomplete_open = true; | |||
}); | |||
this.$input.on("awesomplete-close", () => { | |||
let modal = this.$input.parents('.modal-dialog')[0]; | |||
if (modal) { | |||
$(modal).addClass("modal-dialog-scrollable"); | |||
} | |||
this.autocomplete_open = false; | |||
}); | |||
this.$input.on('awesomplete-selectcomplete', () => { | |||
this.$input.trigger('change'); | |||
}); | |||
@@ -55,9 +55,12 @@ frappe.ui.form.ControlButton = frappe.ui.form.ControlData.extend({ | |||
set_empty_description: function() { | |||
this.$wrapper.find(".help-box").empty().toggle(false); | |||
}, | |||
set_label: function() { | |||
set_label: function(label) { | |||
if (label) { | |||
this.df.label = label; | |||
} | |||
label = (this.df.icon ? frappe.utils.icon(this.df.icon) : "") + __(this.df.label); | |||
$(this.label_span).html(" "); | |||
this.$input && this.$input.html((this.df.icon ? | |||
('<i class="'+this.df.icon+' fa-fw"></i> ') : "") + __(this.df.label)); | |||
this.$input && this.$input.html(label); | |||
} | |||
}); |
@@ -6,6 +6,8 @@ | |||
// add_fetches | |||
import Awesomplete from 'awesomplete'; | |||
frappe.ui.form.recent_link_validations = {}; | |||
frappe.ui.form.ControlLink = frappe.ui.form.ControlData.extend({ | |||
make_input: function() { | |||
var me = this; | |||
@@ -230,15 +232,20 @@ frappe.ui.form.ControlLink = frappe.ui.form.ControlData.extend({ | |||
} | |||
}); | |||
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-open", () => { | |||
let modal = this.$input.parents('.modal-dialog')[0]; | |||
if (modal) { | |||
$(modal).removeClass("modal-dialog-scrollable"); | |||
} | |||
this.autocomplete_open = true; | |||
}); | |||
this.$input.on("awesomplete-close", function() { | |||
me.$wrapper.css({"z-index": 1}); | |||
me.autocomplete_open = false; | |||
this.$input.on("awesomplete-close", () => { | |||
let modal = this.$input.parents('.modal-dialog')[0]; | |||
if (modal) { | |||
$(modal).addClass("modal-dialog-scrollable"); | |||
} | |||
this.autocomplete_open = false; | |||
}); | |||
this.$input.on("awesomplete-select", function(e) { | |||
@@ -434,40 +441,44 @@ frappe.ui.form.ControlLink = frappe.ui.form.ControlData.extend({ | |||
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(""); | |||
} | |||
} | |||
}); | |||
// if default and no fetch, no need to validate | |||
if (!fetch && df.__default_value && df.__default_value===value) return value; | |||
this.fetch_and_validate_link(resolve, df, doctype, docname, value, fetch); | |||
}); | |||
} | |||
}, | |||
fetch_and_validate_link(resolve, df, doctype, docname, value, fetch) { | |||
frappe.call({ | |||
method:'frappe.desk.form.utils.validate_link', | |||
type: "GET", | |||
args: { | |||
'value': value, | |||
'options': doctype, | |||
'fetch': fetch | |||
}, | |||
no_spinner: true, | |||
callback: (r) => { | |||
if(r.message=='Ok') { | |||
if(r.fetch_values && docname) { | |||
this.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++) { | |||
@@ -210,7 +210,7 @@ frappe.ui.form.ControlMultiSelectList = frappe.ui.form.ControlData.extend({ | |||
<strong>${option.label}</strong> | |||
<div class="small">${option.description}</div> | |||
</div> | |||
<div><span class="octicon octicon-check text-muted"></span></div> | |||
<div class="multiselect-check">${frappe.utils.icon('tick', 'xs')}</div> | |||
</li>`; | |||
}).join(''); | |||
if (!html) { | |||
@@ -87,7 +87,7 @@ frappe.ui.form.ControlMultiSelectPills = frappe.ui.form.ControlAutocomplete.exte | |||
return `<div class="btn-group tb-selected-value" data-value="${encoded_value}"> | |||
<button class="btn btn-default btn-xs btn-link-to-form">${__(value)}</button> | |||
<button class="btn btn-default btn-xs btn-remove"> | |||
<i class="fa fa-remove text-muted"></i> | |||
${frappe.utils.icon('close')} | |||
</button> | |||
</div>`; | |||
}, | |||
@@ -126,7 +126,7 @@ frappe.ui.form.ControlTableMultiSelect = frappe.ui.form.ControlLink.extend({ | |||
return `<div class="btn-group tb-selected-value" data-value="${encoded_value}"> | |||
<button class="btn btn-default btn-xs btn-link-to-form">${__(value)}</button> | |||
<button class="btn btn-default btn-xs btn-remove"> | |||
<i class="fa fa-remove text-muted"></i> | |||
${frappe.utils.icon('close')} | |||
</button> | |||
</div>`; | |||
}, | |||
@@ -157,12 +157,11 @@ frappe.ui.form.ControlTextEditor = frappe.ui.form.ControlCode.extend({ | |||
get_toolbar_options() { | |||
return [ | |||
[{ 'header': [1, 2, 3, false] }], | |||
['bold', 'italic', 'underline'], | |||
['bold', 'italic', 'underline', 'clean'], | |||
[{ 'color': [] }, { 'background': [] }], | |||
['blockquote', 'code-block'], | |||
['link', 'image'], | |||
[{ 'list': 'ordered' }, { 'list': 'bullet' }, { 'list': 'check' }], | |||
[{ 'align': [] }], | |||
[{ 'indent': '-1'}, { 'indent': '+1' }], | |||
[{'table': [ | |||
'insert-table', | |||
@@ -174,7 +173,6 @@ frappe.ui.form.ControlTextEditor = frappe.ui.form.ControlCode.extend({ | |||
'delete-column', | |||
'delete-table', | |||
]}], | |||
['clean'] | |||
]; | |||
}, | |||
@@ -1,92 +1,101 @@ | |||
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors | |||
// MIT License. See license.txt | |||
frappe.ui.form.Dashboard = Class.extend({ | |||
init: function(opts) { | |||
frappe.ui.form.Dashboard = class FormDashboard { | |||
constructor(opts) { | |||
$.extend(this, opts); | |||
this.section = this.frm.fields_dict._form_dashboard.wrapper; | |||
this.parent = this.section.find('.section-body'); | |||
this.wrapper = $(frappe.render_template('form_dashboard', | |||
{frm: this.frm})).appendTo(this.parent); | |||
this.progress_area = this.wrapper.find(".progress-area"); | |||
this.heatmap_area = this.wrapper.find('.form-heatmap'); | |||
this.chart_area = this.wrapper.find('.form-graph'); | |||
this.stats_area = this.wrapper.find('.form-stats'); | |||
this.stats_area_row = this.stats_area.find('.row'); | |||
this.links_area = this.wrapper.find('.form-links'); | |||
this.transactions_area = this.links_area.find('.transactions'); | |||
}, | |||
reset: function() { | |||
this.setup_dashboard_sections(); | |||
} | |||
setup_dashboard_sections() { | |||
this.progress_area = new Section(this.parent, { | |||
css_class: 'progress-area', | |||
hidden: 1, | |||
collapsible: 1 | |||
}); | |||
this.heatmap_area = new Section(this.parent, { | |||
title: __("Overview"), | |||
css_class: 'form-heatmap', | |||
hidden: 1, | |||
collapsible: 1, | |||
body_html: ` | |||
<div id="heatmap-${frappe.model.scrub(this.frm.doctype)}" class="heatmap"></div> | |||
<div class="text-muted small heatmap-message hidden"></div> | |||
` | |||
}); | |||
this.chart_area = new Section(this.parent, { | |||
title: __("Graph"), | |||
css_class: 'form-graph', | |||
hidden: 1, | |||
collapsible: 1 | |||
}); | |||
this.stats_area_row = $(`<div class="row"></div>`); | |||
this.stats_area = new Section(this.parent, { | |||
title: __("Stats"), | |||
css_class: 'form-stats', | |||
hidden: 1, | |||
collapsible: 1, | |||
body_html: this.stats_area_row | |||
}); | |||
this.transactions_area = $(`<div class="transactions"></div`); | |||
this.links_area = new Section(this.parent, { | |||
title: __("Connections"), | |||
css_class: 'form-links', | |||
hidden: 1, | |||
collapsible: 1, | |||
body_html: this.transactions_area | |||
}); | |||
} | |||
reset() { | |||
this.hide(); | |||
this.clear_headline(); | |||
// clear progress | |||
this.progress_area.empty().addClass('hidden'); | |||
this.progress_area.body.empty(); | |||
this.progress_area.hide(); | |||
// clear links | |||
this.links_area.addClass('hidden'); | |||
this.links_area.find('.count, .open-notification').addClass('hidden'); | |||
this.links_area.body.find('.count, .open-notification').addClass('hidden'); | |||
this.links_area.hide(); | |||
// clear stats | |||
this.stats_area.addClass('hidden') | |||
this.stats_area_row.empty(); | |||
this.stats_area.hide(); | |||
// clear custom | |||
this.wrapper.find('.custom').remove(); | |||
this.parent.find('.custom').remove(); | |||
this.hide(); | |||
}, | |||
set_headline: function(html, color) { | |||
this.frm.layout.show_message(html, color); | |||
}, | |||
clear_headline: function() { | |||
this.frm.layout.show_message(); | |||
}, | |||
add_comment: function(text, alert_class, permanent) { | |||
var me = this; | |||
this.set_headline_alert(text, alert_class); | |||
if(!permanent) { | |||
setTimeout(function() { | |||
me.clear_headline(); | |||
}, 10000); | |||
} | |||
}, | |||
clear_comment: function() { | |||
this.clear_headline(); | |||
}, | |||
set_headline_alert: function(text, color) { | |||
if(text) { | |||
this.set_headline(`<div>${text}</div>`, color); | |||
} else { | |||
this.clear_headline(); | |||
} | |||
}, | |||
} | |||
add_section: function(html, section_head=null) { | |||
let section = $(`<div class="form-dashboard-section custom"></div>`); | |||
if (section_head) { | |||
section.append(`<div class="section-head">${section_head}</div>`); | |||
} | |||
section.append(html); | |||
section.appendTo(this.wrapper); | |||
return section; | |||
}, | |||
add_section(body_html, title=null, css_class="custom", hidden=false) { | |||
let options = { | |||
title, | |||
css_class, | |||
hidden, | |||
body_html, | |||
make_card: true, | |||
collapsible: 1 | |||
}; | |||
return new Section(this.parent, options).body; | |||
} | |||
add_progress: function(title, percent, message) { | |||
var progress_chart = this.make_progress_chart(title); | |||
add_progress(title, percent, message) { | |||
let progress_chart = this.make_progress_chart(title); | |||
if(!$.isArray(percent)) { | |||
if (!$.isArray(percent)) { | |||
percent = this.format_percent(title, percent); | |||
} | |||
var progress = $('<div class="progress"></div>').appendTo(progress_chart); | |||
let progress = $('<div class="progress"></div>').appendTo(progress_chart); | |||
$.each(percent, function(i, opts) { | |||
$(repl('<div class="progress-bar %(progress_class)s" style="width: %(width)s" \ | |||
title="%(title)s"></div>', opts)).appendTo(progress); | |||
$(`<div class="progress-bar ${opts.progress_class}" style="width: ${opts.width}" title="${opts.title}"></div>`).appendTo(progress); | |||
}); | |||
if (!message) message = ''; | |||
@@ -95,9 +104,9 @@ frappe.ui.form.Dashboard = Class.extend({ | |||
this.show(); | |||
return progress_chart; | |||
}, | |||
} | |||
show_progress: function(title, percent, message) { | |||
show_progress(title, percent, message) { | |||
this._progress_map = this._progress_map || {}; | |||
let progress_chart = this._progress_map[title]; | |||
// create a new progress chart if it doesnt exist | |||
@@ -119,19 +128,19 @@ frappe.ui.form.Dashboard = Class.extend({ | |||
if (!message) message = ''; | |||
progress_chart.find('.progress-message').text(message); | |||
}, | |||
} | |||
hide_progress: function(title) { | |||
if (title){ | |||
hide_progress(title) { | |||
if (title) { | |||
this._progress_map[title].remove(); | |||
delete this._progress_map[title]; | |||
} else { | |||
this._progress_map = {}; | |||
this.progress_area.empty(); | |||
this.progress_area.hide(); | |||
} | |||
}, | |||
} | |||
format_percent: function(title, percent) { | |||
format_percent(title, percent) { | |||
const percentage = cint(percent); | |||
const width = percentage < 0 ? 100 : percentage; | |||
const progress_class = percentage < 0 ? "progress-bar-danger" : "progress-bar-success"; | |||
@@ -141,28 +150,30 @@ frappe.ui.form.Dashboard = Class.extend({ | |||
width: width + '%', | |||
progress_class: progress_class | |||
}]; | |||
}, | |||
make_progress_chart: function(title) { | |||
} | |||
make_progress_chart(title) { | |||
this.progress_area.show(); | |||
var progress_chart = $('<div class="progress-chart" title="'+(title || '')+'"></div>') | |||
.appendTo(this.progress_area.removeClass('hidden')); | |||
.appendTo(this.progress_area.body); | |||
return progress_chart; | |||
}, | |||
} | |||
refresh: function() { | |||
refresh() { | |||
this.reset(); | |||
if(this.frm.doc.__islocal) { | |||
if (this.frm.doc.__islocal || !frappe.boot.desk_settings.dashboard) { | |||
return; | |||
} | |||
if(!this.data) { | |||
if (!this.data) { | |||
this.init_data(); | |||
} | |||
var show = false; | |||
if(this.data && ((this.data.transactions || []).length | |||
if (this.data && ((this.data.transactions || []).length | |||
|| (this.data.reports || []).length)) { | |||
if(this.data.docstatus && this.frm.doc.docstatus !== this.data.docstatus) { | |||
if (this.data.docstatus && this.frm.doc.docstatus !== this.data.docstatus) { | |||
// limited docstatus | |||
return; | |||
} | |||
@@ -171,53 +182,53 @@ frappe.ui.form.Dashboard = Class.extend({ | |||
show = true; | |||
} | |||
if(this.data.heatmap) { | |||
if (this.data.heatmap) { | |||
this.render_heatmap(); | |||
show = true; | |||
} | |||
if(this.data.graph) { | |||
if (this.data.graph) { | |||
this.setup_graph(); | |||
// show = true; | |||
} | |||
if(show) { | |||
if (show) { | |||
this.show(); | |||
} | |||
}, | |||
} | |||
after_refresh: function() { | |||
after_refresh() { | |||
var me = this; | |||
// show / hide new buttons (if allowed) | |||
this.links_area.find('.btn-new').each(function() { | |||
if(me.frm.can_create($(this).attr('data-doctype'))) { | |||
this.links_area.body.find('.btn-new').each(function() { | |||
if (me.frm.can_create($(this).attr('data-doctype'))) { | |||
$(this).removeClass('hidden'); | |||
} | |||
}); | |||
}, | |||
} | |||
init_data: function() { | |||
init_data() { | |||
this.data = this.frm.meta.__dashboard || {}; | |||
if(!this.data.transactions) this.data.transactions = []; | |||
if(!this.data.internal_links) this.data.internal_links = {}; | |||
if (!this.data.transactions) this.data.transactions = []; | |||
if (!this.data.internal_links) this.data.internal_links = {}; | |||
this.filter_permissions(); | |||
}, | |||
} | |||
add_transactions: function(opts) { | |||
add_transactions(opts) { | |||
// add additional data on dashboard | |||
let group_added = []; | |||
if(!Array.isArray(opts)) opts=[opts]; | |||
if (!Array.isArray(opts)) opts=[opts]; | |||
if(!this.data) { | |||
if (!this.data) { | |||
this.init_data(); | |||
} | |||
if(this.data && (this.data.transactions || []).length) { | |||
if (this.data && (this.data.transactions || []).length) { | |||
// check if label already exists, add items to it | |||
this.data.transactions.map(group => { | |||
opts.map(d => { | |||
if(d.label == group.label) { | |||
if (d.label == group.label) { | |||
group_added.push(d.label); | |||
group.items.push(...d.items); | |||
} | |||
@@ -226,80 +237,81 @@ frappe.ui.form.Dashboard = Class.extend({ | |||
// if label not already present, add new label and items under it | |||
opts.map(d => { | |||
if(!group_added.includes(d.label)) { | |||
if (!group_added.includes(d.label)) { | |||
this.data.transactions.push(d); | |||
} | |||
}); | |||
this.filter_permissions(); | |||
} | |||
}, | |||
} | |||
filter_permissions: function() { | |||
filter_permissions() { | |||
// filter out transactions for which the user | |||
// does not have permission | |||
var transactions = []; | |||
let transactions = []; | |||
(this.data.transactions || []).forEach(function(group) { | |||
var items = []; | |||
let items = []; | |||
group.items.forEach(function(doctype) { | |||
if(frappe.model.can_read(doctype)) { | |||
if (frappe.model.can_read(doctype)) { | |||
items.push(doctype); | |||
} | |||
}); | |||
// only add thie group, if there is atleast | |||
// only add this group, if there is at-least | |||
// one item with permission | |||
if(items.length) { | |||
if (items.length) { | |||
group.items = items; | |||
transactions.push(group); | |||
} | |||
}); | |||
this.data.transactions = transactions; | |||
}, | |||
render_links: function() { | |||
} | |||
render_links() { | |||
var me = this; | |||
this.links_area.removeClass('hidden'); | |||
this.links_area.find('.btn-new').addClass('hidden'); | |||
if(this.data_rendered) { | |||
this.links_area.show(); | |||
this.links_area.body.find('.btn-new').addClass('hidden'); | |||
if (this.data_rendered) { | |||
return; | |||
} | |||
//this.transactions_area.empty(); | |||
this.data.frm = this.frm; | |||
let transactions_area_body = this.transactions_area; | |||
$(frappe.render_template('form_links', this.data)) | |||
.appendTo(this.transactions_area) | |||
.appendTo(transactions_area_body); | |||
if (this.data.reports && this.data.reports.length) { | |||
$(frappe.render_template('report_links', this.data)) | |||
.appendTo(this.transactions_area) | |||
.appendTo(transactions_area_body); | |||
} | |||
// bind links | |||
this.transactions_area.find(".badge-link").on('click', function() { | |||
transactions_area_body.find(".badge-link").on('click', function() { | |||
me.open_document_list($(this).parent()); | |||
}); | |||
// bind reports | |||
this.transactions_area.find(".report-link").on('click', function() { | |||
transactions_area_body.find(".report-link").on('click', function() { | |||
me.open_report($(this).parent()); | |||
}); | |||
// bind open notifications | |||
this.transactions_area.find('.open-notification').on('click', function() { | |||
transactions_area_body.find('.open-notification').on('click', function() { | |||
me.open_document_list($(this).parent(), true); | |||
}); | |||
// bind new | |||
this.transactions_area.find('.btn-new').on('click', function() { | |||
transactions_area_body.find('.btn-new').on('click', function() { | |||
me.frm.make_new($(this).attr('data-doctype')); | |||
}); | |||
this.data_rendered = true; | |||
}, | |||
open_report: function($link) { | |||
} | |||
open_report($link) { | |||
let report = $link.attr('data-report'); | |||
let fieldname = this.data.non_standard_fieldnames | |||
@@ -308,28 +320,30 @@ frappe.ui.form.Dashboard = Class.extend({ | |||
frappe.route_options[fieldname] = this.frm.doc.name; | |||
frappe.set_route("query-report", report); | |||
}, | |||
open_document_list: function($link, show_open) { | |||
} | |||
open_document_list($link, show_open) { | |||
// show document list with filters | |||
var doctype = $link.attr('data-doctype'), | |||
names = $link.attr('data-names') || []; | |||
if(this.data.internal_links[doctype]) { | |||
if(names.length) { | |||
if (this.data.internal_links[doctype]) { | |||
if (names.length) { | |||
frappe.route_options = {'name': ['in', names]}; | |||
} else { | |||
return false; | |||
} | |||
} else if(this.data.fieldname) { | |||
} else if (this.data.fieldname) { | |||
frappe.route_options = this.get_document_filter(doctype); | |||
if(show_open) { | |||
if (show_open) { | |||
frappe.ui.notifications.show_open_count_list(doctype); | |||
} | |||
} | |||
frappe.set_route("List", doctype, "List"); | |||
}, | |||
get_document_filter: function(doctype) { | |||
} | |||
get_document_filter(doctype) { | |||
// return the default filter for the given document | |||
// like {"customer": frm.doc.name} | |||
var filter = {}; | |||
@@ -344,9 +358,10 @@ frappe.ui.form.Dashboard = Class.extend({ | |||
filter[fieldname] = this.frm.doc.name; | |||
return filter; | |||
}, | |||
set_open_count: function() { | |||
if(!this.data.transactions || !this.data.fieldname) { | |||
} | |||
set_open_count() { | |||
if (!this.data.transactions || !this.data.fieldname) { | |||
return; | |||
} | |||
@@ -355,7 +370,9 @@ frappe.ui.form.Dashboard = Class.extend({ | |||
me = this; | |||
this.data.transactions.forEach(function(group) { | |||
group.items.forEach(function(item) { items.push(item); }); | |||
group.items.forEach(function(item) { | |||
items.push(item); | |||
}); | |||
}); | |||
var method = this.data.method || 'frappe.desk.notifications.get_open_count'; | |||
@@ -368,7 +385,7 @@ frappe.ui.form.Dashboard = Class.extend({ | |||
items: items | |||
}, | |||
callback: function(r) { | |||
if(r.message.timeline_data) { | |||
if (r.message.timeline_data) { | |||
me.update_heatmap(r.message.timeline_data); | |||
} | |||
@@ -404,12 +421,13 @@ frappe.ui.form.Dashboard = Class.extend({ | |||
} | |||
}); | |||
}, | |||
set_badge_count: function(doctype, open_count, count, names) { | |||
} | |||
set_badge_count(doctype, open_count, count, names) { | |||
var $link = $(this.transactions_area) | |||
.find('.document-link[data-doctype="'+doctype+'"]'); | |||
if(open_count) { | |||
if (open_count) { | |||
$link.find('.open-notification') | |||
.removeClass('hidden') | |||
.html((open_count > 99) ? '99+' : open_count); | |||
@@ -421,24 +439,24 @@ frappe.ui.form.Dashboard = Class.extend({ | |||
.text((count > 99) ? '99+' : count); | |||
} | |||
if(this.data.internal_links[doctype]) { | |||
if(names && names.length) { | |||
if (this.data.internal_links[doctype]) { | |||
if (names && names.length) { | |||
$link.attr('data-names', names ? names.join(',') : ''); | |||
} else { | |||
$link.find('a').attr('disabled', true); | |||
} | |||
} | |||
}, | |||
} | |||
update_heatmap: function(data) { | |||
if(this.heatmap) { | |||
update_heatmap(data) { | |||
if (this.heatmap) { | |||
this.heatmap.update({dataPoints: data}); | |||
} | |||
}, | |||
} | |||
// heatmap | |||
render_heatmap: function() { | |||
if(!this.heatmap) { | |||
render_heatmap() { | |||
if (!this.heatmap) { | |||
this.heatmap = new frappe.Chart("#heatmap-" + frappe.model.scrub(this.frm.doctype), { | |||
type: 'heatmap', | |||
start: new Date(moment().subtract(1, 'year').toDate()), | |||
@@ -449,32 +467,36 @@ frappe.ui.form.Dashboard = Class.extend({ | |||
}); | |||
// center the heatmap | |||
this.heatmap_area.removeClass('hidden').find('svg').css({'margin': 'auto'}); | |||
this.heatmap_area.show(); | |||
this.heatmap_area.body.find('svg').css({'margin': 'auto'}); | |||
// message | |||
var heatmap_message = this.heatmap_area.find('.heatmap-message'); | |||
if(this.data.heatmap_message) { | |||
var heatmap_message = this.heatmap_area.body.find('.heatmap-message'); | |||
if (this.data.heatmap_message) { | |||
heatmap_message.removeClass('hidden').html(this.data.heatmap_message); | |||
} else { | |||
heatmap_message.addClass('hidden'); | |||
} | |||
} | |||
}, | |||
} | |||
add_indicator: function(label, color) { | |||
add_indicator(label, color) { | |||
this.show(); | |||
this.stats_area.removeClass('hidden'); | |||
this.stats_area.show(); | |||
// set colspan | |||
var indicators = this.stats_area_row.find('.indicator-column'); | |||
var n_indicators = indicators.length + 1; | |||
var colspan; | |||
if(n_indicators > 4) { colspan = 3 } | |||
else { colspan = 12 / n_indicators; } | |||
if (n_indicators > 4) { | |||
colspan = 3; | |||
} else { | |||
colspan = 12 / n_indicators; | |||
} | |||
// reset classes in existing indicators | |||
if(indicators.length) { | |||
if (indicators.length) { | |||
indicators.removeClass().addClass('col-sm-'+colspan).addClass('indicator-column'); | |||
} | |||
@@ -482,10 +504,10 @@ frappe.ui.form.Dashboard = Class.extend({ | |||
+label+'</span></div>').appendTo(this.stats_area_row); | |||
return indicator; | |||
}, | |||
} | |||
// graphs | |||
setup_graph: function() { | |||
setup_graph() { | |||
var me = this; | |||
var method = this.data.graph_method; | |||
var args = { | |||
@@ -500,7 +522,7 @@ frappe.ui.form.Dashboard = Class.extend({ | |||
args: args, | |||
callback: function(r) { | |||
if(r.message) { | |||
if (r.message) { | |||
me.render_graph(r.message); | |||
me.show(); | |||
} else { | |||
@@ -508,11 +530,11 @@ frappe.ui.form.Dashboard = Class.extend({ | |||
} | |||
} | |||
}); | |||
}, | |||
} | |||
render_graph: function(args) { | |||
var me = this; | |||
this.chart_area.empty().removeClass('hidden'); | |||
render_graph(args) { | |||
this.chart_area.show(); | |||
this.chart_area.body.empty(); | |||
$.extend(args, { | |||
type: 'line', | |||
colors: ['green'], | |||
@@ -524,21 +546,151 @@ frappe.ui.form.Dashboard = Class.extend({ | |||
this.show(); | |||
this.chart = new frappe.Chart('.form-graph', args); | |||
if(!this.chart) { | |||
if (!this.chart) { | |||
this.hide(); | |||
} | |||
}, | |||
} | |||
show: function() { | |||
show() { | |||
this.toggle_visibility(true); | |||
}, | |||
} | |||
hide: function() { | |||
hide() { | |||
this.toggle_visibility(false); | |||
}, | |||
} | |||
toggle_visibility(show) { | |||
this.section.toggleClass('visible-section', show); | |||
this.section.toggleClass('empty-section', !show); | |||
this.parent.toggleClass('visible-section', show); | |||
this.parent.toggleClass('empty-section', !show); | |||
} | |||
// TODO: Review! code related to headline should be the part of layout/form | |||
set_headline(html, color) { | |||
this.frm.layout.show_message(html, color); | |||
} | |||
clear_headline() { | |||
this.frm.layout.show_message(); | |||
} | |||
add_comment(text, alert_class, permanent) { | |||
var me = this; | |||
this.set_headline_alert(text, alert_class); | |||
if (!permanent) { | |||
setTimeout(function() { | |||
me.clear_headline(); | |||
}, 10000); | |||
} | |||
} | |||
clear_comment() { | |||
this.clear_headline(); | |||
} | |||
set_headline_alert(text, color) { | |||
if (text) { | |||
this.set_headline(`<div>${text}</div>`, color); | |||
} else { | |||
this.clear_headline(); | |||
} | |||
} | |||
}; | |||
class Section { | |||
constructor(parent, options) { | |||
this.parent = parent; | |||
this.df = options || {}; | |||
this.make(); | |||
if (this.df.title && this.df.collapsible) { | |||
this.collapse(); | |||
} | |||
this.refresh(); | |||
} | |||
make() { | |||
this.wrapper = $(`<div class="form-dashboard-section ${ this.df.make_card ? "card-section" : "" }">`) | |||
.appendTo(this.parent); | |||
if (this.df) { | |||
if (this.df.title) { | |||
this.make_head(); | |||
} | |||
if (this.df.description) { | |||
this.description_wrapper = $( | |||
`<div class="col-sm-12 form-section-description"> | |||
${__(this.df.description)} | |||
</div>` | |||
); | |||
this.wrapper.append(this.description_wrapper); | |||
} | |||
if (this.df.css_class) { | |||
this.wrapper.addClass(this.df.css_class); | |||
} | |||
if (this.df.hide_border) { | |||
this.wrapper.toggleClass("hide-border", true); | |||
} | |||
} | |||
this.body = $('<div class="section-body">').appendTo(this.wrapper); | |||
if (this.df.body_html) { | |||
this.body.append(this.df.body_html); | |||
} | |||
} | |||
make_head() { | |||
this.head = $(` | |||
<div class="section-head"> | |||
${__(this.df.title)} | |||
<span class="ml-2 collapse-indicator mb-1"></span> | |||
</div> | |||
`); | |||
this.head.appendTo(this.wrapper); | |||
this.indicator = this.head.find('.collapse-indicator'); | |||
this.indicator.hide(); | |||
if (this.df.collapsible) { | |||
// show / hide based on status | |||
this.collapse_link = this.head.on("click", () => { | |||
this.collapse(); | |||
}); | |||
this.indicator.show(); | |||
} | |||
} | |||
refresh() { | |||
if (!this.df) return; | |||
// hide if explicitly hidden | |||
let hide = this.df.hidden; | |||
this.wrapper.toggle(!hide); | |||
} | |||
collapse(hide) { | |||
if (hide === undefined) { | |||
hide = !this.body.hasClass("hide"); | |||
} | |||
this.body.toggleClass("hide", hide); | |||
this.head && this.head.toggleClass("collapsed", hide); | |||
let indicator_icon = hide ? 'down' : 'up-line'; | |||
this.indicator & this.indicator.html(frappe.utils.icon(indicator_icon, 'sm', 'mb-1')); | |||
} | |||
is_collapsed() { | |||
return this.body.hasClass('hide'); | |||
} | |||
hide() { | |||
this.wrapper.hide(); | |||
} | |||
show() { | |||
this.wrapper.show(); | |||
} | |||
}); | |||
} |
@@ -40,26 +40,40 @@ class BaseTimeline { | |||
this.timeline_items_wrapper.empty(); | |||
this.timeline_items = []; | |||
this.doc_info = this.frm && this.frm.get_docinfo() || {}; | |||
this.prepare_timeline_contents(); | |||
this.timeline_items.sort((item1, item2) => new Date(item1.creation) - new Date(item2.creation)); | |||
this.timeline_items.forEach(this.add_timeline_item.bind(this)); | |||
let response = this.prepare_timeline_contents(); | |||
if (response instanceof Promise) { | |||
response.then(() => { | |||
this.timeline_items.sort((item1, item2) => new Date(item2.creation) - new Date(item1.creation)); | |||
this.timeline_items.forEach(this.add_timeline_item.bind(this)); | |||
}); | |||
} else { | |||
this.timeline_items.sort((item1, item2) => new Date(item2.creation) - new Date(item1.creation)); | |||
this.timeline_items.forEach(this.add_timeline_item.bind(this)); | |||
} | |||
} | |||
prepare_timeline_contents() { | |||
// | |||
} | |||
add_timeline_item(item) { | |||
add_timeline_item(item, append_at_the_end=false) { | |||
let timeline_item = this.get_timeline_item(item); | |||
this.timeline_items_wrapper.prepend(timeline_item); | |||
if (append_at_the_end) { | |||
this.timeline_items_wrapper.append(timeline_item); | |||
} else { | |||
this.timeline_items_wrapper.prepend(timeline_item); | |||
} | |||
return timeline_item; | |||
} | |||
add_timeline_items(items, append_at_the_end=false) { | |||
items.forEach((item) => this.add_timeline_item(item, append_at_the_end)); | |||
} | |||
get_timeline_item(item) { | |||
// item can have content*, creation*, | |||
// timeline_badge, icon, icon_size, | |||
// hide_timestamp, card | |||
// hide_timestamp, is_card | |||
const timeline_item = $(`<div class="timeline-item">`); | |||
if (item.icon) { | |||
@@ -74,9 +88,9 @@ class BaseTimeline { | |||
timeline_item.append(`<div class="timeline-dot">`); | |||
} | |||
timeline_item.append(`<div class="timeline-content ${item.card ? 'frappe-card' : ''}">`); | |||
timeline_item.append(`<div class="timeline-content ${item.is_card ? 'frappe-card' : ''}">`); | |||
timeline_item.find('.timeline-content').append(item.content); | |||
if (!item.hide_timestamp && !item.card) { | |||
if (!item.hide_timestamp && !item.is_card) { | |||
timeline_item.find('.timeline-content').append(`<span> - ${comment_when(item.creation)}</span>`); | |||
} | |||
return timeline_item; | |||
@@ -48,7 +48,7 @@ frappe.ui.form.Footer = Class.extend({ | |||
}); | |||
}, | |||
get_names_for_mentions() { | |||
let names_for_mentions = Object.keys(frappe.boot.user_info) | |||
let names_for_mentions = Object.keys(frappe.boot.user_info || []) | |||
.filter(user => { | |||
return !["Administrator", "Guest"].includes(user) | |||
&& frappe.boot.user_info[user].allowed_in_mentions; | |||
@@ -47,7 +47,7 @@ class FormTimeline extends BaseTimeline { | |||
<div class="timeline-dot"></div> | |||
<div class="timeline-content flex align-center"> | |||
<h4>${__('Activity')}</h4> | |||
<nav class="nav nav-pills flex-column flex-sm-row"> | |||
<nav class="nav nav-pills flex-row"> | |||
<a class="flex-sm-fill text-sm-center nav-link" data-only-communication="true">${__('Communication')}</a> | |||
<a class="flex-sm-fill text-sm-center nav-link active">${__('All')}</a> | |||
</nav> | |||
@@ -72,7 +72,7 @@ class FormTimeline extends BaseTimeline { | |||
this.document_email_link_wrapper = $(` | |||
<div class="document-email-link-container"> | |||
<div class="timeline-dot"></div> | |||
<span>${message}</span> | |||
<span class="ellipsis">${message}</span> | |||
</div> | |||
`); | |||
this.timeline_wrapper.prepend(this.document_email_link_wrapper); | |||
@@ -130,7 +130,7 @@ class FormTimeline extends BaseTimeline { | |||
icon: 'mail', | |||
icon_size: 'sm', | |||
creation: communication.creation, | |||
card: true, | |||
is_card: true, | |||
content: this.get_communication_timeline_content(communication), | |||
}); | |||
}); | |||
@@ -151,7 +151,7 @@ class FormTimeline extends BaseTimeline { | |||
comment_timeline_contents.push({ | |||
icon: 'small-message', | |||
creation: comment.creation, | |||
card: true, | |||
is_card: true, | |||
content: this.get_comment_timeline_content(comment), | |||
}); | |||
}); | |||
@@ -248,7 +248,7 @@ class FormTimeline extends BaseTimeline { | |||
custom_timeline_contents.push({ | |||
icon: custom_item.icon, | |||
icon_size: 'sm', | |||
card: custom_item.show_card, | |||
is_card: custom_item.show_card, | |||
creation: custom_item.creation, | |||
content: custom_item.content || frappe.render_template(custom_item.template, custom_item.template_data), | |||
}); | |||
@@ -1,4 +1,5 @@ | |||
frappe.provide('frappe.ui.form'); | |||
frappe.provide('frappe.model.docinfo'); | |||
import './quick_entry'; | |||
import './toolbar'; | |||
@@ -23,13 +24,10 @@ frappe.ui.form.Form = class FrappeForm { | |||
this.docname = ''; | |||
this.doctype = doctype; | |||
this.doctype_layout_name = doctype_layout_name; | |||
if (doctype_layout_name) { | |||
this.doctype_layout = frappe.get_doc('DocType Layout', doctype_layout_name); | |||
} | |||
this.in_form = in_form ? true : false; | |||
this.hidden = false; | |||
this.refresh_if_stale_for = 120; | |||
var me = this; | |||
this.opendocs = {}; | |||
this.custom_buttons = {}; | |||
this.sections = []; | |||
@@ -39,17 +37,8 @@ frappe.ui.form.Form = class FrappeForm { | |||
this.pformat = {}; | |||
this.fetch_dict = {}; | |||
this.parent = parent; | |||
this.doctype_layout = frappe.get_doc('DocType Layout', doctype_layout_name); | |||
this.setup_meta(doctype); | |||
// show in form instead of in dialog, when called using url (router.js) | |||
this.in_form = in_form ? true : false; | |||
// notify on rename | |||
$(document).on('rename', function(event, dt, old_name, new_name) { | |||
if(dt==me.doctype) | |||
me.rename_notify(dt, old_name, new_name); | |||
}); | |||
} | |||
setup_meta() { | |||
@@ -107,7 +96,7 @@ frappe.ui.form.Form = class FrappeForm { | |||
this.script_manager.setup(); | |||
this.watch_model_updates(); | |||
if(!this.meta.hide_toolbar) { | |||
if(!this.meta.hide_toolbar && frappe.boot.desk_settings.timeline) { | |||
this.footer = new frappe.ui.form.Footer({ | |||
frm: this, | |||
parent: $('<div>').appendTo(this.page.main.parent()) | |||
@@ -117,6 +106,7 @@ frappe.ui.form.Form = class FrappeForm { | |||
this.setup_file_drop(); | |||
this.setup_doctype_actions(); | |||
this.setup_docinfo_change_listener(); | |||
this.setup_notify_on_rename(); | |||
this.setup_done = true; | |||
} | |||
@@ -175,6 +165,7 @@ frappe.ui.form.Form = class FrappeForm { | |||
this.dashboard = new frappe.ui.form.Dashboard({ | |||
frm: this, | |||
parent: $('<div class="form-dashboard">').insertAfter(this.layout.wrapper.find('.form-message')) | |||
}); | |||
// workflow state | |||
@@ -222,6 +213,13 @@ frappe.ui.form.Form = class FrappeForm { | |||
}); | |||
} | |||
setup_notify_on_rename() { | |||
$(document).on('rename', (ev, dt, old_name, new_name) => { | |||
if(dt==this.doctype) | |||
this.rename_notify(dt, old_name, new_name); | |||
}); | |||
} | |||
setup_file_drop() { | |||
var me = this; | |||
this.$wrapper.on('dragenter dragover', false) | |||
@@ -445,11 +443,13 @@ frappe.ui.form.Form = class FrappeForm { | |||
this.layout.doc = this.doc; | |||
this.layout.attach_doc_and_docfields(); | |||
this.sidebar = new frappe.ui.form.Sidebar({ | |||
frm: this, | |||
page: this.page | |||
}); | |||
this.sidebar.make(); | |||
if (frappe.boot.desk_settings.form_sidebar) { | |||
this.sidebar = new frappe.ui.form.Sidebar({ | |||
frm: this, | |||
page: this.page | |||
}); | |||
this.sidebar.make(); | |||
} | |||
// clear layout message | |||
this.layout.show_message(); | |||
@@ -560,6 +560,7 @@ frappe.ui.form.Form = class FrappeForm { | |||
} | |||
this.dashboard.refresh(); | |||
frappe.breadcrumbs.update(); | |||
this.show_submit_message(); | |||
this.clear_custom_buttons(); | |||
@@ -1665,7 +1666,7 @@ frappe.ui.form.Form = class FrappeForm { | |||
}); | |||
driver.defineSteps(steps); | |||
frappe.route.on('change', () => driver.reset()); | |||
frappe.router.on('change', () => driver.reset()); | |||
driver.start(); | |||
} | |||
@@ -0,0 +1,40 @@ | |||
frappe.ui.form.FormViewers = class FormViewers { | |||
constructor({ frm, parent }) { | |||
this.frm = frm; | |||
this.parent = parent; | |||
this.parent.tooltip({ title: __('Currently Viewing') }); | |||
} | |||
refresh() { | |||
let users = this.frm.get_docinfo()['viewers']; | |||
let currently_viewing = users.current.filter(user => user != frappe.session.user); | |||
let avatar_group = frappe.avatar_group(currently_viewing, 5, {'align': 'left', 'overlap': true}); | |||
this.parent.empty().append(avatar_group); | |||
} | |||
}; | |||
frappe.ui.form.FormViewers.set_users = function(data, type) { | |||
const doctype = data.doctype; | |||
const docname = data.docname; | |||
const docinfo = frappe.model.get_docinfo(doctype, docname); | |||
const past_users = ((docinfo && docinfo[type]) || {}).past || []; | |||
const users = data.users || []; | |||
const new_users = users.filter(user => !past_users.includes(user)); | |||
frappe.model.set_docinfo(doctype, docname, type, { | |||
past: past_users.concat(new_users), | |||
new: new_users, | |||
current: users | |||
}); | |||
if ( | |||
cur_frm && | |||
cur_frm.doc && | |||
cur_frm.doc.doctype === doctype && | |||
cur_frm.doc.name == docname && | |||
cur_frm.viewers | |||
) { | |||
cur_frm.viewers.refresh(true, type); | |||
} | |||
}; |
@@ -121,7 +121,7 @@ frappe.form.formatters = { | |||
{onclick: docfield.link_onclick.replace(/"/g, '"'), value:value}); | |||
} else if(docfield && doctype) { | |||
return `<a | |||
href="/app/form/${encodeURIComponent(frappe.router.slug(doctype))}/${encodeURIComponent(original_value)}" | |||
href="/app/${encodeURIComponent(frappe.router.slug(doctype))}/${encodeURIComponent(original_value)}" | |||
data-doctype="${doctype}" | |||
data-name="${original_value}"> | |||
${__(options && options.label || value)}</a>` | |||
@@ -127,14 +127,6 @@ frappe.ui.form.Layout = Class.extend({ | |||
if (this.no_opening_section()) { | |||
this.fields.unshift({fieldtype: 'Section Break'}); | |||
} | |||
this.fields.unshift({ | |||
fieldtype: 'Section Break', | |||
fieldname: '_form_dashboard', | |||
cssClass: 'form-dashboard', | |||
collapsible: 1, | |||
// hidden: 1 | |||
}); | |||
}, | |||
replace_field: function(fieldname, df, render) { | |||
@@ -312,10 +304,6 @@ frappe.ui.form.Layout = Class.extend({ | |||
collapse = false; | |||
} | |||
if (df.fieldname === '_form_dashboard') { | |||
collapse = localStorage.getItem('collapseFormDashboard')==='yes' ? true : false; | |||
} | |||
section.collapse(collapse); | |||
} | |||
} | |||
@@ -587,17 +575,13 @@ frappe.ui.form.Section = Class.extend({ | |||
wrapper: this.wrapper | |||
}; | |||
if (this.df.collapsible && this.df.fieldname !== '_form_dashboard') { | |||
this.collapse(true); | |||
} | |||
this.refresh(); | |||
}, | |||
make: function() { | |||
if (!this.layout.page) { | |||
this.layout.page = $('<div class="form-page"></div>').appendTo(this.layout.wrapper); | |||
} | |||
let make_card = this.layout.card_layout && this.df.fieldname !== '_form_dashboard'; | |||
let make_card = this.layout.card_layout; | |||
this.wrapper = $(`<div class="row form-section ${ make_card ? "card-section" : "" }">`) | |||
.appendTo(this.layout.page); | |||
this.layout.sections.push(this); | |||
@@ -664,18 +648,12 @@ frappe.ui.form.Section = Class.extend({ | |||
hide = !this.body.hasClass("hide"); | |||
} | |||
if (this.df.fieldname==='_form_dashboard') { | |||
localStorage.setItem('collapseFormDashboard', hide ? 'yes' : 'no'); | |||
} | |||
this.body.toggleClass("hide", hide); | |||
this.head.toggleClass("collapsed", hide); | |||
let indicator_icon = hide ? 'down' : 'up-line'; | |||
this.indicator & this.indicator.html(frappe.utils.icon(indicator_icon, 'sm', 'mb-1')); | |||
// this.indicator && this.indicator.toggleClass("octicon-chevron-down", hide); | |||
// this.indicator && this.indicator.toggleClass("octicon-chevron-up", !hide); | |||
// refresh signature fields | |||
this.fields_list.forEach((f) => { | |||
@@ -173,11 +173,8 @@ frappe.ui.form.save = function (frm, action, callback, btn) { | |||
return !has_errors; | |||
}; | |||
var scroll_to = function (fieldname) { | |||
var f = cur_frm.fields_dict[fieldname]; | |||
if (f) { | |||
$(document).scrollTop($(f.wrapper).offset().top - 60); | |||
} | |||
const scroll_to = (fieldname) => { | |||
frm.scroll_to_field(fieldname); | |||
frm.scroll_set = true; | |||
}; | |||
@@ -6,7 +6,7 @@ | |||
frappe.ui.form.AssignTo = Class.extend({ | |||
init: function(opts) { | |||
$.extend(this, opts); | |||
this.btn = this.parent.find(".add-assignment-btn > button").on("click", () => this.add()); | |||
this.btn = this.parent.find(".add-assignment-btn").on("click", () => this.add()); | |||
this.btn_wrapper = this.btn.parent(); | |||
this.refresh(); | |||
@@ -21,51 +21,38 @@ frappe.ui.form.AssignTo = Class.extend({ | |||
}, | |||
render: function(assignments) { | |||
this.frm.get_docinfo().assignments = assignments; | |||
this.parent.find(".assignment-row").remove(); | |||
if (this.primary_action) { | |||
this.primary_action.remove(); | |||
this.primary_action = null; | |||
} | |||
let assignments_wrapper = this.parent.find('.assignments'); | |||
assignments_wrapper.empty(); | |||
let assigned_users = assignments.map(d => d.owner); | |||
if (this.dialog) { | |||
this.dialog.hide(); | |||
if (!assigned_users.length) { | |||
assignments_wrapper.hide(); | |||
return; | |||
} | |||
let add_assignment_button = this.parent.find('.add-assignment-btn'); | |||
assignments.forEach(assignment => { | |||
let user_info = frappe.user_info(assignment.owner); | |||
user_info.assign_to_name = assignment.name; | |||
user_info.owner = assignment.owner; | |||
user_info.avatar = frappe.avatar(assignment.owner); | |||
user_info.description = assignment.description || ""; | |||
let avatar_group = frappe.avatar_group(assigned_users, 5, {'align': 'left', 'overlap': true}); | |||
this.get_assignment_block(user_info).insertBefore(add_assignment_button); | |||
if (assignment.owner === frappe.session.user) { | |||
this.primary_action = this.frm.page.add_menu_item(__("Assignment Complete"), () => { | |||
this.remove(frappe.session.user); | |||
}, "fa fa-check", "btn-success"); | |||
} | |||
assignments_wrapper.show(); | |||
assignments_wrapper.append(avatar_group); | |||
avatar_group.click(() => { | |||
new frappe.ui.form.AssignmentDialog({ | |||
assignments: assigned_users, | |||
frm: this.frm, | |||
remove_action: this.remove.bind(this) | |||
}); | |||
}); | |||
}, | |||
get_assignment_block(assignee_info) { | |||
let remove_action = false; | |||
if (assignee_info.owner === frappe.session.user || this.frm.perm[0].write) { | |||
remove_action = this.remove.bind(this); | |||
} | |||
return $(`<li class="assignment-row">`) | |||
.append(frappe.get_data_pill(assignee_info.owner, assignee_info.owner, remove_action)); | |||
}, | |||
add: function() { | |||
var me = this; | |||
if(this.frm.is_new()) { | |||
if (this.frm.is_new()) { | |||
frappe.throw(__("Please save the document before assignment")); | |||
return; | |||
} | |||
if(!me.assign_to) { | |||
if (!me.assign_to) { | |||
me.assign_to = new frappe.ui.form.AssignToDialog({ | |||
method: "frappe.desk.form.assign_to.add", | |||
doctype: me.frm.doctype, | |||
@@ -80,23 +67,17 @@ frappe.ui.form.AssignTo = Class.extend({ | |||
me.assign_to.dialog.show(); | |||
}, | |||
remove: function(owner) { | |||
var me = this; | |||
if(this.frm.is_new()) { | |||
if (this.frm.is_new()) { | |||
frappe.throw(__("Please save the document before removing assignment")); | |||
return; | |||
} | |||
frappe.call({ | |||
method:'frappe.desk.form.assign_to.remove', | |||
args: { | |||
doctype: me.frm.doctype, | |||
name: me.frm.docname, | |||
assign_to: owner | |||
}, | |||
callback:function(r,rt) { | |||
me.render(r.message); | |||
} | |||
return frappe.xcall('frappe.desk.form.assign_to.remove', { | |||
doctype: this.frm.doctype, | |||
name: this.frm.docname, | |||
assign_to: owner | |||
}).then((assignments) => { | |||
this.render(assignments); | |||
}); | |||
} | |||
}); | |||
@@ -231,33 +212,24 @@ frappe.ui.form.AssignToDialog = Class.extend({ | |||
frappe.ui.form.AssignmentDialog = class { | |||
constructor(opts) { | |||
// this.frm = opts.frm; | |||
this.frm = opts.frm; | |||
this.assignments = opts.assignments; | |||
this.remove_action = opts.remove_action; | |||
this.make(); | |||
} | |||
make() { | |||
this.dialog = new frappe.ui.Dialog({ | |||
title: __('Assign Users'), | |||
title: __('Assigned To'), | |||
size: 'small', | |||
fields: [{ | |||
'fieldtype': 'Link', | |||
'fieldname': 'selected_user', | |||
'options': 'User', | |||
'label': 'User', | |||
'change': () => { | |||
let user = this.dialog.get_value('selected_user'); | |||
if (user && user !== '') { | |||
this.update_assignment(user); | |||
this.dialog.set_value('selected_user', null); | |||
} | |||
} | |||
}, { | |||
'fieldtype': 'HTML', | |||
'fieldname': 'assignment_list' | |||
}] | |||
}); | |||
this.assignment_list = $(this.dialog.get_field('assignment_list').wrapper); | |||
this.assignment_list.removeClass('frappe-control'); | |||
this.assignments.forEach(assignment => { | |||
this.update_assignment(assignment); | |||
@@ -274,14 +246,20 @@ frappe.ui.form.AssignmentDialog = class { | |||
${frappe.avatar(assignment)} | |||
${frappe.user.full_name(assignment)} | |||
</span> | |||
<span class="remove-btn"> | |||
${frappe.utils.icon('close')} | |||
</span> | |||
</div> | |||
`); | |||
row.find('.remove-btn').click(() => { | |||
row.remove(); | |||
}); | |||
if (assignment === frappe.session.user || this.frm.perm[0].write) { | |||
row.append(` | |||
<span class="remove-btn cursor-pointer"> | |||
${frappe.utils.icon('close')} | |||
</span> | |||
`); | |||
row.find('.remove-btn').click(() => { | |||
this.remove_action && this.remove_action(assignment); | |||
row.remove(); | |||
}); | |||
} | |||
return row; | |||
} | |||
}; |
@@ -84,8 +84,25 @@ frappe.ui.form.Attachments = Class.extend({ | |||
}; | |||
} | |||
let icon; | |||
// REDESIGN-TODO: set icon using frappe.utils.icon | |||
if (attachment.is_private) { | |||
icon = `<div><svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> | |||
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.07685 1.45015H8.02155C7.13255 1.44199 6.2766 1.78689 5.64159 2.40919C5.00596 3.0321 4.64377 3.88196 4.63464 4.77188L4.63462 4.77188V4.77701V5.12157H3.75C2.64543 5.12157 1.75 6.017 1.75 7.12157V12.5132C1.75 13.6177 2.64543 14.5132 3.75 14.5132H12.2885C13.393 14.5132 14.2885 13.6177 14.2885 12.5132V7.12157C14.2885 6.017 13.393 5.12157 12.2885 5.12157H11.4037V4.83708C11.4119 3.94809 11.067 3.09213 10.4447 2.45713C9.82175 1.8215 8.97189 1.4593 8.08198 1.45018L8.08198 1.45015H8.07685ZM10.4037 5.12157V4.8347V4.82972L10.4037 4.82972C10.4099 4.20495 10.1678 3.60329 9.73045 3.15705C9.29371 2.7114 8.69805 2.4572 8.07417 2.45015H8.01916H8.01418L8.01419 2.45013C7.38942 2.44391 6.78776 2.68609 6.34152 3.12341C5.89586 3.56015 5.64166 4.15581 5.63462 4.77969V5.12157H10.4037ZM3.75 6.12157C3.19772 6.12157 2.75 6.56929 2.75 7.12157V12.5132C2.75 13.0655 3.19772 13.5132 3.75 13.5132H12.2885C12.8407 13.5132 13.2885 13.0655 13.2885 12.5132V7.12157C13.2885 6.56929 12.8407 6.12157 12.2885 6.12157H3.75ZM8.01936 10.3908C8.33605 10.3908 8.59279 10.134 8.59279 9.81734C8.59279 9.50064 8.33605 9.24391 8.01936 9.24391C7.70266 9.24391 7.44593 9.50064 7.44593 9.81734C7.44593 10.134 7.70266 10.3908 8.01936 10.3908ZM9.59279 9.81734C9.59279 10.6863 8.88834 11.3908 8.01936 11.3908C7.15038 11.3908 6.44593 10.6863 6.44593 9.81734C6.44593 8.94836 7.15038 8.24391 8.01936 8.24391C8.88834 8.24391 9.59279 8.94836 9.59279 9.81734Z" fill="currentColor"/> | |||
</svg></div>`; | |||
} else { | |||
icon = `<div><svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> | |||
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.07685 1.45014H8.02155C7.13255 1.44198 6.2766 1.78687 5.64159 2.40918C5.00596 3.03209 4.64377 3.88195 4.63464 4.77187L4.63462 4.77187V4.777V5.12156H3.75C2.64543 5.12156 1.75 6.01699 1.75 7.12156V12.5132C1.75 13.6177 2.64543 14.5132 3.75 14.5132H12.2885C13.393 14.5132 14.2885 13.6177 14.2885 12.5132V7.12156C14.2885 6.01699 13.393 5.12156 12.2885 5.12156H5.63462V4.77968C5.64166 4.1558 5.89586 3.56014 6.34152 3.12339C6.78776 2.68608 7.38942 2.4439 8.01419 2.45012L8.01418 2.45014H8.01916H8.07417C8.69805 2.45718 9.29371 2.71138 9.73045 3.15704C9.92373 3.35427 10.2403 3.35746 10.4375 3.16418C10.6347 2.9709 10.6379 2.65434 10.4447 2.45711C9.82175 1.82149 8.97189 1.45929 8.08198 1.45017L8.08198 1.45014H8.07685ZM3.75 6.12156C3.19772 6.12156 2.75 6.56927 2.75 7.12156V12.5132C2.75 13.0655 3.19772 13.5132 3.75 13.5132H12.2885C12.8407 13.5132 13.2885 13.0655 13.2885 12.5132V7.12156C13.2885 6.56927 12.8407 6.12156 12.2885 6.12156H3.75ZM8.01936 10.3908C8.33605 10.3908 8.59279 10.134 8.59279 9.81732C8.59279 9.50063 8.33605 9.2439 8.01936 9.2439C7.70266 9.2439 7.44593 9.50063 7.44593 9.81732C7.44593 10.134 7.70266 10.3908 8.01936 10.3908ZM9.59279 9.81732C9.59279 10.6863 8.88834 11.3908 8.01936 11.3908C7.15038 11.3908 6.44593 10.6863 6.44593 9.81732C6.44593 8.94835 7.15038 8.2439 8.01936 8.2439C8.88834 8.2439 9.59279 8.94835 9.59279 9.81732Z" fill="currentColor"/> | |||
</svg></div>`; | |||
} | |||
$(`<li class="attachment-row">`) | |||
.append(frappe.get_data_pill(file_label, fileid, remove_action)) | |||
.append(frappe.get_data_pill( | |||
file_label, | |||
fileid, | |||
remove_action, | |||
icon | |||
)) | |||
.insertAfter(this.attachments_label.addClass("has-attachments")); | |||
}, | |||
@@ -65,28 +65,6 @@ frappe.ui.form.SidebarUsers = class { | |||
} else { | |||
frappe.show_alert(__('{0} are currently {1}', [frappe.utils.comma_and(users), message])); | |||
} | |||
} | |||
} | |||
}; | |||
frappe.ui.form.set_users = function(data, type) { | |||
const doctype = data.doctype; | |||
const docname = data.docname; | |||
const docinfo = frappe.model.get_docinfo(doctype, docname); | |||
const past_users = ((docinfo && docinfo[type]) || {}).past || []; | |||
const users = data.users || []; | |||
const new_users = users.filter(user => !past_users.includes(user)); | |||
frappe.model.set_docinfo(doctype, docname, type, { | |||
past: past_users.concat(new_users), | |||
new: new_users, | |||
current: users | |||
}); | |||
if (cur_frm && cur_frm.doc && cur_frm.doc.doctype===doctype | |||
&& cur_frm.doc.name==docname && cur_frm.viewers) { | |||
cur_frm.viewers.refresh(true, type); | |||
} | |||
}; |
@@ -132,10 +132,11 @@ frappe.ui.form.Review = class Review { | |||
this.reviews.find('.review').remove(); | |||
review_logs.forEach(log => { | |||
let review_pill = $(` | |||
<div class="review ${log.points < 0 ? 'criticism' : 'appreciation'}"> | |||
<div> | |||
${Math.abs(log.points)} | |||
</div> | |||
<div class="review ${log.points < 0 ? 'criticism' : 'appreciation'} cursor-pointer"> | |||
${frappe.avatar(log.owner)} | |||
<span class="review-points"> | |||
${log.points > 0 ? '+': ''}${log.points} | |||
</span> | |||
</div> | |||
`); | |||
this.reviews.prepend(review_pill); | |||
@@ -12,12 +12,9 @@ frappe.ui.form.Share = Class.extend({ | |||
this.render_sidebar(); | |||
}, | |||
render_sidebar: function() { | |||
this.shares.empty(); | |||
const shared = this.shared || this.frm.get_docinfo().shared; | |||
const shared_users = shared.filter(Boolean).map(s => s.user); | |||
// REDESIGN-TODO: handle "shared with everyone" | |||
this.shares.append(frappe.avatar_group(shared_users, 5, {'align': 'left', 'overlap': true})); | |||
if (this.frm.is_new()) { | |||
this.parent.find(".share-doc-btn").hide(); | |||
} | |||
@@ -25,6 +22,17 @@ frappe.ui.form.Share = Class.extend({ | |||
this.parent.find(".share-doc-btn").on("click", () => { | |||
this.frm.share_doc(); | |||
}); | |||
this.shares.empty(); | |||
if (!shared_users.length) { | |||
this.shares.hide(); | |||
return; | |||
} | |||
this.shares.show(); | |||
// REDESIGN-TODO: handle "shared with everyone" | |||
this.shares.append(frappe.avatar_group(shared_users, 5, {'align': 'left', 'overlap': true})); | |||
}, | |||
show: function() { | |||
var me = this; | |||
@@ -1,10 +1,3 @@ | |||
<ul class="list-unstyled sidebar-menu visible-sm visible-xs"> | |||
<li> | |||
<a class="navbar-home" href="#"> | |||
<img class="app-logo" src="{{ frappe.app.logo_url }}"> | |||
</a> | |||
</li> | |||
</ul> | |||
<ul class="list-unstyled sidebar-menu user-actions hidden"></ul> | |||
<ul class="list-unstyled sidebar-menu sidebar-image-section hidden-xs hidden-sm hide"> | |||
<li class="sidebar-image-wrapper"> | |||
@@ -25,7 +18,7 @@ | |||
</ul> | |||
{% if frm.meta.beta %} | |||
<div class="sidebar-menu"> | |||
<p><label class="indicator-pill yellow" title="{{ __("This feature is brand new and still experimental") }}">{{ __("Under Development") }}</label></p> | |||
<p><label class="indicator-pill yellow" title="{{ __("This feature is brand new and still experimental") }}">{{ __("Experimental") }}</label></p> | |||
<p><a class="small text-muted" href="https://github.com/frappe/{{ frappe.boot.module_app[frappe.scrub(frm.meta.module)] }}/issues/new" | |||
target="_blank"> | |||
{{ __("Click here to post bugs and suggestions") }}</a></p> | |||
@@ -50,14 +43,10 @@ | |||
<svg class="icon icon-sm"><use href="#icon-assign"></use></svg> | |||
{%= __("Assigned To") %} | |||
</li> | |||
<li class="add-assignment-btn"> | |||
<button class="data-pill btn"> | |||
<span class="pill-label ellipsis"> | |||
{%= __("Assign to someone") %} | |||
</span> | |||
<svg class="icon icon-sm"> | |||
<use href="#icon-add"></use> | |||
</svg> | |||
<li class="flex flex-wrap"> | |||
<span class="assignments"></span> | |||
<button class="text-muted btn btn-default icon-btn add-assignment-btn"> | |||
<svg class="icon icon-sm"><use href="#icon-add"></use></svg> | |||
</button> | |||
</li> | |||
</ul> | |||
@@ -169,7 +158,3 @@ | |||
{% if(frappe.get_form_sidebar_extension) { %} | |||
{{ frappe.get_form_sidebar_extension() }} | |||
{% } %} | |||
<ul class="list-unstyled visible-xs visible-sm"> | |||
<li class="close-sidebar">Close</li> | |||
</ul> |
@@ -1,54 +1,59 @@ | |||
<div> | |||
<div class="row"> | |||
<div class="col-xs-6"><h6>{%= __("User") %}</h6></div> | |||
<div class="col-xs-2"><h6>{%= __("Can Read") %}</h6></div> | |||
<div class="col-xs-2"><h6>{%= __("Can Write") %}</h6></div> | |||
<div class="col-xs-2"><h6>{%= __("Can Share") %}</h6></div> | |||
</div> | |||
<div class="row"> | |||
<div class="col-xs-6"><h6>{%= __("User") %}</h6></div> | |||
<div class="col-xs-2 flex justify-center align-center"><h6>{%= __("Can Read") %}</h6></div> | |||
<div class="col-xs-2 flex justify-center align-center"><h6>{%= __("Can Write") %}</h6></div> | |||
<div class="col-xs-2 flex justify-center align-center"><h6>{%= __("Can Share") %}</h6></div> | |||
</div> | |||
<div class="row shared-user" data-everyone=1> | |||
<div class="col-xs-6 share-all"><b>{{ __("Everyone") }}</b></div> | |||
<div class="col-xs-2"><input type="checkbox" name="read" | |||
{% if(cint(everyone.read)) { %}checked{% } %} class="edit-share"></div> | |||
<div class="col-xs-2"><input type="checkbox" name="write" | |||
{% if(cint(everyone.write)) { %}checked{% } %} class="edit-share"{% if (!frm.perm[0].write){ %} disabled="disabled"{% } %}></div> | |||
<div class="col-xs-2"><input type="checkbox" name="share" | |||
{% if(cint(everyone.share)) { %}checked{% } %} class="edit-share"></div> | |||
<div class="col-xs-6 share-all"><b>{{ __("Everyone") }}</b></div> | |||
<div class="col-xs-2 flex justify-center align-center"><input type="checkbox" name="read" | |||
{% if(cint(everyone.read)) { %}checked{% } %} class="edit-share"></div> | |||
<div class="col-xs-2 flex justify-center align-center"><input type="checkbox" name="write" | |||
class="edit-share" | |||
{% if(cint(everyone.write)) { %}checked{% } %} | |||
{% if (!frm.perm[0].write){ %} disabled="disabled"{% } %}> | |||
</div> | |||
<div class="col-xs-2 flex justify-center align-center"><input type="checkbox" name="share" | |||
{% if(cint(everyone.share)) { %}checked{% } %} class="edit-share"></div> | |||
</div> | |||
{% for (var i=0, l=shared.length; i < l; i++) { | |||
var s = shared[i]; %} | |||
{% if(s && !s.everyone) { %} | |||
<div class="row shared-user" data-user="{%= s.user %}" data-name="{%= s.name %}"> | |||
<div class="col-xs-6">{%= s.user %}</div> | |||
<div class="col-xs-2"><input type="checkbox" name="read" | |||
{% if(cint(s.read)) { %}checked{% } %} class="edit-share"></div> | |||
<div class="col-xs-2"><input type="checkbox" name="write" | |||
{% if(cint(s.write)) { %}checked{% } %} class="edit-share"{% if (!frm.perm[0].write){ %} disabled="disabled"{% } %}></div> | |||
<div class="col-xs-2"><input type="checkbox" name="share" | |||
{% if(cint(s.share)) { %}checked{% } %} class="edit-share"></div> | |||
</div> | |||
{% } %} | |||
{% } %} | |||
{% for (var i=0, l=shared.length; i < l; i++) { | |||
var s = shared[i]; %} | |||
{% if(s && !s.everyone) { %} | |||
<div class="row shared-user" data-user="{%= s.user %}" data-name="{%= s.name %}"> | |||
<div class="col-xs-6">{%= s.user %}</div> | |||
<div class="col-xs-2 flex justify-center align-center"><input type="checkbox" name="read" | |||
{% if(cint(s.read)) { %}checked{% } %} class="edit-share"></div> | |||
<div class="col-xs-2 flex justify-center align-center"><input type="checkbox" name="write" | |||
{% if(cint(s.write)) { %}checked{% } %} class="edit-share"{% if (!frm.perm[0].write){ %} disabled="disabled"{% } %}></div> | |||
<div class="col-xs-2 flex justify-center align-center"><input type="checkbox" name="share" | |||
{% if(cint(s.share)) { %}checked{% } %} class="edit-share"></div> | |||
</div> | |||
{% } %} | |||
{% } %} | |||
{% if(frappe.model.can_share(null, frm)) { %} | |||
<hr> | |||
{% if(frappe.model.can_share(null, frm)) { %} | |||
<hr> | |||
<div class="row"> | |||
<div class="col-xs-6"><h6>{%= __("Share this document with") %}</h6></div> | |||
<div class="col-xs-2"><h6>{%= __("Can Read") %}</h6></div> | |||
<div class="col-xs-2"><h6>{%= __("Can Write") %}</h6></div> | |||
<div class="col-xs-2"><h6>{%= __("Can Share") %}</h6></div> | |||
</div> | |||
<div class="row"> | |||
<div class="col-xs-6"><h6>{%= __("Share this document with") %}</h6></div> | |||
<div class="col-xs-2 flex justify-center align-center"><h6>{%= __("Can Read") %}</h6></div> | |||
<div class="col-xs-2 flex justify-center align-center"><h6>{%= __("Can Write") %}</h6></div> | |||
<div class="col-xs-2 flex justify-center align-center"><h6>{%= __("Can Share") %}</h6></div> | |||
</div> | |||
<div class="row"> | |||
<div class="col-xs-6 input-wrapper-add-share"></div> | |||
<div class="col-xs-2"><input type="checkbox" class="add-share-read" name="read"></div> | |||
<div class="col-xs-2"><input type="checkbox" class="add-share-write" name="write" {% if (!frm.perm[0].write){ %} disabled="disabled"{% } %}></div> | |||
<div class="col-xs-2"><input type="checkbox" class="add-share-share" name="share"></div> | |||
</div> | |||
<div> | |||
<button class="btn btn-primary btn-sm btn-add-share">{{ __("Add") }}</button> | |||
</div> | |||
<div class="row"> | |||
<div class="col-xs-6 input-wrapper-add-share"></div> | |||
<div class="col-xs-2 flex justify-center align-flex-start mt-2"><input type="checkbox" class="add-share-read" name="read"></div> | |||
<div class="col-xs-2 flex justify-center align-flex-start mt-2"><input type="checkbox" class="add-share-write" name="write" | |||
{% if (!frm.perm[0].write){ %} disabled="disabled"{% } %}> | |||
</div> | |||
<div class="col-xs-2 flex justify-center align-flex-start mt-2"><input type="checkbox" class="add-share-share" name="share"></div> | |||
</div> | |||
<div> | |||
<button class="btn btn-primary btn-sm btn-add-share">{{ __("Add") }}</button> | |||
</div> | |||
{% endif %} | |||
</div> |
@@ -1,16 +1,18 @@ | |||
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors | |||
// MIT License. See license.txt | |||
import './linked_with'; | |||
import './form_viewers'; | |||
frappe.ui.form.Toolbar = Class.extend({ | |||
init: function(opts) { | |||
frappe.ui.form.Toolbar = class Toolbar { | |||
constructor(opts) { | |||
$.extend(this, opts); | |||
this.refresh(); | |||
this.add_update_button_on_dirty(); | |||
this.setup_editable_title(); | |||
}, | |||
refresh: function() { | |||
} | |||
refresh() { | |||
this.make_menu(); | |||
this.make_viewers(); | |||
this.set_title(); | |||
this.page.clear_user_actions(); | |||
this.show_title_as_dirty(); | |||
@@ -27,9 +29,11 @@ frappe.ui.form.Toolbar = Class.extend({ | |||
this.print_icon && this.print_icon.removeClass("hide"); | |||
} | |||
} | |||
}, | |||
set_title: function() { | |||
if(this.frm.meta.title_field) { | |||
} | |||
set_title() { | |||
if (this.frm.is_new()) { | |||
var title = __('New {0}', [this.frm.meta.name]); | |||
} else if (this.frm.meta.title_field) { | |||
let title_field = (this.frm.doc[this.frm.meta.title_field] || "").toString().trim(); | |||
var title = strip_html(title_field || this.frm.docname); | |||
if(this.frm.doc.__islocal || title === this.frm.docname || this.frm.meta.autoname==="hash") { | |||
@@ -56,8 +60,8 @@ frappe.ui.form.Toolbar = Class.extend({ | |||
!!(this.is_title_editable() || this.can_rename())); | |||
this.set_indicator(); | |||
}, | |||
is_title_editable: function() { | |||
} | |||
is_title_editable() { | |||
let title_field = this.frm.meta.title_field; | |||
let doc_field = this.frm.get_docfield(title_field); | |||
@@ -70,16 +74,16 @@ frappe.ui.form.Toolbar = Class.extend({ | |||
} else { | |||
return false; | |||
} | |||
}, | |||
can_rename: function() { | |||
} | |||
can_rename() { | |||
return this.frm.perm[0].write && this.frm.meta.allow_rename && !this.frm.doc.__islocal; | |||
}, | |||
show_unchanged_document_alert: function() { | |||
} | |||
show_unchanged_document_alert() { | |||
frappe.show_alert({ | |||
indicator: "info", | |||
message: __("Unchanged") | |||
}); | |||
}, | |||
} | |||
rename_document_title(new_name, new_title, merge=false) { | |||
const docname = this.frm.doc.name; | |||
const title_field = this.frm.meta.title_field || ''; | |||
@@ -123,8 +127,8 @@ frappe.ui.form.Toolbar = Class.extend({ | |||
rename_document().then(resolve).catch(reject); | |||
} | |||
}); | |||
}, | |||
setup_editable_title: function () { | |||
} | |||
setup_editable_title () { | |||
let me = this; | |||
this.page.$title_area.find(".title-text").on("click", () => { | |||
@@ -180,11 +184,11 @@ frappe.ui.form.Toolbar = Class.extend({ | |||
}); | |||
} | |||
}); | |||
}, | |||
get_dropdown_menu: function(label) { | |||
} | |||
get_dropdown_menu(label) { | |||
return this.page.add_dropdown(label); | |||
}, | |||
set_indicator: function() { | |||
} | |||
set_indicator() { | |||
var indicator = frappe.get_indicator(this.frm.doc); | |||
if (this.frm.save_disabled && indicator && [__('Saved'), __('Not Saved')].includes(indicator[0])) { | |||
return; | |||
@@ -194,40 +198,58 @@ frappe.ui.form.Toolbar = Class.extend({ | |||
} else { | |||
this.page.clear_indicator(); | |||
} | |||
}, | |||
make_menu: function() { | |||
} | |||
make_menu() { | |||
this.page.clear_icons(); | |||
this.page.clear_menu(); | |||
var me = this; | |||
var p = this.frm.perm[0]; | |||
var docstatus = cint(this.frm.doc.docstatus); | |||
var is_submittable = frappe.model.is_submittable(this.frm.doc.doctype) | |||
var issingle = this.frm.meta.issingle; | |||
var print_settings = frappe.model.get_doc(":Print Settings", "Print Settings") | |||
var allow_print_for_draft = cint(print_settings.allow_print_for_draft); | |||
var allow_print_for_cancelled = cint(print_settings.allow_print_for_cancelled); | |||
// Navigate | |||
if (!this.frm.is_new() && !issingle) { | |||
this.page.add_action_icon("left", function() { | |||
me.frm.navigate_records(1); | |||
}, 'prev-doc'); | |||
this.page.add_action_icon("right", function() { | |||
me.frm.navigate_records(0); | |||
}, 'next-doc'); | |||
if (frappe.boot.desk_settings.form_sidebar) { | |||
this.make_navigation(); | |||
this.make_menu_items(); | |||
} | |||
} | |||
make_viewers() { | |||
if (this.frm.viewers) return; | |||
this.frm.viewers = new frappe.ui.form.FormViewers({ | |||
frm: this.frm, | |||
parent: $('<div class="form-viewers d-flex"></div>').prependTo(this.frm.page.page_actions) | |||
}); | |||
} | |||
make_navigation() { | |||
// Navigate | |||
if (!this.frm.is_new() && !this.frm.meta.issingle) { | |||
this.page.add_action_icon("left", () => { | |||
this.frm.navigate_records(1); | |||
}, 'prev-doc', __("Previous")); | |||
this.page.add_action_icon("right", ()=> { | |||
this.frm.navigate_records(0); | |||
}, 'next-doc', __("Next")); | |||
} | |||
} | |||
make_menu_items() { | |||
const me = this; | |||
const p = this.frm.perm[0]; | |||
const docstatus = cint(this.frm.doc.docstatus); | |||
const is_submittable = frappe.model.is_submittable(this.frm.doc.doctype) | |||
const print_settings = frappe.model.get_doc(":Print Settings", "Print Settings") | |||
const allow_print_for_draft = cint(print_settings.allow_print_for_draft); | |||
const allow_print_for_cancelled = cint(print_settings.allow_print_for_cancelled); | |||
if (!is_submittable || docstatus == 1 || | |||
(allow_print_for_cancelled && docstatus == 2)|| | |||
(allow_print_for_draft && docstatus == 0)) { | |||
if (frappe.model.can_print(null, me.frm) && !issingle) { | |||
if (frappe.model.can_print(null, me.frm) && !this.frm.meta.issingle) { | |||
this.page.add_menu_item(__("Print"), function() { | |||
me.frm.print_doc(); | |||
}, true); | |||
this.print_icon = this.page.add_action_icon("printer", function() { | |||
me.frm.print_doc(); | |||
}); | |||
},'', __("Print")); | |||
} | |||
} | |||
@@ -283,14 +305,35 @@ frappe.ui.form.Toolbar = Class.extend({ | |||
}); | |||
} | |||
this.make_customize_buttons(); | |||
// Auto Repeat | |||
if(this.can_repeat()) { | |||
this.page.add_menu_item(__("Repeat"), function(){ | |||
frappe.utils.new_auto_repeat_prompt(me.frm); | |||
}, true); | |||
} | |||
// New | |||
if(p[CREATE] && !this.frm.meta.issingle) { | |||
this.page.add_menu_item(__("New {0}", [__(me.frm.doctype)]), function() { | |||
frappe.new_doc(me.frm.doctype, true); | |||
}, true, { | |||
shortcut: 'Ctrl+B', | |||
condition: () => !this.frm.is_new() | |||
}); | |||
} | |||
} | |||
make_customize_buttons() { | |||
if (frappe.user_roles.includes("System Manager")) { | |||
let is_doctype_form = me.frm.doctype === 'DocType'; | |||
let doctype = is_doctype_form ? me.frm.docname : me.frm.doctype; | |||
let is_doctype_custom = is_doctype_form ? me.frm.doc.custom : false; | |||
let is_doctype_form = this.frm.doctype === 'DocType'; | |||
let doctype = is_doctype_form ? this.frm.docname : this.frm.doctype; | |||
let is_doctype_custom = is_doctype_form ? this.frm.doc.custom : false; | |||
if (doctype != 'DocType' && !is_doctype_custom && me.frm.meta.issingle === 0) { | |||
this.page.add_menu_item(__("Customize"), function() { | |||
if (me.frm.meta && me.frm.meta.custom) { | |||
if (doctype != 'DocType' && !is_doctype_custom && this.frm.meta.issingle === 0) { | |||
this.page.add_menu_item(__("Customize"), () => { | |||
if (this.frm.meta && this.frm.meta.custom) { | |||
frappe.set_route('Form', 'DocType', doctype); | |||
} else { | |||
frappe.set_route('Form', 'Customize Form', { | |||
@@ -302,77 +345,62 @@ frappe.ui.form.Toolbar = Class.extend({ | |||
if (frappe.boot.developer_mode===1 && !is_doctype_form) { | |||
// edit doctype | |||
this.page.add_menu_item(__("Edit DocType"), function() { | |||
frappe.set_route('Form', 'DocType', me.frm.doctype); | |||
this.page.add_menu_item(__("Edit DocType"), () => { | |||
frappe.set_route('Form', 'DocType', this.frm.doctype); | |||
}, true); | |||
} | |||
} | |||
// Auto Repeat | |||
if(this.can_repeat()) { | |||
this.page.add_menu_item(__("Repeat"), function(){ | |||
frappe.utils.new_auto_repeat_prompt(me.frm); | |||
}, true); | |||
} | |||
} | |||
// New | |||
if(p[CREATE] && !this.frm.meta.issingle) { | |||
this.page.add_menu_item(__("New {0}", [__(me.frm.doctype)]), function() { | |||
frappe.new_doc(me.frm.doctype, true); | |||
}, true, { | |||
shortcut: 'Ctrl+B', | |||
condition: () => !this.frm.is_new() | |||
}); | |||
} | |||
}, | |||
can_repeat: function() { | |||
can_repeat() { | |||
return this.frm.meta.allow_auto_repeat | |||
&& !this.frm.is_new() | |||
&& !this.frm.doc.auto_repeat; | |||
}, | |||
can_save: function() { | |||
} | |||
can_save() { | |||
return this.get_docstatus()===0; | |||
}, | |||
can_submit: function() { | |||
} | |||
can_submit() { | |||
return this.get_docstatus()===0 | |||
&& !this.frm.doc.__islocal | |||
&& !this.frm.doc.__unsaved | |||
&& this.frm.perm[0].submit | |||
&& !this.has_workflow(); | |||
}, | |||
can_update: function() { | |||
} | |||
can_update() { | |||
return this.get_docstatus()===1 | |||
&& !this.frm.doc.__islocal | |||
&& this.frm.perm[0].submit | |||
&& this.frm.doc.__unsaved | |||
}, | |||
can_cancel: function() { | |||
} | |||
can_cancel() { | |||
return this.get_docstatus()===1 | |||
&& this.frm.perm[0].cancel | |||
&& !this.read_only; | |||
}, | |||
can_amend: function() { | |||
} | |||
can_amend() { | |||
return this.get_docstatus()===2 | |||
&& this.frm.perm[0].amend | |||
&& !this.read_only; | |||
}, | |||
has_workflow: function() { | |||
} | |||
has_workflow() { | |||
if(this._has_workflow === undefined) | |||
this._has_workflow = frappe.get_list("Workflow", {document_type: this.frm.doctype}).length; | |||
return this._has_workflow; | |||
}, | |||
get_docstatus: function() { | |||
} | |||
get_docstatus() { | |||
return cint(this.frm.doc.docstatus); | |||
}, | |||
show_linked_with: function() { | |||
} | |||
show_linked_with() { | |||
if(!this.frm.linked_with) { | |||
this.frm.linked_with = new frappe.ui.form.LinkedWith({ | |||
frm: this.frm | |||
}); | |||
} | |||
this.frm.linked_with.show(); | |||
}, | |||
set_primary_action: function(dirty) { | |||
} | |||
set_primary_action(dirty) { | |||
if (!dirty) { | |||
// don't clear actions menu if dirty | |||
this.page.clear_user_actions(); | |||
@@ -403,8 +431,8 @@ frappe.ui.form.Toolbar = Class.extend({ | |||
this.page.clear_actions(); | |||
this.current_status = null; | |||
} | |||
}, | |||
get_action_status: function() { | |||
} | |||
get_action_status() { | |||
var status = null; | |||
if (this.frm.page.current_view_name==='print' || this.frm.hidden) { | |||
status = "Edit"; | |||
@@ -425,8 +453,8 @@ frappe.ui.form.Toolbar = Class.extend({ | |||
status = "Amend"; | |||
} | |||
return status; | |||
}, | |||
set_page_actions: function(status) { | |||
} | |||
set_page_actions(status) { | |||
var me = this; | |||
this.page.clear_actions(); | |||
@@ -469,8 +497,8 @@ frappe.ui.form.Toolbar = Class.extend({ | |||
} | |||
this.current_status = status; | |||
}, | |||
add_update_button_on_dirty: function() { | |||
} | |||
add_update_button_on_dirty() { | |||
var me = this; | |||
$(this.frm.wrapper).on("dirty", function() { | |||
me.show_title_as_dirty(); | |||
@@ -483,8 +511,8 @@ frappe.ui.form.Toolbar = Class.extend({ | |||
me.set_primary_action(true); | |||
} | |||
}); | |||
}, | |||
show_title_as_dirty: function() { | |||
} | |||
show_title_as_dirty() { | |||
if(this.frm.save_disabled) | |||
return; | |||
@@ -493,7 +521,7 @@ frappe.ui.form.Toolbar = Class.extend({ | |||
} | |||
$(this.frm.wrapper).attr("data-state", this.frm.doc.__unsaved ? "dirty" : "clean"); | |||
}, | |||
} | |||
show_jump_to_field_dialog() { | |||
let visible_fields_filter = f => | |||
@@ -525,4 +553,4 @@ frappe.ui.form.Toolbar = Class.extend({ | |||
dialog.show(); | |||
} | |||
}) | |||
} |
@@ -182,15 +182,17 @@ frappe.views.BaseList = class BaseList { | |||
'Dashboard': 'dashboard' | |||
} | |||
this.views_menu = this.page.add_custom_button_group(__(`{0} View`, [this.view_name]), icon_map[this.view_name] || 'list'); | |||
this.views_list = new frappe.views.Views({ | |||
doctype: this.doctype, | |||
parent: this.views_menu, | |||
page: this.page, | |||
list_view: this, | |||
sidebar: this.list_sidebar, | |||
icon_map: icon_map | |||
}); | |||
if (frappe.boot.desk_settings.view_switcher) { | |||
this.views_menu = this.page.add_custom_button_group(__(`{0} View`, [this.view_name]), icon_map[this.view_name] || 'list'); | |||
this.views_list = new frappe.views.Views({ | |||
doctype: this.doctype, | |||
parent: this.views_menu, | |||
page: this.page, | |||
list_view: this, | |||
sidebar: this.list_sidebar, | |||
icon_map: icon_map | |||
}); | |||
} | |||
} | |||
set_default_secondary_action() { | |||
@@ -236,7 +238,7 @@ frappe.views.BaseList = class BaseList { | |||
} | |||
setup_side_bar() { | |||
if (this.hide_sidebar) return; | |||
if (this.hide_sidebar || !frappe.boot.desk_settings.list_sidebar) return; | |||
this.list_sidebar = new frappe.views.ListSidebar({ | |||
doctype: this.doctype, | |||
stats: this.stats, | |||
@@ -779,7 +781,7 @@ class FilterArea { | |||
<span class="filter-icon"> | |||
${frappe.utils.icon('filter')} | |||
</span> | |||
<span class="button-label"> | |||
<span class="button-label hidden-xs"> | |||
${__("Filter")} | |||
<span> | |||
</button> | |||
@@ -167,6 +167,7 @@ export default class ListFilter { | |||
} | |||
get_list_filters() { | |||
if (frappe.session.user === 'Guest') return Promise.resolve(); | |||
return frappe.db | |||
.get_list('List Filter', { | |||
fields: ['name', 'filter_name', 'for_user', 'filters'], | |||
@@ -55,7 +55,7 @@ frappe.views.ListGroupBy = class ListGroupBy { | |||
<div class="list-group-by-fields"> | |||
</div> | |||
<li class="add-list-group-by sidebar-action"> | |||
<a class="add-group-by hidden-xs"> | |||
<a class="add-group-by"> | |||
${__('Edit Filters')} | |||
</a> | |||
</li> | |||
@@ -9,7 +9,6 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { | |||
const doctype = route[1]; | |||
if (route.length === 2) { | |||
// List/{doctype} => List/{doctype}/{last_view} or List | |||
const user_settings = frappe.get_user_settings(doctype); | |||
const last_view = user_settings.last_view; | |||
frappe.set_route( | |||
@@ -164,10 +163,13 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { | |||
show_restricted_list_indicator_if_applicable() { | |||
const match_rules_list = frappe.perm.get_match_rules(this.doctype); | |||
if (match_rules_list.length) { | |||
this.restricted_list = $(`<button class="restricted-button">${__('Restricted')}</button>`) | |||
.prepend('<span class="octicon octicon-lock"></span>') | |||
.click(() => this.show_restrictions(match_rules_list)) | |||
.appendTo(this.page.page_form); | |||
this.restricted_list = $( | |||
`<button class="btn btn-default btn-xs restricted-button flex align-center"> | |||
${frappe.utils.icon('lock', 'xs')} | |||
</button>` | |||
) | |||
.click(() => this.show_restrictions(match_rules_list)) | |||
.appendTo(this.page.page_form); | |||
} | |||
} | |||
@@ -802,25 +804,28 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { | |||
let settings_button = null; | |||
if (this.settings.button && this.settings.button.show(doc)) { | |||
settings_button = ` | |||
<span> | |||
<span class="list-actions"> | |||
<button class="btn btn-action btn-default btn-xs" | |||
data-name="${doc.name}" data-idx="${doc._idx}" | |||
title="${this.settings.button.get_description(doc)}"> | |||
${this.settings.button.get_label(doc)} | |||
</button> | |||
</span> | |||
</span> | |||
`; | |||
} | |||
const modified = comment_when(doc.modified, true); | |||
let assigned_to = `<span class="avatar avatar-small"> | |||
let assigned_to = `<div class="list-assignments"> | |||
<span class="avatar avatar-small"> | |||
<span class="avatar-empty"></span> | |||
</span>`; | |||
</div>`; | |||
let assigned_users = JSON.parse(doc._assign || "[]"); | |||
if (assigned_users.length) { | |||
assigned_to = frappe.avatar_group(assigned_users, 3, {'filterable': true})[0].outerHTML; | |||
assigned_to = `<div class="list-assignments"> | |||
${frappe.avatar_group(assigned_users, 3, { filterable: true })[0].outerHTML} | |||
</div>`; | |||
} | |||
const comment_count = `<span class="${ | |||
@@ -831,9 +836,10 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { | |||
</span>`; | |||
html += ` | |||
<div class="level-item list-row-activity"> | |||
${settings_button || assigned_to} | |||
<div class="level-item list-row-activity hidden-xs"> | |||
<div class="hidden-md hidden-xs"> | |||
${settings_button || assigned_to} | |||
</div> | |||
${modified} | |||
${comment_count} | |||
</div> | |||
@@ -870,7 +876,7 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { | |||
? encodeURIComponent(doc.name) | |||
: doc.name; | |||
return "/app/form/" + frappe.router.slug(frappe.router.doctype_layout || this.doctype) + "/" + docname; | |||
return `/app/${frappe.router.slug(frappe.router.doctype_layout || this.doctype)}/${docname}`; | |||
} | |||
get_seen_class(doc) { | |||
@@ -1083,7 +1089,7 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { | |||
} | |||
setup_list_click() { | |||
this.$result.on("click", ".list-row, .image-view-header", (e) => { | |||
this.$result.on("click", ".list-row, .image-view-header, .file-header", (e) => { | |||
const $target = $(e.target); | |||
// tick checkbox if Ctrl/Meta key is pressed | |||
if (e.ctrlKey || (e.metaKey && !$target.is("a"))) { | |||
@@ -1098,16 +1104,20 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { | |||
if ( | |||
$target.hasClass("filterable") || | |||
$target.hasClass("icon-heart") || | |||
$target.is(":checkbox") || | |||
$target.is("a") | |||
$target.is(":checkbox") | |||
) { | |||
e.stopPropagation(); | |||
return; | |||
} | |||
// open form | |||
// link, let the event be handled via set_route | |||
if ($target.is("a")) { return; } | |||
// clicked on the row, open form | |||
const $row = $(e.currentTarget); | |||
const link = $row.find(".list-subject a").get(0); | |||
if (link) { | |||
window.location.href = link.href; | |||
frappe.set_route(link.pathname); | |||
return false; | |||
} | |||
}); | |||
@@ -35,7 +35,7 @@ frappe.views.Views = class Views { | |||
} | |||
set_route(view, calendar_name) { | |||
const route = ['list', frappe.router.doctype_layout || this.doctype, view]; | |||
const route = [this.get_doctype_route(), 'view', view]; | |||
if (calendar_name) route.push(calendar_name); | |||
frappe.set_route(route); | |||
} | |||
@@ -156,7 +156,7 @@ frappe.views.Views = class Views { | |||
if (item.name == frappe.utils.to_title_case(frappe.get_route().slice(-1)[0] || '')) { | |||
placeholder = item.name; | |||
} | |||
html += `<li><a class="dropdown-item" href="#${item.route}">${item.name}</a></li>`; | |||
html += `<li><a class="dropdown-item" href="/app/${item.route}">${item.name}</a></li>`; | |||
}); | |||
} | |||
@@ -181,7 +181,7 @@ frappe.views.Views = class Views { | |||
reports.map((r) => { | |||
if (!r.ref_doctype || r.ref_doctype == this.doctype) { | |||
const report_type = r.report_type === 'Report Builder' ? | |||
`list/${r.ref_doctype}/report` : 'query-report'; | |||
`/app/list/${r.ref_doctype}/report` : 'query-report'; | |||
const route = r.route || report_type + '/' + (r.title || r.name); | |||
@@ -233,11 +233,11 @@ frappe.views.Views = class Views { | |||
// has standard calendar view | |||
calendars.push({ | |||
name: 'Default', | |||
route: `list/${this.doctype}/calendar/default` | |||
route: `/app/${this.get_doctype_route()}/view/calendar/default` | |||
}); | |||
} | |||
result.map(calendar => { | |||
calendars.push({name: calendar.name, route: `list/${doctype}/calendar/${calendar.name}`}); | |||
calendars.push({name: calendar.name, route: `/app/${this.get_doctype_route()}/view/calendar/${calendar.name}`}); | |||
}); | |||
return calendars; | |||
@@ -249,7 +249,7 @@ frappe.views.Views = class Views { | |||
let accounts = frappe.boot.email_accounts; | |||
accounts.forEach(account => { | |||
let email_account = (account.email_id == "All Accounts") ? "All Accounts" : account.email_account; | |||
let route = ["List", "Communication", "Inbox", email_account].join('/'); | |||
let route = `/app/communication/inbox/${email_account}`; | |||
let display_name = ["All Accounts", "Sent Mail", "Spam", "Trash"].includes(account.email_id) | |||
? __(account.email_id) | |||
: account.email_account; | |||
@@ -262,4 +262,8 @@ frappe.views.Views = class Views { | |||
return accounts_to_add; | |||
} | |||
get_doctype_route() { | |||
return frappe.router.slug(frappe.router.doctype_layout || this.doctype); | |||
} | |||
} |
@@ -126,6 +126,7 @@ $.extend(frappe.model, { | |||
var user_permissions = frappe.defaults.get_user_permissions(); | |||
let allowed_records = []; | |||
let default_doc = null; | |||
let value = null; | |||
if(user_permissions) { | |||
({allowed_records, default_doc} = frappe.perm.filter_allowed_docs_for_doctype(user_permissions[df.options], doc.doctype)); | |||
} | |||
@@ -139,71 +140,79 @@ $.extend(frappe.model, { | |||
if (df.fieldtype==="Link" && df.options!=="User") { | |||
// If user permission has Is Default enabled or single-user permission has found against respective doctype. | |||
if (has_user_permissions && default_doc) { | |||
return default_doc; | |||
} | |||
if(!df.ignore_user_permissions) { | |||
value = default_doc; | |||
} else { | |||
// 2 - look in user defaults | |||
var user_defaults = frappe.defaults.get_user_defaults(df.options); | |||
if (user_defaults && user_defaults.length===1) { | |||
// Use User Permission value when only when it has a single value | |||
user_default = user_defaults[0]; | |||
} | |||
} | |||
if (!user_default) { | |||
user_default = frappe.defaults.get_user_default(df.fieldname); | |||
} | |||
if(!user_default && df.remember_last_selected_value && frappe.boot.user.last_selected_values) { | |||
user_default = frappe.boot.user.last_selected_values[df.options]; | |||
if(!df.ignore_user_permissions) { | |||
var user_defaults = frappe.defaults.get_user_defaults(df.options); | |||
if (user_defaults && user_defaults.length===1) { | |||
// Use User Permission value when only when it has a single value | |||
user_default = user_defaults[0]; | |||
} | |||
} | |||
else if (!user_default) { | |||
user_default = frappe.defaults.get_user_default(df.fieldname); | |||
} | |||
else if(!user_default && df.remember_last_selected_value && frappe.boot.user.last_selected_values) { | |||
user_default = frappe.boot.user.last_selected_values[df.options]; | |||
} | |||
var is_allowed_user_default = user_default && | |||
(!has_user_permissions || allowed_records.includes(user_default)); | |||
// is this user default also allowed as per user permissions? | |||
if (is_allowed_user_default) { | |||
value = user_default; | |||
} | |||
} | |||
var is_allowed_user_default = user_default && | |||
(!has_user_permissions || allowed_records.includes(user_default)); | |||
// is this user default also allowed as per user permissions? | |||
if (is_allowed_user_default) { | |||
return user_default; | |||
} | |||
} | |||
// 3 - look in default of docfield | |||
if (df['default']) { | |||
if (!value || df['default']) { | |||
const default_val = String(df['default']); | |||
if (default_val == "__user" || default_val.toLowerCase() == "user") { | |||
return frappe.session.user; | |||
value = frappe.session.user; | |||
} else if (default_val == "user_fullname") { | |||
return frappe.session.user_fullname; | |||
value = frappe.session.user_fullname; | |||
} else if (default_val == "Today") { | |||
return frappe.datetime.get_today(); | |||
value = frappe.datetime.get_today(); | |||
} else if ((default_val || "").toLowerCase() === "now") { | |||
return frappe.datetime.now_datetime(); | |||
value = frappe.datetime.now_datetime(); | |||
} else if (default_val[0]===":") { | |||
var boot_doc = frappe.model.get_default_from_boot_docs(df, doc, parent_doc); | |||
var is_allowed_boot_doc = !has_user_permissions || allowed_records.includes(boot_doc); | |||
if (is_allowed_boot_doc) { | |||
return boot_doc; | |||
value = boot_doc; | |||
} | |||
} else if (df.fieldname===meta.title_field) { | |||
// ignore defaults for title field | |||
return ""; | |||
value = ""; | |||
} else { | |||
// is this default value is also allowed as per user permissions? | |||
var is_allowed_default = !has_user_permissions || allowed_records.includes(df.default); | |||
if (df.fieldtype!=="Link" || df.options==="User" || is_allowed_default) { | |||
value = df["default"]; | |||
} | |||
} | |||
// is this default value is also allowed as per user permissions? | |||
var is_allowed_default = !has_user_permissions || allowed_records.includes(df.default); | |||
if (df.fieldtype!=="Link" || df.options==="User" || is_allowed_default) { | |||
return df["default"]; | |||
} | |||
} else if (df.fieldtype=="Time") { | |||
return frappe.datetime.now_time(); | |||
value = frappe.datetime.now_time(); | |||
} | |||
// set it here so we know it was set as a default | |||
df.__default_value = value; | |||
return value; | |||
}, | |||
get_default_from_boot_docs: function(df, doc, parent_doc) { | |||
@@ -103,6 +103,31 @@ $.extend(frappe.model, { | |||
return docfield[0]; | |||
}, | |||
get_from_localstorage: function(doctype) { | |||
if (localStorage["_doctype:" + doctype]) { | |||
return JSON.parse(localStorage["_doctype:" + doctype]); | |||
} | |||
}, | |||
set_in_localstorage: function(doctype, docs) { | |||
try { | |||
localStorage["_doctype:" + doctype] = JSON.stringify(docs); | |||
} catch(e) { | |||
// if quota is exceeded, clear local storage and set item | |||
console.warn("localStorage quota exceeded, clearing doctype cache") | |||
frappe.model.clear_local_storage(); | |||
localStorage["_doctype:" + doctype] = JSON.stringify(docs); | |||
} | |||
}, | |||
clear_local_storage: function() { | |||
for(var key in localStorage) { | |||
if (key.startsWith("_doctype:")) { | |||
localStorage.removeItem(key); | |||
} | |||
} | |||
}, | |||
with_doctype: function(doctype, callback, async) { | |||
if(locals.DocType[doctype]) { | |||
callback && callback(); | |||
@@ -110,13 +135,15 @@ $.extend(frappe.model, { | |||
let cached_timestamp = null; | |||
let cached_doc = null; | |||
if(localStorage["_doctype:" + doctype]) { | |||
let cached_docs = JSON.parse(localStorage["_doctype:" + doctype]); | |||
let cached_docs = frappe.model.get_from_localstorage(doctype) | |||
if (cached_docs) { | |||
cached_doc = cached_docs.filter(doc => doc.name === doctype)[0]; | |||
if(cached_doc) { | |||
cached_timestamp = cached_doc.modified; | |||
} | |||
} | |||
return frappe.call({ | |||
method:'frappe.desk.form.load.getdoctype', | |||
type: "GET", | |||
@@ -134,7 +161,7 @@ $.extend(frappe.model, { | |||
if(r.message=="use_cache") { | |||
frappe.model.sync(cached_doc); | |||
} else { | |||
localStorage["_doctype:" + doctype] = JSON.stringify(r.docs); | |||
frappe.model.set_in_localstorage(doctype, r.docs) | |||
} | |||
frappe.model.init_doctype(doctype); | |||
@@ -6,6 +6,8 @@ $.extend(frappe.model.user_settings, { | |||
.then(r => JSON.parse(r.message || '{}')); | |||
}, | |||
save: function(doctype, key, value) { | |||
if (frappe.session.user === 'Guest') return Promise.resolve(); | |||
const old_user_settings = frappe.model.user_settings[doctype] || {}; | |||
const new_user_settings = $.extend(true, {}, old_user_settings); // deep copy | |||
@@ -31,6 +33,7 @@ $.extend(frappe.model.user_settings, { | |||
return this.update(doctype, user_settings); | |||
}, | |||
update: function(doctype, user_settings) { | |||
if (frappe.session.user === 'Guest') return Promise.resolve(); | |||
return frappe.call({ | |||
method: 'frappe.model.utils.user_settings.save', | |||
args: { | |||
@@ -8,6 +8,7 @@ frappe.provide('frappe.request.error_handlers'); | |||
frappe.request.url = '/'; | |||
frappe.request.ajax_count = 0; | |||
frappe.request.waiting_for_ajax = []; | |||
frappe.request.logs = {} | |||
frappe.xcall = function(method, params) { | |||
return new Promise((resolve, reject) => { | |||
@@ -89,6 +90,11 @@ frappe.call = function(opts) { | |||
delete args.cmd; | |||
} | |||
// debouce if required | |||
if (opts.debounce && frappe.request.is_fresh(args, opts.debounce)) { | |||
return Promise.resolve(); | |||
} | |||
return frappe.request.call({ | |||
type: opts.type || "POST", | |||
args: args, | |||
@@ -127,7 +133,7 @@ frappe.request.call = function(opts) { | |||
message: __('The resource you are looking for is not available')}); | |||
}, | |||
403: function(xhr) { | |||
if (frappe.session.user === 'Guest') { | |||
if (frappe.session.logged_in_user !== 'Guest') { | |||
// session expired | |||
frappe.app.handle_session_expired(); | |||
} | |||
@@ -239,7 +245,7 @@ frappe.request.call = function(opts) { | |||
status_code_handler(data, xhr); | |||
} | |||
} catch(e) { | |||
console.log("Unable to handle success response"); // eslint-disable-line | |||
console.log("Unable to handle success response", data); // eslint-disable-line | |||
console.trace(e); // eslint-disable-line | |||
} | |||
@@ -278,6 +284,26 @@ frappe.request.call = function(opts) { | |||
}); | |||
} | |||
frappe.request.is_fresh = function(args, threshold) { | |||
// return true if a request with similar args has been sent recently | |||
if (!frappe.request.logs[args.cmd]) { | |||
frappe.request.logs[args.cmd] = []; | |||
} | |||
for (let past_request of frappe.request.logs[args.cmd]) { | |||
// check if request has same args and was made recently | |||
if ((new Date() - past_request.timestamp) < threshold | |||
&& frappe.utils.deep_equal(args, past_request.args)) { | |||
console.log('throttled'); | |||
return true; | |||
} | |||
} | |||
// log the request | |||
frappe.request.logs[args.cmd].push({args: args, timestamp: new Date()}); | |||
return false; | |||
} | |||
// call execute serverside request | |||
frappe.request.prepare = function(opts) { | |||
$("body").attr("data-ajax-state", "triggered"); | |||
@@ -322,7 +348,8 @@ frappe.request.cleanup = function(opts, r) { | |||
if(r) { | |||
// session expired? - Guest has no business here! | |||
if (r.session_expired || frappe.session.user === "Guest") { | |||
if (r.session_expired || | |||
(frappe.session.user === 'Guest' && frappe.session.logged_in_user !== "Guest")) { | |||
frappe.app.handle_session_expired(); | |||
return; | |||
} | |||
@@ -20,50 +20,76 @@ $(window).on('hashchange', function() { | |||
let sub_path = frappe.router.get_sub_path(window.location.hash); | |||
window.location.hash = ''; | |||
frappe.router.push_state(sub_path); | |||
return false; | |||
} | |||
}); | |||
window.addEventListener('popstate', () => { | |||
window.addEventListener('popstate', (e) => { | |||
// forward-back button, just re-render based on current route | |||
frappe.route(); | |||
frappe.router.route(); | |||
e.preventDefault(); | |||
return false; | |||
}); | |||
// routing v2, capture all clicks so that the target is managed with push-state | |||
$('body').on('click', 'a', function(e) { | |||
let override = (e, route) => { | |||
let override = (route) => { | |||
e.preventDefault(); | |||
frappe.set_route(route); | |||
return false; | |||
}; | |||
// click handled, but not by href | |||
if (e.currentTarget.getAttribute('onclick')) return; | |||
if (e.currentTarget.getAttribute('onclick')) { | |||
return; | |||
} | |||
const href = e.currentTarget.getAttribute('href'); | |||
if (href==='#') return; | |||
if (href==='') { | |||
return override(e, '/app'); | |||
return override('/app'); | |||
} | |||
// target has "#" ,this is a v1 style route, so remake it. | |||
if (e.currentTarget.hash) { | |||
return override(e, e.currentTarget.hash); | |||
return override(e.currentTarget.hash); | |||
} | |||
// target has "/app, this is a v2 style route. | |||
if (e.currentTarget.pathname && | |||
(e.currentTarget.pathname.startsWith('/app') || e.currentTarget.pathname.startsWith('app'))) { | |||
return override(e, e.currentTarget.pathname); | |||
if (e.currentTarget.pathname && frappe.router.is_app_route(e.currentTarget.pathname)) { | |||
return override(e.currentTarget.pathname); | |||
} | |||
}); | |||
frappe.router = { | |||
current_route: null, | |||
doctype_names: {}, | |||
factory_views: ['form', 'list', 'report', 'tree', 'print'], | |||
routes: {}, | |||
factory_views: ['form', 'list', 'report', 'tree', 'print', 'dashboard'], | |||
list_views: ['list', 'kanban', 'report', 'calendar', 'tree', 'gantt', 'dashboard', 'image', 'inbox'], | |||
layout_mapped: {}, | |||
is_app_route(path) { | |||
// desk paths must begin with /app or doctype route | |||
if (path.substr(0, 1) === '/') path = path.substr(1); | |||
path = path.split('/'); | |||
if (path[0]) { | |||
return path[0]==='app'; | |||
} | |||
}, | |||
setup() { | |||
// setup the route names by forming slugs of the given doctypes | |||
for(let doctype of frappe.boot.user.can_read) { | |||
this.routes[this.slug(doctype)] = {doctype: doctype}; | |||
} | |||
if (frappe.boot.doctype_layouts) { | |||
for (let doctype_layout of frappe.boot.doctype_layouts) { | |||
this.routes[this.slug(doctype_layout.name)] = {doctype: doctype_layout.document_type, doctype_layout: doctype_layout.name }; | |||
} | |||
} | |||
}, | |||
route() { | |||
// resolve the route from the URL or hash | |||
// translate it so the objects are well defined | |||
@@ -71,64 +97,86 @@ frappe.router = { | |||
if (!frappe.app) return; | |||
let sub_path = frappe.router.get_sub_path(); | |||
if (frappe.router.re_route(sub_path)) return; | |||
frappe.router.translate_doctype_name().then(() => { | |||
frappe.router.set_history(sub_path); | |||
let sub_path = this.get_sub_path(); | |||
if (this.re_route(sub_path)) return; | |||
if (frappe.router.current_route[0]) { | |||
frappe.router.render_page(); | |||
} else { | |||
// Show home | |||
frappe.views.pageview.show(''); | |||
} | |||
this.current_route = this.parse(); | |||
this.set_history(sub_path); | |||
this.render(); | |||
this.set_title(); | |||
this.trigger('change'); | |||
}, | |||
frappe.router.set_title(); | |||
frappe.route.trigger('change'); | |||
}); | |||
parse(route) { | |||
route = this.get_sub_path_string(route).split('/'); | |||
route = $.map(route, this.decode_component); | |||
this.set_route_options_from_url(route); | |||
return this.convert_to_standard_route(route); | |||
}, | |||
translate_doctype_name() { | |||
return new Promise((resolve) => { | |||
const route = frappe.router.current_route = frappe.router.parse(); | |||
const factory = route[0].toLowerCase(); | |||
const set_name = () => { | |||
const d = frappe.router.doctype_names[route[1]]; | |||
route[1] = d.doctype; | |||
frappe.router.doctype_layout = d.doctype_layout; | |||
resolve(); | |||
}; | |||
if (frappe.router.factory_views.includes(factory)) { | |||
// translate the doctype to its original name | |||
if (frappe.router.doctype_names[route[1]]) { | |||
set_name(); | |||
convert_to_standard_route(route) { | |||
// /app/user = ["List", "User"] | |||
// /app/user/view/report = ["List", "User", "Report"] | |||
// /app/user/view/tree = ["Tree", "User"] | |||
// /app/user/user-001 = ["Form", "User", "user-001"] | |||
// /app/user/user-001 = ["Form", "User", "user-001"] | |||
// /app/event/view/calendar/default = ["List", "Event", "Calendar", "Default"] | |||
let standard_route = route; | |||
let doctype_route = this.routes[route[0]]; | |||
if (doctype_route) { | |||
// doctype route | |||
if (route[1]) { | |||
if (route[2] && route[1]==='view') { | |||
if (route[2].toLowerCase()==='tree') { | |||
standard_route = ['Tree', doctype_route.doctype]; | |||
} else { | |||
standard_route = ['List', doctype_route.doctype, frappe.utils.to_title_case(route[2])]; | |||
if (route[3]) { | |||
// calendar / kanban / dashboard name | |||
standard_route.push(route[3]); | |||
} | |||
} | |||
} else { | |||
frappe.xcall('frappe.desk.utils.get_doctype_name', {name: route[1]}).then((data) => { | |||
frappe.router.doctype_names[route[1]] = data.name_map; | |||
set_name(); | |||
}); | |||
standard_route = ['Form', doctype_route.doctype, route[1]]; | |||
} | |||
} else if (frappe.model.is_single(doctype_route.doctype)) { | |||
standard_route = ['Form', doctype_route.doctype, doctype_route.doctype]; | |||
} else { | |||
resolve(); | |||
standard_route = ['List', doctype_route.doctype, 'List']; | |||
} | |||
}); | |||
if (doctype_route.doctype_layout) { | |||
// set the layout | |||
this.doctype_layout = doctype_route.doctype_layout; | |||
} | |||
} | |||
return standard_route; | |||
}, | |||
set_history(sub_path) { | |||
frappe.route_history.push(frappe.router.current_route); | |||
frappe.route_history.push(this.current_route); | |||
frappe.route_titles[sub_path] = frappe._original_title || document.title; | |||
frappe.ui.hide_open_dialog(); | |||
}, | |||
render() { | |||
if (this.current_route[0]) { | |||
this.render_page(); | |||
} else { | |||
// Show home | |||
frappe.views.pageview.show(''); | |||
} | |||
}, | |||
render_page() { | |||
// create the page generator (factory) object and call `show` | |||
// if there is no generator, render the `Page` object | |||
// first the router needs to know if its a "page", "doctype", "workspace" | |||
const route = frappe.router.current_route; | |||
const route = this.current_route; | |||
const factory = frappe.utils.to_title_case(route[0]); | |||
if (factory === 'Workspace') { | |||
frappe.views.pageview.show(''); | |||
@@ -155,12 +203,12 @@ frappe.router = { | |||
re_route(sub_path) { | |||
if (frappe.re_route[sub_path] !== undefined) { | |||
// after saving a doc, for example, | |||
// "New DocType 1" and the renamed "TestDocType", both exist in history | |||
// "new-doctype-1" and the renamed "TestDocType", both exist in history | |||
// now if we try to go back, | |||
// it doesn't allow us to go back to the one prior to "New DocType 1" | |||
// it doesn't allow us to go back to the one prior to "new-doctype-1" | |||
// Hence if this check is true, instead of changing location hash, | |||
// we just do a back to go to the doc previous to the "New DocType 1" | |||
var re_route_val = frappe.router.get_sub_path(frappe.re_route[sub_path]); | |||
// we just do a back to go to the doc previous to the "new-doctype-1" | |||
var re_route_val = this.get_sub_path(frappe.re_route[sub_path]); | |||
if (decodeURIComponent(re_route_val) === decodeURIComponent(sub_path)) { | |||
window.history.back(); | |||
return true; | |||
@@ -185,32 +233,14 @@ frappe.router = { | |||
// set the route (push state) with given arguments | |||
// example 1: frappe.set_route('a', 'b', 'c'); | |||
// example 2: frappe.set_route(['a', 'b', 'c']); | |||
// example 3: frappe.set_route('a/b/c'); | |||
// example 3: frappe.set_route('a/b/c'); | |||
let route = arguments; | |||
return new Promise(resolve => { | |||
var route = arguments; | |||
if (route.length===1 && $.isArray(route[0])) { | |||
// called as frappe.set_route(['a', 'b', 'c']); | |||
route = route[0]; | |||
} | |||
if (route.length===1 && route[0].includes('/')) { | |||
// called as frappe.set_route('a/b/c') | |||
route = $.map(route[0].split('/'), frappe.router.decode_component); | |||
} | |||
if (route && route[0] == '') { | |||
route.shift(); | |||
} | |||
if (route && ['desk', 'app'].includes(route[0])) { | |||
// we only need subpath, remove "app" (or "desk") | |||
route.shift(); | |||
} | |||
frappe.router.slug_parts(route); | |||
const sub_path = frappe.router.make_url_from_list(route); | |||
frappe.router.push_state(sub_path); | |||
route = this.get_route_from_arguments(route); | |||
route = this.convert_from_standard_route(route); | |||
const sub_path = this.make_url(route); | |||
this.push_state(sub_path); | |||
setTimeout(() => { | |||
frappe.after_ajax && frappe.after_ajax(() => { | |||
@@ -220,19 +250,71 @@ frappe.router = { | |||
}); | |||
}, | |||
get_route_from_arguments(route) { | |||
if (route.length===1 && $.isArray(route[0])) { | |||
// called as frappe.set_route(['a', 'b', 'c']); | |||
route = route[0]; | |||
} | |||
if (route.length===1 && route[0].includes('/')) { | |||
// called as frappe.set_route('a/b/c') | |||
route = $.map(route[0].split('/'), this.decode_component); | |||
} | |||
if (route && route[0] == '') { | |||
route.shift(); | |||
} | |||
if (route && ['desk', 'app'].includes(route[0])) { | |||
// we only need subpath, remove "app" (or "desk") | |||
route.shift(); | |||
} | |||
return route; | |||
}, | |||
convert_from_standard_route(route) { | |||
// ["List", "Sales Order"] => /sales-order | |||
// ["Form", "Sales Order", "SO-0001"] => /sales-order/SO-0001 | |||
// ["Tree", "Account"] = /account/view/tree | |||
const view = route[0] ? route[0].toLowerCase() : ''; | |||
let new_route = route; | |||
if (view === 'list') { | |||
if (route[2] && route[2] !== 'list') { | |||
new_route = [this.slug(route[1]), 'view', route[2].toLowerCase()]; | |||
// calendar / inbox | |||
if (route[3]) new_route.push(route[3]); | |||
} else { | |||
new_route = [this.slug(route[1])]; | |||
} | |||
} else if (view === 'form') { | |||
new_route = [this.slug(route[1])]; | |||
if (route[2]) { | |||
// if not single | |||
new_route.push(route[2]); | |||
} | |||
} else if (view === 'tree') { | |||
new_route = [this.slug(route[1]), 'view', 'tree']; | |||
} | |||
return new_route; | |||
}, | |||
slug_parts(route) { | |||
// slug doctype | |||
// if app is part of the route, then first 2 elements are "" and "app" | |||
if (route[0] && frappe.router.factory_views.includes(route[0].toLowerCase())) { | |||
if (route[0] && this.factory_views.includes(route[0].toLowerCase())) { | |||
route[0] = route[0].toLowerCase(); | |||
route[1] = frappe.router.slug(route[1]); | |||
route[1] = this.slug(route[1]); | |||
} | |||
return route; | |||
}, | |||
make_url_from_list(params) { | |||
return $.map(params, function(a) { | |||
make_url(params) { | |||
return '/app/' + $.map(params, function(a) { | |||
if ($.isPlainObject(a)) { | |||
frappe.route_options = a; | |||
return null; | |||
@@ -247,10 +329,8 @@ frappe.router = { | |||
}).join('/'); | |||
}, | |||
push_state(sub_path) { | |||
push_state(url) { | |||
// change the URL and call the router | |||
const url = `/app/${sub_path}`; | |||
if (window.location.pathname !== url) { | |||
// cleanup any remenants of v1 routing | |||
window.location.hash = ''; | |||
@@ -259,19 +339,10 @@ frappe.router = { | |||
history.pushState(null, null, url); | |||
// now process the route | |||
frappe.router.route(); | |||
this.route(); | |||
} | |||
}, | |||
parse(route) { | |||
route = frappe.router.get_sub_path_string(route).split('/'); | |||
route = $.map(route, frappe.router.decode_component); | |||
frappe.router.set_route_options_from_url(route); | |||
return route; | |||
}, | |||
get_sub_path_string(route) { | |||
// return clean sub_path from hash or url | |||
// supports both v1 and v2 routing | |||
@@ -279,7 +350,7 @@ frappe.router = { | |||
route = window.location.hash || window.location.pathname; | |||
} | |||
return frappe.router.strip_prefix(route); | |||
return this.strip_prefix(route); | |||
}, | |||
strip_prefix(route) { | |||
@@ -292,8 +363,8 @@ frappe.router = { | |||
}, | |||
get_sub_path(route) { | |||
var sub_path = frappe.router.get_sub_path_string(route); | |||
route = $.map(sub_path.split('/'), frappe.router.decode_component).join('/'); | |||
var sub_path = this.get_sub_path_string(route); | |||
route = $.map(sub_path.split('/'), this.decode_component).join('/'); | |||
return route; | |||
}, | |||
@@ -333,10 +404,9 @@ frappe.router = { | |||
}; | |||
// global functions for backward compatibility | |||
frappe.route = frappe.router.route; | |||
frappe.get_route = () => frappe.router.current_route; | |||
frappe.get_route_str = () => frappe.router.current_route.join('/'); | |||
frappe.set_route = frappe.router.set_route; | |||
frappe.set_route = function() { return frappe.router.set_route.apply(frappe.router, arguments) }; | |||
frappe.get_prev_route = function() { | |||
if (frappe.route_history && frappe.route_history.length > 1) { | |||
@@ -356,4 +426,4 @@ frappe.has_route_options = function() { | |||
return Boolean(Object.keys(frappe.route_options || {}).length); | |||
}; | |||
frappe.utils.make_event_emitter(frappe.route); | |||
frappe.utils.make_event_emitter(frappe.router); |
@@ -1,19 +1,21 @@ | |||
frappe.provide('frappe.route'); | |||
frappe.route_history_queue = []; | |||
const routes_to_skip = ['Form', 'social', 'setup-wizard', 'recorder']; | |||
const save_routes = frappe.utils.debounce(() => { | |||
if (frappe.session.user === 'Guest') return; | |||
const routes = frappe.route_history_queue; | |||
frappe.route_history_queue = []; | |||
frappe.xcall('frappe.deferred_insert.deferred_insert', { | |||
'doctype': 'Route History', | |||
'records': routes | |||
}).catch(() => { | |||
frappe.route_history_queue.concat(routes); | |||
}); | |||
}); | |||
}, 10000); | |||
frappe.route.on('change', () => { | |||
frappe.router.on('change', () => { | |||
const route = frappe.get_route(); | |||
if (is_route_useful(route)) { | |||
frappe.route_history_queue.push({ | |||