diff options
| author | Mathieu Duckerts-Antoine <dam@odoo.com> | 2020-04-28 12:43:30 +0000 |
|---|---|---|
| committer | Mathieu Duckerts-Antoine <dam@odoo.com> | 2020-05-05 19:23:20 +0000 |
| commit | 2c102c292f22f5d400d8caf779428fdc55c15e43 (patch) | |
| tree | 6436980060455bd35619672d5e0ba693551e3902 /addons/web/static/src | |
| parent | c19dcf3983c797d82cd808d9701e190aee619af2 (diff) | |
[IMP] web: search panel counter changes
This commit introduces several changes in the search panel with
respect to record counts:
- the record counts are now also available for
the fields with select="one" attribute
(if not disabled explicitely).
- the record counts are better computed using the idea that
selected values within a group should not impact the counts
for the group values but only the counts for the other
group values.
On the way we have changed two keys in the values returned
by the server:
- 'count' becomes '__count'.
It has been done to avoid a possible clash in case a model would
have a field named 'count' and that the field values would be
wanted for some reason.
- 'name' (multi case) becomes 'display_name'.
It has been done in order to make the select one and multi cases
more similar and factorize some code.
TASK-ID: 2166814
Co-authored-by: Raphaël Collet <rco@openerp.com>
Co-authored-by: Mathieu Duckerts-Antoine <dam@odoo.com>
Co-authored-by: Alexis Lacroix <laa@odoo.com>
Co-authored-by: Julien Mougenot <jum@odoo.com>
Diffstat (limited to 'addons/web/static/src')
| -rw-r--r-- | addons/web/static/src/js/views/search_panel.js | 170 | ||||
| -rw-r--r-- | addons/web/static/src/scss/search_panel.scss | 32 | ||||
| -rw-r--r-- | addons/web/static/src/xml/kanban.xml | 28 |
3 files changed, 173 insertions, 57 deletions
diff --git a/addons/web/static/src/js/views/search_panel.js b/addons/web/static/src/js/views/search_panel.js index a820e46f529..ef1fd05e4fb 100644 --- a/addons/web/static/src/js/views/search_panel.js +++ b/addons/web/static/src/js/views/search_panel.js @@ -44,6 +44,7 @@ function _processSearchPanelNode(node, fields) { const section = { color: childNode.attrs.color, description: childNode.attrs.string || fields[fieldName].string, + disableCounters: !!pyUtils.py_eval(childNode.attrs.disable_counters || '0'), fieldName: fieldName, icon: childNode.attrs.icon, id: sectionId, @@ -53,7 +54,6 @@ function _processSearchPanelNode(node, fields) { if (section.type === 'category') { section.icon = section.icon || 'fa-folder'; } else if (section.type === 'filter') { - section.disableCounters = !!pyUtils.py_eval(childNode.attrs.disable_counters || '0'); section.domain = childNode.attrs.domain || '[]'; section.groupBy = childNode.attrs.groupby; section.icon = section.icon || 'fa-filter'; @@ -110,6 +110,7 @@ const SearchPanel = Widget.extend({ this.fields = params.fields; this.model = params.model; this.className = params.classes.concat(['o_search_panel']).join(' '); + this.reloadOptions = null; this.searchDomain = params.searchDomain; this.viewDomain = params.viewDomain; }, @@ -122,7 +123,7 @@ const SearchPanel = Widget.extend({ this.filters = this.initialState.filters; this.categories = this.initialState.categories; } else { - await this._fetchCategories(); + await this._fetchCategories(true); await this._fetchFilters(); await this._applyDefaultFilterValues(); } @@ -221,8 +222,8 @@ const SearchPanel = Widget.extend({ this._render(); }, /** - * Reload the filters and re-render. Note that we only reload the filters if - * the controlPanel domain or searchPanel domain has changed. + * Reload the filters and categories and re-render. Note that we only reload them + * if the controlPanel domain or searchPanel domain has changed. * * @param {Object} params * @param {Array[]} params.searchDomain domain coming from controlPanel @@ -232,11 +233,14 @@ const SearchPanel = Widget.extend({ async update(params) { const currentDomain = JSON.stringify([...this.searchDomain, ...this.viewDomain]); const newDomain = JSON.stringify([...params.searchDomain, ...params.viewDomain]); - if (this.needReload || (currentDomain !== newDomain)) { - this.needReload = false; + if (this.reloadOptions || (currentDomain !== newDomain)) { this.searchDomain = params.searchDomain; this.viewDomain = params.viewDomain; + if (!this.reloadOptions || this.reloadOptions.shouldFetchCategories) { + await this._fetchCategories(); + } await this._fetchFilters(); + this.reloadOptions = null; } return this._render(); }, @@ -277,13 +281,16 @@ const SearchPanel = Widget.extend({ parentField = false; } } + const unfoldedIds = Object.values(category.values || {}) + .filter(c => c.folded === false) + .map(c => c.id); category.values = {}; values.forEach(value => { category.values[value.id] = Object.assign({}, value, { childrenIds: [], - folded: true, - parentId: value[parentField] && value[parentField][0] || false, + folded: !unfoldedIds.includes(value.id), + parentId: value[parentField] || false, }); }); values.forEach(value => { @@ -374,35 +381,35 @@ const SearchPanel = Widget.extend({ } }, /** - * Fetch values for each category. This is done only once, at startup. + * Fetch values for each category at startup. At reload a category is only + * fetched if the searchDomain changes and displayCounters is true for it. * * @private + * @param {boolean} [force] * @returns {Promise} resolved when all categories have been fetched */ - _fetchCategories() { + _fetchCategories(force) { const proms = []; - let prom; for (const category of Object.values(this.categories)) { const field = this.fields[category.fieldName]; - if (field.type === 'selection') { - const values = field.selection.map(value => { - return { id: value[0], display_name: value[1] }; - }); - prom = Promise.resolve(values); - } else { - prom = this._rpc({ + if (force || !category.disableCounters) { + const prom = this._rpc({ method: 'search_panel_select_range', model: this.model, args: [category.fieldName], - }).then(result => { - category.parentField = result.parent_field; - return result.values; + kwargs: { + category_domain: this._getCategoryDomain(category.id), + disable_counters: category.disableCounters, + search_domain: this.searchDomain, + }, + }).then(({ parent_field, values }) => { + if (field.type === 'many2one') { + category.parentField = parent_field; + } + this._createCategoryTree(category.id, values); }); + proms.push(prom); } - prom.then(values => { - this._createCategoryTree(category.id, values); - }); - proms.push(prom); } return Promise.all(proms); }, @@ -419,7 +426,6 @@ const SearchPanel = Widget.extend({ evalContext[category.fieldName] = category.activeValueId; } const categoryDomain = this._getCategoryDomain(); - const filterDomain = this._getFilterDomain(); const proms = []; for (const filter of Object.values(this.filters)) { const prom = this._rpc({ @@ -430,8 +436,9 @@ const SearchPanel = Widget.extend({ category_domain: categoryDomain, comodel_domain: Domain.prototype.stringToArray(filter.domain, evalContext), disable_counters: filter.disableCounters, - filter_domain: filterDomain, + filter_domain: this._getFilterDomain(filter.id), group_by: filter.groupBy || false, + group_domain: this._getGroupDomain(filter), search_domain: [...this.searchDomain, ...this.viewDomain], }, }).then(values => { this._createFilterTree(filter.id, values); }); @@ -443,7 +450,7 @@ const SearchPanel = Widget.extend({ * @private * @param {Object} category * @param {Array} validValues - * @returns id of the default item of the category or false + * @returns {(number|boolean)} id of the default item of the category or false */ _getCategoryDefaultValue(category, validValues) { // set active value from context @@ -460,11 +467,15 @@ const SearchPanel = Widget.extend({ * Compute and return the domain based on the current active categories. * * @private + * @param {string} [excludedCategoryId] * @returns {Array[]} */ - _getCategoryDomain() { + _getCategoryDomain(excludedCategoryId) { const domain = []; for (const category of Object.values(this.categories)) { + if (category.id === excludedCategoryId) { + continue; + } if (category.activeValueId) { const field = this.fields[category.fieldName]; const op = (field.type === 'many2one' && category.parentField) ? 'child_of' : '='; @@ -482,10 +493,10 @@ const SearchPanel = Widget.extend({ * a group (and grouped using an 'OR'). * * @private - * @param {string} filterId + * @param {string} [excludedFilterId] * @returns {Array[]} */ - _getFilterDomain(filterId) { + _getFilterDomain(excludedFilterId) { const domain = []; function addCondition(fieldName, checkedValues) { @@ -496,7 +507,7 @@ const SearchPanel = Widget.extend({ } for (const filter of Object.values(this.filters)) { - if (filter.id === filterId) { + if (filter.id === excludedFilterId) { continue; } const { fieldName } = filter; @@ -514,6 +525,81 @@ const SearchPanel = Widget.extend({ return domain; }, /** + * Returns a domain or an object of domains used to complement + * the filter domains to accurately describe the constraints on + * records when computing record counts associated with the filter + * values (if a groupBy is provided). The idea is that the checked values + * within a group should not impact the counts for the other values + * in the same group. + * + * @private + * @param {string} filter + * @returns {(Array{}|Array[]|undefined)} + */ + _getGroupDomain(filter) { + const { fieldName, groups, disableCounters } = filter; + const { type: fieldType } = this.fields[fieldName]; + + if (disableCounters || !groups) { + switch (fieldType) { + case 'many2one': return []; + case 'many2many': return {}; + default: return; + } + } + + let groupDomain; + if (fieldType === 'many2one') { + for (const group of Object.values(groups)) { + const valueIds = []; + let active = false; + for (const value of Object.values(group.values || {})) { + const { id, checked } = value; + valueIds.push(id); + if (checked) { + active = true; + } + } + if (active) { + if (groupDomain) { + groupDomain = Domain.FALSE_DOMAIN; + break; + } else { + groupDomain = [[fieldName, 'in', valueIds]]; + } + } + } + } else if (fieldType === 'many2many') { + const checkedValueIds = {}; + for (const group of Object.values(groups)) { + for (const value of Object.values(group.values || {})) { + const { id, checked } = value; + if (checked) { + if (!checkedValueIds[group.id]) { + checkedValueIds[group.id] = []; + } + checkedValueIds[group.id].push(id); + } + } + } + groupDomain = {}; + for (const gId in checkedValueIds) { + const ids = checkedValueIds[gId]; + for (const group of Object.values(groups)) { + if (gId !== group.id) { + if (!groupDomain[group.id]) { + groupDomain[group.id] = []; + } + groupDomain[group.id].push( + [fieldName, 'in', ids] + ); + } + } + } + } + return groupDomain; + }, + /** * The active id of each category is stored in the localStorage, s.t. it * can be restored afterwards (when the action is reloaded, for instance). * This function returns the key in the sessionStorage for a given category. @@ -527,8 +613,8 @@ const SearchPanel = Widget.extend({ /** * @private * @param {Object} category - * @param {integer} categoryValueId - * @returns {integer[]} list of ids of the ancestors of the given value in + * @param {number} categoryValueId + * @returns {number[]} list of ids of the ancestors of the given value in * the given category */ _getAncestorValueIds(category, categoryValueId) { @@ -547,9 +633,10 @@ const SearchPanel = Widget.extend({ * the searchPanel and the reloading of the data. * * @private + * @param {Object} [reloadOptions={}] */ - _notifyDomainUpdated() { - this.needReload = true; + _notifyDomainUpdated(reloadOptions = {}) { + this.reloadOptions = reloadOptions; this.trigger_up('search_panel_domain_updated', { domain: this.getDomain(), }); @@ -638,13 +725,18 @@ const SearchPanel = Widget.extend({ const valueId = !isNaN(item.dataset.id) ? Number(item.dataset.id) : item.dataset.id || false; + const hasChanged = category.activeValueId !== valueId; category.activeValueId = valueId; if (category.values[valueId]) { category.values[valueId].folded = !category.values[valueId].folded; } - const storageKey = this._getLocalStorageKey(category); - this.call('local_storage', 'setItem', storageKey, valueId); - this._notifyDomainUpdated(); + if (hasChanged) { + const storageKey = this._getLocalStorageKey(category); + this.call('local_storage', 'setItem', storageKey, valueId); + this._notifyDomainUpdated({ shouldFetchCategories: true }); + } else { + this._render(); + } }, /** * @private diff --git a/addons/web/static/src/scss/search_panel.scss b/addons/web/static/src/scss/search_panel.scss index 0c7b2266a25..ac7f05d3169 100644 --- a/addons/web/static/src/scss/search_panel.scss +++ b/addons/web/static/src/scss/search_panel.scss @@ -32,13 +32,19 @@ $o-searchpanel-filter-default-color: #D59244; } .o_search_panel_label { - display: flex; + align-items: center; cursor: pointer; + display: flex; + justify-content: space-between; user-select: none; + // Without counters: the caret is set at the right border + &:not(.o_with_counters) { + width: 100%; + } + .o_toggle_fold { - margin-left: auto; - padding: 3px; + width: 2rem; } } .o_search_panel_section_header { @@ -64,14 +70,17 @@ $o-searchpanel-filter-default-color: #D59244; } .o_search_panel_category_value { cursor: pointer; + header { + align-items: center; + display: flex; + justify-content: space-between; margin-top: -4px; margin-bottom: -4px; - padding: 4px 7px; - - .o_toggle_fold { - padding-left: 11px; - } + padding: 4px 6px; + } + .o_search_panel_label.o_with_counters { + overflow: hidden; } .o_search_panel_category_value { position: relative; @@ -99,5 +108,12 @@ $o-searchpanel-filter-default-color: #D59244; } } } + .o_search_panel_filter_group { + padding-right: 6px; + + .o_search_panel_label.o_with_counters { + justify-content: flex-start; + } + } } } diff --git a/addons/web/static/src/xml/kanban.xml b/addons/web/static/src/xml/kanban.xml index cb34cd16851..970f9fd9825 100644 --- a/addons/web/static/src/xml/kanban.xml +++ b/addons/web/static/src/xml/kanban.xml @@ -245,10 +245,14 @@ <t t-set="value" t-value="category.values[valueId]"/> <li class="o_search_panel_category_value border-0 list-group-item" t-att-data-id="value.id" t-att-data-category-id="category.id"> <header t-att-class="'list-group-item-action' + (value.id === category.activeValueId ? ' active' : '')"> - <label t-att-for="value.display_name" class="o_search_panel_label mb0"> + <label t-att-for="value.display_name" t-attf-class="o_search_panel_label mb0{{ category.disableCounters ? '' : ' o_with_counters' }}"> <span class="o_search_panel_label_title"><t t-esc="value.display_name"/></span> - <i t-if="value.childrenIds.length" t-attf-class="fa fa-fw o_toggle_fold {{value.folded ? 'fa-caret-left' : 'fa-caret-down'}}"/> + <i t-if="value.childrenIds.length" t-attf-class="o_toggle_fold fa fa-fw fa-caret-{{ value.folded ? 'right' : 'down' }}"/> </label> + <span t-if="!category.disableCounters and value.__count > 0" + class="text-muted ml-2 small" + t-esc="value.__count" + /> </header> <ul t-if="!value.folded" class="list-group d-block"> <t t-call="SearchPanel.CategoryValues"> @@ -285,10 +289,12 @@ <div class="custom-control custom-checkbox"> <t t-set="inputId" t-value="_.uniqueId('input_')"/> <input type="checkbox" t-att-id="inputId" t-att-checked="group.state === 'checked' ? 'checked' : undefined" class="custom-control-input"/> - <label t-att-for="inputId" class="o_search_panel_label custom-control-label d-block" t-att-title="group.tooltip"> - <i t-att-class="'fa fa-fw pull-right o_toggle_fold ' + (group.folded ? 'fa-caret-left' : 'fa-caret-down')"/> - <span t-if="group.hex_color" t-att-style="'color: ' + group.hex_color + ';'">●</span> - <span class="o_search_panel_label_title"><t t-esc="group.name"/></span> + <label t-att-for="inputId" t-attf-class="o_search_panel_label custom-control-label{{ group.disableCounters ? '' : ' o_with_counters' }}" t-att-title="group.tooltip"> + <span class="o_search_panel_label_title"> + <span t-if="group.hex_color" t-attf-style="color: {{ group.hex_color }};">●</span> + <t t-esc="group.name"/> + </span> + <i t-attf-class="o_toggle_fold fa fa-fw fa-caret-{{ group.folded ? 'right' : 'down' }}"/> </label> </div> <ul t-if="!group.folded" class="list-group d-block"> @@ -313,10 +319,12 @@ <div class="custom-control custom-checkbox"> <t t-set="inputId" t-value="_.uniqueId('input_')"/> <input type="checkbox" t-att-id="inputId" t-att-checked="value.checked ? 'checked' : undefined" class="custom-control-input"/> - <label t-att-for="inputId" class="o_search_panel_label custom-control-label d-block" t-att-title="group && group.tooltip"> - <span class="o_search_panel_label_title"><t t-esc="value.name"/></span> - <span t-if="value.count > 0" class="pull-right text-muted mr-2 mt-1 small"><t t-esc="value.count"/></span> - <span t-if="filter.disableCounters" class="pull-right text-muted mr-2 mt-1 small">?</span> + <label t-att-for="inputId" class="o_search_panel_label custom-control-label" t-att-title="group && group.tooltip"> + <span class="o_search_panel_label_title" t-esc="value.display_name"/> + <span t-if="!filter.disableCounters and value.__count > 0" + class="text-muted ml-2 small" + t-esc="value.__count" + /> </label> </div> </li> |
