Ver a proveniência

feat: allow syncing new fields in Doctype Layout (#18408)

* 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
Rohan há 2 anos
committed by GitHub
ascendente
cometimento
c73e2e2219
Não foi encontrada uma chave conhecida para esta assinatura, na base de dados ID da chave GPG: 4AEE18F83AFDEB23
6 ficheiros alterados com 182 adições e 31 eliminações
  1. +14
    -0
      frappe/custom/doctype/custom_field/custom_field.py
  2. +89
    -16
      frappe/custom/doctype/doctype_layout/doctype_layout.js
  3. +6
    -3
      frappe/custom/doctype/doctype_layout/doctype_layout.json
  4. +66
    -0
      frappe/custom/doctype/doctype_layout/doctype_layout.py
  5. +5
    -6
      frappe/public/js/frappe/form/script_manager.js
  6. +2
    -6
      frappe/public/js/frappe/router.js

+ 14
- 0
frappe/custom/doctype/custom_field/custom_field.py Ver ficheiro

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


+ 89
- 16
frappe/custom/doctype/doctype_layout/doctype_layout.js Ver ficheiro

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

+ 6
- 3
frappe/custom/doctype/doctype_layout/doctype_layout.json Ver ficheiro

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

+ 66
- 0
frappe/custom/doctype/doctype_layout/doctype_layout.py Ver ficheiro

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

+ 5
- 6
frappe/public/js/frappe/form/script_manager.js Ver ficheiro

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


+ 2
- 6
frappe/public/js/frappe/router.js Ver ficheiro

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



Carregando…
Cancelar
Guardar