Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: rearranging standard fields in customize form (backport #19822, #20168) #22120

Merged
merged 5 commits into from
Aug 21, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
21 changes: 21 additions & 0 deletions frappe/custom/doctype/custom_field/custom_field.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,27 @@ frappe.ui.form.on("Custom Field", {
frm.toggle_enable("dt", frm.doc.__islocal);
frm.trigger("dt");
frm.toggle_reqd("label", !frm.doc.fieldname);

if (frm.doc.is_system_generated) {
frm.dashboard.add_comment(
__(
"<strong>Warning:</strong> This field is system generated and may be overwritten by a future update. Modify it using {0} instead.",
[
frappe.utils.get_form_link(
"Customize Form",
"Customize Form",
true,
__("Customize Form"),
{
doc_type: frm.doc.dt,
}
),
]
),
"yellow",
true
);
}
},
dt: function (frm) {
if (!frm.doc.dt) {
Expand Down
50 changes: 24 additions & 26 deletions frappe/custom/doctype/customize_form/customize_form.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,14 +49,6 @@ frappe.ui.form.on("Customize Form", {
grid_row.row.addClass("highlight");
}
});

$(frm.wrapper).on("grid-make-sortable", function (e, frm) {
frm.trigger("setup_sortable");
});

$(frm.wrapper).on("grid-move-row", function (e, frm) {
frm.trigger("setup_sortable");
});
},

doc_type: function (frm) {
Expand All @@ -71,7 +63,7 @@ frappe.ui.form.on("Customize Form", {
frm.set_value("doc_type", "");
} else {
frm.refresh();
frm.trigger("setup_sortable");
frm.trigger("add_customize_child_table_button");
frm.trigger("setup_default_views");
}
}
Expand All @@ -87,23 +79,16 @@ frappe.ui.form.on("Customize Form", {
frm.trigger("setup_default_views");
},

setup_sortable: function (frm) {
add_customize_child_table_button: function (frm) {
frm.doc.fields.forEach(function (f) {
if (!f.is_custom_field) {
f._sortable = false;
}
if (!in_list(["Table", "Table MultiSelect"], f.fieldtype)) return;

if (f.fieldtype == "Table") {
frm.add_custom_button(
f.options,
function () {
frm.set_value("doc_type", f.options);
},
__("Customize Child Table")
);
}
frm.add_custom_button(
f.options,
() => frm.set_value("doc_type", f.options),
__("Customize Child Table")
);
});
frm.fields_dict.fields.grid.refresh();
},

refresh: function (frm) {
Expand Down Expand Up @@ -236,10 +221,23 @@ frappe.ui.form.on("Customize Form", {
// can't delete standard fields
frappe.ui.form.on("Customize Form Field", {
before_fields_remove: function (frm, doctype, name) {
var row = frappe.get_doc(doctype, name);
const row = frappe.get_doc(doctype, name);

if (row.is_system_generated) {
frappe.throw(
__(
"Cannot delete system generated field <strong>{0}</strong>. You can hide it instead.",
[__(row.label) || row.fieldname]
)
);
}

if (!(row.is_custom_field || row.__islocal)) {
frappe.msgprint(__("Cannot delete standard field. You can hide it if you want"));
throw "cannot delete standard field";
frappe.throw(
__("Cannot delete standard field <strong>{0}</strong>. You can hide it instead.", [
__(row.label) || row.fieldname,
])
);
}
},
fields_add: function (frm, cdt, cdn) {
Expand Down
53 changes: 44 additions & 9 deletions frappe/custom/doctype/customize_form/customize_form.py
Original file line number Diff line number Diff line change
Expand Up @@ -195,18 +195,47 @@ def set_property_setters(self):
# docfield
for df in self.get("fields"):
meta_df = meta.get("fields", {"fieldname": df.fieldname})
if not meta_df or meta_df[0].get("is_custom_field"):
if not meta_df or not is_standard_or_system_generated_field(meta_df[0]):
continue

self.set_property_setters_for_docfield(meta, df, meta_df)

# action and links
self.set_property_setters_for_actions_and_links(meta)

def set_property_setter_for_field_order(self, meta):
new_order = [df.fieldname for df in self.fields]
existing_order = getattr(meta, "field_order", None)
default_order = [
fieldname for fieldname, df in meta._fields.items() if not getattr(df, "is_custom_field", False)
]

if new_order == default_order:
if existing_order:
delete_property_setter(self.doc_type, "field_order")

return

if existing_order and new_order == json.loads(existing_order):
return

frappe.make_property_setter(
{
"doctype": self.doc_type,
"doctype_or_field": "DocType",
"property": "field_order",
"value": json.dumps(new_order),
},
is_system_generated=False,
)

def set_property_setters_for_doctype(self, meta):
for prop, prop_type in doctype_properties.items():
if self.get(prop) != meta.get(prop):
self.make_property_setter(prop, self.get(prop), prop_type)

self.set_property_setter_for_field_order(meta)

def set_property_setters_for_docfield(self, meta, df, meta_df):
for prop, prop_type in docfield_properties.items():
if prop != "idx" and (df.get(prop) or "") != (meta_df[0].get(prop) or ""):
Expand Down Expand Up @@ -352,12 +381,14 @@ def clear_removed_items(self, doctype, items):

def update_custom_fields(self):
for i, df in enumerate(self.get("fields")):
if df.get("is_custom_field"):
if not frappe.db.exists("Custom Field", {"dt": self.doc_type, "fieldname": df.fieldname}):
self.add_custom_field(df, i)
self.flags.update_db = True
else:
self.update_in_custom_field(df, i)
if is_standard_or_system_generated_field(df):
continue

if not frappe.db.exists("Custom Field", {"dt": self.doc_type, "fieldname": df.fieldname}):
self.add_custom_field(df, i)
self.flags.update_db = True
else:
self.update_in_custom_field(df, i)

self.delete_custom_fields()

Expand All @@ -382,7 +413,7 @@ def add_custom_field(self, df, i):
def update_in_custom_field(self, df, i):
meta = frappe.get_meta(self.doc_type)
meta_df = meta.get("fields", {"fieldname": df.fieldname})
if not (meta_df and meta_df[0].get("is_custom_field")):
if not meta_df or is_standard_or_system_generated_field(meta_df[0]):
# not a custom field
return

Expand Down Expand Up @@ -418,7 +449,7 @@ def delete_custom_fields(self):
}
for fieldname in fields_to_remove:
df = meta.get("fields", {"fieldname": fieldname})[0]
if df.get("is_custom_field"):
if not is_standard_or_system_generated_field(df):
frappe.delete_doc("Custom Field", df.name)

def make_property_setter(
Expand Down Expand Up @@ -563,6 +594,10 @@ def reset_customization(doctype):
frappe.clear_cache(doctype=doctype)


def is_standard_or_system_generated_field(df):
return not df.get("is_custom_field") or df.get("is_system_generated")


doctype_properties = {
"search_fields": "Data",
"title_field": "Data",
Expand Down
34 changes: 34 additions & 0 deletions frappe/custom/doctype/customize_form/test_customize_form.py
Original file line number Diff line number Diff line change
Expand Up @@ -400,3 +400,37 @@ def test_change_to_autoincrement_autoname(self):

with self.assertRaises(frappe.ValidationError):
d.run_method("save_customization")

def test_system_generated_fields(self):
doctype = "Event"
custom_field_name = "custom_test_field"

custom_field = frappe.get_doc("Custom Field", {"dt": doctype, "fieldname": custom_field_name})
custom_field.is_system_generated = 1
custom_field.save()

d = self.get_customize_form(doctype)
custom_field = d.getone("fields", {"fieldname": custom_field_name})
custom_field.description = "Test Description"
d.run_method("save_customization")

property_setter_filters = {
"doc_type": doctype,
"field_name": custom_field_name,
"property": "description",
}
self.assertEqual(
frappe.db.get_value("Property Setter", property_setter_filters, "value"), "Test Description"
)

def test_custom_field_order(self):
# shuffle fields
customize_form = self.get_customize_form(doctype="ToDo")
customize_form.fields.insert(0, customize_form.fields.pop())
customize_form.save_customization()

field_order_property = json.loads(
frappe.db.get_value("Property Setter", {"doc_type": "ToDo", "property": "field_order"}, "value")
)

self.assertEqual(field_order_property, [df.fieldname for df in frappe.get_meta("ToDo").fields])
71 changes: 56 additions & 15 deletions frappe/model/meta.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,13 +134,10 @@ def process(self):
self.init_field_caches()
return

has_custom_fields = self.add_custom_fields()
self.add_custom_fields()
self.apply_property_setters()
self.init_field_caches()

if has_custom_fields:
self.sort_fields()

self.sort_fields()
self.get_valid_columns()
self.set_custom_permissions()
self.add_custom_links_and_actions()
Expand Down Expand Up @@ -362,7 +359,6 @@ def add_custom_fields(self):
return

self.extend("fields", custom_fields)
return True

def apply_property_setters(self):
"""
Expand All @@ -373,11 +369,11 @@ def apply_property_setters(self):
if not frappe.db.table_exists("Property Setter"):
return

property_setters = frappe.db.sql(
"""select * from `tabProperty Setter` where
doc_type=%s""",
(self.name,),
as_dict=1,
property_setters = frappe.db.get_values(
"Property Setter",
filters={"doc_type": self.name},
fieldname="*",
as_dict=True,
)

if not property_setters:
Expand Down Expand Up @@ -453,14 +449,56 @@ def init_field_caches(self):
self._table_fields = self.get("fields", {"fieldtype": ["in", table_fields]})

def sort_fields(self):
"""Sort custom fields on the basis of insert_after"""
"""
Sort fields on the basis of following rules (priority descending):
- `field_order` property setter
- `insert_after` computed based on default order for standard fields
- `insert_after` property for custom fields
"""

if field_order := getattr(self, "field_order", []):
field_order = [fieldname for fieldname in json.loads(field_order) if fieldname in self._fields]

# all fields match, best case scenario
if len(field_order) == len(self.fields):
self._update_fields_based_on_order(field_order)
return

# if the first few standard fields are not in the field order, prepare to prepend them
if self.fields[0].fieldname not in field_order:
fields_to_prepend = []
standard_field_found = False

for fieldname, field in self._fields.items():
if getattr(field, "is_custom_field", False):
# all custom fields from here on
break

if fieldname in field_order:
standard_field_found = True
break

fields_to_prepend.append(fieldname)

if standard_field_found:
field_order = fields_to_prepend + field_order
else:
# worst case scenario, invalidate field_order
field_order = fields_to_prepend

field_order = []
existing_fields = set(field_order) if field_order else False
insert_after_map = {}

for field in self.fields:
for index, field in enumerate(self.fields):
if existing_fields and field.fieldname in existing_fields:
continue

if not getattr(field, "is_custom_field", False):
field_order.append(field.fieldname)
if existing_fields:
# compute insert_after from previous field
insert_after_map.setdefault(self.fields[index - 1].fieldname, []).append(field.fieldname)
else:
field_order.append(field.fieldname)

elif insert_after := getattr(field, "insert_after", None):
insert_after_map.setdefault(insert_after, []).append(field.fieldname)
Expand All @@ -472,6 +510,9 @@ def sort_fields(self):
if insert_after_map:
_update_field_order_based_on_insert_after(field_order, insert_after_map)

self._update_fields_based_on_order(field_order)

def _update_fields_based_on_order(self, field_order):
sorted_fields = []

for idx, fieldname in enumerate(field_order, 1):
Expand Down