浏览代码

Multiselect dialog for getting items (#3255)

* First approach by making a control

* Implement multi select for child tables

* Basic UI and items fetch in place

* Multiselect with checkboxes

* Functional modal with filters and new_doc

* Map filter fields to target_doc

* pass json arrays instead of strings

* Get items from quotation (in SO) working

* [minor] fix link route in list

* [minor] cleanup

* Add date, select first by default

* map_docs test, default date field

* [minor][fix] make new button bug

* [minor] move map_docs to erpnext

* [minor] format dates
version-14
Prateeksha Singh 8 年前
committed by Rushabh Mehta
父节点
当前提交
b23aa1446b
共有 9 个文件被更改,包括 263 次插入21 次删除
  1. +12
    -7
      frappe/desk/search.py
  2. +17
    -6
      frappe/model/mapper.py
  3. +2
    -0
      frappe/public/build.json
  4. +7
    -0
      frappe/public/css/desk.css
  5. +208
    -0
      frappe/public/js/frappe/form/multi_select_dialog.js
  6. +5
    -0
      frappe/public/js/frappe/model/model.js
  7. +0
    -1
      frappe/public/js/frappe/ui/field_group.js
  8. +1
    -5
      frappe/public/js/frappe/ui/toolbar/search_utils.js
  9. +11
    -2
      frappe/public/less/desk.less

+ 12
- 7
frappe/desk/search.py 查看文件

@@ -3,7 +3,7 @@

# Search
from __future__ import unicode_literals
import frappe
import frappe, json
from frappe.utils import cstr, unique

# this is called by the Link Field
@@ -16,7 +16,7 @@ def search_link(doctype, txt, query=None, filters=None, page_len=20, searchfield
# this is called by the search box
@frappe.whitelist()
def search_widget(doctype, txt, query=None, searchfield=None, start=0,
page_len=10, filters=None, as_dict=False):
page_len=10, filters=None, filter_fields=None, as_dict=False):
if isinstance(filters, basestring):
import json
filters = json.loads(filters)
@@ -76,20 +76,24 @@ def search_widget(doctype, txt, query=None, searchfield=None, start=0,
if meta.get("fields", {"fieldname":"disabled", "fieldtype":"Check"}):
filters.append([doctype, "disabled", "!=", 1])

# format a list of fields combining search fields and filter fields
fields = get_std_fields_list(meta, searchfield or "name")
if filter_fields:
fields = list(set(fields + json.loads(filter_fields)))
formatted_fields = ['`tab%s`.`%s`' % (meta.name, f.strip()) for f in fields]

# find relevance as location of search term from the beginning of string `name`. used for sorting results.
fields.append("""locate("{_txt}", `tab{doctype}`.`name`) as `_relevance`""".format(
formatted_fields.append("""locate("{_txt}", `tab{doctype}`.`name`) as `_relevance`""".format(
_txt=frappe.db.escape((txt or "").replace("%", "")), doctype=frappe.db.escape(doctype)))

# In order_by, `idx` gets second priority, because it stores link count
from frappe.model.db_query import get_order_by
order_by_based_on_meta = get_order_by(doctype, meta)
order_by = "if(_relevance, _relevance, 99999), idx desc, {0}".format(order_by_based_on_meta)
values = frappe.get_list(doctype,
filters=filters, fields=fields,
filters=filters, fields=formatted_fields,
or_filters = or_filters, limit_start = start,
limit_page_length=page_len,
order_by=order_by,
@@ -97,6 +101,7 @@ def search_widget(doctype, txt, query=None, searchfield=None, start=0,
as_list=not as_dict)

# remove _relevance from results
frappe.response["fields"] = fields
frappe.response["values"] = [r[:-1] for r in values]

def get_std_fields_list(meta, key):
@@ -107,7 +112,7 @@ def get_std_fields_list(meta, key):
if not key in sflist:
sflist = sflist + [key]

return ['`tab%s`.`%s`' % (meta.name, f.strip()) for f in sflist]
return sflist

def build_for_autosuggest(res):
results = []


+ 17
- 6
frappe/model/mapper.py 查看文件

@@ -25,16 +25,21 @@ def make_mapped_doc(method, source_name, selected_children=None):

return method(source_name)

@frappe.whitelist()
def map_docs(method, source_names, target_doc):
'''Returns the mapped document calling the given mapper method
with each of the given source docs on the target doc'''
method = frappe.get_attr(method)
if method not in frappe.whitelisted:
raise frappe.PermissionError

for src in json.loads(source_names):
target_doc = method(src, target_doc)
return target_doc

def get_mapped_doc(from_doctype, from_docname, table_maps, target_doc=None,
postprocess=None, ignore_permissions=False, ignore_child_tables=False):

source_doc = frappe.get_doc(from_doctype, from_docname)

if not ignore_permissions:
if not source_doc.has_permission("read"):
source_doc.raise_no_permission_to("read")

# main
if not target_doc:
target_doc = frappe.new_doc(table_maps[from_doctype]["doctype"])
@@ -44,6 +49,12 @@ def get_mapped_doc(from_doctype, from_docname, table_maps, target_doc=None,
if not ignore_permissions and not target_doc.has_permission("create"):
target_doc.raise_no_permission_to("create")

source_doc = frappe.get_doc(from_doctype, from_docname)

if not ignore_permissions:
if not source_doc.has_permission("read"):
source_doc.raise_no_permission_to("read")

map_doc(source_doc, target_doc, table_maps[source_doc.doctype])

row_exists_for_parentfield = {}


+ 2
- 0
frappe/public/build.json 查看文件

@@ -30,6 +30,7 @@
"public/js/frappe/ui/field_group.js",
"public/js/frappe/form/control.js",
"public/js/frappe/form/link_selector.js",
"public/js/frappe/form/multi_select_dialog.js",
"public/js/frappe/ui/dialog.js"
],
"css/desk.min.css": [
@@ -104,6 +105,7 @@
"public/js/frappe/ui/field_group.js",
"public/js/frappe/form/control.js",
"public/js/frappe/form/link_selector.js",
"public/js/frappe/form/multi_select_dialog.js",
"public/js/frappe/ui/dialog.js",
"public/js/frappe/ui/app_icon.js",



+ 7
- 0
frappe/public/css/desk.css 查看文件

@@ -978,6 +978,13 @@ input[type="checkbox"]:checked:before {
font-size: 13px;
color: #3b99fc;
}
.multiselect-empty-state {
min-height: 300px;
display: flex;
align-items: center;
justify-content: center;
height: 100%;
}
@-moz-document url-prefix() {
input[type="checkbox"] {
visibility: visible;


+ 208
- 0
frappe/public/js/frappe/form/multi_select_dialog.js 查看文件

@@ -0,0 +1,208 @@
// 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);

var me = this;
if(this.doctype!="[Select]") {
frappe.model.with_doctype(this.doctype, function(r) {
me.make();
});
} else {
this.make();
}
},
make: function() {
let me = this;

let fields = [];
let count = 0;
if(!this.date_field) {
this.date_field = "transaction_date";
}
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 - 1) {
fields.push({fieldtype: "Column Break"});
}
});

fields = fields.concat([
{ fieldtype: "Section Break" },
{ fieldtype: "HTML", fieldname: "results_area" },
{ fieldtype: "Button", fieldname: "make_new", label: __("Make a new " + me.doctype) }
]);

let doctype_plural = !this.doctype.endsWith('y') ? this.doctype + 's'
: this.doctype.slice(0, -1) + 'ies';

this.dialog = new frappe.ui.Dialog({
title: __("Select {0}", [(this.doctype=='[Select]') ? __("value") : __(doctype_plural)]),
fields: fields,
primary_action_label: __("Get Items"),
primary_action: function() {
me.action(me.get_checked_values(), me.args);
}
});

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>`);
this.$results = this.$wrapper.find('.results');
this.$make_new_btn = this.dialog.fields_dict.make_new.$wrapper;

this.$placeholder = $(`<div class="multiselect-empty-state">
<span class="text-center" style="margin-top: -40px;">
<i class="fa fa-2x fa-tags text-extra-muted"></i>
<p class="text-extra-muted">No ${this.doctype} found</p>
<button class="btn btn-default btn-xs text-muted" data-fieldtype="Button"
data-fieldname="make_new" placeholder="" value="">Make a new ${this.doctype}</button>
</span>
</div>`);

this.args = {};

this.bind_events();
this.get_results();
this.dialog.show();
},

bind_events: function() {
let me = this;
this.$results.on('click', '.list-item-container', function (e) {
if (!$(e.target).is(':checkbox') && !$(e.target).is('a')) {
$(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.get_results();
});
this.$parent.on('click', '.btn[data-fieldname="make_new"]', (e) => {
frappe.route_options = {};
Object.keys(this.setters).forEach(function(setter) {
frappe.route_options[setter] = me.dialog.fields_dict[setter].get_value() || undefined;
});
frappe.new_doc(this.doctype, true);
});
},

get_checked_values: function() {
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();
},

make_list_row: function(result={}) {
var me = this;
// Make a head row by default (if result not passed)
let head = Object.keys(result).length === 0;

let contents = ``;
let columns = (["name"].concat(Object.keys(this.setters))).concat("Date");
columns.forEach(function(column) {
contents += `<div class="list-item__content ellipsis">
${
head ? __(frappe.model.unscrub(column))

: (column !== "name" ? __(result[column])
: `<a href="${"#Form/"+ me.doctype + "/" + result[column]}" class="list-id">
${__(result[column])}</a>`)
}
</div>`;
})

let $row = $(`<div class="list-item">
<div class="list-item__content ellipsis" style="flex: 0 0 10px;">
<input type="checkbox" class="list-row-check" ${result.checked ? 'checked' : ''}/>
</div>
${contents}
</div>`);

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) {
var me = this;
this.$results.empty();
if(results.length === 0) {
this.$make_new_btn.addClass('hide');
this.$results.append(me.$placeholder);
return;
}
this.$make_new_btn.removeClass('hide');

this.$results.append(this.make_list_row());
results.forEach((result) => {
me.$results.append(me.make_list_row(result));
})
},

get_results: function() {
let me = this;

let filters = this.get_query().filters;
Object.keys(this.setters).forEach(function(setter) {
filters[setter] = me.dialog.fields_dict[setter].get_value() || undefined;
me.args[setter] = filters[setter];
});

let args = {
doctype: me.doctype,
txt: '',
filters: filters,
filter_fields: Object.keys(me.setters).concat([me.date_field])
}
frappe.call({
type: "GET",
method:'frappe.desk.search.search_widget',
no_spinner: true,
args: args,
callback: function(r) {
if(r.values) {
let results = [];
r.values.forEach(function(value_list) {
let result = {};
value_list.forEach(function(value, index){
if(r.fields[index] === me.date_field) {
result["Date"] = value;
} else {
result[r.fields[index]] = value;
}
});
result.checked = 0;
result.parsed_date = Date.parse(result["Date"]);
results.push(result);
});
let min_date = Math.min.apply( Math, results.map((result) => result.parsed_date) );

results.map( (result) => {
result["Date"] = frappe.format(result["Date"], {"fieldtype":"Date"});
if(result.parsed_date === min_date) {
result.checked = 1;
}
})
me.render_result_list(results);
}
}
});
},

});

+ 5
- 0
frappe/public/js/frappe/model/model.js 查看文件

@@ -187,6 +187,11 @@ $.extend(frappe.model, {
return txt.replace(/ /g, "_").toLowerCase();
},

unscrub: function(txt) {
return __(txt || '').replace(/-|_/g, " ").replace(/\w*/g,
function(keywords){return keywords.charAt(0).toUpperCase() + keywords.substr(1).toLowerCase();});
},

can_create: function(doctype) {
return frappe.boot.user.can_create.indexOf(doctype)!==-1;
},


+ 0
- 1
frappe/public/js/frappe/ui/field_group.js 查看文件

@@ -67,7 +67,6 @@ frappe.ui.FieldGroup = frappe.ui.form.Layout.extend({
var f = this.fields_dict[key];
if(f.get_parsed_value) {
var v = f.get_parsed_value();

if(f.df.reqd && is_null(v))
errors.push(__(f.df.label));



+ 1
- 5
frappe/public/js/frappe/ui/toolbar/search_utils.js 查看文件

@@ -575,10 +575,6 @@ frappe.search.utils = {
return rendered;
}

},
}

unscrub_and_titlecase: function(str) {
return __(str || '').replace(/-|_/g, " ").replace(/\w*/g,
function(keywords){return keywords.charAt(0).toUpperCase() + keywords.substr(1).toLowerCase();});
},
}

+ 11
- 2
frappe/public/less/desk.less 查看文件

@@ -431,7 +431,7 @@ textarea.form-control {
flex: 0 0 36px;
order: -1;
justify-content: flex-end;
input[type="checkbox"] {
margin-right: 0;
}
@@ -894,8 +894,17 @@ input[type="checkbox"] {
}
}

// Will not be required after commonifying lists with empty state
.multiselect-empty-state{
min-height: 300px;
display: flex;
align-items: center;
justify-content: center;
height: 100%;
}

// mozilla doesn't support pseudo elements on checkbox
// mozilla doesn't support
// pseudo elements on checkbox
@-moz-document url-prefix() {
input[type="checkbox"] {
visibility: visible;


正在加载...
取消
保存