feat: Apply Filters to Link Fields Via Form Builder (#22844)

Co-authored-by: Suraj Shetty <13928957+surajshetty3416@users.noreply.github.com>
This commit is contained in:
RitvikSardana 2023-11-15 11:34:19 +05:30 committed by GitHub
parent 5d66112518
commit 0c4245634f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 261 additions and 17 deletions

View file

@ -10,6 +10,12 @@ export default {
fieldtype: "Data",
label: "Data 3",
},
{
fieldname: "gender",
fieldtype: "Link",
label: "Gender",
options: "Gender",
},
{
fieldname: "tab",
fieldtype: "Tab Break",

View file

@ -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();

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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());
</script>
@ -111,22 +215,17 @@ onMounted(() => selected.value && label_input.value.focus_on_label());
</template>
<template #actions>
<div class="field-actions" :hidden="store.read_only">
<AddFieldButton
v-if="column.fields.indexOf(field) != column.fields.length - 1"
ref="add_field_ref"
:field="field"
:column="column"
:tooltip="__('Add field below')"
<button
v-if="field.df.fieldtype === 'Link'"
class="btn btn-xs btn-icon"
:class="is_filter_applied()"
@click="edit_filters"
>
<div v-html="frappe.utils.icon('filter', 'sm')"></div>
</button>
<AddFieldButton ref="add_field_ref" :column="column" :field="field">
<div v-html="frappe.utils.icon('add', 'sm')" />
</AddFieldButton>
<button
class="btn btn-xs btn-icon"
:title="__('Duplicate field')"
@click.stop="duplicate_field"
>
<div v-html="frappe.utils.icon('duplicate', 'sm')"></div>
</button>
<button
v-if="column.fields.indexOf(field)"
class="btn btn-xs btn-icon"
@ -137,6 +236,13 @@ onMounted(() => selected.value && label_input.value.focus_on_label());
>
<div v-html="frappe.utils.icon('move', 'sm')"></div>
</button>
<button
class="btn btn-xs btn-icon"
:title="__('Duplicate field')"
@click.stop="duplicate_field"
>
<div v-html="frappe.utils.icon('duplicate', 'sm')"></div>
</button>
<button
class="btn btn-xs btn-icon"
:title="__('Remove field')"
@ -211,4 +317,10 @@ onMounted(() => selected.value && label_input.value.focus_on_label());
}
}
}
.btn-filter-applied {
background-color: var(--gray-300) !important;
&:hover {
background-color: var(--gray-400) !important;
}
}
</style>

View file

@ -201,6 +201,18 @@ export const useStore = defineStore("form-builder-store", () => {
get_field_data(df)
);
}
// check if link_filters format is correct or not
if (df.link_filters) {
try {
let link_filters = JSON.parse(df.link_filters);
} catch (e) {
error_message = __(
`Invalid Filter Format. Try using filter icon on the field to set it correctly`
);
}
}
});
return error_message;

View file

@ -107,6 +107,7 @@ frappe.ui.form.Form = class FrappeForm {
// 2 column layout
this.setup_std_layout();
this.setup_filters();
// client script must be called after "setup" - there are no fields_dict attached to the frm otherwise
this.script_manager = new frappe.ui.form.ScriptManager({
@ -272,6 +273,41 @@ frappe.ui.form.Form = class FrappeForm {
});
}
setup_filters() {
let fields_with_filters = frappe
.get_meta(this.doctype)
.fields.filter((field) => field.link_filters)
.map((field) => JSON.parse(field.link_filters));
if (fields_with_filters.length === 0) return;
fields_with_filters = this.parse_filters(fields_with_filters);
for (let link_field in fields_with_filters) {
const filters = fields_with_filters[link_field];
this.set_query(link_field, () => filters);
}
}
parse_filters(data) {
const parsed_data = {};
for (const d of data) {
for (const condition of d) {
let [doctype, field, operator, value] = condition;
doctype = doctype.fieldname;
if (!parsed_data[doctype]) {
parsed_data[doctype] = {
filters: {},
};
}
if (!parsed_data[doctype].filters[field]) {
parsed_data[doctype].filters[field] = [operator, value];
}
}
}
return parsed_data;
}
watch_model_updates() {
// watch model updates
var me = this;

View file

@ -1,7 +1,7 @@
frappe.ui.FilterGroup = class {
constructor(opts) {
$.extend(this, opts);
this.filters = [];
this.filters = this.filters || [];
window.fltr = this;
if (!this.filter_button) {
this.wrapper = this.parent;
@ -239,6 +239,7 @@ frappe.ui.FilterGroup = class {
},
filter_list: this.base_list || this,
};
let filter = new frappe.ui.Filter(args);
this.filters.push(filter);
return filter;