diff options
| author | Mathieu Duckerts-Antoine <dam@odoo.com> | 2020-05-08 08:57:44 +0000 |
|---|---|---|
| committer | Julien Mougenot <jum@odoo.com> | 2020-06-12 13:34:04 +0000 |
| commit | 3306a0f41492ee51e93ce9cedcd42296e09df6a0 (patch) | |
| tree | 6864dfb7530b63e451b43c791127c9bdfbeade36 /addons/web/static | |
| parent | e5585c078ef936c3533ba9361c05188fb5e3ab1d (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.js | 73 | ||||
| -rw-r--r-- | addons/web/static/src/xml/kanban.xml | 10 | ||||
| -rw-r--r-- | addons/web/static/tests/helpers/mock_server.js | 88 | ||||
| -rw-r--r-- | addons/web/static/tests/views/search_panel_tests.js | 77 |
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(); + }); }); }); |
