[ADD] searching to faceted field

* Removed idea of "text" facet category (~null category) working on
  all text-kinded fields at the same time, fp thinks this should be
  done by defining search view fields with filter_domain

- Adding a facet to the facets collection does not trigger a search,
  nor does editing the value of an (editable) facet

bzr revid: xmo@openerp.com-20120321154550-0ubjon97vfnj3dxf
This commit is contained in:
Xavier Morel 2012-03-21 16:45:50 +01:00
parent c3b181d7f3
commit 60c07691f1
2 changed files with 139 additions and 125 deletions

View File

@ -87,7 +87,6 @@ openerp.web.SearchView = openerp.web.Widget.extend(/** @lends openerp.web.Search
this.has_defaults = !_.isEmpty(this.defaults); this.has_defaults = !_.isEmpty(this.defaults);
this.inputs = []; this.inputs = [];
this.enabled_filters = [];
this.has_focus = false; this.has_focus = false;
@ -97,6 +96,7 @@ openerp.web.SearchView = openerp.web.Widget.extend(/** @lends openerp.web.Search
this.ready = $.Deferred(); this.ready = $.Deferred();
}, },
start: function() { start: function() {
var self = this;
var p = this._super(); var p = this._super();
this.setup_global_completion(); this.setup_global_completion();
@ -107,7 +107,7 @@ openerp.web.SearchView = openerp.web.Widget.extend(/** @lends openerp.web.Search
make_facet: this.proxy('make_visualsearch_facet'), make_facet: this.proxy('make_visualsearch_facet'),
make_input: this.proxy('make_visualsearch_input'), make_input: this.proxy('make_visualsearch_input'),
search: function (query, searchCollection) { search: function (query, searchCollection) {
console.log(query, searchCollection); self.do_search();
}, },
facetMatches: function (callback) { facetMatches: function (callback) {
}, },
@ -141,7 +141,6 @@ openerp.web.SearchView = openerp.web.Widget.extend(/** @lends openerp.web.Search
* Sets up search view's view-wide auto-completion widget * Sets up search view's view-wide auto-completion widget
*/ */
setup_global_completion: function () { setup_global_completion: function () {
var self = this;
this.$element.autocomplete({ this.$element.autocomplete({
source: this.proxy('complete_global_search'), source: this.proxy('complete_global_search'),
select: this.proxy('select_completion'), select: this.proxy('select_completion'),
@ -157,23 +156,21 @@ openerp.web.SearchView = openerp.web.Widget.extend(/** @lends openerp.web.Search
if ('value' in item) { if ('value' in item) {
// regular completion item // regular completion item
$item.append( return $item.append(
('label' in item) ('label' in item)
? $('<a>').html(item.label) ? $('<a>').html(item.label)
: $('<a>').text(item.value)); : $('<a>').text(item.value));
} else {
$item.text(item.category)
.css({
borderTop: '1px solid #cccccc',
margin: 0,
padding: 0,
zoom: 1,
'float': 'left',
clear: 'left',
width: '100%'
});
} }
return $item; return $item.text(item.category)
.css({
borderTop: '1px solid #cccccc',
margin: 0,
padding: 0,
zoom: 1,
'float': 'left',
clear: 'left',
width: '100%'
});
} }
}, },
/** /**
@ -184,21 +181,10 @@ openerp.web.SearchView = openerp.web.Widget.extend(/** @lends openerp.web.Search
* @param {Function} resp response callback * @param {Function} resp response callback
*/ */
complete_global_search: function (req, resp) { complete_global_search: function (req, resp) {
var completion = [{
category: null,
value: req.term,
label: _.str.sprintf(_.str.escapeHTML(
_t("Search for: %(value)s")), {
value: '<strong>' + _.str.escapeHTML(req.term) + '</strong>'
}),
field: null
}];
$.when.apply(null, _(this.inputs).chain() $.when.apply(null, _(this.inputs).chain()
.invoke('complete', req.term) .invoke('complete', req.term)
.value()).then(function () { .value()).then(function () {
var results = completion.concat.apply( resp(_(_(arguments).compact()).flatten(true));
completion, _(arguments).compact());
resp(results);
}); });
}, },
@ -525,43 +511,20 @@ openerp.web.SearchView = openerp.web.Widget.extend(/** @lends openerp.web.Search
* @param e jQuery event object coming from the "Search" button * @param e jQuery event object coming from the "Search" button
*/ */
do_search: function (e) { do_search: function (e) {
console.log(this.vs.searchBox.value()); var self = this;
console.log(this.vs.searchQuery.facets()); var domains = [], contexts = [], groupbys = [], errors = [];
return this.on_search([], [], []);
if (this.headless && !this.has_defaults) { this.vs.searchQuery.each(function (facet) {
return this.on_search([], [], []); var field = facet.get('field');
}
// reset filters management
var select = this.$element.find(".oe_search-view-filters-management");
select.val("_filters");
if (e && e.preventDefault) { e.preventDefault(); }
var data = this.build_search_data();
if (data.errors.length) {
this.on_invalid(data.errors);
return;
}
this.on_search(data.domains, data.contexts, data.groupbys);
},
build_search_data: function() {
var domains = [],
contexts = [],
errors = [];
_.each(this.inputs, function (input) {
try { try {
var domain = input.get_domain(); var domain = field.get_domain(facet);
if (domain) { if (domain) {
domains.push(domain); domains.push(domain);
} }
var context = field.get_context(facet);
var context = input.get_context();
if (context) { if (context) {
contexts.push(context); contexts.push(context);
groupbys.push(context);
} }
} catch (e) { } catch (e) {
if (e instanceof openerp.web.search.Invalid) { if (e instanceof openerp.web.search.Invalid) {
@ -572,13 +535,11 @@ openerp.web.SearchView = openerp.web.Widget.extend(/** @lends openerp.web.Search
} }
}); });
// TODO: do we need to handle *fields* with group_by in their context? if (!_.isEmpty(errors)) {
var groupbys = _(this.enabled_filters) this.on_invalid(errors);
.chain() return;
.map(function (filter) { return filter.get_context();}) }
.compact() return this.on_search(domains, contexts, groupbys);
.value();
return {domains: domains, contexts: contexts, errors: errors, groupbys: groupbys};
}, },
/** /**
* Triggered after the SearchView has collected all relevant domains and * Triggered after the SearchView has collected all relevant domains and
@ -876,15 +837,33 @@ openerp.web.search.FilterGroup = openerp.web.search.Input.extend(/** @lends open
field: this, field: this,
app: this.view.vs app: this.view.vs
})); }));
}, },
get_context: function () { }, /**
* Fetches contexts for all enabled filters in the group
*
* @param {VS.model.SearchFacet} facet
* @return {*} combined contexts of the enabled filters in this group
*/
get_context: function (facet) {
var contexts = _(facet.get('json')).chain()
.map(function (filter) { return filter.attrs.context; })
.reject(_.isEmpty)
.value();
if (!contexts.length) { return; }
if (contexts.length === 1) { return contexts[0]; }
return _.extend(new openerp.web.CompoundContext, {
__contexts: contexts
});
},
/** /**
* Handles domains-fetching for all the filters within it: groups them. * Handles domains-fetching for all the filters within it: groups them.
*
* @param {VS.model.SearchFacet} facet
* @return {*} combined domains of the enabled filters in this group
*/ */
get_domain: function () { get_domain: function (facet) {
var domains = _(this.filters).chain() var domains = _(facet.get('json')).chain()
.filter(function (filter) { return filter.is_enabled(); })
.map(function (filter) { return filter.attrs.domain; }) .map(function (filter) { return filter.attrs.domain; })
.reject(_.isEmpty) .reject(_.isEmpty)
.value(); .value();
@ -905,6 +884,10 @@ openerp.web.search.Filter = openerp.web.search.Input.extend(/** @lends openerp.w
* Implementation of the OpenERP filters (button with a context and/or * Implementation of the OpenERP filters (button with a context and/or
* a domain sent as-is to the search view) * a domain sent as-is to the search view)
* *
* Filters are only attributes holder, the actual work (compositing
* domains and contexts, converting between facets and filters) is
* performed by the filter group.
*
* @constructs openerp.web.search.Filter * @constructs openerp.web.search.Filter
* @extends openerp.web.search.Input * @extends openerp.web.search.Input
* *
@ -916,16 +899,7 @@ openerp.web.search.Filter = openerp.web.search.Input.extend(/** @lends openerp.w
this.load_attrs(node.attrs); this.load_attrs(node.attrs);
}, },
facet_for: function () { return $.when(null); }, facet_for: function () { return $.when(null); },
get_context: function () { get_context: function () { },
if (!this.is_enabled()) {
return;
}
return this.attrs.context;
},
/**
* Does not return anything: filter domain is handled at the FilterGroup
* level
*/
get_domain: function () { } get_domain: function () { }
}); });
openerp.web.search.Field = openerp.web.search.Input.extend( /** @lends openerp.web.search.Field# */ { openerp.web.search.Field = openerp.web.search.Input.extend( /** @lends openerp.web.search.Field# */ {
@ -968,8 +942,11 @@ openerp.web.search.Field = openerp.web.search.Input.extend( /** @lends openerp.w
this._super(); this._super();
this.filters.start(); this.filters.start();
}, },
get_context: function () { get_value: function (facet) {
var val = this.get_value(); return facet.value();
},
get_context: function (facet) {
var val = this.get_value(facet);
// A field needs a value to be "active", and a context to send when // A field needs a value to be "active", and a context to send when
// active // active
var has_value = (val !== null && val !== ''); var has_value = (val !== null && val !== '');
@ -992,11 +969,11 @@ openerp.web.search.Field = openerp.web.search.Input.extend( /** @lends openerp.w
* @param {Number|String} value parsed value for the field * @param {Number|String} value parsed value for the field
* @returns {Array<Array>} domain to include in the resulting search * @returns {Array<Array>} domain to include in the resulting search
*/ */
make_domain: function (name, operator, value) { make_domain: function (name, operator, facet) {
return [[name, operator, value]]; return [[name, operator, facet.value()]];
}, },
get_domain: function () { get_domain: function (facet) {
var val = this.get_value(); var val = this.get_value(facet);
if (val === null || val === '') { if (val === null || val === '') {
return; return;
} }
@ -1006,7 +983,7 @@ openerp.web.search.Field = openerp.web.search.Input.extend( /** @lends openerp.w
return this.make_domain( return this.make_domain(
this.attrs.name, this.attrs.name,
this.attrs.operator || this.default_operator, this.attrs.operator || this.default_operator,
val); facet);
} }
return _.extend({}, domain, {own_values: {self: val}}); return _.extend({}, domain, {own_values: {self: val}});
} }
@ -1034,9 +1011,6 @@ openerp.web.search.CharField = openerp.web.search.Field.extend( /** @lends opene
value: value, value: value,
field: this field: this
}]); }]);
},
get_value: function () {
return this.$element.val();
} }
}); });
openerp.web.search.NumberField = openerp.web.search.Field.extend(/** @lends openerp.web.search.NumberField# */{ openerp.web.search.NumberField = openerp.web.search.Field.extend(/** @lends openerp.web.search.NumberField# */{
@ -1124,7 +1098,7 @@ openerp.web.search.SelectionField = openerp.web.search.Field.extend(/** @lends o
}).value(); }).value();
if (_.isEmpty(results)) { return $.when(null); } if (_.isEmpty(results)) { return $.when(null); }
return $.when.apply(null, [{ return $.when.apply(null, [{
category: this.attrs.name category: this.attrs.string
}].concat(results)); }].concat(results));
}, },
facet_for: function (value) { facet_for: function (value) {
@ -1140,12 +1114,8 @@ openerp.web.search.SelectionField = openerp.web.search.Field.extend(/** @lends o
app: this.view.app app: this.view.app
})); }));
}, },
get_value: function () { get_value: function (facet) {
var index = parseInt(this.$element.val(), 10); return facet.get('json');
if (isNaN(index)) { return null; }
var value = this.attrs.selection[index][0];
if (value === false) { return null; }
return value;
}, },
clear: function () { clear: function () {
var self = this, d = $.Deferred(), selection = this.attrs.selection; var self = this, d = $.Deferred(), selection = this.attrs.selection;
@ -1286,16 +1256,13 @@ openerp.web.search.ManyToOneField = openerp.web.search.CharField.extend({
function () { started.resolve(); }); function () { started.resolve(); });
return started.promise(); return started.promise();
}, },
make_domain: function (name, operator, value) { make_domain: function (name, operator, facet) {
if (this.id && this.name) { // ``json`` -> actual auto-completed id
if (value === this.name) { if (facet.get('json')) {
return [[name, '=', this.id]]; return [[name, '=', facet.get('json')]];
} else {
delete this.id;
delete this.name;
}
} }
return this._super(name, operator, value);
return this._super(name, operator, facet);
} }
}); });

View File

@ -50,7 +50,7 @@ set on all search facets created programmatically:
``category`` ``category``
the *name* of the facet, displayed in the first section of a facet the *name* of the facet, displayed in the first section of a facet
view. The category *may* be ``null``. view.
``value`` ``value``
the *displayed value* of the facet, it is directly printed to the the *displayed value* of the facet, it is directly printed to the
@ -60,17 +60,17 @@ The search view uses additional keys to store state and data it needs
to associate with facet objects: to associate with facet objects:
``field`` ``field``
the search field instance which created the facet, optional. May or the search field instance which created the facet, used when the
may not be inferrable from ``category``. search view needs to serialize the facets.
``json`` ``json``
the "logical" value of the facet, can be absent if the logical and the "logical" value of the facet, can be absent if the logical and
"printable" values of the facet are the same (e.g. for a basic text "printable" values of the facet are the same (e.g. for a basic text
field). field).
This value may be a complex javascript object such as an array or an This value may be a complex javascript object such as an array (the
object (the name stands for json-compatible value, it is not name stands for json-compatible value, it is not a JSON-encoded
JSON-encoded). string).
.. note:: .. note::
@ -120,10 +120,10 @@ Loading Defaults
---------------- ----------------
After loading the view data, the SearchView will call After loading the view data, the SearchView will call
:js:func:`openerp.web.search.Input.facet_for_defaults` with the :js:func:`openerp.web.search.Input.facet_for_defaults` on each of its
``defaults`` mapping of key:values (where each key corresponds to an inputs with the ``defaults`` mapping of key:values (where each key
input). This method should look into the ``defaults`` mapping and corresponds to an input). This method should look into the
fetch the field's default value as a ``defaults`` mapping and fetch the field's default value as a
:js:class:`~VS.models.SearchFacet` if applicable. :js:class:`~VS.models.SearchFacet` if applicable.
The default implementation is to check if there is a default value for The default implementation is to check if there is a default value for
@ -168,7 +168,7 @@ facet (as described above) and can have one more optional property:
When rendering an item in the list, the renderer will first try to use When rendering an item in the list, the renderer will first try to use
the ``label`` property if it exists (``label`` can contain HTML and the ``label`` property if it exists (``label`` can contain HTML and
will be inserted as-is, so it can bold or emphasize some of its will be inserted as-is, so it can bold or emphasize some of its
elements), if it does not it'll use the ``value`` property. elements), if it does not the ``value`` property will be used.
.. note:: the ``app`` key should not be specified on completion item, .. note:: the ``app`` key should not be specified on completion item,
it will be set automatically when the search view creates it will be set automatically when the search view creates
@ -177,10 +177,10 @@ elements), if it does not it'll use the ``value`` property.
Section titles Section titles
++++++++++++++ ++++++++++++++
A second option is to use section titles. Section titles are similar A second kind of completion values is the section titles. Section
to completion items but only have a ``category`` property. They will titles are similar to completion items but only have a ``category``
be rendered in a different style and can not be selected in the property. They will be rendered in a different style and can not be
auto-completion (they will be skipped). selected in the auto-completion (they will be skipped).
.. note:: .. note::
@ -192,8 +192,30 @@ auto-completion (they will be skipped).
If an input *may* fetch more than one completion item, it *should* If an input *may* fetch more than one completion item, it *should*
prepend a section title (using its own name) to the completion items. prepend a section title (using its own name) to the completion items.
Converting to and from facet objects Converting from facet objects
------------------------------------ -----------------------------
Ultimately, the point of the search view is to allow searching. In
OpenERP this is done via :ref:`domains <openerpserver:domains>`. On
the other hand, the OpenERP Web 6.2 search view's state is modelled
after a collection of :js:class:`~VS.model.SearchFacet`, and each
field of a search view may have special requirements when it comes to
the domains it produces [#]_.
So there needs to be some way of mapping
:js:class:`~VS.model.SearchFacet` objects to OpenERP search data.
This is done via an input's
:js:func:`~openerp.web.search.Input.get_domain` and
:js:func:`~openerp.web.search.Input.get_context`. Each takes a
:js:class:`~VS.model.SearchFacet` and returns whatever it's supposed
to generate (a domain or a context, respectively). Either can return
``null`` if the current value does not map to a domain or context, and
can throw an :js:class:`~openerp.web.search.Invalid` exception if the
value is not valid at all for the field.
Converting to facet objects
---------------------------
Changes Changes
------- -------
@ -204,8 +226,8 @@ The displaying of the search view was significantly altered from
OpenERP Web 6.1 to OpenERP Web 6.2. OpenERP Web 6.1 to OpenERP Web 6.2.
As a result, while the external API used to interact with the search As a result, while the external API used to interact with the search
view does not change the internal details — including the interaction view does not change many internal details — including the interaction
between the search view and its widgets — is significantly altered: between the search view and its widgets — were significantly altered:
Widgets API Widgets API
+++++++++++ +++++++++++
@ -215,11 +237,33 @@ Widgets API
* Search field objects are not openerp widgets anymore, their * Search field objects are not openerp widgets anymore, their
``start`` is not generally called ``start`` is not generally called
* :js:func:`~openerp.web.search.Input.get_domain` and
:js:func:`~openerp.web.search.Input.get_context` now take a
:js:class:`~VS.model.SearchFacet` as parameter, from which it's
their job to get whatever value they want
Filters Filters
+++++++ +++++++
* :js:func:`openerp.web.search.Filter.is_enabled` has been removed * :js:func:`openerp.web.search.Filter.is_enabled` has been removed
Fields
++++++
* ``get_value`` now takes a :js:class:`~VS.model.SearchFacet` (instead
of taking no argument).
A default implementation is provided as
:js:func:`openerp.web.search.Field.get_value` and simply calls
:js:func:`VS.model.SearchFacet.value`.
* The third argument to
:js:func:`~openerp.web.search.Field.make_domain` is now the
:js:class:`~VS.model.SearchFacet` received by
:js:func:`~openerp.web.search.Field.get_domain`, so child classes
have all the information they need to derive the "right" resulting
domain.
Many To One Many To One
+++++++++++ +++++++++++
@ -232,5 +276,8 @@ Many To One
Search view's implementation module. Changes to the Search view's implementation module. Changes to the
VisualSearch code should only update the library to new VisualSearch code should only update the library to new
revisions or releases. revisions or releases.
.. [#] search view fields may also bundle context data to add to the
search context
.. _commit 3fca87101d: .. _commit 3fca87101d:
https://github.com/documentcloud/visualsearch/commit/3fca87101d https://github.com/documentcloud/visualsearch/commit/3fca87101d