diff --git a/cypress/support/commands.js b/cypress/support/commands.js index 41b33cc9da..5c6bb6e46f 100644 --- a/cypress/support/commands.js +++ b/cypress/support/commands.js @@ -209,14 +209,14 @@ Cypress.Commands.add('awesomebar', text => { }); Cypress.Commands.add('new_form', doctype => { - let route = `Form/${doctype}/New ${doctype} 1`; + let route = `form/${doctype}/new`; cy.visit(`/app/${route}`); cy.get('body').should('have.attr', 'data-route', route); cy.get('body').should('have.attr', 'data-ajax-state', 'complete'); }); Cypress.Commands.add('go_to_list', doctype => { - cy.visit(`/app/List/${doctype}/List`); + cy.visit(`/app/list/${doctype}/list`); }); Cypress.Commands.add('clear_cache', () => { diff --git a/frappe/core/doctype/doctype/doctype.js b/frappe/core/doctype/doctype/doctype.js index d4fe53aea3..cff918d189 100644 --- a/frappe/core/doctype/doctype/doctype.js +++ b/frappe/core/doctype/doctype/doctype.js @@ -24,12 +24,11 @@ frappe.ui.form.on('DocType', { if (!frm.is_new() && !frm.doc.istable) { if (frm.doc.issingle) { frm.add_custom_button(__('Go to {0}', [frm.doc.name]), () => { - window.open(`/app/Form/${frm.doc.name}`); - // frappe.set_route('Form', frm.doc.name); + window.open(`/app/form/${frm.doc.name}`); }); } else { frm.add_custom_button(__('Go to {0} List', [frm.doc.name]), () => { - window.open(`/app/List/${frm.doc.name}/List`); + window.open(`/app/list/${frm.doc.name}/list`); }); } } diff --git a/frappe/custom/doctype/doctype_layout/__init__.py b/frappe/custom/doctype/doctype_layout/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frappe/custom/doctype/doctype_layout/doctype_layout.js b/frappe/custom/doctype/doctype_layout/doctype_layout.js new file mode 100644 index 0000000000..679330e065 --- /dev/null +++ b/frappe/custom/doctype/doctype_layout/doctype_layout.js @@ -0,0 +1,30 @@ +// Copyright (c) 2020, Frappe Technologies and contributors +// For license information, please see license.txt + +frappe.ui.form.on('DocType Layout', { + refresh: function(frm) { + frm.trigger('document_type'); + frm.events.set_button(frm); + }, + + document_type(frm) { + frm.set_fields_as_options('fields', frm.doc.document_type, null, [], 'fieldname').then(() => { + // child table empty? then show all fields as default + if (frm.doc.document_type) { + if (!(frm.doc.fields || []).length) { + for (let f of frappe.get_doc('DocType', frm.doc.document_type).fields) { + frm.add_child('fields', { fieldname: f.fieldname, label: f.label }); + } + } + } + }); + }, + + set_button(frm) { + if (!frm.is_new()) { + frm.add_custom_button(__('Go to {0} List', [frm.doc.name]), () => { + window.open(`/app/list/${frappe.router.slug(frm.doc.name)}/list`); + }); + } + } +}); diff --git a/frappe/custom/doctype/doctype_layout/doctype_layout.json b/frappe/custom/doctype/doctype_layout/doctype_layout.json new file mode 100644 index 0000000000..420ce09a99 --- /dev/null +++ b/frappe/custom/doctype/doctype_layout/doctype_layout.json @@ -0,0 +1,60 @@ +{ + "actions": [], + "allow_rename": 1, + "autoname": "Prompt", + "creation": "2020-11-16 17:05:35.306846", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "document_type", + "fields", + "client_script" + ], + "fields": [ + { + "fieldname": "document_type", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Document Type", + "options": "DocType", + "reqd": 1 + }, + { + "fieldname": "fields", + "fieldtype": "Table", + "label": "Fields", + "options": "DocType Layout Field", + "reqd": 1 + }, + { + "fieldname": "client_script", + "fieldtype": "Code", + "label": "Client Script" + } + ], + "index_web_pages_for_search": 1, + "links": [], + "modified": "2020-11-17 15:49:49.669291", + "modified_by": "Administrator", + "module": "Custom", + "name": "DocType Layout", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/frappe/custom/doctype/doctype_layout/doctype_layout.py b/frappe/custom/doctype/doctype_layout/doctype_layout.py new file mode 100644 index 0000000000..a8385eaa18 --- /dev/null +++ b/frappe/custom/doctype/doctype_layout/doctype_layout.py @@ -0,0 +1,12 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals + +import frappe +from frappe.model.document import Document + +class DocTypeLayout(Document): + def validate(self): + frappe.cache().delete_value('doctype_name_map') diff --git a/frappe/custom/doctype/doctype_layout/test_doctype_layout.py b/frappe/custom/doctype/doctype_layout/test_doctype_layout.py new file mode 100644 index 0000000000..5765c86262 --- /dev/null +++ b/frappe/custom/doctype/doctype_layout/test_doctype_layout.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies and Contributors +# See license.txt +from __future__ import unicode_literals + +# import frappe +import unittest + +class TestDocTypeLayout(unittest.TestCase): + pass diff --git a/frappe/custom/doctype/doctype_layout_field/__init__.py b/frappe/custom/doctype/doctype_layout_field/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frappe/custom/doctype/doctype_layout_field/doctype_layout_field.json b/frappe/custom/doctype/doctype_layout_field/doctype_layout_field.json new file mode 100644 index 0000000000..a1a36216c3 --- /dev/null +++ b/frappe/custom/doctype/doctype_layout_field/doctype_layout_field.json @@ -0,0 +1,39 @@ +{ + "actions": [], + "creation": "2020-11-16 16:03:43.771801", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "label", + "fieldname" + ], + "fields": [ + { + "fieldname": "fieldname", + "fieldtype": "Select", + "in_list_view": 1, + "label": "Fieldname", + "reqd": 1 + }, + { + "fieldname": "label", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Label", + "reqd": 1 + } + ], + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2020-11-16 17:13:01.892345", + "modified_by": "Administrator", + "module": "Custom", + "name": "DocType Layout Field", + "owner": "Administrator", + "permissions": [], + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/frappe/custom/doctype/doctype_layout_field/doctype_layout_field.py b/frappe/custom/doctype/doctype_layout_field/doctype_layout_field.py new file mode 100644 index 0000000000..7f8c8edfce --- /dev/null +++ b/frappe/custom/doctype/doctype_layout_field/doctype_layout_field.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +# import frappe +from frappe.model.document import Document + +class DocTypeLayoutField(Document): + pass diff --git a/frappe/desk/utils.py b/frappe/desk/utils.py index 6bc59dac29..99318bd54b 100644 --- a/frappe/desk/utils.py +++ b/frappe/desk/utils.py @@ -6,11 +6,23 @@ 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(' ', '-')] = d.name + 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 - return frappe.cache().get_value('doctype_name_map', get_name_map).get(name, name) \ No newline at end of file + 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 \ No newline at end of file diff --git a/frappe/hooks.py b/frappe/hooks.py index b3d7623e0e..67ded8f0cf 100644 --- a/frappe/hooks.py +++ b/frappe/hooks.py @@ -39,7 +39,6 @@ app_include_js = [ app_include_css = [ "/assets/css/desk.min.css", "/assets/css/list.min.css", - "/assets/css/form.min.css", "/assets/css/report.min.css", ] diff --git a/frappe/public/js/frappe/form/form.js b/frappe/public/js/frappe/form/form.js index e8a400a6fe..5423e87984 100644 --- a/frappe/public/js/frappe/form/form.js +++ b/frappe/public/js/frappe/form/form.js @@ -19,9 +19,13 @@ frappe.ui.form.Controller = Class.extend({ }); frappe.ui.form.Form = class FrappeForm { - constructor(doctype, parent, in_form) { + constructor(doctype, parent, in_form, doctype_layout_name) { 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.hidden = false; this.refresh_if_stale_for = 120; @@ -30,7 +34,7 @@ frappe.ui.form.Form = class FrappeForm { this.custom_buttons = {}; this.sections = []; this.grids = []; - this.cscript = new frappe.ui.form.Controller({frm:this}); + this.cscript = new frappe.ui.form.Controller({ frm: this }); this.events = {}; this.pformat = {}; this.fetch_dict = {}; @@ -159,6 +163,7 @@ frappe.ui.form.Form = class FrappeForm { this.layout = new frappe.ui.form.Layout({ parent: this.body, doctype: this.doctype, + doctype_layout: this.doctype_layout, frm: this, with_dashboard: true, card_layout: true, @@ -1693,8 +1698,9 @@ frappe.ui.form.Form = class FrappeForm { // Filters fields from the reference doctype and sets them as options for a Select field set_fields_as_options(fieldname, reference_doctype, filter_function, default_options=[], table_fieldname) { - if (!reference_doctype) return; - let options = default_options; + if (!reference_doctype) return Promise.resolve(); + let options = default_options || []; + if (!filter_function) filter_function = (f) => f; return new Promise(resolve => { frappe.model.with_doctype(reference_doctype, () => { frappe.get_meta(reference_doctype).fields.map(df => { diff --git a/frappe/public/js/frappe/form/formatters.js b/frappe/public/js/frappe/form/formatters.js index 042aebab1d..075d1cec84 100644 --- a/frappe/public/js/frappe/form/formatters.js +++ b/frappe/public/js/frappe/form/formatters.js @@ -86,8 +86,8 @@ frappe.form.formatters = { } }, Check: function(value) { - if(value) { - return `` + if (value) { + return ``; } else { return ``; } diff --git a/frappe/public/js/frappe/form/layout.js b/frappe/public/js/frappe/form/layout.js index 279cbd8511..730f43c16e 100644 --- a/frappe/public/js/frappe/form/layout.js +++ b/frappe/public/js/frappe/form/layout.js @@ -11,52 +11,73 @@ frappe.ui.form.Layout = Class.extend({ $.extend(this, opts); }, make: function() { - if(!this.parent && this.body) { + if (!this.parent && this.body) { this.parent = this.body; } this.wrapper = $('
').appendTo(this.parent); this.message = $('').appendTo(this.wrapper); - if(!this.fields) { + if (!this.fields) { this.fields = this.get_doctype_fields(); } this.setup_tabbing(); this.render(); }, show_empty_form_message: function() { - if(!(this.wrapper.find(".frappe-control:visible").length || this.wrapper.find(".section-head.collapsed").length)) { + if (!(this.wrapper.find(".frappe-control:visible").length || this.wrapper.find(".section-head.collapsed").length)) { this.show_message(__("This form does not have any input")); } }, + get_doctype_fields: function() { let fields = [ - { - parent: this.frm.doctype, - fieldtype: 'Data', - fieldname: '__newname', - reqd: 1, - hidden: 1, - label: __('Name'), - get_status: function(field) { - if (field.frm && field.frm.is_new() - && field.frm.meta.autoname - && ['prompt', 'name'].includes(field.frm.meta.autoname.toLowerCase())) { - return 'Write'; - } - return 'None'; - } - } + this.get_new_name_field() ]; - fields = fields.concat(frappe.meta.sort_docfields(frappe.meta.docfield_map[this.doctype])); + if (this.doctype_layout) { + fields = fields.concat(this.get_fields_from_layout()) + } else { + fields = fields.concat(frappe.meta.sort_docfields(frappe.meta.docfield_map[this.doctype])); + } + return fields; }, + + get_new_name_field() { + return { + parent: this.frm.doctype, + fieldtype: 'Data', + fieldname: '__newname', + reqd: 1, + hidden: 1, + label: __('Name'), + get_status: function(field) { + if (field.frm && field.frm.is_new() + && field.frm.meta.autoname + && ['prompt', 'name'].includes(field.frm.meta.autoname.toLowerCase())) { + return 'Write'; + } + return 'None'; + } + }; + }, + + get_fields_from_layout() { + const fields = []; + for (let f of this.doctype_layout.fields) { + const docfield = copy_dict(frappe.meta.docfield_map[this.doctype][f.fieldname]); + docfield.label = f.label; + fields.push(docfield); + } + return fields; + }, + show_message: function(html, color) { if (this.message_color) { // remove previous color this.message.removeClass(this.message_color); } this.message_color = (color && ['yellow', 'blue'].includes(color)) ? color : 'blue'; - if(html) { - if(html.substr(0, 1)!=='<') { + if (html) { + if (html.substr(0, 1)!=='<') { // wrap in a block html = '
' + html + '
'; } @@ -139,7 +160,7 @@ frappe.ui.form.Layout = Class.extend({ const fieldobj = this.init_field(df, render); this.fields_list.push(fieldobj); this.fields_dict[df.fieldname] = fieldobj; - if(this.frm) { + if (this.frm) { fieldobj.perm = this.frm.perm; } @@ -172,7 +193,7 @@ frappe.ui.form.Layout = Class.extend({ this.fold_btn = head.find(".btn-fold").on("click", function() { var page = $(this).parent().next(); - if(page.hasClass("hide")) { + if (page.hasClass("hide")) { $(this).removeClass("btn-fold").html(__("Hide details")); page.removeClass("hide"); frappe.utils.scroll_to($(this), true, 30); @@ -196,7 +217,7 @@ frappe.ui.form.Layout = Class.extend({ this.section = new frappe.ui.form.Section(this, df); // append to layout fields - if(df) { + if (df) { this.fields_dict[df.fieldname] = this.section; this.fields_list.push(this.section); } @@ -206,14 +227,14 @@ frappe.ui.form.Layout = Class.extend({ make_column: function(df) { this.column = new frappe.ui.form.Column(this.section, df); - if(df && df.fieldname) { + if (df && df.fieldname) { this.fields_list.push(this.column); } }, refresh: function(doc) { var me = this; - if(doc) this.doc = doc; + if (doc) this.doc = doc; if (this.frm) { this.wrapper.find(".empty-form-alert").remove(); @@ -222,7 +243,7 @@ frappe.ui.form.Layout = Class.extend({ // NOTE this might seem redundant at first, but it needs to be executed when frm.refresh_fields is called me.attach_doc_and_docfields(true); - if(this.frm && this.frm.wrapper) { + if (this.frm && this.frm.wrapper) { $(this.frm.wrapper).trigger("refresh-fields"); } @@ -256,13 +277,13 @@ frappe.ui.form.Layout = Class.extend({ refresh_fields: function(fields) { let fieldnames = fields.map((field) => { - if(field.fieldname) return field.fieldname; + if (field.fieldname) return field.fieldname; }); this.fields_list.map(fieldobj => { - if(fieldnames.includes(fieldobj.df.fieldname)) { + if (fieldnames.includes(fieldobj.df.fieldname)) { fieldobj.refresh(); - if(fieldobj.df["default"]) { + if (fieldobj.df["default"]) { fieldobj.set_input(fieldobj.df["default"]); } } @@ -275,15 +296,15 @@ frappe.ui.form.Layout = Class.extend({ }, refresh_section_collapse: function() { - if(!this.doc) return; + if (!this.doc) return; - for(var i=0; i
').appendTo(this.layout.wrapper); } let make_card = this.layout.card_layout && this.df.fieldname !== '_form_dashboard'; @@ -581,15 +602,15 @@ frappe.ui.form.Section = Class.extend({ .appendTo(this.layout.page); this.layout.sections.push(this); - if(this.df) { - if(this.df.label) { + if (this.df) { + if (this.df.label) { this.make_head(); } - if(this.df.description) { + if (this.df.description) { $('
' + __(this.df.description) + '
') .appendTo(this.wrapper); } - if(this.df.cssClass) { + if (this.df.cssClass) { this.wrapper.addClass(this.df.cssClass); } if (this.df.hide_border) { @@ -620,14 +641,14 @@ frappe.ui.form.Section = Class.extend({ } }, refresh: function() { - if(!this.df) + if (!this.df) return; // hide if explictly hidden var hide = this.df.hidden || this.df.hidden_due_to_dependency; // hide if no perm - if(!hide && this.layout && this.layout.frm && !this.layout.frm.get_perm(this.df.permlevel || 0, "read")) { + if (!hide && this.layout && this.layout.frm && !this.layout.frm.get_perm(this.df.permlevel || 0, "read")) { hide = true; } @@ -639,7 +660,7 @@ frappe.ui.form.Section = Class.extend({ return; } - if(hide===undefined) { + if (hide===undefined) { hide = !this.body.hasClass("hide"); } @@ -683,7 +704,7 @@ frappe.ui.form.Section = Class.extend({ frappe.ui.form.Column = Class.extend({ init: function(section, df) { - if(!df) df = {}; + if (!df) df = {}; this.df = df; this.section = section; diff --git a/frappe/public/js/frappe/form/script_manager.js b/frappe/public/js/frappe/form/script_manager.js index 640c64d4fd..55152304f4 100644 --- a/frappe/public/js/frappe/form/script_manager.js +++ b/frappe/public/js/frappe/form/script_manager.js @@ -156,16 +156,22 @@ frappe.ui.form.ScriptManager = Class.extend({ return handlers; }, setup: function() { - var doctype = this.frm.meta; - var me = this; + const doctype = this.frm.meta; + const me = this; + let client_script; - // js - var cs = doctype.__js; - if(cs) { - var tmp = eval(cs); + // process the custom script for this form + if (this.frm.doctype_layout) { + client_script = this.frm.doctype_layout.client_script; + } else { + client_script = doctype.__js; } - if(doctype.__custom_js) { + if (client_script) { + eval(client_script); + } + + if(!this.frm.doctype_layout && doctype.__custom_js) { try { eval(doctype.__custom_js); } catch(e) { diff --git a/frappe/public/js/frappe/form/sidebar/form_sidebar_users.js b/frappe/public/js/frappe/form/sidebar/form_sidebar_users.js index 57e3ed8e3e..a7ea07777b 100644 --- a/frappe/public/js/frappe/form/sidebar/form_sidebar_users.js +++ b/frappe/public/js/frappe/form/sidebar/form_sidebar_users.js @@ -85,7 +85,8 @@ frappe.ui.form.set_users = function(data, type) { current: users }); - if (cur_frm && cur_frm.doc && cur_frm.doc.doctype===doctype && cur_frm.doc.name==docname) { + 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); } }; \ No newline at end of file diff --git a/frappe/public/js/frappe/list/base_list.js b/frappe/public/js/frappe/list/base_list.js index 5050032238..19e793368f 100644 --- a/frappe/public/js/frappe/list/base_list.js +++ b/frappe/public/js/frappe/list/base_list.js @@ -34,7 +34,7 @@ frappe.views.BaseList = class BaseList { setup_defaults() { this.page_name = frappe.get_route_str(); - this.page_title = this.page_title || __(this.doctype); + this.page_title = this.page_title || frappe.router.doctype_layout || __(this.doctype); this.meta = frappe.get_meta(this.doctype); this.settings = frappe.listview_settings[this.doctype] || {}; this.user_settings = frappe.get_user_settings(this.doctype); diff --git a/frappe/public/js/frappe/list/list_view.js b/frappe/public/js/frappe/list/list_view.js index 8e7631f797..0e72a332d1 100644 --- a/frappe/public/js/frappe/list/list_view.js +++ b/frappe/public/js/frappe/list/list_view.js @@ -14,8 +14,8 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { const last_view = user_settings.last_view; frappe.set_route( "list", - frappe.router.slug(doctype), - frappe.views.is_valid(last_view) ? last_view : "list" + frappe.router.doctype_layout || doctype, + frappe.views.is_valid(last_view) ? last_view.toLowerCase() : "list" ); return true; } @@ -232,7 +232,7 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { set_primary_action() { if (this.can_create) { this.page.set_primary_action( - `${__("Add")} ${__(this.doctype)}`, + `${__("Add")} ${frappe.router.doctype_layout || __(this.doctype)}`, () => { if (this.settings.primary_action) { this.settings.primary_action(); @@ -871,7 +871,7 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { ? encodeURIComponent(doc.name) : doc.name; - return "/app/form/" + frappe.router.slug(this.doctype) + "/" + docname; + return "/app/form/" + frappe.router.slug(frappe.router.doctype_layout || this.doctype) + "/" + docname; } get_seen_class(doc) { @@ -1402,7 +1402,7 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { items.push({ label: __("Import"), action: () => - frappe.set_route("List", "Data Import", { + frappe.set_route("list", "data-import", { reference_doctype: doctype, }), standard: true, @@ -1413,7 +1413,7 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { items.push({ label: __("User Permissions"), action: () => - frappe.set_route("List", "User Permission", { + frappe.set_route("list", "user-permission", { allow: doctype, }), standard: true, @@ -1435,9 +1435,9 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { action: () => { if (!this.meta) return; if (this.meta.custom) { - frappe.set_route("Form", "DocType", doctype); + frappe.set_route("form", "doctype", doctype); } else if (!this.meta.custom) { - frappe.set_route("Form", "Customize Form", { + frappe.set_route("form", "customize-form", { doc_type: doctype, }); } @@ -1469,7 +1469,7 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { // edit doctype items.push({ label: __("Edit DocType"), - action: () => frappe.set_route("Form", "DocType", doctype), + action: () => frappe.set_route("form", "doctype", doctype), standard: true, }); } diff --git a/frappe/public/js/frappe/list/views.js b/frappe/public/js/frappe/list/views.js index d14e31f40d..59836d05d1 100644 --- a/frappe/public/js/frappe/list/views.js +++ b/frappe/public/js/frappe/list/views.js @@ -22,7 +22,7 @@ frappe.views.Views = class Views { set_current_view() { this.current_view = 'List'; const route = frappe.get_route(); - const view_name = frappe.utils.to_title_case(route[2] || '') + const view_name = frappe.utils.to_title_case(route[2] || ''); if (route.length > 2 && frappe.views.view_modes.includes(view_name)) { this.current_view = view_name; @@ -34,15 +34,21 @@ frappe.views.Views = class Views { } } + set_route(view, calendar_name) { + const route = ['list', frappe.router.doctype_layout || this.doctype, view]; + if (calendar_name) route.push(calendar_name); + frappe.set_route(route); + } + setup_views() { const views = { 'List': { condition: true, - action: () => frappe.set_route('list', this.doctype, 'list') + action: () => this.set_route('list') }, 'Report': { condition: true, - action: () => frappe.set_route('list', this.doctype, 'report'), + action: () => this.set_route('report'), current_view_handler: () => { const reports = this.get_reports(); this.setup_dropdown_in_sidebar( @@ -50,18 +56,18 @@ frappe.views.Views = class Views { reports, { label: __('Report Builder'), - action: () => frappe.set_route('list', this.doctype, 'report') + action: () => this.set_route('report') } ); } }, 'Dashboard': { condition: true, - action: () => frappe.set_route('list', this.doctype, 'dashboard') + action: () => this.set_route('dashboard') }, 'Calendar': { condition: frappe.views.calendar[this.doctype], - action: () => frappe.set_route('list', this.doctype, 'calendar', 'default'), + action: () => this.set_route('calendar', 'default'), current_view_handler: () => { this.get_calendars().then(calendars => { this.setup_dropdown_in_sidebar( @@ -73,11 +79,11 @@ frappe.views.Views = class Views { }, 'Gantt': { condition: frappe.views.calendar[this.doctype], - action: () => frappe.set_route('list', this.doctype, 'gantt') + action: () => this.set_route('gantt') }, 'Inbox': { condition: this.doctype === "Communication" && frappe.boot.email_accounts.length, - action: () => frappe.set_route('list', this.doctype, 'inbox'), + action: () => this.set_route('inbox'), current_view_handler: () => { const accounts = this.get_email_accounts(); let default_action; @@ -96,11 +102,11 @@ frappe.views.Views = class Views { }, 'Image': { condition: this.list_view.meta.image_field, - action: () => frappe.set_route('list', this.doctype, 'image') + action: () => this.set_route('image') }, 'Tree': { condition: frappe.treeview_settings[this.doctype] || frappe.get_meta(this.doctype).is_tree, - action: () => frappe.set_route('list', this.doctype, 'tree') + action: () => this.set_route('tree') }, 'Kanban': { condition: true, @@ -147,7 +153,7 @@ frappe.views.Views = class Views { `; } else { items.map(item => { - if (item.name == frappe.get_route().slice(-1)[0]) { + if (item.name == frappe.utils.to_title_case(frappe.get_route().slice(-1)[0] || '')) { placeholder = item.name; } html += `
  • ${item.name}
  • `; @@ -206,7 +212,7 @@ frappe.views.Views = class Views { const last_opened_kanban = frappe.model.user_settings[this.doctype]['Kanban'] && frappe.model.user_settings[this.doctype]['Kanban'].last_kanban_board; if (last_opened_kanban) { - frappe.set_route('List', this.doctype, 'kanban', last_opened_kanban); + frappe.set_route('list', this.doctype, 'kanban', last_opened_kanban); } else { frappe.views.KanbanView.show_kanban_dialog(this.doctype, true); } diff --git a/frappe/public/js/frappe/model/create_new.js b/frappe/public/js/frappe/model/create_new.js index 7be7fc5baa..61737d0a7e 100644 --- a/frappe/public/js/frappe/model/create_new.js +++ b/frappe/public/js/frappe/model/create_new.js @@ -80,7 +80,7 @@ $.extend(frappe.model, { if(!cnt[doctype]) cnt[doctype] = 0; cnt[doctype]++; - return __('New') + ' '+ __(doctype) + ' ' + cnt[doctype]; + return frappe.router.slug(`new-${doctype}-${cnt[doctype]}`); }, set_default_values: function(doc, parent_doc) { @@ -170,20 +170,20 @@ $.extend(frappe.model, { // 3 - look in default of docfield if (df['default']) { - - if (df["default"] == "__user" || df["default"].toLowerCase() == "user") { + const default_val = String(df['default']); + if (default_val == "__user" || default_val.toLowerCase() == "user") { return frappe.session.user; - } else if (df["default"] == "user_fullname") { + } else if (default_val == "user_fullname") { return frappe.session.user_fullname; - } else if (df["default"] == "Today") { + } else if (default_val == "Today") { return frappe.datetime.get_today(); - } else if ((df["default"] || "").toLowerCase() === "now") { + } else if ((default_val || "").toLowerCase() === "now") { return frappe.datetime.now_datetime(); - } else if (df["default"][0]===":") { + } 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); diff --git a/frappe/public/js/frappe/model/model.js b/frappe/public/js/frappe/model/model.js index 308d9bd5f8..1bc0731e2f 100644 --- a/frappe/public/js/frappe/model/model.js +++ b/frappe/public/js/frappe/model/model.js @@ -273,6 +273,11 @@ $.extend(frappe.model, { return frappe.boot.treeviews.indexOf(doctype) != -1; }, + is_fresh(doc) { + // returns true if document has been recently loaded (5 seconds ago) + return doc && doc.__last_sync_on && ((new Date() - doc.__last_sync_on)) < 5000; + }, + can_import: function(doctype, frm) { // system manager can always import if(frappe.user_roles.includes("System Manager")) return true; diff --git a/frappe/public/js/frappe/router.js b/frappe/public/js/frappe/router.js index 1b1d3cbdbd..d3fe28dd10 100644 --- a/frappe/public/js/frappe/router.js +++ b/frappe/public/js/frappe/router.js @@ -40,8 +40,9 @@ $('body').on('click', 'a', function(e) { if (e.currentTarget.getAttribute('onclick')) return; const href = e.currentTarget.getAttribute('href'); + if (href==='#') return; - if (href==='#' || href==='') { + if (href==='') { return override(e, '/app'); } @@ -61,6 +62,7 @@ frappe.router = { current_route: null, doctype_names: {}, factory_views: ['form', 'list', 'report', 'tree', 'print'], + layout_mapped: {}, route() { // resolve the route from the URL or hash @@ -91,16 +93,21 @@ frappe.router = { 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]]) { - route[1] = frappe.router.doctype_names[route[1]]; - resolve(); + set_name(); } else { frappe.xcall('frappe.desk.utils.get_doctype_name', {name: route[1]}).then((data) => { - route[1] = frappe.router.doctype_names[route[1]] = data; - resolve(); + frappe.router.doctype_names[route[1]] = data.name_map; + set_name(); }); } } else { diff --git a/frappe/public/js/frappe/ui/toolbar/navbar.html b/frappe/public/js/frappe/ui/toolbar/navbar.html index 0cc2c296db..0c70514539 100644 --- a/frappe/public/js/frappe/ui/toolbar/navbar.html +++ b/frappe/public/js/frappe/ui/toolbar/navbar.html @@ -1,6 +1,6 @@