From 0c4245634faeb128462caaa16a35a5bf4ad62f88 Mon Sep 17 00:00:00 2001 From: RitvikSardana <65544983+RitvikSardana@users.noreply.github.com> Date: Wed, 15 Nov 2023 11:34:19 +0530 Subject: [PATCH] feat: Apply Filters to Link Fields Via Form Builder (#22844) Co-authored-by: Suraj Shetty <13928957+surajshetty3416@users.noreply.github.com> --- cypress/fixtures/form_builder_doctype.js | 6 + cypress/integration/form_builder.js | 34 +++++ frappe/core/doctype/docfield/docfield.json | 10 +- frappe/core/doctype/docfield/docfield.py | 1 + frappe/core/doctype/doctype/doctype.py | 1 + .../doctype/custom_field/custom_field.json | 7 + .../doctype/custom_field/custom_field.py | 1 + .../customize_form/customize_form.json | 7 + .../doctype/customize_form/customize_form.py | 13 ++ .../customize_form_field.json | 8 +- .../customize_form_field.py | 1 + .../js/form_builder/components/Field.vue | 138 ++++++++++++++++-- frappe/public/js/form_builder/store.js | 12 ++ frappe/public/js/frappe/form/form.js | 36 +++++ .../js/frappe/ui/filters/filter_list.js | 3 +- 15 files changed, 261 insertions(+), 17 deletions(-) diff --git a/cypress/fixtures/form_builder_doctype.js b/cypress/fixtures/form_builder_doctype.js index 08b598f82a..995971bed4 100644 --- a/cypress/fixtures/form_builder_doctype.js +++ b/cypress/fixtures/form_builder_doctype.js @@ -10,6 +10,12 @@ export default { fieldtype: "Data", label: "Data 3", }, + { + fieldname: "gender", + fieldtype: "Link", + label: "Gender", + options: "Gender", + }, { fieldname: "tab", fieldtype: "Tab Break", diff --git a/cypress/integration/form_builder.js b/cypress/integration/form_builder.js index b8105d870b..4f7a1c5b1f 100644 --- a/cypress/integration/form_builder.js +++ b/cypress/integration/form_builder.js @@ -35,6 +35,40 @@ context("Form Builder", () => { cy.get(".title-area .indicator-pill.orange").should("have.text", "Not Saved"); }); + it("Check if Filters are applied to the link field", () => { + // Visit the Form Builder + cy.visit(`/app/doctype/${doctype_name}`); + cy.findByRole("tab", { name: "Form" }).click(); + + cy.get("[data-fieldname='gender']").click(); + + // click on filter action button + cy.get('[data-fieldname="gender"] .field-actions button:first').click(); + + // add filter + cy.get(".modal-body .clear-filters").click(); + cy.get(".modal-body .filter-action-buttons .add-filter").click(); + cy.wait(100); + cy.get(".modal-body .filter-box .list_filter .filter-field .link-field input").type( + "Male" + ); + cy.get(".btn-modal-primary").click(); + + // Save the document + cy.click_doc_primary_button("Save"); + + // Open a new Form + cy.new_form(doctype_name); + // Click on the "salutation" field + cy.get_field("gender").clear().click(); + + cy.intercept("POST", "/api/method/frappe.desk.search.search_link").as("search_link"); + cy.wait("@search_link").then((data) => { + expect(data.response.body.message.length).to.eq(1); + expect(data.response.body.message[0].value).to.eq("Male"); + }); + }); + it("Add empty section and save", () => { cy.visit(`/app/doctype/${doctype_name}`); cy.findByRole("tab", { name: "Form" }).click(); diff --git a/frappe/core/doctype/docfield/docfield.json b/frappe/core/doctype/docfield/docfield.json index 42e8d21e34..f67969f43e 100644 --- a/frappe/core/doctype/docfield/docfield.json +++ b/frappe/core/doctype/docfield/docfield.json @@ -23,6 +23,7 @@ "options", "sort_options", "show_dashboard", + "link_filters", "defaults_section", "default", "column_break_6", @@ -416,7 +417,7 @@ "width": "50px" }, { - "description": "Number of columns for a field in a grid (total columns should be less than 11)", + "description": "Number of columns for a field in a List View or a Grid (Total Columns should be less than 11)", "fieldname": "columns", "fieldtype": "Int", "label": "Columns" @@ -560,13 +561,18 @@ "fieldname": "sort_options", "fieldtype": "Check", "label": "Sort Options" + }, + { + "fieldname": "link_filters", + "fieldtype": "JSON", + "label": "Link Filters" } ], "idx": 1, "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2023-10-25 06:53:45.194081", + "modified": "2023-11-13 11:48:51.502812", "modified_by": "Administrator", "module": "Core", "name": "DocField", diff --git a/frappe/core/doctype/docfield/docfield.py b/frappe/core/doctype/docfield/docfield.py index c4067646e6..351dd0caae 100644 --- a/frappe/core/doctype/docfield/docfield.py +++ b/frappe/core/doctype/docfield/docfield.py @@ -87,6 +87,7 @@ class DocField(Document): is_virtual: DF.Check label: DF.Data | None length: DF.Int + link_filters: DF.JSON | None mandatory_depends_on: DF.Code | None max_height: DF.Data | None no_copy: DF.Check diff --git a/frappe/core/doctype/doctype/doctype.py b/frappe/core/doctype/doctype/doctype.py index 7b5c58dedd..c3a1aa0e41 100644 --- a/frappe/core/doctype/doctype/doctype.py +++ b/frappe/core/doctype/doctype/doctype.py @@ -136,6 +136,7 @@ class DocType(Document): is_virtual: DF.Check issingle: DF.Check istable: DF.Check + link_filters: DF.JSON links: DF.Table[DocTypeLink] make_attachments_public: DF.Check max_attachments: DF.Int diff --git a/frappe/custom/doctype/custom_field/custom_field.json b/frappe/custom/doctype/custom_field/custom_field.json index 53a003c88e..332969b036 100644 --- a/frappe/custom/doctype/custom_field/custom_field.json +++ b/frappe/custom/doctype/custom_field/custom_field.json @@ -15,6 +15,7 @@ "fieldname", "insert_after", "length", + "link_filters", "column_break_6", "fieldtype", "precision", @@ -444,6 +445,12 @@ "fieldname": "sort_options", "fieldtype": "Check", "label": "Sort Options" + }, + { + "fieldname": "link_filters", + "fieldtype": "JSON", + "hidden": 1, + "label": "Link Filters" } ], "icon": "fa fa-glass", diff --git a/frappe/custom/doctype/custom_field/custom_field.py b/frappe/custom/doctype/custom_field/custom_field.py index a1aa4fd342..c77d2f4bb2 100644 --- a/frappe/custom/doctype/custom_field/custom_field.py +++ b/frappe/custom/doctype/custom_field/custom_field.py @@ -94,6 +94,7 @@ class CustomField(Document): is_virtual: DF.Check label: DF.Data | None length: DF.Int + link_filters: DF.JSON | None mandatory_depends_on: DF.Code | None module: DF.Link | None no_copy: DF.Check diff --git a/frappe/custom/doctype/customize_form/customize_form.json b/frappe/custom/doctype/customize_form/customize_form.json index d345698f2d..b872ac73e5 100644 --- a/frappe/custom/doctype/customize_form/customize_form.json +++ b/frappe/custom/doctype/customize_form/customize_form.json @@ -11,6 +11,7 @@ "properties", "label", "search_fields", + "link_filters", "column_break_5", "istable", "is_calendar_and_gantt", @@ -385,6 +386,12 @@ "fieldname": "form_tab", "fieldtype": "Tab Break", "label": "Form" + }, + { + "fieldname": "link_filters", + "fieldtype": "JSON", + "hidden": 1, + "label": "Link Filters" } ], "hide_toolbar": 1, diff --git a/frappe/custom/doctype/customize_form/customize_form.py b/frappe/custom/doctype/customize_form/customize_form.py index 730d3dfc6c..34933978a6 100644 --- a/frappe/custom/doctype/customize_form/customize_form.py +++ b/frappe/custom/doctype/customize_form/customize_form.py @@ -54,6 +54,7 @@ class CustomizeForm(Document): is_calendar_and_gantt: DF.Check istable: DF.Check label: DF.Data | None + link_filters: DF.JSON | None links: DF.Table[DocTypeLink] make_attachments_public: DF.Check max_attachments: DF.Int @@ -681,6 +682,17 @@ def is_standard_or_system_generated_field(df): return not df.get("is_custom_field") or df.get("is_system_generated") +@frappe.whitelist() +def get_link_filters_from_doc_without_customisations(doctype, fieldname): + """Get the filters of a link field from a doc without customisations + In backend the customisations are not applied. + Customisations are applied in the client side. + """ + doc = frappe.get_doc("DocType", doctype) + field = list(filter(lambda x: x.fieldname == fieldname, doc.fields)) + return field[0].link_filters + + doctype_properties = { "search_fields": "Data", "title_field": "Data", @@ -761,6 +773,7 @@ docfield_properties = { "hide_days": "Check", "hide_seconds": "Check", "is_virtual": "Check", + "link_filters": "JSON", } doctype_link_properties = { diff --git a/frappe/custom/doctype/customize_form_field/customize_form_field.json b/frappe/custom/doctype/customize_form_field/customize_form_field.json index fa86df2735..aebb143677 100644 --- a/frappe/custom/doctype/customize_form_field/customize_form_field.json +++ b/frappe/custom/doctype/customize_form_field/customize_form_field.json @@ -24,6 +24,7 @@ "no_copy", "allow_in_quick_entry", "translatable", + "link_filters", "column_break_7", "default", "precision", @@ -471,13 +472,18 @@ "fieldname": "sort_options", "fieldtype": "Check", "label": "Sort Options" + }, + { + "fieldname": "link_filters", + "fieldtype": "JSON", + "label": "Link Filters" } ], "idx": 1, "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2023-10-25 06:55:50.718441", + "modified": "2023-11-07 13:17:21.373626", "modified_by": "Administrator", "module": "Custom", "name": "Customize Form Field", diff --git a/frappe/custom/doctype/customize_form_field/customize_form_field.py b/frappe/custom/doctype/customize_form_field/customize_form_field.py index 1afb5ad34b..59b0155a98 100644 --- a/frappe/custom/doctype/customize_form_field/customize_form_field.py +++ b/frappe/custom/doctype/customize_form_field/customize_form_field.py @@ -86,6 +86,7 @@ class CustomizeFormField(Document): is_virtual: DF.Check label: DF.Data | None length: DF.Int + link_filters: DF.JSON | None mandatory_depends_on: DF.Code | None no_copy: DF.Check non_negative: DF.Check diff --git a/frappe/public/js/form_builder/components/Field.vue b/frappe/public/js/form_builder/components/Field.vue index d44674964a..08e65473ee 100644 --- a/frappe/public/js/form_builder/components/Field.vue +++ b/frappe/public/js/form_builder/components/Field.vue @@ -75,6 +75,110 @@ function duplicate_field() { store.form.selected_field = duplicate_field.df; } +function make_dialog(frm) { + frm.dialog = new frappe.ui.Dialog({ + title: __("Set Filters"), + fields: [ + { + fieldtype: "HTML", + fieldname: "filter_area", + }, + ], + primary_action: () => { + let fieldname = props.field.df.fieldname; + let field_option = props.field.df.options; + let filters = frm.filter_group.get_filters().map((filter) => { + // last element is a boolean which hides the filter hence not required to store in meta + filter.pop(); + + // filter_group component requires options and frm.set_query requires fieldname so storing both + filter[0] = { fieldname, field_option }; + return filter; + }); + + props.field.df.link_filters = JSON.stringify(filters); + frm.dialog.hide(); + }, + primary_action_label: __("Apply"), + }); + + if (frm.doctype === "Customize Form") { + let current_doctype = frm.doc.doc_type; + let fieldname = props.field.df.fieldname; + let property = "link_filters"; + let property_setter_id = current_doctype + "-" + fieldname + "-" + property; + + frappe.db.exists("Property Setter", property_setter_id).then((exits) => { + if (exits) { + frm.dialog.set_secondary_action_label(__("Reset To Default")); + frm.dialog.set_secondary_action(() => { + frappe.call({ + method: "frappe.custom.doctype.customize_form.customize_form.get_link_filters_from_doc_without_customisations", + args: { + doctype: current_doctype, + fieldname: fieldname, + }, + callback: function (r) { + if (r.message) { + props.field.df.link_filters = r.message; + + frm.filter_group.clear_filters(); + add_existing_filter(frm, props.field.df); + // hide the secondary action button + frm.dialog.get_secondary_btn().addClass("hidden"); + } + }, + }); + }); + } + }); + } + + // Setting selected field in store because when we click on the dialog the selected field is set to null + frm.dialog.$wrapper.on("click", () => { + store.form.selected_field = props.field.df; + }); +} + +function make_filter_area(frm, doctype) { + frm.filter_group = new frappe.ui.FilterGroup({ + parent: frm.dialog.get_field("filter_area").$wrapper, + doctype: doctype, + on_change: () => {}, + }); +} + +function add_existing_filter(frm, df) { + if (df.link_filters) { + let filters = JSON.parse(df.link_filters); + filters.map((filter) => { + // filter_group component requires options and frm.set_query requires fieldname + filter[0] = filter[0].field_option; + }); + if (filters) { + frm.filter_group.add_filters_to_filter_group(filters); + } + } +} + +function edit_filters() { + let field_doctype = props.field.df.options; + const { frm } = store; + + make_dialog(frm); + make_filter_area(frm, field_doctype); + frappe.model.with_doctype(field_doctype, () => { + frm.dialog.show(); + add_existing_filter(frm, props.field.df); + }); +} + +function is_filter_applied() { + if (props.field.df.link_filters && JSON.parse(props.field.df.link_filters).length > 0) { + return "btn-filter-applied"; + } +} + onMounted(() => selected.value && label_input.value.focus_on_label()); @@ -111,22 +215,17 @@ onMounted(() => selected.value && label_input.value.focus_on_label());