瀏覽代碼

Merge branch 'rebrand-ui' of https://github.com/frappe/frappe into desk-enhancements

version-14
Shivam Mishra 4 年之前
父節點
當前提交
8b77346066
共有 100 個檔案被更改,包括 1735 行新增1387 行删除
  1. +5
    -1
      frappe/__init__.py
  2. +14
    -0
      frappe/automation/doctype/auto_repeat/auto_repeat.js
  3. +10
    -1
      frappe/automation/doctype/auto_repeat/auto_repeat.json
  4. +10
    -1
      frappe/automation/doctype/auto_repeat/auto_repeat.py
  5. +51
    -0
      frappe/automation/doctype/auto_repeat/test_auto_repeat.py
  6. +5
    -0
      frappe/boot.py
  7. +2
    -2
      frappe/cache_manager.py
  8. +2
    -4
      frappe/commands/site.py
  9. +1
    -1
      frappe/core/doctype/data_import_legacy/data_import_legacy.js
  10. +4
    -3
      frappe/core/doctype/doctype/doctype.json
  11. +23
    -19
      frappe/core/doctype/doctype/doctype.py
  12. +7
    -0
      frappe/core/doctype/doctype/patches/set_route.py
  13. +1
    -1
      frappe/core/doctype/navbar_settings/navbar_settings.py
  14. +8
    -8
      frappe/core/doctype/role/role.json
  15. +1
    -1
      frappe/core/doctype/role/role.py
  16. +30
    -11
      frappe/core/doctype/server_script/server_script.js
  17. +3
    -3
      frappe/core/doctype/server_script/server_script.json
  18. +10
    -2
      frappe/core/doctype/server_script/server_script.py
  19. +9
    -2
      frappe/core/doctype/server_script/server_script_utils.py
  20. +25
    -0
      frappe/core/doctype/server_script/test_server_script.py
  21. +31
    -32
      frappe/core/doctype/system_settings/system_settings.js
  22. +2
    -2
      frappe/core/doctype/user_permission/user_permission.py
  23. +5
    -0
      frappe/custom/doctype/customize_form/customize_form.js
  24. +13
    -1
      frappe/custom/doctype/doctype_layout/doctype_layout.json
  25. +4
    -1
      frappe/custom/doctype/doctype_layout/doctype_layout.py
  26. +13
    -0
      frappe/custom/doctype/doctype_layout/patches/convert_web_forms_to_doctype_layout.py
  27. +1
    -1
      frappe/database/mariadb/framework_mariadb.sql
  28. +1
    -1
      frappe/desk/desktop.py
  29. +19
    -0
      frappe/desk/doctype/dashboard/dashboard.py
  30. +19
    -11
      frappe/desk/doctype/dashboard_chart/dashboard_chart.py
  31. +11
    -2
      frappe/desk/doctype/number_card/number_card.py
  32. +2
    -2
      frappe/desk/form/load.py
  33. +11
    -7
      frappe/desk/form/meta.py
  34. +1
    -1
      frappe/desk/listview.py
  35. +30
    -0
      frappe/desk/page/user_profile/user_profile.css
  36. +3
    -4
      frappe/desk/page/user_profile/user_profile.html
  37. +3
    -4
      frappe/desk/page/user_profile/user_profile.js
  38. +57
    -31
      frappe/desk/page/user_profile/user_profile_controller.js
  39. +1
    -1
      frappe/desk/reportview.py
  40. +2
    -23
      frappe/desk/utils.py
  41. +20
    -2
      frappe/email/doctype/email_template/email_template.json
  42. +24
    -3
      frappe/email/doctype/email_template/email_template.py
  43. +3
    -3
      frappe/email/doctype/newsletter/newsletter.py
  44. +1
    -0
      frappe/hooks.py
  45. +7
    -13
      frappe/installer.py
  46. +5
    -27
      frappe/integrations/doctype/s3_backup_settings/s3_backup_settings.json
  47. +14
    -44
      frappe/integrations/doctype/s3_backup_settings/s3_backup_settings.py
  48. +3
    -3
      frappe/model/base_document.py
  49. +11
    -2
      frappe/model/db_query.py
  50. +2
    -1
      frappe/model/meta.py
  51. +5
    -2
      frappe/patches.txt
  52. +21
    -0
      frappe/patches/v13_0/add_switch_theme_to_navbar_settings.py
  53. +13
    -0
      frappe/patches/v13_0/update_icons_in_customized_desk_pages.py
  54. +17
    -1
      frappe/printing/doctype/print_format/print_format.js
  55. +10
    -1
      frappe/printing/doctype/print_format/print_format.json
  56. +1
    -0
      frappe/printing/page/print/print.js
  57. +2
    -2
      frappe/printing/page/print_format_builder/print_format_builder.js
  58. +2
    -2
      frappe/printing/print_style/redesign/redesign.json
  59. +1
    -2
      frappe/public/build.json
  60. +0
    -315
      frappe/public/css/page.css
  61. +34
    -31
      frappe/public/icons/timeless/symbol-defs.svg
  62. +0
    -14
      frappe/public/js/frappe/chat.js
  63. +1
    -0
      frappe/public/js/frappe/data_import/import_preview.js
  64. +7
    -22
      frappe/public/js/frappe/desk.js
  65. +5
    -2
      frappe/public/js/frappe/dom.js
  66. +16
    -0
      frappe/public/js/frappe/form/controls/autocomplete.js
  67. +6
    -3
      frappe/public/js/frappe/form/controls/button.js
  68. +41
    -30
      frappe/public/js/frappe/form/controls/link.js
  69. +1
    -1
      frappe/public/js/frappe/form/controls/multiselect_list.js
  70. +1
    -1
      frappe/public/js/frappe/form/controls/multiselect_pills.js
  71. +1
    -1
      frappe/public/js/frappe/form/controls/table_multiselect.js
  72. +1
    -3
      frappe/public/js/frappe/form/controls/text_editor.js
  73. +325
    -173
      frappe/public/js/frappe/form/dashboard.js
  74. +23
    -9
      frappe/public/js/frappe/form/footer/base_timeline.js
  75. +1
    -1
      frappe/public/js/frappe/form/footer/footer.js
  76. +5
    -5
      frappe/public/js/frappe/form/footer/form_timeline.js
  77. +23
    -22
      frappe/public/js/frappe/form/form.js
  78. +40
    -0
      frappe/public/js/frappe/form/form_viewers.js
  79. +1
    -1
      frappe/public/js/frappe/form/formatters.js
  80. +1
    -23
      frappe/public/js/frappe/form/layout.js
  81. +2
    -5
      frappe/public/js/frappe/form/save.js
  82. +43
    -65
      frappe/public/js/frappe/form/sidebar/assign_to.js
  83. +18
    -1
      frappe/public/js/frappe/form/sidebar/attachments.js
  84. +0
    -22
      frappe/public/js/frappe/form/sidebar/form_sidebar_users.js
  85. +5
    -4
      frappe/public/js/frappe/form/sidebar/review.js
  86. +11
    -3
      frappe/public/js/frappe/form/sidebar/share.js
  87. +5
    -20
      frappe/public/js/frappe/form/templates/form_sidebar.html
  88. +49
    -44
      frappe/public/js/frappe/form/templates/set_sharing.html
  89. +121
    -93
      frappe/public/js/frappe/form/toolbar.js
  90. +13
    -11
      frappe/public/js/frappe/list/base_list.js
  91. +1
    -0
      frappe/public/js/frappe/list/list_filter.js
  92. +1
    -1
      frappe/public/js/frappe/list/list_sidebar_group_by.js
  93. +29
    -19
      frappe/public/js/frappe/list/list_view.js
  94. +10
    -6
      frappe/public/js/frappe/list/views.js
  95. +45
    -36
      frappe/public/js/frappe/model/create_new.js
  96. +30
    -3
      frappe/public/js/frappe/model/model.js
  97. +3
    -0
      frappe/public/js/frappe/model/user_settings.js
  98. +30
    -3
      frappe/public/js/frappe/request.js
  99. +169
    -99
      frappe/public/js/frappe/router.js
  100. +5
    -3
      frappe/public/js/frappe/router_history.js

+ 5
- 1
frappe/__init__.py 查看文件

@@ -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 "[]")



+ 14
- 0
frappe/automation/doctype/auto_repeat/auto_repeat.js 查看文件

@@ -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) {


+ 10
- 1
frappe/automation/doctype/auto_repeat/auto_repeat.json 查看文件

@@ -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",


+ 10
- 1
frappe/automation/doctype/auto_repeat/auto_repeat.py 查看文件

@@ -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))



+ 51
- 0
frappe/automation/doctype/auto_repeat/test_auto_repeat.py 查看文件

@@ -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()

+ 5
- 0
frappe/boot.py 查看文件

@@ -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()]


+ 2
- 2
frappe/cache_manager.py 查看文件

@@ -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 = {}


+ 2
- 4
frappe/commands/site.py 查看文件

@@ -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


+ 1
- 1
frappe/core/doctype/data_import_legacy/data_import_legacy.js 查看文件

@@ -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 + "%");


+ 4
- 3
frappe/core/doctype/doctype/doctype.json 查看文件

@@ -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",


+ 23
- 19
frappe/core/doctype/doctype/doctype.py 查看文件

@@ -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]:


+ 7
- 0
frappe/core/doctype/doctype/patches/set_route.py 查看文件

@@ -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)

+ 1
- 1
frappe/core/doctype/navbar_settings/navbar_settings.py 查看文件

