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 @@
-
+
diff --git a/frappe/public/js/frappe/views/breadcrumbs.js b/frappe/public/js/frappe/views/breadcrumbs.js
index 961f47c979..cb83384b06 100644
--- a/frappe/public/js/frappe/views/breadcrumbs.js
+++ b/frappe/public/js/frappe/views/breadcrumbs.js
@@ -109,7 +109,7 @@ frappe.breadcrumbs = {
}
let set_list_breadcrumb = (doctype) => {
- if (doctype==="User"
+ if ((doctype==="User" && !frappe.user.has_role('System Manager'))
|| frappe.get_doc('DocType', doctype).issingle) {
// no user listview for non-system managers and single doctypes
} else {
@@ -118,7 +118,7 @@ frappe.breadcrumbs = {
let view = frappe.model.user_settings[breadcrumbs.doctype].last_view || 'Tree';
route = view + '/' + breadcrumbs.doctype;
} else {
- route = 'List/' + breadcrumbs.doctype;
+ route = 'list/' + (frappe.router.doctype_layout || breadcrumbs.doctype);
}
$(`
${doctype}`)
.appendTo($breadcrumbs)
@@ -132,8 +132,8 @@ frappe.breadcrumbs = {
if (breadcrumbs.doctype && frappe.get_route()[0] === "print") {
set_list_breadcrumb(breadcrumbs.doctype);
let docname = frappe.get_route()[2];
- let form_route = `form/${frappe.router.slug(breadcrumbs.doctype)}/${docname}`;
- $(`
${docname}`)
+ let form_route = `/app/form/${frappe.router.slug(breadcrumbs.doctype)}/${docname}`;
+ $(`
${docname}`)
.appendTo($breadcrumbs);
}
diff --git a/frappe/public/js/frappe/views/formview.js b/frappe/public/js/frappe/views/formview.js
index 35769abfcd..c993df335a 100644
--- a/frappe/public/js/frappe/views/formview.js
+++ b/frappe/public/js/frappe/views/formview.js
@@ -5,21 +5,25 @@ frappe.provide('frappe.views.formview');
frappe.views.FormFactory = class FormFactory extends frappe.views.Factory {
make(route) {
- var me = this,
- dt = route[1];
+ var doctype = route[1],
+ doctype_layout = frappe.router.doctype_layout || doctype;
- if(!frappe.views.formview[dt]) {
- frappe.model.with_doctype(dt, function() {
- me.page = frappe.container.add_page("Form/" + dt);
- frappe.views.formview[dt] = me.page;
- me.page.frm = new frappe.ui.form.Form(dt, me.page, true);
- me.show_doc(route);
+ if (!frappe.views.formview[doctype_layout]) {
+ frappe.model.with_doctype(doctype, () => {
+ this.page = frappe.container.add_page("form/" + doctype_layout);
+ frappe.views.formview[doctype_layout] = this.page;
+ this.page.frm = new frappe.ui.form.Form(doctype, this.page, true, frappe.router.doctype_layout);
+ this.show_doc(route);
});
} else {
- me.show_doc(route);
+ this.show_doc(route);
}
- if(!this.initialized) {
+ this.setup_events();
+ }
+
+ setup_events() {
+ if (!this.initialized) {
$(document).on("page-change", function() {
frappe.ui.form.close_grid_form();
});
@@ -34,47 +38,57 @@ frappe.views.FormFactory = class FormFactory extends frappe.views.Factory {
frappe.ui.form.set_users(data, 'typers');
});
}
-
-
this.initialized = true;
}
show_doc(route) {
- var dt = route[1],
- dn = route.slice(2).join("/"),
- me = this;
+ var doctype = route[1],
+ doctype_layout = frappe.router.doctype_layout || doctype,
+ name = route.slice(2).join("/");
- if(frappe.model.new_names[dn]) {
- dn = frappe.model.new_names[dn];
- frappe.set_route("Form", dt, dn);
+ if (frappe.model.new_names[name]) {
+ // document has been renamed, reroute
+ name = frappe.model.new_names[name];
+ frappe.set_route("Form", doctype_layout, name);
return;
}
- frappe.model.with_doc(dt, dn, function(dn, r) {
- if(r && r['403']) return; // not permitted
+ const doc = frappe.get_doc(doctype, name);
+ if (doc && (doc.__islocal || frappe.model.is_recent(doc))) {
+ // is document available and recent?
+ this.render(doctype_layout, name);
+ } else {
+ this.fetch_and_render(doctype, name, doctype_layout);
+ }
+ }
- if(!(locals[dt] && locals[dt][dn])) {
- // doc not found, but starts with New,
- // make a new doc and set it
- var new_str = __("New") + " ";
- if(dn && dn.substr(0, new_str.length)==new_str) {
- var new_name = frappe.model.make_new_doc_and_get_name(dt, true);
- if(new_name===dn) {
- me.load(dt, dn);
- } else {
- frappe.set_route("Form", dt, new_name)
- }
+ fetch_and_render(doctype, name, doctype_layout) {
+ frappe.model.with_doc(doctype, name, (name, r) => {
+ if (r && r['403']) return; // not permitted
+
+ if (!(locals[doctype] && locals[doctype][name])) {
+ if (name && name==='new') {
+ this.render_new_doc(doctype, name, doctype_layout);
} else {
frappe.show_not_found(route);
}
return;
}
- me.load(dt, dn);
+ this.render(doctype_layout, name);
});
}
- load(dt, dn) {
- frappe.container.change_to("Form/" + dt);
- frappe.views.formview[dt].frm.refresh(dn);
+ render_new_doc(doctype, name, doctype_layout) {
+ const new_name = frappe.model.make_new_doc_and_get_name(doctype, true);
+ if (new_name===name) {
+ this.render(doctype_layout, name);
+ } else {
+ frappe.set_route("Form", doctype_layout, new_name);
+ }
+ }
+
+ render(doctype_layout, name) {
+ frappe.container.change_to("form/" + doctype_layout);
+ frappe.views.formview[doctype_layout].frm.refresh(name);
}
}
diff --git a/frappe/public/scss/desk/global.scss b/frappe/public/scss/desk/global.scss
index 024648247a..c82aa3d6f7 100644
--- a/frappe/public/scss/desk/global.scss
+++ b/frappe/public/scss/desk/global.scss
@@ -339,8 +339,8 @@ input[type="checkbox"] {
&.disabled-deselected:before {
- background: #FCFCFD;
- border: 0.5px solid #E2E6E9;
+ background: $gray-50;
+ border: 0.5px solid var(--gray-400);
box-sizing: border-box;
box-shadow: inset 0px 1px 3px rgba(0, 0, 0, 0.1);
border-radius: 4px;
@@ -348,7 +348,7 @@ input[type="checkbox"] {
&.disabled-selected:before {
content: url("data: image/svg+xml;utf8,
");
- background: $gray-50;
+ background: $gray-500;
box-sizing: border-box;
box-shadow: inset 0px 1px 3px rgba(0, 0, 0, 0.1);
border-radius: 4px;