diff --git a/frappe/public/js/frappe/form/multi_select_dialog.js b/frappe/public/js/frappe/form/multi_select_dialog.js index cdd385a6ea..41b87e0207 100644 --- a/frappe/public/js/frappe/form/multi_select_dialog.js +++ b/frappe/public/js/frappe/form/multi_select_dialog.js @@ -1,110 +1,62 @@ -// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors -// MIT License. See license.txt - -frappe.ui.form.MultiSelectDialog = Class.extend({ - init: function(opts) { - /* Options: doctype, target, setters, get_query, action */ - $.extend(this, opts); - +frappe.ui.form.MultiSelectDialog = class MultiSelectDialog { + constructor(opts) { + /* Options: doctype, target, setters, get_query, action, add_filters_group, data_fields, primary_action_label */ + Object.assign(this, opts); var me = this; - if(this.doctype!="[Select]") { - frappe.model.with_doctype(this.doctype, function(r) { + if (this.doctype != "[Select]") { + frappe.model.with_doctype(this.doctype, function () { me.make(); }); } else { this.make(); } - }, - make: function() { - let me = this; + } + make() { + let me = this; this.page_length = 20; this.start = 0; + let fields = this.get_primary_filters(); - let fields = [ - { - fieldtype: "Data", - label: __("Search Term"), - fieldname: "search_term" - }, - { - fieldtype: "Column Break" - } - ]; - let count = 0; - if(!this.date_field) { - this.date_field = "transaction_date"; - } - - // setters can be defined as a dict or a list of fields - // setters define the additional filters that get applied - // for selection - - // CASE 1: DocType name and fieldname is the same, example "customer" and "customer" - // setters define the filters applied in the modal - // if the fieldnames and doctypes are consistently named, - // pass a dict with the setter key and value, for example - // {customer: [customer_name]} - - // CASE 2: if the fieldname of the target is different, - // then pass a list of fields with appropriate fieldname - - if($.isArray(this.setters)) { - for (let df of this.setters) { - fields.push(df, {fieldtype: "Column Break"}); - } - } else { - Object.keys(this.setters).forEach(function(setter) { - fields.push({ - fieldtype: me.target.fields_dict[setter].df.fieldtype, - label: me.target.fields_dict[setter].df.label, - fieldname: setter, - options: me.target.fields_dict[setter].df.options, - default: me.setters[setter] - }); - if (count++ < Object.keys(me.setters).length) { - fields.push({fieldtype: "Column Break"}); - } - }); - } - + // Make results area fields = fields.concat([ - { - "fieldname":"date_range", - "label": __("Date Range"), - "fieldtype": "DateRange", - }, - { fieldtype: "Section Break" }, { fieldtype: "HTML", fieldname: "results_area" }, - { fieldtype: "Button", fieldname: "more_btn", label: __("More"), - click: function(){ - me.start += 20; - frappe.flags.auto_scroll = true; - me.get_results(); + { + fieldtype: "Button", fieldname: "more_btn", label: __("More"), + click: () => { + this.start += 20; + this.get_results(); } } ]); - let doctype_plural = !this.doctype.endsWith('y') ? this.doctype + 's' - : this.doctype.slice(0, -1) + 'ies'; + // Custom Data Fields + if (this.data_fields) { + fields.push({ fieldtype: "Section Break" }); + fields = fields.concat(this.data_fields); + } + + let doctype_plural = this.doctype.plural(); + this.dialog = new frappe.ui.Dialog({ - title: __("Select {0}", [(this.doctype=='[Select]') ? __("value") : __(doctype_plural)]), + title: __("Select {0}", [(this.doctype == '[Select]') ? __("value") : __(doctype_plural)]), fields: fields, - primary_action_label: __("Get Items"), + primary_action_label: this.primary_action_label || __("Get Items"), secondary_action_label: __("Make {0}", [me.doctype]), - primary_action: function() { - me.action(me.get_checked_values(), me.args); + primary_action: function () { + let filters_data = me.get_custom_filters(); + me.action(me.get_checked_values(), cur_dialog.get_values(), me.args, filters_data); }, - secondary_action: function(e) { + secondary_action: function (e) { // If user wants to close the modal if (e) { frappe.route_options = {}; - if($.isArray(me.setters)) { + if (Array.isArray(me.setters)) { for (let df of me.setters) { frappe.route_options[df.fieldname] = me.dialog.fields_dict[df.fieldname].get_value() || undefined; } } else { - Object.keys(me.setters).forEach(function(setter) { + Object.keys(me.setters).forEach(function (setter) { frappe.route_options[setter] = me.dialog.fields_dict[setter].get_value() || undefined; }); } @@ -114,6 +66,10 @@ frappe.ui.form.MultiSelectDialog = Class.extend({ } }); + if (this.add_filters_group) { + this.make_filter_area(); + } + this.$parent = $(this.dialog.body); this.$wrapper = this.dialog.fields_dict.results_area.$wrapper.append(`
`); @@ -126,9 +82,89 @@ frappe.ui.form.MultiSelectDialog = Class.extend({ this.bind_events(); this.get_results(); this.dialog.show(); - }, + } - bind_events: function() { + get_primary_filters() { + let fields = []; + + let columns = new Array(3); + + // Hack for three column layout + // To add column break + columns[0] = [ + { + fieldtype: "Data", + label: __("Search"), + fieldname: "search_term" + } + ]; + columns[1] = []; + columns[2] = []; + + Object.keys(this.setters).forEach((setter, index) => { + let df_prop = frappe.meta.docfield_map[this.doctype][setter]; + + // Index + 1 to start filling from index 1 + // Since Search is a standrd field already pushed + columns[(index + 1) % 3].push({ + fieldtype: df_prop.fieldtype, + label: df_prop.label, + fieldname: setter, + options: df_prop.options, + default: this.setters[setter] + }); + }); + + // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/seal + if (Object.seal) { + Object.seal(columns); + // now a is a fixed-size array with mutable entries + } + + fields = [ + ...columns[0], + { fieldtype: "Column Break" }, + ...columns[1], + { fieldtype: "Column Break" }, + ...columns[2], + { fieldtype: "Section Break", fieldname: "primary_filters_sb" } + ]; + + if (this.add_filters_group) { + fields.push( + { + fieldtype: 'HTML', + fieldname: 'filter_area', + } + ); + } + + return fields; + } + + make_filter_area() { + this.filter_group = new frappe.ui.FilterGroup({ + parent: this.dialog.get_field('filter_area').$wrapper, + doctype: this.doctype, + on_change: () => { + this.get_results(); + } + }); + } + + get_custom_filters() { + if (this.add_filters_group && this.filter_group) { + return this.filter_group.get_filters().reduce((acc, filter) => { + return Object.assign(acc, { + [filter[1]]: [filter[2], filter[3]] + }); + }, {}); + } else { + return []; + } + } + + bind_events() { let me = this; this.$results.on('click', '.list-item-container', function (e) { @@ -136,48 +172,44 @@ frappe.ui.form.MultiSelectDialog = Class.extend({ $(this).find(':checkbox').trigger('click'); } }); + this.$results.on('click', '.list-item--head :checkbox', (e) => { this.$results.find('.list-item-container .list-row-check') .prop("checked", ($(e.target).is(':checked'))); }); - this.$parent.find('.input-with-feedback').on('change', (e) => { + this.$parent.find('.input-with-feedback').on('change', () => { frappe.flags.auto_scroll = false; this.get_results(); }); - this.$parent.find('[data-fieldname="date_range"]').on('blur', (e) => { - frappe.flags.auto_scroll = false; - this.get_results(); - }); - - this.$parent.find('[data-fieldname="search_term"]').on('input', (e) => { + this.$parent.find('[data-fieldtype="Data"]').on('input', () => { var $this = $(this); clearTimeout($this.data('timeout')); - $this.data('timeout', setTimeout(function() { + $this.data('timeout', setTimeout(function () { frappe.flags.auto_scroll = false; me.empty_list(); me.get_results(); }, 300)); }); - }, + } - get_checked_values: function() { + get_checked_values() { // Return name of checked value. - return this.$results.find('.list-item-container').map(function() { - if ($(this).find('.list-row-check:checkbox:checked').length > 0 ) { + return this.$results.find('.list-item-container').map(function () { + if ($(this).find('.list-row-check:checkbox:checked').length > 0) { return $(this).attr('data-item-name'); } }).get(); - }, + } - get_checked_items: function() { + get_checked_items() { // Return checked items with all the column values. let checked_values = this.get_checked_values(); return this.results.filter(res => checked_values.includes(res.name)); - }, + } - make_list_row: function(result={}) { + make_list_row(result = {}) { var me = this; // Make a head row by default (if result not passed) let head = Object.keys(result).length === 0; @@ -185,26 +217,17 @@ frappe.ui.form.MultiSelectDialog = Class.extend({ let contents = ``; let columns = ["name"]; - if($.isArray(this.setters)) { - for (let df of this.setters) { - columns.push(df.fieldname); - } - } else { - columns = columns.concat(Object.keys(this.setters)); - } - columns.push("Date"); + columns = columns.concat(Object.keys(this.setters)); - columns.forEach(function(column) { + columns.forEach(function (column) { contents += `
${ - head ? `${__(frappe.model.unscrub(column))}` - - : (column !== "name" ? `${__(result[column])}` - : ` - ${__(result[column])}`) - } + head ? `${__(frappe.model.unscrub(column))}` + : (column !== "name" ? `${__(result[column] || '')}` + : ` + ${__(result[column] || '')}`)}
`; - }) + }); let $row = $(`
@@ -215,10 +238,12 @@ frappe.ui.form.MultiSelectDialog = Class.extend({ head ? $row.addClass('list-item--head') : $row = $(`
`).append($row); - return $row; - }, - render_result_list: function(results, more = 0, empty=true) { + $(".modal-dialog .list-item--head").css("z-index", 0); + return $row; + } + + render_result_list(results, more = 0, empty = true) { var me = this; var more_btn = me.dialog.fields_dict.more_btn.$wrapper; @@ -240,44 +265,44 @@ frappe.ui.form.MultiSelectDialog = Class.extend({ }); if (frappe.flags.auto_scroll) { - this.$results.animate({scrollTop: me.$results.prop('scrollHeight')}, 500); + this.$results.animate({ scrollTop: me.$results.prop('scrollHeight') }, 500); } - }, + } - empty_list: function() { + empty_list() { + // Store all checked items let checked = this.get_checked_items().map(item => { return { ...item, checked: true - } + }; }); + + // Remove **all** items this.$results.find('.list-item-container').remove(); + + // Rerender checked items this.render_result_list(checked, 0, false); - }, + } - get_results: function() { + get_results() { let me = this; + let filters = this.get_query ? this.get_query().filters : {} || {}; + let filter_fields = []; - let filters = this.get_query ? this.get_query().filters : {}; - let filter_fields = [me.date_field]; - if($.isArray(this.setters)) { - for (let df of this.setters) { - filters[df.fieldname] = me.dialog.fields_dict[df.fieldname].get_value() || undefined; - me.args[df.fieldname] = filters[df.fieldname]; - filter_fields.push(df.fieldname); - } - } else { - Object.keys(this.setters).forEach(function(setter) { - filters[setter] = me.dialog.fields_dict[setter].get_value() || undefined; + Object.keys(this.setters).forEach(function (setter) { + var value = me.dialog.fields_dict[setter].get_value(); + if (me.dialog.fields_dict[setter].df.fieldtype == "Data" && value) { + filters[setter] = ["like", "%" + value + "%"]; + } else { + filters[setter] = value || undefined; me.args[setter] = filters[setter]; filter_fields.push(setter); - }); - } + } + }); - let date_val = this.dialog.fields_dict["date_range"].get_value(); - if(date_val) { - filters[this.date_field] = ['between', date_val]; - } + let filter_group = this.get_custom_filters(); + Object.assign(filters, filter_group); let args = { doctype: me.doctype, @@ -288,13 +313,13 @@ frappe.ui.form.MultiSelectDialog = Class.extend({ page_length: this.page_length + 1, query: this.get_query ? this.get_query().query : '', as_dict: 1 - } + }; frappe.call({ type: "GET", - method:'frappe.desk.search.search_widget', + method: 'frappe.desk.search.search_widget', no_spinner: true, args: args, - callback: function(r) { + callback: function (r) { let more = 0; me.results = []; if (r.values.length) { @@ -302,30 +327,13 @@ frappe.ui.form.MultiSelectDialog = Class.extend({ r.values.pop(); more = 1; } - r.values.forEach(function(result) { - if(me.date_field in result) { - result["Date"] = result[me.date_field] - } + r.values.forEach(function (result) { result.checked = 0; - result.parsed_date = Date.parse(result["Date"]); me.results.push(result); }); - me.results.map( (result) => { - result["Date"] = frappe.format(result["Date"], {"fieldtype":"Date"}); - }) - - me.results.sort((a, b) => { - return a.parsed_date - b.parsed_date; - }); - - // Preselect oldest entry - if (me.start < 1 && r.values.length === 1) { - me.results[0].checked = 1; - } } me.render_result_list(me.results, more); } }); - }, - -}); \ No newline at end of file + } +}; \ No newline at end of file diff --git a/frappe/public/js/frappe/utils/utils.js b/frappe/public/js/frappe/utils/utils.js index 7d2c20c693..50109f010d 100644 --- a/frappe/public/js/frappe/utils/utils.js +++ b/frappe/public/js/frappe/utils/utils.js @@ -823,3 +823,115 @@ if (!Array.prototype.uniqBy) { } }); } + +// Pluralize +String.prototype.plural = function(revert) { + const plural = { + "(quiz)$": "$1zes", + "^(ox)$": "$1en", + "([m|l])ouse$": "$1ice", + "(matr|vert|ind)ix|ex$": "$1ices", + "(x|ch|ss|sh)$": "$1es", + "([^aeiouy]|qu)y$": "$1ies", + "(hive)$": "$1s", + "(?:([^f])fe|([lr])f)$": "$1$2ves", + "(shea|lea|loa|thie)f$": "$1ves", + sis$: "ses", + "([ti])um$": "$1a", + "(tomat|potat|ech|her|vet)o$": "$1oes", + "(bu)s$": "$1ses", + "(alias)$": "$1es", + "(octop)us$": "$1i", + "(ax|test)is$": "$1es", + "(us)$": "$1es", + "([^s]+)$": "$1s", + }; + + const singular = { + "(quiz)zes$": "$1", + "(matr)ices$": "$1ix", + "(vert|ind)ices$": "$1ex", + "^(ox)en$": "$1", + "(alias)es$": "$1", + "(octop|vir)i$": "$1us", + "(cris|ax|test)es$": "$1is", + "(shoe)s$": "$1", + "(o)es$": "$1", + "(bus)es$": "$1", + "([m|l])ice$": "$1ouse", + "(x|ch|ss|sh)es$": "$1", + "(m)ovies$": "$1ovie", + "(s)eries$": "$1eries", + "([^aeiouy]|qu)ies$": "$1y", + "([lr])ves$": "$1f", + "(tive)s$": "$1", + "(hive)s$": "$1", + "(li|wi|kni)ves$": "$1fe", + "(shea|loa|lea|thie)ves$": "$1f", + "(^analy)ses$": "$1sis", + "((a)naly|(b)a|(d)iagno|(p)arenthe|(p)rogno|(s)ynop|(t)he)ses$": + "$1$2sis", + "([ti])a$": "$1um", + "(n)ews$": "$1ews", + "(h|bl)ouses$": "$1ouse", + "(corpse)s$": "$1", + "(us)es$": "$1", + s$: "", + }; + + const irregular = { + move: "moves", + foot: "feet", + goose: "geese", + sex: "sexes", + child: "children", + man: "men", + tooth: "teeth", + person: "people", + }; + + const uncountable = [ + "sheep", + "fish", + "deer", + "moose", + "series", + "species", + "money", + "rice", + "information", + "equipment", + ]; + + // save some time in the case that singular and plural are the same + if (uncountable.indexOf(this.toLowerCase()) >= 0) return this; + + // check for irregular forms + let word; + let pattern; + let replace; + for (word in irregular) { + if (revert) { + pattern = new RegExp(irregular[word] + "$", "i"); + replace = word; + } else { + pattern = new RegExp(word + "$", "i"); + replace = irregular[word]; + } + if (pattern.test(this)) return this.replace(pattern, replace); + } + + let array; + if (revert) array = singular; + else array = plural; + + // check for matches using regular expressions + let reg; + for (reg in array) { + pattern = new RegExp(reg, "i"); + + if (pattern.test(this)) return this.replace(pattern, array[reg]); + } + + return this; +}; \ No newline at end of file