summaryrefslogtreecommitdiff
path: root/addons/web/static
diff options
context:
space:
mode:
authorMathieu Duckerts-Antoine <dam@odoo.com>2020-05-08 08:57:44 +0000
committerJulien Mougenot <jum@odoo.com>2020-06-12 13:34:04 +0000
commit3306a0f41492ee51e93ce9cedcd42296e09df6a0 (patch)
tree6864dfb7530b63e451b43c791127c9bdfbeade36 /addons/web/static
parente5585c078ef936c3533ba9361c05188fb5e3ab1d (diff)
[IMP] *: limit in search panel
This commit introduces a new attribute 'limit' for search panel fields that allows to avoid performance issues. That integer attribute (with default 200) allows to fix a maximal number of values to display for the fields. When the number of field values to display reaches the limit, no values will be displayed. Instead, a warning message will be shown in the corresponding search panel section. Note it is possible to have no limit using limit="0" on a field. This commit reintroduces in a better way the principle brought by the fix 8d57153b34c04952a85f6642951cb2697016da84. Task ID: 2154749
Diffstat (limited to 'addons/web/static')
-rw-r--r--addons/web/static/src/js/views/search_panel.js73
-rw-r--r--addons/web/static/src/xml/kanban.xml10
-rw-r--r--addons/web/static/tests/helpers/mock_server.js88
-rw-r--r--addons/web/static/tests/views/search_panel_tests.js77
4 files changed, 196 insertions, 52 deletions
diff --git a/addons/web/static/src/js/views/search_panel.js b/addons/web/static/src/js/views/search_panel.js
index 7aaeb99b648..81698a46c56 100644
--- a/addons/web/static/src/js/views/search_panel.js
+++ b/addons/web/static/src/js/views/search_panel.js
@@ -21,6 +21,8 @@ const qweb = core.qweb;
const defaultViewTypes = ['kanban', 'tree'];
let nextSectionId = 1;
+const SEARCH_PANEL_DEFAULT_LIMIT = 200;
+
/**
* Given a <searchpanel> arch node, iterate over its children to generate the
* description of each section (being either a category or a filter).
@@ -31,31 +33,34 @@ let nextSectionId = 1;
*/
function _processSearchPanelNode(node, fields) {
const sections = {};
- node.children.forEach((childNode, index) => {
- if (childNode.tag !== 'field' || childNode.attrs.invisible === "1") {
+ node.children.forEach(({ attrs, tag }, index) => {
+ if (tag !== 'field' || attrs.invisible === "1") {
return;
}
- const fieldName = childNode.attrs.name;
- const type = childNode.attrs.select === 'multi' ? 'filter' : 'category';
+ const fieldName = attrs.name;
+ const type = attrs.select === 'multi' ? 'filter' : 'category';
const sectionId = `section_${nextSectionId++}`;
const section = {
- color: childNode.attrs.color,
- description: childNode.attrs.string || fields[fieldName].string,
- enableCounters: !!pyUtils.py_eval(childNode.attrs.enable_counters || '0'),
- expand: !!pyUtils.py_eval(childNode.attrs.expand || '0'),
+ color: attrs.color,
+ description: attrs.string || fields[fieldName].string,
+ enableCounters: !!pyUtils.py_eval(attrs.enable_counters || '0'),
+ expand: !!pyUtils.py_eval(attrs.expand || '0'),
fieldName,
- icon: childNode.attrs.icon,
+ icon: attrs.icon,
id: sectionId,
index,
+ limit: attrs.limit ?
+ pyUtils.py_eval(attrs.limit) :
+ SEARCH_PANEL_DEFAULT_LIMIT,
type,
};
if (section.type === 'category') {
section.icon = section.icon || 'fa-folder';
- section.hierarchize = !!pyUtils.py_eval(childNode.attrs.hierarchize || '1');
+ section.hierarchize = !!pyUtils.py_eval(attrs.hierarchize || '1');
} else if (section.type === 'filter') {
- section.domain = childNode.attrs.domain || '[]';
- section.groupBy = childNode.attrs.groupby;
+ section.domain = attrs.domain || '[]';
+ section.groupBy = attrs.groupby;
section.icon = section.icon || 'fa-filter';
}
sections[sectionId] = section;
@@ -267,11 +272,18 @@ const SearchPanel = Widget.extend({
/**
* @private
* @param {string} categoryId
- * @param {Object[]} values
+ * @param {Object} result
*/
- _createCategoryTree(categoryId, values) {
+ _createCategoryTree(categoryId, result) {
const category = this.categories[categoryId];
- const parentField = category.parentField;
+ let { error_msg, parent_field: parentField, values } = result;
+ if (error_msg) {
+ category.errorMsg = error_msg;
+ values = [];
+ }
+ if (category.hierarchize) {
+ category.parentField = parentField;
+ }
const unfoldedIds = Object.values(category.values || {})
.filter(c => c.folded === false)
@@ -315,10 +327,15 @@ const SearchPanel = Widget.extend({
/**
* @private
* @param {string} filterId
- * @param {Object[]} values
+ * @param {Object} result
*/
- _createFilterTree(filterId, values) {
+ _createFilterTree(filterId, result) {
const filter = this.filters[filterId];
+ let { error_msg, values } = result;
+ if (error_msg) {
+ filter.errorMsg = error_msg;
+ values = [];
+ }
// restore checked property
values.forEach(value => {
@@ -379,8 +396,7 @@ const SearchPanel = Widget.extend({
_fetchCategories(force) {
const proms = [];
for (const category of Object.values(this.categories)) {
- const { enableCounters, expand, hierarchize } = category;
- const field = this.fields[category.fieldName];
+ const { enableCounters, expand, hierarchize, limit } = category;
if (force || enableCounters || !expand) {
const prom = this._rpc({
method: 'search_panel_select_range',
@@ -391,14 +407,12 @@ const SearchPanel = Widget.extend({
enable_counters: enableCounters,
expand,
hierarchize,
+ limit,
search_domain: this.searchDomain,
},
- }).then(({ parent_field, values }) => {
- if (field.type === 'many2one' && hierarchize) {
- category.parentField = parent_field;
- }
- this._createCategoryTree(category.id, values);
- });
+ }).then(
+ (result) => this._createCategoryTree(category.id, result)
+ );
proms.push(prom);
}
}
@@ -419,7 +433,7 @@ const SearchPanel = Widget.extend({
const categoryDomain = this._getCategoryDomain();
const proms = [];
for (const filter of Object.values(this.filters)) {
- const { enableCounters, expand, groupBy } = filter;
+ const { enableCounters, expand, groupBy, limit } = filter;
const prom = this._rpc({
method: 'search_panel_select_multi_range',
model: this.model,
@@ -432,9 +446,12 @@ const SearchPanel = Widget.extend({
expand,
group_by: groupBy || false,
group_domain: this._getGroupDomain(filter),
+ limit,
search_domain: [...this.searchDomain, ...this.viewDomain],
},
- }).then(values => { this._createFilterTree(filter.id, values); });
+ }).then(
+ (result) => this._createFilterTree(filter.id, result)
+ );
proms.push(prom);
}
return Promise.all(proms);
@@ -646,7 +663,7 @@ const SearchPanel = Widget.extend({
const sections = categories.concat(filters).sort((s1, s2) => s1.index - s2.index);
sections.forEach(section => {
- if (Object.keys(section.values).length) {
+ if (Object.keys(section.values).length || section.errorMsg) {
if (section.type === 'category') {
this.$el.append(this._renderCategory(section));
} else {
diff --git a/addons/web/static/src/xml/kanban.xml b/addons/web/static/src/xml/kanban.xml
index 114f0ccd876..f4657d4b3a3 100644
--- a/addons/web/static/src/xml/kanban.xml
+++ b/addons/web/static/src/xml/kanban.xml
@@ -222,7 +222,10 @@
<t t-call="SearchPanel.SectionHeader">
<t t-set="section" t-value="category"/>
</t>
- <ul class="list-group d-block o_search_panel_field" t-att-data-field-name="category.fieldName">
+ <div t-if="category.errorMsg" class="alert alert-warning">
+ <span><t t-esc="category.errorMsg"/></span>
+ </div>
+ <ul t-else="" class="list-group d-block o_search_panel_field" t-att-data-field-name="category.fieldName">
<li class="o_search_panel_category_value border-0 list-group-item" t-att-data-category-id="category.id">
<header t-att-class="'list-group-item-action' + (!category.activeValueId ? ' active' : '')">
<label class="o_search_panel_label mb0">
@@ -267,7 +270,10 @@
<t t-call="SearchPanel.SectionHeader">
<t t-set="section" t-value="filter"/>
</t>
- <ul class="list-group d-block o_search_panel_field" t-att-data-field-name="filter.fieldName">
+ <div t-if="filter.errorMsg" class="alert alert-warning">
+ <span><t t-esc="filter.errorMsg"/></span>
+ </div>
+ <ul t-else="" class="list-group d-block o_search_panel_field" t-att-data-field-name="filter.fieldName">
<t t-if="filter.groups" t-call="SearchPanel.FilterGroups">
<t t-set="groups" t-value="filter.groups"/>
</t>
diff --git a/addons/web/static/tests/helpers/mock_server.js b/addons/web/static/tests/helpers/mock_server.js
index 575752a8698..451743c8318 100644
--- a/addons/web/static/tests/helpers/mock_server.js
+++ b/addons/web/static/tests/helpers/mock_server.js
@@ -539,15 +539,16 @@ var MockServer = Class.extend({
return modelFields;
},
/**
- * Simulate a call to the server '_search_panel_domain_image' method.
+ * Simulates a call to the server '_search_panel_domain_image' method.
*
+ * @private
* @param {string} model
* @param {Array[]} domain
* @param {string} fieldName
* @param {boolean} setCount
* @returns {Map}
*/
- _mockSearchPanelDomainImage: function (model, fieldName, domain, setCount=false) {
+ _mockSearchPanelDomainImage: function (model, fieldName, domain, setCount=false, limit=false) {
const field = this.data[model].fields[fieldName];
let groupIdName;
if (field.type === 'many2one') {
@@ -569,6 +570,7 @@ var MockServer = Class.extend({
domain,
fields: [fieldName],
groupby: [fieldName],
+ limit,
});
const domainImage = new Map();
for (const group of groups) {
@@ -582,8 +584,9 @@ var MockServer = Class.extend({
return domainImage;
},
/**
- * Simulate a call to the server '_search_panel_global_counters' method.
+ * Simulates a call to the server '_search_panel_global_counters' method.
*
+ * @private
* @param {Map} valuesRange
* @param {(string|boolean)} parentName 'parent_id' or false
*/
@@ -602,8 +605,9 @@ var MockServer = Class.extend({
}
},
/**
- * Simulate a call to the server '_search_panel_sanitized_parent_hierarchy' method.
+ * Simulates a call to the server '_search_panel_sanitized_parent_hierarchy' method.
*
+ * @private
* @param {Object[]} records
* @param {(string|boolean)} parentName 'parent_id' or false
* @param {number[]} ids
@@ -641,8 +645,9 @@ var MockServer = Class.extend({
return records.filter(rec => recordsToKeep[rec.id]);
},
/**
- * Simulate a call to the server 'search_panel_selection_range' method.
+ * Simulates a call to the server 'search_panel_selection_range' method.
*
+ * @private
* @param {string} model
* @param {string} fieldName
* @param {Object} kwargs
@@ -677,8 +682,9 @@ var MockServer = Class.extend({
return selectionRange;
},
/**
- * Simulate a call to the server 'search_panel_select_range' method.
+ * Simulates a call to the server 'search_panel_select_range' method.
*
+ * @private
* @param {string} model
* @param {string[]} args
* @param {string} args[fieldName]
@@ -688,6 +694,7 @@ var MockServer = Class.extend({
* @param {Array[]} [kwargs.comodel_domain] domain of field values (if relational)
* (this parameter is used in _search_panel_range)
* @param {boolean} [kwargs.enable_counters] whether to count records by value
+ * @param {integer} [kwargs.limit] maximal number of values to fetch
* @param {Array[]} [kwargs.search_domain] base domain of search (this parameter
* is used in _search_panel_range)
* @returns {Object}
@@ -710,12 +717,7 @@ var MockServer = Class.extend({
values: this._mockSearchPanelSelectionRange(model, fieldName, kwargs),
};
}
- const enableCounters = kwargs.enable_counters;
- const expand = kwargs.expand;
- let domainImage = new Map();
- if (enableCounters || !expand) {
- domainImage = this._mockSearchPanelDomainImage(model, fieldName, modelDomain, enableCounters);
- }
+
const fieldNames = ['display_name'];
let hierarchize = 'hierarchize' in kwargs ? kwargs.hierarchize : true;
let getParentId;
@@ -728,7 +730,20 @@ var MockServer = Class.extend({
hierarchize = false;
}
let comodelDomain = kwargs.comodel_domain || [];
+ const enableCounters = kwargs.enable_counters;
+ const expand = kwargs.expand;
+ const limit = kwargs.limit;
+ let domainImage = new Map();
+ if (enableCounters || !expand) {
+ domainImage = this._mockSearchPanelDomainImage(
+ model, fieldName, modelDomain, enableCounters,
+ !expand && !hierarchize && !comodelDomain.length ? limit : false
+ );
+ }
if (!expand && !hierarchize && !comodelDomain.length) {
+ if (limit && domainImage.size === limit) {
+ return { error_msg: "Too many items to display." };
+ }
return {
parent_field: parentName,
values: [...domainImage.values()],
@@ -759,11 +774,16 @@ var MockServer = Class.extend({
condition,
]);
}
- let comodelRecords = this._mockSearchRead(field.relation, [comodelDomain, fieldNames], {});
+ let comodelRecords = this._mockSearchRead(field.relation, [comodelDomain, fieldNames], { limit });
+
if (hierarchize) {
const ids = expand ? comodelRecords.map(rec => rec.id) : imageElementIds;
comodelRecords = this._mockSearchPanelSanitizedParentHierarchy(comodelRecords, parentName, ids);
}
+
+ if (limit && comodelRecords.length === limit) {
+ return { error_msg: "Too many items to display." };
+ }
// A map is used to keep the initial order.
const fieldRange = new Map();
for (const record of comodelRecords) {
@@ -790,8 +810,9 @@ var MockServer = Class.extend({
};
},
/**
- * Simulate a call to the server 'search_panel_select_multi_range' method.
+ * Simulates a call to the server 'search_panel_select_multi_range' method.
*
+ * @private
* @param {string} model
* @param {string[]} args
* @param {string} args[fieldName]
@@ -806,8 +827,9 @@ var MockServer = Class.extend({
* @param {Array[]} [kwargs.group_domain] dict, one domain for each activated
* group for the group_by (if any). Those domains are used to fech accurate
* counters for values in each group
+ * @param {integer} [kwargs.limit] maximal number of values to fetch
* @param {Array[]} [kwargs.search_domain] base domain of search
- * @returns {Object[]}
+ * @returns {Object}
*/
_mockSearchPanelSelectMultiRange: function (model, [fieldName], kwargs) {
const field = this.data[model].fields[fieldName];
@@ -823,7 +845,9 @@ var MockServer = Class.extend({
]);
if (field.type === 'selection') {
kwargs.model_domain = modelDomain;
- return this._mockSearchPanelSelectionRange(model, fieldName, kwargs);
+ return {
+ values: this._mockSearchPanelSelectionRange(model, fieldName, kwargs),
+ };
}
const fieldNames = ['display_name'];
const groupBy = kwargs.group_by;
@@ -844,8 +868,13 @@ var MockServer = Class.extend({
let comodelDomain = kwargs.comodel_domain || [];
const enableCounters = kwargs.enable_counters;
const expand = kwargs.expand;
+ const limit = kwargs.limit;
if (field.type === 'many2many') {
- const comodelRecords = this._mockSearchRead(field.relation, [comodelDomain, fieldNames], {});
+ const comodelRecords = this._mockSearchRead(field.relation, [comodelDomain, fieldNames], { limit });
+ if (expand && limit && comodelRecords.length === limit) {
+ return { error_msg: "Too many items to display." };
+ }
+
const groupDomain = kwargs.group_domain;
const fieldRange = [];
for (const record of comodelRecords) {
@@ -881,7 +910,12 @@ var MockServer = Class.extend({
fieldRange.push(values);
}
}
- return fieldRange;
+
+ if (!expand && limit && fieldRange.length === limit) {
+ return { error_msg: "Too many items to display." };
+ }
+
+ return { values: fieldRange };
}
if (field.type === 'many2one') {
@@ -891,10 +925,16 @@ var MockServer = Class.extend({
...modelDomain,
...(kwargs.group_domain || []),
]);
- domainImage = this._mockSearchPanelDomainImage(model, fieldName, modelDomain, enableCounters);
+ domainImage = this._mockSearchPanelDomainImage(
+ model, fieldName, modelDomain, enableCounters,
+ !expand && !groupBy && !comodelDomain.length ? limit : false,
+ );
}
if (!expand && !groupBy && !comodelDomain.length) {
- return [...domainImage.values()];
+ if (limit && domainImage.size === limit) {
+ return { error_msg: "Too many items to display." };
+ }
+ return { values: [...domainImage.values()] };
}
if (!expand) {
const imageElementIds = [...domainImage.keys()].map(Number);
@@ -903,7 +943,11 @@ var MockServer = Class.extend({
['id', 'in', imageElementIds],
]);
}
- const comodelRecords = this._mockSearchRead(field.relation, [comodelDomain, fieldNames], {});
+ const comodelRecords = this._mockSearchRead(field.relation, [comodelDomain, fieldNames], { limit });
+ if (limit && comodelRecords.length === limit) {
+ return { error_msg: "Too many items to display." };
+ }
+
const fieldRange = [];
for (const record of comodelRecords) {
const values= {
@@ -920,7 +964,7 @@ var MockServer = Class.extend({
}
fieldRange.push(values);
}
- return fieldRange;
+ return { values: fieldRange };
}
},
/**
diff --git a/addons/web/static/tests/views/search_panel_tests.js b/addons/web/static/tests/views/search_panel_tests.js
index 065d0ae310d..d2b303b9ec0 100644
--- a/addons/web/static/tests/views/search_panel_tests.js
+++ b/addons/web/static/tests/views/search_panel_tests.js
@@ -1948,6 +1948,7 @@ QUnit.module('Views', {
comodel_domain: [['parent_id', '=', false]],
group_domain: [],
enable_counters: true,
+ limit: 200,
});
}
return result;
@@ -3834,5 +3835,81 @@ QUnit.module('Views', {
kanban.destroy();
});
+
+ QUnit.test("reached limit for a category", async function (assert) {
+ assert.expect(6);
+
+ const kanban = await createView({
+ arch: `
+ <kanban>
+ <templates>
+ <t t-name="kanban-box">
+ <div>
+ <field name="foo"/>
+ </div>
+ </t>
+ </templates>
+ </kanban>`,
+ archs: {
+ 'partner,false,search': `
+ <search>
+ <searchpanel>
+ <field name="company_id" limit="2"/>
+ </searchpanel>
+ </search>`,
+ },
+ data: this.data,
+ model: 'partner',
+ services: this.services,
+ View: KanbanView,
+ });
+
+ assert.containsOnce(kanban, '.o_search_panel_section');
+ assert.containsOnce(kanban, '.o_search_panel_section_header');
+ assert.strictEqual(kanban.el.querySelector('.o_search_panel_section_header').innerText, " COMPANY");
+ assert.containsOnce(kanban, 'section div.alert.alert-warning');
+ assert.strictEqual(kanban.el.querySelector('section div.alert.alert-warning').innerText, "Too many items to display.");
+ assert.containsNone(kanban, '.o_search_panel_category_value');
+
+ kanban.destroy();
+ });
+
+ QUnit.test("reached limit for a filter", async function (assert) {
+ assert.expect(6);
+
+ const kanban = await createView({
+ arch: `
+ <kanban>
+ <templates>
+ <t t-name="kanban-box">
+ <div>
+ <field name="foo"/>
+ </div>
+ </t>
+ </templates>
+ </kanban>`,
+ archs: {
+ 'partner,false,search': `
+ <search>
+ <searchpanel>
+ <field name="company_id" select="multi" limit="2"/>
+ </searchpanel>
+ </search>`,
+ },
+ data: this.data,
+ model: 'partner',
+ services: this.services,
+ View: KanbanView,
+ });
+
+ assert.containsOnce(kanban, '.o_search_panel_section');
+ assert.containsOnce(kanban, '.o_search_panel_section_header');
+ assert.strictEqual(kanban.el.querySelector('.o_search_panel_section_header').innerText, " COMPANY");
+ assert.containsOnce(kanban, 'section div.alert.alert-warning');
+ assert.strictEqual(kanban.el.querySelector('section div.alert.alert-warning').innerText, "Too many items to display.");
+ assert.containsNone(kanban, '.o_search_panel_filter_value');
+
+ kanban.destroy();
+ });
});
});