diff --git a/frappe/__init__.py b/frappe/__init__.py
index 6544206979..2fae079cde 100644
--- a/frappe/__init__.py
+++ b/frappe/__init__.py
@@ -14,7 +14,7 @@ import os, sys, importlib, inspect, json
from .exceptions import *
from .utils.jinja import get_jenv, get_template, render_template, get_email_from_template
-__version__ = '10.0.15'
+__version__ = '10.0.16'
__title__ = "Frappe Framework"
local = Local()
diff --git a/frappe/public/js/frappe/list/list_renderer.js b/frappe/public/js/frappe/list/list_renderer.js
index bfb3054fae..c298420665 100644
--- a/frappe/public/js/frappe/list/list_renderer.js
+++ b/frappe/public/js/frappe/list/list_renderer.js
@@ -3,9 +3,10 @@
frappe.provide('frappe.views');
-// Renders customized list
-// usually based on `in_list_view` property
-
+/**
+* Renders customized list. Usually based on `in_list_view` property.
+* It carries information that is used in frappe.views.ListView
+*/
frappe.views.ListRenderer = Class.extend({
name: 'List',
init: function (opts) {
@@ -38,6 +39,7 @@ frappe.views.ListRenderer = Class.extend({
// default settings
this.order_by = this.order_by || 'modified desc';
+ this.group_by = this.group_by || '';
this.filters = this.filters || [];
this.or_filters = this.or_filters || [];
this.page_length = this.page_length || 20;
@@ -54,6 +56,7 @@ frappe.views.ListRenderer = Class.extend({
this.init_user_settings();
this.order_by = this.user_settings.order_by || this.settings.order_by;
+ this.group_by = this.get_group_by();
this.filters = this.user_settings.filters || this.settings.filters;
this.page_length = this.settings.page_length;
@@ -62,6 +65,16 @@ frappe.views.ListRenderer = Class.extend({
this.filters = [[this.doctype, "docstatus", "!=", 2]];
}
},
+
+ /**
+ * Get the name of the column to use in SQL `group by`.
+ * It defaults to 'creation'
+ */
+ get_group_by: function() {
+ const default_column = this.settings.group_by || 'creation';
+ const group_by = $.format('`tab{0}`.`{1}`', [this.doctype, default_column]);
+ return group_by;
+ },
init_user_settings: function () {
frappe.provide('frappe.model.user_settings.' + this.doctype + '.' + this.name);
this.user_settings = frappe.get_user_settings(this.doctype)[this.name];
diff --git a/frappe/public/js/frappe/ui/base_list.js b/frappe/public/js/frappe/ui/base_list.js
new file mode 100644
index 0000000000..494e2d6656
--- /dev/null
+++ b/frappe/public/js/frappe/ui/base_list.js
@@ -0,0 +1,540 @@
+// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
+// MIT License. See license.txt
+
+// new re-re-factored Listing object
+// now called BaseList
+//
+// opts:
+// parent
+
+// method (method to call on server)
+// args (additional args to method)
+// get_args (method to return args as dict)
+
+// show_filters [false]
+// doctype
+// filter_fields (if given, this list is rendered, else built from doctype)
+
+// query or get_query (will be deprecated)
+// query_max
+// buttons_in_frame
+
+// no_result_message ("No result")
+
+// page_length (20)
+// hide_refresh (False)
+// no_toolbar
+// new_doctype
+// [function] render_row(parent, data)
+// [function] onrun
+// no_loading (no ajax indicator)
+
+frappe.provide('frappe.ui');
+
+frappe.ui.BaseList = Class.extend({
+ init: function (opts) {
+ this.opts = opts || {};
+ this.set_defaults();
+ if (opts) {
+ this.make();
+ }
+ },
+ set_defaults: function () {
+ this.page_length = 20;
+ this.start = 0;
+ this.data = [];
+ },
+ make: function (opts) {
+ if (opts) {
+ this.opts = opts;
+ }
+ this.prepare_opts();
+
+ $.extend(this, this.opts);
+
+ // make dom
+ this.wrapper = $(frappe.render_template('listing', this.opts));
+ this.parent.append(this.wrapper);
+
+ this.set_events();
+
+ if (this.page) {
+ this.wrapper.find('.list-toolbar-wrapper').hide();
+ }
+
+ if (this.show_filters) {
+ this.make_filters();
+ }
+ },
+ prepare_opts: function () {
+ if (this.opts.new_doctype) {
+ if (!frappe.boot.user.can_create.includes(this.opts.new_doctype)) {
+ this.opts.new_doctype = null;
+ }
+ }
+ if (!this.opts.no_result_message) {
+ this.opts.no_result_message = __('Nothing to show');
+ }
+ if (!this.opts.page_length) {
+ this.opts.page_length = this.user_settings && this.user_settings.limit || 20;
+ }
+ this.opts._more = __('More');
+ },
+ add_button: function (label, click, icon) {
+ if (this.page) {
+ return this.page.add_menu_item(label, click, icon)
+ } else {
+ this.wrapper.find('.list-toolbar-wrapper').removeClass('hide');
+ return $('')
+ .appendTo(this.wrapper.find('.list-toolbar'))
+ .html((icon ? (' ') : '') + label)
+ .click(click);
+ }
+ },
+ set_events: function () {
+ var me = this;
+
+ // next page
+ this.wrapper.find('.btn-more').click(function () {
+ me.run(true);
+ });
+
+ this.wrapper.find(".btn-group-paging").on('click', '.btn', function () {
+ me.page_length = cint($(this).attr("data-value"));
+
+ me.wrapper.find(".btn-group-paging .btn-info").removeClass("btn-info");
+ $(this).addClass("btn-info");
+
+ // always reset when changing list page length
+ me.run();
+ });
+
+ // select the correct page length
+ if (this.opts.page_length !== 20) {
+ this.wrapper.find(".btn-group-paging .btn-info").removeClass("btn-info");
+ this.wrapper
+ .find(".btn-group-paging .btn[data-value='" + this.opts.page_length + "']")
+ .addClass('btn-info');
+ }
+
+ // title
+ if (this.title) {
+ this.wrapper.find('h3').html(this.title).show();
+ }
+
+ // new
+ this.set_primary_action();
+
+ if (me.no_toolbar || me.hide_toolbar) {
+ me.wrapper.find('.list-toolbar-wrapper').hide();
+ }
+ },
+
+ set_primary_action: function () {
+ var me = this;
+ if (this.new_doctype) {
+ this.page.set_primary_action(
+ __("New"),
+ me.make_new_doc.bind(me, me.new_doctype),
+ "octicon octicon-plus"
+ );
+ } else {
+ this.page.clear_primary_action();
+ }
+ },
+
+ make_new_doc: function (doctype) {
+ var me = this;
+ frappe.model.with_doctype(doctype, function () {
+ if (me.custom_new_doc) {
+ me.custom_new_doc(doctype);
+ } else {
+ if (me.filter_list) {
+ frappe.route_options = {};
+ me.filter_list.get_filters().forEach(function (f, i) {
+ if (f[2] === "=" && !frappe.model.std_fields_list.includes(f[1])) {
+ frappe.route_options[f[1]] = f[3];
+ }
+ });
+ }
+ frappe.new_doc(doctype, true);
+ }
+ });
+ },
+
+ make_filters: function () {
+ this.make_standard_filters();
+
+ this.filter_list = new frappe.ui.FilterList({
+ base_list: this,
+ parent: this.wrapper.find('.list-filters').show(),
+ doctype: this.doctype,
+ filter_fields: this.filter_fields,
+ default_filters: this.default_filters || []
+ });
+ // default filter for submittable doctype
+ if (frappe.model.is_submittable(this.doctype)) {
+ this.filter_list.add_filter(this.doctype, "docstatus", "!=", 2);
+ }
+ },
+
+ make_standard_filters: function() {
+ var me = this;
+ if (this.standard_filters_added) {
+ return;
+ }
+
+ if (this.meta) {
+ var filter_count = 1;
+ if(this.is_list_view) {
+ $(``)
+ .prependTo(this.page.page_form);
+ }
+ this.page.add_field({
+ fieldtype: 'Data',
+ label: 'ID',
+ condition: 'like',
+ fieldname: 'name',
+ onchange: () => { me.refresh(true); }
+ });
+
+ this.meta.fields.forEach(function(df, i) {
+ if(df.in_standard_filter && !frappe.model.no_value_type.includes(df.fieldtype)) {
+ let options = df.options;
+ let condition = '=';
+ let fieldtype = df.fieldtype;
+ if (['Text', 'Small Text', 'Text Editor', 'Data'].includes(fieldtype)) {
+ fieldtype = 'Data';
+ condition = 'like';
+ }
+ if(df.fieldtype == "Select" && df.options) {
+ options = df.options.split("\n");
+ if(options.length > 0 && options[0] != "") {
+ options.unshift("");
+ options = options.join("\n");
+ }
+ }
+ let f = me.page.add_field({
+ fieldtype: fieldtype,
+ label: __(df.label),
+ options: options,
+ fieldname: df.fieldname,
+ condition: condition,
+ onchange: () => {me.refresh(true);}
+ });
+ filter_count ++;
+ if (filter_count > 3) {
+ $(f.wrapper).addClass('hidden-sm').addClass('hidden-xs');
+ }
+ if (filter_count > 5) {
+ return false;
+ }
+ }
+ });
+ }
+
+ this.standard_filters_added = true;
+ },
+
+ update_standard_filters: function(filters) {
+ let me = this;
+ for(let key in this.page.fields_dict) {
+ let field = this.page.fields_dict[key];
+ let value = field.get_value();
+ if (value) {
+ if (field.df.condition==='like' && !value.includes('%')) {
+ value = '%' + value + '%';
+ }
+ filters.push([
+ me.doctype,
+ field.df.fieldname,
+ field.df.condition || '=',
+ value
+ ]);
+ }
+ }
+ },
+
+
+ clear: function () {
+ this.data = [];
+ this.wrapper.find('.result-list').empty();
+ this.wrapper.find('.result').show();
+ this.wrapper.find('.no-result').hide();
+ this.start = 0;
+ this.onreset && this.onreset();
+ },
+
+ /*
+ * Uses the value of `frappe.route_options` to automatically set
+ * a filter in a list view.
+ */
+ set_filters_from_route_options: function ({clear_filters=true} = {}) {
+ var me = this;
+ if(this.filter_list && clear_filters) {
+ this.filter_list.clear_filters();
+ }
+
+ for(var field in frappe.route_options) {
+ var value = frappe.route_options[field];
+ var doctype = null;
+
+ // if `Child DocType.fieldname`
+ if (field.includes(".")) {
+ doctype = field.split(".")[0];
+ field = field.split(".")[1];
+ }
+
+ // find the table in which the key exists
+ // for example the filter could be {"item_code": "X"}
+ // where item_code is in the child table.
+
+ // we can search all tables for mapping the doctype
+ if (!doctype) {
+ doctype = frappe.meta.get_doctype_for_field(me.doctype, field);
+ }
+
+ if (doctype && me.filter_list) {
+ if ($.isArray(value)) {
+ me.filter_list.add_filter(doctype, field, value[0], value[1]);
+ } else {
+ me.filter_list.add_filter(doctype, field, "=", value);
+ }
+ }
+ }
+ frappe.route_options = null;
+ },
+
+ run: function(more) {
+ setTimeout(() => this._run(more), 100);
+ },
+
+ _run: function (more) {
+ var me = this;
+ if (!more) {
+ this.start = 0;
+ this.onreset && this.onreset();
+ }
+
+ var args = this.get_call_args();
+ this.save_user_settings_locally(args);
+
+ // user_settings are saved by db_query.py when dirty
+ $.extend(args, {
+ user_settings: frappe.model.user_settings[this.doctype]
+ });
+
+ return frappe.call({
+ method: this.opts.method || 'frappe.desk.query_builder.runquery',
+ type: "GET",
+ freeze: this.opts.freeze !== undefined ? this.opts.freeze : true,
+ args: args,
+ callback: function (r) {
+ me.dirty = false;
+ me.render_results(r);
+ },
+ no_spinner: this.opts.no_loading
+ });
+ },
+ save_user_settings_locally: function (args) {
+ if (this.opts.save_user_settings && this.doctype && !this.docname) {
+ // save list settings locally
+ var user_settings = frappe.model.user_settings[this.doctype];
+ var different = false;
+
+ if (!user_settings) {
+ return;
+ }
+
+ if (!frappe.utils.arrays_equal(args.filters, user_settings.filters)) {
+ // settings are dirty if filters change
+ user_settings.filters = args.filters;
+ different = true;
+ }
+
+ if (user_settings.order_by !== args.order_by) {
+ user_settings.order_by = args.order_by;
+ different = true;
+ }
+
+ if (user_settings.limit !== args.limit_page_length) {
+ user_settings.limit = args.limit_page_length || 20
+ different = true;
+ }
+
+ // save fields in list settings
+ if (args.save_user_settings_fields) {
+ user_settings.fields = args.fields;
+ }
+
+ if (different) {
+ user_settings.updated_on = moment().toString();
+ }
+ }
+ },
+
+ /*
+ * Prepares arguments that will be used to query the database to
+ * return the desired records for the list view
+ */
+ get_call_args: function () {
+ if (!this.method) {
+ var query = this.get_query && this.get_query() || this.query;
+ query = this.add_limits(query);
+ var args = {
+ query_max: this.query_max,
+ as_dict: 1
+ }
+ args.simple_query = query;
+ } else {
+ var args = {
+ start: this.start,
+ page_length: this.page_length
+ }
+ }
+
+ // append user-defined arguments
+ if (this.args)
+ $.extend(args, this.args)
+
+ if (this.get_args) {
+ $.extend(args, this.get_args());
+ }
+ return args;
+ },
+ render_results: function (r) {
+ if (this.start === 0)
+ this.clear();
+
+ this.wrapper.find('.btn-more, .list-loading').hide();
+
+ var values = [];
+
+ if (r.message) {
+ values = this.get_values_from_response(r.message);
+ }
+
+ var show_results = true;
+ if(this.show_no_result) {
+ if($.isFunction(this.show_no_result)) {
+ show_results = !this.show_no_result()
+ } else {
+ show_results = !this.show_no_result;
+ }
+ }
+
+ // render result view when
+ // length > 0 OR
+ // explicitly set by flag
+ if (values.length || show_results) {
+ this.data = this.data.concat(values);
+ this.render_view(values);
+ this.update_paging(values);
+ } else if (this.start === 0) {
+ // show no result message
+ this.wrapper.find('.result').hide();
+
+ var msg = '';
+ var no_result_message = this.no_result_message;
+ if(no_result_message && $.isFunction(no_result_message)) {
+ msg = no_result_message();
+ } else if(typeof no_result_message === 'string') {
+ msg = no_result_message;
+ } else {
+ msg = __('No Results')
+ }
+
+ this.wrapper.find('.no-result').html(msg).show();
+ }
+
+ this.wrapper.find('.list-paging-area')
+ .toggle(values.length > 0|| this.start > 0);
+
+ // callbacks
+ if (this.onrun) this.onrun();
+ if (this.callback) this.callback(r);
+ this.wrapper.trigger("render-complete");
+ },
+
+ get_values_from_response: function (data) {
+ // make dictionaries from keys and values
+ if (data.keys && $.isArray(data.keys)) {
+ return frappe.utils.dict(data.keys, data.values);
+ } else {
+ return data;
+ }
+ },
+
+ render_view: function (values) {
+ // override this method in derived class
+ },
+
+ update_paging: function (values) {
+ if (values.length >= this.page_length) {
+ this.wrapper.find('.btn-more').show();
+ this.start += this.page_length;
+ }
+ },
+
+ refresh: function () {
+ this.run();
+ },
+ add_limits: function (query) {
+ return query + ' LIMIT ' + this.start + ',' + (this.page_length + 1);
+ },
+ set_filter: function (fieldname, label, no_run, no_duplicate) {
+ var filter = this.filter_list.get_filter(fieldname);
+ if (filter) {
+ var value = cstr(filter.field.get_value());
+ if (value.includes(label)) {
+ // already set
+ return false
+
+ } else if (no_duplicate) {
+ filter.set_values(this.doctype, fieldname, "=", label);
+ } else {
+ // second filter set for this field
+ if (fieldname == '_user_tags' || fieldname == "_liked_by") {
+ // and for tags
+ this.filter_list.add_filter(this.doctype, fieldname, 'like', '%' + label + '%');
+ } else {
+ // or for rest using "in"
+ filter.set_values(this.doctype, fieldname, 'in', value + ', ' + label);
+ }
+ }
+ } else {
+ // no filter for this item,
+ // setup one
+ if (['_user_tags', '_comments', '_assign', '_liked_by'].includes(fieldname)) {
+ this.filter_list.add_filter(this.doctype, fieldname, 'like', '%' + label + '%');
+ } else {
+ this.filter_list.add_filter(this.doctype, fieldname, '=', label);
+ }
+ }
+ if (!no_run)
+ this.run();
+ },
+ init_user_settings: function () {
+ this.user_settings = frappe.model.user_settings[this.doctype] || {};
+ },
+ call_for_selected_items: function (method, args) {
+ var me = this;
+ args.names = this.get_checked_items().map(function (item) {
+ return item.name;
+ });
+
+ frappe.call({
+ method: method,
+ args: args,
+ freeze: true,
+ callback: function (r) {
+ if (!r.exc) {
+ if (me.list_header) {
+ me.list_header.find(".list-select-all").prop("checked", false);
+ }
+ me.refresh(true);
+ }
+ }
+ });
+ }
+});
diff --git a/frappe/public/js/frappe/ui/filters/filters.js b/frappe/public/js/frappe/ui/filters/filters.js
new file mode 100644
index 0000000000..ce513bd73e
--- /dev/null
+++ b/frappe/public/js/frappe/ui/filters/filters.js
@@ -0,0 +1,691 @@
+// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
+// MIT License. See license.txt
+
+frappe.ui.FilterList = Class.extend({
+ init: function(opts) {
+ $.extend(this, opts);
+ this.filters = [];
+ this.wrapper = this.parent;
+ this.stats = [];
+ this.make();
+ this.set_events();
+ },
+ make: function() {
+ this.wrapper.find('.show_filters, .filter_area').remove();
+ this.wrapper.append(`
+
+
+
+
+
+ `);
+ },
+ set_events: function() {
+ var me = this;
+ // show filters
+ this.wrapper.find('.new-filter').bind('click', function() {
+ me.add_filter();
+ });
+
+ this.wrapper.find('.clear-filters').bind('click', function() {
+ me.clear_filters();
+ $('.date-range-picker').val('')
+ me.base_list.run();
+ $(this).addClass("hide");
+ });
+ },
+
+ show_filters: function() {
+ this.wrapper.find('.show_filters').toggle();
+ if(!this.filters.length) {
+ this.add_filter(this.doctype, 'name');
+ this.filters[0].wrapper.find(".filter_field input").focus();
+ }
+ },
+
+ /*
+ * Removes all filters.
+ */
+ clear_filters: function() {
+ $.each(this.filters, function(i, f) { f.remove(true); });
+ if(this.base_list.page.fields_dict) {
+ $.each(this.base_list.page.fields_dict, (key, value) => {
+ value.set_input('');
+ });
+ }
+ this.filters = [];
+ },
+
+ /*
+ * Adds a new filter.
+ * @param {string} doctype
+ * @param {string} fieldname
+ * @param {string} condition
+ * @param {string} value
+ * @param {string} hidden
+ * @returns {Boolean} - Returns true if filter is added
+ */
+ add_filter: function(doctype, fieldname, condition, value, hidden) {
+ // adds a new filter, returns true if filter has been added
+
+ // allow equal to be used as like
+ let base_filter = this.base_list.page.fields_dict[fieldname];
+ if (base_filter
+ && (base_filter.df.condition==condition
+ || (condition==='=' && base_filter.df.condition==='like'))) {
+ // if filter exists in base_list, then exit
+ this.base_list.page.fields_dict[fieldname].set_input(value);
+
+ return true;
+ }
+
+ if(doctype && fieldname
+ && !frappe.meta.has_field(doctype, fieldname)
+ && !in_list(frappe.model.std_fields_list, fieldname)) {
+ frappe.msgprint({
+ message: __('Filter {0} missing', [fieldname.bold()]),
+ title: 'Invalid Filter',
+ indicator: 'red'
+ });
+ return false;
+ }
+
+ this.wrapper.find('.show_filters').toggle(true);
+ var is_new_filter = arguments.length===0;
+
+ if (is_new_filter && this.wrapper.find(".is-new-filter:visible").length) {
+ // only allow 1 new filter at a time!
+ return false;
+ }
+
+ var filter = this.push_new_filter(doctype, fieldname, condition, value);
+ if (!filter) return;
+
+ if(this.wrapper.find('.clear-filters').hasClass("hide")) {
+ this.wrapper.find('.clear-filters').removeClass("hide");
+ }
+
+ if (filter && is_new_filter) {
+ filter.wrapper.addClass("is-new-filter");
+ } else {
+ filter.freeze();
+ }
+
+ if (hidden) {
+ filter.$btn_group.addClass("hide");
+ }
+
+ return true;
+ },
+ push_new_filter: function(doctype, fieldname, condition, value) {
+ if(this.filter_exists(doctype, fieldname, condition, value)) {
+ return;
+ }
+
+ // if standard filter exists, then clear it.
+ if(this.base_list.page.fields_dict[fieldname]) {
+ this.base_list.page.fields_dict[fieldname].set_input('');
+ }
+
+ var filter = new frappe.ui.Filter({
+ flist: this,
+ _doctype: doctype,
+ fieldname: fieldname,
+ condition: condition,
+ value: value
+ });
+
+ this.filters.push(filter);
+
+ return filter;
+ },
+
+ remove: function(filter) {
+ // remove `filter` from flist
+ for (var i in this.filters) {
+ if (this.filters[i] === filter) {
+ break;
+ }
+ }
+ if (i!==undefined) {
+ // remove index
+ this.filters.splice(i, 1);
+ }
+ },
+
+ filter_exists: function(doctype, fieldname, condition, value) {
+ var flag = false;
+ for(var i in this.filters) {
+ if(this.filters[i].field) {
+ var f = this.filters[i].get_value();
+
+ if(f[0]==doctype && f[1]==fieldname && f[2]==condition && f[3]==value) {
+ flag = true;
+ } else if($.isArray(value) && frappe.utils.arrays_equal(value, f[3])) {
+ flag = true;
+ }
+ }
+ }
+ return flag;
+ },
+
+ get_filters: function() {
+ // get filter values as dict
+ var values = [];
+ $.each(this.filters, function(i, filter) {
+ if(filter.field) {
+ filter.freeze();
+ values.push(filter.get_value());
+ }
+ });
+ this.base_list.update_standard_filters(values);
+
+ return values;
+ },
+
+ // remove hidden filters
+ update_filters: function() {
+ var fl = [];
+ $.each(this.filters, function(i, f) {
+ if(f.field) fl.push(f);
+ })
+ this.filters = fl;
+ if(this.filters.length === 0) {
+ this.wrapper.find('.clear-filters').addClass("hide");
+ }
+ },
+
+ get_filter: function(fieldname) {
+ for(var i in this.filters) {
+ if(this.filters[i].field && this.filters[i].field.df.fieldname==fieldname)
+ return this.filters[i];
+ }
+ },
+
+ get_formatted_value: function(field, val){
+ var value = val;
+
+ if(field.df.fieldname==="docstatus") {
+ value = {0:"Draft", 1:"Submitted", 2:"Cancelled"}[value] || value;
+ } else if(field.df.original_type==="Check") {
+ value = {0:"No", 1:"Yes"}[cint(value)];
+ }
+
+ value = frappe.format(value, field.df, {only_value: 1});
+ return value;
+ }
+});
+
+frappe.ui.Filter = Class.extend({
+ init: function(opts) {
+ $.extend(this, opts);
+
+ this.doctype = this.flist.doctype;
+ this.make();
+ this.make_select();
+ this.set_events();
+ },
+ make: function() {
+ this.wrapper = $(frappe.render_template("edit_filter", {}))
+ .appendTo(this.flist.wrapper.find('.filter_area'));
+ },
+ make_select: function() {
+ var me = this;
+ this.fieldselect = new frappe.ui.FieldSelect({
+ parent: this.wrapper.find('.fieldname_select_area'),
+ doctype: this.doctype,
+ filter_fields: this.filter_fields,
+ select: function(doctype, fieldname) {
+ me.set_field(doctype, fieldname);
+ }
+ });
+ if(this.fieldname) {
+ this.fieldselect.set_value(this._doctype || this.doctype, this.fieldname);
+ }
+ },
+ set_events: function() {
+ var me = this;
+
+ this.wrapper.find("a.remove-filter").on("click", function() {
+ me.remove();
+ });
+
+ this.wrapper.find(".set-filter-and-run").on("click", function() {
+ me.wrapper.removeClass("is-new-filter");
+ me.flist.base_list.run();
+ me.apply();
+ });
+
+ // add help for "in" codition
+ me.wrapper.find('.condition').change(function() {
+ if(!me.field) return;
+ var condition = $(this).val();
+ if(in_list(["in", "like", "not in", "not like"], condition)) {
+ me.set_field(me.field.df.parent, me.field.df.fieldname, 'Data', condition);
+ if(!me.field.desc_area) {
+ me.field.desc_area = $('').appendTo(me.field.wrapper);
+ }
+ // set description
+ me.field.desc_area.html((in_list(["in", "not in"], condition)==="in"
+ ? __("values separated by commas")
+ : __("use % as wildcard"))+'
');
+ } else {
+ //if condition selected after refresh
+ me.set_field(me.field.df.parent, me.field.df.fieldname, null, condition);
+ }
+ });
+
+ // set the field
+ if(me.fieldname) {
+ // pre-sets given (could be via tags!)
+ return this.set_values(me._doctype, me.fieldname, me.condition, me.value);
+ } else {
+ me.set_field(me.doctype, 'name');
+ }
+ },
+
+ apply: function() {
+ var f = this.get_value();
+
+ this.flist.remove(this);
+ this.flist.push_new_filter(f[0], f[1], f[2], f[3]);
+ this.wrapper.remove();
+ this.flist.update_filters();
+ },
+
+ remove: function(dont_run) {
+ this.wrapper.remove();
+ this.$btn_group && this.$btn_group.remove();
+ this.field = null;
+ this.flist.update_filters();
+
+ if(!dont_run) {
+ this.flist.base_list.refresh(true);
+ }
+ },
+
+ set_values: function(doctype, fieldname, condition, value) {
+ // presents given (could be via tags!)
+ this.set_field(doctype, fieldname);
+
+ // change 0,1 to Yes, No for check field type
+ if(this.field.df.original_type==='Check') {
+ if(value==0) value = 'No';
+ else if(value==1) value = 'Yes';
+ }
+
+ if(condition) {
+ this.wrapper.find('.condition').val(condition).change();
+ }
+ if(value!=null) {
+ return this.field.set_value(value);
+ }
+ },
+
+ set_field: function(doctype, fieldname, fieldtype, condition) {
+ var me = this;
+
+ // set in fieldname (again)
+ var cur = me.field ? {
+ fieldname: me.field.df.fieldname,
+ fieldtype: me.field.df.fieldtype,
+ parent: me.field.df.parent,
+ } : {};
+
+ var original_docfield = me.fieldselect.fields_by_name[doctype][fieldname];
+ if(!original_docfield) {
+ frappe.msgprint(__("Field {0} is not selectable.", [fieldname]));
+ return;
+ }
+
+ var df = copy_dict(me.fieldselect.fields_by_name[doctype][fieldname]);
+
+ // filter field shouldn't be read only or hidden
+ df.read_only = 0;
+ df.hidden = 0;
+
+ if(!condition) this.set_default_condition(df, fieldtype);
+ this.set_fieldtype(df, fieldtype);
+
+ // called when condition is changed,
+ // don't change if all is well
+ if(me.field && cur.fieldname == fieldname && df.fieldtype == cur.fieldtype &&
+ df.parent == cur.parent) {
+ return;
+ }
+
+ // clear field area and make field
+ me.fieldselect.selected_doctype = doctype;
+ me.fieldselect.selected_fieldname = fieldname;
+
+ // save old text
+ var old_text = null;
+ if(me.field) {
+ old_text = me.field.get_value();
+ }
+
+ var field_area = me.wrapper.find('.filter_field').empty().get(0);
+ var f = frappe.ui.form.make_control({
+ df: df,
+ parent: field_area,
+ only_input: true,
+ })
+ f.refresh();
+
+ me.field = f;
+ if(old_text && me.field.df.fieldtype===cur.fieldtype) {
+ me.field.set_value(old_text);
+ }
+
+ // run on enter
+ $(me.field.wrapper).find(':input').keydown(function(ev) {
+ if(ev.which==13) {
+ me.flist.base_list.run();
+ }
+ })
+ },
+
+ set_fieldtype: function(df, fieldtype) {
+ // reset
+ if(df.original_type)
+ df.fieldtype = df.original_type;
+ else
+ df.original_type = df.fieldtype;
+
+ df.description = ''; df.reqd = 0;
+ df.ignore_link_validation = true;
+
+ // given
+ if(fieldtype) {
+ df.fieldtype = fieldtype;
+ return;
+ }
+
+ // scrub
+ if(df.fieldname=="docstatus") {
+ df.fieldtype="Select",
+ df.options=[
+ {value:0, label:__("Draft")},
+ {value:1, label:__("Submitted")},
+ {value:2, label:__("Cancelled")}
+ ]
+ } else if(df.fieldtype=='Check') {
+ df.fieldtype='Select';
+ df.options='No\nYes';
+ } else if(['Text','Small Text','Text Editor','Code','Tag','Comments',
+ 'Dynamic Link','Read Only','Assign'].indexOf(df.fieldtype)!=-1) {
+ df.fieldtype = 'Data';
+ } else if(df.fieldtype=='Link' && ['=', '!='].indexOf(this.wrapper.find('.condition').val())==-1) {
+ df.fieldtype = 'Data';
+ }
+ if(df.fieldtype==="Data" && (df.options || "").toLowerCase()==="email") {
+ df.options = null;
+ }
+ if(this.wrapper.find('.condition').val()== "Between" && (df.fieldtype == 'Date' || df.fieldtype == 'Datetime')){
+ df.fieldtype = 'DateRange';
+ }
+ },
+
+ set_default_condition: function(df, fieldtype) {
+ if(!fieldtype) {
+ // set as "like" for data fields
+ if (df.fieldtype == 'Data') {
+ this.wrapper.find('.condition').val('like');
+ } else if (df.fieldtype == 'Date' || df.fieldtype == 'Datetime'){
+ this.wrapper.find('.condition').val('Between');
+ }else{
+ this.wrapper.find('.condition').val('=');
+ }
+ }
+ },
+
+ get_value: function() {
+ return [this.fieldselect.selected_doctype,
+ this.field.df.fieldname, this.get_condition(), this.get_selected_value()];
+ },
+
+ get_selected_value: function() {
+ var val = this.field.get_value();
+
+ if(typeof val==='string') {
+ val = strip(val);
+ }
+
+ if(this.field.df.original_type == 'Check') {
+ val = (val=='Yes' ? 1 :0);
+ }
+
+ if(this.get_condition().indexOf('like', 'not like')!==-1) {
+ // automatically append wildcards
+ if(val) {
+ if(val.slice(0,1) !== "%") {
+ val = "%" + val;
+ }
+ if(val.slice(-1) !== "%") {
+ val = val + "%";
+ }
+ }
+ } else if(in_list(["in", "not in"], this.get_condition())) {
+ if(val) {
+ val = $.map(val.split(","), function(v) { return strip(v); });
+ }
+ } if(val === '%') {
+ val = "";
+ }
+
+ return val;
+ },
+
+ get_condition: function() {
+ return this.wrapper.find('.condition').val();
+ },
+
+ freeze: function() {
+ if(this.$btn_group) {
+ // already made, just hide the condition setter
+ this.set_filter_button_text();
+ this.wrapper.toggle(false);
+ return;
+ }
+
+ var me = this;
+
+ // add a button for new filter if missing
+ this.$btn_group = $(`
+
+
`)
+ .insertAfter(this.flist.wrapper.find(".set-filters .new-filter"));
+
+ this.set_filter_button_text();
+
+ this.$btn_group.find(".remove-filter").on("click", function() {
+ me.remove();
+ });
+
+ this.$btn_group.find(".toggle-filter").on("click", function() {
+ $(this).closest('.show_filters').find('.filter_area').show()
+ me.wrapper.toggle();
+ })
+ this.wrapper.toggle(false);
+ },
+
+ set_filter_button_text: function() {
+ var value = this.get_selected_value();
+ value = this.flist.get_formatted_value(this.field, value);
+
+ // for translations
+ // __("like"), __("not like"), __("in")
+
+ this.$btn_group.find(".toggle-filter")
+ .html(repl('%(label)s %(condition)s "%(value)s"', {
+ label: __(this.field.df.label),
+ condition: __(this.get_condition()),
+ value: __(value),
+ }));
+ }
+
+});
+
+//