Merge pull request #10206 from Anurag810/multi_select_dialog_redesign

feat: Redesign Multiselect Dialog
This commit is contained in:
mergify[bot] 2020-05-20 18:48:45 +00:00 committed by GitHub
commit 46e52d7d77
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 283 additions and 163 deletions

View file

@ -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(`<div class="results"
style="border: 1px solid #d1d8dd; border-radius: 3px; height: 300px; overflow: auto;"></div>`);
@ -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 += `<div class="list-item__content ellipsis">
${
head ? `<span class="ellipsis">${__(frappe.model.unscrub(column))}</span>`
: (column !== "name" ? `<span class="ellipsis">${__(result[column])}</span>`
: `<a href="${"#Form/"+ me.doctype + "/" + result[column]}" class="list-id ellipsis">
${__(result[column])}</a>`)
}
head ? `<span class="ellipsis text-muted" title="${__(frappe.model.unscrub(column))}">${__(frappe.model.unscrub(column))}</span>`
: (column !== "name" ? `<span class="ellipsis result-row" title="${__(result[column] || '')}">${__(result[column] || '')}</span>`
: `<a href="${"#Form/" + me.doctype + "/" + result[column] || ''}" class="list-id ellipsis" title="${__(result[column] || '')}">
${__(result[column] || '')}</a>`)}
</div>`;
})
});
let $row = $(`<div class="list-item">
<div class="list-item__content" style="flex: 0 0 10px;">
@ -215,10 +238,12 @@ frappe.ui.form.MultiSelectDialog = Class.extend({
head ? $row.addClass('list-item--head')
: $row = $(`<div class="list-item-container" data-item-name="${result.name}"></div>`).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);
}
});
},
});
}
};

View file

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