* feat: allow syncing new fields in Doctype Layout * fix: handle new layout syncs differently * fix: ux improvements on doctype layout * fix: append custom layout JS to existing doctype JS * style: linter [skip ci] Co-authored-by: Ankush Menat <ankush@frappe.io>version-14
@@ -102,6 +102,20 @@ class CustomField(Document): | |||||
# delete property setter entries | # delete property setter entries | ||||
frappe.db.delete("Property Setter", {"doc_type": self.dt, "field_name": self.fieldname}) | frappe.db.delete("Property Setter", {"doc_type": self.dt, "field_name": self.fieldname}) | ||||
# update doctype layouts | |||||
doctype_layouts = frappe.get_all( | |||||
"DocType Layout", filters={"document_type": self.dt}, pluck="name" | |||||
) | |||||
for layout in doctype_layouts: | |||||
layout_doc = frappe.get_doc("DocType Layout", layout) | |||||
for field in layout_doc.fields: | |||||
if field.fieldname == self.fieldname: | |||||
layout_doc.remove(field) | |||||
layout_doc.save() | |||||
break | |||||
frappe.clear_cache(doctype=self.dt) | frappe.clear_cache(doctype=self.dt) | ||||
def validate_insert_after(self, meta): | def validate_insert_after(self, meta): | ||||
@@ -2,31 +2,104 @@ | |||||
// For license information, please see license.txt | // For license information, please see license.txt | ||||
frappe.ui.form.on("DocType Layout", { | frappe.ui.form.on("DocType Layout", { | ||||
refresh: function (frm) { | |||||
frm.trigger("document_type"); | |||||
frm.events.set_button(frm); | |||||
onload_post_render(frm) { | |||||
// disallow users from manually adding/deleting rows; this doctype should only | |||||
// be used for managing layout, and docfields and custom fields should be used | |||||
// to manage other field metadata (hidden, etc.) | |||||
frm.set_df_property("fields", "cannot_add_rows", true); | |||||
frm.set_df_property("fields", "cannot_delete_rows", true); | |||||
$(frm.wrapper).on("grid-move-row", (e, frm) => { | |||||
// refresh the layout after moving a row | |||||
frm.dirty(); | |||||
}); | |||||
}, | }, | ||||
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 }); | |||||
} | |||||
} | |||||
} | |||||
refresh(frm) { | |||||
frm.events.add_buttons(frm); | |||||
}, | |||||
async document_type(frm) { | |||||
if (frm.doc.document_type) { | |||||
// refreshing the doctype fields resets the new name input field; | |||||
// once the fields are set, reset the name to the original input | |||||
if (frm.is_new()) { | |||||
const document_name = frm.doc.__newname || frm.doc.name; | |||||
} | |||||
frm.set_value("fields", []); | |||||
await frm.events.sync_fields(frm, false); | |||||
if (frm.is_new()) { | |||||
frm.doc.__newname = document_name; | |||||
frm.refresh_field("__newname"); | |||||
} | } | ||||
); | |||||
} | |||||
}, | }, | ||||
set_button(frm) { | |||||
add_buttons(frm) { | |||||
if (!frm.is_new()) { | if (!frm.is_new()) { | ||||
frm.add_custom_button(__("Go to {0} List", [frm.doc.name]), () => { | frm.add_custom_button(__("Go to {0} List", [frm.doc.name]), () => { | ||||
window.open(`/app/${frappe.router.slug(frm.doc.name)}`); | window.open(`/app/${frappe.router.slug(frm.doc.name)}`); | ||||
}); | }); | ||||
frm.add_custom_button(__("Sync {0} Fields", [frm.doc.name]), async () => { | |||||
await frm.events.sync_fields(frm, true); | |||||
}); | |||||
} | |||||
}, | |||||
async sync_fields(frm, notify) { | |||||
frappe.dom.freeze("Fetching fields..."); | |||||
const response = await frm.call({ doc: frm.doc, method: "sync_fields" }); | |||||
frm.refresh_field("fields"); | |||||
frappe.dom.unfreeze(); | |||||
if (!response.message) { | |||||
frappe.msgprint(__("No changes to sync")); | |||||
return; | |||||
} | |||||
frm.dirty(); | |||||
if (notify) { | |||||
const addedFields = response.message.added; | |||||
const removedFields = response.message.removed; | |||||
const getChangedMessage = (fields) => { | |||||
let changes = ""; | |||||
for (const field of fields) { | |||||
if (field.label) { | |||||
changes += `<li>Row #${field.idx}: ${field.fieldname.bold()} (${ | |||||
field.label | |||||
})</li>`; | |||||
} else { | |||||
changes += `<li>Row #${field.idx}: ${field.fieldname.bold()}</li>`; | |||||
} | |||||
} | |||||
return changes; | |||||
}; | |||||
let message = ""; | |||||
if (addedFields.length) { | |||||
message += `The following fields have been added:<br><br><ul>${getChangedMessage( | |||||
addedFields | |||||
)}</ul>`; | |||||
} | |||||
if (removedFields.length) { | |||||
message += `The following fields have been removed:<br><br><ul>${getChangedMessage( | |||||
removedFields | |||||
)}</ul>`; | |||||
} | |||||
if (message) { | |||||
frappe.msgprint({ | |||||
message: __(message), | |||||
indicator: "green", | |||||
title: __("Synced Fields"), | |||||
}); | |||||
} | |||||
} | } | ||||
}, | }, | ||||
}); | }); |
@@ -1,7 +1,7 @@ | |||||
{ | { | ||||
"actions": [], | "actions": [], | ||||
"allow_rename": 1, | "allow_rename": 1, | ||||
"autoname": "Prompt", | |||||
"autoname": "prompt", | |||||
"creation": "2020-11-16 17:05:35.306846", | "creation": "2020-11-16 17:05:35.306846", | ||||
"doctype": "DocType", | "doctype": "DocType", | ||||
"editable_grid": 1, | "editable_grid": 1, | ||||
@@ -19,7 +19,8 @@ | |||||
"in_list_view": 1, | "in_list_view": 1, | ||||
"label": "Document Type", | "label": "Document Type", | ||||
"options": "DocType", | "options": "DocType", | ||||
"reqd": 1 | |||||
"reqd": 1, | |||||
"set_only_once": 1 | |||||
}, | }, | ||||
{ | { | ||||
"fieldname": "fields", | "fieldname": "fields", | ||||
@@ -42,10 +43,11 @@ | |||||
], | ], | ||||
"index_web_pages_for_search": 1, | "index_web_pages_for_search": 1, | ||||
"links": [], | "links": [], | ||||
"modified": "2020-12-10 15:01:04.352184", | |||||
"modified": "2022-09-01 03:22:33.973058", | |||||
"modified_by": "Administrator", | "modified_by": "Administrator", | ||||
"module": "Custom", | "module": "Custom", | ||||
"name": "DocType Layout", | "name": "DocType Layout", | ||||
"naming_rule": "Set by user", | |||||
"owner": "Administrator", | "owner": "Administrator", | ||||
"permissions": [ | "permissions": [ | ||||
{ | { | ||||
@@ -68,5 +70,6 @@ | |||||
"route": "doctype-layout", | "route": "doctype-layout", | ||||
"sort_field": "modified", | "sort_field": "modified", | ||||
"sort_order": "DESC", | "sort_order": "DESC", | ||||
"states": [], | |||||
"track_changes": 1 | "track_changes": 1 | ||||
} | } |
@@ -1,11 +1,77 @@ | |||||
# Copyright (c) 2020, Frappe Technologies and contributors | # Copyright (c) 2020, Frappe Technologies and contributors | ||||
# License: MIT. See LICENSE | # License: MIT. See LICENSE | ||||
from typing import TYPE_CHECKING | |||||
import frappe | |||||
from frappe.desk.utils import slug | from frappe.desk.utils import slug | ||||
from frappe.model.document import Document | from frappe.model.document import Document | ||||
if TYPE_CHECKING: | |||||
from frappe.core.doctype.docfield.docfield import DocField | |||||
class DocTypeLayout(Document): | class DocTypeLayout(Document): | ||||
def validate(self): | def validate(self): | ||||
if not self.route: | if not self.route: | ||||
self.route = slug(self.name) | self.route = slug(self.name) | ||||
@frappe.whitelist() | |||||
def sync_fields(self): | |||||
doctype_fields = frappe.get_meta(self.document_type).fields | |||||
if self.is_new(): | |||||
added_fields = [field.fieldname for field in doctype_fields] | |||||
removed_fields = [] | |||||
else: | |||||
doctype_fieldnames = {field.fieldname for field in doctype_fields} | |||||
layout_fieldnames = {field.fieldname for field in self.fields} | |||||
added_fields = list(doctype_fieldnames - layout_fieldnames) | |||||
removed_fields = list(layout_fieldnames - doctype_fieldnames) | |||||
if not (added_fields or removed_fields): | |||||
return | |||||
added = self.add_fields(added_fields, doctype_fields) | |||||
removed = self.remove_fields(removed_fields) | |||||
for index, field in enumerate(self.fields): | |||||
field.idx = index + 1 | |||||
return {"added": added, "removed": removed} | |||||
def add_fields(self, added_fields: list[str], doctype_fields: list["DocField"]) -> list[dict]: | |||||
added = [] | |||||
for field in added_fields: | |||||
field_details = next((f for f in doctype_fields if f.fieldname == field), None) | |||||
if not field_details: | |||||
continue | |||||
# remove 'doctype' data from the DocField to allow adding it to the layout | |||||
row = self.append("fields", field_details.as_dict(no_default_fields=True)) | |||||
row_data = row.as_dict() | |||||
if field_details.get("insert_after"): | |||||
insert_after = next( | |||||
(f for f in self.fields if f.fieldname == field_details.insert_after), | |||||
None, | |||||
) | |||||
# initialize new row to just after the insert_after field | |||||
if insert_after: | |||||
self.fields.insert(insert_after.idx, row) | |||||
self.fields.pop() | |||||
row_data = {"idx": insert_after.idx + 1, "fieldname": row.fieldname, "label": row.label} | |||||
added.append(row_data) | |||||
return added | |||||
def remove_fields(self, removed_fields: list[str]) -> list[dict]: | |||||
removed = [] | |||||
for field in removed_fields: | |||||
field_details = next((f for f in self.fields if f.fieldname == field), None) | |||||
if field_details: | |||||
self.remove(field_details) | |||||
removed.append(field_details.as_dict()) | |||||
return removed |
@@ -167,13 +167,12 @@ frappe.ui.form.ScriptManager = class ScriptManager { | |||||
setup() { | setup() { | ||||
const doctype = this.frm.meta; | const doctype = this.frm.meta; | ||||
const me = this; | const me = this; | ||||
let client_script; | |||||
let client_script = doctype.__js; | |||||
// 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; | |||||
// append the custom script for this form's layout | |||||
if (this.frm.doctype_layout?.client_script) { | |||||
// add a newline to avoid conflict with doctype JS | |||||
client_script += `\n${this.frm.doctype_layout.client_script}`; | |||||
} | } | ||||
if (client_script) { | if (client_script) { | ||||
@@ -190,12 +190,8 @@ frappe.router = { | |||||
} else { | } else { | ||||
route = ["List", doctype_route.doctype, "List"]; | route = ["List", doctype_route.doctype, "List"]; | ||||
} | } | ||||
if (doctype_route.doctype_layout) { | |||||
// set the layout | |||||
this.doctype_layout = doctype_route.doctype_layout; | |||||
} | |||||
// reset the layout to avoid using incorrect views | |||||
this.doctype_layout = doctype_route.doctype_layout; | |||||
return route; | return route; | ||||
}, | }, | ||||