|
|
@@ -2,86 +2,191 @@ 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 () { |
|
|
|
me.make(); |
|
|
|
}); |
|
|
|
this.for_select = this.doctype == "[Select]"; |
|
|
|
if (!this.for_select) { |
|
|
|
frappe.model.with_doctype(this.doctype, () => this.init()); |
|
|
|
} else { |
|
|
|
this.make(); |
|
|
|
this.init(); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
make() { |
|
|
|
let me = this; |
|
|
|
init() { |
|
|
|
this.page_length = 20; |
|
|
|
this.start = 0; |
|
|
|
let fields = this.get_primary_filters(); |
|
|
|
this.fields = this.get_fields(); |
|
|
|
|
|
|
|
this.make(); |
|
|
|
} |
|
|
|
|
|
|
|
get_fields() { |
|
|
|
const primary_fields = this.get_primary_filters(); |
|
|
|
const result_fields = this.get_result_fields(); |
|
|
|
const data_fields = this.get_data_fields(); |
|
|
|
const child_selection_fields = this.get_child_selection_fields(); |
|
|
|
|
|
|
|
// Make results area |
|
|
|
fields = fields.concat([ |
|
|
|
{ fieldtype: "HTML", fieldname: "results_area" }, |
|
|
|
return [...primary_fields, ...result_fields, ...data_fields, ...child_selection_fields]; |
|
|
|
} |
|
|
|
|
|
|
|
get_result_fields() { |
|
|
|
const show_next_page = () => { |
|
|
|
this.start += 20; |
|
|
|
this.get_results(); |
|
|
|
}; |
|
|
|
return [ |
|
|
|
{ |
|
|
|
fieldtype: "Button", fieldname: "more_btn", label: __("More"), |
|
|
|
click: () => { |
|
|
|
this.start += 20; |
|
|
|
this.get_results(); |
|
|
|
} |
|
|
|
fieldtype: "HTML", fieldname: "results_area" |
|
|
|
}, |
|
|
|
{ |
|
|
|
fieldtype: "Button", fieldname: "more_btn", |
|
|
|
label: __("More"), click: show_next_page.bind(this) |
|
|
|
} |
|
|
|
]); |
|
|
|
]; |
|
|
|
} |
|
|
|
|
|
|
|
// Custom Data Fields |
|
|
|
if (this.data_fields) { |
|
|
|
fields.push({ fieldtype: "Section Break" }); |
|
|
|
fields = fields.concat(this.data_fields); |
|
|
|
get_data_fields() { |
|
|
|
if (this.data_fields && this.data_fields.length) { |
|
|
|
// Custom Data Fields |
|
|
|
return [ |
|
|
|
{ fieldtype: "Section Break" }, |
|
|
|
...this.data_fields |
|
|
|
]; |
|
|
|
} else { |
|
|
|
return []; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
get_child_selection_fields() { |
|
|
|
const fields = []; |
|
|
|
if (this.allow_child_item_selection && this.child_fieldname) { |
|
|
|
fields.push({ fieldtype: "HTML", fieldname: "child_selection_area" }); |
|
|
|
} |
|
|
|
return fields; |
|
|
|
} |
|
|
|
|
|
|
|
make() { |
|
|
|
let doctype_plural = this.doctype.plural(); |
|
|
|
let title = __("Select {0}", [this.for_select ? __("value") : __(doctype_plural)]); |
|
|
|
|
|
|
|
this.dialog = new frappe.ui.Dialog({ |
|
|
|
title: __("Select {0}", [(this.doctype == '[Select]') ? __("value") : __(doctype_plural)]), |
|
|
|
fields: fields, |
|
|
|
title: title, |
|
|
|
fields: this.fields, |
|
|
|
primary_action_label: this.primary_action_label || __("Get Items"), |
|
|
|
secondary_action_label: __("Make {0}", [__(me.doctype)]), |
|
|
|
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_label: __("Make {0}", [__(this.doctype)]), |
|
|
|
primary_action: () => { |
|
|
|
let filters_data = this.get_custom_filters(); |
|
|
|
const data_values = cur_dialog.get_values(); // to pass values of data fields |
|
|
|
const filtered_children = this.get_selected_child_names(); |
|
|
|
const selected_documents = [...this.get_checked_values(), ...this.get_parent_name_of_selected_children()]; |
|
|
|
this.action(selected_documents, { |
|
|
|
...this.args, |
|
|
|
...data_values, |
|
|
|
...filters_data, |
|
|
|
filtered_children |
|
|
|
}); |
|
|
|
}, |
|
|
|
secondary_action: function (e) { |
|
|
|
// If user wants to close the modal |
|
|
|
if (e) { |
|
|
|
frappe.route_options = {}; |
|
|
|
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) { |
|
|
|
frappe.route_options[setter] = me.dialog.fields_dict[setter].get_value() || undefined; |
|
|
|
}); |
|
|
|
} |
|
|
|
|
|
|
|
frappe.new_doc(me.doctype, true); |
|
|
|
} |
|
|
|
} |
|
|
|
secondary_action: this.make_new_document.bind(this) |
|
|
|
}); |
|
|
|
|
|
|
|
if (this.add_filters_group) { |
|
|
|
this.make_filter_area(); |
|
|
|
} |
|
|
|
|
|
|
|
this.args = {}; |
|
|
|
|
|
|
|
this.setup_results(); |
|
|
|
this.bind_events(); |
|
|
|
this.get_results(); |
|
|
|
this.dialog.show(); |
|
|
|
} |
|
|
|
|
|
|
|
make_new_document(e) { |
|
|
|
// If user wants to close the modal |
|
|
|
if (e) { |
|
|
|
this.set_route_options(); |
|
|
|
frappe.new_doc(this.doctype, true); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
set_route_options() { |
|
|
|
// set route options to get pre-filled form fields |
|
|
|
frappe.route_options = {}; |
|
|
|
if (Array.isArray(this.setters)) { |
|
|
|
for (let df of this.setters) { |
|
|
|
frappe.route_options[df.fieldname] = this.dialog.fields_dict[df.fieldname].get_value() || undefined; |
|
|
|
} |
|
|
|
} else { |
|
|
|
Object.keys(this.setters).forEach(setter => { |
|
|
|
frappe.route_options[setter] = this.dialog.fields_dict[setter].get_value() || undefined; |
|
|
|
}); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
setup_results() { |
|
|
|
this.$parent = $(this.dialog.body); |
|
|
|
this.$wrapper = this.dialog.fields_dict.results_area.$wrapper.append(`<div class="results" |
|
|
|
this.$wrapper = this.dialog.fields_dict.results_area.$wrapper.append(`<div class="results mt-3" |
|
|
|
style="border: 1px solid #d1d8dd; border-radius: 3px; height: 300px; overflow: auto;"></div>`); |
|
|
|
|
|
|
|
this.$results = this.$wrapper.find('.results'); |
|
|
|
this.$results.append(this.make_list_row()); |
|
|
|
} |
|
|
|
|
|
|
|
this.args = {}; |
|
|
|
toggle_child_selection() { |
|
|
|
if (this.dialog.fields_dict['allow_child_item_selection'].get_value()) { |
|
|
|
this.get_child_result().then(r => { |
|
|
|
this.child_results = r.message || []; |
|
|
|
this.render_child_datatable(); |
|
|
|
|
|
|
|
this.$wrapper.addClass('hidden'); |
|
|
|
this.$child_wrapper.removeClass('hidden'); |
|
|
|
this.dialog.fields_dict.more_btn.$wrapper.hide(); |
|
|
|
}); |
|
|
|
} else { |
|
|
|
this.child_results = []; |
|
|
|
this.get_results(); |
|
|
|
this.$wrapper.removeClass('hidden'); |
|
|
|
this.$child_wrapper.addClass('hidden'); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
this.bind_events(); |
|
|
|
this.get_results(); |
|
|
|
this.dialog.show(); |
|
|
|
render_child_datatable() { |
|
|
|
if (!this.child_datatable) { |
|
|
|
this.setup_child_datatable(); |
|
|
|
} else { |
|
|
|
setTimeout(() => { |
|
|
|
this.child_datatable.rowmanager.checkMap = []; |
|
|
|
this.child_datatable.refresh(this.get_child_datatable_rows()); |
|
|
|
this.$child_wrapper.find('.dt-scrollable').css('height', '300px'); |
|
|
|
}, 500); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
get_child_datatable_columns() { |
|
|
|
const parent = this.doctype; |
|
|
|
return [parent, ...this.child_columns].map(d => ({ name: frappe.unscrub(d), editable: false })); |
|
|
|
} |
|
|
|
|
|
|
|
get_child_datatable_rows() { |
|
|
|
return this.child_results.map(d => Object.values(d).slice(1)); // slice name field |
|
|
|
} |
|
|
|
|
|
|
|
setup_child_datatable() { |
|
|
|
const header_columns = this.get_child_datatable_columns(); |
|
|
|
const rows = this.get_child_datatable_rows(); |
|
|
|
this.$child_wrapper = this.dialog.fields_dict.child_selection_area.$wrapper; |
|
|
|
this.$child_wrapper.addClass('mt-3'); |
|
|
|
|
|
|
|
this.child_datatable = new frappe.DataTable(this.$child_wrapper.get(0), { |
|
|
|
columns: header_columns, |
|
|
|
data: rows, |
|
|
|
layout: 'fluid', |
|
|
|
inlineFilters: true, |
|
|
|
serialNoColumn: false, |
|
|
|
checkboxColumn: true, |
|
|
|
cellHeight: 35, |
|
|
|
noDataMessage: __('No Data'), |
|
|
|
disableReorderColumn: true |
|
|
|
}); |
|
|
|
this.$child_wrapper.find('.dt-scrollable').css('height', '300px'); |
|
|
|
} |
|
|
|
|
|
|
|
get_primary_filters() { |
|
|
@@ -94,7 +199,7 @@ frappe.ui.form.MultiSelectDialog = class MultiSelectDialog { |
|
|
|
columns[0] = [ |
|
|
|
{ |
|
|
|
fieldtype: "Data", |
|
|
|
label: __("Search"), |
|
|
|
label: __("Name"), |
|
|
|
fieldname: "search_term" |
|
|
|
} |
|
|
|
]; |
|
|
@@ -127,6 +232,16 @@ frappe.ui.form.MultiSelectDialog = class MultiSelectDialog { |
|
|
|
// now a is a fixed-size array with mutable entries |
|
|
|
} |
|
|
|
|
|
|
|
if (this.allow_child_item_selection) { |
|
|
|
this.child_doctype = frappe.meta.get_docfield(this.doctype, this.child_fieldname).options; |
|
|
|
columns[0].push({ |
|
|
|
fieldtype: "Check", |
|
|
|
label: __("Select {0}", [this.child_doctype]), |
|
|
|
fieldname: "allow_child_item_selection", |
|
|
|
onchange: this.toggle_child_selection.bind(this) |
|
|
|
}); |
|
|
|
} |
|
|
|
|
|
|
|
fields = [ |
|
|
|
...columns[0], |
|
|
|
{ fieldtype: "Column Break" }, |
|
|
@@ -156,6 +271,9 @@ frappe.ui.form.MultiSelectDialog = class MultiSelectDialog { |
|
|
|
this.get_results(); |
|
|
|
} |
|
|
|
}); |
|
|
|
// 'Apply Filter' breaks since the filers are not in a popover |
|
|
|
// Hence keeping it hidden |
|
|
|
this.filter_group.wrapper.find('.apply-filters').hide(); |
|
|
|
} |
|
|
|
|
|
|
|
get_custom_filters() { |
|
|
@@ -166,7 +284,7 @@ frappe.ui.form.MultiSelectDialog = class MultiSelectDialog { |
|
|
|
}); |
|
|
|
}, {}); |
|
|
|
} else { |
|
|
|
return []; |
|
|
|
return {}; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
@@ -200,6 +318,34 @@ frappe.ui.form.MultiSelectDialog = class MultiSelectDialog { |
|
|
|
}); |
|
|
|
} |
|
|
|
|
|
|
|
get_parent_name_of_selected_children() { |
|
|
|
if (!this.child_datatable || !this.child_datatable.datamanager.rows.length) return []; |
|
|
|
|
|
|
|
let parent_names = this.child_datatable.rowmanager.checkMap.reduce((parent_names, checked, index) => { |
|
|
|
if (checked == 1) { |
|
|
|
const parent_name = this.child_results[index].parent; |
|
|
|
parent_names.push(parent_name); |
|
|
|
} |
|
|
|
return parent_names; |
|
|
|
}, []); |
|
|
|
|
|
|
|
return parent_names; |
|
|
|
} |
|
|
|
|
|
|
|
get_selected_child_names() { |
|
|
|
if (!this.child_datatable || !this.child_datatable.datamanager.rows.length) return []; |
|
|
|
|
|
|
|
let checked_names = this.child_datatable.rowmanager.checkMap.reduce((checked_names, checked, index) => { |
|
|
|
if (checked == 1) { |
|
|
|
const child_row_name = this.child_results[index].name; |
|
|
|
checked_names.push(child_row_name); |
|
|
|
} |
|
|
|
return checked_names; |
|
|
|
}, []); |
|
|
|
|
|
|
|
return checked_names; |
|
|
|
} |
|
|
|
|
|
|
|
get_checked_values() { |
|
|
|
// Return name of checked value. |
|
|
|
return this.$results.find('.list-item-container').map(function () { |
|
|
@@ -276,6 +422,8 @@ frappe.ui.form.MultiSelectDialog = class MultiSelectDialog { |
|
|
|
me.$results.append(me.make_list_row(result)); |
|
|
|
}); |
|
|
|
|
|
|
|
this.$results.find(".list-item--head").css("z-index", 0); |
|
|
|
|
|
|
|
if (frappe.flags.auto_scroll) { |
|
|
|
this.$results.animate({ scrollTop: me.$results.prop('scrollHeight') }, 500); |
|
|
|
} |
|
|
@@ -297,7 +445,7 @@ frappe.ui.form.MultiSelectDialog = class MultiSelectDialog { |
|
|
|
this.render_result_list(checked, 0, false); |
|
|
|
} |
|
|
|
|
|
|
|
get_results() { |
|
|
|
get_filters_from_setters() { |
|
|
|
let me = this; |
|
|
|
let filters = this.get_query ? this.get_query().filters : {} || {}; |
|
|
|
let filter_fields = []; |
|
|
@@ -321,12 +469,18 @@ frappe.ui.form.MultiSelectDialog = class MultiSelectDialog { |
|
|
|
}); |
|
|
|
} |
|
|
|
|
|
|
|
let filter_group = this.get_custom_filters(); |
|
|
|
Object.assign(filters, filter_group); |
|
|
|
return [filters, filter_fields]; |
|
|
|
} |
|
|
|
|
|
|
|
get_args_for_search() { |
|
|
|
let [filters, filter_fields] = this.get_filters_from_setters(); |
|
|
|
|
|
|
|
let custom_filters = this.get_custom_filters(); |
|
|
|
Object.assign(filters, custom_filters); |
|
|
|
|
|
|
|
let args = { |
|
|
|
doctype: me.doctype, |
|
|
|
txt: me.dialog.fields_dict["search_term"].get_value(), |
|
|
|
return { |
|
|
|
doctype: this.doctype, |
|
|
|
txt: this.dialog.fields_dict["search_term"].get_value(), |
|
|
|
filters: filters, |
|
|
|
filter_fields: filter_fields, |
|
|
|
start: this.start, |
|
|
@@ -334,25 +488,81 @@ frappe.ui.form.MultiSelectDialog = class MultiSelectDialog { |
|
|
|
query: this.get_query ? this.get_query().query : '', |
|
|
|
as_dict: 1 |
|
|
|
}; |
|
|
|
frappe.call({ |
|
|
|
} |
|
|
|
|
|
|
|
async perform_search(args) { |
|
|
|
const res = await frappe.call({ |
|
|
|
type: "GET", |
|
|
|
method: 'frappe.desk.search.search_widget', |
|
|
|
no_spinner: true, |
|
|
|
args: args, |
|
|
|
callback: function (r) { |
|
|
|
let more = 0; |
|
|
|
me.results = []; |
|
|
|
if (r.values.length) { |
|
|
|
if (r.values.length > me.page_length) { |
|
|
|
r.values.pop(); |
|
|
|
more = 1; |
|
|
|
} |
|
|
|
r.values.forEach(function (result) { |
|
|
|
result.checked = 0; |
|
|
|
me.results.push(result); |
|
|
|
}); |
|
|
|
}); |
|
|
|
const more = res.values.length && res.values.length > this.page_length ? 1 : 0; |
|
|
|
if (more) { |
|
|
|
res.values.pop(); |
|
|
|
} |
|
|
|
|
|
|
|
return [res, more]; |
|
|
|
} |
|
|
|
|
|
|
|
async get_results() { |
|
|
|
const args = this.get_args_for_search(); |
|
|
|
const [res, more] = await this.perform_search(args); |
|
|
|
|
|
|
|
this.results = []; |
|
|
|
if (res.values.length) { |
|
|
|
res.values.forEach(result => { |
|
|
|
result.checked = 0; |
|
|
|
this.results.push(result); |
|
|
|
}); |
|
|
|
} |
|
|
|
this.render_result_list(this.results, more); |
|
|
|
} |
|
|
|
|
|
|
|
async get_filtered_parents_for_child_search() { |
|
|
|
const parent_search_args = this.get_args_for_search(); |
|
|
|
parent_search_args.filter_fields = ['name']; |
|
|
|
// eslint-disable-next-line no-unused-vars |
|
|
|
const [response, _] = await this.perform_search(parent_search_args); |
|
|
|
|
|
|
|
let parent_names = []; |
|
|
|
if (response.values.length) { |
|
|
|
parent_names = response.values.map(v => v.name); |
|
|
|
} |
|
|
|
return parent_names; |
|
|
|
} |
|
|
|
|
|
|
|
async add_parent_filters(filters) { |
|
|
|
const parent_names = await this.get_filtered_parents_for_child_search(); |
|
|
|
if (parent_names.length) { |
|
|
|
filters.push([ "parent", "in", parent_names ]); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
add_custom_child_filters(filters) { |
|
|
|
if (this.add_filters_group && this.filter_group) { |
|
|
|
this.filter_group.get_filters().forEach(filter => { |
|
|
|
if (filter[0] == this.child_doctype) { |
|
|
|
filters.push([filter[1], filter[2], filter[3]]); |
|
|
|
} |
|
|
|
me.render_result_list(me.results, more); |
|
|
|
}); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
async get_child_result() { |
|
|
|
let filters = [["parentfield", "=", this.child_fieldname]]; |
|
|
|
|
|
|
|
await this.add_parent_filters(filters); |
|
|
|
this.add_custom_child_filters(filters); |
|
|
|
|
|
|
|
return frappe.call({ |
|
|
|
method: "frappe.client.get_list", |
|
|
|
args: { |
|
|
|
doctype: this.child_doctype, |
|
|
|
filters: filters, |
|
|
|
fields: ['name', 'parent', ...this.child_columns], |
|
|
|
parent: this.doctype, |
|
|
|
order_by: 'parent' |
|
|
|
} |
|
|
|
}); |
|
|
|
} |
|
|
|