2013-08-06 12:50:22 +00:00
|
|
|
|
|
|
|
(function() {
|
|
|
|
|
|
|
|
var instance = openerp;
|
|
|
|
openerp.web.search = {};
|
|
|
|
|
2012-04-17 12:02:10 +00:00
|
|
|
var QWeb = instance.web.qweb,
|
|
|
|
_t = instance.web._t,
|
|
|
|
_lt = instance.web._lt;
|
2012-03-26 14:50:17 +00:00
|
|
|
_.mixin({
|
|
|
|
sum: function (obj) { return _.reduce(obj, function (a, b) { return a + b; }, 0); }
|
|
|
|
});
|
2011-03-29 13:18:54 +00:00
|
|
|
|
2012-04-24 10:59:41 +00:00
|
|
|
/** @namespace */
|
|
|
|
var my = instance.web.search = {};
|
|
|
|
|
2012-04-27 15:46:14 +00:00
|
|
|
var B = Backbone;
|
|
|
|
my.FacetValue = B.Model.extend({
|
2012-03-19 16:43:04 +00:00
|
|
|
|
2012-04-24 10:59:41 +00:00
|
|
|
});
|
2012-04-27 15:46:14 +00:00
|
|
|
my.FacetValues = B.Collection.extend({
|
2012-04-24 10:59:41 +00:00
|
|
|
model: my.FacetValue
|
|
|
|
});
|
2012-04-27 15:46:14 +00:00
|
|
|
my.Facet = B.Model.extend({
|
2012-04-24 10:59:41 +00:00
|
|
|
initialize: function (attrs) {
|
|
|
|
var values = attrs.values;
|
|
|
|
delete attrs.values;
|
2012-03-19 16:43:04 +00:00
|
|
|
|
2012-04-27 15:46:14 +00:00
|
|
|
B.Model.prototype.initialize.apply(this, arguments);
|
2012-03-19 16:43:04 +00:00
|
|
|
|
2012-04-24 10:59:41 +00:00
|
|
|
this.values = new my.FacetValues(values || []);
|
2014-04-22 15:03:16 +00:00
|
|
|
this.values.on('add remove change reset', function (_, options) {
|
|
|
|
this.trigger('change', this, options);
|
2012-04-24 10:59:41 +00:00
|
|
|
}, this);
|
|
|
|
},
|
|
|
|
get: function (key) {
|
|
|
|
if (key !== 'values') {
|
2012-04-27 15:46:14 +00:00
|
|
|
return B.Model.prototype.get.call(this, key);
|
2012-04-24 10:59:41 +00:00
|
|
|
}
|
|
|
|
return this.values.toJSON();
|
|
|
|
},
|
|
|
|
set: function (key, value) {
|
|
|
|
if (key !== 'values') {
|
2012-04-27 15:46:14 +00:00
|
|
|
return B.Model.prototype.set.call(this, key, value);
|
2012-04-24 10:59:41 +00:00
|
|
|
}
|
|
|
|
this.values.reset(value);
|
2012-04-26 10:29:49 +00:00
|
|
|
},
|
|
|
|
toJSON: function () {
|
|
|
|
var out = {};
|
|
|
|
var attrs = this.attributes;
|
|
|
|
for(var att in attrs) {
|
|
|
|
if (!attrs.hasOwnProperty(att) || att === 'field') {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
out[att] = attrs[att];
|
|
|
|
}
|
|
|
|
out.values = this.values.toJSON();
|
|
|
|
return out;
|
2012-04-24 10:59:41 +00:00
|
|
|
}
|
2012-03-19 16:43:04 +00:00
|
|
|
});
|
2012-04-27 15:46:14 +00:00
|
|
|
my.SearchQuery = B.Collection.extend({
|
2012-04-24 10:59:41 +00:00
|
|
|
model: my.Facet,
|
|
|
|
initialize: function () {
|
2012-04-27 15:46:14 +00:00
|
|
|
B.Collection.prototype.initialize.apply(
|
2012-04-24 10:59:41 +00:00
|
|
|
this, arguments);
|
|
|
|
this.on('change', function (facet) {
|
|
|
|
if(!facet.values.isEmpty()) { return; }
|
|
|
|
|
2012-10-18 12:40:05 +00:00
|
|
|
this.remove(facet, {silent: true});
|
2012-04-24 10:59:41 +00:00
|
|
|
}, this);
|
|
|
|
},
|
2012-04-24 12:38:24 +00:00
|
|
|
add: function (values, options) {
|
2013-07-25 10:07:49 +00:00
|
|
|
options = options || {};
|
2013-12-02 15:10:37 +00:00
|
|
|
|
|
|
|
if (!values) {
|
|
|
|
values = [];
|
|
|
|
} else if (!(values instanceof Array)) {
|
2012-04-24 12:38:24 +00:00
|
|
|
values = [values];
|
2012-03-20 14:47:38 +00:00
|
|
|
}
|
2012-04-24 12:38:24 +00:00
|
|
|
|
|
|
|
_(values).each(function (value) {
|
|
|
|
var model = this._prepareModel(value, options);
|
|
|
|
var previous = this.detect(function (facet) {
|
|
|
|
return facet.get('category') === model.get('category')
|
|
|
|
&& facet.get('field') === model.get('field');
|
|
|
|
});
|
|
|
|
if (previous) {
|
2014-01-10 15:48:12 +00:00
|
|
|
previous.values.add(model.get('values'), _.omit(options, 'at', 'merge'));
|
2012-04-24 12:38:24 +00:00
|
|
|
return;
|
|
|
|
}
|
2012-04-27 15:46:14 +00:00
|
|
|
B.Collection.prototype.add.call(this, model, options);
|
2012-04-24 12:38:24 +00:00
|
|
|
}, this);
|
2013-12-02 15:18:38 +00:00
|
|
|
// warning: in backbone 1.0+ add is supposed to return the added models,
|
|
|
|
// but here toggle may delegate to add and return its value directly.
|
|
|
|
// return value of neither seems actually used but should be tested
|
|
|
|
// before change, probably
|
2012-04-24 12:38:24 +00:00
|
|
|
return this;
|
2012-04-24 10:59:41 +00:00
|
|
|
},
|
|
|
|
toggle: function (value, options) {
|
2013-07-25 10:07:49 +00:00
|
|
|
options = options || {};
|
2012-04-24 10:59:41 +00:00
|
|
|
|
|
|
|
var facet = this.detect(function (facet) {
|
|
|
|
return facet.get('category') === value.category
|
|
|
|
&& facet.get('field') === value.field;
|
|
|
|
});
|
|
|
|
if (!facet) {
|
|
|
|
return this.add(value, options);
|
|
|
|
}
|
|
|
|
|
|
|
|
var changed = false;
|
|
|
|
_(value.values).each(function (val) {
|
|
|
|
var already_value = facet.values.detect(function (v) {
|
|
|
|
return v.get('value') === val.value
|
|
|
|
&& v.get('label') === val.label;
|
|
|
|
});
|
|
|
|
// toggle value
|
|
|
|
if (already_value) {
|
|
|
|
facet.values.remove(already_value, {silent: true});
|
|
|
|
} else {
|
|
|
|
facet.values.add(val, {silent: true});
|
|
|
|
}
|
|
|
|
changed = true;
|
|
|
|
});
|
|
|
|
// "Commit" changes to values array as a single call, so observers of
|
|
|
|
// change event don't get misled by intermediate incomplete toggling
|
|
|
|
// states
|
|
|
|
facet.trigger('change', facet);
|
|
|
|
return this;
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
2012-05-09 13:32:41 +00:00
|
|
|
function assert(condition, message) {
|
|
|
|
if(!condition) {
|
|
|
|
throw new Error(message);
|
|
|
|
}
|
|
|
|
}
|
2012-04-24 10:59:41 +00:00
|
|
|
my.InputView = instance.web.Widget.extend({
|
2012-04-30 15:59:12 +00:00
|
|
|
template: 'SearchView.InputView',
|
2012-11-06 15:04:49 +00:00
|
|
|
events: {
|
|
|
|
focus: function () { this.trigger('focused', this); },
|
|
|
|
blur: function () { this.$el.text(''); this.trigger('blurred', this); },
|
2013-01-23 09:16:00 +00:00
|
|
|
keydown: 'onKeydown',
|
|
|
|
paste: 'onPaste',
|
2012-05-09 13:32:41 +00:00
|
|
|
},
|
|
|
|
getSelection: function () {
|
2013-06-04 15:05:51 +00:00
|
|
|
this.el.normalize();
|
2012-05-09 13:32:41 +00:00
|
|
|
// get Text node
|
2013-01-23 09:29:46 +00:00
|
|
|
var root = this.el.childNodes[0];
|
2012-05-09 13:32:41 +00:00
|
|
|
if (!root || !root.textContent) {
|
|
|
|
// if input does not have a child node, or the child node is an
|
|
|
|
// empty string, then the selection can only be (0, 0)
|
|
|
|
return {start: 0, end: 0};
|
|
|
|
}
|
2013-01-23 09:29:46 +00:00
|
|
|
var range = window.getSelection().getRangeAt(0);
|
2013-06-04 15:05:51 +00:00
|
|
|
// In Firefox, depending on the way text is selected (drag, double- or
|
|
|
|
// triple-click) the range may start or end on the parent of the
|
|
|
|
// selected text node‽ Check for this condition and fixup the range
|
|
|
|
// note: apparently with C-a this can go even higher?
|
|
|
|
if (range.startContainer === this.el && range.startOffset === 0) {
|
|
|
|
range.setStart(root, 0);
|
|
|
|
}
|
|
|
|
if (range.endContainer === this.el && range.endOffset === 1) {
|
2013-07-25 10:33:01 +00:00
|
|
|
range.setEnd(root, root.length);
|
2013-06-04 15:05:51 +00:00
|
|
|
}
|
2013-01-23 09:29:46 +00:00
|
|
|
assert(range.startContainer === root,
|
|
|
|
"selection should be in the input view");
|
|
|
|
assert(range.endContainer === root,
|
|
|
|
"selection should be in the input view");
|
|
|
|
return {
|
|
|
|
start: range.startOffset,
|
|
|
|
end: range.endOffset
|
2013-07-25 10:33:01 +00:00
|
|
|
};
|
2012-05-09 13:32:41 +00:00
|
|
|
},
|
|
|
|
onKeydown: function (e) {
|
2013-06-04 15:05:51 +00:00
|
|
|
this.el.normalize();
|
2012-05-09 13:32:41 +00:00
|
|
|
var sel;
|
|
|
|
switch (e.which) {
|
|
|
|
// Do not insert newline, but let it bubble so searchview can use it
|
|
|
|
case $.ui.keyCode.ENTER:
|
|
|
|
e.preventDefault();
|
|
|
|
break;
|
|
|
|
|
2012-05-09 14:12:42 +00:00
|
|
|
// FIXME: may forget content if non-empty but caret at index 0, ok?
|
|
|
|
case $.ui.keyCode.BACKSPACE:
|
|
|
|
sel = this.getSelection();
|
|
|
|
if (sel.start === 0 && sel.start === sel.end) {
|
|
|
|
e.preventDefault();
|
|
|
|
var preceding = this.getParent().siblingSubview(this, -1);
|
|
|
|
if (preceding && (preceding instanceof my.FacetView)) {
|
|
|
|
preceding.model.destroy();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
|
2012-05-09 13:32:41 +00:00
|
|
|
// let left/right events propagate to view if caret is at input border
|
|
|
|
// and not a selection
|
|
|
|
case $.ui.keyCode.LEFT:
|
|
|
|
sel = this.getSelection();
|
|
|
|
if (sel.start !== 0 || sel.start !== sel.end) {
|
|
|
|
e.stopPropagation();
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
case $.ui.keyCode.RIGHT:
|
|
|
|
sel = this.getSelection();
|
2012-08-24 18:27:07 +00:00
|
|
|
var len = this.$el.text().length;
|
2012-05-09 13:32:41 +00:00
|
|
|
if (sel.start !== len || sel.start !== sel.end) {
|
|
|
|
e.stopPropagation();
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
}
|
2013-01-23 09:16:00 +00:00
|
|
|
},
|
|
|
|
setCursorAtEnd: function () {
|
2013-06-04 15:05:51 +00:00
|
|
|
this.el.normalize();
|
2013-01-23 09:16:00 +00:00
|
|
|
var sel = window.getSelection();
|
|
|
|
sel.removeAllRanges();
|
|
|
|
var range = document.createRange();
|
|
|
|
// in theory, range.selectNodeContents should work here. In practice,
|
|
|
|
// MSIE9 has issues from time to time, instead of selecting the inner
|
|
|
|
// text node it would select the reference node instead (e.g. in demo
|
|
|
|
// data, company news, copy across the "Company News" link + the title,
|
|
|
|
// from about half the link to half the text, paste in search box then
|
|
|
|
// hit the left arrow key, getSelection would blow up).
|
|
|
|
//
|
2013-06-04 15:05:51 +00:00
|
|
|
// Explicitly selecting only the inner text node (only child node
|
|
|
|
// since we've normalized the parent) avoids the issue
|
2013-01-23 09:16:00 +00:00
|
|
|
range.selectNode(this.el.childNodes[0]);
|
|
|
|
range.collapse(false);
|
|
|
|
sel.addRange(range);
|
|
|
|
},
|
|
|
|
onPaste: function () {
|
2013-06-04 15:05:51 +00:00
|
|
|
this.el.normalize();
|
2013-01-23 09:16:00 +00:00
|
|
|
// In MSIE and Webkit, it is possible to get various representations of
|
|
|
|
// the clipboard data at this point e.g.
|
|
|
|
// window.clipboardData.getData('Text') and
|
|
|
|
// event.clipboardData.getData('text/plain') to ensure we have a plain
|
|
|
|
// text representation of the object (and probably ensure the object is
|
|
|
|
// pastable as well, so nobody puts an image in the search view)
|
|
|
|
// (nb: since it's not possible to alter the content of the clipboard
|
|
|
|
// — at least in Webkit — to ensure only textual content is available,
|
|
|
|
// using this would require 1. getting the text data; 2. manually
|
|
|
|
// inserting the text data into the content; and 3. cancelling the
|
|
|
|
// paste event)
|
|
|
|
//
|
|
|
|
// But Firefox doesn't support the clipboard API (as of FF18)
|
|
|
|
// although it correctly triggers the paste event (Opera does not even
|
|
|
|
// do that) => implement lowest-denominator system where onPaste
|
|
|
|
// triggers a followup "cleanup" pass after the data has been pasted
|
|
|
|
setTimeout(function () {
|
|
|
|
// Read text content (ignore pasted HTML)
|
|
|
|
var data = this.$el.text();
|
2013-09-02 14:21:33 +00:00
|
|
|
if (!data)
|
|
|
|
return;
|
2013-01-23 09:16:00 +00:00
|
|
|
// paste raw text back in
|
|
|
|
this.$el.empty().text(data);
|
2013-06-04 15:05:51 +00:00
|
|
|
this.el.normalize();
|
2013-01-23 09:16:00 +00:00
|
|
|
// Set the cursor at the end of the text, so the cursor is not lost
|
|
|
|
// in some kind of error-spawning limbo.
|
|
|
|
this.setCursorAtEnd();
|
|
|
|
}.bind(this), 0);
|
2012-04-30 15:59:12 +00:00
|
|
|
}
|
2012-04-24 10:59:41 +00:00
|
|
|
});
|
|
|
|
my.FacetView = instance.web.Widget.extend({
|
|
|
|
template: 'SearchView.FacetView',
|
2012-11-06 15:04:49 +00:00
|
|
|
events: {
|
|
|
|
'focus': function () { this.trigger('focused', this); },
|
|
|
|
'blur': function () { this.trigger('blurred', this); },
|
|
|
|
'click': function (e) {
|
|
|
|
if ($(e.target).is('.oe_facet_remove')) {
|
|
|
|
this.model.destroy();
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
this.$el.focus();
|
|
|
|
e.stopPropagation();
|
|
|
|
},
|
|
|
|
'keydown': function (e) {
|
|
|
|
var keys = $.ui.keyCode;
|
|
|
|
switch (e.which) {
|
|
|
|
case keys.BACKSPACE:
|
|
|
|
case keys.DELETE:
|
|
|
|
this.model.destroy();
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
},
|
2012-04-24 10:59:41 +00:00
|
|
|
init: function (parent, model) {
|
|
|
|
this._super(parent);
|
|
|
|
this.model = model;
|
|
|
|
this.model.on('change', this.model_changed, this);
|
|
|
|
},
|
|
|
|
destroy: function () {
|
|
|
|
this.model.off('change', this.model_changed, this);
|
|
|
|
this._super();
|
|
|
|
},
|
|
|
|
start: function () {
|
|
|
|
var self = this;
|
2012-11-06 15:21:48 +00:00
|
|
|
var $e = this.$('> span:last-child');
|
2012-11-07 09:10:47 +00:00
|
|
|
return $.when(this._super()).then(function () {
|
2012-11-06 15:04:49 +00:00
|
|
|
return $.when.apply(null, self.model.values.map(function (value) {
|
2012-04-24 10:59:41 +00:00
|
|
|
return new my.FacetValueView(self, value).appendTo($e);
|
2012-11-06 15:04:49 +00:00
|
|
|
}));
|
2012-04-24 10:59:41 +00:00
|
|
|
});
|
|
|
|
},
|
|
|
|
model_changed: function () {
|
2012-08-24 18:27:07 +00:00
|
|
|
this.$el.text(this.$el.text() + '*');
|
2012-04-24 10:59:41 +00:00
|
|
|
}
|
|
|
|
});
|
|
|
|
my.FacetValueView = instance.web.Widget.extend({
|
|
|
|
template: 'SearchView.FacetView.Value',
|
|
|
|
init: function (parent, model) {
|
|
|
|
this._super(parent);
|
|
|
|
this.model = model;
|
|
|
|
this.model.on('change', this.model_changed, this);
|
|
|
|
},
|
|
|
|
destroy: function () {
|
|
|
|
this.model.off('change', this.model_changed, this);
|
|
|
|
this._super();
|
|
|
|
},
|
|
|
|
model_changed: function () {
|
2012-08-24 18:27:07 +00:00
|
|
|
this.$el.text(this.$el.text() + '*');
|
2012-03-20 14:47:38 +00:00
|
|
|
}
|
|
|
|
});
|
2012-03-16 11:16:08 +00:00
|
|
|
|
2012-04-17 12:02:10 +00:00
|
|
|
instance.web.SearchView = instance.web.Widget.extend(/** @lends instance.web.SearchView# */{
|
2012-03-19 16:43:04 +00:00
|
|
|
template: "SearchView",
|
2012-11-06 15:04:49 +00:00
|
|
|
events: {
|
|
|
|
// focus last input if view itself is clicked
|
|
|
|
'click': function (e) {
|
2012-11-06 15:21:48 +00:00
|
|
|
if (e.target === this.$('.oe_searchview_facets')[0]) {
|
|
|
|
this.$('.oe_searchview_input:last').focus();
|
2012-11-06 15:04:49 +00:00
|
|
|
}
|
|
|
|
},
|
|
|
|
// search button
|
|
|
|
'click button.oe_searchview_search': function (e) {
|
|
|
|
e.stopImmediatePropagation();
|
|
|
|
this.do_search();
|
|
|
|
},
|
|
|
|
'click .oe_searchview_clear': function (e) {
|
|
|
|
e.stopImmediatePropagation();
|
|
|
|
this.query.reset();
|
|
|
|
},
|
|
|
|
'click .oe_searchview_unfold_drawer': function (e) {
|
|
|
|
e.stopImmediatePropagation();
|
2014-05-20 08:54:58 +00:00
|
|
|
if (this.drawer)
|
|
|
|
this.drawer.toggle();
|
2012-11-06 15:04:49 +00:00
|
|
|
},
|
|
|
|
'keydown .oe_searchview_input, .oe_searchview_facet': function (e) {
|
|
|
|
switch(e.which) {
|
|
|
|
case $.ui.keyCode.LEFT:
|
2014-06-19 10:43:25 +00:00
|
|
|
this.focusPreceding(e.target);
|
2012-11-06 15:04:49 +00:00
|
|
|
e.preventDefault();
|
|
|
|
break;
|
|
|
|
case $.ui.keyCode.RIGHT:
|
2014-06-25 11:26:41 +00:00
|
|
|
if (!this.autocomplete.is_expandable()) {
|
|
|
|
this.focusFollowing(e.target);
|
|
|
|
}
|
2012-11-06 15:04:49 +00:00
|
|
|
e.preventDefault();
|
|
|
|
break;
|
|
|
|
}
|
2013-03-06 11:00:51 +00:00
|
|
|
},
|
|
|
|
'autocompleteopen': function () {
|
2014-03-11 10:49:47 +00:00
|
|
|
this.$el.autocomplete('widget').css('z-index', 9999);
|
2013-03-06 11:00:51 +00:00
|
|
|
},
|
2012-11-06 15:04:49 +00:00
|
|
|
},
|
2011-09-12 12:06:04 +00:00
|
|
|
/**
|
2012-04-17 12:02:10 +00:00
|
|
|
* @constructs instance.web.SearchView
|
2012-04-24 10:59:41 +00:00
|
|
|
* @extends instance.web.Widget
|
2011-12-09 12:35:14 +00:00
|
|
|
*
|
2011-09-12 12:06:04 +00:00
|
|
|
* @param parent
|
|
|
|
* @param dataset
|
|
|
|
* @param view_id
|
|
|
|
* @param defaults
|
2013-01-14 09:30:14 +00:00
|
|
|
* @param {Object} [options]
|
|
|
|
* @param {Boolean} [options.hidden=false] hide the search view
|
|
|
|
* @param {Boolean} [options.disable_custom_filters=false] do not load custom filters from ir.filters
|
2011-09-12 12:06:04 +00:00
|
|
|
*/
|
2013-01-14 09:30:14 +00:00
|
|
|
init: function(parent, dataset, view_id, defaults, options) {
|
|
|
|
this.options = _.defaults(options || {}, {
|
|
|
|
hidden: false,
|
|
|
|
disable_custom_filters: false,
|
|
|
|
});
|
2011-09-15 09:52:14 +00:00
|
|
|
this._super(parent);
|
2011-03-17 13:41:55 +00:00
|
|
|
this.dataset = dataset;
|
|
|
|
this.model = dataset.model;
|
|
|
|
this.view_id = view_id;
|
2011-03-24 16:18:42 +00:00
|
|
|
|
2011-03-25 10:34:25 +00:00
|
|
|
this.defaults = defaults || {};
|
2011-09-28 15:13:06 +00:00
|
|
|
this.has_defaults = !_.isEmpty(this.defaults);
|
2011-03-25 10:34:25 +00:00
|
|
|
|
2013-01-14 09:30:14 +00:00
|
|
|
this.headless = this.options.hidden && !this.has_defaults;
|
2011-09-28 15:13:06 +00:00
|
|
|
|
2012-04-24 10:59:41 +00:00
|
|
|
this.input_subviews = [];
|
2014-05-27 12:21:52 +00:00
|
|
|
this.view_manager = null;
|
|
|
|
this.$view_manager_header = null;
|
2012-04-24 10:59:41 +00:00
|
|
|
|
2011-07-15 11:58:00 +00:00
|
|
|
this.ready = $.Deferred();
|
2014-05-19 12:16:09 +00:00
|
|
|
this.drawer_ready = $.Deferred();
|
|
|
|
this.fields_view_get = $.Deferred();
|
2014-05-20 13:27:44 +00:00
|
|
|
this.drawer = new instance.web.SearchViewDrawer(parent, this);
|
|
|
|
|
2011-03-17 13:41:55 +00:00
|
|
|
},
|
|
|
|
start: function() {
|
2012-03-21 15:45:50 +00:00
|
|
|
var self = this;
|
2012-03-09 13:51:22 +00:00
|
|
|
var p = this._super();
|
|
|
|
|
2014-05-27 12:21:52 +00:00
|
|
|
this.$view_manager_header = this.$el.parents(".oe_view_manager_header").first();
|
|
|
|
|
2012-03-19 16:43:04 +00:00
|
|
|
this.setup_global_completion();
|
2012-04-24 10:59:41 +00:00
|
|
|
this.query = new my.SearchQuery()
|
2012-05-03 11:56:08 +00:00
|
|
|
.on('add change reset remove', this.proxy('do_search'))
|
2014-04-22 15:03:16 +00:00
|
|
|
.on('change', this.proxy('renderChangedFacets'))
|
2014-06-10 14:22:02 +00:00
|
|
|
.on('add reset remove', this.proxy('renderFacets'));
|
2012-03-09 13:51:22 +00:00
|
|
|
|
2013-01-14 09:30:14 +00:00
|
|
|
if (this.options.hidden) {
|
2012-08-24 18:27:07 +00:00
|
|
|
this.$el.hide();
|
2011-09-28 15:13:06 +00:00
|
|
|
}
|
|
|
|
if (this.headless) {
|
|
|
|
this.ready.resolve();
|
|
|
|
} else {
|
2012-12-04 16:39:48 +00:00
|
|
|
var load_view = instance.web.fields_view_get({
|
|
|
|
model: this.dataset._model,
|
2011-11-16 13:38:03 +00:00
|
|
|
view_id: this.view_id,
|
2012-11-26 09:38:18 +00:00
|
|
|
view_type: 'search',
|
2013-03-18 17:45:03 +00:00
|
|
|
context: this.dataset.get_context(),
|
2012-11-26 09:38:18 +00:00
|
|
|
});
|
|
|
|
|
2014-02-04 15:05:44 +00:00
|
|
|
this.alive($.when(load_view)).then(function (r) {
|
2014-05-19 12:16:09 +00:00
|
|
|
self.fields_view_get.resolve(r);
|
2013-07-25 10:33:01 +00:00
|
|
|
return self.search_view_loaded(r);
|
2012-11-26 09:38:18 +00:00
|
|
|
}).fail(function () {
|
|
|
|
self.ready.reject.apply(null, arguments);
|
|
|
|
});
|
2011-09-28 15:13:06 +00:00
|
|
|
}
|
2012-03-26 14:50:17 +00:00
|
|
|
|
2014-05-20 08:54:58 +00:00
|
|
|
var view_manager = this.getParent();
|
2014-05-20 13:27:44 +00:00
|
|
|
while (!(view_manager instanceof instance.web.ViewManager) &&
|
2014-05-22 07:55:04 +00:00
|
|
|
view_manager && view_manager.getParent) {
|
2014-05-20 08:54:58 +00:00
|
|
|
view_manager = view_manager.getParent();
|
|
|
|
}
|
|
|
|
|
2014-05-20 13:27:44 +00:00
|
|
|
if (view_manager) {
|
2014-05-27 12:21:52 +00:00
|
|
|
this.view_manager = view_manager;
|
2014-05-20 13:27:44 +00:00
|
|
|
view_manager.on('switch_mode', this, function (e) {
|
2014-05-22 07:55:04 +00:00
|
|
|
self.drawer.toggle(e === 'graph');
|
2014-05-20 13:27:44 +00:00
|
|
|
});
|
2014-05-26 12:51:47 +00:00
|
|
|
}
|
2014-05-27 12:21:52 +00:00
|
|
|
return $.when(p, this.ready);
|
2011-03-17 13:41:55 +00:00
|
|
|
},
|
2014-05-19 12:16:09 +00:00
|
|
|
|
|
|
|
set_drawer: function (drawer) {
|
2014-05-20 08:54:58 +00:00
|
|
|
this.drawer = drawer;
|
2014-05-19 12:16:09 +00:00
|
|
|
},
|
|
|
|
|
2011-04-01 10:45:00 +00:00
|
|
|
show: function () {
|
2012-08-24 18:27:07 +00:00
|
|
|
this.$el.show();
|
2011-04-01 10:45:00 +00:00
|
|
|
},
|
|
|
|
hide: function () {
|
2012-08-24 18:27:07 +00:00
|
|
|
this.$el.hide();
|
2011-03-17 13:41:55 +00:00
|
|
|
},
|
2012-03-19 16:43:04 +00:00
|
|
|
|
2012-05-09 13:32:41 +00:00
|
|
|
subviewForRoot: function (subview_root) {
|
|
|
|
return _(this.input_subviews).detect(function (subview) {
|
2012-08-24 18:27:07 +00:00
|
|
|
return subview.$el[0] === subview_root;
|
2012-05-09 13:32:41 +00:00
|
|
|
});
|
|
|
|
},
|
2012-05-09 14:12:42 +00:00
|
|
|
siblingSubview: function (subview, direction, wrap_around) {
|
2012-05-09 13:32:41 +00:00
|
|
|
var index = _(this.input_subviews).indexOf(subview) + direction;
|
2012-05-09 14:12:42 +00:00
|
|
|
if (wrap_around && index < 0) {
|
2012-05-09 13:32:41 +00:00
|
|
|
index = this.input_subviews.length - 1;
|
2012-05-09 14:12:42 +00:00
|
|
|
} else if (wrap_around && index >= this.input_subviews.length) {
|
2012-05-09 13:32:41 +00:00
|
|
|
index = 0;
|
|
|
|
}
|
2012-05-09 14:12:42 +00:00
|
|
|
return this.input_subviews[index];
|
2012-05-09 13:32:41 +00:00
|
|
|
},
|
|
|
|
focusPreceding: function (subview_root) {
|
2012-05-09 14:12:42 +00:00
|
|
|
return this.siblingSubview(
|
|
|
|
this.subviewForRoot(subview_root), -1, true)
|
2012-08-24 18:27:07 +00:00
|
|
|
.$el.focus();
|
2012-05-09 13:32:41 +00:00
|
|
|
},
|
|
|
|
focusFollowing: function (subview_root) {
|
2012-05-09 14:12:42 +00:00
|
|
|
return this.siblingSubview(
|
|
|
|
this.subviewForRoot(subview_root), +1, true)
|
2012-08-24 18:27:07 +00:00
|
|
|
.$el.focus();
|
2012-05-09 13:32:41 +00:00
|
|
|
},
|
|
|
|
|
2012-03-19 16:43:04 +00:00
|
|
|
/**
|
|
|
|
* Sets up search view's view-wide auto-completion widget
|
|
|
|
*/
|
|
|
|
setup_global_completion: function () {
|
2014-06-25 11:26:41 +00:00
|
|
|
var self = this;
|
|
|
|
this.autocomplete = new instance.web.search.AutoComplete(this, {
|
2012-03-19 16:43:04 +00:00
|
|
|
source: this.proxy('complete_global_search'),
|
|
|
|
select: this.proxy('select_completion'),
|
2014-06-25 11:26:41 +00:00
|
|
|
get_search_string: function () {
|
2012-11-07 09:10:47 +00:00
|
|
|
return self.$('div.oe_searchview_input').text();
|
2012-10-31 17:30:09 +00:00
|
|
|
},
|
2014-06-25 11:26:41 +00:00
|
|
|
width: this.$el.width(),
|
2012-10-31 17:30:09 +00:00
|
|
|
});
|
2014-06-25 11:26:41 +00:00
|
|
|
this.autocomplete.appendTo(this.$el);
|
2012-03-19 16:43:04 +00:00
|
|
|
},
|
|
|
|
/**
|
|
|
|
* Provide auto-completion result for req.term (an array to `resp`)
|
|
|
|
*
|
|
|
|
* @param {Object} req request to complete
|
|
|
|
* @param {String} req.term searched term to complete
|
|
|
|
* @param {Function} resp response callback
|
|
|
|
*/
|
|
|
|
complete_global_search: function (req, resp) {
|
2014-05-20 08:54:58 +00:00
|
|
|
$.when.apply(null, _(this.drawer.inputs).chain()
|
2013-02-13 09:01:08 +00:00
|
|
|
.filter(function (input) { return input.visible(); })
|
2012-03-19 16:43:04 +00:00
|
|
|
.invoke('complete', req.term)
|
|
|
|
.value()).then(function () {
|
2013-11-07 11:28:58 +00:00
|
|
|
resp(_(arguments).chain()
|
|
|
|
.compact()
|
|
|
|
.flatten(true)
|
|
|
|
.value());
|
|
|
|
});
|
2012-03-19 16:43:04 +00:00
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Action to perform in case of selection: create a facet (model)
|
|
|
|
* and add it to the search collection
|
|
|
|
*
|
|
|
|
* @param {Object} e selection event, preventDefault to avoid setting value on object
|
|
|
|
* @param {Object} ui selection information
|
|
|
|
* @param {Object} ui.item selected completion item
|
|
|
|
*/
|
|
|
|
select_completion: function (e, ui) {
|
|
|
|
e.preventDefault();
|
2012-04-24 10:59:41 +00:00
|
|
|
|
2012-05-09 15:12:17 +00:00
|
|
|
var input_index = _(this.input_subviews).indexOf(
|
|
|
|
this.subviewForRoot(
|
2012-11-06 15:21:48 +00:00
|
|
|
this.$('div.oe_searchview_input:focus')[0]));
|
2012-05-09 15:12:17 +00:00
|
|
|
this.query.add(ui.item.facet, {at: input_index / 2});
|
2012-03-19 16:43:04 +00:00
|
|
|
},
|
2012-05-14 15:41:58 +00:00
|
|
|
childFocused: function () {
|
2012-08-24 18:27:07 +00:00
|
|
|
this.$el.addClass('oe_focused');
|
2012-05-14 15:41:58 +00:00
|
|
|
},
|
|
|
|
childBlurred: function () {
|
2012-08-24 18:27:07 +00:00
|
|
|
var val = this.$el.val();
|
|
|
|
this.$el.val('');
|
|
|
|
this.$el.removeClass('oe_focused')
|
2012-07-26 11:33:11 +00:00
|
|
|
.trigger('blur');
|
2014-06-25 11:26:41 +00:00
|
|
|
this.autocomplete.close();
|
2012-05-14 15:41:58 +00:00
|
|
|
},
|
2012-05-10 12:42:44 +00:00
|
|
|
/**
|
2014-04-22 15:03:16 +00:00
|
|
|
* Call the renderFacets method with the correct arguments.
|
|
|
|
* This is due to the fact that change events are called with two arguments
|
|
|
|
* (model, options) while add, reset and remove events are called with
|
|
|
|
* (collection, model, options) as arguments
|
|
|
|
*/
|
|
|
|
renderChangedFacets: function (model, options) {
|
|
|
|
this.renderFacets(undefined, model, options);
|
|
|
|
},
|
|
|
|
/**
|
|
|
|
* @param {openerp.web.search.SearchQuery | undefined} Undefined if event is change
|
|
|
|
* @param {openerp.web.search.Facet}
|
2012-05-10 12:42:44 +00:00
|
|
|
* @param {Object} [options]
|
|
|
|
*/
|
2014-04-22 15:03:16 +00:00
|
|
|
renderFacets: function (collection, model, options) {
|
2012-04-24 10:59:41 +00:00
|
|
|
var self = this;
|
2012-05-10 12:42:44 +00:00
|
|
|
var started = [];
|
2012-11-06 15:21:48 +00:00
|
|
|
var $e = this.$('div.oe_searchview_facets');
|
2012-04-24 10:59:41 +00:00
|
|
|
_.invoke(this.input_subviews, 'destroy');
|
|
|
|
this.input_subviews = [];
|
2012-03-19 16:43:04 +00:00
|
|
|
|
2012-04-24 10:59:41 +00:00
|
|
|
var i = new my.InputView(this);
|
2012-05-10 12:42:44 +00:00
|
|
|
started.push(i.appendTo($e));
|
2012-04-24 10:59:41 +00:00
|
|
|
this.input_subviews.push(i);
|
|
|
|
this.query.each(function (facet) {
|
|
|
|
var f = new my.FacetView(this, facet);
|
2012-05-10 12:42:44 +00:00
|
|
|
started.push(f.appendTo($e));
|
2012-04-24 10:59:41 +00:00
|
|
|
self.input_subviews.push(f);
|
2012-04-06 11:46:33 +00:00
|
|
|
|
2012-04-24 10:59:41 +00:00
|
|
|
var i = new my.InputView(this);
|
2012-05-10 12:42:44 +00:00
|
|
|
started.push(i.appendTo($e));
|
2012-04-24 10:59:41 +00:00
|
|
|
self.input_subviews.push(i);
|
|
|
|
}, this);
|
2012-05-14 15:41:58 +00:00
|
|
|
_.each(this.input_subviews, function (childView) {
|
|
|
|
childView.on('focused', self, self.proxy('childFocused'));
|
|
|
|
childView.on('blurred', self, self.proxy('childBlurred'));
|
|
|
|
});
|
2012-05-10 12:42:44 +00:00
|
|
|
|
|
|
|
$.when.apply(null, started).then(function () {
|
2014-04-22 15:03:16 +00:00
|
|
|
if (options && options.focus_input === false) return;
|
2012-05-10 12:42:44 +00:00
|
|
|
var input_to_focus;
|
|
|
|
// options.at: facet inserted at given index, focus next input
|
|
|
|
// otherwise just focus last input
|
|
|
|
if (!options || typeof options.at !== 'number') {
|
|
|
|
input_to_focus = _.last(self.input_subviews);
|
|
|
|
} else {
|
|
|
|
input_to_focus = self.input_subviews[(options.at + 1) * 2];
|
|
|
|
}
|
2012-08-24 18:27:07 +00:00
|
|
|
input_to_focus.$el.focus();
|
2012-05-10 12:42:44 +00:00
|
|
|
});
|
2012-03-19 16:43:04 +00:00
|
|
|
},
|
|
|
|
|
2012-10-18 08:50:28 +00:00
|
|
|
search_view_loaded: function(data) {
|
2012-03-20 10:28:46 +00:00
|
|
|
var self = this;
|
2012-11-26 09:38:18 +00:00
|
|
|
this.fields_view = data;
|
2014-11-25 12:11:15 +00:00
|
|
|
this.view_id = this.view_id || data.view_id;
|
2012-11-26 09:38:18 +00:00
|
|
|
if (data.type !== 'search' ||
|
|
|
|
data.arch.tag !== 'search') {
|
2011-11-15 12:30:59 +00:00
|
|
|
throw new Error(_.str.sprintf(
|
2011-11-14 12:39:48 +00:00
|
|
|
"Got non-search view after asking for a search view: type %s, arch root %s",
|
2012-11-26 09:38:18 +00:00
|
|
|
data.type, data.arch.tag));
|
2011-11-14 12:39:48 +00:00
|
|
|
}
|
2012-04-27 15:04:36 +00:00
|
|
|
|
2014-05-19 12:16:09 +00:00
|
|
|
return this.drawer_ready
|
|
|
|
.then(this.proxy('setup_default_query'))
|
2012-12-13 14:09:14 +00:00
|
|
|
.then(function () {
|
2012-10-18 08:50:28 +00:00
|
|
|
self.trigger("search_view_loaded", data);
|
|
|
|
self.ready.resolve();
|
|
|
|
});
|
2011-07-25 11:37:40 +00:00
|
|
|
},
|
2012-05-23 11:23:14 +00:00
|
|
|
setup_default_query: function () {
|
|
|
|
// Hacky implementation of CustomFilters#facet_for_defaults ensure
|
|
|
|
// CustomFilters will be ready (and CustomFilters#filters will be
|
|
|
|
// correctly filled) by the time this method executes.
|
2014-05-20 08:54:58 +00:00
|
|
|
var custom_filters = this.drawer.custom_filters.filters;
|
2013-01-14 09:30:14 +00:00
|
|
|
if (!this.options.disable_custom_filters && !_(custom_filters).isEmpty()) {
|
2012-05-23 11:23:14 +00:00
|
|
|
// Check for any is_default custom filter
|
|
|
|
var personal_filter = _(custom_filters).find(function (filter) {
|
|
|
|
return filter.user_id && filter.is_default;
|
|
|
|
});
|
|
|
|
if (personal_filter) {
|
2014-05-20 08:54:58 +00:00
|
|
|
this.drawer.custom_filters.toggle_filter(personal_filter, true);
|
2012-05-23 11:23:14 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
var global_filter = _(custom_filters).find(function (filter) {
|
|
|
|
return !filter.user_id && filter.is_default;
|
|
|
|
});
|
|
|
|
if (global_filter) {
|
2014-05-20 08:54:58 +00:00
|
|
|
this.drawer.custom_filters.toggle_filter(global_filter, true);
|
2012-05-23 11:23:14 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// No custom filter, or no is_default custom filter, apply view defaults
|
|
|
|
this.query.reset(_(arguments).compact(), {preventSearch: true});
|
|
|
|
},
|
2011-03-29 10:47:44 +00:00
|
|
|
/**
|
2012-05-08 16:21:37 +00:00
|
|
|
* Extract search data from the view's facets.
|
2011-03-29 10:47:44 +00:00
|
|
|
*
|
2012-05-08 16:21:37 +00:00
|
|
|
* Result is an object with 4 (own) properties:
|
2011-03-29 10:47:44 +00:00
|
|
|
*
|
2012-05-08 16:21:37 +00:00
|
|
|
* errors
|
|
|
|
* An array of any error generated during data validation and
|
|
|
|
* extraction, contains the validation error objects
|
|
|
|
* domains
|
|
|
|
* Array of domains
|
|
|
|
* contexts
|
|
|
|
* Array of contexts
|
|
|
|
* groupbys
|
|
|
|
* Array of domains, in groupby order rather than view order
|
2011-03-29 10:47:44 +00:00
|
|
|
*
|
2012-05-08 16:21:37 +00:00
|
|
|
* @return {Object}
|
2011-03-29 10:47:44 +00:00
|
|
|
*/
|
2012-05-08 16:21:37 +00:00
|
|
|
build_search_data: function () {
|
2012-03-21 15:45:50 +00:00
|
|
|
var domains = [], contexts = [], groupbys = [], errors = [];
|
2011-04-07 23:41:30 +00:00
|
|
|
|
2012-04-24 10:59:41 +00:00
|
|
|
this.query.each(function (facet) {
|
2012-03-21 15:45:50 +00:00
|
|
|
var field = facet.get('field');
|
2011-03-29 10:47:44 +00:00
|
|
|
try {
|
2012-03-21 15:45:50 +00:00
|
|
|
var domain = field.get_domain(facet);
|
2011-03-29 10:47:44 +00:00
|
|
|
if (domain) {
|
|
|
|
domains.push(domain);
|
|
|
|
}
|
2012-03-21 15:45:50 +00:00
|
|
|
var context = field.get_context(facet);
|
2011-03-29 10:47:44 +00:00
|
|
|
if (context) {
|
|
|
|
contexts.push(context);
|
2012-04-03 12:07:42 +00:00
|
|
|
}
|
|
|
|
var group_by = field.get_groupby(facet);
|
|
|
|
if (group_by) {
|
|
|
|
groupbys.push.apply(groupbys, group_by);
|
2011-03-29 10:47:44 +00:00
|
|
|
}
|
|
|
|
} catch (e) {
|
2012-04-17 12:02:10 +00:00
|
|
|
if (e instanceof instance.web.search.Invalid) {
|
2011-03-29 10:47:44 +00:00
|
|
|
errors.push(e);
|
|
|
|
} else {
|
|
|
|
throw e;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
2012-05-08 16:21:37 +00:00
|
|
|
return {
|
|
|
|
domains: domains,
|
|
|
|
contexts: contexts,
|
|
|
|
groupbys: groupbys,
|
|
|
|
errors: errors
|
|
|
|
};
|
2012-10-18 12:46:07 +00:00
|
|
|
},
|
|
|
|
/**
|
2012-05-08 16:21:37 +00:00
|
|
|
* Performs the search view collection of widget data.
|
|
|
|
*
|
|
|
|
* If the collection went well (all fields are valid), then triggers
|
|
|
|
* :js:func:`instance.web.SearchView.on_search`.
|
|
|
|
*
|
|
|
|
* If at least one field failed its validation, triggers
|
|
|
|
* :js:func:`instance.web.SearchView.on_invalid` instead.
|
|
|
|
*
|
2012-11-06 15:04:49 +00:00
|
|
|
* @param [_query]
|
|
|
|
* @param {Object} [options]
|
2012-05-08 16:21:37 +00:00
|
|
|
*/
|
2012-05-18 15:23:42 +00:00
|
|
|
do_search: function (_query, options) {
|
|
|
|
if (options && options.preventSearch) {
|
|
|
|
return;
|
|
|
|
}
|
2012-05-08 16:21:37 +00:00
|
|
|
var search = this.build_search_data();
|
|
|
|
if (!_.isEmpty(search.errors)) {
|
|
|
|
this.on_invalid(search.errors);
|
2012-03-21 15:45:50 +00:00
|
|
|
return;
|
|
|
|
}
|
2012-10-19 11:15:58 +00:00
|
|
|
this.trigger('search_data', search.domains, search.contexts, search.groupbys);
|
2011-03-24 16:18:42 +00:00
|
|
|
},
|
2011-03-25 08:12:27 +00:00
|
|
|
/**
|
2011-03-29 10:47:44 +00:00
|
|
|
* Triggered after the SearchView has collected all relevant domains and
|
|
|
|
* contexts.
|
2011-03-25 08:12:27 +00:00
|
|
|
*
|
|
|
|
* It is provided with an Array of domains and an Array of contexts, which
|
|
|
|
* may or may not be evaluated (each item can be either a valid domain or
|
|
|
|
* context, or a string to evaluate in order in the sequence)
|
|
|
|
*
|
2011-03-29 10:47:44 +00:00
|
|
|
* It is also passed an array of contexts used for group_by (they are in
|
|
|
|
* the correct order for group_by evaluation, which contexts may not be)
|
|
|
|
*
|
|
|
|
* @event
|
2011-03-28 16:39:17 +00:00
|
|
|
* @param {Array} domains an array of literal domains or domain references
|
|
|
|
* @param {Array} contexts an array of literal contexts or context refs
|
|
|
|
* @param {Array} groupbys ordered contexts which may or may not have group_by keys
|
2011-03-25 08:12:27 +00:00
|
|
|
*/
|
2011-03-29 10:47:44 +00:00
|
|
|
/**
|
|
|
|
* Triggered after a validation error in the SearchView fields.
|
|
|
|
*
|
|
|
|
* Error objects have three keys:
|
|
|
|
* * ``field`` is the name of the invalid field
|
|
|
|
* * ``value`` is the invalid value
|
|
|
|
* * ``message`` is the (in)validation message provided by the field
|
|
|
|
*
|
|
|
|
* @event
|
|
|
|
* @param {Array} errors a never-empty array of error objects
|
|
|
|
*/
|
2011-04-07 23:41:30 +00:00
|
|
|
on_invalid: function (errors) {
|
2011-11-14 10:12:21 +00:00
|
|
|
this.do_notify(_t("Invalid Search"), _t("triggered from search view"));
|
2012-10-19 11:15:58 +00:00
|
|
|
this.trigger('invalid_search', errors);
|
2014-05-20 13:27:44 +00:00
|
|
|
},
|
|
|
|
|
2014-05-22 07:55:04 +00:00
|
|
|
// The method appendTo is overwrited to be able to insert the drawer anywhere
|
|
|
|
appendTo: function ($searchview_parent, $searchview_drawer_node) {
|
|
|
|
var $searchview_drawer_node = $searchview_drawer_node || $searchview_parent;
|
2014-05-20 13:27:44 +00:00
|
|
|
|
2014-05-22 07:55:04 +00:00
|
|
|
return $.when(
|
|
|
|
this._super($searchview_parent),
|
|
|
|
this.drawer.appendTo($searchview_drawer_node)
|
|
|
|
);
|
2014-05-20 13:27:44 +00:00
|
|
|
},
|
|
|
|
|
|
|
|
destroy: function () {
|
|
|
|
this.drawer.destroy();
|
|
|
|
this.getParent().destroy.call(this);
|
2011-03-17 13:41:55 +00:00
|
|
|
}
|
|
|
|
});
|
|
|
|
|
2014-05-19 12:16:09 +00:00
|
|
|
instance.web.SearchViewDrawer = instance.web.Widget.extend({
|
|
|
|
template: "SearchViewDrawer",
|
|
|
|
|
|
|
|
init: function(parent, searchview) {
|
|
|
|
this._super(parent);
|
|
|
|
this.searchview = searchview;
|
|
|
|
this.searchview.set_drawer(this);
|
|
|
|
this.ready = searchview.drawer_ready;
|
|
|
|
this.controls = [];
|
|
|
|
this.inputs = [];
|
|
|
|
},
|
|
|
|
|
2014-05-22 07:55:04 +00:00
|
|
|
toggle: function (visibility) {
|
|
|
|
this.$el.toggle(visibility);
|
2014-05-30 07:58:07 +00:00
|
|
|
var $view_manager_body = this.$el.closest('.oe_view_manager_body');
|
|
|
|
if ($view_manager_body.length) {
|
|
|
|
$view_manager_body.scrollTop(0);
|
|
|
|
}
|
2014-05-20 08:54:58 +00:00
|
|
|
},
|
|
|
|
|
2014-05-19 12:16:09 +00:00
|
|
|
start: function() {
|
2014-05-26 13:45:28 +00:00
|
|
|
var self = this;
|
2014-07-29 17:43:56 +00:00
|
|
|
if (this.searchview.headless) return $.when(this._super(), this.searchview.ready);
|
2014-05-19 12:16:09 +00:00
|
|
|
var filters_ready = this.searchview.fields_view_get
|
|
|
|
.then(this.proxy('prepare_filters'));
|
2014-05-26 13:45:28 +00:00
|
|
|
return $.when(this._super(), filters_ready).then(function () {
|
|
|
|
var defaults = arguments[1][0];
|
|
|
|
self.ready.resolve.apply(null, defaults);
|
|
|
|
});
|
2014-05-19 12:16:09 +00:00
|
|
|
},
|
|
|
|
prepare_filters: function (data) {
|
|
|
|
this.make_widgets(
|
|
|
|
data['arch'].children,
|
|
|
|
data.fields);
|
|
|
|
|
|
|
|
this.add_common_inputs();
|
|
|
|
|
|
|
|
// build drawer
|
2014-05-19 13:40:45 +00:00
|
|
|
var in_drawer = this.select_for_drawer();
|
2014-05-19 12:16:09 +00:00
|
|
|
|
2014-05-21 11:18:30 +00:00
|
|
|
var $first_col = this.$(".col-md-7"),
|
|
|
|
$snd_col = this.$(".col-md-5");
|
2014-05-19 12:16:09 +00:00
|
|
|
|
2014-06-30 17:48:11 +00:00
|
|
|
var add_custom_filters = in_drawer[0].appendTo($first_col),
|
2014-05-21 11:18:30 +00:00
|
|
|
add_filters = in_drawer[1].appendTo($first_col),
|
|
|
|
add_rest = $.when.apply(null, _(in_drawer.slice(2)).invoke('appendTo', $snd_col)),
|
|
|
|
defaults_fetched = $.when.apply(null, _(this.inputs).invoke(
|
2014-05-19 13:40:45 +00:00
|
|
|
'facet_for_defaults', this.searchview.defaults));
|
2014-05-19 12:16:09 +00:00
|
|
|
|
2014-06-30 17:48:11 +00:00
|
|
|
return $.when(defaults_fetched, add_custom_filters, add_filters, add_rest);
|
2014-05-19 12:16:09 +00:00
|
|
|
},
|
|
|
|
/**
|
|
|
|
* Sets up thingie where all the mess is put?
|
|
|
|
*/
|
|
|
|
select_for_drawer: function () {
|
|
|
|
return _(this.inputs).filter(function (input) {
|
|
|
|
return input.in_drawer();
|
|
|
|
});
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Builds a list of widget rows (each row is an array of widgets)
|
|
|
|
*
|
|
|
|
* @param {Array} items a list of nodes to convert to widgets
|
|
|
|
* @param {Object} fields a mapping of field names to (ORM) field attributes
|
|
|
|
* @param {Object} [group] group to put the new controls in
|
|
|
|
*/
|
|
|
|
make_widgets: function (items, fields, group) {
|
|
|
|
if (!group) {
|
|
|
|
group = new instance.web.search.Group(
|
|
|
|
this, 'q', {attrs: {string: _t("Filters")}});
|
|
|
|
}
|
|
|
|
var self = this;
|
|
|
|
var filters = [];
|
|
|
|
_.each(items, function (item) {
|
|
|
|
if (filters.length && item.tag !== 'filter') {
|
|
|
|
group.push(new instance.web.search.FilterGroup(filters, group));
|
|
|
|
filters = [];
|
|
|
|
}
|
|
|
|
|
|
|
|
switch (item.tag) {
|
2014-06-30 12:42:05 +00:00
|
|
|
case 'separator': case 'newline':
|
2014-05-19 12:16:09 +00:00
|
|
|
break;
|
|
|
|
case 'filter':
|
|
|
|
filters.push(new instance.web.search.Filter(item, group));
|
|
|
|
break;
|
|
|
|
case 'group':
|
2014-06-30 13:06:52 +00:00
|
|
|
self.add_separator();
|
2014-05-19 12:16:09 +00:00
|
|
|
self.make_widgets(item.children, fields,
|
|
|
|
new instance.web.search.Group(group, 'w', item));
|
2014-06-30 13:06:52 +00:00
|
|
|
self.add_separator();
|
2014-05-19 12:16:09 +00:00
|
|
|
break;
|
|
|
|
case 'field':
|
|
|
|
var field = this.make_field(
|
|
|
|
item, fields[item['attrs'].name], group);
|
|
|
|
group.push(field);
|
|
|
|
// filters
|
|
|
|
self.make_widgets(item.children, fields, group);
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}, this);
|
|
|
|
|
|
|
|
if (filters.length) {
|
|
|
|
group.push(new instance.web.search.FilterGroup(filters, this));
|
|
|
|
}
|
|
|
|
},
|
2014-06-30 13:06:52 +00:00
|
|
|
|
|
|
|
add_separator: function () {
|
|
|
|
if (!(_.last(this.inputs) instanceof instance.web.search.Separator))
|
|
|
|
new instance.web.search.Separator(this);
|
|
|
|
},
|
2014-05-19 12:16:09 +00:00
|
|
|
/**
|
|
|
|
* Creates a field for the provided field descriptor item (which comes
|
|
|
|
* from fields_view_get)
|
|
|
|
*
|
|
|
|
* @param {Object} item fields_view_get node for the field
|
|
|
|
* @param {Object} field fields_get result for the field
|
|
|
|
* @param {Object} [parent]
|
|
|
|
* @returns instance.web.search.Field
|
|
|
|
*/
|
|
|
|
make_field: function (item, field, parent) {
|
|
|
|
// M2O combined with selection widget is pointless and broken in search views,
|
|
|
|
// but has been used in the past for unsupported hacks -> ignore it
|
|
|
|
if (field.type === "many2one" && item.attrs.widget === "selection"){
|
|
|
|
item.attrs.widget = undefined;
|
|
|
|
}
|
|
|
|
var obj = instance.web.search.fields.get_any( [item.attrs.widget, field.type]);
|
|
|
|
if(obj) {
|
|
|
|
return new (obj) (item, field, parent || this);
|
|
|
|
} else {
|
|
|
|
console.group('Unknown field type ' + field.type);
|
|
|
|
console.error('View node', item);
|
|
|
|
console.info('View field', field);
|
|
|
|
console.info('In view', this);
|
|
|
|
console.groupEnd();
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
add_common_inputs: function() {
|
2014-05-21 11:18:30 +00:00
|
|
|
// add custom filters to this.inputs
|
2014-06-30 17:48:11 +00:00
|
|
|
this.custom_filters = new instance.web.search.CustomFilters(this);
|
2014-05-19 12:16:09 +00:00
|
|
|
// add Filters to this.inputs, need view.controls filled
|
|
|
|
(new instance.web.search.Filters(this));
|
2014-05-21 11:18:30 +00:00
|
|
|
(new instance.web.search.SaveFilter(this, this.custom_filters));
|
2014-05-19 12:16:09 +00:00
|
|
|
// add Advanced to this.inputs
|
|
|
|
(new instance.web.search.Advanced(this));
|
|
|
|
},
|
|
|
|
|
|
|
|
});
|
|
|
|
|
2011-03-31 10:40:12 +00:00
|
|
|
/**
|
2012-04-17 12:02:10 +00:00
|
|
|
* Registry of search fields, called by :js:class:`instance.web.SearchView` to
|
2011-03-31 10:40:12 +00:00
|
|
|
* find and instantiate its field widgets.
|
|
|
|
*/
|
2012-04-17 12:02:10 +00:00
|
|
|
instance.web.search.fields = new instance.web.Registry({
|
|
|
|
'char': 'instance.web.search.CharField',
|
|
|
|
'text': 'instance.web.search.CharField',
|
2012-10-29 11:15:42 +00:00
|
|
|
'html': 'instance.web.search.CharField',
|
2012-04-17 12:02:10 +00:00
|
|
|
'boolean': 'instance.web.search.BooleanField',
|
|
|
|
'integer': 'instance.web.search.IntegerField',
|
|
|
|
'id': 'instance.web.search.IntegerField',
|
|
|
|
'float': 'instance.web.search.FloatField',
|
|
|
|
'selection': 'instance.web.search.SelectionField',
|
|
|
|
'datetime': 'instance.web.search.DateTimeField',
|
|
|
|
'date': 'instance.web.search.DateField',
|
|
|
|
'many2one': 'instance.web.search.ManyToOneField',
|
|
|
|
'many2many': 'instance.web.search.CharField',
|
|
|
|
'one2many': 'instance.web.search.CharField'
|
2011-03-31 10:40:12 +00:00
|
|
|
});
|
2012-04-17 12:02:10 +00:00
|
|
|
instance.web.search.Invalid = instance.web.Class.extend( /** @lends instance.web.search.Invalid# */{
|
2011-03-30 15:27:36 +00:00
|
|
|
/**
|
|
|
|
* Exception thrown by search widgets when they hold invalid values,
|
|
|
|
* which they can not return when asked.
|
|
|
|
*
|
2012-04-17 12:02:10 +00:00
|
|
|
* @constructs instance.web.search.Invalid
|
|
|
|
* @extends instance.web.Class
|
2011-09-12 11:34:37 +00:00
|
|
|
*
|
2011-03-30 15:27:36 +00:00
|
|
|
* @param field the name of the field holding an invalid value
|
|
|
|
* @param value the invalid value
|
|
|
|
* @param message validation failure message
|
|
|
|
*/
|
2011-03-24 17:37:21 +00:00
|
|
|
init: function (field, value, message) {
|
|
|
|
this.field = field;
|
|
|
|
this.value = value;
|
|
|
|
this.message = message;
|
|
|
|
},
|
|
|
|
toString: function () {
|
2011-11-15 12:30:59 +00:00
|
|
|
return _.str.sprintf(
|
2011-11-14 10:12:21 +00:00
|
|
|
_t("Incorrect value for field %(fieldname)s: [%(value)s] is %(message)s"),
|
|
|
|
{fieldname: this.field, value: this.value, message: this.message}
|
|
|
|
);
|
2011-03-24 17:37:21 +00:00
|
|
|
}
|
|
|
|
});
|
2012-06-11 09:18:57 +00:00
|
|
|
instance.web.search.Widget = instance.web.Widget.extend( /** @lends instance.web.search.Widget# */{
|
2011-03-30 15:27:36 +00:00
|
|
|
template: null,
|
|
|
|
/**
|
|
|
|
* Root class of all search widgets
|
|
|
|
*
|
2012-04-17 12:02:10 +00:00
|
|
|
* @constructs instance.web.search.Widget
|
2012-06-11 09:18:57 +00:00
|
|
|
* @extends instance.web.Widget
|
2011-03-30 15:27:36 +00:00
|
|
|
*
|
2013-02-14 07:43:02 +00:00
|
|
|
* @param parent parent of this widget
|
2011-03-30 15:27:36 +00:00
|
|
|
*/
|
2013-02-14 07:43:02 +00:00
|
|
|
init: function (parent) {
|
|
|
|
this._super(parent);
|
|
|
|
var ancestor = parent;
|
|
|
|
do {
|
2014-05-19 12:16:09 +00:00
|
|
|
this.drawer = ancestor;
|
|
|
|
} while (!(ancestor instanceof instance.web.SearchViewDrawer)
|
2013-02-14 07:43:02 +00:00
|
|
|
&& (ancestor = (ancestor.getParent && ancestor.getParent())));
|
2014-05-22 07:55:04 +00:00
|
|
|
this.view = this.drawer.searchview || this.drawer;
|
2011-03-17 13:41:55 +00:00
|
|
|
}
|
2011-03-24 12:46:53 +00:00
|
|
|
});
|
2013-02-14 07:43:02 +00:00
|
|
|
|
2012-04-17 12:02:10 +00:00
|
|
|
instance.web.search.add_expand_listener = function($root) {
|
2011-03-30 15:04:21 +00:00
|
|
|
$root.find('a.searchview_group_string').click(function (e) {
|
2011-03-28 07:37:22 +00:00
|
|
|
$root.toggleClass('folded expanded');
|
|
|
|
e.stopPropagation();
|
|
|
|
e.preventDefault();
|
|
|
|
});
|
|
|
|
};
|
2012-04-17 12:02:10 +00:00
|
|
|
instance.web.search.Group = instance.web.search.Widget.extend({
|
2013-02-14 07:43:02 +00:00
|
|
|
init: function (parent, icon, node) {
|
|
|
|
this._super(parent);
|
|
|
|
var attrs = node.attrs;
|
|
|
|
this.modifiers = attrs.modifiers =
|
|
|
|
attrs.modifiers ? JSON.parse(attrs.modifiers) : {};
|
|
|
|
this.attrs = attrs;
|
|
|
|
this.icon = icon;
|
|
|
|
this.name = attrs.string;
|
|
|
|
this.children = [];
|
|
|
|
|
2014-05-19 12:16:09 +00:00
|
|
|
this.drawer.controls.push(this);
|
2013-02-14 07:43:02 +00:00
|
|
|
},
|
|
|
|
push: function (input) {
|
|
|
|
this.children.push(input);
|
|
|
|
},
|
|
|
|
visible: function () {
|
|
|
|
return !this.modifiers.invisible;
|
|
|
|
},
|
2011-03-24 16:18:42 +00:00
|
|
|
});
|
2011-03-29 13:18:54 +00:00
|
|
|
|
2012-04-17 12:02:10 +00:00
|
|
|
instance.web.search.Input = instance.web.search.Widget.extend( /** @lends instance.web.search.Input# */{
|
2012-04-27 15:04:36 +00:00
|
|
|
_in_drawer: false,
|
2011-03-30 15:27:36 +00:00
|
|
|
/**
|
2012-04-17 12:02:10 +00:00
|
|
|
* @constructs instance.web.search.Input
|
|
|
|
* @extends instance.web.search.Widget
|
2011-03-30 15:27:36 +00:00
|
|
|
*
|
2013-02-14 07:43:02 +00:00
|
|
|
* @param parent
|
2011-03-30 15:27:36 +00:00
|
|
|
*/
|
2013-02-14 07:43:02 +00:00
|
|
|
init: function (parent) {
|
|
|
|
this._super(parent);
|
2013-02-13 09:01:08 +00:00
|
|
|
this.load_attrs({});
|
2014-05-19 12:16:09 +00:00
|
|
|
this.drawer.inputs.push(this);
|
2011-03-24 16:18:42 +00:00
|
|
|
},
|
2012-03-19 16:43:04 +00:00
|
|
|
/**
|
|
|
|
* Fetch auto-completion values for the widget.
|
|
|
|
*
|
|
|
|
* The completion values should be an array of objects with keys category,
|
|
|
|
* label, value prefixed with an object with keys type=section and label
|
|
|
|
*
|
|
|
|
* @param {String} value value to complete
|
2012-03-20 10:28:46 +00:00
|
|
|
* @returns {jQuery.Deferred<null|Array>}
|
2012-03-19 16:43:04 +00:00
|
|
|
*/
|
|
|
|
complete: function (value) {
|
2013-07-25 10:33:01 +00:00
|
|
|
return $.when(null);
|
2012-03-19 16:43:04 +00:00
|
|
|
},
|
2012-03-20 10:28:46 +00:00
|
|
|
/**
|
2012-04-24 10:59:41 +00:00
|
|
|
* Returns a Facet instance for the provided defaults if they apply to
|
|
|
|
* this widget, or null if they don't.
|
2012-03-20 10:28:46 +00:00
|
|
|
*
|
|
|
|
* This default implementation will try calling
|
2012-04-17 12:02:10 +00:00
|
|
|
* :js:func:`instance.web.search.Input#facet_for` if the widget's name
|
2012-03-20 10:28:46 +00:00
|
|
|
* matches the input key
|
|
|
|
*
|
|
|
|
* @param {Object} defaults
|
|
|
|
* @returns {jQuery.Deferred<null|Object>}
|
|
|
|
*/
|
|
|
|
facet_for_defaults: function (defaults) {
|
|
|
|
if (!this.attrs ||
|
|
|
|
!(this.attrs.name in defaults && defaults[this.attrs.name])) {
|
|
|
|
return $.when(null);
|
|
|
|
}
|
|
|
|
return this.facet_for(defaults[this.attrs.name]);
|
|
|
|
},
|
2012-04-27 15:04:36 +00:00
|
|
|
in_drawer: function () {
|
|
|
|
return !!this._in_drawer;
|
|
|
|
},
|
2011-03-24 16:18:42 +00:00
|
|
|
get_context: function () {
|
|
|
|
throw new Error(
|
|
|
|
"get_context not implemented for widget " + this.attrs.type);
|
|
|
|
},
|
2012-04-03 12:07:42 +00:00
|
|
|
get_groupby: function () {
|
|
|
|
throw new Error(
|
|
|
|
"get_groupby not implemented for widget " + this.attrs.type);
|
|
|
|
},
|
2011-03-24 16:18:42 +00:00
|
|
|
get_domain: function () {
|
|
|
|
throw new Error(
|
|
|
|
"get_domain not implemented for widget " + this.attrs.type);
|
2011-12-16 15:11:08 +00:00
|
|
|
},
|
|
|
|
load_attrs: function (attrs) {
|
2013-02-13 09:01:08 +00:00
|
|
|
attrs.modifiers = attrs.modifiers ? JSON.parse(attrs.modifiers) : {};
|
2011-12-16 15:11:08 +00:00
|
|
|
this.attrs = attrs;
|
2013-02-13 09:01:08 +00:00
|
|
|
},
|
|
|
|
/**
|
|
|
|
* Returns whether the input is "visible". The default behavior is to
|
|
|
|
* query the ``modifiers.invisible`` flag on the input's description or
|
|
|
|
* view node.
|
|
|
|
*
|
|
|
|
* @returns {Boolean}
|
|
|
|
*/
|
|
|
|
visible: function () {
|
2013-02-14 07:43:02 +00:00
|
|
|
if (this.attrs.modifiers.invisible) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
var parent = this;
|
|
|
|
while ((parent = parent.getParent()) &&
|
|
|
|
( (parent instanceof instance.web.search.Group)
|
|
|
|
|| (parent instanceof instance.web.search.Input))) {
|
|
|
|
if (!parent.visible()) {
|
|
|
|
return false;
|
2011-12-16 15:11:08 +00:00
|
|
|
}
|
|
|
|
}
|
2013-02-14 07:43:02 +00:00
|
|
|
return true;
|
2013-02-13 09:01:08 +00:00
|
|
|
},
|
2011-03-24 12:46:53 +00:00
|
|
|
});
|
2012-04-17 12:02:10 +00:00
|
|
|
instance.web.search.FilterGroup = instance.web.search.Input.extend(/** @lends instance.web.search.FilterGroup# */{
|
2011-09-15 08:46:26 +00:00
|
|
|
template: 'SearchView.filters',
|
2012-05-04 09:57:17 +00:00
|
|
|
icon: 'q',
|
2012-05-15 06:43:40 +00:00
|
|
|
completion_label: _lt("Filter on: %s"),
|
2011-09-15 08:46:26 +00:00
|
|
|
/**
|
|
|
|
* Inclusive group of filters, creates a continuous "button" with clickable
|
|
|
|
* sections (the normal display for filters is to be a self-contained button)
|
|
|
|
*
|
2012-04-17 12:02:10 +00:00
|
|
|
* @constructs instance.web.search.FilterGroup
|
|
|
|
* @extends instance.web.search.Input
|
2011-09-15 08:46:26 +00:00
|
|
|
*
|
2012-04-17 12:02:10 +00:00
|
|
|
* @param {Array<instance.web.search.Filter>} filters elements of the group
|
2013-02-14 07:43:02 +00:00
|
|
|
* @param {instance.web.SearchView} parent parent in which the filters are contained
|
2011-09-15 08:46:26 +00:00
|
|
|
*/
|
2013-02-14 07:43:02 +00:00
|
|
|
init: function (filters, parent) {
|
2012-05-04 09:43:06 +00:00
|
|
|
// If all filters are group_by and we're not initializing a GroupbyGroup,
|
|
|
|
// create a GroupbyGroup instead of the current FilterGroup
|
|
|
|
if (!(this instanceof instance.web.search.GroupbyGroup) &&
|
|
|
|
_(filters).all(function (f) {
|
2013-03-07 12:42:22 +00:00
|
|
|
if (!f.attrs.context) { return false; }
|
|
|
|
var c = instance.web.pyeval.eval('context', f.attrs.context);
|
|
|
|
return !_.isEmpty(c.group_by);})) {
|
2013-02-14 07:43:02 +00:00
|
|
|
return new instance.web.search.GroupbyGroup(filters, parent);
|
2012-05-04 09:43:06 +00:00
|
|
|
}
|
2013-02-14 07:43:02 +00:00
|
|
|
this._super(parent);
|
2011-09-15 08:46:26 +00:00
|
|
|
this.filters = filters;
|
2012-05-18 15:23:42 +00:00
|
|
|
this.view.query.on('add remove change reset', this.proxy('search_change'));
|
2011-09-15 08:46:26 +00:00
|
|
|
},
|
2012-03-26 14:50:17 +00:00
|
|
|
start: function () {
|
2012-08-24 18:27:07 +00:00
|
|
|
this.$el.on('click', 'li', this.proxy('toggle_filter'));
|
2012-03-26 14:50:17 +00:00
|
|
|
return $.when(null);
|
|
|
|
},
|
2012-05-18 15:23:42 +00:00
|
|
|
/**
|
|
|
|
* Handles change of the search query: any of the group's filter which is
|
|
|
|
* in the search query should be visually checked in the drawer
|
|
|
|
*/
|
|
|
|
search_change: function () {
|
|
|
|
var self = this;
|
2014-05-19 15:03:05 +00:00
|
|
|
var $filters = this.$('> li').removeClass('badge');
|
2012-05-18 15:23:42 +00:00
|
|
|
var facet = this.view.query.find(_.bind(this.match_facet, this));
|
|
|
|
if (!facet) { return; }
|
|
|
|
facet.values.each(function (v) {
|
|
|
|
var i = _(self.filters).indexOf(v.get('value'));
|
|
|
|
if (i === -1) { return; }
|
2013-03-22 10:36:06 +00:00
|
|
|
$filters.filter(function () {
|
|
|
|
return Number($(this).data('index')) === i;
|
2014-05-19 15:03:05 +00:00
|
|
|
}).addClass('badge');
|
2012-05-18 15:23:42 +00:00
|
|
|
});
|
|
|
|
},
|
|
|
|
/**
|
|
|
|
* Matches the group to a facet, in order to find if the group is
|
|
|
|
* represented in the current search query
|
|
|
|
*/
|
|
|
|
match_facet: function (facet) {
|
|
|
|
return facet.get('field') === this;
|
|
|
|
},
|
2012-05-04 09:43:06 +00:00
|
|
|
make_facet: function (values) {
|
|
|
|
return {
|
|
|
|
category: _t("Filter"),
|
2012-05-04 09:57:17 +00:00
|
|
|
icon: this.icon,
|
2012-05-04 09:43:06 +00:00
|
|
|
values: values,
|
|
|
|
field: this
|
2013-07-25 10:33:01 +00:00
|
|
|
};
|
2012-05-04 09:43:06 +00:00
|
|
|
},
|
2012-05-15 06:43:40 +00:00
|
|
|
make_value: function (filter) {
|
|
|
|
return {
|
|
|
|
label: filter.attrs.string || filter.attrs.help || filter.attrs.name,
|
|
|
|
value: filter
|
|
|
|
};
|
|
|
|
},
|
2012-03-20 12:00:58 +00:00
|
|
|
facet_for_defaults: function (defaults) {
|
2012-05-15 06:43:40 +00:00
|
|
|
var self = this;
|
2012-04-26 13:58:17 +00:00
|
|
|
var fs = _(this.filters).chain()
|
|
|
|
.filter(function (f) {
|
|
|
|
return f.attrs && f.attrs.name && !!defaults[f.attrs.name];
|
|
|
|
}).map(function (f) {
|
2012-05-15 06:43:40 +00:00
|
|
|
return self.make_value(f);
|
2012-04-26 13:58:17 +00:00
|
|
|
}).value();
|
2012-03-20 12:00:58 +00:00
|
|
|
if (_.isEmpty(fs)) { return $.when(null); }
|
2012-05-04 09:43:06 +00:00
|
|
|
return $.when(this.make_facet(fs));
|
2012-03-21 15:45:50 +00:00
|
|
|
},
|
|
|
|
/**
|
|
|
|
* Fetches contexts for all enabled filters in the group
|
|
|
|
*
|
2012-05-03 12:59:49 +00:00
|
|
|
* @param {openerp.web.search.Facet} facet
|
2012-03-21 15:45:50 +00:00
|
|
|
* @return {*} combined contexts of the enabled filters in this group
|
|
|
|
*/
|
|
|
|
get_context: function (facet) {
|
2012-05-03 19:26:45 +00:00
|
|
|
var contexts = facet.values.chain()
|
2012-05-03 12:59:49 +00:00
|
|
|
.map(function (f) { return f.get('value').attrs.context; })
|
2012-11-29 11:24:13 +00:00
|
|
|
.without('{}')
|
2012-03-21 15:45:50 +00:00
|
|
|
.reject(_.isEmpty)
|
|
|
|
.value();
|
2012-03-20 12:00:58 +00:00
|
|
|
|
2012-03-21 15:45:50 +00:00
|
|
|
if (!contexts.length) { return; }
|
|
|
|
if (contexts.length === 1) { return contexts[0]; }
|
2013-07-25 10:07:49 +00:00
|
|
|
return _.extend(new instance.web.CompoundContext(), {
|
2012-03-21 15:45:50 +00:00
|
|
|
__contexts: contexts
|
|
|
|
});
|
2011-09-15 08:46:26 +00:00
|
|
|
},
|
2012-04-03 12:07:42 +00:00
|
|
|
/**
|
|
|
|
* Fetches group_by sequence for all enabled filters in the group
|
|
|
|
*
|
|
|
|
* @param {VS.model.SearchFacet} facet
|
|
|
|
* @return {Array} enabled filters in this group
|
|
|
|
*/
|
|
|
|
get_groupby: function (facet) {
|
2012-05-03 19:26:45 +00:00
|
|
|
return facet.values.chain()
|
2012-05-03 12:59:49 +00:00
|
|
|
.map(function (f) { return f.get('value').attrs.context; })
|
2012-11-29 11:24:13 +00:00
|
|
|
.without('{}')
|
2012-04-03 12:07:42 +00:00
|
|
|
.reject(_.isEmpty)
|
|
|
|
.value();
|
|
|
|
},
|
2011-09-15 08:46:26 +00:00
|
|
|
/**
|
|
|
|
* Handles domains-fetching for all the filters within it: groups them.
|
2012-03-21 15:45:50 +00:00
|
|
|
*
|
|
|
|
* @param {VS.model.SearchFacet} facet
|
|
|
|
* @return {*} combined domains of the enabled filters in this group
|
2011-09-15 08:46:26 +00:00
|
|
|
*/
|
2012-03-21 15:45:50 +00:00
|
|
|
get_domain: function (facet) {
|
2012-05-03 19:26:45 +00:00
|
|
|
var domains = facet.values.chain()
|
2012-05-03 12:59:49 +00:00
|
|
|
.map(function (f) { return f.get('value').attrs.domain; })
|
2012-11-29 11:24:13 +00:00
|
|
|
.without('[]')
|
2011-09-20 13:46:07 +00:00
|
|
|
.reject(_.isEmpty)
|
2011-09-15 08:46:26 +00:00
|
|
|
.value();
|
|
|
|
|
|
|
|
if (!domains.length) { return; }
|
|
|
|
if (domains.length === 1) { return domains[0]; }
|
|
|
|
for (var i=domains.length; --i;) {
|
|
|
|
domains.unshift(['|']);
|
|
|
|
}
|
2012-04-17 12:02:10 +00:00
|
|
|
return _.extend(new instance.web.CompoundDomain(), {
|
2011-09-15 08:46:26 +00:00
|
|
|
__domains: domains
|
|
|
|
});
|
2012-03-26 14:50:17 +00:00
|
|
|
},
|
|
|
|
toggle_filter: function (e) {
|
2013-03-21 15:51:23 +00:00
|
|
|
this.toggle(this.filters[Number($(e.target).data('index'))]);
|
2012-04-02 12:32:00 +00:00
|
|
|
},
|
|
|
|
toggle: function (filter) {
|
2012-05-15 06:43:40 +00:00
|
|
|
this.view.query.toggle(this.make_facet([this.make_value(filter)]));
|
|
|
|
},
|
|
|
|
complete: function (item) {
|
|
|
|
var self = this;
|
|
|
|
item = item.toLowerCase();
|
|
|
|
var facet_values = _(this.filters).chain()
|
2013-02-14 07:43:02 +00:00
|
|
|
.filter(function (filter) { return filter.visible(); })
|
2012-05-15 06:43:40 +00:00
|
|
|
.filter(function (filter) {
|
|
|
|
var at = {
|
|
|
|
string: filter.attrs.string || '',
|
|
|
|
help: filter.attrs.help || '',
|
|
|
|
name: filter.attrs.name || ''
|
|
|
|
};
|
|
|
|
var include = _.str.include;
|
|
|
|
return include(at.string.toLowerCase(), item)
|
|
|
|
|| include(at.help.toLowerCase(), item)
|
|
|
|
|| include(at.name.toLowerCase(), item);
|
|
|
|
})
|
|
|
|
.map(this.make_value)
|
|
|
|
.value();
|
|
|
|
if (_(facet_values).isEmpty()) { return $.when(null); }
|
|
|
|
return $.when(_.map(facet_values, function (facet_value) {
|
|
|
|
return {
|
|
|
|
label: _.str.sprintf(self.completion_label.toString(),
|
2013-06-18 11:10:59 +00:00
|
|
|
_.escape(facet_value.label)),
|
2012-05-15 06:43:40 +00:00
|
|
|
facet: self.make_facet([facet_value])
|
2013-07-25 10:33:01 +00:00
|
|
|
};
|
2012-05-15 06:43:40 +00:00
|
|
|
}));
|
2012-05-04 09:43:06 +00:00
|
|
|
}
|
|
|
|
});
|
|
|
|
instance.web.search.GroupbyGroup = instance.web.search.FilterGroup.extend({
|
2012-05-04 09:57:17 +00:00
|
|
|
icon: 'w',
|
2012-05-15 06:43:40 +00:00
|
|
|
completion_label: _lt("Group by: %s"),
|
2013-02-14 07:43:02 +00:00
|
|
|
init: function (filters, parent) {
|
|
|
|
this._super(filters, parent);
|
2012-05-04 09:43:06 +00:00
|
|
|
// Not flanders: facet unicity is handled through the
|
|
|
|
// (category, field) pair of facet attributes. This is all well and
|
2012-05-18 15:23:42 +00:00
|
|
|
// good for regular filter groups where a group matches a facet, but for
|
2012-05-04 09:43:06 +00:00
|
|
|
// groupby we want a single facet. So cheat: add an attribute on the
|
|
|
|
// view which proxies to the first GroupbyGroup, so it can be used
|
|
|
|
// for every GroupbyGroup and still provides the various methods needed
|
|
|
|
// by the search view. Use weirdo name to avoid risks of conflicts
|
2013-02-14 07:43:02 +00:00
|
|
|
if (!this.view._s_groupby) {
|
|
|
|
this.view._s_groupby = {
|
2012-05-04 09:43:06 +00:00
|
|
|
help: "See GroupbyGroup#init",
|
|
|
|
get_context: this.proxy('get_context'),
|
|
|
|
get_domain: this.proxy('get_domain'),
|
|
|
|
get_groupby: this.proxy('get_groupby')
|
2013-07-25 10:33:01 +00:00
|
|
|
};
|
2012-05-04 09:43:06 +00:00
|
|
|
}
|
|
|
|
},
|
2012-05-18 15:23:42 +00:00
|
|
|
match_facet: function (facet) {
|
2013-02-14 07:43:02 +00:00
|
|
|
return facet.get('field') === this.view._s_groupby;
|
2012-05-18 15:23:42 +00:00
|
|
|
},
|
2012-05-04 09:43:06 +00:00
|
|
|
make_facet: function (values) {
|
|
|
|
return {
|
|
|
|
category: _t("GroupBy"),
|
2012-05-04 09:57:17 +00:00
|
|
|
icon: this.icon,
|
2012-05-04 09:43:06 +00:00
|
|
|
values: values,
|
2013-03-12 09:28:24 +00:00
|
|
|
field: this.view._s_groupby
|
2012-05-04 09:43:06 +00:00
|
|
|
};
|
2011-09-15 08:46:26 +00:00
|
|
|
}
|
|
|
|
});
|
2012-04-17 12:02:10 +00:00
|
|
|
instance.web.search.Filter = instance.web.search.Input.extend(/** @lends instance.web.search.Filter# */{
|
2011-03-24 16:18:42 +00:00
|
|
|
template: 'SearchView.filter',
|
2011-09-12 12:06:04 +00:00
|
|
|
/**
|
|
|
|
* Implementation of the OpenERP filters (button with a context and/or
|
|
|
|
* a domain sent as-is to the search view)
|
|
|
|
*
|
2012-03-21 15:45:50 +00:00
|
|
|
* Filters are only attributes holder, the actual work (compositing
|
|
|
|
* domains and contexts, converting between facets and filters) is
|
|
|
|
* performed by the filter group.
|
|
|
|
*
|
2012-04-17 12:02:10 +00:00
|
|
|
* @constructs instance.web.search.Filter
|
|
|
|
* @extends instance.web.search.Input
|
2011-09-12 12:06:04 +00:00
|
|
|
*
|
|
|
|
* @param node
|
2013-02-14 07:43:02 +00:00
|
|
|
* @param parent
|
2011-09-12 12:06:04 +00:00
|
|
|
*/
|
2013-02-14 07:43:02 +00:00
|
|
|
init: function (node, parent) {
|
|
|
|
this._super(parent);
|
2011-12-16 15:11:08 +00:00
|
|
|
this.load_attrs(node.attrs);
|
2011-03-25 10:34:25 +00:00
|
|
|
},
|
2012-03-20 12:00:58 +00:00
|
|
|
facet_for: function () { return $.when(null); },
|
2012-03-21 15:45:50 +00:00
|
|
|
get_context: function () { },
|
2012-04-03 12:07:42 +00:00
|
|
|
get_domain: function () { },
|
2011-03-24 16:18:42 +00:00
|
|
|
});
|
2014-06-27 10:05:34 +00:00
|
|
|
|
|
|
|
instance.web.search.Separator = instance.web.search.Input.extend({
|
2014-06-30 12:42:05 +00:00
|
|
|
_in_drawer: false,
|
|
|
|
|
2014-06-27 10:05:34 +00:00
|
|
|
complete: function () {
|
|
|
|
return {is_separator: true};
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
2012-04-17 12:02:10 +00:00
|
|
|
instance.web.search.Field = instance.web.search.Input.extend( /** @lends instance.web.search.Field# */ {
|
2011-03-24 12:46:53 +00:00
|
|
|
template: 'SearchView.field',
|
2011-03-24 17:37:21 +00:00
|
|
|
default_operator: '=',
|
2011-03-30 15:27:36 +00:00
|
|
|
/**
|
2012-04-17 12:02:10 +00:00
|
|
|
* @constructs instance.web.search.Field
|
|
|
|
* @extends instance.web.search.Input
|
2011-03-30 15:27:36 +00:00
|
|
|
*
|
|
|
|
* @param view_section
|
|
|
|
* @param field
|
2013-02-14 07:43:02 +00:00
|
|
|
* @param parent
|
2011-03-30 15:27:36 +00:00
|
|
|
*/
|
2013-02-14 07:43:02 +00:00
|
|
|
init: function (view_section, field, parent) {
|
|
|
|
this._super(parent);
|
2011-12-16 15:11:08 +00:00
|
|
|
this.load_attrs(_.extend({}, field, view_section.attrs));
|
2011-03-24 16:18:42 +00:00
|
|
|
},
|
2012-03-20 10:28:46 +00:00
|
|
|
facet_for: function (value) {
|
2012-04-24 10:59:41 +00:00
|
|
|
return $.when({
|
2012-04-26 13:58:17 +00:00
|
|
|
field: this,
|
2012-03-20 15:29:35 +00:00
|
|
|
category: this.attrs.string || this.attrs.name,
|
2012-04-24 10:59:41 +00:00
|
|
|
values: [{label: String(value), value: value}]
|
|
|
|
});
|
2012-03-20 10:28:46 +00:00
|
|
|
},
|
2012-05-03 15:43:11 +00:00
|
|
|
value_from: function (facetValue) {
|
|
|
|
return facetValue.get('value');
|
2012-03-21 15:45:50 +00:00
|
|
|
},
|
|
|
|
get_context: function (facet) {
|
2012-05-03 15:43:11 +00:00
|
|
|
var self = this;
|
|
|
|
// A field needs a context to send when active
|
2011-03-29 09:09:41 +00:00
|
|
|
var context = this.attrs.context;
|
2012-06-27 09:57:44 +00:00
|
|
|
if (_.isEmpty(context) || !facet.values.length) {
|
2011-03-24 17:37:21 +00:00
|
|
|
return;
|
|
|
|
}
|
2012-05-03 15:43:11 +00:00
|
|
|
var contexts = facet.values.map(function (facetValue) {
|
|
|
|
return new instance.web.CompoundContext(context)
|
|
|
|
.set_eval_context({self: self.value_from(facetValue)});
|
|
|
|
});
|
|
|
|
|
|
|
|
if (contexts.length === 1) { return contexts[0]; }
|
|
|
|
|
2013-07-25 10:07:49 +00:00
|
|
|
return _.extend(new instance.web.CompoundContext(), {
|
2012-05-03 15:43:11 +00:00
|
|
|
__contexts: contexts
|
|
|
|
});
|
2011-03-24 17:37:21 +00:00
|
|
|
},
|
2012-04-03 12:07:42 +00:00
|
|
|
get_groupby: function () { },
|
2011-08-24 14:52:05 +00:00
|
|
|
/**
|
|
|
|
* Function creating the returned domain for the field, override this
|
|
|
|
* methods in children if you only need to customize the field's domain
|
|
|
|
* without more complex alterations or tests (and without the need to
|
|
|
|
* change override the handling of filter_domain)
|
|
|
|
*
|
|
|
|
* @param {String} name the field's name
|
|
|
|
* @param {String} operator the field's operator (either attribute-specified or default operator for the field
|
2013-02-14 07:43:02 +00:00
|
|
|
* @param {Number|String} facet parsed value for the field
|
2011-08-24 14:52:05 +00:00
|
|
|
* @returns {Array<Array>} domain to include in the resulting search
|
|
|
|
*/
|
2012-03-21 15:45:50 +00:00
|
|
|
make_domain: function (name, operator, facet) {
|
2012-05-03 15:43:11 +00:00
|
|
|
return [[name, operator, this.value_from(facet)]];
|
2011-08-24 14:52:05 +00:00
|
|
|
},
|
2012-03-21 15:45:50 +00:00
|
|
|
get_domain: function (facet) {
|
2012-05-03 15:43:11 +00:00
|
|
|
if (!facet.values.length) { return; }
|
2011-03-17 13:41:55 +00:00
|
|
|
|
2012-05-03 15:43:11 +00:00
|
|
|
var value_to_domain;
|
|
|
|
var self = this;
|
2011-03-29 09:09:41 +00:00
|
|
|
var domain = this.attrs['filter_domain'];
|
2012-05-03 15:43:11 +00:00
|
|
|
if (domain) {
|
|
|
|
value_to_domain = function (facetValue) {
|
|
|
|
return new instance.web.CompoundDomain(domain)
|
|
|
|
.set_eval_context({self: self.value_from(facetValue)});
|
|
|
|
};
|
|
|
|
} else {
|
|
|
|
value_to_domain = function (facetValue) {
|
|
|
|
return self.make_domain(
|
|
|
|
self.attrs.name,
|
|
|
|
self.attrs.operator || self.default_operator,
|
|
|
|
facetValue);
|
|
|
|
};
|
2011-03-24 17:37:21 +00:00
|
|
|
}
|
2012-05-03 15:43:11 +00:00
|
|
|
var domains = facet.values.map(value_to_domain);
|
|
|
|
|
|
|
|
if (domains.length === 1) { return domains[0]; }
|
|
|
|
for (var i = domains.length; --i;) {
|
|
|
|
domains.unshift(['|']);
|
|
|
|
}
|
|
|
|
|
2013-07-25 10:07:49 +00:00
|
|
|
return _.extend(new instance.web.CompoundDomain(), {
|
2012-05-03 15:43:11 +00:00
|
|
|
__domains: domains
|
|
|
|
});
|
2011-03-24 12:46:53 +00:00
|
|
|
}
|
|
|
|
});
|
2011-03-30 15:27:36 +00:00
|
|
|
/**
|
|
|
|
* Implementation of the ``char`` OpenERP field type:
|
|
|
|
*
|
|
|
|
* * Default operator is ``ilike`` rather than ``=``
|
|
|
|
*
|
|
|
|
* * The Javascript and the HTML values are identical (strings)
|
|
|
|
*
|
|
|
|
* @class
|
2012-04-17 12:02:10 +00:00
|
|
|
* @extends instance.web.search.Field
|
2011-03-30 15:27:36 +00:00
|
|
|
*/
|
2012-04-17 12:02:10 +00:00
|
|
|
instance.web.search.CharField = instance.web.search.Field.extend( /** @lends instance.web.search.CharField# */ {
|
2011-03-24 18:01:29 +00:00
|
|
|
default_operator: 'ilike',
|
2012-03-19 16:43:04 +00:00
|
|
|
complete: function (value) {
|
2012-03-28 10:36:36 +00:00
|
|
|
if (_.isEmpty(value)) { return $.when(null); }
|
2012-03-21 09:28:47 +00:00
|
|
|
var label = _.str.sprintf(_.str.escapeHTML(
|
|
|
|
_t("Search %(field)s for: %(value)s")), {
|
2013-06-18 11:10:59 +00:00
|
|
|
field: '<em>' + _.escape(this.attrs.string) + '</em>',
|
|
|
|
value: '<strong>' + _.escape(value) + '</strong>'});
|
2012-03-21 09:28:47 +00:00
|
|
|
return $.when([{
|
|
|
|
label: label,
|
2012-04-27 08:06:07 +00:00
|
|
|
facet: {
|
|
|
|
category: this.attrs.string,
|
|
|
|
field: this,
|
|
|
|
values: [{label: value, value: value}]
|
|
|
|
}
|
2012-03-21 09:28:47 +00:00
|
|
|
}]);
|
2011-03-24 17:37:21 +00:00
|
|
|
}
|
2011-03-24 12:46:53 +00:00
|
|
|
});
|
2012-04-17 12:02:10 +00:00
|
|
|
instance.web.search.NumberField = instance.web.search.Field.extend(/** @lends instance.web.search.NumberField# */{
|
2013-03-28 12:03:37 +00:00
|
|
|
complete: function (value) {
|
|
|
|
var val = this.parse(value);
|
2013-03-28 15:35:02 +00:00
|
|
|
if (isNaN(val)) { return $.when(); }
|
|
|
|
var label = _.str.sprintf(
|
|
|
|
_t("Search %(field)s for: %(value)s"), {
|
2013-06-18 11:10:59 +00:00
|
|
|
field: '<em>' + _.escape(this.attrs.string) + '</em>',
|
|
|
|
value: '<strong>' + _.escape(value) + '</strong>'});
|
2013-03-28 12:03:37 +00:00
|
|
|
return $.when([{
|
|
|
|
label: label,
|
|
|
|
facet: {
|
|
|
|
category: this.attrs.string,
|
|
|
|
field: this,
|
|
|
|
values: [{label: value, value: val}]
|
|
|
|
}
|
|
|
|
}]);
|
|
|
|
},
|
2011-03-24 19:24:59 +00:00
|
|
|
});
|
2011-07-20 11:01:34 +00:00
|
|
|
/**
|
|
|
|
* @class
|
2012-04-17 12:02:10 +00:00
|
|
|
* @extends instance.web.search.NumberField
|
2011-07-20 11:01:34 +00:00
|
|
|
*/
|
2012-04-17 12:02:10 +00:00
|
|
|
instance.web.search.IntegerField = instance.web.search.NumberField.extend(/** @lends instance.web.search.IntegerField# */{
|
2011-11-14 10:12:21 +00:00
|
|
|
error_message: _t("not a valid integer"),
|
2011-07-20 11:01:34 +00:00
|
|
|
parse: function (value) {
|
2011-09-19 13:46:15 +00:00
|
|
|
try {
|
2012-04-17 12:02:10 +00:00
|
|
|
return instance.web.parse_value(value, {'widget': 'integer'});
|
2011-09-19 13:46:15 +00:00
|
|
|
} catch (e) {
|
|
|
|
return NaN;
|
|
|
|
}
|
2011-07-20 11:01:34 +00:00
|
|
|
}
|
|
|
|
});
|
|
|
|
/**
|
|
|
|
* @class
|
2012-04-17 12:02:10 +00:00
|
|
|
* @extends instance.web.search.NumberField
|
2011-07-20 11:01:34 +00:00
|
|
|
*/
|
2012-04-17 12:02:10 +00:00
|
|
|
instance.web.search.FloatField = instance.web.search.NumberField.extend(/** @lends instance.web.search.FloatField# */{
|
2011-11-14 10:12:21 +00:00
|
|
|
error_message: _t("not a valid number"),
|
2011-07-20 11:01:34 +00:00
|
|
|
parse: function (value) {
|
2011-09-19 13:20:26 +00:00
|
|
|
try {
|
2012-04-17 12:02:10 +00:00
|
|
|
return instance.web.parse_value(value, {'widget': 'float'});
|
2011-09-19 13:20:26 +00:00
|
|
|
} catch (e) {
|
|
|
|
return NaN;
|
|
|
|
}
|
2011-03-24 17:37:21 +00:00
|
|
|
}
|
2011-03-24 12:46:53 +00:00
|
|
|
});
|
2012-04-27 08:30:24 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Utility function for m2o & selection fields taking a selection/name_get pair
|
|
|
|
* (value, name) and converting it to a Facet descriptor
|
|
|
|
*
|
|
|
|
* @param {instance.web.search.Field} field holder field
|
|
|
|
* @param {Array} pair pair value to convert
|
|
|
|
*/
|
|
|
|
function facet_from(field, pair) {
|
|
|
|
return {
|
|
|
|
field: field,
|
|
|
|
category: field['attrs'].string,
|
|
|
|
values: [{label: pair[1], value: pair[0]}]
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2011-08-10 10:49:10 +00:00
|
|
|
/**
|
|
|
|
* @class
|
2012-04-17 12:02:10 +00:00
|
|
|
* @extends instance.web.search.Field
|
2011-08-10 10:49:10 +00:00
|
|
|
*/
|
2012-04-17 12:02:10 +00:00
|
|
|
instance.web.search.SelectionField = instance.web.search.Field.extend(/** @lends instance.web.search.SelectionField# */{
|
[FIX] m2o fields with selection widget in search view
Plan was originally to just ignore this because "it should just work"
but turns out m2o and m2o[@widget=selection] fields have very
different behaviors when it comes to default values, especially
custom domains and contexts:
* An m2o field uses its string value always (behaves like a char
field), for UI and clarity purposes we added an [[name, '=', id]]
case when the user specifically selects an autocompletion value,
but that's not "cannon", when it comes to dealing with custom
domains (filter_domain) and contexts the field always uses its
string value.
* An m2o[@widget=selection] field on the other hand uses its ids
always (behaves like a selection field).
That's not entirely true, really, because it has the converse to
what we implemented on the m2o field in the web client (in the GTK
client): if there is no @filter_domain *and* the user has entered a
value which is not in the dropdown (it's a combobox in the GTK
client), then it falls back on using 'ilike'. This string value is
*not* used in custom domains and custom filters, which simply are
not submitted.
This second section has *not* been implemented so far in the web
client, we'll come round to it if people actually need it.
bzr revid: xmo@openerp.com-20111006063949-fl5rbg3wwubcaay8
2011-10-06 06:39:49 +00:00
|
|
|
// This implementation is a basic <select> field, but it may have to be
|
|
|
|
// altered to be more in line with the GTK client, which uses a combo box
|
|
|
|
// (~ jquery.autocomplete):
|
|
|
|
// * If an option was selected in the list, behave as currently
|
|
|
|
// * If something which is not in the list was entered (via the text input),
|
|
|
|
// the default domain should become (`ilike` string_value) but **any
|
|
|
|
// ``context`` or ``filter_domain`` becomes falsy, idem if ``@operator``
|
|
|
|
// is specified. So at least get_domain needs to be quite a bit
|
|
|
|
// overridden (if there's no @value and there is no filter_domain and
|
|
|
|
// there is no @operator, return [[name, 'ilike', str_val]]
|
2011-03-24 18:47:13 +00:00
|
|
|
template: 'SearchView.field.selection',
|
[FIX] m2o fields with selection widget in search view
Plan was originally to just ignore this because "it should just work"
but turns out m2o and m2o[@widget=selection] fields have very
different behaviors when it comes to default values, especially
custom domains and contexts:
* An m2o field uses its string value always (behaves like a char
field), for UI and clarity purposes we added an [[name, '=', id]]
case when the user specifically selects an autocompletion value,
but that's not "cannon", when it comes to dealing with custom
domains (filter_domain) and contexts the field always uses its
string value.
* An m2o[@widget=selection] field on the other hand uses its ids
always (behaves like a selection field).
That's not entirely true, really, because it has the converse to
what we implemented on the m2o field in the web client (in the GTK
client): if there is no @filter_domain *and* the user has entered a
value which is not in the dropdown (it's a combobox in the GTK
client), then it falls back on using 'ilike'. This string value is
*not* used in custom domains and custom filters, which simply are
not submitted.
This second section has *not* been implemented so far in the web
client, we'll come round to it if people actually need it.
bzr revid: xmo@openerp.com-20111006063949-fl5rbg3wwubcaay8
2011-10-06 06:39:49 +00:00
|
|
|
init: function () {
|
|
|
|
this._super.apply(this, arguments);
|
|
|
|
// prepend empty option if there is no empty option in the selection list
|
|
|
|
this.prepend_empty = !_(this.attrs.selection).detect(function (item) {
|
|
|
|
return !item[1];
|
|
|
|
});
|
|
|
|
},
|
2012-03-19 16:43:04 +00:00
|
|
|
complete: function (needle) {
|
|
|
|
var self = this;
|
|
|
|
var results = _(this.attrs.selection).chain()
|
|
|
|
.filter(function (sel) {
|
|
|
|
var value = sel[0], label = sel[1];
|
2015-01-30 12:39:02 +00:00
|
|
|
if (value === undefined || !label) { return false; }
|
2012-03-19 16:43:04 +00:00
|
|
|
return label.toLowerCase().indexOf(needle.toLowerCase()) !== -1;
|
|
|
|
})
|
|
|
|
.map(function (sel) {
|
|
|
|
return {
|
2013-06-18 11:10:59 +00:00
|
|
|
label: _.escape(sel[1]),
|
2015-01-30 10:37:59 +00:00
|
|
|
indent: true,
|
2012-04-27 08:30:24 +00:00
|
|
|
facet: facet_from(self, sel)
|
2012-03-19 16:43:04 +00:00
|
|
|
};
|
|
|
|
}).value();
|
|
|
|
if (_.isEmpty(results)) { return $.when(null); }
|
2012-04-27 08:06:07 +00:00
|
|
|
return $.when.call(null, [{
|
2013-06-18 11:10:59 +00:00
|
|
|
label: _.escape(this.attrs.string)
|
2012-03-21 09:28:47 +00:00
|
|
|
}].concat(results));
|
2012-03-19 16:43:04 +00:00
|
|
|
},
|
2012-03-20 10:28:46 +00:00
|
|
|
facet_for: function (value) {
|
|
|
|
var match = _(this.attrs.selection).detect(function (sel) {
|
|
|
|
return sel[0] === value;
|
|
|
|
});
|
|
|
|
if (!match) { return $.when(null); }
|
2012-04-27 08:30:24 +00:00
|
|
|
return $.when(facet_from(this, match));
|
2011-03-24 18:47:13 +00:00
|
|
|
}
|
|
|
|
});
|
2012-04-17 12:02:10 +00:00
|
|
|
instance.web.search.BooleanField = instance.web.search.SelectionField.extend(/** @lends instance.web.search.BooleanField# */{
|
2011-08-10 10:49:10 +00:00
|
|
|
/**
|
2012-04-17 12:02:10 +00:00
|
|
|
* @constructs instance.web.search.BooleanField
|
|
|
|
* @extends instance.web.search.BooleanField
|
2011-08-10 10:49:10 +00:00
|
|
|
*/
|
|
|
|
init: function () {
|
|
|
|
this._super.apply(this, arguments);
|
|
|
|
this.attrs.selection = [
|
2012-05-03 15:47:51 +00:00
|
|
|
[true, _t("Yes")],
|
|
|
|
[false, _t("No")]
|
2011-08-10 10:49:10 +00:00
|
|
|
];
|
|
|
|
}
|
|
|
|
});
|
2011-09-12 12:06:04 +00:00
|
|
|
/**
|
|
|
|
* @class
|
2012-04-17 12:02:10 +00:00
|
|
|
* @extends instance.web.search.DateField
|
2011-09-12 12:06:04 +00:00
|
|
|
*/
|
2012-04-17 12:02:10 +00:00
|
|
|
instance.web.search.DateField = instance.web.search.Field.extend(/** @lends instance.web.search.DateField# */{
|
2012-05-03 15:47:51 +00:00
|
|
|
value_from: function (facetValue) {
|
2012-05-21 10:43:34 +00:00
|
|
|
return instance.web.date_to_str(facetValue.get('value'));
|
2011-04-01 15:17:35 +00:00
|
|
|
},
|
2012-04-03 09:08:07 +00:00
|
|
|
complete: function (needle) {
|
2014-11-14 16:57:10 +00:00
|
|
|
var d;
|
2014-10-22 10:07:00 +00:00
|
|
|
try {
|
2014-11-14 16:57:10 +00:00
|
|
|
var t = (this.attrs && this.attrs.type === 'datetime') ? 'datetime' : 'date';
|
|
|
|
var v = instance.web.parse_value(needle, {'widget': t});
|
|
|
|
if (t === 'datetime'){
|
|
|
|
d = instance.web.str_to_datetime(v);
|
|
|
|
}
|
|
|
|
else{
|
|
|
|
d = instance.web.str_to_date(v);
|
|
|
|
}
|
2014-10-22 10:07:00 +00:00
|
|
|
} catch (e) {
|
2014-11-14 16:57:10 +00:00
|
|
|
// pass
|
2014-10-22 10:07:00 +00:00
|
|
|
}
|
2012-04-03 09:08:07 +00:00
|
|
|
if (!d) { return $.when(null); }
|
2012-04-27 08:06:07 +00:00
|
|
|
var date_string = instance.web.format_value(d, this.attrs);
|
2012-04-03 09:08:07 +00:00
|
|
|
var label = _.str.sprintf(_.str.escapeHTML(
|
|
|
|
_t("Search %(field)s at: %(value)s")), {
|
2013-06-18 11:10:59 +00:00
|
|
|
field: '<em>' + _.escape(this.attrs.string) + '</em>',
|
2012-04-27 08:06:07 +00:00
|
|
|
value: '<strong>' + date_string + '</strong>'});
|
2012-04-03 09:08:07 +00:00
|
|
|
return $.when([{
|
|
|
|
label: label,
|
2012-04-27 08:06:07 +00:00
|
|
|
facet: {
|
|
|
|
category: this.attrs.string,
|
|
|
|
field: this,
|
|
|
|
values: [{label: date_string, value: d}]
|
|
|
|
}
|
2012-04-03 09:08:07 +00:00
|
|
|
}]);
|
2011-03-24 17:37:21 +00:00
|
|
|
}
|
2011-03-24 12:46:53 +00:00
|
|
|
});
|
2011-08-24 14:56:47 +00:00
|
|
|
/**
|
|
|
|
* Implementation of the ``datetime`` openerp field type:
|
|
|
|
*
|
|
|
|
* * Uses the same widget as the ``date`` field type (a simple date)
|
|
|
|
*
|
|
|
|
* * Builds a slighly more complex, it's a datetime range (includes time)
|
|
|
|
* spanning the whole day selected by the date widget
|
|
|
|
*
|
|
|
|
* @class
|
2012-04-17 12:02:10 +00:00
|
|
|
* @extends instance.web.DateField
|
2011-08-24 14:56:47 +00:00
|
|
|
*/
|
2012-04-17 12:02:10 +00:00
|
|
|
instance.web.search.DateTimeField = instance.web.search.DateField.extend(/** @lends instance.web.search.DateTimeField# */{
|
2012-05-03 15:47:51 +00:00
|
|
|
value_from: function (facetValue) {
|
2012-05-21 10:43:34 +00:00
|
|
|
return instance.web.datetime_to_str(facetValue.get('value'));
|
2011-08-24 14:52:05 +00:00
|
|
|
}
|
2011-03-24 18:16:55 +00:00
|
|
|
});
|
2012-04-17 12:02:10 +00:00
|
|
|
instance.web.search.ManyToOneField = instance.web.search.CharField.extend({
|
2012-05-03 16:37:09 +00:00
|
|
|
default_operator: {},
|
2013-02-14 07:43:02 +00:00
|
|
|
init: function (view_section, field, parent) {
|
|
|
|
this._super(view_section, field, parent);
|
2012-04-17 12:02:10 +00:00
|
|
|
this.model = new instance.web.Model(this.attrs.relation);
|
2011-07-15 11:58:00 +00:00
|
|
|
},
|
2014-06-25 11:26:41 +00:00
|
|
|
|
|
|
|
complete: function (value) {
|
|
|
|
if (_.isEmpty(value)) { return $.when(null); }
|
|
|
|
var label = _.str.sprintf(_.str.escapeHTML(
|
|
|
|
_t("Search %(field)s for: %(value)s")), {
|
|
|
|
field: '<em>' + _.escape(this.attrs.string) + '</em>',
|
|
|
|
value: '<strong>' + _.escape(value) + '</strong>'});
|
|
|
|
return $.when([{
|
|
|
|
label: label,
|
|
|
|
facet: {
|
|
|
|
category: this.attrs.string,
|
|
|
|
field: this,
|
2014-08-20 08:54:30 +00:00
|
|
|
values: [{label: value, value: value, operator: 'ilike'}]
|
2014-06-25 11:26:41 +00:00
|
|
|
},
|
|
|
|
expand: this.expand.bind(this),
|
|
|
|
}]);
|
|
|
|
},
|
|
|
|
|
|
|
|
expand: function (needle) {
|
2012-03-19 16:43:04 +00:00
|
|
|
var self = this;
|
|
|
|
// FIXME: "concurrent" searches (multiple requests, mis-ordered responses)
|
2013-02-25 16:52:36 +00:00
|
|
|
var context = instance.web.pyeval.eval(
|
|
|
|
'contexts', [this.view.dataset.get_context()]);
|
2015-04-17 08:06:14 +00:00
|
|
|
var args = this.attrs.domain;
|
|
|
|
if(typeof args === 'string') {
|
|
|
|
try {
|
|
|
|
args = instance.web.pyeval.eval('domain', args);
|
|
|
|
} catch(e) {
|
|
|
|
args = [];
|
|
|
|
}
|
|
|
|
}
|
2012-03-21 16:56:56 +00:00
|
|
|
return this.model.call('name_search', [], {
|
2012-03-19 16:43:04 +00:00
|
|
|
name: needle,
|
2015-04-17 08:06:14 +00:00
|
|
|
args: args,
|
2012-03-19 16:43:04 +00:00
|
|
|
limit: 8,
|
2013-02-25 16:52:36 +00:00
|
|
|
context: context
|
2012-10-30 14:06:30 +00:00
|
|
|
}).then(function (results) {
|
2012-03-19 16:43:04 +00:00
|
|
|
if (_.isEmpty(results)) { return null; }
|
2014-06-27 09:40:37 +00:00
|
|
|
return _(results).map(function (result) {
|
|
|
|
return {
|
|
|
|
label: _.escape(result[1]),
|
|
|
|
facet: facet_from(self, result)
|
|
|
|
};
|
|
|
|
});
|
2012-03-19 16:43:04 +00:00
|
|
|
});
|
|
|
|
},
|
2012-03-20 10:28:46 +00:00
|
|
|
facet_for: function (value) {
|
2012-04-27 08:30:24 +00:00
|
|
|
var self = this;
|
2012-03-20 10:28:46 +00:00
|
|
|
if (value instanceof Array) {
|
2012-07-24 11:56:59 +00:00
|
|
|
if (value.length === 2 && _.isString(value[1])) {
|
|
|
|
return $.when(facet_from(this, value));
|
|
|
|
}
|
2012-09-28 13:06:53 +00:00
|
|
|
assert(value.length <= 1,
|
2012-10-02 22:07:20 +00:00
|
|
|
_t("M2O search fields do not currently handle multiple default values"));
|
2012-07-24 11:56:59 +00:00
|
|
|
// there are many cases of {search_default_$m2ofield: [id]}, need
|
|
|
|
// to handle this as if it were a single value.
|
|
|
|
value = value[0];
|
2012-03-20 10:28:46 +00:00
|
|
|
}
|
2015-04-16 10:15:25 +00:00
|
|
|
var context = instance.web.pyeval.eval('contexts', [this.view.dataset.get_context()]);
|
|
|
|
return this.model.call('name_get', [value], {context: context}).then(function (names) {
|
2012-04-26 13:58:17 +00:00
|
|
|
if (_(names).isEmpty()) { return null; }
|
2012-04-27 08:30:24 +00:00
|
|
|
return facet_from(self, names[0]);
|
2013-07-25 10:33:01 +00:00
|
|
|
});
|
2012-05-03 16:37:09 +00:00
|
|
|
},
|
|
|
|
value_from: function (facetValue) {
|
|
|
|
return facetValue.get('label');
|
|
|
|
},
|
|
|
|
make_domain: function (name, operator, facetValue) {
|
2014-08-20 08:54:30 +00:00
|
|
|
operator = facetValue.get('operator') || operator;
|
|
|
|
|
2014-04-22 15:09:18 +00:00
|
|
|
switch(operator){
|
|
|
|
case this.default_operator:
|
2012-05-03 16:37:09 +00:00
|
|
|
return [[name, '=', facetValue.get('value')]];
|
2014-08-20 08:54:30 +00:00
|
|
|
case 'ilike':
|
|
|
|
return [[name, 'ilike', facetValue.get('value')]];
|
2014-04-22 15:09:18 +00:00
|
|
|
case 'child_of':
|
|
|
|
return [[name, 'child_of', facetValue.get('value')]];
|
2012-05-03 16:37:09 +00:00
|
|
|
}
|
|
|
|
return this._super(name, operator, facetValue);
|
2012-06-26 09:13:07 +00:00
|
|
|
},
|
|
|
|
get_context: function (facet) {
|
|
|
|
var values = facet.values;
|
2012-06-27 09:57:44 +00:00
|
|
|
if (_.isEmpty(this.attrs.context) && values.length === 1) {
|
2012-06-26 09:13:07 +00:00
|
|
|
var c = {};
|
2014-11-27 16:04:02 +00:00
|
|
|
var v = values.at(0);
|
|
|
|
if (v.get('operator') !== 'ilike') {
|
|
|
|
c['default_' + this.attrs.name] = v.get('value');
|
|
|
|
}
|
2012-06-26 09:13:07 +00:00
|
|
|
return c;
|
|
|
|
}
|
|
|
|
return this._super(facet);
|
2011-07-15 11:58:00 +00:00
|
|
|
}
|
2011-03-17 13:41:55 +00:00
|
|
|
});
|
|
|
|
|
2014-06-30 17:48:11 +00:00
|
|
|
instance.web.search.CustomFilters = instance.web.search.Input.extend({
|
|
|
|
template: 'SearchView.Custom',
|
2012-05-10 18:52:14 +00:00
|
|
|
_in_drawer: true,
|
2012-05-23 11:23:14 +00:00
|
|
|
init: function () {
|
|
|
|
this.is_ready = $.Deferred();
|
2014-05-21 11:18:30 +00:00
|
|
|
this._super.apply(this,arguments);
|
2012-05-23 11:23:14 +00:00
|
|
|
},
|
2012-05-08 16:21:37 +00:00
|
|
|
start: function () {
|
2012-05-11 14:32:25 +00:00
|
|
|
var self = this;
|
2012-05-10 18:52:14 +00:00
|
|
|
this.model = new instance.web.Model('ir.filters');
|
|
|
|
this.filters = {};
|
2012-05-23 10:49:59 +00:00
|
|
|
this.$filters = {};
|
2012-06-26 08:10:55 +00:00
|
|
|
this.view.query
|
|
|
|
.on('remove', function (facet) {
|
|
|
|
if (!facet.get('is_custom_filter')) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
self.clear_selection();
|
|
|
|
})
|
|
|
|
.on('reset', this.proxy('clear_selection'));
|
2014-06-30 17:15:30 +00:00
|
|
|
return this.model.call('get_filters', [this.view.model, this.get_action_id()])
|
2012-11-12 16:41:36 +00:00
|
|
|
.then(this.proxy('set_filters'))
|
2012-11-26 09:38:18 +00:00
|
|
|
.done(function () { self.is_ready.resolve(); })
|
|
|
|
.fail(function () { self.is_ready.reject.apply(self.is_ready, arguments); });
|
2012-05-23 11:23:14 +00:00
|
|
|
},
|
2014-06-30 17:15:30 +00:00
|
|
|
get_action_id: function(){
|
|
|
|
var action = instance.client.action_manager.inner_action;
|
|
|
|
if (action) return action.id;
|
|
|
|
},
|
2012-05-23 11:23:14 +00:00
|
|
|
/**
|
|
|
|
* Special implementation delaying defaults until CustomFilters is loaded
|
|
|
|
*/
|
|
|
|
facet_for_defaults: function () {
|
|
|
|
return this.is_ready;
|
2012-05-08 16:21:37 +00:00
|
|
|
},
|
2012-05-23 10:49:59 +00:00
|
|
|
/**
|
|
|
|
* Generates a mapping key (in the filters and $filter mappings) for the
|
|
|
|
* filter descriptor object provided (as returned by ``get_filters``).
|
|
|
|
*
|
|
|
|
* The mapping key is guaranteed to be unique for a given (user_id, name)
|
|
|
|
* pair.
|
|
|
|
*
|
|
|
|
* @param {Object} filter
|
|
|
|
* @param {String} filter.name
|
|
|
|
* @param {Number|Pair<Number, String>} [filter.user_id]
|
|
|
|
* @return {String} mapping key corresponding to the filter
|
|
|
|
*/
|
|
|
|
key_for: function (filter) {
|
2014-06-30 17:15:30 +00:00
|
|
|
var user_id = filter.user_id,
|
|
|
|
action_id = filter.action_id;
|
2012-05-23 10:49:59 +00:00
|
|
|
var uid = (user_id instanceof Array) ? user_id[0] : user_id;
|
2014-06-30 17:15:30 +00:00
|
|
|
var act_id = (action_id instanceof Array) ? action_id[0] : action_id;
|
|
|
|
return _.str.sprintf('(%s)(%s)%s', uid, act_id, filter.name);
|
2012-05-23 10:49:59 +00:00
|
|
|
},
|
2012-05-23 11:23:14 +00:00
|
|
|
/**
|
|
|
|
* Generates a :js:class:`~instance.web.search.Facet` descriptor from a
|
|
|
|
* filter descriptor
|
|
|
|
*
|
|
|
|
* @param {Object} filter
|
|
|
|
* @param {String} filter.name
|
|
|
|
* @param {Object} [filter.context]
|
|
|
|
* @param {Array} [filter.domain]
|
|
|
|
* @return {Object}
|
|
|
|
*/
|
|
|
|
facet_for: function (filter) {
|
|
|
|
return {
|
2012-11-07 11:25:16 +00:00
|
|
|
category: _t("Custom Filter"),
|
2012-05-23 11:23:14 +00:00
|
|
|
icon: 'M',
|
|
|
|
field: {
|
|
|
|
get_context: function () { return filter.context; },
|
|
|
|
get_groupby: function () { return [filter.context]; },
|
|
|
|
get_domain: function () { return filter.domain; }
|
|
|
|
},
|
2013-01-31 13:51:25 +00:00
|
|
|
_id: filter['id'],
|
2012-11-07 11:25:16 +00:00
|
|
|
is_custom_filter: true,
|
|
|
|
values: [{label: filter.name, value: null}]
|
2012-05-23 11:23:14 +00:00
|
|
|
};
|
|
|
|
},
|
2012-06-26 08:10:55 +00:00
|
|
|
clear_selection: function () {
|
2014-05-19 15:03:05 +00:00
|
|
|
this.$('span.badge').removeClass('badge');
|
2012-06-26 08:10:55 +00:00
|
|
|
},
|
2012-05-10 18:52:14 +00:00
|
|
|
append_filter: function (filter) {
|
2012-05-08 16:21:37 +00:00
|
|
|
var self = this;
|
2012-05-23 10:49:59 +00:00
|
|
|
var key = this.key_for(filter);
|
2013-01-23 10:53:20 +00:00
|
|
|
var warning = _t("This filter is global and will be removed for everybody if you continue.");
|
2012-05-10 18:52:14 +00:00
|
|
|
|
|
|
|
var $filter;
|
2012-05-23 10:49:59 +00:00
|
|
|
if (key in this.$filters) {
|
|
|
|
$filter = this.$filters[key];
|
2012-05-10 18:52:14 +00:00
|
|
|
} else {
|
|
|
|
var id = filter.id;
|
2012-05-23 10:49:59 +00:00
|
|
|
this.filters[key] = filter;
|
2014-05-19 15:03:05 +00:00
|
|
|
$filter = $('<li></li>')
|
2012-11-06 15:21:48 +00:00
|
|
|
.appendTo(this.$('.oe_searchview_custom_list'))
|
2012-11-12 16:24:30 +00:00
|
|
|
.toggleClass('oe_searchview_custom_default', filter.is_default)
|
2014-05-19 15:03:05 +00:00
|
|
|
.append(this.$filters[key] = $('<span>').text(filter.name));
|
|
|
|
|
|
|
|
this.$filters[key].addClass(filter.user_id ? 'oe_searchview_custom_private'
|
|
|
|
: 'oe_searchview_custom_public')
|
2012-05-10 19:04:39 +00:00
|
|
|
|
2012-05-25 17:00:53 +00:00
|
|
|
$('<a class="oe_searchview_custom_delete">x</a>')
|
2012-05-11 14:32:25 +00:00
|
|
|
.click(function (e) {
|
|
|
|
e.stopPropagation();
|
2013-01-23 10:53:20 +00:00
|
|
|
if (!(filter.user_id || confirm(warning))) {
|
|
|
|
return;
|
|
|
|
}
|
2012-10-30 14:06:30 +00:00
|
|
|
self.model.call('unlink', [id]).done(function () {
|
2012-05-10 18:52:14 +00:00
|
|
|
$filter.remove();
|
2012-05-23 10:49:59 +00:00
|
|
|
delete self.$filters[key];
|
|
|
|
delete self.filters[key];
|
2014-05-30 09:33:56 +00:00
|
|
|
if (_.isEmpty(self.filters)) {
|
2014-05-21 12:27:17 +00:00
|
|
|
self.hide();
|
|
|
|
}
|
2012-05-10 18:52:14 +00:00
|
|
|
});
|
2012-05-15 06:12:12 +00:00
|
|
|
})
|
|
|
|
.appendTo($filter);
|
2012-05-10 18:52:14 +00:00
|
|
|
}
|
|
|
|
|
2014-05-19 15:03:05 +00:00
|
|
|
this.$filters[key].unbind('click').click(function () {
|
2013-01-31 11:26:17 +00:00
|
|
|
self.toggle_filter(filter);
|
2012-05-10 18:52:14 +00:00
|
|
|
});
|
2014-05-21 12:27:17 +00:00
|
|
|
this.show();
|
2012-05-10 18:52:14 +00:00
|
|
|
},
|
2013-01-31 11:26:17 +00:00
|
|
|
toggle_filter: function (filter, preventSearch) {
|
|
|
|
var current = this.view.query.find(function (facet) {
|
2013-01-31 13:51:25 +00:00
|
|
|
return facet.get('_id') === filter.id;
|
2013-01-31 11:26:17 +00:00
|
|
|
});
|
|
|
|
if (current) {
|
|
|
|
this.view.query.remove(current);
|
2014-05-19 15:03:05 +00:00
|
|
|
this.$filters[this.key_for(filter)].removeClass('badge');
|
2013-01-31 11:26:17 +00:00
|
|
|
return;
|
|
|
|
}
|
2012-11-12 15:48:15 +00:00
|
|
|
this.view.query.reset([this.facet_for(filter)], {
|
|
|
|
preventSearch: preventSearch || false});
|
2014-05-19 15:03:05 +00:00
|
|
|
this.$filters[this.key_for(filter)].addClass('badge');
|
2012-11-12 15:48:15 +00:00
|
|
|
},
|
2012-05-10 18:52:14 +00:00
|
|
|
set_filters: function (filters) {
|
|
|
|
_(filters).map(_.bind(this.append_filter, this));
|
2014-05-21 12:27:17 +00:00
|
|
|
if (!filters.length) {
|
|
|
|
this.hide();
|
|
|
|
}
|
|
|
|
},
|
|
|
|
hide: function () {
|
|
|
|
this.$el.hide();
|
|
|
|
},
|
|
|
|
show: function () {
|
|
|
|
this.$el.show();
|
2012-05-08 16:21:37 +00:00
|
|
|
},
|
2014-05-21 11:18:30 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
instance.web.search.SaveFilter = instance.web.search.Input.extend({
|
|
|
|
template: 'SearchView.SaveFilter',
|
|
|
|
_in_drawer: true,
|
2014-06-30 17:48:11 +00:00
|
|
|
init: function (parent, custom_filters) {
|
2014-05-21 11:18:30 +00:00
|
|
|
this._super(parent);
|
2014-06-30 17:48:11 +00:00
|
|
|
this.custom_filters = custom_filters;
|
2014-05-21 11:18:30 +00:00
|
|
|
},
|
|
|
|
start: function () {
|
|
|
|
var self = this;
|
|
|
|
this.model = new instance.web.Model('ir.filters');
|
|
|
|
this.$el.on('submit', 'form', this.proxy('save_current'));
|
|
|
|
this.$el.on('click', 'input[type=checkbox]', function() {
|
|
|
|
$(this).siblings('input[type=checkbox]').prop('checked', false);
|
|
|
|
});
|
|
|
|
this.$el.on('click', 'h4', function () {
|
|
|
|
self.$el.toggleClass('oe_opened');
|
|
|
|
});
|
|
|
|
},
|
2012-05-08 16:21:37 +00:00
|
|
|
save_current: function () {
|
|
|
|
var self = this;
|
2012-11-06 15:21:48 +00:00
|
|
|
var $name = this.$('input:first');
|
2012-11-07 11:25:16 +00:00
|
|
|
var private_filter = !this.$('#oe_searchview_custom_public').prop('checked');
|
|
|
|
var set_as_default = this.$('#oe_searchview_custom_default').prop('checked');
|
2013-10-22 16:25:19 +00:00
|
|
|
if (_.isEmpty($name.val())){
|
|
|
|
this.do_warn(_t("Error"), _t("Filter name is required."));
|
|
|
|
return false;
|
|
|
|
}
|
2012-05-08 16:21:37 +00:00
|
|
|
var search = this.view.build_search_data();
|
2012-11-23 11:39:32 +00:00
|
|
|
instance.web.pyeval.eval_domains_and_contexts({
|
2012-05-08 16:21:37 +00:00
|
|
|
domains: search.domains,
|
|
|
|
contexts: search.contexts,
|
|
|
|
group_by_seq: search.groupbys || []
|
2012-10-30 14:06:30 +00:00
|
|
|
}).done(function (results) {
|
2012-05-08 16:21:37 +00:00
|
|
|
if (!_.isEmpty(results.group_by)) {
|
|
|
|
results.context.group_by = results.group_by;
|
|
|
|
}
|
2013-03-04 10:14:14 +00:00
|
|
|
// Don't save user_context keys in the custom filter, otherwise end
|
|
|
|
// up with e.g. wrong uid or lang stored *and used in subsequent
|
|
|
|
// reqs*
|
|
|
|
var ctx = results.context;
|
|
|
|
_(_.keys(instance.session.user_context)).each(function (key) {
|
|
|
|
delete ctx[key];
|
|
|
|
});
|
2012-05-10 18:52:14 +00:00
|
|
|
var filter = {
|
|
|
|
name: $name.val(),
|
2012-08-14 15:29:00 +00:00
|
|
|
user_id: private_filter ? instance.session.uid : false,
|
2012-05-08 16:21:37 +00:00
|
|
|
model_id: self.view.model,
|
|
|
|
context: results.context,
|
2012-05-23 11:23:14 +00:00
|
|
|
domain: results.domain,
|
2014-06-30 17:48:11 +00:00
|
|
|
is_default: set_as_default,
|
|
|
|
action_id: self.custom_filters.get_action_id()
|
2012-05-10 18:52:14 +00:00
|
|
|
};
|
|
|
|
// FIXME: current context?
|
2012-10-30 14:06:30 +00:00
|
|
|
return self.model.call('create_or_replace', [filter]).done(function (id) {
|
2012-05-18 11:12:02 +00:00
|
|
|
filter.id = id;
|
2014-06-30 17:48:11 +00:00
|
|
|
if (self.custom_filters) {
|
|
|
|
self.custom_filters.append_filter(filter);
|
2014-05-21 11:18:30 +00:00
|
|
|
}
|
2012-08-24 18:27:07 +00:00
|
|
|
self.$el
|
2012-05-11 14:32:25 +00:00
|
|
|
.removeClass('oe_opened')
|
|
|
|
.find('form')[0].reset();
|
2012-05-10 18:52:14 +00:00
|
|
|
});
|
2012-05-08 16:21:37 +00:00
|
|
|
});
|
|
|
|
return false;
|
2014-05-21 11:18:30 +00:00
|
|
|
},
|
2012-05-08 16:21:37 +00:00
|
|
|
});
|
|
|
|
|
2012-04-27 15:04:36 +00:00
|
|
|
instance.web.search.Filters = instance.web.search.Input.extend({
|
|
|
|
template: 'SearchView.Filters',
|
|
|
|
_in_drawer: true,
|
|
|
|
start: function () {
|
|
|
|
var self = this;
|
|
|
|
var is_group = function (i) { return i instanceof instance.web.search.FilterGroup; };
|
2014-05-19 12:16:09 +00:00
|
|
|
var visible_filters = _(this.drawer.controls).chain().reject(function (group) {
|
2013-02-14 07:59:06 +00:00
|
|
|
return _(_(group.children).filter(is_group)).isEmpty()
|
|
|
|
|| group.modifiers.invisible;
|
|
|
|
});
|
2012-04-27 15:04:36 +00:00
|
|
|
|
2014-05-19 12:16:09 +00:00
|
|
|
var groups = visible_filters.map(function (group) {
|
2013-02-14 07:43:02 +00:00
|
|
|
var filters = _(group.children).filter(is_group);
|
2013-02-13 12:56:06 +00:00
|
|
|
return {
|
2013-02-14 07:43:02 +00:00
|
|
|
name: _.str.sprintf("<span class='oe_i'>%s</span> %s",
|
|
|
|
group.icon, group.name),
|
2013-02-13 12:56:06 +00:00
|
|
|
filters: filters,
|
|
|
|
length: _(filters).chain().map(function (i) {
|
|
|
|
return i.filters.length; }).sum().value()
|
|
|
|
};
|
|
|
|
}).value();
|
2012-04-27 15:04:36 +00:00
|
|
|
|
2014-05-19 12:16:09 +00:00
|
|
|
var $dl = $('<dl class="dl-horizontal">').appendTo(this.$el);
|
2012-04-27 15:04:36 +00:00
|
|
|
|
2014-05-19 12:16:09 +00:00
|
|
|
var rendered_lines = _.map(groups, function (group) {
|
|
|
|
$('<dt>').html(group.name).appendTo($dl);
|
|
|
|
var $dd = $('<dd>').appendTo($dl);
|
|
|
|
return $.when.apply(null, _(group.filters).invoke('appendTo', $dd));
|
|
|
|
});
|
|
|
|
|
|
|
|
return $.when.apply(this, rendered_lines);
|
|
|
|
},
|
2012-04-27 15:04:36 +00:00
|
|
|
});
|
2012-06-28 12:10:53 +00:00
|
|
|
|
2012-04-17 12:02:10 +00:00
|
|
|
instance.web.search.Advanced = instance.web.search.Input.extend({
|
2012-03-30 15:42:27 +00:00
|
|
|
template: 'SearchView.advanced',
|
2012-04-27 15:04:36 +00:00
|
|
|
_in_drawer: true,
|
2011-05-19 15:12:49 +00:00
|
|
|
start: function () {
|
|
|
|
var self = this;
|
2012-08-24 18:27:07 +00:00
|
|
|
this.$el
|
2012-03-30 15:42:27 +00:00
|
|
|
.on('keypress keydown keyup', function (e) { e.stopPropagation(); })
|
|
|
|
.on('click', 'h4', function () {
|
2012-08-24 18:27:07 +00:00
|
|
|
self.$el.toggleClass('oe_opened');
|
2012-03-30 15:42:27 +00:00
|
|
|
}).on('click', 'button.oe_add_condition', function () {
|
|
|
|
self.append_proposition();
|
2012-04-03 09:56:15 +00:00
|
|
|
}).on('submit', 'form', function (e) {
|
|
|
|
e.preventDefault();
|
2012-03-30 15:42:27 +00:00
|
|
|
self.commit_search();
|
2011-05-19 15:12:49 +00:00
|
|
|
});
|
2012-03-30 15:42:27 +00:00
|
|
|
return $.when(
|
|
|
|
this._super(),
|
2013-01-22 14:16:57 +00:00
|
|
|
new instance.web.Model(this.view.model).call('fields_get', {
|
|
|
|
context: this.view.dataset.context
|
|
|
|
}).done(function(data) {
|
2013-12-18 11:35:47 +00:00
|
|
|
self.fields = {
|
2014-08-06 15:27:27 +00:00
|
|
|
id: { string: 'ID', type: 'id', searchable: true }
|
2013-12-18 11:35:47 +00:00
|
|
|
};
|
|
|
|
_.each(data, function(field_def, field_name) {
|
|
|
|
if (field_def.selectable !== false && field_name != 'id') {
|
|
|
|
self.fields[field_name] = field_def;
|
|
|
|
}
|
|
|
|
});
|
2012-10-30 14:06:30 +00:00
|
|
|
})).done(function () {
|
2012-03-30 15:42:27 +00:00
|
|
|
self.append_proposition();
|
2011-05-19 15:12:49 +00:00
|
|
|
});
|
|
|
|
},
|
2012-03-30 15:42:27 +00:00
|
|
|
append_proposition: function () {
|
2012-11-06 16:17:15 +00:00
|
|
|
var self = this;
|
2012-04-17 12:02:10 +00:00
|
|
|
return (new instance.web.search.ExtendedSearchProposition(this, this.fields))
|
2012-11-07 09:10:47 +00:00
|
|
|
.appendTo(this.$('ul')).done(function () {
|
2012-11-06 16:17:15 +00:00
|
|
|
self.$('button.oe_apply').prop('disabled', false);
|
|
|
|
});
|
|
|
|
},
|
|
|
|
remove_proposition: function (prop) {
|
|
|
|
// removing last proposition, disable apply button
|
|
|
|
if (this.getChildren().length <= 1) {
|
|
|
|
this.$('button.oe_apply').prop('disabled', true);
|
|
|
|
}
|
|
|
|
prop.destroy();
|
2011-06-21 13:05:01 +00:00
|
|
|
},
|
2012-03-30 15:42:27 +00:00
|
|
|
commit_search: function () {
|
|
|
|
// Get domain sections from all propositions
|
2012-05-11 12:07:26 +00:00
|
|
|
var children = this.getChildren();
|
2012-05-16 13:22:37 +00:00
|
|
|
var propositions = _.invoke(children, 'get_proposition');
|
|
|
|
var domain = _(propositions).pluck('value');
|
2012-05-11 12:07:26 +00:00
|
|
|
for (var i = domain.length; --i;) {
|
|
|
|
domain.unshift('|');
|
|
|
|
}
|
|
|
|
|
2012-05-03 19:36:45 +00:00
|
|
|
this.view.query.add({
|
2012-04-06 11:51:41 +00:00
|
|
|
category: _t("Advanced"),
|
2012-05-16 13:22:37 +00:00
|
|
|
values: propositions,
|
2012-05-11 12:07:26 +00:00
|
|
|
field: {
|
|
|
|
get_context: function () { },
|
|
|
|
get_domain: function () { return domain;},
|
|
|
|
get_groupby: function () { }
|
|
|
|
}
|
2012-04-03 09:51:46 +00:00
|
|
|
});
|
2012-05-11 12:07:26 +00:00
|
|
|
|
2012-03-30 15:42:27 +00:00
|
|
|
// remove all propositions
|
2012-04-02 11:44:59 +00:00
|
|
|
_.invoke(children, 'destroy');
|
2012-03-30 15:42:27 +00:00
|
|
|
// add new empty proposition
|
|
|
|
this.append_proposition();
|
2012-04-03 09:53:03 +00:00
|
|
|
// TODO: API on searchview
|
2012-08-24 18:27:07 +00:00
|
|
|
this.view.$el.removeClass('oe_searchview_open_drawer');
|
2011-05-19 15:12:49 +00:00
|
|
|
}
|
|
|
|
});
|
|
|
|
|
2012-08-14 13:42:46 +00:00
|
|
|
instance.web.search.ExtendedSearchProposition = instance.web.Widget.extend(/** @lends instance.web.search.ExtendedSearchProposition# */{
|
2011-04-01 12:53:13 +00:00
|
|
|
template: 'SearchView.extended_search.proposition',
|
2012-11-06 14:47:36 +00:00
|
|
|
events: {
|
|
|
|
'change .searchview_extended_prop_field': 'changed',
|
2013-02-14 14:06:37 +00:00
|
|
|
'change .searchview_extended_prop_op': 'operator_changed',
|
2012-11-06 14:47:36 +00:00
|
|
|
'click .searchview_extended_delete_prop': function (e) {
|
|
|
|
e.stopPropagation();
|
2012-11-06 16:17:15 +00:00
|
|
|
this.getParent().remove_proposition(this);
|
2012-11-06 14:47:36 +00:00
|
|
|
}
|
|
|
|
},
|
2011-09-12 12:06:04 +00:00
|
|
|
/**
|
2012-04-17 12:02:10 +00:00
|
|
|
* @constructs instance.web.search.ExtendedSearchProposition
|
2012-08-14 13:42:46 +00:00
|
|
|
* @extends instance.web.Widget
|
2011-09-12 12:06:04 +00:00
|
|
|
*
|
|
|
|
* @param parent
|
|
|
|
* @param fields
|
|
|
|
*/
|
2011-04-01 12:53:13 +00:00
|
|
|
init: function (parent, fields) {
|
|
|
|
this._super(parent);
|
|
|
|
this.fields = _(fields).chain()
|
|
|
|
.map(function(val, key) { return _.extend({}, val, {'name': key}); })
|
2014-08-06 12:59:57 +00:00
|
|
|
.filter(function (field) { return !field.deprecated && field.searchable; })
|
2011-04-01 12:53:13 +00:00
|
|
|
.sortBy(function(field) {return field.string;})
|
|
|
|
.value();
|
|
|
|
this.attrs = {_: _, fields: this.fields, selected: null};
|
|
|
|
this.value = null;
|
|
|
|
},
|
|
|
|
start: function () {
|
2012-11-07 09:10:47 +00:00
|
|
|
return this._super().done(this.proxy('changed'));
|
2011-04-01 12:53:13 +00:00
|
|
|
},
|
|
|
|
changed: function() {
|
2012-11-06 15:21:48 +00:00
|
|
|
var nval = this.$(".searchview_extended_prop_field").val();
|
2013-07-25 10:07:49 +00:00
|
|
|
if(this.attrs.selected === null || this.attrs.selected === undefined || nval != this.attrs.selected.name) {
|
2011-04-01 12:53:13 +00:00
|
|
|
this.select_field(_.detect(this.fields, function(x) {return x.name == nval;}));
|
|
|
|
}
|
|
|
|
},
|
2013-02-14 14:06:37 +00:00
|
|
|
operator_changed: function (e) {
|
2013-02-14 14:25:45 +00:00
|
|
|
var $value = this.$('.searchview_extended_prop_value');
|
2013-02-14 14:06:37 +00:00
|
|
|
switch ($(e.target).val()) {
|
|
|
|
case '∃':
|
|
|
|
case '∄':
|
|
|
|
$value.hide();
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
$value.show();
|
|
|
|
}
|
|
|
|
},
|
2011-04-01 12:53:13 +00:00
|
|
|
/**
|
|
|
|
* Selects the provided field object
|
|
|
|
*
|
|
|
|
* @param field a field descriptor object (as returned by fields_get, augmented by the field name)
|
|
|
|
*/
|
|
|
|
select_field: function(field) {
|
2011-12-20 17:51:37 +00:00
|
|
|
var self = this;
|
2013-07-25 10:07:49 +00:00
|
|
|
if(this.attrs.selected !== null && this.attrs.selected !== undefined) {
|
2012-02-21 16:29:12 +00:00
|
|
|
this.value.destroy();
|
2011-04-01 12:53:13 +00:00
|
|
|
this.value = null;
|
2012-11-06 15:21:48 +00:00
|
|
|
this.$('.searchview_extended_prop_op').html('');
|
2011-04-01 12:53:13 +00:00
|
|
|
}
|
|
|
|
this.attrs.selected = field;
|
2013-07-25 10:07:49 +00:00
|
|
|
if(field === null || field === undefined) {
|
2011-04-01 12:53:13 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2011-04-12 16:27:03 +00:00
|
|
|
var type = field.type;
|
2012-05-16 12:25:51 +00:00
|
|
|
var Field = instance.web.search.custom_filters.get_object(type);
|
|
|
|
if(!Field) {
|
|
|
|
Field = instance.web.search.custom_filters.get_object("char");
|
2011-05-19 17:00:41 +00:00
|
|
|
}
|
2012-05-16 12:25:51 +00:00
|
|
|
this.value = new Field(this, field);
|
2011-04-12 16:27:03 +00:00
|
|
|
_.each(this.value.operators, function(operator) {
|
2011-12-20 17:51:37 +00:00
|
|
|
$('<option>', {value: operator.value})
|
|
|
|
.text(String(operator.text))
|
2012-11-06 15:21:48 +00:00
|
|
|
.appendTo(self.$('.searchview_extended_prop_op'));
|
2011-04-12 16:27:03 +00:00
|
|
|
});
|
2013-02-14 14:06:37 +00:00
|
|
|
var $value_loc = this.$('.searchview_extended_prop_value').show().empty();
|
2012-05-16 12:25:51 +00:00
|
|
|
this.value.appendTo($value_loc);
|
2011-12-09 12:35:14 +00:00
|
|
|
|
2011-04-01 12:53:13 +00:00
|
|
|
},
|
|
|
|
get_proposition: function() {
|
2013-07-25 10:07:49 +00:00
|
|
|
if (this.attrs.selected === null || this.attrs.selected === undefined)
|
2011-04-01 12:53:13 +00:00
|
|
|
return null;
|
2012-05-16 13:22:37 +00:00
|
|
|
var field = this.attrs.selected;
|
2013-02-14 14:06:37 +00:00
|
|
|
var op_select = this.$('.searchview_extended_prop_op')[0];
|
|
|
|
var operator = op_select.options[op_select.selectedIndex];
|
|
|
|
|
2012-05-16 13:22:37 +00:00
|
|
|
return {
|
2013-02-14 14:29:52 +00:00
|
|
|
label: this.value.get_label(field, operator),
|
|
|
|
value: this.value.get_domain(field, operator),
|
2012-05-16 13:22:37 +00:00
|
|
|
};
|
2011-04-01 12:53:13 +00:00
|
|
|
}
|
|
|
|
});
|
|
|
|
|
2012-05-16 12:25:51 +00:00
|
|
|
instance.web.search.ExtendedSearchProposition.Field = instance.web.Widget.extend({
|
|
|
|
init: function (parent, field) {
|
|
|
|
this._super(parent);
|
|
|
|
this.field = field;
|
2012-05-16 13:22:37 +00:00
|
|
|
},
|
2013-02-14 14:29:52 +00:00
|
|
|
get_label: function (field, operator) {
|
|
|
|
var format;
|
|
|
|
switch (operator.value) {
|
|
|
|
case '∃': case '∄': format = _t('%(field)s %(operator)s'); break;
|
|
|
|
default: format = _t('%(field)s %(operator)s "%(value)s"'); break;
|
|
|
|
}
|
|
|
|
return this.format_label(format, field, operator);
|
|
|
|
},
|
|
|
|
format_label: function (format, field, operator) {
|
|
|
|
return _.str.sprintf(format, {
|
|
|
|
field: field.string,
|
|
|
|
// According to spec, HTMLOptionElement#label should return
|
|
|
|
// HTMLOptionElement#text when not defined/empty, but it does
|
|
|
|
// not in older Webkit (between Safari 5.1.5 and Chrome 17) and
|
|
|
|
// Gecko (pre Firefox 7) browsers, so we need a manual fallback
|
|
|
|
// for those
|
|
|
|
operator: operator.label || operator.text,
|
|
|
|
value: this
|
|
|
|
});
|
|
|
|
},
|
|
|
|
get_domain: function (field, operator) {
|
|
|
|
switch (operator.value) {
|
|
|
|
case '∃': return this.make_domain(field.name, '!=', false);
|
|
|
|
case '∄': return this.make_domain(field.name, '=', false);
|
|
|
|
default: return this.make_domain(
|
|
|
|
field.name, operator.value, this.get_value());
|
|
|
|
}
|
|
|
|
},
|
|
|
|
make_domain: function (field, operator, value) {
|
|
|
|
return [field, operator, value];
|
|
|
|
},
|
2012-05-16 13:22:37 +00:00
|
|
|
/**
|
|
|
|
* Returns a human-readable version of the value, in case the "logical"
|
|
|
|
* and the "semantic" values of a field differ (as for selection fields,
|
|
|
|
* for instance).
|
|
|
|
*
|
|
|
|
* The default implementation simply returns the value itself.
|
|
|
|
*
|
|
|
|
* @return {String} human-readable version of the value
|
|
|
|
*/
|
|
|
|
toString: function () {
|
|
|
|
return this.get_value();
|
2011-11-14 09:57:38 +00:00
|
|
|
}
|
|
|
|
});
|
2012-04-17 12:02:10 +00:00
|
|
|
instance.web.search.ExtendedSearchProposition.Char = instance.web.search.ExtendedSearchProposition.Field.extend({
|
2011-04-01 12:53:13 +00:00
|
|
|
template: 'SearchView.extended_search.proposition.char',
|
|
|
|
operators: [
|
2011-12-20 17:51:37 +00:00
|
|
|
{value: "ilike", text: _lt("contains")},
|
|
|
|
{value: "not ilike", text: _lt("doesn't contain")},
|
|
|
|
{value: "=", text: _lt("is equal to")},
|
2013-02-14 14:06:37 +00:00
|
|
|
{value: "!=", text: _lt("is not equal to")},
|
|
|
|
{value: "∃", text: _lt("is set")},
|
|
|
|
{value: "∄", text: _lt("is not set")}
|
2011-04-01 12:53:13 +00:00
|
|
|
],
|
|
|
|
get_value: function() {
|
2012-08-24 18:27:07 +00:00
|
|
|
return this.$el.val();
|
2011-04-01 12:53:13 +00:00
|
|
|
}
|
|
|
|
});
|
2012-04-17 12:02:10 +00:00
|
|
|
instance.web.search.ExtendedSearchProposition.DateTime = instance.web.search.ExtendedSearchProposition.Field.extend({
|
2011-09-21 12:03:47 +00:00
|
|
|
template: 'SearchView.extended_search.proposition.empty',
|
2011-04-01 12:53:13 +00:00
|
|
|
operators: [
|
2011-12-20 17:51:37 +00:00
|
|
|
{value: "=", text: _lt("is equal to")},
|
|
|
|
{value: "!=", text: _lt("is not equal to")},
|
|
|
|
{value: ">", text: _lt("greater than")},
|
|
|
|
{value: "<", text: _lt("less than")},
|
|
|
|
{value: ">=", text: _lt("greater or equal than")},
|
2013-02-14 14:06:37 +00:00
|
|
|
{value: "<=", text: _lt("less or equal than")},
|
|
|
|
{value: "∃", text: _lt("is set")},
|
|
|
|
{value: "∄", text: _lt("is not set")}
|
2011-04-01 12:53:13 +00:00
|
|
|
],
|
2012-05-16 12:25:51 +00:00
|
|
|
/**
|
|
|
|
* Date widgets live in view_form which is not yet loaded when this is
|
|
|
|
* initialized -_-
|
|
|
|
*/
|
|
|
|
widget: function () { return instance.web.DateTimeWidget; },
|
2011-04-01 12:53:13 +00:00
|
|
|
get_value: function() {
|
2011-09-21 12:03:47 +00:00
|
|
|
return this.datewidget.get_value();
|
2011-05-19 15:12:49 +00:00
|
|
|
},
|
2012-11-21 13:56:57 +00:00
|
|
|
toString: function () {
|
|
|
|
return instance.web.format_value(this.get_value(), { type:"datetime" });
|
|
|
|
},
|
2011-05-19 15:53:02 +00:00
|
|
|
start: function() {
|
2012-05-16 12:25:51 +00:00
|
|
|
var ready = this._super();
|
|
|
|
this.datewidget = new (this.widget())(this);
|
2012-08-24 18:27:07 +00:00
|
|
|
this.datewidget.appendTo(this.$el);
|
2012-05-16 12:25:51 +00:00
|
|
|
return ready;
|
2011-04-01 12:53:13 +00:00
|
|
|
}
|
|
|
|
});
|
2012-05-16 12:25:51 +00:00
|
|
|
instance.web.search.ExtendedSearchProposition.Date = instance.web.search.ExtendedSearchProposition.DateTime.extend({
|
2012-11-21 13:56:57 +00:00
|
|
|
widget: function () { return instance.web.DateWidget; },
|
|
|
|
toString: function () {
|
|
|
|
return instance.web.format_value(this.get_value(), { type:"date" });
|
|
|
|
}
|
2011-05-19 15:53:02 +00:00
|
|
|
});
|
2012-04-17 12:02:10 +00:00
|
|
|
instance.web.search.ExtendedSearchProposition.Integer = instance.web.search.ExtendedSearchProposition.Field.extend({
|
2011-05-19 16:18:35 +00:00
|
|
|
template: 'SearchView.extended_search.proposition.integer',
|
|
|
|
operators: [
|
2011-12-20 17:51:37 +00:00
|
|
|
{value: "=", text: _lt("is equal to")},
|
|
|
|
{value: "!=", text: _lt("is not equal to")},
|
|
|
|
{value: ">", text: _lt("greater than")},
|
|
|
|
{value: "<", text: _lt("less than")},
|
|
|
|
{value: ">=", text: _lt("greater or equal than")},
|
2013-02-14 14:06:37 +00:00
|
|
|
{value: "<=", text: _lt("less or equal than")},
|
|
|
|
{value: "∃", text: _lt("is set")},
|
|
|
|
{value: "∄", text: _lt("is not set")}
|
2011-05-19 16:18:35 +00:00
|
|
|
],
|
2012-05-16 13:22:37 +00:00
|
|
|
toString: function () {
|
2012-08-24 18:27:07 +00:00
|
|
|
return this.$el.val();
|
2012-05-16 13:22:37 +00:00
|
|
|
},
|
2011-05-19 16:18:35 +00:00
|
|
|
get_value: function() {
|
2011-09-19 13:20:26 +00:00
|
|
|
try {
|
2012-11-23 06:45:25 +00:00
|
|
|
var val =this.$el.val();
|
2013-07-25 10:07:49 +00:00
|
|
|
return instance.web.parse_value(val === "" ? 0 : val, {'widget': 'integer'});
|
2011-09-19 13:20:26 +00:00
|
|
|
} catch (e) {
|
2011-05-19 16:18:35 +00:00
|
|
|
return "";
|
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
2012-04-17 12:02:10 +00:00
|
|
|
instance.web.search.ExtendedSearchProposition.Id = instance.web.search.ExtendedSearchProposition.Integer.extend({
|
2011-12-20 17:51:37 +00:00
|
|
|
operators: [{value: "=", text: _lt("is")}]
|
2011-11-14 09:57:38 +00:00
|
|
|
});
|
2012-04-17 12:02:10 +00:00
|
|
|
instance.web.search.ExtendedSearchProposition.Float = instance.web.search.ExtendedSearchProposition.Field.extend({
|
2011-05-19 16:18:35 +00:00
|
|
|
template: 'SearchView.extended_search.proposition.float',
|
|
|
|
operators: [
|
2011-12-20 17:51:37 +00:00
|
|
|
{value: "=", text: _lt("is equal to")},
|
|
|
|
{value: "!=", text: _lt("is not equal to")},
|
|
|
|
{value: ">", text: _lt("greater than")},
|
|
|
|
{value: "<", text: _lt("less than")},
|
|
|
|
{value: ">=", text: _lt("greater or equal than")},
|
2013-02-14 14:06:37 +00:00
|
|
|
{value: "<=", text: _lt("less or equal than")},
|
|
|
|
{value: "∃", text: _lt("is set")},
|
|
|
|
{value: "∄", text: _lt("is not set")}
|
2011-05-19 16:18:35 +00:00
|
|
|
],
|
2012-05-16 13:22:37 +00:00
|
|
|
toString: function () {
|
2012-08-24 18:27:07 +00:00
|
|
|
return this.$el.val();
|
2012-05-16 13:22:37 +00:00
|
|
|
},
|
2011-05-19 16:18:35 +00:00
|
|
|
get_value: function() {
|
2011-09-19 13:20:26 +00:00
|
|
|
try {
|
2012-11-23 06:45:25 +00:00
|
|
|
var val =this.$el.val();
|
2013-07-25 10:07:49 +00:00
|
|
|
return instance.web.parse_value(val === "" ? 0.0 : val, {'widget': 'float'});
|
2011-09-19 13:20:26 +00:00
|
|
|
} catch (e) {
|
2011-05-19 16:18:35 +00:00
|
|
|
return "";
|
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
2012-04-17 12:02:10 +00:00
|
|
|
instance.web.search.ExtendedSearchProposition.Selection = instance.web.search.ExtendedSearchProposition.Field.extend({
|
2011-05-19 17:00:41 +00:00
|
|
|
template: 'SearchView.extended_search.proposition.selection',
|
|
|
|
operators: [
|
2011-12-20 17:51:37 +00:00
|
|
|
{value: "=", text: _lt("is")},
|
2013-02-14 14:06:37 +00:00
|
|
|
{value: "!=", text: _lt("is not")},
|
|
|
|
{value: "∃", text: _lt("is set")},
|
|
|
|
{value: "∄", text: _lt("is not set")}
|
2011-05-19 17:00:41 +00:00
|
|
|
],
|
2012-05-16 13:22:37 +00:00
|
|
|
toString: function () {
|
2012-08-24 18:27:07 +00:00
|
|
|
var select = this.$el[0];
|
2012-05-16 13:22:37 +00:00
|
|
|
var option = select.options[select.selectedIndex];
|
|
|
|
return option.label || option.text;
|
|
|
|
},
|
2011-05-19 17:00:41 +00:00
|
|
|
get_value: function() {
|
2012-08-24 18:27:07 +00:00
|
|
|
return this.$el.val();
|
2011-05-19 17:00:41 +00:00
|
|
|
}
|
|
|
|
});
|
2012-04-17 12:02:10 +00:00
|
|
|
instance.web.search.ExtendedSearchProposition.Boolean = instance.web.search.ExtendedSearchProposition.Field.extend({
|
2012-05-22 06:46:07 +00:00
|
|
|
template: 'SearchView.extended_search.proposition.empty',
|
2011-05-19 17:00:41 +00:00
|
|
|
operators: [
|
2011-12-20 17:51:37 +00:00
|
|
|
{value: "=", text: _lt("is true")},
|
|
|
|
{value: "!=", text: _lt("is false")}
|
2011-05-19 17:00:41 +00:00
|
|
|
],
|
2013-02-14 14:29:52 +00:00
|
|
|
get_label: function (field, operator) {
|
|
|
|
return this.format_label(
|
|
|
|
_t('%(field)s %(operator)s'), field, operator);
|
|
|
|
},
|
2011-05-19 17:00:41 +00:00
|
|
|
get_value: function() {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
});
|
2011-05-19 15:53:02 +00:00
|
|
|
|
2012-04-17 12:02:10 +00:00
|
|
|
instance.web.search.custom_filters = new instance.web.Registry({
|
|
|
|
'char': 'instance.web.search.ExtendedSearchProposition.Char',
|
|
|
|
'text': 'instance.web.search.ExtendedSearchProposition.Char',
|
|
|
|
'one2many': 'instance.web.search.ExtendedSearchProposition.Char',
|
|
|
|
'many2one': 'instance.web.search.ExtendedSearchProposition.Char',
|
|
|
|
'many2many': 'instance.web.search.ExtendedSearchProposition.Char',
|
2011-12-09 12:35:14 +00:00
|
|
|
|
2012-04-17 12:02:10 +00:00
|
|
|
'datetime': 'instance.web.search.ExtendedSearchProposition.DateTime',
|
|
|
|
'date': 'instance.web.search.ExtendedSearchProposition.Date',
|
|
|
|
'integer': 'instance.web.search.ExtendedSearchProposition.Integer',
|
|
|
|
'float': 'instance.web.search.ExtendedSearchProposition.Float',
|
|
|
|
'boolean': 'instance.web.search.ExtendedSearchProposition.Boolean',
|
|
|
|
'selection': 'instance.web.search.ExtendedSearchProposition.Selection',
|
2011-11-14 09:57:38 +00:00
|
|
|
|
2012-04-17 12:02:10 +00:00
|
|
|
'id': 'instance.web.search.ExtendedSearchProposition.Id'
|
2011-05-19 15:53:02 +00:00
|
|
|
});
|
2011-04-01 12:53:13 +00:00
|
|
|
|
2014-06-25 11:26:41 +00:00
|
|
|
instance.web.search.AutoComplete = instance.web.Widget.extend({
|
|
|
|
template: "SearchView.autocomplete",
|
|
|
|
|
|
|
|
// Parameters for autocomplete constructor:
|
|
|
|
//
|
|
|
|
// parent: this is used to detect keyboard events
|
|
|
|
//
|
|
|
|
// options.source: function ({term:query}, callback). This function will be called to
|
|
|
|
// obtain the search results corresponding to the query string. It is assumed that
|
|
|
|
// options.source will call callback with the results.
|
|
|
|
// options.select: function (ev, {item: {facet:facet}}). Autocomplete widget will call
|
|
|
|
// that function when a selection is made by the user
|
|
|
|
// options.get_search_string: function (). This function will be called by autocomplete
|
|
|
|
// to obtain the current search string.
|
|
|
|
init: function (parent, options) {
|
|
|
|
this._super(parent);
|
|
|
|
this.$input = parent.$el;
|
|
|
|
this.source = options.source;
|
2015-03-11 16:42:54 +00:00
|
|
|
this.select = options.select;
|
2014-06-25 11:26:41 +00:00
|
|
|
this.get_search_string = options.get_search_string;
|
|
|
|
this.width = options.width || 400;
|
|
|
|
|
|
|
|
this.current_result = null;
|
|
|
|
|
|
|
|
this.searching = true;
|
2015-03-11 16:42:54 +00:00
|
|
|
this.search_string = '';
|
2014-06-25 11:26:41 +00:00
|
|
|
this.current_search = null;
|
|
|
|
},
|
|
|
|
start: function () {
|
|
|
|
var self = this;
|
|
|
|
this.$el.width(this.width);
|
|
|
|
this.$input.on('keyup', function (ev) {
|
|
|
|
if (ev.which === $.ui.keyCode.RIGHT) {
|
|
|
|
self.searching = true;
|
|
|
|
ev.preventDefault();
|
|
|
|
return;
|
|
|
|
}
|
2014-11-20 09:26:13 +00:00
|
|
|
if (ev.which === $.ui.keyCode.ENTER) {
|
2015-03-11 16:42:54 +00:00
|
|
|
if (self.search_string.length) {
|
2014-11-20 09:26:13 +00:00
|
|
|
self.select_item(ev);
|
|
|
|
}
|
|
|
|
return;
|
|
|
|
}
|
[FIX] web: fix issue with search view input
This patch hopefully solves an annoying issue with the search view:
when the user types really fast (or uses a barcode scanner for example),
the resulting search string was (sometimes) missing the last few
characters. I'm unclear on the exact reason why it happens. From the
code, I can only guess that it happens because the scanner use a TAB
key instead of ENTER (TAB is handled in keydown event, ENTER in keyup),
or that the key events aren't in a correct order (if the user press a
key A, then press ENTER, then release ENTER, then release A, it results
in an empty key).
Anyway, the way this patch probably solves the issue is by using the
keypress event for triggering the search view. I hope that in every
case, the keypress event are correctly ordered. It leads to some code
I'm not really proud of this patch, but the key event handling are
quite messy. Also, I need to handle the search string also when the
keyup event is fired, because it might change the search (for example
backspace trigger a keyup, but not a keypress).
2015-03-10 10:27:15 +00:00
|
|
|
var search_string = self.get_search_string();
|
|
|
|
if (self.search_string !== search_string) {
|
|
|
|
if (search_string.length) {
|
|
|
|
self.search_string = search_string;
|
2015-03-11 16:42:54 +00:00
|
|
|
self.initiate_search(search_string);
|
[FIX] web: fix issue with search view input
This patch hopefully solves an annoying issue with the search view:
when the user types really fast (or uses a barcode scanner for example),
the resulting search string was (sometimes) missing the last few
characters. I'm unclear on the exact reason why it happens. From the
code, I can only guess that it happens because the scanner use a TAB
key instead of ENTER (TAB is handled in keydown event, ENTER in keyup),
or that the key events aren't in a correct order (if the user press a
key A, then press ENTER, then release ENTER, then release A, it results
in an empty key).
Anyway, the way this patch probably solves the issue is by using the
keypress event for triggering the search view. I hope that in every
case, the keypress event are correctly ordered. It leads to some code
I'm not really proud of this patch, but the key event handling are
quite messy. Also, I need to handle the search string also when the
keyup event is fired, because it might change the search (for example
backspace trigger a keyup, but not a keypress).
2015-03-10 10:27:15 +00:00
|
|
|
} else {
|
|
|
|
self.close();
|
|
|
|
}
|
2014-06-25 11:26:41 +00:00
|
|
|
}
|
[FIX] web: fix issue with search view input
This patch hopefully solves an annoying issue with the search view:
when the user types really fast (or uses a barcode scanner for example),
the resulting search string was (sometimes) missing the last few
characters. I'm unclear on the exact reason why it happens. From the
code, I can only guess that it happens because the scanner use a TAB
key instead of ENTER (TAB is handled in keydown event, ENTER in keyup),
or that the key events aren't in a correct order (if the user press a
key A, then press ENTER, then release ENTER, then release A, it results
in an empty key).
Anyway, the way this patch probably solves the issue is by using the
keypress event for triggering the search view. I hope that in every
case, the keypress event are correctly ordered. It leads to some code
I'm not really proud of this patch, but the key event handling are
quite messy. Also, I need to handle the search string also when the
keyup event is fired, because it might change the search (for example
backspace trigger a keyup, but not a keypress).
2015-03-10 10:27:15 +00:00
|
|
|
});
|
|
|
|
this.$input.on('keypress', function (ev) {
|
2015-03-11 16:42:54 +00:00
|
|
|
self.search_string = self.search_string + String.fromCharCode(ev.which);
|
2014-06-25 11:26:41 +00:00
|
|
|
if (self.search_string.length) {
|
[FIX] web: fix issue with search view input
This patch hopefully solves an annoying issue with the search view:
when the user types really fast (or uses a barcode scanner for example),
the resulting search string was (sometimes) missing the last few
characters. I'm unclear on the exact reason why it happens. From the
code, I can only guess that it happens because the scanner use a TAB
key instead of ENTER (TAB is handled in keydown event, ENTER in keyup),
or that the key events aren't in a correct order (if the user press a
key A, then press ENTER, then release ENTER, then release A, it results
in an empty key).
Anyway, the way this patch probably solves the issue is by using the
keypress event for triggering the search view. I hope that in every
case, the keypress event are correctly ordered. It leads to some code
I'm not really proud of this patch, but the key event handling are
quite messy. Also, I need to handle the search string also when the
keyup event is fired, because it might change the search (for example
backspace trigger a keyup, but not a keypress).
2015-03-10 10:27:15 +00:00
|
|
|
self.searching = true;
|
2014-06-25 11:26:41 +00:00
|
|
|
var search_string = self.search_string;
|
2015-03-11 16:42:54 +00:00
|
|
|
self.initiate_search(search_string);
|
2014-06-25 11:26:41 +00:00
|
|
|
} else {
|
|
|
|
self.close();
|
|
|
|
}
|
|
|
|
});
|
|
|
|
this.$input.on('keydown', function (ev) {
|
|
|
|
switch (ev.which) {
|
2015-03-11 16:42:54 +00:00
|
|
|
case $.ui.keyCode.ENTER:
|
|
|
|
|
2014-11-24 17:45:22 +00:00
|
|
|
// TAB and direction keys are handled at KeyDown because KeyUp
|
|
|
|
// is not guaranteed to fire.
|
|
|
|
// See e.g. https://github.com/aef-/jquery.masterblaster/issues/13
|
2014-06-25 11:26:41 +00:00
|
|
|
case $.ui.keyCode.TAB:
|
2015-03-11 16:42:54 +00:00
|
|
|
if (self.search_string.length) {
|
2014-06-25 11:26:41 +00:00
|
|
|
self.select_item(ev);
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
case $.ui.keyCode.DOWN:
|
|
|
|
self.move('down');
|
|
|
|
self.searching = false;
|
|
|
|
ev.preventDefault();
|
|
|
|
break;
|
|
|
|
case $.ui.keyCode.UP:
|
|
|
|
self.move('up');
|
|
|
|
self.searching = false;
|
|
|
|
ev.preventDefault();
|
|
|
|
break;
|
|
|
|
case $.ui.keyCode.RIGHT:
|
|
|
|
self.searching = false;
|
|
|
|
var current = self.current_result
|
|
|
|
if (current && current.expand && !current.expanded) {
|
|
|
|
self.expand();
|
|
|
|
self.searching = true;
|
|
|
|
}
|
|
|
|
ev.preventDefault();
|
|
|
|
break;
|
|
|
|
case $.ui.keyCode.ESCAPE:
|
|
|
|
self.close();
|
|
|
|
self.searching = false;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
});
|
|
|
|
},
|
|
|
|
initiate_search: function (query) {
|
|
|
|
if (query === this.search_string && query !== this.current_search) {
|
|
|
|
this.search(query);
|
|
|
|
}
|
|
|
|
},
|
|
|
|
search: function (query) {
|
|
|
|
var self = this;
|
|
|
|
this.current_search = query;
|
|
|
|
this.source({term:query}, function (results) {
|
|
|
|
if (results.length) {
|
|
|
|
self.render_search_results(results);
|
|
|
|
self.focus_element(self.$('li:first-child'));
|
|
|
|
} else {
|
|
|
|
self.close();
|
|
|
|
}
|
|
|
|
});
|
|
|
|
},
|
|
|
|
render_search_results: function (results) {
|
|
|
|
var self = this;
|
|
|
|
var $list = this.$('ul');
|
|
|
|
$list.empty();
|
2014-06-27 10:05:34 +00:00
|
|
|
var render_separator = false;
|
2014-06-25 11:26:41 +00:00
|
|
|
results.forEach(function (result) {
|
2014-06-27 10:05:34 +00:00
|
|
|
if (result.is_separator) {
|
|
|
|
if (render_separator)
|
|
|
|
$list.append($('<li>').addClass('oe-separator'));
|
|
|
|
render_separator = false;
|
|
|
|
} else {
|
|
|
|
var $item = self.make_list_item(result).appendTo($list);
|
|
|
|
result.$el = $item;
|
|
|
|
render_separator = true;
|
|
|
|
}
|
2014-06-25 11:26:41 +00:00
|
|
|
});
|
|
|
|
this.show();
|
|
|
|
},
|
|
|
|
make_list_item: function (result) {
|
|
|
|
var self = this;
|
|
|
|
var $li = $('<li>')
|
|
|
|
.hover(function (ev) {self.focus_element($li);})
|
|
|
|
.mousedown(function (ev) {
|
|
|
|
if (ev.button === 0) { // left button
|
|
|
|
self.select(ev, {item: {facet: result.facet}});
|
|
|
|
self.close();
|
|
|
|
} else {
|
|
|
|
ev.preventDefault();
|
|
|
|
}
|
|
|
|
})
|
|
|
|
.data('result', result);
|
|
|
|
if (result.expand) {
|
|
|
|
var $expand = $('<span class="oe-expand">').text('▶').appendTo($li);
|
|
|
|
$expand.mousedown(function (ev) {
|
|
|
|
ev.preventDefault();
|
|
|
|
ev.stopPropagation();
|
|
|
|
if (result.expanded)
|
|
|
|
self.fold();
|
|
|
|
else
|
|
|
|
self.expand();
|
|
|
|
});
|
|
|
|
result.expanded = false;
|
|
|
|
}
|
|
|
|
if (result.indent) $li.addClass('oe-indent');
|
|
|
|
$li.append($('<span>').html(result.label));
|
|
|
|
return $li;
|
|
|
|
},
|
|
|
|
expand: function () {
|
|
|
|
var self = this;
|
2015-04-25 16:05:09 +00:00
|
|
|
var current_result = this.current_result;
|
|
|
|
current_result.expand(this.get_search_string()).then(function (results) {
|
2014-06-27 09:40:37 +00:00
|
|
|
(results || [{label: '(no result)'}]).reverse().forEach(function (result) {
|
2014-06-25 11:26:41 +00:00
|
|
|
result.indent = true;
|
|
|
|
var $li = self.make_list_item(result);
|
2015-04-25 16:05:09 +00:00
|
|
|
current_result.$el.after($li);
|
2014-06-25 11:26:41 +00:00
|
|
|
});
|
2015-04-25 16:05:09 +00:00
|
|
|
current_result.expanded = true;
|
|
|
|
current_result.$el.find('span.oe-expand').html('▼');
|
2014-06-25 11:26:41 +00:00
|
|
|
});
|
|
|
|
},
|
|
|
|
fold: function () {
|
|
|
|
var $next = this.current_result.$el.next();
|
|
|
|
while ($next.hasClass('oe-indent')) {
|
|
|
|
$next.remove();
|
|
|
|
$next = this.current_result.$el.next();
|
|
|
|
}
|
|
|
|
this.current_result.expanded = false;
|
|
|
|
this.current_result.$el.find('span.oe-expand').html('▶');
|
|
|
|
},
|
|
|
|
focus_element: function ($li) {
|
|
|
|
this.$('li').removeClass('oe-selection-focus');
|
|
|
|
$li.addClass('oe-selection-focus');
|
|
|
|
this.current_result = $li.data('result');
|
|
|
|
},
|
|
|
|
select_item: function (ev) {
|
|
|
|
if (this.current_result.facet) {
|
|
|
|
this.select(ev, {item: {facet: this.current_result.facet}});
|
|
|
|
this.close();
|
|
|
|
}
|
|
|
|
},
|
|
|
|
show: function () {
|
|
|
|
this.$el.show();
|
|
|
|
},
|
|
|
|
close: function () {
|
|
|
|
this.current_search = null;
|
2015-03-11 16:42:54 +00:00
|
|
|
this.search_string = '';
|
2014-06-25 11:26:41 +00:00
|
|
|
this.searching = true;
|
|
|
|
this.$el.hide();
|
|
|
|
},
|
|
|
|
move: function (direction) {
|
|
|
|
var $next;
|
|
|
|
if (direction === 'down') {
|
2014-06-27 11:57:33 +00:00
|
|
|
$next = this.$('li.oe-selection-focus').nextAll(':not(.oe-separator)').first();
|
2014-06-25 11:26:41 +00:00
|
|
|
if (!$next.length) $next = this.$('li:first-child');
|
|
|
|
} else {
|
2014-06-27 11:57:33 +00:00
|
|
|
$next = this.$('li.oe-selection-focus').prevAll(':not(.oe-separator)').first();
|
2014-07-01 07:39:26 +00:00
|
|
|
if (!$next.length) $next = this.$('li:not(.oe-separator)').last();
|
2014-06-25 11:26:41 +00:00
|
|
|
}
|
|
|
|
this.focus_element($next);
|
|
|
|
},
|
|
|
|
is_expandable: function () {
|
|
|
|
return !!this.$('.oe-selection-focus .oe-expand').length;
|
|
|
|
},
|
|
|
|
});
|
|
|
|
|
2013-08-06 12:50:22 +00:00
|
|
|
})();
|
2011-03-02 23:34:42 +00:00
|
|
|
|
|
|
|
// vim:et fdc=0 fdl=0 foldnestmax=3 fdm=syntax:
|