@@ -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:


+ 8
- 8
frappe/core/doctype/role/role.json 查看文件

@@ -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",


+ 1
- 1
frappe/core/doctype/role/role.py 查看文件

@@ -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):


+ 30
- 11
frappe/core/doctype/server_script/server_script.js 查看文件

@@ -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/&lt;method-name&gt;</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>
`);
}



+ 3
- 3
frappe/core/doctype/server_script/server_script.json 查看文件

@@ -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",


+ 10
- 2
frappe/core/doctype/server_script/server_script.py 查看文件

@@ -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))


+ 9
- 2
frappe/core/doctype/server_script/server_script_utils.py 查看文件

@@ -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

+ 25
- 0
frappe/core/doctype/server_script/test_server_script.py 查看文件

@@ -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)

+ 31
- 32
frappe/core/doctype/system_settings/system_settings.js 查看文件

@@ -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);
}
}
}
});

+ 2
- 2
frappe/core/doctype/user_permission/user_permission.py 查看文件

@@ -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)


+ 5
- 0
frappe/custom/doctype/customize_form/customize_form.js 查看文件

@@ -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();
},


+ 13
- 1
frappe/custom/doctype/doctype_layout/doctype_layout.json 查看文件

@@ -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

+ 4
- 1
frappe/custom/doctype/doctype_layout/doctype_layout.py 查看文件

@@ -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)

+ 13
- 0
frappe/custom/doctype/doctype_layout/patches/convert_web_forms_to_doctype_layout.py 查看文件

@@ -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)

+ 1
- 1
frappe/database/mariadb/framework_mariadb.sql 查看文件

@@ -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;


+ 1
- 1
frappe/desk/desktop.py 查看文件

@@ -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":


+ 19
- 0
frappe/desk/doctype/dashboard/dashboard.py 查看文件

@@ -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 = []


+ 19
- 11
frappe/desk/doctype/dashboard_chart/dashboard_chart.py 查看文件

@@ -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)


+ 11
- 2
frappe/desk/doctype/number_card/number_card.py 查看文件

@@ -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)


+ 2
- 2
frappe/desk/form/load.py 查看文件

@@ -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"""



+ 11
- 7
frappe/desk/form/meta.py 查看文件

@@ -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 = []


+ 1
- 1
frappe/desk/listview.py 查看文件

@@ -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)


+ 30
- 0
frappe/desk/page/user_profile/user_profile.css 查看文件

@@ -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;
}

+ 3
- 4
frappe/desk/page/user_profile/user_profile.html 查看文件

@@ -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>

+ 3
- 4
frappe/desk/page/user_profile/user_profile.js 查看文件

@@ -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();
});

+ 57
- 31
frappe/desk/page/user_profile/user_profile_controller.js 查看文件

@@ -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');

+ 1
- 1
frappe/desk/reportview.py 查看文件

@@ -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()


+ 2
- 23
frappe/desk/utils.py 查看文件

@@ -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(' ', '-')

+ 20
- 2
frappe/email/doctype/email_template/email_template.json 查看文件

@@ -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 &gt; 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",


+ 24
- 3
frappe/email/doctype/email_template/email_template.py 查看文件

@@ -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)

+ 3
- 3
frappe/email/doctype/newsletter/newsletter.py 查看文件

@@ -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):


+ 1
- 0
frappe/hooks.py 查看文件

@@ -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",


+ 7
- 13
frappe/installer.py 查看文件

@@ -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)


+ 5
- 27
frappe/integrations/doctype/s3_backup_settings/s3_backup_settings.json 查看文件

@@ -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
}
}

+ 14
- 44
frappe/integrations/doctype/s3_backup_settings/s3_backup_settings.py 查看文件

@@ -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()

+ 3
- 3
frappe/model/base_document.py 查看文件

@@ -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):


+ 11
- 2
frappe/model/db_query.py 查看文件

@@ -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:


+ 2
- 1
frappe/model/meta.py 查看文件

@@ -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



+ 5
- 2
frappe/patches.txt 查看文件

@@ -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

+ 21
- 0
frappe/patches/v13_0/add_switch_theme_to_navbar_settings.py 查看文件

@@ -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()

+ 13
- 0
frappe/patches/v13_0/update_icons_in_customized_desk_pages.py 查看文件

@@ -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)

+ 17
- 1
frappe/printing/doctype/print_format/print_format.js 查看文件

@@ -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);
});
}
}
})
});

+ 10
- 1
frappe/printing/doctype/print_format/print_format.json 查看文件

@@ -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",


+ 1
- 0
frappe/printing/page/print/print.js 查看文件

@@ -453,6 +453,7 @@ frappe.ui.form.PrintView = class {
display: block !important;
order: 1;
margin-top: auto;
padding-top: var(--padding-xl)
`
);
}


