diff options
Diffstat (limited to 'addons/web/static/src/js/views/graph/graph_controller.js')
| -rw-r--r-- | addons/web/static/src/js/views/graph/graph_controller.js | 356 |
1 files changed, 356 insertions, 0 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 new file mode 100644 index 00000000..6cb2b899 --- /dev/null +++ b/addons/web/static/src/js/views/graph/graph_controller.js @@ -0,0 +1,356 @@ +odoo.define('web.GraphController', function (require) { +"use strict"; + +/*--------------------------------------------------------- + * Odoo Graph view + *---------------------------------------------------------*/ + +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'); +const { _t } = require('web.core'); + +class CarretDropdownMenu extends DropdownMenu { + /** + * @override + */ + get displayCaret() { + return true; + } +} + +var GraphController = AbstractController.extend({ + custom_events: _.extend({}, AbstractController.prototype.custom_events, { + item_selected: '_onItemSelected', + open_view: '_onOpenView', + }), + + /** + * @override + * @param {Widget} parent + * @param {GraphModel} model + * @param {GraphRenderer} renderer + * @param {Object} params + * @param {string[]} params.measures + * @param {boolean} params.isEmbedded + * @param {string[]} params.groupableFields, + */ + init: function (parent, model, renderer, params) { + 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; + // views to use in the action triggered when the graph is clicked + this.views = params.views; + this.title = params.title; + + // this parameter determines what is the list of fields + // that may be used within the groupby menu available when + // the view is embedded + this.groupableFields = params.groupableFields; + this.buttonDropdownPromises = []; + }, + /** + * @override + */ + start: function () { + this.$el.addClass('o_graph_controller'); + return this._super.apply(this, arguments); + }, + /** + * @todo check if this can be removed (mostly duplicate with + * AbstractController method) + */ + destroy: function () { + if (this.$buttons) { + // remove jquery's tooltip() handlers + this.$buttons.find('button').off().tooltip('dispose'); + } + this._super.apply(this, arguments); + }, + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + /** + * Returns the current mode, measure and groupbys, so we can restore the + * view when we save the current state in the search view, or when we add it + * to the dashboard. + * + * @override + * @returns {Object} + */ + getOwnedQueryParams: function () { + var state = this.model.get(); + return { + context: { + graph_measure: state.measure, + graph_mode: state.mode, + graph_groupbys: state.groupBy, + } + }; + }, + /** + * @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 + * + * @param {jQuery} [$node] a jQuery node where the rendered buttons should + * be inserted $node may be undefined, in which case the GraphView does + * nothing + */ + renderButtons: function ($node) { + this.$buttons = $(qweb.render('GraphView.buttons')); + this.$buttons.find('button').tooltip(); + this.$buttons.click(ev => this._onButtonClick(ev)); + + if (this.withButtons) { + 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: _t("Measures"), + items: this.measures, + }); + this.buttonDropdownPromises = [this.measureMenu.mount(fragment)]; + if ($node) { + if (this.isEmbedded) { + // Instantiate and append GroupBy menu + this.groupByMenu = new ComponentWrapper(this, CarretDropdownMenu, { + title: _t("Group By"), + icon: 'fa fa-bars', + items: this._getGroupBys(state.groupBy), + }); + this.buttonDropdownPromises.push(this.groupByMenu.mount(fragment)); + } + this.$buttons.appendTo($node); + } + } + }, + /** + * Makes sure that the buttons in the control panel matches the current + * state (so, correct active buttons and stuff like that). + * + * @override + */ + updateButtons: function () { + if (!this.$buttons) { + return; + } + var state = this.model.get(); + this.$buttons.find('.o_graph_button').removeClass('active'); + this.$buttons + .find('.o_graph_button[data-mode="' + state.mode + '"]') + .addClass('active'); + this.$buttons + .find('.o_graph_button[data-mode="stack"]') + .data('stacked', state.stacked) + .toggleClass('active', state.stacked) + .toggleClass('o_hidden', state.mode !== 'bar'); + this.$buttons + .find('.o_graph_button[data-order]') + .toggleClass('o_hidden', state.mode === 'pie' || !!Object.keys(state.timeRanges).length) + .filter('.o_graph_button[data-order="' + state.orderBy + '"]') + .toggleClass('active', !!state.orderBy); + + if (this.withButtons) { + this._attachDropdownComponents(); + } + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * Attaches the different dropdown components to the buttons container. + * + * @returns {Promise} + */ + async _attachDropdownComponents() { + await Promise.all(this.buttonDropdownPromises); + const actionsContainer = this.$buttons[0]; + // Attach "measures" button + actionsContainer.appendChild(this.measureMenu.el); + this.measureMenu.el.classList.add('o_graph_measures_list'); + if (this.isEmbedded) { + // Attach "groupby" button + actionsContainer.appendChild(this.groupByMenu.el); + this.groupByMenu.el.classList.add('o_group_by_menu'); + } + // Update button classes accordingly to the current mode + 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; + } + } + }, + + /** + * 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; + }); + }, + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * Do what need to be done when a button from the control panel is clicked. + * + * @private + * @param {MouseEvent} ev + */ + _onButtonClick: function (ev) { + var $target = $(ev.target); + if ($target.hasClass('o_graph_button')) { + if (_.contains(['bar','line', 'pie'], $target.data('mode'))) { + this.update({ mode: $target.data('mode') }); + } else if ($target.data('mode') === 'stack') { + this.update({ stacked: !$target.data('stacked') }); + } else if (['asc', 'desc'].includes($target.data('order'))) { + const order = $target.data('order'); + const state = this.model.get(); + this.update({ orderBy: state.orderBy === order ? false : order }); + } + } + }, + + /** + * @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); + } + } + 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 }); + } + }, + + /** + * @private + * @param {OdooEvent} ev + * @param {Array[]} ev.data.domain + */ + _onOpenView(ev) { + ev.stopPropagation(); + const state = this.model.get(); + const context = Object.assign({}, state.context); + Object.keys(context).forEach(x => { + if (x === 'group_by' || x.startsWith('search_default_')) { + delete context[x]; + } + }); + this.do_action({ + context: context, + domain: ev.data.domain, + name: this.title, + res_model: this.modelName, + target: 'current', + type: 'ir.actions.act_window', + view_mode: 'list', + views: this.views, + }); + }, +}); + +return GraphController; + +}); |
