Merge pull request #10206 from Anurag810/multi_select_dialog_redesign
feat: Redesign Multiselect Dialog
This commit is contained in:
commit
46e52d7d77
2 changed files with 283 additions and 163 deletions
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
@ -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;
|
||||
};
|
||||
Loading…
Add table
Reference in a new issue