+ 2
- 2
frappe/printing/page/print_format_builder/print_format_builder.js 查看文件

@@ -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>';
}



+ 2
- 2
frappe/printing/print_style/redesign/redesign.json 查看文件

@@ -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",


+ 1
- 2
frappe/public/build.json 查看文件

@@ -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",


+ 0
- 315
frappe/public/css/page.css 查看文件

@@ -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;
}

+ 34
- 31
frappe/public/icons/timeless/symbol-defs.svg 查看文件

@@ -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>

+ 0
- 14
frappe/public/js/frappe/chat.js 查看文件

@@ -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 {


+ 1
- 0
frappe/public/js/frappe/data_import/import_preview.js 查看文件

@@ -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">


+ 7
- 22
frappe/public/js/frappe/desk.js 查看文件

@@ -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) {


+ 5
- 2
frappe/public/js/frappe/dom.js 查看文件

@@ -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>
`);



+ 16
- 0
frappe/public/js/frappe/form/controls/autocomplete.js 查看文件

@@ -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');
});


+ 6
- 3
frappe/public/js/frappe/form/controls/button.js 查看文件

@@ -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("&nbsp;");
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);
}
});

+ 41
- 30
frappe/public/js/frappe/form/controls/link.js 查看文件

@@ -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++) {


+ 1
- 1
frappe/public/js/frappe/form/controls/multiselect_list.js 查看文件

@@ -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) {


+ 1
- 1
frappe/public/js/frappe/form/controls/multiselect_pills.js 查看文件

@@ -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>`;
},


