From 0a4a7f3d20e3cce435824d3aa756346088b0c73b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9ry=20Debongnie?= Date: Wed, 25 Jun 2014 13:26:41 +0200 Subject: [PATCH 1/7] [IMP] new autocomplete widget Task #5009 This new autocomplete widget (the one used in the search bar) does not do remote calls automatically, but on demand. In theory, it should lead to a better user experience, not having the ui blocked every time long remote calls are done. It also has the benefits of bringing us one step closer to not depending on jquery.ui. Bonus point: the code is quite short (< 200 loc i believe) --- addons/web/static/src/css/base.css | 32 ++++ addons/web/static/src/css/base.sass | 25 +++ addons/web/static/src/js/search.js | 279 +++++++++++++++++++++++----- addons/web/static/src/xml/base.xml | 4 + addons/web/static/test/search.js | 23 ++- 5 files changed, 308 insertions(+), 55 deletions(-) diff --git a/addons/web/static/src/css/base.css b/addons/web/static/src/css/base.css index 91146789602..dab2e482a18 100644 --- a/addons/web/static/src/css/base.css +++ b/addons/web/static/src/css/base.css @@ -1588,6 +1588,38 @@ -webkit-border-radius: 2px; border-radius: 2px; } +.openerp .oe_searchview .oe-autocomplete { + display: none; + position: absolute; + width: 300px; + background-color: white; + border: 1px solid black; + z-index: 666; + margin-top: 5px; + cursor: default; +} +.openerp .oe_searchview .oe-autocomplete ul { + list-style-type: none; + padding-left: 0; + margin: 5px; +} +.openerp .oe_searchview .oe-autocomplete ul li { + padding-left: 20px; + text-shadow: 0 0 0 white; +} +.openerp .oe_searchview .oe-autocomplete ul li span:first-child { + margin-right: 5px; +} +.openerp .oe_searchview .oe-autocomplete ul li span.oe-expand { + cursor: pointer; +} +.openerp .oe_searchview .oe-autocomplete ul li.oe-indent { + margin-left: 20px; +} +.openerp .oe_searchview .oe-autocomplete ul li.oe-selection-focus { + background-color: #7c7bad; + color: white; +} .openerp .oe_searchview_drawer_container { overflow: auto; } diff --git a/addons/web/static/src/css/base.sass b/addons/web/static/src/css/base.sass index 07d683268f1..523ee846622 100644 --- a/addons/web/static/src/css/base.sass +++ b/addons/web/static/src/css/base.sass @@ -1307,6 +1307,31 @@ $sheet-padding: 16px text-shadow: 0 1px 1px rgba(0, 0, 0, 0.4) @include radius(2px) + .oe-autocomplete + display: none + position: absolute + width: 300px + background-color: white + border: 1px solid black + z-index: 666 + margin-top: 5px + cursor: default + ul + list-style-type: none + padding-left: 0 + margin: 5px + li + padding-left: 20px + text-shadow: 0 0 0 white + span:first-child + margin-right: 5px + span.oe-expand + cursor: pointer + li.oe-indent + margin-left: 20px + li.oe-selection-focus + background-color: #7c7bad + color: white .oe_searchview_drawer_container overflow: auto .oe_searchview_drawer diff --git a/addons/web/static/src/js/search.js b/addons/web/static/src/js/search.js index ac441d8eed6..b0b73d28e4f 100644 --- a/addons/web/static/src/js/search.js +++ b/addons/web/static/src/js/search.js @@ -350,7 +350,9 @@ instance.web.SearchView = instance.web.Widget.extend(/** @lends instance.web.Sea e.preventDefault(); break; case $.ui.keyCode.RIGHT: - this.focusFollowing(e.target); + if (!this.autocomplete.is_expandable()) { + this.focusFollowing(e.target); + } e.preventDefault(); break; } @@ -484,53 +486,18 @@ instance.web.SearchView = instance.web.Widget.extend(/** @lends instance.web.Sea * Sets up search view's view-wide auto-completion widget */ setup_global_completion: function () { - var autocomplete = this.$el.autocomplete({ + var self = this; + + this.autocomplete = new instance.web.search.AutoComplete(this, { source: this.proxy('complete_global_search'), select: this.proxy('select_completion'), - focus: function (e) { e.preventDefault(); }, - html: true, - autoFocus: true, - minLength: 1, - delay: 250, - }).data('autocomplete'); - - this.$el.on('input', function () { - this.$el.autocomplete('close'); - }.bind(this)); - - // MonkeyPatch autocomplete instance - _.extend(autocomplete, { - _renderItem: function (ul, item) { - // item of completion list - var $item = $( "
  • " ) - .data( "item.autocomplete", item ) - .appendTo( ul ); - - if (item.facet !== undefined) { - // regular completion item - if (item.first) { - $item.css('borderTop', '1px solid #cccccc'); - } - return $item.append( - (item.label) - ? $('').html(item.label) - : $('').text(item.value)); - } - return $item.text(item.label) - .css({ - borderTop: '1px solid #cccccc', - margin: 0, - padding: 0, - zoom: 1, - 'float': 'left', - clear: 'left', - width: '100%' - }); - }, - _value: function() { + delay: 0, + get_search_string: function () { return self.$('div.oe_searchview_input').text(); }, + width: this.$el.width(), }); + this.autocomplete.appendTo(this.$el); }, /** * Provide auto-completion result for req.term (an array to `resp`) @@ -579,14 +546,9 @@ instance.web.SearchView = instance.web.Widget.extend(/** @lends instance.web.Sea childBlurred: function () { var val = this.$el.val(); this.$el.val(''); - var complete = this.$el.data('autocomplete'); - if ((val && complete.term === undefined) || complete.previous) { - throw new Error("new jquery.ui version altering implementation" + - " details relied on"); - } - delete complete.term; this.$el.removeClass('oe_focused') .trigger('blur'); + this.autocomplete.close(); }, /** * Call the renderFacets method with the correct arguments. @@ -1639,7 +1601,25 @@ instance.web.search.ManyToOneField = instance.web.search.CharField.extend({ this._super(view_section, field, parent); this.model = new instance.web.Model(this.attrs.relation); }, - complete: function (needle) { + + complete: function (value) { + if (_.isEmpty(value)) { return $.when(null); } + var label = _.str.sprintf(_.str.escapeHTML( + _t("Search %(field)s for: %(value)s")), { + field: '' + _.escape(this.attrs.string) + '', + value: '' + _.escape(value) + ''}); + return $.when([{ + label: label, + facet: { + category: this.attrs.string, + field: this, + values: [{label: value, value: value}] + }, + expand: this.expand.bind(this), + }]); + }, + + expand: function (needle) { var self = this; // FIXME: "concurrent" searches (multiple requests, mis-ordered responses) var context = instance.web.pyeval.eval( @@ -2311,6 +2291,205 @@ instance.web.search.custom_filters = new instance.web.Registry({ 'id': 'instance.web.search.ExtendedSearchProposition.Id' }); +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.delay: delay in millisecond before calling source. Useful if you don't want + // to make too many rpc calls + // 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; + this.delay = options.delay; + this.select = options.select, + this.get_search_string = options.get_search_string; + this.width = options.width || 400; + + this.current_result = null; + + this.searching = true; + this.search_string = null; + 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; + } + if (!self.searching) { + self.searching = true; + return; + } + self.search_string = self.get_search_string(); + if (self.search_string.length) { + var search_string = self.search_string; + setTimeout(function () { self.initiate_search(search_string);}, self.delay); + } else { + self.close(); + } + }); + this.$input.on('keydown', function (ev) { + switch (ev.which) { + case $.ui.keyCode.TAB: + case $.ui.keyCode.ENTER: + if (self.get_search_string().length) { + 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(); + results.forEach(function (result) { + var $item = self.make_list_item(result).appendTo($list); + result.$el = $item; + }); + this.show(); + }, + make_list_item: function (result) { + var self = this; + var $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 = $('').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($('').html(result.label)); + return $li; + }, + expand: function () { + var self = this; + this.current_result.expand(this.get_search_string()).then(function (results) { + (results || [{label: '(no result)'}]).forEach(function (result) { + result.indent = true; + var $li = self.make_list_item(result); + self.current_result.$el.after($li); + }); + self.current_result.expanded = true; + self.current_result.$el.find('span.oe-expand').html('▼'); + }); + }, + 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; + this.search_string = null; + this.searching = true; + this.$el.hide(); + }, + move: function (direction) { + var $next; + if (direction === 'down') { + $next = this.$('li.oe-selection-focus').next(); + if (!$next.length) $next = this.$('li:first-child'); + } else { + $next = this.$('li.oe-selection-focus').prev(); + if (!$next.length) $next = this.$('li:last-child'); + } + this.focus_element($next); + }, + is_expandable: function () { + return !!this.$('.oe-selection-focus .oe-expand').length; + }, +}); + })(); // vim:et fdc=0 fdl=0 foldnestmax=3 fdm=syntax: diff --git a/addons/web/static/src/xml/base.xml b/addons/web/static/src/xml/base.xml index 69af5cbe91d..1caef25a907 100644 --- a/addons/web/static/src/xml/base.xml +++ b/addons/web/static/src/xml/base.xml @@ -1802,6 +1802,10 @@ +
    +
      +
    +
    Export diff --git a/addons/web/static/test/search.js b/addons/web/static/test/search.js index 4724ca0792d..d6490abee22 100644 --- a/addons/web/static/test/search.js +++ b/addons/web/static/test/search.js @@ -558,7 +558,20 @@ openerp.testing.section('search.completions', { new Date(2012, 4, 21, 21, 21, 21).getTime()); }); }); - test("M2O", {asserts: 13}, function (instance, $s, mock) { + test("M2O complete", {asserts: 4}, function (instance, $s, mock) { + var view = {inputs: [], dataset: {get_context: function () {}}}; + var f = new instance.web.search.ManyToOneField( + {attrs: {string: 'Dummy'}}, {relation: 'dummy.model'}, view); + return f.complete("bob") + .done(function (c) { + equal(c.length, 1, "should return one line"); + var bob = c[0]; + ok(bob.expand, "should return an expand callback"); + ok(bob.facet, "should have a facet"); + ok(bob.label, "should have a label"); + }); + }); + test("M2O expand", {asserts: 13}, function (instance, $s, mock) { mock('dummy.model:name_search', function (args, kwargs) { deepEqual(args, []); strictEqual(kwargs.name, 'bob'); @@ -568,7 +581,7 @@ openerp.testing.section('search.completions', { var view = {inputs: [], dataset: {get_context: function () {}}}; var f = new instance.web.search.ManyToOneField( {attrs: {string: 'Dummy'}}, {relation: 'dummy.model'}, view); - return f.complete("bob") + return f.expand("bob") .done(function (c) { equal(c.length, 3, "should return results + title"); var title = c[0]; @@ -597,7 +610,7 @@ openerp.testing.section('search.completions', { var view = {inputs: [], dataset: {get_context: function () {}}}; var f = new instance.web.search.ManyToOneField( {attrs: {string: 'Dummy'}}, {relation: 'dummy.model'}, view); - return f.complete("bob") + return f.expand("bob") .done(function (c) { ok(!c, "no match should yield no completion"); }); @@ -620,7 +633,7 @@ openerp.testing.section('search.completions', { var f = new instance.web.search.ManyToOneField( {attrs: {string: 'Dummy', domain: '[["foo", "=", "bar"]]'}}, {relation: 'dummy.model'}, view); - return f.complete("bob"); + return f.expand("bob"); }); test("M2O custom operator", {asserts: 10}, function (instance, $s, mock) { mock('dummy.model:name_search', function (args, kwargs) { @@ -635,7 +648,7 @@ openerp.testing.section('search.completions', { {attrs: {string: 'Dummy', operator: 'ilike'}}, {relation: 'dummy.model'}, view); - return f.complete('bob') + return f.expand('bob') .done(function (c) { equal(c.length, 2, "should return result + title"); var title = c[0]; From 3c1c6888baf4dfd2f774a65a7b57316156bf9a98 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9ry=20Debongnie?= Date: Fri, 27 Jun 2014 11:40:37 +0200 Subject: [PATCH 2/7] [FIX] fix 2 display bugs with expand autocomplete * the order was reversed * there was an useless facet with just the string name --- addons/web/static/src/js/search.js | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/addons/web/static/src/js/search.js b/addons/web/static/src/js/search.js index b0b73d28e4f..a8d1571527a 100644 --- a/addons/web/static/src/js/search.js +++ b/addons/web/static/src/js/search.js @@ -1632,13 +1632,12 @@ instance.web.search.ManyToOneField = instance.web.search.CharField.extend({ context: context }).then(function (results) { if (_.isEmpty(results)) { return null; } - return [{label: self.attrs.string}].concat( - _(results).map(function (result) { - return { - label: _.escape(result[1]), - facet: facet_from(self, result) - }; - })); + return _(results).map(function (result) { + return { + label: _.escape(result[1]), + facet: facet_from(self, result) + }; + }); }); }, facet_for: function (value) { @@ -2436,7 +2435,7 @@ instance.web.search.AutoComplete = instance.web.Widget.extend({ expand: function () { var self = this; this.current_result.expand(this.get_search_string()).then(function (results) { - (results || [{label: '(no result)'}]).forEach(function (result) { + (results || [{label: '(no result)'}]).reverse().forEach(function (result) { result.indent = true; var $li = self.make_list_item(result); self.current_result.$el.after($li); From 512f7acfdccd4208c9da4d95727e0d8d22889e55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9ry=20Debongnie?= Date: Fri, 27 Jun 2014 12:05:34 +0200 Subject: [PATCH 3/7] [IMP] autocomplete now display separators separators given in the search view are now displayed in the autocomplete widget --- addons/web/static/src/css/base.css | 8 ++++++++ addons/web/static/src/css/base.sass | 8 ++++++++ addons/web/static/src/js/search.js | 23 ++++++++++++++++++++--- 3 files changed, 36 insertions(+), 3 deletions(-) diff --git a/addons/web/static/src/css/base.css b/addons/web/static/src/css/base.css index dab2e482a18..c14100a7d5f 100644 --- a/addons/web/static/src/css/base.css +++ b/addons/web/static/src/css/base.css @@ -1620,6 +1620,14 @@ background-color: #7c7bad; color: white; } +.openerp .oe_searchview .oe-autocomplete ul li.oe-separator { + margin-top: 2px; + margin-bottom: 2px; + border-top: 1px solid #afafb6; +} +.openerp .oe_searchview .oe-autocomplete ul li.oe-separator:last-child { + display: none; +} .openerp .oe_searchview_drawer_container { overflow: auto; } diff --git a/addons/web/static/src/css/base.sass b/addons/web/static/src/css/base.sass index 523ee846622..e3a84a8f78c 100644 --- a/addons/web/static/src/css/base.sass +++ b/addons/web/static/src/css/base.sass @@ -1332,6 +1332,14 @@ $sheet-padding: 16px li.oe-selection-focus background-color: #7c7bad color: white + li.oe-separator + margin-top: 2px + margin-bottom: 2px + border-top: 1px solid #afafb6 + li.oe-separator:last-child + display: none + + .oe_searchview_drawer_container overflow: auto .oe_searchview_drawer diff --git a/addons/web/static/src/js/search.js b/addons/web/static/src/js/search.js index a8d1571527a..eabc6a9c104 100644 --- a/addons/web/static/src/js/search.js +++ b/addons/web/static/src/js/search.js @@ -846,7 +846,9 @@ instance.web.SearchViewDrawer = instance.web.Widget.extend({ } switch (item.tag) { - case 'separator': case 'newline': + case 'separator': + case 'newline': + filters.push(new instance.web.search.Separator(this)) break; case 'filter': filters.push(new instance.web.search.Filter(item, group)); @@ -1311,6 +1313,13 @@ instance.web.search.Filter = instance.web.search.Input.extend(/** @lends instanc get_context: function () { }, get_domain: function () { }, }); + +instance.web.search.Separator = instance.web.search.Input.extend({ + complete: function () { + return {is_separator: true}; + } +}); + instance.web.search.Field = instance.web.search.Input.extend( /** @lends instance.web.search.Field# */ { template: 'SearchView.field', default_operator: '=', @@ -2397,9 +2406,17 @@ instance.web.search.AutoComplete = instance.web.Widget.extend({ var self = this; var $list = this.$('ul'); $list.empty(); + var render_separator = false; results.forEach(function (result) { - var $item = self.make_list_item(result).appendTo($list); - result.$el = $item; + if (result.is_separator) { + if (render_separator) + $list.append($('
  • ').addClass('oe-separator')); + render_separator = false; + } else { + var $item = self.make_list_item(result).appendTo($list); + result.$el = $item; + render_separator = true; + } }); this.show(); }, From e556fd735e66dc8d769be0bb3b7ea23777c24116 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9ry=20Debongnie?= Date: Fri, 27 Jun 2014 13:57:33 +0200 Subject: [PATCH 4/7] [FIX] ignore separators when moving in autocomplet --- addons/web/static/src/js/search.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/addons/web/static/src/js/search.js b/addons/web/static/src/js/search.js index eabc6a9c104..92396bc181b 100644 --- a/addons/web/static/src/js/search.js +++ b/addons/web/static/src/js/search.js @@ -2493,10 +2493,10 @@ instance.web.search.AutoComplete = instance.web.Widget.extend({ move: function (direction) { var $next; if (direction === 'down') { - $next = this.$('li.oe-selection-focus').next(); + $next = this.$('li.oe-selection-focus').nextAll(':not(.oe-separator)').first(); if (!$next.length) $next = this.$('li:first-child'); } else { - $next = this.$('li.oe-selection-focus').prev(); + $next = this.$('li.oe-selection-focus').prevAll(':not(.oe-separator)').first(); if (!$next.length) $next = this.$('li:last-child'); } this.focus_element($next); From 2c43d6051810201eb0e24befc9401775b9c39525 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9ry=20Debongnie?= Date: Mon, 30 Jun 2014 14:42:05 +0200 Subject: [PATCH 5/7] [FIX] don't display separators in the drawer now, separators (defined by the tag 'group'!) are only displayed in the autocomplete and not in the searchview drawer --- addons/web/static/src/js/search.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/addons/web/static/src/js/search.js b/addons/web/static/src/js/search.js index 92396bc181b..b21e5649d6e 100644 --- a/addons/web/static/src/js/search.js +++ b/addons/web/static/src/js/search.js @@ -846,17 +846,17 @@ instance.web.SearchViewDrawer = instance.web.Widget.extend({ } switch (item.tag) { - case 'separator': - case 'newline': - filters.push(new instance.web.search.Separator(this)) + case 'separator': case 'newline': break; case 'filter': filters.push(new instance.web.search.Filter(item, group)); break; case 'group': + self.inputs.push(new instance.web.search.Separator(this)) self.make_widgets(item.children, fields, new instance.web.search.Group(group, 'w', item)); break; + self.inputs.push(new instance.web.search.Separator(this)) case 'field': var field = this.make_field( item, fields[item['attrs'].name], group); @@ -1315,6 +1315,8 @@ instance.web.search.Filter = instance.web.search.Input.extend(/** @lends instanc }); instance.web.search.Separator = instance.web.search.Input.extend({ + _in_drawer: false, + complete: function () { return {is_separator: true}; } From cde4ad1159105b3b26e9c4a7a9d0f0f072b4e0eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9ry=20Debongnie?= Date: Mon, 30 Jun 2014 14:58:12 +0200 Subject: [PATCH 6/7] [IMP] remove useless code the 'first' attribute is not used anymore --- addons/web/static/src/js/search.js | 6 ------ 1 file changed, 6 deletions(-) diff --git a/addons/web/static/src/js/search.js b/addons/web/static/src/js/search.js index b21e5649d6e..945b83256f2 100644 --- a/addons/web/static/src/js/search.js +++ b/addons/web/static/src/js/search.js @@ -513,12 +513,6 @@ instance.web.SearchView = instance.web.Widget.extend(/** @lends instance.web.Sea .value()).then(function () { resp(_(arguments).chain() .compact() - .map(function (completion) { - if (completion.length && completion[0].facet !== undefined) { - completion[0].first = true; - } - return completion; - }) .flatten(true) .value()); }); From 88bb66e784d254063887c6b5791e3d40ff99d6fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9ry=20Debongnie?= Date: Mon, 30 Jun 2014 15:06:52 +0200 Subject: [PATCH 7/7] [FIX] fix tests in autocompletion --- addons/web/static/src/js/search.js | 9 ++++++-- addons/web/static/test/search.js | 36 +++++++++++++----------------- 2 files changed, 23 insertions(+), 22 deletions(-) diff --git a/addons/web/static/src/js/search.js b/addons/web/static/src/js/search.js index 945b83256f2..0ec63b890e9 100644 --- a/addons/web/static/src/js/search.js +++ b/addons/web/static/src/js/search.js @@ -846,11 +846,11 @@ instance.web.SearchViewDrawer = instance.web.Widget.extend({ filters.push(new instance.web.search.Filter(item, group)); break; case 'group': - self.inputs.push(new instance.web.search.Separator(this)) + self.add_separator(); self.make_widgets(item.children, fields, new instance.web.search.Group(group, 'w', item)); + self.add_separator(); break; - self.inputs.push(new instance.web.search.Separator(this)) case 'field': var field = this.make_field( item, fields[item['attrs'].name], group); @@ -865,6 +865,11 @@ instance.web.SearchViewDrawer = instance.web.Widget.extend({ group.push(new instance.web.search.FilterGroup(filters, this)); } }, + + add_separator: function () { + if (!(_.last(this.inputs) instanceof instance.web.search.Separator)) + new instance.web.search.Separator(this); + }, /** * Creates a field for the provided field descriptor item (which comes * from fields_view_get) diff --git a/addons/web/static/test/search.js b/addons/web/static/test/search.js index d6490abee22..5faf13b2599 100644 --- a/addons/web/static/test/search.js +++ b/addons/web/static/test/search.js @@ -571,7 +571,7 @@ openerp.testing.section('search.completions', { ok(bob.label, "should have a label"); }); }); - test("M2O expand", {asserts: 13}, function (instance, $s, mock) { + test("M2O expand", {asserts: 11}, function (instance, $s, mock) { mock('dummy.model:name_search', function (args, kwargs) { deepEqual(args, []); strictEqual(kwargs.name, 'bob'); @@ -583,19 +583,16 @@ openerp.testing.section('search.completions', { {attrs: {string: 'Dummy'}}, {relation: 'dummy.model'}, view); return f.expand("bob") .done(function (c) { - equal(c.length, 3, "should return results + title"); - var title = c[0]; - equal(title.label, f.attrs.string, "title should match field name"); - ok(!title.facet, "title should not have a facet"); + equal(c.length, 2, "should return results"); - var f1 = new instance.web.search.Facet(c[1].facet); - equal(c[1].label, "choice 1"); + var f1 = new instance.web.search.Facet(c[0].facet); + equal(c[0].label, "choice 1"); equal(f1.get('category'), f.attrs.string); equal(f1.get('field'), f); deepEqual(f1.values.toJSON(), [{label: 'choice 1', value: 42}]); - var f2 = new instance.web.search.Facet(c[2].facet); - equal(c[2].label, "choice @"); + var f2 = new instance.web.search.Facet(c[1].facet); + equal(c[1].label, "choice @"); equal(f2.get('category'), f.attrs.string); equal(f2.get('field'), f); deepEqual(f2.values.toJSON(), [{label: 'choice @', value: 43}]); @@ -635,7 +632,7 @@ openerp.testing.section('search.completions', { {relation: 'dummy.model'}, view); return f.expand("bob"); }); - test("M2O custom operator", {asserts: 10}, function (instance, $s, mock) { + test("M2O custom operator", {asserts: 8}, function (instance, $s, mock) { mock('dummy.model:name_search', function (args, kwargs) { deepEqual(args, [], "should have no positional arguments"); // the operator is meant for the final search term generation, not the autocompletion @@ -650,13 +647,10 @@ openerp.testing.section('search.completions', { return f.expand('bob') .done(function (c) { - equal(c.length, 2, "should return result + title"); - var title = c[0]; - equal(title.label, f.attrs.string, "title should match field name"); - ok(!title.facet, "title should not have a facet"); + equal(c.length, 1, "should return result"); - var f1 = new instance.web.search.Facet(c[1].facet); - equal(c[1].label, "Match"); + var f1 = new instance.web.search.Facet(c[0].facet); + equal(c[0].label, "Match"); equal(f1.get('category'), f.attrs.string); equal(f1.get('field'), f); deepEqual(f1.values.toJSON(), [{label: 'Match', value: 42}]); @@ -1223,13 +1217,14 @@ openerp.testing.section('search.groupby', { var view = makeSearchView(instance); return view.appendTo($fix) .done(function () { - // 3 filters, 3 filtergroups, 1 custom filter, 1 advanced, 1 Filters + // 3 filters, 3 filtergroups, 1 custom filter, 1 advanced, 1 Filters, // and 1 SaveFilter widget equal(view.drawer.inputs.length, 10, "should have 10 inputs total"); var groups = _.filter(view.drawer.inputs, function (f) { return f instanceof instance.web.search.GroupbyGroup; }); + equal(groups.length, 3, "should have 3 GroupbyGroups"); groups[0].toggle(groups[0].filters[0]); @@ -1562,10 +1557,11 @@ openerp.testing.section('search.invisible', { var done = $.Deferred(); view.complete_global_search({term: 'filter'}, function (compls) { done.resolve(); - strictEqual(compls.length, 2, - "should have 2 completions"); + console.log("completions", compls); + strictEqual(compls.length, 5, + "should have 5 completions"); // 2 filters and 3 separators deepEqual(_.pluck(compls, 'label'), - ['Field 0', 'Filter on: Filter 0'], + [undefined, 'Field 0', 'Filter on: Filter 0', undefined, undefined], "should complete on field 0 and filter 0"); }); return done;