[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.inputs = [];
this.enabled_filters = [];
this.has_focus = false;
@ -97,6 +96,7 @@ openerp.web.SearchView = openerp.web.Widget.extend(/** @lends openerp.web.Search
this.ready = $.Deferred();
},
start: function() {
var self = this;
var p = this._super();
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_input: this.proxy('make_visualsearch_input'),
search: function (query, searchCollection) {
console.log(query, searchCollection);
self.do_search();
},
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
*/
setup_global_completion: function () {
var self = this;
this.$element.autocomplete({
source: this.proxy('complete_global_search'),
select: this.proxy('select_completion'),
@ -157,23 +156,21 @@ openerp.web.SearchView = openerp.web.Widget.extend(/** @lends openerp.web.Search
if ('value' in item) {
// regular completion item
$item.append(
return $item.append(
('label' in item)
? $('<a>').html(item.label)
: $('<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
*/
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()
.invoke('complete', req.term)
.value()).then(function () {
var results = completion.concat.apply(
completion, _(arguments).compact());
resp(results);
resp(_(_(arguments).compact()).flatten(true));
});
},
@ -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
*/
do_search: function (e) {
console.log(this.vs.searchBox.value());
console.log(this.vs.searchQuery.facets());
return this.on_search([], [], []);
var self = this;
var domains = [], contexts = [], groupbys = [], errors = [];
if (this.headless && !this.has_defaults) {
return this.on_search([], [], []);
}
// 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) {
this.vs.searchQuery.each(function (facet) {
var field = facet.get('field');
try {
var domain = input.get_domain();
var domain = field.get_domain(facet);
if (domain) {
domains.push(domain);
}
var context = input.get_context();
var context = field.get_context(facet);
if (context) {
contexts.push(context);
groupbys.push(context);
}
} catch (e) {
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?
var groupbys = _(this.enabled_filters)
.chain()
.map(function (filter) { return filter.get_context();})
.compact()
.value();
return {domains: domains, contexts: contexts, errors: errors, groupbys: groupbys};
if (!_.isEmpty(errors)) {
this.on_invalid(errors);
return;
}
return this.on_search(domains, contexts, groupbys);
},
/**
* 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,
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.
*
* @param {VS.model.SearchFacet} facet
* @return {*} combined domains of the enabled filters in this group
*/
get_domain: function () {
var domains = _(this.filters).chain()
.filter(function (filter) { return filter.is_enabled(); })
get_domain: function (facet) {
var domains = _(facet.get('json')).chain()
.map(function (filter) { return filter.attrs.domain; })
.reject(_.isEmpty)
.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
* 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
* @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);
},
facet_for: function () { return $.when(null); },
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_context: function () { },
get_domain: function () { }
});
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.filters.start();
},
get_context: function () {
var val = this.get_value();
get_value: function (facet) {
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
// active
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
* @returns {Array<Array>} domain to include in the resulting search
*/
make_domain: function (name, operator, value) {
return [[name, operator, value]];
make_domain: function (name, operator, facet) {
return [[name, operator, facet.value()]];
},
get_domain: function () {
var val = this.get_value();
get_domain: function (facet) {
var val = this.get_value(facet);
if (val === null || val === '') {
return;
}
@ -1006,7 +983,7 @@ openerp.web.search.Field = openerp.web.search.Input.extend( /** @lends openerp.w
return this.make_domain(
this.attrs.name,
this.attrs.operator || this.default_operator,
val);
facet);
}
return _.extend({}, domain, {own_values: {self: val}});
}
@ -1034,9 +1011,6 @@ openerp.web.search.CharField = openerp.web.search.Field.extend( /** @lends opene
value: value,
field: this
}]);
},
get_value: function () {
return this.$element.val();
}
});
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();
if (_.isEmpty(results)) { return $.when(null); }
return $.when.apply(null, [{
category: this.attrs.name
category: this.attrs.string
}].concat(results));
},
facet_for: function (value) {
@ -1140,12 +1114,8 @@ openerp.web.search.SelectionField = openerp.web.search.Field.extend(/** @lends o
app: this.view.app
}));
},
get_value: function () {
var index = parseInt(this.$element.val(), 10);
if (isNaN(index)) { return null; }
var value = this.attrs.selection[index][0];
if (value === false) { return null; }
return value;
get_value: function (facet) {
return facet.get('json');
},
clear: function () {
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(); });
return started.promise();
},
make_domain: function (name, operator, value) {
if (this.id && this.name) {
if (value === this.name) {
return [[name, '=', this.id]];
} else {
delete this.id;
delete this.name;
}
make_domain: function (name, operator, facet) {
// ``json`` -> actual auto-completed id
if (facet.get('json')) {
return [[name, '=', facet.get('json')]];
}
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``
the *name* of the facet, displayed in the first section of a facet
view. The category *may* be ``null``.
view.
``value``
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:
``field``
the search field instance which created the facet, optional. May or
may not be inferrable from ``category``.
the search field instance which created the facet, used when the
search view needs to serialize the facets.
``json``
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
field).
This value may be a complex javascript object such as an array or an
object (the name stands for json-compatible value, it is not
JSON-encoded).
This value may be a complex javascript object such as an array (the
name stands for json-compatible value, it is not a JSON-encoded
string).
.. note::
@ -120,10 +120,10 @@ Loading Defaults
----------------
After loading the view data, the SearchView will call
:js:func:`openerp.web.search.Input.facet_for_defaults` with the
``defaults`` mapping of key:values (where each key corresponds to an
input). This method should look into the ``defaults`` mapping and
fetch the field's default value as a
:js:func:`openerp.web.search.Input.facet_for_defaults` on each of its
inputs with the ``defaults`` mapping of key:values (where each key
corresponds to an input). This method should look into the
``defaults`` mapping and fetch the field's default value as a
:js:class:`~VS.models.SearchFacet` if applicable.
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
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
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,
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
++++++++++++++
A second option is to use section titles. Section titles are similar
to completion items but only have a ``category`` property. They will
be rendered in a different style and can not be selected in the
auto-completion (they will be skipped).
A second kind of completion values is the section titles. Section
titles are similar to completion items but only have a ``category``
property. They will be rendered in a different style and can not be
selected in the auto-completion (they will be skipped).
.. note::
@ -192,8 +192,30 @@ auto-completion (they will be skipped).
If an input *may* fetch more than one completion item, it *should*
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
-------
@ -204,8 +226,8 @@ The displaying of the search view was significantly altered from
OpenERP Web 6.1 to OpenERP Web 6.2.
As a result, while the external API used to interact with the search
view does not change the internal details — including the interaction
between the search view and its widgets — is significantly altered:
view does not change many internal details — including the interaction
between the search view and its widgets — were significantly altered:
Widgets API
+++++++++++
@ -215,11 +237,33 @@ Widgets API
* Search field objects are not openerp widgets anymore, their
``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
+++++++
* :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
+++++++++++
@ -232,5 +276,8 @@ Many To One
Search view's implementation module. Changes to the
VisualSearch code should only update the library to new
revisions or releases.
.. [#] search view fields may also bundle context data to add to the
search context
.. _commit 3fca87101d:
https://github.com/documentcloud/visualsearch/commit/3fca87101d