+ 1
- 1
frappe/public/js/frappe/form/controls/table_multiselect.js 查看文件

@@ -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>`;
},


+ 1
- 3
frappe/public/js/frappe/form/controls/text_editor.js 查看文件

@@ -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']
];
},



+ 325
- 173
frappe/public/js/frappe/form/dashboard.js 查看文件

@@ -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();
}
});
}

+ 23
- 9
frappe/public/js/frappe/form/footer/base_timeline.js 查看文件

@@ -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;


+ 1
- 1
frappe/public/js/frappe/form/footer/footer.js 查看文件

@@ -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;


+ 5
- 5
frappe/public/js/frappe/form/footer/form_timeline.js 查看文件

@@ -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),
});


+ 23
- 22
frappe/public/js/frappe/form/form.js 查看文件

@@ -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();
}



+ 40
- 0
frappe/public/js/frappe/form/form_viewers.js 查看文件

@@ -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);
}
};

+ 1
- 1
frappe/public/js/frappe/form/formatters.js 查看文件

@@ -121,7 +121,7 @@ frappe.form.formatters = {
{onclick: docfield.link_onclick.replace(/"/g, '&quot;'), 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>`


+ 1
- 23
frappe/public/js/frappe/form/layout.js 查看文件

@@ -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) => {


+ 2
- 5
frappe/public/js/frappe/form/save.js 查看文件

@@ -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;
};



+ 43
- 65
frappe/public/js/frappe/form/sidebar/assign_to.js 查看文件

@@ -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;
}
};

+ 18
- 1
frappe/public/js/frappe/form/sidebar/attachments.js 查看文件

@@ -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"));

},


+ 0
- 22
frappe/public/js/frappe/form/sidebar/form_sidebar_users.js 查看文件

@@ -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);
}
};

+ 5
- 4
frappe/public/js/frappe/form/sidebar/review.js 查看文件

@@ -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);


+ 11
- 3
frappe/public/js/frappe/form/sidebar/share.js 查看文件

@@ -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;


+ 5
- 20
frappe/public/js/frappe/form/templates/form_sidebar.html 查看文件

@@ -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>

+ 49
- 44
frappe/public/js/frappe/form/templates/set_sharing.html 查看文件

@@ -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>

+ 121
- 93
frappe/public/js/frappe/form/toolbar.js 查看文件

@@ -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() {
// Print
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();
}
})
}

+ 13
- 11
frappe/public/js/frappe/list/base_list.js 查看文件

@@ -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>


+ 1
- 0
frappe/public/js/frappe/list/list_filter.js 查看文件

@@ -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'],


+ 1
- 1
frappe/public/js/frappe/list/list_sidebar_group_by.js 查看文件

@@ -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>


+ 29
- 19
frappe/public/js/frappe/list/list_view.js 查看文件

@@ -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;
}
});


+ 10
- 6
frappe/public/js/frappe/list/views.js 查看文件

@@ -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);
}
}

+ 45
- 36
frappe/public/js/frappe/model/create_new.js 查看文件

@@ -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) {


+ 30
- 3
frappe/public/js/frappe/model/model.js 查看文件

@@ -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);



+ 3
- 0
frappe/public/js/frappe/model/user_settings.js 查看文件

@@ -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: {


+ 30
- 3
frappe/public/js/frappe/request.js 查看文件

@@ -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;
}


+ 169
- 99
frappe/public/js/frappe/router.js 查看文件

@@ -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);

+ 5
- 3
frappe/public/js/frappe/router_history.js 查看文件

@@ -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({


部分文件因文件數量過多而無法顯示

Loading…
取消
儲存