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:
parent
5d66112518
commit
0c4245634f
15 changed files with 261 additions and 17 deletions
|
|
@ -10,6 +10,12 @@ export default {
|
|||
fieldtype: "Data",
|
||||
label: "Data 3",
|
||||
},
|
||||
{
|
||||
fieldname: "gender",
|
||||
fieldtype: "Link",
|
||||
label: "Gender",
|
||||
options: "Gender",
|
||||
},
|
||||
{
|
||||
fieldname: "tab",
|
||||
fieldtype: "Tab Break",
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue