* 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 | |||
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) | |||
def validate_insert_after(self, meta): | |||
@@ -2,31 +2,104 @@ | |||
// 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); | |||
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()) { | |||
frm.add_custom_button(__("Go to {0} List", [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": [], | |||
"allow_rename": 1, | |||
"autoname": "Prompt", | |||
"autoname": "prompt", | |||
"creation": "2020-11-16 17:05:35.306846", | |||
"doctype": "DocType", | |||
"editable_grid": 1, | |||
@@ -19,7 +19,8 @@ | |||
"in_list_view": 1, | |||
"label": "Document Type", | |||
"options": "DocType", | |||
"reqd": 1 | |||
"reqd": 1, | |||
"set_only_once": 1 | |||
}, | |||
{ | |||
"fieldname": "fields", | |||
@@ -42,10 +43,11 @@ | |||
], | |||
"index_web_pages_for_search": 1, | |||
"links": [], | |||
"modified": "2020-12-10 15:01:04.352184", | |||
"modified": "2022-09-01 03:22:33.973058", | |||
"modified_by": "Administrator", | |||
"module": "Custom", | |||
"name": "DocType Layout", | |||
"naming_rule": "Set by user", | |||
"owner": "Administrator", | |||
"permissions": [ | |||
{ | |||
@@ -68,5 +70,6 @@ | |||
"route": "doctype-layout", | |||
"sort_field": "modified", | |||
"sort_order": "DESC", | |||
"states": [], | |||
"track_changes": 1 | |||
} |
@@ -1,11 +1,77 @@ | |||
# Copyright (c) 2020, Frappe Technologies and contributors | |||
# License: MIT. See LICENSE | |||
from typing import TYPE_CHECKING | |||
import frappe | |||
from frappe.desk.utils import slug | |||
from frappe.model.document import Document | |||
if TYPE_CHECKING: | |||
from frappe.core.doctype.docfield.docfield import DocField | |||
class DocTypeLayout(Document): | |||
def validate(self): | |||
if not self.route: | |||
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() { | |||
const doctype = this.frm.meta; | |||
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) { | |||
@@ -190,12 +190,8 @@ frappe.router = { | |||
} else { | |||
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; | |||
}, | |||