var QWeb = openerp.web.qweb,
_t = openerp.web._t,
_lt = openerp.web._lt;
+_.mixin({
+ sum: function (obj) { return _.reduce(obj, function (a, b) { return a + b; }, 0); }
+});
+
+// Have SearchBox optionally use callback function to produce inputs and facets
+// (views) set on callbacks.make_facet and callbacks.make_input keys when
+// initializing VisualSearch
+var SearchBox_renderFacet = function (facet, position) {
+ var view = new (this.app.options.callbacks['make_facet'] || VS.ui.SearchFacet)({
+ app : this.app,
+ model : facet,
+ order : position
+ });
+
+ // Input first, facet second.
+ this.renderSearchInput();
+ this.facetViews.push(view);
+ this.$('.VS-search-inner').children().eq(position*2).after(view.render().el);
+
+ view.calculateSize();
+ _.defer(_.bind(view.calculateSize, view));
-openerp.web.SearchView = openerp.web.OldWidget.extend(/** @lends openerp.web.SearchView# */{
- template: "EmptyComponent",
+ return view;
+ }; // warning: will not match
+// Ensure we're replacing the function we think
+if (SearchBox_renderFacet.toString() !== VS.ui.SearchBox.prototype.renderFacet.toString().replace(/(VS\.ui\.SearchFacet)/, "(this.app.options.callbacks['make_facet'] || $1)")) {
+ throw new Error(
+ "Trying to replace wrong version of VS.ui.SearchBox#renderFacet. "
+ + "Please fix replacement.");
+}
+var SearchBox_renderSearchInput = function () {
+ var input = new (this.app.options.callbacks['make_input'] || VS.ui.SearchInput)({position: this.inputViews.length, app: this.app});
+ this.$('.VS-search-inner').append(input.render().el);
+ this.inputViews.push(input);
+ };
+// Ensure we're replacing the function we think
+if (SearchBox_renderSearchInput.toString() !== VS.ui.SearchBox.prototype.renderSearchInput.toString().replace(/(VS\.ui\.SearchInput)/, "(this.app.options.callbacks['make_input'] || $1)")) {
+ throw new Error(
+ "Trying to replace wrong version of VS.ui.SearchBox#renderSearchInput. "
+ + "Please fix replacement.");
+}
+var SearchBox_searchEvent = function (e) {
+ var query = null;
+ this.renderFacets();
+ this.focusSearch(e);
+ this.app.options.callbacks.search(query, this.app.searchQuery);
+ };
+if (SearchBox_searchEvent.toString() !== VS.ui.SearchBox.prototype.searchEvent.toString().replace(
+ /this\.value\(\);\n[ ]{4}this\.focusSearch\(e\);\n[ ]{4}this\.value\(query\)/,
+ 'null;\n this.renderFacets();\n this.focusSearch(e)')) {
+ throw new Error(
+ "Trying to replace wrong version of VS.ui.SearchBox#searchEvent. "
+ + "Please fix replacement.");
+}
+_.extend(VS.ui.SearchBox.prototype, {
+ renderFacet: SearchBox_renderFacet,
+ renderSearchInput: SearchBox_renderSearchInput,
+ searchEvent: SearchBox_searchEvent
+});
+_.extend(VS.model.SearchFacet.prototype, {
+ value: function () {
+ if (this.has('json')) {
+ return this.get('json');
+ }
+ return this.get('value');
+ }
+});
+
+openerp.web.SearchView = openerp.web.Widget.extend(/** @lends openerp.web.SearchView# */{
+ template: "SearchView",
/**
* @constructs openerp.web.SearchView
* @extends openerp.web.OldWidget
this.has_defaults = !_.isEmpty(this.defaults);
this.inputs = [];
- this.enabled_filters = [];
-
- this.has_focus = false;
+ this.controls = {};
this.hidden = !!hidden;
this.headless = this.hidden && !this.has_defaults;
this.ready = $.Deferred();
},
start: function() {
- this._super();
+ var self = this;
+ var p = this._super();
+
+ this.setup_global_completion();
+ this.vs = VS.init({
+ container: this.$element,
+ query: '',
+ callbacks: {
+ make_facet: this.proxy('make_visualsearch_facet'),
+ make_input: this.proxy('make_visualsearch_input'),
+ search: function (query, searchCollection) {
+ self.do_search();
+ },
+ facetMatches: function (callback) {
+ },
+ valueMatches : function(facet, searchTerm, callback) {
+ }
+ }
+ });
+
+ var search = function () { self.vs.searchBox.searchEvent({}); };
+ // searchQuery operations
+ this.vs.searchQuery
+ .off('add').on('add', search)
+ .off('change').on('change', search)
+ .off('reset').on('reset', search)
+ .off('remove').on('remove', function (record, collection, options) {
+ if (options['trigger_search']) {
+ search();
+ }
+ });
+
if (this.hidden) {
this.$element.hide();
}
if (this.headless) {
this.ready.resolve();
} else {
- this.rpc("/web/searchview/load", {
+ var load_view = this.rpc("/web/searchview/load", {
model: this.model,
view_id: this.view_id,
- context: this.dataset.get_context()
- }, this.on_loaded);
+ context: this.dataset.get_context() });
+ // FIXME: local eval of domain and context to get rid of special endpoint
+ var filters = this.rpc('/web/searchview/get_filters', {
+ model: this.model
+ }).then(function (filters) { self.custom_filters = filters; });
+
+ $.when(load_view, filters)
+ .pipe(function (load) { return load[0]; })
+ .then(this.on_loaded);
}
- return this.ready.promise();
+
+ this.$element.on('click', '.oe_vs_unfold_drawer', function () {
+ self.$element.toggleClass('oe_searchview_open_drawer');
+ });
+
+ return $.when(p, this.ready);
},
show: function () {
this.$element.show();
hide: function () {
this.$element.hide();
},
+
+ /**
+ * Sets up thingie where all the mess is put?
+ */
+ setup_stuff_drawer: function () {
+ var self = this;
+ $('<div class="oe_vs_unfold_drawer">').appendTo(this.$element.find('.VS-search-box'));
+ var $drawer = $('<div class="oe_searchview_drawer">').appendTo(this.$element);
+
+ var running_count = 0;
+ // get total filters count
+ var is_group = function (i) { return i instanceof openerp.web.search.FilterGroup; };
+ var filters_count = _(this.controls).chain()
+ .flatten()
+ .filter(is_group)
+ .map(function (i) { return i.filters.length; })
+ .sum()
+ .value();
+
+ var col1 = [], col2 = _(this.controls).map(function (inputs, group) {
+ var filters = _(inputs).filter(is_group);
+ return {
+ name: group === 'null' ? _t("Filters") : group,
+ filters: filters,
+ length: _(filters).chain().map(function (i) {
+ return i.filters.length; }).sum().value()
+ };
+ });
+
+ while (col2.length) {
+ // col1 + group should be smaller than col2 + group
+ if ((running_count + col2[0].length) <= (filters_count - running_count)) {
+ running_count += col2[0].length;
+ col1.push(col2.shift());
+ } else {
+ break;
+ }
+ }
+
+ // Create a Custom Filter FilterGroup for each custom filter read from
+ // the db, add all of this as a group in the smallest column
+ [].push.call(col1.length <= col2.length ? col1 : col2, {
+ name: _t("Custom Filters"),
+ filters: _.map(this.custom_filters, function (filter) {
+ // FIXME: handling of ``disabled`` being set
+ var f = new openerp.web.search.Filter({attrs: {
+ string: filter.name,
+ context: filter.context,
+ domain: filter.domain
+ }}, self);
+ return new openerp.web.search.FilterGroup([f], self);
+ }),
+ length: 3
+ });
+
+ return $.when(
+ this.render_column(col1, $('<div>').appendTo($drawer)),
+ this.render_column(col2, $('<div>').appendTo($drawer)));
+ },
+ render_column: function (column, $el) {
+ return $.when.apply(null, _(column).map(function (group) {
+ $('<h3>').text(group.name).appendTo($el);
+ return $.when.apply(null,
+ _(group.filters).invoke('appendTo', $el));
+ }));
+ },
+ /**
+ * Sets up search view's view-wide auto-completion widget
+ */
+ setup_global_completion: function () {
+ // Prevent keydown from within a facet's input from reaching the
+ // auto-completion widget and opening the completion list
+ this.$element.on('keydown', '.search_facet input', function (e) {
+ e.stopImmediatePropagation();
+ });
+
+ this.$element.autocomplete({
+ source: this.proxy('complete_global_search'),
+ select: this.proxy('select_completion'),
+ focus: function (e) { e.preventDefault(); },
+ html: true,
+ minLength: 0,
+ delay: 0
+ }).data('autocomplete')._renderItem = function (ul, item) {
+ // item of completion list
+ var $item = $( "<li></li>" )
+ .data( "item.autocomplete", item )
+ .appendTo( ul );
+
+ if (item.value !== undefined) {
+ // regular completion item
+ return $item.append(
+ (item.label)
+ ? $('<a>').html(item.label)
+ : $('<a>').text(item.value));
+ }
+ return $item.text(item.category)
+ .css({
+ borderTop: '1px solid #cccccc',
+ margin: 0,
+ padding: 0,
+ zoom: 1,
+ 'float': 'left',
+ clear: 'left',
+ width: '100%'
+ });
+ }
+ },
+ /**
+ * 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) {
+ $.when.apply(null, _(this.inputs).chain()
+ .invoke('complete', req.term)
+ .value()).then(function () {
+ resp(_(_(arguments).compact()).flatten(true));
+ });
+ },
+
+ /**
+ * 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();
+ this.vs.searchQuery.add(new VS.model.SearchFacet(_.extend(
+ {app: this.vs}, ui.item)));
+ this.vs.searchBox.searchEvent({});
+ },
+
+ /**
+ * Builds the right SearchFacet view based on the facet object to render
+ * (e.g. readonly facets for filters)
+ *
+ * @param {Object} options
+ * @param {VS.model.SearchFacet} options.model facet object to render
+ */
+ make_visualsearch_facet: function (options) {
+ if (options.model.get('field') instanceof openerp.web.search.FilterGroup) {
+ return new openerp.web.search.FilterGroupFacet(options);
+ }
+ return new VS.ui.SearchFacet(options);
+ },
+ /**
+ * Proxies searches on a SearchInput to the search view's global completion
+ *
+ * Also disables SearchInput.autocomplete#_move so search view's
+ * autocomplete can get the corresponding events, or something.
+ *
+ * @param options
+ */
+ make_visualsearch_input: function (options) {
+ var self = this, input = new VS.ui.SearchInput(options);
+ input.setupAutocomplete = function () {
+ _.extend(this.box.autocomplete({
+ minLength: 1,
+ delay: 0,
+ search: function () {
+ self.$element.autocomplete('search', input.box.val());
+ return false;
+ }
+ }).data('autocomplete'), {
+ _move: function () {},
+ close: function () { self.$element.autocomplete('close'); }
+ });
+ };
+ return input;
+ },
+
/**
* 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
- * @returns Array
+ * @param {String} [group_name] name of the group to put the new controls in
*/
- make_widgets: function (items, fields) {
- var rows = [],
- row = [];
- rows.push(row);
+ make_widgets: function (items, fields, group_name) {
+ group_name = group_name || null;
+ if (!(group_name in this.controls)) {
+ this.controls[group_name] = [];
+ }
+ var self = this, group = this.controls[group_name];
var filters = [];
_.each(items, function (item) {
if (filters.length && item.tag !== 'filter') {
- row.push(
- new openerp.web.search.FilterGroup(
- filters, this));
+ group.push(new openerp.web.search.FilterGroup(filters, this));
filters = [];
}
- if (item.tag === 'newline') {
- row = [];
- rows.push(row);
- } else if (item.tag === 'filter') {
- if (!this.has_focus) {
- item.attrs.default_focus = '1';
- this.has_focus = true;
- }
- filters.push(
- new openerp.web.search.Filter(
- item, this));
- } else if (item.tag === 'separator') {
- // a separator is a no-op
- } else {
- if (item.tag === 'group') {
- // TODO: group and field should be fetched from registries, maybe even filters
- row.push(
- new openerp.web.search.Group(
- item, this, fields));
- } else if (item.tag === 'field') {
- if (!this.has_focus) {
- item.attrs.default_focus = '1';
- this.has_focus = true;
- }
- row.push(
- this.make_field(
- item, fields[item['attrs'].name]));
- }
+ switch (item.tag) {
+ case 'separator': case 'newline':
+ break;
+ case 'filter':
+ filters.push(new openerp.web.search.Filter(item, this));
+ break;
+ case 'group':
+ self.make_widgets(item.children, fields, item.attrs.string);
+ break;
+ case 'field':
+ group.push(this.make_field(item, fields[item['attrs'].name]));
+ // filters
+ self.make_widgets(item.children, fields, group_name);
+ break;
}
}, this);
+
if (filters.length) {
- row.push(new openerp.web.search.FilterGroup(filters, this));
+ group.push(new openerp.web.search.FilterGroup(filters, this));
}
-
- return rows;
},
/**
* Creates a field for the provided field descriptor item (which comes
}
},
on_loaded: function(data) {
+ var self = this;
this.fields_view = data.fields_view;
if (data.fields_view.type !== 'search' ||
data.fields_view.arch.tag !== 'search') {
"Got non-search view after asking for a search view: type %s, arch root %s",
data.fields_view.type, data.fields_view.arch.tag));
}
- var self = this,
- lines = this.make_widgets(
- data.fields_view['arch'].children,
- data.fields_view.fields);
-
- // for extended search view
- var ext = new openerp.web.search.ExtendedSearch(this, this.model);
- lines.push([ext]);
- this.extended_search = ext;
-
- var render = QWeb.render("SearchView", {
- 'view': data.fields_view['arch'],
- 'lines': lines,
- 'defaults': this.defaults
- });
- this.$element.html(render);
+ this.make_widgets(
+ data.fields_view['arch'].children,
+ data.fields_view.fields);
- var f = this.$element.find('form');
- this.$element.find('form')
- .submit(this.do_search)
- .bind('reset', this.do_clear);
- // start() all the widgets
- var widget_starts = _(lines).chain().flatten().map(function (widget) {
- return widget.start();
- }).value();
-
- $.when.apply(null, widget_starts).then(function () {
- self.ready.resolve();
- });
-
- this.reload_managed_filters();
- },
- reload_managed_filters: function() {
- var self = this;
- return this.rpc('/web/searchview/get_filters', {
- model: this.dataset.model
- }).then(function(result) {
- self.managed_filters = result;
- var filters = self.$element.find(".oe_search-view-filters-management");
- filters.html(QWeb.render("SearchView.managed-filters", {
- filters: result,
- disabled_filter_message: _t('Filter disabled due to invalid syntax')
- }));
- filters.change(self.on_filters_management);
- });
+ // load defaults
+ return $.when(
+ this.setup_stuff_drawer(),
+ $.when.apply(null, _(this.inputs).invoke('facet_for_defaults', this.defaults))
+ .then(function () { self.vs.searchQuery.reset(_(arguments).compact()); }))
+ .then(function () { self.ready.resolve(); })
},
/**
* Handle event when the user make a selection in the filters management select box.
*
* @param e jQuery event object coming from the "Search" button
*/
- do_search: function (e) {
- if (this.headless && !this.has_defaults) {
- return this.on_search([], [], []);
- }
+ do_search: function () {
+ var domains = [], contexts = [], groupbys = [], errors = [];
- 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) {
}
});
- // 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();
-
- if (this.filter_data.contexts) {
- contexts = this.filter_data.contexts.concat(contexts)
- }
- if (this.filter_data.domains) {
- domains = this.filter_data.domains.concat(domains);
- }
- if (this.filter_data.groupbys) {
- groupbys = this.filter_data.groupbys.concat(groupbys);
+ if (!_.isEmpty(errors)) {
+ this.on_invalid(errors);
+ return;
}
- return {domains: domains, contexts: contexts, errors: errors, groupbys: groupbys};
+ return this.on_search(domains, contexts, groupbys);
},
/**
* Triggered after the SearchView has collected all relevant domains and
*/
on_invalid: function (errors) {
this.do_notify(_t("Invalid Search"), _t("triggered from search view"));
- },
- /**
- * @param {Boolean} [reload_view=true]
- */
- do_clear: function (reload_view) {
- this.filter_data = {};
- this.$element.find(".oe_search-view-filters-management").val('');
-
- this.$element.find('.filter_label, .filter_icon').removeClass('enabled');
- this.enabled_filters.splice(0);
- var string = $('a.searchview_group_string');
- _.each(string, function(str){
- $(str).closest('div.searchview_group').removeClass("expanded").addClass('folded');
- });
- this.$element.find('table:last').hide();
-
- $('.searchview_extended_groups_list').empty();
- return $.async_when.apply(
- null, _(this.inputs).invoke('clear')).pipe(
- reload_view !== false ? this.on_clear : null);
- },
- /**
- * Triggered when the search view gets cleared
- *
- * @event
- */
- on_clear: function () {
- this.do_search();
- },
- /**
- * Called by a filter propagating its state changes
- *
- * @param {openerp.web.search.Filter} filter a filter which got toggled
- * @param {Boolean} default_enabled filter got enabled through the default values, at render time.
- */
- do_toggle_filter: function (filter, default_enabled) {
- if (default_enabled || filter.is_enabled()) {
- this.enabled_filters.push(filter);
- } else {
- this.enabled_filters = _.without(
- this.enabled_filters, filter);
- }
-
- if (!default_enabled) {
- // selecting a filter after initial loading automatically
- // triggers refresh
- this.$element.find('form').submit();
- }
}
});
/** @namespace */
openerp.web.search = {};
+
+openerp.web.search.FilterGroupFacet = VS.ui.SearchFacet.extend({
+ events: _.extend({
+ 'click': 'selectFacet'
+ }, VS.ui.SearchFacet.prototype.events),
+
+ render: function () {
+ this.setMode('not', 'editing');
+ this.setMode('not', 'selected');
+
+ var value = this.model.get('value');
+ this.$el.html(QWeb.render('SearchView.filters.facet', {
+ facet: this.model
+ }));
+ // virtual input so SearchFacet code has something to play with
+ this.box = $('<input>').val(value);
+
+ return this;
+ }
+});
/**
* Registry of search fields, called by :js:class:`openerp.web.SearchView` to
* find and instantiate its field widgets.
init: function (view) {
this._super(view);
this.view = view;
- },
- /**
- * Sets and returns a globally unique identifier for the widget.
- *
- * If a prefix is specified, the identifier will be appended to it.
- *
- * @params prefix prefix sections, empty/falsy sections will be removed
- */
- make_id: function () {
- this.element_id = _.uniqueId(
- ['search'].concat(
- _.compact(_.toArray(arguments)),
- ['']).join('_'));
- return this.element_id;
- },
- /**
- * "Starts" the widgets. Called at the end of the rendering, this allows
- * widgets to hook themselves to their view sections.
- *
- * On widgets, if they kept a reference to a view and have an element_id,
- * will fetch and set their root element on $element.
- */
- start: function () {
- this._super();
- if (this.view && this.element_id) {
- // id is unique, and no getElementById on elements
- this.$element = $(document.getElementById(
- this.element_id));
- }
- },
- /**
- * "Stops" the widgets. Called when the view destroys itself, this
- * lets the widgets clean up after themselves.
- */
- destroy: function () {
- delete this.view;
- this._super();
- },
- render: function (defaults) {
- // FIXME
- return this._super(_.extend(this, {defaults: defaults}));
}
});
openerp.web.search.add_expand_listener = function($root) {
this.attrs = view_section.attrs;
this.lines = view.make_widgets(
view_section.children, fields);
- this.make_id('group');
- },
- start: function () {
- this._super();
- openerp.web.search.add_expand_listener(this.$element);
- var widget_starts = _(this.lines).chain().flatten()
- .map(function (widget) { return widget.start(); })
- .value();
- return $.when.apply(null, widget_starts);
}
});
this.view.inputs.push(this);
this.style = undefined;
},
+ /**
+ * 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
+ * @returns {jQuery.Deferred<null|Array>}
+ */
+ complete: function (value) {
+ return $.when(null)
+ },
+ /**
+ * Returns a VS.model.SearchFacet instance for the provided defaults if
+ * they apply to this widget, or null if they don't.
+ *
+ * This default implementation will try calling
+ * :js:func:`openerp.web.search.Input#facet_for` if the widget's name
+ * 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]);
+ },
get_context: function () {
throw new Error(
"get_context not implemented for widget " + this.attrs.type);
}
}
this.attrs = attrs;
- },
- /**
- * Specific clearing operations, if any
- */
- clear: function () {}
+ }
});
openerp.web.search.FilterGroup = openerp.web.search.Input.extend(/** @lends openerp.web.search.FilterGroup# */{
template: 'SearchView.filters',
init: function (filters, view) {
this._super(view);
this.filters = filters;
- this.length = filters.length;
},
start: function () {
- this._super();
- _.each(this.filters, function (filter) {
- filter.start();
+ this.$element.on('click', 'li', this.proxy('toggle_filter'));
+ return $.when(null);
+ },
+ facet_for_defaults: function (defaults) {
+ var fs = _(this.filters).filter(function (f) {
+ return f.attrs && f.attrs.name && !!defaults[f.attrs.name];
+ });
+ if (_.isEmpty(fs)) { return $.when(null); }
+ return $.when(new VS.model.SearchFacet({
+ category: 'q',
+ value: _(fs).map(function (f) {
+ return f.attrs.string || f.attrs.name }).join(' | '),
+ json: fs,
+ field: this,
+ app: this.view.vs
+ }));
+ },
+ /**
+ * 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
});
},
- get_context: function () { },
/**
* 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();
return _.extend(new openerp.web.CompoundDomain(), {
__domains: domains
});
+ },
+ toggle_filter: function (e) {
+ // FIXME: oh god, my eyes, they hurt
+ var self = this, fs;
+ var filter = this.filters[$(e.target).index()];
+
+ var facet = this.view.vs.searchQuery.detect(function (f) {
+ return f.get('field') === self; });
+ if (facet) {
+ fs = facet.get('json');
+
+ if (_.include(fs, filter)) {
+ fs = _.without(fs, filter);
+ } else {
+ fs.push(filter);
+ }
+ if (_(fs).isEmpty()) {
+ this.view.vs.searchQuery.remove(facet, {trigger_search: true});
+ } else {
+ facet.set({
+ json: fs,
+ value: _(fs).map(function (f) {
+ return f.attrs.string || f.attrs.name }).join(' | ')
+ });
+ }
+ return;
+ } else {
+ fs = [filter];
+ }
+
+ this.view.vs.searchQuery.add({
+ category: 'q',
+ value: _(fs).map(function (f) {
+ return f.attrs.string || f.attrs.name }).join(' | '),
+ json: fs,
+ field: this,
+ app: this.view.vs
+ });
}
});
openerp.web.search.Filter = openerp.web.search.Input.extend(/** @lends openerp.web.search.Filter# */{
* 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
*
init: function (node, view) {
this._super(view);
this.load_attrs(node.attrs);
- this.classes = [this.attrs.string ? 'filter_label' : 'filter_icon'];
- this.make_id('filter', this.attrs.name);
- },
- start: function () {
- this._super();
- var self = this;
- this.$element.click(function (e) {
- $(this).toggleClass('enabled');
- self.view.do_toggle_filter(self);
- });
- },
- /**
- * Returns whether the filter is currently enabled (in use) or not.
- *
- * @returns a boolean
- */
- is_enabled:function () {
- return this.$element.hasClass('enabled');
},
- /**
- * If the filter is present in the defaults (and has a truthy value),
- * enable the filter.
- *
- * @param {Object} defaults the search view's default values
- */
- render: function (defaults) {
- if (this.attrs.name && defaults[this.attrs.name]) {
- this.classes.push('enabled');
- this.view.do_toggle_filter(this, true);
- }
- return this._super(defaults);
- },
- get_context: function () {
- if (!this.is_enabled()) {
- return;
- }
- return this.attrs.context;
- },
- /**
- * Does not return anything: filter domain is handled at the FilterGroup
- * level
- */
+ facet_for: function () { return $.when(null); },
+ get_context: function () { },
get_domain: function () { }
});
openerp.web.search.Field = openerp.web.search.Input.extend( /** @lends openerp.web.search.Field# */ {
init: function (view_section, field, view) {
this._super(view);
this.load_attrs(_.extend({}, field, view_section.attrs));
- this.filters = new openerp.web.search.FilterGroup(_.compact(_.map(
- view_section.children, function (filter_node) {
- if (filter_node.attrs.string &&
- typeof console !== 'undefined' && console.debug) {
- console.debug("Filter-in-field with a 'string' attribute "
- + "in view", view);
- }
- delete filter_node.attrs.string;
- return new openerp.web.search.Filter(
- filter_node, view);
- })), view);
- this.make_id('input', field.type, this.attrs.name);
},
- start: function () {
- this._super();
- this.filters.start();
+ facet_for: function (value) {
+ return $.when(new VS.model.SearchFacet({
+ category: this.attrs.string || this.attrs.name,
+ value: String(value),
+ json: value,
+ field: this,
+ app: this.view.vs
+ }));
},
- 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 !== '');
if (!(has_value && context)) {
return;
}
- return _.extend(
- {}, context,
- {own_values: {self: val}});
+ return new openerp.web.CompoundContext(context)
+ .set_eval_context({self: val});
},
/**
* Function creating the returned domain for the field, override this
* @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;
}
return this.make_domain(
this.attrs.name,
this.attrs.operator || this.default_operator,
- val);
+ facet);
}
- return _.extend({}, domain, {own_values: {self: val}});
+ return new openerp.web.CompoundDomain(domain)
+ .set_eval_context({self: val});
}
});
/**
*/
openerp.web.search.CharField = openerp.web.search.Field.extend( /** @lends openerp.web.search.CharField# */ {
default_operator: 'ilike',
- get_value: function () {
- return this.$element.val();
+ complete: function (value) {
+ if (_.isEmpty(value)) { return $.when(null); }
+ var label = _.str.sprintf(_.str.escapeHTML(
+ _t("Search %(field)s for: %(value)s")), {
+ field: '<em>' + this.attrs.string + '</em>',
+ value: '<strong>' + _.str.escapeHTML(value) + '</strong>'});
+ return $.when([{
+ category: this.attrs.string,
+ label: label,
+ value: value,
+ field: this
+ }]);
}
});
openerp.web.search.NumberField = openerp.web.search.Field.extend(/** @lends openerp.web.search.NumberField# */{
return !item[1];
});
},
- 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;
- },
- /**
- * The selection field needs a default ``false`` value in case none is
- * provided, so that selector options with a ``false`` value (convention
- * for explicitly empty options) get selected by default rather than the
- * first (value-holding) option in the selection.
- *
- * @param {Object} defaults search default values
- */
- render: function (defaults) {
- if (!defaults[this.attrs.name]) {
- defaults[this.attrs.name] = false;
- }
- return this._super(defaults);
- },
- clear: function () {
- var self = this, d = $.Deferred(), selection = this.attrs.selection;
- for(var index=0; index<selection.length; ++index) {
- var item = selection[index];
- if (!item[1]) {
- setTimeout(function () {
- // won't override mutable, because we immediately bail out
- //noinspection JSReferencingMutableVariableFromClosure
- self.$element.val(index);
- d.resolve();
- }, 0);
- return d.promise();
- }
- }
- return d.resolve().promise();
+ complete: function (needle) {
+ var self = this;
+ var results = _(this.attrs.selection).chain()
+ .filter(function (sel) {
+ var value = sel[0], label = sel[1];
+ if (!value) { return false; }
+ return label.toLowerCase().indexOf(needle.toLowerCase()) !== -1;
+ })
+ .map(function (sel) {
+ return {
+ category: self.attrs.string,
+ field: self,
+ value: sel[1],
+ json: sel[0]
+ };
+ }).value();
+ if (_.isEmpty(results)) { return $.when(null); }
+ return $.when.apply(null, [{
+ category: this.attrs.string
+ }].concat(results));
+ },
+ facet_for: function (value) {
+ var match = _(this.attrs.selection).detect(function (sel) {
+ return sel[0] === value;
+ });
+ if (!match) { return $.when(null); }
+ return $.when(new VS.model.SearchFacet({
+ category: this.attrs.string,
+ value: match[1],
+ json: match[0],
+ field: this,
+ app: this.view.app
+ }));
+ },
+ get_value: function (facet) {
+ return facet.get('json');
}
});
openerp.web.search.BooleanField = openerp.web.search.SelectionField.extend(/** @lends openerp.web.search.BooleanField# */{
['false', _t("No")]
];
},
- /**
- * Search defaults likely to be boolean values (for a boolean field).
- *
- * In the HTML, we only want/get strings, and our strings here are ``true``
- * and ``false``, so ensure we use precisely those by truth-testing the
- * default value (iif there is one in the view's defaults).
- *
- * @param {Object} defaults default values for this search view
- * @returns {String} rendered boolean field
- */
- render: function (defaults) {
- var name = this.attrs.name;
- if (name in defaults) {
- defaults[name] = defaults[name] ? "true" : "false";
- }
- return this._super(defaults);
- },
- get_value: function () {
- switch (this._super()) {
+ get_value: function (facet) {
+ switch (this._super(facet)) {
case 'false': return false;
case 'true': return true;
default: return null;
},
get_value: function () {
return this.datewidget.get_value() || null;
- },
- clear: function () {
- this.datewidget.set_value(false);
}
});
/**
openerp.web.search.ManyToOneField = openerp.web.search.CharField.extend({
init: function (view_section, field, view) {
this._super(view_section, field, view);
+ this.model = new openerp.web.Model(this.attrs.relation);
+ },
+ complete: function (needle) {
var self = this;
- this.got_name = $.Deferred().then(function () {
- self.$element.val(self.name);
+ // TODO: context
+ // FIXME: "concurrent" searches (multiple requests, mis-ordered responses)
+ return this.model.call('name_search', [], {
+ name: needle,
+ limit: 8,
+ context: {}
+ }).pipe(function (results) {
+ if (_.isEmpty(results)) { return null; }
+ return [{category: self.attrs.string}].concat(
+ _(results).map(function (result) {
+ return {
+ category: self.attrs.string,
+ value: result[1],
+ json: result[0],
+ field: self
+ };
+ }));
});
- this.dataset = new openerp.web.DataSet(
- this.view, this.attrs['relation']);
},
- start: function () {
- this._super();
- this.setup_autocomplete();
- var started = $.Deferred();
- this.got_name.then(function () { started.resolve();},
- function () { started.resolve(); });
- return started.promise();
- },
- setup_autocomplete: function () {
+ facet_for: function (value) {
var self = this;
- this.$element.autocomplete({
- source: function (req, resp) {
- if (self.abort_last) {
- self.abort_last();
- delete self.abort_last;
- }
- self.dataset.name_search(
- req.term, self.attrs.domain, 'ilike', 8, function (data) {
- resp(_.map(data, function (result) {
- return {id: result[0], label: result[1]}
- }));
- });
- self.abort_last = self.dataset.abort_last;
- },
- select: function (event, ui) {
- self.id = ui.item.id;
- self.name = ui.item.label;
- },
- delay: 0
- })
- },
- on_name_get: function (name_get) {
- if (!name_get.length) {
- delete this.id;
- this.got_name.reject();
- return;
- }
- this.name = name_get[0][1];
- this.got_name.resolve();
- },
- render: function (defaults) {
- if (defaults[this.attrs.name]) {
- this.id = defaults[this.attrs.name];
- if (this.id instanceof Array)
- this.id = this.id[0];
- // TODO: maybe this should not be completely removed
- delete defaults[this.attrs.name];
- this.dataset.name_get([this.id], $.proxy(this, 'on_name_get'));
- } else {
- this.got_name.reject();
+ if (value instanceof Array) {
+ return $.when(new VS.model.SearchFacet({
+ category: this.attrs.string,
+ value: value[1],
+ json: value[0],
+ field: this,
+ app: this.view.vs
+ }));
}
- return this._super(defaults);
+ return this.model.call('name_get', [value], {}).pipe(function (names) {
+ return new VS.model.SearchFacet({
+ category: self.attrs.string,
+ value: names[0][1],
+ json: names[0][0],
+ field: self,
+ app: self.view.vs
+ });
+ })
},
- 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);
}
});