diff --git a/cypress/integration/grid.js b/cypress/integration/grid.js index 607db6746e..60f8601482 100644 --- a/cypress/integration/grid.js +++ b/cypress/integration/grid.js @@ -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); + }); + }); + }); }); diff --git a/frappe/core/doctype/doctype/doctype.json b/frappe/core/doctype/doctype/doctype.json index f77190d18d..c237d840c0 100644 --- a/frappe/core/doctype/doctype/doctype.json +++ b/frappe/core/doctype/doctype/doctype.json @@ -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, diff --git a/frappe/core/doctype/doctype/doctype.py b/frappe/core/doctype/doctype/doctype.py index 39f33d19e4..cf9543d1f1 100644 --- a/frappe/core/doctype/doctype/doctype.py +++ b/frappe/core/doctype/doctype/doctype.py @@ -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 diff --git a/frappe/custom/doctype/customize_form/customize_form.json b/frappe/custom/doctype/customize_form/customize_form.json index 5cb6e91398..f04563a1cb 100644 --- a/frappe/custom/doctype/customize_form/customize_form.json +++ b/frappe/custom/doctype/customize_form/customize_form.json @@ -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", diff --git a/frappe/custom/doctype/customize_form/customize_form.py b/frappe/custom/doctype/customize_form/customize_form.py index e2646dc8c4..78625eb99c 100644 --- a/frappe/custom/doctype/customize_form/customize_form.py +++ b/frappe/custom/doctype/customize_form/customize_form.py @@ -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", diff --git a/frappe/public/js/frappe/form/grid.js b/frappe/public/js/frappe/form/grid.js index ad7b2fa8c8..694c97799b 100644 --- a/frappe/public/js/frappe/form/grid.js +++ b/frappe/public/js/frappe/form/grid.js @@ -87,6 +87,10 @@ export default class Grid { data-action="delete_rows"> ${__("Delete")} +