Merge pull request #38567 from frappe/38159-allow-bulk-edit-in-child-tables
feat: allow Bulk Edit in Child Table
This commit is contained in:
commit
e6daaa6c48
7 changed files with 256 additions and 0 deletions
|
|
@ -111,4 +111,76 @@ context("Grid", () => {
|
|||
cy.get("@table-form").find(".grid-footer-toolbar").click();
|
||||
});
|
||||
});
|
||||
|
||||
it("shows edit button only when child table allow_bulk_edit is enabled", () => {
|
||||
cy.visit("/desk/contact/Test Contact");
|
||||
cy.get('.frappe-control[data-fieldname="phone_nos"]').as("table");
|
||||
|
||||
cy.window()
|
||||
.its("cur_frm")
|
||||
.then((frm) => {
|
||||
const grid = frm.get_field("phone_nos").grid;
|
||||
grid.meta.allow_bulk_edit = false;
|
||||
grid.refresh_edit_rows_button();
|
||||
});
|
||||
|
||||
cy.get("@table").find('.grid-row[data-idx="1"] .grid-row-check').click({ force: true });
|
||||
cy.get("@table").find(".grid-edit-rows").should("have.class", "hidden");
|
||||
|
||||
cy.window()
|
||||
.its("cur_frm")
|
||||
.then((frm) => {
|
||||
const grid = frm.get_field("phone_nos").grid;
|
||||
grid.meta.allow_bulk_edit = true;
|
||||
grid.refresh_edit_rows_button();
|
||||
});
|
||||
|
||||
cy.get("@table").find(".grid-edit-rows").should("not.have.class", "hidden");
|
||||
});
|
||||
|
||||
it("bulk edit updates only selected child rows", () => {
|
||||
const updated_phone = `99999${Date.now().toString().slice(-5)}`;
|
||||
|
||||
cy.visit("/desk/contact/Test Contact");
|
||||
cy.get('.frappe-control[data-fieldname="phone_nos"]').as("table");
|
||||
|
||||
cy.window()
|
||||
.its("cur_frm")
|
||||
.then((frm) => {
|
||||
const grid = frm.get_field("phone_nos").grid;
|
||||
grid.meta.allow_bulk_edit = true;
|
||||
grid.refresh_edit_rows_button();
|
||||
|
||||
expect(frm.doc.phone_nos.length).to.be.greaterThan(1);
|
||||
const phone_df = grid.docfields.find((df) => df.fieldname === "phone");
|
||||
expect(phone_df).to.exist;
|
||||
cy.wrap(phone_df.label).as("phoneFieldLabel");
|
||||
cy.wrap(frm.doc.phone_nos[1].phone || "").as("secondRowPhoneBefore");
|
||||
});
|
||||
|
||||
cy.get("@table").find('.grid-row[data-idx="1"] .grid-row-check').click({ force: true });
|
||||
cy.get("@table").find(".grid-edit-rows").click({ force: true });
|
||||
|
||||
cy.window()
|
||||
.its("cur_dialog")
|
||||
.then((dialog) => {
|
||||
cy.get("@phoneFieldLabel").then((phoneFieldLabel) => {
|
||||
return dialog
|
||||
.set_value("field", phoneFieldLabel)
|
||||
.then(() => dialog.set_value("value", updated_phone))
|
||||
.then(() => {
|
||||
dialog.get_primary_btn().click();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
cy.window().its("cur_frm.doc.phone_nos.0.phone").should("eq", updated_phone);
|
||||
cy.window()
|
||||
.its("cur_frm")
|
||||
.then((frm) => {
|
||||
cy.get("@secondRowPhoneBefore").then((secondRowPhoneBefore) => {
|
||||
expect(frm.doc.phone_nos[1].phone || "").to.equal(secondRowPhoneBefore);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
{
|
||||
"actions": [],
|
||||
"allow_bulk_edit": 1,
|
||||
"allow_rename": 1,
|
||||
"autoname": "Prompt",
|
||||
"creation": "2013-02-18 13:36:19",
|
||||
|
|
@ -34,6 +35,7 @@
|
|||
"quick_entry",
|
||||
"grid_page_length",
|
||||
"rows_threshold_for_grid_search",
|
||||
"allow_bulk_edit",
|
||||
"cb01",
|
||||
"track_changes",
|
||||
"track_seen",
|
||||
|
|
@ -715,6 +717,14 @@
|
|||
"fieldname": "recipient_account_field",
|
||||
"fieldtype": "Data",
|
||||
"label": "Recipient Account Field"
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
"depends_on": "istable",
|
||||
"description": "Enable bulk update of this field across child table rows.",
|
||||
"fieldname": "allow_bulk_edit",
|
||||
"fieldtype": "Check",
|
||||
"label": "Allow Bulk Edit"
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
|
|
|
|||
|
|
@ -101,6 +101,7 @@ class DocType(Document):
|
|||
|
||||
actions: DF.Table[DocTypeAction]
|
||||
allow_auto_repeat: DF.Check
|
||||
allow_bulk_edit: DF.Check
|
||||
allow_copy: DF.Check
|
||||
allow_events_in_timeline: DF.Check
|
||||
allow_guest_to_view: DF.Check
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@
|
|||
"track_views",
|
||||
"allow_auto_repeat",
|
||||
"allow_import",
|
||||
"allow_bulk_edit",
|
||||
"queue_in_background",
|
||||
"naming_section",
|
||||
"naming_rule",
|
||||
|
|
@ -222,6 +223,14 @@
|
|||
"fieldtype": "Check",
|
||||
"label": "Allow Import (via Data Import Tool)"
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
"depends_on": "istable",
|
||||
"description": "Enable bulk edit for child table fields in Form view.",
|
||||
"fieldname": "allow_bulk_edit",
|
||||
"fieldtype": "Check",
|
||||
"label": "Allow Bulk Edit"
|
||||
},
|
||||
{
|
||||
"depends_on": "email_append_to",
|
||||
"fieldname": "subject_field",
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@ class CustomizeForm(Document):
|
|||
|
||||
actions: DF.Table[DocTypeAction]
|
||||
allow_auto_repeat: DF.Check
|
||||
allow_bulk_edit: DF.Check
|
||||
allow_copy: DF.Check
|
||||
allow_import: DF.Check
|
||||
autoname: DF.Data | None
|
||||
|
|
@ -744,6 +745,7 @@ doctype_properties = {
|
|||
"track_views": "Check",
|
||||
"allow_auto_repeat": "Check",
|
||||
"allow_import": "Check",
|
||||
"allow_bulk_edit": "Check",
|
||||
"show_name_in_global_search": "Check",
|
||||
"show_preview_popup": "Check",
|
||||
"default_email_template": "Data",
|
||||
|
|
|
|||
|
|
@ -87,6 +87,10 @@ export default class Grid {
|
|||
data-action="delete_rows">
|
||||
${__("Delete")}
|
||||
</button>
|
||||
<button type="button" class="btn btn-xs btn-secondary grid-edit-rows hidden"
|
||||
data-action="bulk_edit_rows">
|
||||
${__("Edit")}
|
||||
</button>
|
||||
<button type="button" class="btn btn-xs btn-danger grid-remove-all-rows hidden"
|
||||
data-action="delete_all_rows">
|
||||
${__("Delete all")}
|
||||
|
|
@ -148,6 +152,7 @@ export default class Grid {
|
|||
this.grid_buttons = this.wrapper.find(".grid-buttons");
|
||||
this.grid_custom_buttons = this.wrapper.find(".grid-custom-buttons");
|
||||
this.remove_rows_button = this.grid_buttons.find(".grid-remove-rows");
|
||||
this.edit_rows_button = this.grid_buttons.find(".grid-edit-rows");
|
||||
this.duplicate_rows_button = this.grid_buttons.find(".grid-duplicate-rows");
|
||||
this.remove_all_rows_button = this.grid_buttons.find(".grid-remove-all-rows");
|
||||
|
||||
|
|
@ -253,13 +258,16 @@ export default class Grid {
|
|||
// update "Delete" and "Duplicate" button labels
|
||||
if (num_selected_rows == 1) {
|
||||
this.remove_rows_button.text(__("Delete row"));
|
||||
this.edit_rows_button.text(__("Edit row"));
|
||||
this.duplicate_rows_button.text(__("Duplicate row"));
|
||||
} else {
|
||||
this.remove_rows_button.text(__("Delete {0} rows", [num_selected_rows]));
|
||||
this.edit_rows_button.text(__("Edit {0} rows", [num_selected_rows]));
|
||||
this.duplicate_rows_button.text(__("Duplicate {0} rows", [num_selected_rows]));
|
||||
}
|
||||
|
||||
this.refresh_remove_rows_button();
|
||||
this.refresh_edit_rows_button();
|
||||
this.refresh_duplicate_rows_button();
|
||||
});
|
||||
}
|
||||
|
|
@ -386,6 +394,18 @@ export default class Grid {
|
|||
}
|
||||
}
|
||||
|
||||
refresh_edit_rows_button() {
|
||||
if (!this.meta?.allow_bulk_edit) {
|
||||
this.edit_rows_button.toggleClass("hidden", true);
|
||||
return;
|
||||
}
|
||||
|
||||
const show_button = this.wrapper.find(".grid-body .grid-row-check:checked:first").length
|
||||
? true
|
||||
: false;
|
||||
this.edit_rows_button.toggleClass("hidden", !show_button);
|
||||
}
|
||||
|
||||
debounced_refresh_remove_rows_button = frappe.utils.debounce(
|
||||
this.refresh_remove_rows_button,
|
||||
100
|
||||
|
|
@ -547,6 +567,7 @@ export default class Grid {
|
|||
this.form_grid.toggleClass("error", !!(this.df.reqd && !(this.data && this.data.length)));
|
||||
|
||||
this.refresh_remove_rows_button();
|
||||
this.refresh_edit_rows_button();
|
||||
this.refresh_duplicate_rows_button();
|
||||
|
||||
this.wrapper.trigger("change");
|
||||
|
|
@ -1049,6 +1070,145 @@ export default class Grid {
|
|||
return d;
|
||||
}
|
||||
|
||||
bulk_edit_rows() {
|
||||
if (!this.meta?.allow_bulk_edit) return;
|
||||
|
||||
const selected_children = this.get_selected_children();
|
||||
if (!selected_children.length) {
|
||||
frappe.show_alert({ message: __("No rows selected"), indicator: "orange" });
|
||||
return;
|
||||
}
|
||||
|
||||
const is_field_editable = (field_doc) => {
|
||||
const parent_docstatus = this.frm?.doc?.docstatus;
|
||||
const is_submitted_or_cancelled = [1, 2].includes(parent_docstatus);
|
||||
|
||||
return (
|
||||
field_doc.fieldname &&
|
||||
frappe.model.is_value_type(field_doc) &&
|
||||
field_doc.fieldtype !== "Read Only" &&
|
||||
!field_doc.hidden &&
|
||||
!field_doc.read_only &&
|
||||
!field_doc.is_virtual &&
|
||||
(!is_submitted_or_cancelled || field_doc.allow_on_submit)
|
||||
);
|
||||
};
|
||||
|
||||
const editable_fields = (this.docfields || []).filter((field_doc) =>
|
||||
is_field_editable(field_doc)
|
||||
);
|
||||
if (!editable_fields.length) {
|
||||
frappe.msgprint(__("No editable fields available for bulk edit."));
|
||||
return;
|
||||
}
|
||||
|
||||
const field_mappings = {};
|
||||
editable_fields.forEach((field_doc) => {
|
||||
const field_key = `${field_doc.label}`;
|
||||
field_mappings[field_key] = Object.assign({}, field_doc);
|
||||
});
|
||||
|
||||
const field_options = Object.keys(field_mappings).sort((a, b) =>
|
||||
__(cstr(field_mappings[a].label)).localeCompare(cstr(__(field_mappings[b].label)))
|
||||
);
|
||||
const status_regex = /status/i;
|
||||
const default_field =
|
||||
field_options.find((value) => status_regex.test(value)) ||
|
||||
field_options.find((value) => field_mappings[value]?.fieldtype === "Select");
|
||||
|
||||
const dialog = new frappe.ui.Dialog({
|
||||
title: __("Bulk Edit"),
|
||||
fields: [
|
||||
{
|
||||
fieldtype: "Select",
|
||||
options: field_options,
|
||||
default: default_field,
|
||||
label: __("Field"),
|
||||
fieldname: "field",
|
||||
reqd: 1,
|
||||
onchange: () => {
|
||||
set_value_field(dialog);
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldtype: "Data",
|
||||
label: __("Value"),
|
||||
fieldname: "value",
|
||||
onchange() {
|
||||
show_help_text();
|
||||
},
|
||||
},
|
||||
],
|
||||
primary_action: ({ value }) => {
|
||||
const selected_field = field_mappings[dialog.get_value("field")];
|
||||
const { fieldname } = selected_field;
|
||||
dialog.disable_primary_action();
|
||||
|
||||
const update_value = value || null;
|
||||
const tasks = selected_children.map((doc) =>
|
||||
frappe.model.set_value(doc.doctype, doc.name, fieldname, update_value)
|
||||
);
|
||||
|
||||
Promise.all(tasks).then(() => {
|
||||
this.frm && this.frm.dirty();
|
||||
this.refresh();
|
||||
dialog.hide();
|
||||
const row_label = selected_children.length === 1 ? __("row") : __("rows");
|
||||
frappe.show_alert(
|
||||
__("Updated {0} selected {1}. Save the form to keep changes.", [
|
||||
selected_children.length,
|
||||
row_label,
|
||||
])
|
||||
);
|
||||
});
|
||||
},
|
||||
primary_action_label: __("Update {0} rows", [selected_children.length]),
|
||||
});
|
||||
|
||||
if (default_field) set_value_field(dialog);
|
||||
show_help_text();
|
||||
|
||||
function set_value_field(dialogObj) {
|
||||
const field_value = dialogObj.get_value("field");
|
||||
if (!field_value || !field_mappings[field_value]) return;
|
||||
const new_df = Object.assign({}, field_mappings[field_value]);
|
||||
if (
|
||||
new_df.label?.match(status_regex) &&
|
||||
new_df.fieldtype === "Select" &&
|
||||
!new_df.default
|
||||
) {
|
||||
let options = [];
|
||||
if (typeof new_df.options === "string") {
|
||||
options = new_df.options.split("\n");
|
||||
}
|
||||
new_df.default = options[0] || options[1];
|
||||
}
|
||||
new_df.label = __("Value");
|
||||
new_df.onchange = show_help_text;
|
||||
delete new_df.depends_on;
|
||||
dialogObj.replace_field("value", new_df);
|
||||
show_help_text();
|
||||
}
|
||||
|
||||
function show_help_text() {
|
||||
if (dialog.get_primary_btn().is(":focus, :active")) return;
|
||||
|
||||
let value = dialog.get_value("value");
|
||||
if (value == null || value === "") {
|
||||
dialog.set_df_property(
|
||||
"value",
|
||||
"description",
|
||||
__("You have not entered a value. The field will be set to empty.")
|
||||
);
|
||||
} else {
|
||||
dialog.set_df_property("value", "description", "");
|
||||
}
|
||||
}
|
||||
|
||||
dialog.refresh();
|
||||
dialog.show();
|
||||
}
|
||||
|
||||
set_focus_on_row(idx) {
|
||||
if (!idx && idx !== 0) {
|
||||
idx = this.grid_rows.length - 1;
|
||||
|
|
|
|||
|
|
@ -420,6 +420,8 @@ export default class BulkOperations {
|
|||
}
|
||||
|
||||
function show_help_text() {
|
||||
if (dialog.get_primary_btn().is(":focus, :active")) return;
|
||||
|
||||
let value = dialog.get_value("value");
|
||||
if (value == null || value === "") {
|
||||
dialog.set_df_property(
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue