Merge pull request #11942 from rmehta/doctype-layout

feat(doctype-layout): Ability to add different layouts to doctypes
This commit is contained in:
Rushabh Mehta 2020-11-18 14:48:00 +05:30 committed by GitHub
commit a0effc0338
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
27 changed files with 414 additions and 177 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -86,8 +86,8 @@ frappe.form.formatters = {
}
},
Check: function(value) {
if(value) {
return `<input type="checkbox" class="disabled-selected">`
if (value) {
return `<input type="checkbox" class="disabled-selected">`;
} else {
return `<input type="checkbox" class="disabled-deselected">`;
}

View file

@ -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 = $('<div class="form-layout">').appendTo(this.parent);
this.message = $('<div class="form-message hidden"></div>').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 = '<div>' + html + '</div>';
}
@ -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<this.sections.length; i++) {
for (var i=0; i<this.sections.length; i++) {
var section = this.sections[i];
var df = section.df;
if(df && df.collapsible) {
if (df && df.collapsible) {
var collapse = true;
if(df.collapsible_depends_on) {
if (df.collapsible_depends_on) {
collapse = !this.evaluate_depends_on_value(df.collapsible_depends_on);
}
@ -291,7 +312,7 @@ frappe.ui.form.Layout = Class.extend({
collapse = false;
}
if(df.fieldname === '_form_dashboard') {
if (df.fieldname === '_form_dashboard') {
collapse = localStorage.getItem('collapseFormDashboard')==='yes' ? true : false;
}
@ -302,9 +323,9 @@ frappe.ui.form.Layout = Class.extend({
attach_doc_and_docfields: function(refresh) {
var me = this;
for(var i=0, l=this.fields_list.length; i<l; i++) {
for (var i=0, l=this.fields_list.length; i<l; i++) {
var fieldobj = this.fields_list[i];
if(me.doc) {
if (me.doc) {
fieldobj.doc = me.doc;
fieldobj.doctype = me.doc.doctype;
fieldobj.docname = me.doc.name;
@ -312,7 +333,7 @@ frappe.ui.form.Layout = Class.extend({
fieldobj.df.fieldname, me.frm ? me.frm.doc.name : me.doc.name) || fieldobj.df;
// on form change, permissions can change
if(me.frm) {
if (me.frm) {
fieldobj.perm = me.frm.perm;
}
}
@ -328,11 +349,11 @@ frappe.ui.form.Layout = Class.extend({
setup_tabbing: function() {
var me = this;
this.wrapper.on("keydown", function(ev) {
if(ev.which==9) {
if (ev.which==9) {
var current = $(ev.target),
doctype = current.attr("data-doctype"),
fieldname = current.attr("data-fieldname");
if(doctype)
if (doctype)
return me.handle_tab(doctype, fieldname, ev.shiftKey);
}
});
@ -346,25 +367,25 @@ frappe.ui.form.Layout = Class.extend({
focused = false;
// in grid
if(doctype != me.doctype) {
if (doctype != me.doctype) {
grid_row = me.get_open_grid_row();
if(!grid_row || !grid_row.layout) {
if (!grid_row || !grid_row.layout) {
return;
}
fields = grid_row.layout.fields_list;
}
for(var i=0, len=fields.length; i < len; i++) {
if(fields[i].df.fieldname==fieldname) {
if(shift) {
if(prev) {
for (var i=0, len=fields.length; i < len; i++) {
if (fields[i].df.fieldname==fieldname) {
if (shift) {
if (prev) {
this.set_focus(prev);
} else {
$(this.primary_button).focus();
}
break;
}
if(i < len-1) {
if (i < len-1) {
focused = me.focus_on_next_field(i, fields);
}
@ -372,15 +393,15 @@ frappe.ui.form.Layout = Class.extend({
break;
}
}
if(this.is_visible(fields[i]))
if (this.is_visible(fields[i]))
prev = fields[i];
}
if (!focused) {
// last field in this group
if(grid_row) {
if (grid_row) {
// in grid
if(grid_row.doc.idx==grid_row.grid.grid_rows.length) {
if (grid_row.doc.idx==grid_row.grid.grid_rows.length) {
// last row, close it and find next field
grid_row.toggle_view(false, function() {
grid_row.grid.frm.layout.handle_tab(grid_row.grid.df.parent, grid_row.grid.df.fieldname);
@ -398,12 +419,12 @@ frappe.ui.form.Layout = Class.extend({
},
focus_on_next_field: function(start_idx, fields) {
// loop to find next eligible fields
for(var i= start_idx + 1, len = fields.length; i < len; i++) {
for (var i= start_idx + 1, len = fields.length; i < len; i++) {
var field = fields[i];
if(this.is_visible(field)) {
if(field.df.fieldtype==="Table") {
if (this.is_visible(field)) {
if (field.df.fieldtype==="Table") {
// open table grid
if(!(field.grid.grid_rows && field.grid.grid_rows.length)) {
if (!(field.grid.grid_rows && field.grid.grid_rows.length)) {
// empty grid, add a new row
field.grid.add_new_row();
}
@ -411,7 +432,7 @@ frappe.ui.form.Layout = Class.extend({
field.grid.grid_rows[0].show_form();
return true;
} else if(!in_list(frappe.model.no_value_type, field.df.fieldtype)) {
} else if (!in_list(frappe.model.no_value_type, field.df.fieldtype)) {
this.set_focus(field);
return true;
}
@ -423,15 +444,15 @@ frappe.ui.form.Layout = Class.extend({
},
set_focus: function(field) {
// next is table, show the table
if(field.df.fieldtype=="Table") {
if(!field.grid.grid_rows.length) {
if (field.df.fieldtype=="Table") {
if (!field.grid.grid_rows.length) {
field.grid.add_new_row(1);
} else {
field.grid.grid_rows[0].toggle_view(true);
}
} else if(field.editor) {
} else if (field.editor) {
field.editor.set_focus();
} else if(field.$input) {
} else if (field.$input) {
field.$input.focus();
}
},
@ -466,12 +487,12 @@ frappe.ui.form.Layout = Class.extend({
// show / hide
if (f.guardian_has_value) {
if(f.df.hidden_due_to_dependency) {
if (f.df.hidden_due_to_dependency) {
f.df.hidden_due_to_dependency = false;
f.refresh();
}
} else {
if(!f.df.hidden_due_to_dependency) {
if (!f.df.hidden_due_to_dependency) {
f.df.hidden_due_to_dependency = true;
f.refresh();
}
@ -521,27 +542,27 @@ frappe.ui.form.Layout = Class.extend({
var parent = this.frm ? this.frm.doc : this.doc || null;
if(typeof(expression) === 'boolean') {
if (typeof(expression) === 'boolean') {
out = expression;
} else if(typeof(expression) === 'function') {
} else if (typeof(expression) === 'function') {
out = expression(doc);
} else if(expression.substr(0,5)=='eval:') {
} else if (expression.substr(0,5)=='eval:') {
try {
out = eval(expression.substr(5));
if(parent && parent.istable && expression.includes('is_submittable')) {
if (parent && parent.istable && expression.includes('is_submittable')) {
out = true;
}
} catch(e) {
frappe.throw(__('Invalid "depends_on" expression'));
}
} else if(expression.substr(0,3)=='fn:' && this.frm) {
} else if (expression.substr(0,3)=='fn:' && this.frm) {
out = this.frm.script_manager.trigger(expression.substr(3), this.doctype, this.docname);
} else {
var value = doc[expression];
if($.isArray(value)) {
if ($.isArray(value)) {
out = !!value.length;
} else {
out = !!value;
@ -560,7 +581,7 @@ frappe.ui.form.Section = Class.extend({
this.fields_dict = {};
this.make();
// if(this.frm)
// if (this.frm)
// this.section.body.css({"padding":"0px 3%"})
this.row = {
wrapper: this.wrapper
@ -573,7 +594,7 @@ frappe.ui.form.Section = Class.extend({
this.refresh();
},
make: function() {
if(!this.layout.page) {
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';
@ -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) {
$('<div class="col-sm-12 small text-muted form-section-description">' + __(this.df.description) + '</div>')
.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;

View file

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

View file

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

View file

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

View file

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

View file

@ -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 {
</div>`;
} 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 += `<li><a class="dropdown-item" href="#${item.route}">${item.name}</a></li>`;
@ -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);
}

View file

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

View file

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

View file

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

View file

@ -1,6 +1,6 @@
<header class="navbar navbar-expand sticky-top" role="navigation">
<div class="container">
<a class="navbar-brand navbar-home" href="#">
<a class="navbar-brand navbar-home" href="/app">
<img class="app-logo" style="width: {{ navbar_settings.logo_width || 24 }}px" src="{{ frappe.app.logo_url }}">
</a>
<ul class="nav navbar-nav d-none d-sm-flex" id="navbar-breadcrumbs"></ul>

View file

@ -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);
}
$(`<li><a href="/app/${route}">${doctype}</a></li>`)
.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}`;
$(`<li><a href="#${form_route}">${docname}</a></li>`)
let form_route = `/app/form/${frappe.router.slug(breadcrumbs.doctype)}/${docname}`;
$(`<li><a href="${form_route}">${docname}</a></li>`)
.appendTo($breadcrumbs);
}

View file

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

View file

@ -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, <svg width='8' height='7' viewBox='0 0 8 7' fill='none' xmlns='http://www.w3.org/2000/svg'><path d='M1 4.00001L2.66667 5.80001L7 1.20001' stroke='white' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/></svg>");
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;