Skip to content

Commit

Permalink
[FIX] web: search more in many2one: filter on ids
Browse files Browse the repository at this point in the history
Let's assume a many2one field with a lot of possible values. The
user clicks on 'Search More...'. In the opened dialog, only 160
records are available (their ids have been obtained with a
name_search, with the optional text the user could have typed in
the many2one input).

Before 4cd379c, that extra domain on ids was removed automatically
as soon as the user interacted with the search view in the dialog.
This was especially useful when there were more than 160 records.
However, this was rather an happy coincidence than a designed
feature.

From 4cd379c, the ids selection was added to the initial domain
of the list, so they couldn't be removed from the domain
afterwards. The user was thus stucked with its preselected 160
records.

This rev. doesn't restore the former behavior, but rather improves
the current one, as follows:
 - when there is no text in the many2one input (i.e. no value to
   filter on), we bypass the name_search, s.t. all records are
   available in the dialog
 - when there is some text in the input, we perform a name_search (as
   before) to get a list of record ids, and we add a special filter
   to the search view in the dialog (the filter on those ids), s.t.
   the user can remove it if he wants to access the remaining records.
 - finally, the limit is now set to 320, to mitigate the problem

Issue reported on the saas-12.1 migration pad.

closes #31232
  • Loading branch information
aab-odoo committed Feb 26, 2019
1 parent a76a958 commit 6a8e1b5
Show file tree
Hide file tree
Showing 7 changed files with 164 additions and 14 deletions.
34 changes: 28 additions & 6 deletions addons/web/static/src/js/fields/relational_fields.js
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ var FieldMany2One = AbstractField.extend({
'click': '_onClick',
}),
AUTOCOMPLETE_DELAY: 200,
SEARCH_MORE_LIMIT: 320,

