feat: allow syncing new fields in Doctype Layout

This commit is contained in:
Rohan Bansal 2022-09-01 18:43:05 +05:30
parent fbee80f734
commit c2e44754d1
4 changed files with 152 additions and 19 deletions

View file

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

View file

@ -2,31 +2,88 @@
// 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);
},
refresh(frm) {
frm.events.add_buttons(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 });
}
}
}
}
);
if (frm.doc.document_type) {
frm.set_value("fields", []);
frm.events.sync_fields(frm, false);
}
},
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"),
});
}
}
},
});

View file

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

View file

@ -1,11 +1,70 @@
# 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):
layout_fieldnames = {field.fieldname for field in self.fields}
doctype_fields = frappe.get_meta(self.document_type).fields
doctype_fieldnames = {field.fieldname for field in doctype_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))
if field_details.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
self.fields.insert(insert_after.idx, row)
self.fields.pop()
added.append({"idx": insert_after.idx + 1, "fieldname": row.fieldname, "label": row.label})
else:
added.append(row.as_dict())
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