diff options
| author | Julien Mougenot <jum@odoo.com> | 2020-03-11 09:38:28 +0000 |
|---|---|---|
| committer | Julien Mougenot <jum@odoo.com> | 2020-03-13 14:08:33 +0000 |
| commit | 18e5416636b19b0953b1e6e26bfaeab1ebecce4c (patch) | |
| tree | 5be75af26f1707a5b8b2184545719f3628d2980f /addons/web/static/src/js/views/graph | |
| parent | fbf347498f1cc7b74ef373179b7bcae201715c24 (diff) | |
[REF] web: Adapt views/actions to new control panel
Some web actions/controllers/viewsl have been adapted to properly instantiate the
newly refactored control panel and update it accordingly. We needed to adapt the
starting mechanism of actions and controllers to ensure that the control panel
instantiation did not delay the loading of any widget.
Part of task 2196029
Co-authored-by: Mathieu Duckerts-Antoine <dam@odoo.com>
Diffstat (limited to 'addons/web/static/src/js/views/graph')
| -rw-r--r-- | addons/web/static/src/js/views/graph/graph_controller.js | 268 | ||||
| -rw-r--r-- | addons/web/static/src/js/views/graph/graph_model.js | 112 | ||||
| -rw-r--r-- | addons/web/static/src/js/views/graph/graph_view.js | 79 |
3 files changed, 278 insertions, 181 deletions
diff --git a/addons/web/static/src/js/views/graph/graph_controller.js b/addons/web/static/src/js/views/graph/graph_controller.js index 1c580a25bd6..f47bfaeb5bb 100644 --- a/addons/web/static/src/js/views/graph/graph_controller.js +++ b/addons/web/static/src/js/views/graph/graph_controller.js @@ -1,16 +1,30 @@ odoo.define('web.GraphController', function (require) { "use strict"; + /*--------------------------------------------------------- * Odoo Graph view *---------------------------------------------------------*/ -var core = require('web.core'); -var AbstractController = require('web.AbstractController'); -var GroupByMenuMixin = require('web.GroupByMenuMixin'); +const AbstractController = require('web.AbstractController'); +const { ComponentWrapper } = require('web.OwlCompatibility'); +const DropdownMenu = require('web.DropdownMenu'); +const { DEFAULT_INTERVAL, INTERVAL_OPTIONS } = require('web.searchUtils'); +const { qweb } = require('web.core'); + +class CarretDropdownMenu extends DropdownMenu { + /** + * @override + */ + get displayCaret() { + return true; + } +} -var qweb = core.qweb; +var GraphController = AbstractController.extend({ + custom_events: _.extend({}, AbstractController.prototype.custom_events, { + item_selected: '_onItemSelected', + }), -var GraphController = AbstractController.extend(GroupByMenuMixin,{ /** * @override * @param {Widget} parent @@ -22,12 +36,12 @@ var GraphController = AbstractController.extend(GroupByMenuMixin,{ * @param {string[]} params.groupableFields, */ init: function (parent, model, renderer, params) { - GroupByMenuMixin.init.call(this); this._super.apply(this, arguments); this.measures = params.measures; // this parameter condition the appearance of a 'Group By' // button in the control panel owned by the graph view. this.isEmbedded = params.isEmbedded; + this.withButtons = params.withButtons; // this parameter determines what is the list of fields // that may be used within the groupby menu available when @@ -76,6 +90,18 @@ var GraphController = AbstractController.extend(GroupByMenuMixin,{ }; }, /** + * @override + */ + reload: async function () { + const promises = [this._super(...arguments)]; + if (this.withButtons) { + const state = this.model.get(); + this.measures.forEach(m => m.isActive = m.fieldName === state.measure); + promises.push(this.measureMenu.update({ items: this.measures })); + } + return Promise.all(promises); + }, + /** * Render the buttons according to the GraphView.buttons and * add listeners on it. * Set this.$buttons with the produced jQuery element @@ -85,86 +111,62 @@ var GraphController = AbstractController.extend(GroupByMenuMixin,{ * nothing */ renderButtons: function ($node) { - if ($node) { - var context = { - measures: _.sortBy(_.pairs(_.omit(this.measures, '__count__')), function (x) { return x[1].string.toLowerCase(); }), - }; - this.$buttons = $(qweb.render('GraphView.buttons', context)); - this.$measureList = this.$buttons.find('.o_graph_measures_list'); - this.$buttons.find('button').tooltip(); - this.$buttons.click(this._onButtonClick.bind(this)); - this._updateButtons(); - this.$buttons.appendTo($node); - if (this.isEmbedded) { - this._addGroupByMenu($node, this.groupableFields).then(function(){ - var groupByButton = $node.find('.o_dropdown_toggler_btn'); - groupByButton.removeClass("o_dropdown_toggler_btn btn btn-secondary dropdown-toggle"); - groupByButton.addClass("btn dropdown-toggle btn-outline-secondary"); - }); - } + this.$buttons = $(qweb.render('GraphView.buttons')); + this.$buttons.find('button').tooltip(); + this.$buttons.click(ev => this._onButtonClick(ev)); + if (this.withButtons) { + const actionsContainer = this.$buttons[0]; + const promises = []; + const state = this.model.get(); + const fragment = document.createDocumentFragment(); + // Instantiate and append MeasureMenu + this.measures.forEach(m => m.isActive = m.fieldName === state.measure); + this.measureMenu = new ComponentWrapper(this, CarretDropdownMenu, { + title: "Measures", + items: this.measures, + }); + promises.push(this.measureMenu.mount(fragment).then(() => { + actionsContainer.appendChild(this.measureMenu.el); + this.measureMenu.el.classList.add('o_graph_measures_list'); + })); + if ($node) { + if (this.isEmbedded) { + // Instantiate and append GroupBy menu + this.groupByMenu = new ComponentWrapper(this, CarretDropdownMenu, { + title: "Group By", + icon: 'fa fa-bars', + items: this._getGroupBys(state.groupBy), + }); + promises.push(this.groupByMenu.mount(fragment).then(() => { + actionsContainer.appendChild(this.groupByMenu.el); + this.groupByMenu.el.classList.add('o_group_by_menu'); + })); + } + this.$buttons.appendTo($node); + } + Promise.all(promises).then(() => { + // Similar behaviour for all buttons + const buttons = actionsContainer.querySelectorAll('.o_dropdown_toggler_btn'); + for (const button of buttons) { + button.classList.remove('o_dropdown_toggler_btn', 'btn-secondary'); + if (this.isEmbedded) { + button.classList.add('btn-outline-secondary'); + } else { + button.classList.add('btn-primary'); + button.tabIndex = 0; + } + } + }); } }, - - //-------------------------------------------------------------------------- - // Private - //-------------------------------------------------------------------------- - - /* - * override - * - * @private - * @param {string[]} groupBy - */ - _setGroupby: function (groupBy) { - this.update({groupBy: groupBy}); - }, - /** - * @todo remove this and directly calls update. Update should be overridden - * and modified to call _updateButtons - * - * @private - * - * @param {'pie'|'line'|'bar'} mode - */ - _setMode: function (mode) { - this.update({mode: mode}); - this._updateButtons(); - }, /** - * @todo same as _setMode + * Makes sure that the buttons in the control panel matches the current + * state (so, correct active buttons and stuff like that). * - * @param {string} measure should be a valid (and aggregatable) field name - */ - _setMeasure: function (measure) { - var self = this; - this.update({measure: measure}).then(function () { - self._updateButtons(); - }); - }, - /** - * @private - * - * @param {boolean} stacked - */ - _toggleStackMode: function (stacked) { - this.update({stacked: stacked}); - this._updateButtons(); - }, - /** - * override - * - * @private - */ - _update: function () { - this._updateButtons(); - return this._super.apply(this, arguments); - }, - /** - * makes sure that the buttons in the control panel matches the current - * state (so, correct active buttons and stuff like that) + * @override */ - _updateButtons: function () { + updateButtons: function () { if (!this.$buttons) { return; } @@ -178,9 +180,60 @@ var GraphController = AbstractController.extend(GroupByMenuMixin,{ .data('stacked', state.stacked) .toggleClass('active', state.stacked) .toggleClass('o_hidden', state.mode !== 'bar'); - _.each(this.$measureList.find('.dropdown-item'), function (item) { - var $item = $(item); - $item.toggleClass('selected', $item.data('field') === state.measure); + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * Returns the items used by the Group By menu in embedded mode. + * + * @private + * @param {string[]} activeGroupBys + * @returns {Object[]} + */ + _getGroupBys(activeGroupBys) { + const normalizedGroupBys = this._normalizeActiveGroupBys(activeGroupBys); + const groupBys = Object.keys(this.groupableFields).map(fieldName => { + const field = this.groupableFields[fieldName]; + const groupByActivity = normalizedGroupBys.filter(gb => gb.fieldName === fieldName); + const groupBy = { + id: fieldName, + isActive: Boolean(groupByActivity.length), + description: field.string, + itemType: 'groupBy', + }; + if (['date', 'datetime'].includes(field.type)) { + groupBy.hasOptions = true; + const activeOptionIds = groupByActivity.map(gb => gb.interval); + groupBy.options = Object.values(INTERVAL_OPTIONS).map(o => { + return Object.assign({}, o, { isActive: activeOptionIds.includes(o.id) }); + }); + } + return groupBy; + }).sort((gb1, gb2) => { + return gb1.description.localeCompare(gb2.description); + }); + return groupBys; + }, + + /** + * This method puts the active groupBys in a convenient form. + * + * @private + * @param {string[]} activeGroupBys + * @returns {Object[]} normalizedGroupBys + */ + _normalizeActiveGroupBys(activeGroupBys) { + return activeGroupBys.map(groupBy => { + const fieldName = groupBy.split(':')[0]; + const field = this.groupableFields[fieldName]; + const normalizedGroupBy = { fieldName }; + if (['date', 'datetime'].includes(field.type)) { + normalizedGroupBy.interval = groupBy.split(':')[1] || DEFAULT_INTERVAL; + } + return normalizedGroupBy; }); }, @@ -196,18 +249,51 @@ var GraphController = AbstractController.extend(GroupByMenuMixin,{ */ _onButtonClick: function (ev) { var $target = $(ev.target); - var field; if ($target.hasClass('o_graph_button')) { if (_.contains(['bar','line', 'pie'], $target.data('mode'))) { - this._setMode($target.data('mode')); + this.update({ mode: $target.data('mode') }); } else if ($target.data('mode') === 'stack') { - this._toggleStackMode(!$target.data('stacked')); + this.update({ stacked: !$target.data('stacked') }); + } + } + }, + + /** + * @private + * @param {OdooEvent} ev + */ + _onItemSelected(ev) { + const item = ev.data.item; + if (this.isEmbedded && item.itemType === 'groupBy') { + const fieldName = item.id; + const optionId = ev.data.option && ev.data.option.id; + const activeGroupBys = this.model.get().groupBy; + if (optionId) { + const normalizedGroupBys = this._normalizeActiveGroupBys(activeGroupBys); + const index = normalizedGroupBys.findIndex(ngb => + ngb.fieldName === fieldName && ngb.interval === optionId); + if (index === -1) { + activeGroupBys.push(fieldName + ':' + optionId); + } else { + activeGroupBys.splice(index, 1); + } + } else { + const groupByFieldNames = activeGroupBys.map(gb => gb.split(':')[0]); + const indexOfGroupby = groupByFieldNames.indexOf(fieldName); + if (indexOfGroupby === -1) { + activeGroupBys.push(fieldName); + } else { + activeGroupBys.splice(indexOfGroupby, 1); + } } - } else if ($target.parents('.o_graph_measures_list').length) { - ev.preventDefault(); - ev.stopPropagation(); - field = $target.data('field'); - this._setMeasure(field); + this.update({ groupBy: activeGroupBys }); + this.groupByMenu.update({ + items: this._getGroupBys(activeGroupBys), + }); + } else if (item.itemType === 'measure') { + this.update({ measure: item.fieldName }); + this.measures.forEach(m => m.isActive = m.fieldName === item.fieldName); + this.measureMenu.update({ items: this.measures }); } }, }); diff --git a/addons/web/static/src/js/views/graph/graph_model.js b/addons/web/static/src/js/views/graph/graph_model.js index d8b8df2f1fd..8dcb241ae9c 100644 --- a/addons/web/static/src/js/views/graph/graph_model.js +++ b/addons/web/static/src/js/views/graph/graph_model.js @@ -2,7 +2,7 @@ odoo.define('web.GraphModel', function (require) { "use strict"; var core = require('web.core'); -const { DEFAULT_INTERVAL, rank } = require('web.controlPanelViewParameters'); +const { DEFAULT_INTERVAL, rankInterval } = require('web.searchUtils'); var _t = core._t; @@ -35,10 +35,7 @@ return AbstractModel.extend({ * @returns {Object} */ get: function () { - var self = this; - return _.extend({}, this.chart, { - comparisonFieldIndex: self._getComparisonFieldIndex(), - }); + return _.extend({}, this.chart); }, /** * Initial loading. @@ -47,21 +44,16 @@ return AbstractModel.extend({ * should be done by the graphView I think. * * @param {Object} params - * @param {boolean} params.compare * @param {Object} params.context * @param {Object} params.fields - * @param {string[]} params.comparisonTimeRange * @param {string[]} params.domain * @param {string[]} params.groupBys a list of valid field names * @param {string[]} params.groupedBy a list of valid field names * @param {boolean} params.stacked - * @param {string[]} params.timeRange - * @param {string} params.comparisonField - * @param {string} params.comparisonTimeRangeDescription * @param {string} params.measure a valid field name * @param {'pie'|'bar'|'line'} params.mode * @param {string} params.modelName - * @param {string} params.timeRangeDescription + * @param {Object} params.timeRanges * @returns {Promise} The promise does not return a handle, we don't need * to keep track of various entities. */ @@ -70,11 +62,7 @@ return AbstractModel.extend({ this.initialGroupBys = groupBys; this.fields = params.fields; this.modelName = params.modelName; - this.chart = { - comparisonField: params.comparisonField, - comparisonTimeRange: params.comparisonTimeRange, - comparisonTimeRangeDescription: params.comparisonTimeRangeDescription, - compare: params.compare, + this.chart = Object.assign({ context: params.context, dataPoints: [], domain: params.domain, @@ -83,11 +71,12 @@ return AbstractModel.extend({ mode: params.context.graph_mode || params.mode, origins: [], stacked: params.stacked, - timeRange: params.timeRange, - timeRangeDescription: params.timeRangeDescription, - }; - this.chart.processedGroupBy = this._processGroupBy(this.chart.groupBy); - return this._loadGraph(this._getDomains()); + timeRanges: params.timeRanges, + }); + + this._computeDerivedParams(); + + return this._loadGraph(); }, /** * Reload data. It is similar to the load function. Note that we ignore the @@ -104,6 +93,7 @@ return AbstractModel.extend({ * @param {string[]} [params.groupBy] * @param {string} [params.measure] a valid field name * @param {string} [params.mode] one of 'bar', 'pie', 'line' + * @param {Object} [params.timeRanges] * @returns {Promise} */ reload: function (handle, params) { @@ -112,22 +102,6 @@ return AbstractModel.extend({ this.chart.groupBy = params.context.graph_groupbys || this.chart.groupBy; this.chart.measure = params.context.graph_measure || this.chart.measure; this.chart.mode = params.context.graph_mode || this.chart.mode; - var timeRangeMenuData = params.context.timeRangeMenuData; - if (timeRangeMenuData) { - this.chart.comparisonField = timeRangeMenuData.comparisonField || undefined; - this.chart.comparisonTimeRange = timeRangeMenuData.comparisonTimeRange || []; - this.chart.compare = this.chart.comparisonTimeRange.length > 0; - this.chart.comparisonTimeRangeDescription = timeRangeMenuData.comparisonTimeRangeDescription; - this.chart.timeRange = timeRangeMenuData.timeRange || []; - this.chart.timeRangeDescription = timeRangeMenuData.timeRangeDescription; - } else { - this.chart.comparisonField = undefined; - this.chart.comparisonTimeRange = []; - this.chart.compare = false; - this.chart.comparisonTimeRangeDescription = undefined; - this.chart.timeRange = []; - this.chart.timeRangeDescription = undefined; - } } if ('domain' in params) { this.chart.domain = params.domain; @@ -138,7 +112,12 @@ return AbstractModel.extend({ if ('measure' in params) { this.chart.measure = params.measure; } - this.chart.processedGroupBy = this._processGroupBy(this.chart.groupBy); + if ('timeRanges' in params) { + this.chart.timeRanges = params.timeRanges; + } + + this._computeDerivedParams(); + if ('mode' in params) { this.chart.mode = params.mode; return Promise.resolve(); @@ -147,7 +126,7 @@ return AbstractModel.extend({ this.chart.stacked = params.stacked; return Promise.resolve(); } - return this._loadGraph(this._getDomains()); + return this._loadGraph(); }, //-------------------------------------------------------------------------- @@ -155,28 +134,34 @@ return AbstractModel.extend({ //-------------------------------------------------------------------------- /** + * Compute this.chart.processedGroupBy, this.chart.domains, this.chart.origins, + * and this.chart.comparisonFieldIndex. + * Those parameters are determined by this.chart.timeRanges, this.chart.groupBy, and this.chart.domain. + * * @private - * @returns {number} - */ - _getComparisonFieldIndex: function () { - var groupBys = this.chart.processedGroupBy.map(function (gb) { - return gb.split(":")[0]; - }); - return groupBys.indexOf(this.chart.comparisonField); - }, - /** - * @private - * @returns {Array[]} */ - _getDomains: function () { - var domains = [this.chart.domain.concat(this.chart.timeRange)]; - this.chart.origins = [this.chart.timeRangeDescription || ""]; - if (this.chart.compare) { - domains.push(this.chart.domain.concat(this.chart.comparisonTimeRange)); - this.chart.origins.push(this.chart.comparisonTimeRangeDescription); + _computeDerivedParams: function () { + this.chart.processedGroupBy = this._processGroupBy(this.chart.groupBy); + + const { range, rangeDescription, comparisonRange, comparisonRangeDescription, fieldName } = this.chart.timeRanges; + if (range) { + this.chart.domains = [this.chart.domain.concat(range)]; + this.chart.origins = [rangeDescription]; + if (comparisonRange) { + this.chart.domains.push(this.chart.domain.concat(comparisonRange)); + this.chart.origins.push(comparisonRangeDescription); + } + const groupBys = this.chart.processedGroupBy.map(function (gb) { + return gb.split(":")[0]; + }); + this.chart.comparisonFieldIndex = groupBys.indexOf(fieldName); + } else { + this.chart.domains = [this.chart.domain]; + this.chart.origins = [""]; + this.chart.comparisonFieldIndex = -1; } - return domains; }, + /** * Fetch and process graph data. It is basically a(some) read_group(s) * with correct fields for each domain. We have to do some light processing @@ -184,10 +169,9 @@ return AbstractModel.extend({ * with an aggregation function, such as my_date:week. * * @private - * @param {Array[]} domains * @returns {Promise} */ - _loadGraph: function (domains) { + _loadGraph: function () { var self = this; this.chart.dataPoints = []; var groupBy = this.chart.processedGroupBy; @@ -205,10 +189,10 @@ return AbstractModel.extend({ } var context = _.extend({fill_temporal: true}, this.chart.context); - var defs = []; - domains.forEach(function (domain, originIndex) { - defs.push(self._rpc({ + var proms = []; + this.chart.domains.forEach(function (domain, originIndex) { + proms.push(self._rpc({ model: self.modelName, method: 'read_group', context: context, @@ -218,7 +202,7 @@ return AbstractModel.extend({ lazy: false, }).then(self._processData.bind(self, originIndex))); }); - return Promise.all(defs); + return Promise.all(proms); }, /** * Since read_group is insane and returns its result on different keys @@ -286,7 +270,7 @@ return AbstractModel.extend({ } if (groupBysMap.has(fieldName)) { const registeredInterval = groupBysMap.get(fieldName); - if (rank(registeredInterval) < rank(interval)) { + if (rankInterval(registeredInterval) < rankInterval(interval)) { groupBysMap.set(fieldName, interval); } } else { diff --git a/addons/web/static/src/js/views/graph/graph_view.js b/addons/web/static/src/js/views/graph/graph_view.js index 64c65694784..488424536e6 100644 --- a/addons/web/static/src/js/views/graph/graph_view.js +++ b/addons/web/static/src/js/views/graph/graph_view.js @@ -16,8 +16,8 @@ var GraphRenderer = require('web.GraphRenderer'); var _t = core._t; var _lt = core._lt; -var controlPanelViewParameters = require('web.controlPanelViewParameters'); -var GROUPABLE_TYPES = controlPanelViewParameters.GROUPABLE_TYPES; +var searchUtils = require('web.searchUtils'); +var GROUPABLE_TYPES = searchUtils.GROUPABLE_TYPES; var GraphView = AbstractView.extend({ display_name: _lt('Graph'), @@ -39,54 +39,81 @@ var GraphView = AbstractView.extend({ init: function (viewInfo, params) { this._super.apply(this, arguments); - var self = this; - var measure; - var groupBys = []; - var measures = {__count__: {string: _t("Count"), type: "integer"}}; - var groupableFields = {}; - this.fields.__count__ = {string: _t("Count"), type: "integer"}; + const additionalMeasures = params.additionalMeasures || []; + let measure; + const measures = {}; + const measureStrings = {}; + const groupBys = []; + const groupableFields = {}; + this.fields.__count__ = { string: _t("Count"), type: 'integer' }; - var measureString = {}; - - this.arch.children.forEach(function (field) { - var fieldName = field.attrs.name; + this.arch.children.forEach(field => { + let fieldName = field.attrs.name; if (fieldName === "id") { return; } - var interval = field.attrs.interval; + const interval = field.attrs.interval; if (interval) { fieldName = fieldName + ':' + interval; } if (field.attrs.type === 'measure') { + const { string } = this.fields[fieldName]; measure = fieldName; - measures[fieldName] = self.fields[fieldName]; + measures[fieldName] = { + description: string, + fieldName, + groupNumber: 0, + isActive: false, + itemType: 'measure', + }; } else { groupBys.push(fieldName); } if (field.attrs.string) { - measureString[fieldName] = field.attrs.string; + measureStrings[fieldName] = field.attrs.string; } }); - _.each(this.fields, function (field, name) { + for (const name in this.fields) { + const field = this.fields[name]; if (name !== 'id' && field.store === true) { - if (_.contains(['integer', 'float', 'monetary'], field.type) || - _.contains(params.additionalMeasures, name)) { - measures[name] = field; + if ( + ['integer', 'float', 'monetary'].includes(field.type) || + additionalMeasures.includes(name) + ) { + measures[name] = { + description: field.string, + fieldName: name, + groupNumber: 0, + isActive: false, + itemType: 'measure', + }; } - if (_.contains(GROUPABLE_TYPES, field.type)) { + if (GROUPABLE_TYPES.includes(field.type)) { groupableFields[name] = field; } } - }); - - _.each(measureString, function (string, name) { + } + for (const name in measureStrings) { if (measures[name]) { - measures[name].string = string; + measures[name].description = measureStrings[name]; } - }); + } - this.controllerParams.measures = measures; + const sortedMeasures = Object.values(measures).sort((a, b) => { + const descA = a.description.toLowerCase(); + const descB = b.description.toLowerCase(); + return descA > descB ? 1 : descA < descB ? -1 : 0; + }); + const countMeasure = { + description: _t("Count"), + fieldName: '__count__', + groupNumber: 1, + isActive: false, + itemType: 'measure', + }; + this.controllerParams.withButtons = params.withButtons !== false; + this.controllerParams.measures = [...sortedMeasures, countMeasure]; this.controllerParams.groupableFields = groupableFields; this.rendererParams.fields = this.fields; this.rendererParams.title = this.arch.attrs.title; // TODO: use attrs.string instead |