/**
* @override
Expand Down Expand Up @@ -501,18 +502,33 @@ var FieldMany2One = AbstractField.extend({
values.push({
label: _t("Search More..."),
action: function () {
self._rpc({
var prom;
if (search_val !== '') {
prom = self._rpc({
model: self.field.relation,
method: 'name_search',
kwargs: {
name: search_val,
args: domain,
operator: "ilike",
limit: 160,
limit: self.SEARCH_MORE_LIMIT,
context: context,
},
})
.then(self._searchCreatePopup.bind(self, "search"));
});
}
$.when(prom).then(function (results) {
var dynamicFilters;
if (results) {
var ids = _.map(results, function (x) {
return x[0];
});
dynamicFilters = [{
description: _.str.sprintf(_t('Quick search: %s'), search_val),
domain: [['id', 'in', ids]],
}];
}
self._searchCreatePopup("search", false, {}, dynamicFilters);
});
},
classname: 'o_m2o_dropdown_option',
});
Expand Down Expand Up @@ -555,19 +571,25 @@ var FieldMany2One = AbstractField.extend({
/**
* all search/create popup handling
*
* TODO: ids argument is no longer used, remove it in master (as well as
* initial_ids param of the dialog)
*
* @private
* @param {any} view
* @param {any} ids
* @param {any} context
* @param {Object[]} [dynamicFilters=[]] filters to add to the search view
* in the dialog (each filter has keys 'description' and 'domain')
*/
_searchCreatePopup: function (view, ids, context) {
_searchCreatePopup: function (view, ids, context, dynamicFilters) {
var self = this;
return new dialogs.SelectCreateDialog(this, _.extend({}, this.nodeOptions, {
res_model: this.field.relation,
domain: this.record.getDomain({fieldName: this.name}),
context: _.extend({}, this.record.getContext(this.recordParams), context || {}),
dynamicFilters: dynamicFilters || [],
title: (view === 'search' ? _t("Search: ") : _t("Create: ")) + this.string,
initial_ids: ids ? _.map(ids, function (x) { return x[0]; }) : undefined,
initial_ids: ids,
initial_view: view,
disable_multiple_selection: true,
no_create: !self.can_create,
Expand Down
3 changes: 3 additions & 0 deletions addons/web/static/src/js/views/abstract_view.js
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,8 @@ var AbstractView = Factory.extend({
* @param {string} [params.controllerState]
* @param {string} [params.displayName]
* @param {Array[]} [params.domain=[]]
* @param {Object[]} [params.dynamicFilters] transmitted to the
* ControlPanelView
* @param {number[]} [params.ids]
* @param {boolean} [params.isEmbedded=false]
* @param {Object} [params.searchQuery={}]
Expand Down Expand Up @@ -165,6 +167,7 @@ var AbstractView = Factory.extend({
this.controlPanelParams = {
action: action,
activateDefaultFavorite: params.activateDefaultFavorite,
dynamicFilters: params.dynamicFilters,
breadcrumbs: params.breadcrumbs,
context: this.loadParams.context,
domain: this.loadParams.domain,
Expand Down
21 changes: 19 additions & 2 deletions addons/web/static/src/js/views/control_panel/control_panel_view.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,12 @@ var ControlPanelView = Factory.extend({
* breadcrumbs won't be rendered
* @param {boolean} [params.withSearchBar=true] if set to false, no default
* search bar will be rendered
* @param {boolean} [activateDefaultFavorite=false] determine if the default
* custom filters can be activated
* @param {boolean} [params.activateDefaultFavorite] determine if the
* default custom filters can be activated
* @param {Object[]} [params.dynamicFilters=[]] filters to add to the
* search (in addition to those described in the arch), each filter being
* an object with keys 'description' (what is displayed in the searchbar)
* and 'domain'
*/
init: function (params) {
var self = this;
Expand Down Expand Up @@ -103,6 +107,19 @@ var ControlPanelView = Factory.extend({
INTERVAL_OPTIONS = INTERVAL_OPTIONS.map(function (option) {
return _.extend(option, {description: option.description.toString()});
});

// add a filter group with the dynamic filters, if any
if (params.dynamicFilters && params.dynamicFilters.length) {
var dynamicFiltersGroup = params.dynamicFilters.map(function (filter) {
return {
description: filter.description,
domain: JSON.stringify(filter.domain),
isDefault: true,
type: 'filter',
};
});
this.loadParams.groups.unshift(dynamicFiltersGroup);
}
},

//--------------------------------------------------------------------------
Expand Down
2 changes: 2 additions & 0 deletions addons/web/static/src/js/views/view_dialogs.js
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,7 @@ var SelectCreateDialog = ViewDialog.extend({
* - list_view_options: dict of options to pass to the List View
* - on_selected: optional callback to execute when records are selected
* - disable_multiple_selection: true to allow create/select multiple records
* - dynamicFilters: filters to add to the searchview
*/
init: function () {
this._super.apply(this, arguments);
Expand Down Expand Up @@ -346,6 +347,7 @@ var SelectCreateDialog = ViewDialog.extend({
controlPanelFieldsView: fieldsViews.search,
},
action_buttons: false,
dynamicFilters: this.options.dynamicFilters,
context: this.context,
domain: domain,
hasSelectors: !this.options.disable_multiple_selection,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -958,7 +958,7 @@ QUnit.module('fields', {}, function () {
return result;
},
});
await testUtils.fields.many2one.searchAndClickItem('trululu', 'b');
await testUtils.fields.many2one.searchAndClickItem('trululu', {search: 'b'});
testUtils.form.clickSave(form);

assert.verifySteps(['name_create'],
Expand Down Expand Up @@ -2596,6 +2596,107 @@ QUnit.module('fields', {}, function () {
form.destroy();
});

QUnit.test('search more in many2one: no text in input', async function (assert) {
// when the user clicks on 'Search More...' in a many2one dropdown, and there is no text
// in the input (i.e. no value to search on), we bypass the name_search that is meant to
// return a list of preselected ids to filter on in the list view (opened in a dialog)
assert.expect(6);

for (var i = 0; i < 8; i++) {
this.data.partner.records.push({id: 100 + i, display_name: 'test_' + i});
}

var form = createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form><field name="trululu"/></form>',
archs: {
'partner,false,list': '<list><field name="display_name"/></list>',
'partner,false,search': '<search></search>',
},
mockRPC: function (route, args) {
assert.step(args.method || route);
if (route === '/web/dataset/search_read') {
assert.deepEqual(args.domain, [],
"should not preselect ids as there as nothing in the m2o input");
}
return this._super.apply(this, arguments);
},
});

await testUtils.fields.many2one.searchAndClickItem('trululu', {
item: 'Search More',
search: '',
});

assert.verifySteps([
'default_get',
'name_search', // to display results in the dropdown
'load_views', // list view in dialog
'/web/dataset/search_read', // to display results in the dialog
]);

form.destroy();
});

QUnit.test('search more in many2one: text in input', async function (assert) {
// when the user clicks on 'Search More...' in a many2one dropdown, and there is some
// text in the input, we perform a name_search to get a (limited) list of preselected
// ids and we add a dynamic filter (with those ids) to the search view in the dialog, so
// that the user can remove this filter to bypass the limit
assert.expect(12);

for (var i = 0; i < 8; i++) {
this.data.partner.records.push({id: 100 + i, display_name: 'test_' + i});
}

var expectedDomain;
var form = createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form><field name="trululu"/></form>',
archs: {
'partner,false,list': '<list><field name="display_name"/></list>',
'partner,false,search': '<search></search>',
},
mockRPC: function (route, args) {
assert.step(args.method || route);
if (route === '/web/dataset/search_read') {
assert.deepEqual(args.domain, expectedDomain);
}
return this._super.apply(this, arguments);
},
});

expectedDomain = [['id', 'in', [100, 101, 102, 103, 104, 105, 106, 107]]];
await testUtils.fields.many2one.searchAndClickItem('trululu', {
item: 'Search More',
search: 'test',
});

assert.containsOnce(document.body, '.modal .o_list_view');
assert.containsOnce(document.body, '.modal .o_cp_searchview .o_facet_values',
"should have a special facet for the pre-selected ids");

// remove the filter on ids
expectedDomain = [];
testUtils.dom.click($('.modal .o_cp_searchview .o_facet_remove'));

assert.verifySteps([
'default_get',
'name_search', // empty search, triggered when the user clicks in the input
'name_search', // to display results in the dropdown
'name_search', // to get preselected ids matching the search
'load_views', // list view in dialog
'/web/dataset/search_read', // to display results in the dialog
'/web/dataset/search_read', // after removal of dynamic filter
]);

form.destroy();
});

QUnit.test('updating a many2one from a many2many', function (assert) {
assert.expect(4);

Expand Down
4 changes: 2 additions & 2 deletions addons/web/static/tests/helpers/mock_server.js
Original file line number Diff line number Diff line change
Expand Up @@ -582,8 +582,8 @@ var MockServer = Class.extend({
var result = _.map(records, function (record) {
return [record.id, record.display_name];
});
if (args.limit) {
return result.slice(0, args.limit);
if (_kwargs.limit) {
return result.slice(0, _kwargs.limit);
}
return result;
},
Expand Down
11 changes: 8 additions & 3 deletions addons/web/static/tests/helpers/test_utils_fields.js
Original file line number Diff line number Diff line change
Expand Up @@ -125,15 +125,16 @@ function clickM2OItem(fieldName, searchText) {
* - click to open the dropdown
* - enter a search string in the input
* - wait for the selection
* - click on the active menuitem
* - click on the requested menuitem, or the active one by default
*
* Example:
* testUtils.fields.many2one.searchAndClickM2OItem('partner_id', 'George');
* testUtils.fields.many2one.searchAndClickM2OItem('partner_id', {search: 'George'});
*
* @param {string} fieldName
* @param {[Object]} options
* @param {[string]} options.selector
* @param {[string]} options.search
* @param {[string]} options.item
* @returns {Promise}
*/
function searchAndClickM2OItem(fieldName, options) {
Expand All @@ -156,7 +157,11 @@ function searchAndClickM2OItem(fieldName, options) {
}

return $.when(def).then(function () {
clickM2OHighlightedItem(fieldName, options.selector);
if (options.item) {
clickM2OItem(fieldName, options.item);
} else {
clickM2OHighlightedItem(fieldName, options.selector);
}
});
}
return {
Expand Down

0 comments on commit 6a8e1b5

Please sign in to comment.