diff options
| author | Pierre Rousseau <pro@odoo.com> | 2019-12-03 09:58:52 +0000 |
|---|---|---|
| committer | Vincent Schippefilt <vsc@odoo.com> | 2019-12-09 12:53:24 +0000 |
| commit | 46cf65d68e9c13193dff4b563b9b0bbb2199dfc1 (patch) | |
| tree | 4338abceab39a837283a4075587b9c12d616ba67 /addons/web/static/src/js/views | |
| parent | eef10097aefe04c3a89d5226727a45d540bf8f63 (diff) | |
[REF] web: rewrite pivot view renderer with OWL
Because the webclient and other higher level components such as the
actionManager are not yet rewritten in OWL, we cannot make a complete
migration of the controller and the model to a more OWL-ish code.
We have rewritten the renderer completely in OWL, it uses more
extensively a template and manages its own state and its sub-components.
We did not rewrite the controller, however we have made it inherit from
`web.ControllerAdapter` which is a thin compatibility layer used when a
odoo controller uses a OWL renderer.
In the tests, the only changes are that all the events triggered by the
test and handled by OWL must be triggered on DOM nodes directly and not
on JQuery elements.
closes odoo/odoo#39491
Related: odoo/enterprise#7135
Signed-off-by: VincentSchippefilt <VincentSchippefilt@users.noreply.github.com>
Co-authored-by: VincentSchippefilt <vsc@odoo.com>
Co-authored-by: pro-odoo <pro@odoo.com>
Diffstat (limited to 'addons/web/static/src/js/views')
| -rw-r--r-- | addons/web/static/src/js/views/pivot/pivot_controller.js | 614 | ||||
| -rw-r--r-- | addons/web/static/src/js/views/pivot/pivot_model.js | 63 | ||||
| -rw-r--r-- | addons/web/static/src/js/views/pivot/pivot_renderer.js | 463 | ||||
| -rw-r--r-- | addons/web/static/src/js/views/pivot/pivot_view.js | 263 |
4 files changed, 646 insertions, 757 deletions
diff --git a/addons/web/static/src/js/views/pivot/pivot_controller.js b/addons/web/static/src/js/views/pivot/pivot_controller.js index 71427baff1b..606367d4d3a 100644 --- a/addons/web/static/src/js/views/pivot/pivot_controller.js +++ b/addons/web/static/src/js/views/pivot/pivot_controller.js @@ -1,355 +1,311 @@ odoo.define('web.PivotController', function (require) { -"use strict"; -/** - * Odoo Pivot Table Controller - * - * This class is the Controller for the pivot table view. It has to coordinate - * the actions coming from the search view (through the update method), from - * the renderer, from the model, and from the control panel. - * - * It can display action buttons in the control panel, to select a different - * measure, or to perform some other actions such as download/expand/flip the - * view. - */ - -var AbstractController = require('web.AbstractController'); -var core = require('web.core'); -var framework = require('web.framework'); -var session = require('web.session'); - -var _t = core._t; -var QWeb = core.qweb; - -var PivotController = AbstractController.extend({ - contentTemplate: 'PivotView', - events: { - 'click .o_pivot_field_menu a': '_onGroupByMenuSelection', - 'click .o_pivot_header_cell_closed': '_onClosedHeaderClick', - }, - custom_events: _.extend({}, AbstractController.prototype.custom_events, { - close_group: '_onCloseGroup', - open_view: '_onOpenView', - sort_rows: '_onSortRows', - }), + "use strict"; /** - * @override - * @param {Object} params - * @param {Object} params.groupableFields a map from field names to field - * props + * Odoo Pivot Table Controller + * + * This class is the Controller for the pivot table view. It has to coordinate + * the actions coming from the search view (through the update method), from + * the renderer, from the model, and from the control panel. + * + * It can display action buttons in the control panel, to select a different + * measure, or to perform some other actions such as download/expand/flip the + * view. */ - init: function (parent, model, renderer, params) { - this._super.apply(this, arguments); - this.measures = params.measures; - this.groupableFields = params.groupableFields; - this.title = params.title; - // views to use in the action triggered when a data cell is clicked - this.views = params.views; - this.groupSelected = null; - }, - /** - * @override - */ - start: function () { - this.$groupBySelection = this.$('.o_field_selection'); - core.bus.on('click', this, function () { - this.$groupBySelection.empty(); - }); - return this._super(); - }, - /** - * @override - */ - destroy: function () { - if (this.$buttons) { - // remove jquery's tooltip() handlers - this.$buttons.find('button').off(); - } - return this._super.apply(this, arguments); - }, + const ControllerAdapter = require('web.ControllerAdapter'); + const core = require('web.core'); + const framework = require('web.framework'); + const session = require('web.session'); - //-------------------------------------------------------------------------- - // Public - //-------------------------------------------------------------------------- + const _t = core._t; + const QWeb = core.qweb; - /** - * Returns the current measures 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 method from AbstractController - * @returns {Object} - */ - getOwnedQueryParams: function () { - var state = this.model.get({raw: true}); - return { - context: { - pivot_measures: state.measures, - pivot_column_groupby: state.colGroupBys, - pivot_row_groupby: state.rowGroupBys, + const PivotController = ControllerAdapter.extend({ + events: Object.assign({}, ControllerAdapter.prototype.events, { + closed_header_click: '_onClosedHeaderClicked', + open_view: '_onOpenView', + opened_header_click: '_onOpenedHeaderClicked', + sort_rows: '_onSortRows', + groupby_menu_selection: '_onGroupByMenuSelection', + }), + /** + * @override + * @param parent + * @param model + * @param renderer + * @param {Object} params + * @param {Object} params.groupableFields a map from field names to field + * props + */ + init: function (parent, model, renderer, params) { + this._super(...arguments); + + this.measures = params.measures; + this.title = params.title; + // views to use in the action triggered when a data cell is clicked + this.views = params.views; + this.groupSelected = null; + }, + /** + * @override + */ + destroy: function () { + if (this.$buttons) { + // remove jquery's tooltip() handlers + this.$buttons.find('button').off(); } - }; - }, - /** - * Render the buttons according to the PivotView.buttons template 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 PivotView - * does nothing - */ - renderButtons: function ($node) { - if ($node) { - var context = { - measures: _.sortBy(_.pairs(_.omit(this.measures, '__count')), function (x) { - return x[1].string.toLowerCase(); - }), + return this._super(...arguments); + }, + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + /** + * Returns the current measures 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 method from AbstractController + * @returns {Object} + */ + getOwnedQueryParams: function () { + const state = this.model.get({ raw: true }); + return { + context: { + pivot_measures: state.measures, + pivot_column_groupby: state.colGroupBys, + pivot_row_groupby: state.rowGroupBys, + } }; - this.$buttons = $(QWeb.render('PivotView.buttons', context)); - this.$buttons.click(this._onButtonClick.bind(this)); - this.$buttons.find('button').tooltip(); + }, + /** + * Render the buttons according to the PivotView.buttons template 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 PivotView + * does nothing + */ + renderButtons: function ($node) { + if ($node) { + const context = { + measures: Object.entries(this.measures) + .filter(x => x[0] !== '__count') + .sort((a, b) => a[1].string.toLowerCase() > b[1].string.toLowerCase() ? 1 : -1), + }; + this.$buttons = $(QWeb.render('PivotView.buttons', context)); + this.$buttons.click(this._onButtonClick.bind(this)); + this.$buttons.find('button').tooltip(); - this.$buttons.appendTo($node); - this._updateButtons(); - } - }, + this.$buttons.appendTo($node); + this._updateButtons(); + } + }, - //-------------------------------------------------------------------------- - // Private - //-------------------------------------------------------------------------- + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- - /** - * Export the current pivot table data in a xls file. For this, we have to - * serialize the current state, then call the server /web/pivot/export_xls. - * Force a reload before exporting to ensure to export up-to-date data. - * - * @private - */ - _downloadTable: function () { - var self = this; - if (self.model.getTableWidth() > 256) { - this.call('crash_manager', 'show_message', _t("For Excel compatibility, data cannot be exported if there are more than 256 columns.\n\nTip: try to flip axis, filter further or reduce the number of measures.")); - framework.unblockUI(); - return; - } - var table = self.model.exportData(); - table.title = self.title; - session.get_file({ - url: '/web/pivot/export_xls', - data: {data: JSON.stringify(table)}, - complete: framework.unblockUI, - error: (error) => this.call('crash_manager', 'rpc_error', error), - }); - }, - /** - * Render the field selection menu, to select a groupable field. We disable - * already selected groupbys. - * - * @private - * @param {number} top top coordinate where we have to render the menu - * @param {number} left left coordinate for the menu - */ - _renderGroupBySelection: function (top, left) { - var state = this.model.get({raw: true}); - var groupedFields = state.rowGroupBys - .concat(state.colGroupBys) - .map(function (f) { - return f.split(':')[0]; + /** + * Export the current pivot table data in a xls file. For this, we have to + * serialize the current state, then call the server /web/pivot/export_xls. + * Force a reload before exporting to ensure to export up-to-date data. + * + * @private + */ + _downloadTable: function () { + if (this.model.getTableWidth() > 256) { + this.call('crash_manager', 'show_message', _t("For Excel compatibility, data cannot be exported if there are more than 256 columns.\n\nTip: try to flip axis, filter further or reduce the number of measures.")); + framework.unblockUI(); + return; + } + const table = this.model.exportData(); + table.title = this.title; + session.get_file({ + url: '/web/pivot/export_xls', + data: { data: JSON.stringify(table) }, + complete: framework.unblockUI, + error: (error) => this.call('crash_manager', 'rpc_error', error), }); + }, + /** + * @override + * @private + */ + _update: function () { + this._updateButtons(); + return this._super.apply(this, arguments); + }, + /** + * @private + */ + _updateButtons: function () { + if (!this.$buttons) { + return; + } + const state = this.model.get({ raw: true }); + Object.entries(this.measures).forEach(elt => { + const name = elt[0]; + const isSelected = state.measures.includes(name); + this.$buttons.find('.dropdown-item[data-field="' + name + '"]') + .toggleClass('selected', isSelected); - var fields = _.chain(this.groupableFields) - .pairs() - .sortBy(function (f) { - return f[1].string; - }) - .map(function (f) { - return [f[0], f[1], _.contains(groupedFields, f[0])]; - }) - .value(); + }); + const noDataDisplayed = !state.hasData || !state.measures.length; + this.$buttons.find('.o_pivot_flip_button').prop('disabled', noDataDisplayed); + this.$buttons.find('.o_pivot_expand_button').prop('disabled', noDataDisplayed); + this.$buttons.find('.o_pivot_download').prop('disabled', noDataDisplayed); + }, - this.$groupBySelection.html(QWeb.render('PivotView.GroupBySelection', { - fields: fields - })); + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- - var cssProps = {top: top}; - var isRTL = _t.database.parameters.direction === 'rtl'; - cssProps[isRTL ? 'right' : 'left'] = isRTL ? this.$el.width() - left : left; - this.$groupBySelection.find('.dropdown-menu').first().css(cssProps).addClass('show'); - }, - /** - * @override - * @private - */ - _startRenderer: function () { - return this.renderer.appendTo(this.$('.o_pivot')); - }, - /** - * @override - * @private - */ - _update: function () { - this._updateButtons(); - return this._super.apply(this, arguments); - }, - /** - * @private - */ - _updateButtons: function () { - if (!this.$buttons) { - return; - } - var self = this; - var state = this.model.get({raw: true}); - _.each(this.measures, function (measure, name) { - var isSelected = _.contains(state.measures, name); - self.$buttons.find('.dropdown-item[data-field="' + name + '"]') - .toggleClass('selected', isSelected); - }); - var noDataDisplayed = !state.hasData || !state.measures.length; - this.$buttons.find('.o_pivot_flip_button').prop('disabled', noDataDisplayed); - this.$buttons.find('.o_pivot_expand_button').prop('disabled', noDataDisplayed); - this.$buttons.find('.o_pivot_download').prop('disabled', noDataDisplayed); - }, + /** + * This handler is called when the user clicked on a button in the control + * panel. We then have to react properly: it can either be a change in the + * current measures, or a request to flip/expand/download data. + * + * @private + * @param {MouseEvent} ev + */ + _onButtonClick: async function (ev) { + const $target = $(ev.target); + if ($target.hasClass('o_pivot_flip_button')) { + this.model.flip(); + this.update({}, { reload: false }); + } + if ($target.hasClass('o_pivot_expand_button')) { + await this.model.expandAll(); + this.update({}, { reload: false }); + } + if ($target.parents('.o_pivot_measures_list').length) { + ev.preventDefault(); + ev.stopPropagation(); + const field = $target.data('field'); + await this.model.toggleMeasure(field); + this.update({}, { reload: false }); + } + if ($target.hasClass('o_pivot_download')) { + this._downloadTable(); + } + }, + /** + * + * @private + * @param {OdooEvent} ev + */ + _onCloseGroup: function (ev) { + this.model.closeGroup(ev.data.groupId, ev.data.type); + this.update({}, { reload: false }); + }, + /** + * @param {CustomEvent} ev + * @private + * */ + _onOpenedHeaderClicked: function (ev) { + this.model.closeGroup(ev.detail.cell.groupId, ev.detail.type); + this.update({}, { reload: false }); + }, + /** + * @param {CustomEvent} ev + * @private + * */ + _onClosedHeaderClicked: async function (ev) { + const cell = ev.detail.cell; + const groupId = cell.groupId; + const type = ev.detail.type; - //-------------------------------------------------------------------------- - // Handlers - //-------------------------------------------------------------------------- + const group = { + rowValues: groupId[0], + colValues: groupId[1], + type: type + }; - /** - * This handler is called when the user clicked on a button in the control - * panel. We then have to react properly: it can either be a change in the - * current measures, or a request to flip/expand/download data. - * - * @private - * @param {MouseEvent} ev - */ - _onButtonClick: function (ev) { - var $target = $(ev.target); - if ($target.hasClass('o_pivot_flip_button')) { - this.model.flip(); - this.update({}, {reload: false}); - } - if ($target.hasClass('o_pivot_expand_button')) { - this.model.expandAll().then(this.update.bind(this, {}, {reload: false})); - } - if ($target.parents('.o_pivot_measures_list').length) { - ev.preventDefault(); + const state = this.model.get({ raw: true }); + const groupValues = type === 'row' ? groupId[0] : groupId[1]; + const groupBys = type === 'row' ? + state.rowGroupBys : + state.colGroupBys; + this.selectedGroup = group; + if (groupValues.length < groupBys.length) { + const groupBy = groupBys[groupValues.length]; + await this.model.expandGroup(this.selectedGroup, groupBy); + this.update({}, { reload: false }); + } + }, + /** + * This handler is called when the user selects a groupby in the dropdown menu. + * + * @private + * @param {CustomEvent} ev + */ + _onGroupByMenuSelection: async function (ev) { ev.stopPropagation(); - var field = $target.data('field'); - this.model.toggleMeasure(field).then(this.update.bind(this, {}, {reload: false})); - } - if ($target.hasClass('o_pivot_download')) { - this._downloadTable(); - } - }, - /** - * - * @private - * @param {OdooEvent} ev - */ - _onCloseGroup: function (ev) { - this.model.closeGroup(ev.data.groupId, ev.data.type); - this.update({}, {reload: false}); - }, - /** - * When we click on a closed row (col) header, we either want to open the - * dropdown menu to select a new field to add to rowGroupBys (resp. colGroupBys), - * or we want to open the clicked header, if rowGroupBys (resp. colGroupBys) - * has length strictly greater than header - * - * @private - * @param {MouseEvent} ev - */ - _onClosedHeaderClick: function (ev) { - ev.preventDefault(); - ev.stopPropagation(); - - var $target = $(ev.target); - var groupId = $target.data('groupId'); - var type = $target.data('type'); - var group = { - rowValues: groupId[0], - colValues: groupId[1], - type: type - }; - - var state = this.model.get({raw: true}); + let groupBy = ev.detail.field.name; + const interval = ev.detail.interval; + if (interval) { + groupBy = groupBy + ':' + interval; + } + this.model.addGroupBy(groupBy, this.selectedGroup.type); + await this.model.expandGroup(this.selectedGroup, groupBy); + this.update({}, { reload: false }); + }, + /** + * @private + * @param {CustomEvent} ev + */ + _onOpenView: function (ev) { + ev.stopPropagation(); + const cell = ev.detail; + if (cell.value === undefined || this.initialState.disableLinking) { + return; + } - var groupValues = type === 'row' ? groupId[0] : groupId[1]; - var groupBys = type === 'row' ? - state.rowGroupBys : - state.colGroupBys; + const context = Object.assign({}, this.model.data.context); + Object.keys(context).forEach(x => { + if (x === 'group_by' || x.startsWith('search_default_')) { + delete context[x]; + } + }); - this.selectedGroup = group; - if (groupValues.length < groupBys.length) { - var groupBy = groupBys[groupValues.length]; - this.model - .expandGroup(this.selectedGroup, groupBy) - .then(this.update.bind(this, {}, {reload: false})); - } else { - var position = $target.position(); - var top = position.top + $target.height(); - var left = ev.clientX; - this._renderGroupBySelection(top, left); - } - }, - /** - * This handler is called when the user selects a groupby in the dropdown menu. - * - * @private - * @param {MouseEvent} ev - */ - _onGroupByMenuSelection: function (ev) { - ev.preventDefault(); - var $target = $(ev.target); - if ($target.hasClass('disabled')) { - ev.stopPropagation(); - return; - } + const group = { + rowValues: cell.groupId[0], + colValues: cell.groupId[1], + originIndex: cell.originIndexes[0] + }; - var groupBy = $target.data('field'); - var interval = $target.data('interval'); - if (interval) { - groupBy = groupBy + ':' + interval; - } - this.model.addGroupBy(groupBy, this.selectedGroup.type); - this.model - .expandGroup(this.selectedGroup, groupBy) - .then(this.update.bind(this, {}, {reload: false})); - }, - /** - * @private - * @param {OdooEvent} ev - */ - _onOpenView: function (ev) { - ev.stopPropagation(); - var context = ev.data.context; - var group = ev.data.group; - var domain = this.model._getGroupDomain(group); + const domain = this.model._getGroupDomain(group); - this.do_action({ - type: 'ir.actions.act_window', - name: this.title, - res_model: this.modelName, - views: this.views, - view_mode: 'list', - target: 'current', - context: context, - domain: domain, - }); - }, - /** - * @private - * @param {OdooEvent} ev - */ - _onSortRows: function (ev) { - this.model.sortRows(ev.data.sortedColumn); - this.update({}, {reload: false}); - }, -}); + this.do_action({ + type: 'ir.actions.act_window', + name: this.title, + res_model: this.modelName, + views: this.views, + view_mode: 'list', + target: 'current', + context: context, + domain: domain, + }); + }, + /** + * @private + * @param {CustomEvent} ev + */ + _onSortRows: function (ev) { + this.model.sortRows({ + groupId: ev.detail.groupId, + measure: ev.detail.measure, + order: (ev.detail.order || 'desc') === 'asc' ? 'desc' : 'asc', + originIndexes: ev.detail.originIndexes, + }); + this.update({}, { reload: false }); + }, + }); -return PivotController; + return PivotController; }); diff --git a/addons/web/static/src/js/views/pivot/pivot_model.js b/addons/web/static/src/js/views/pivot/pivot_model.js index cb09f4a40f9..4669c5af7ec 100644 --- a/addons/web/static/src/js/views/pivot/pivot_model.js +++ b/addons/web/static/src/js/views/pivot/pivot_model.js @@ -361,7 +361,7 @@ var PivotModel = AbstractModel.extend({ * @param {string} groupBy * @returns {Promise} */ - expandGroup: function (group, groupBy) { + expandGroup: async function (group, groupBy) { var leftDivisors; var rightDivisors; @@ -475,6 +475,7 @@ var PivotModel = AbstractModel.extend({ function twistKey(key) { return JSON.stringify(JSON.parse(key).reverse()); } + function twist(object) { var newObject = {}; Object.keys(object).forEach(function (key) { @@ -483,6 +484,7 @@ var PivotModel = AbstractModel.extend({ }); return newObject; } + this.measurements = twist(this.measurements); this.counts = twist(this.counts); this.groupDomains = twist(this.groupDomains); @@ -507,6 +509,7 @@ var PivotModel = AbstractModel.extend({ measures: this.data.measures, origins: this.data.origins, rowGroupBys: groupBys.rowGroupBys, + selectionGroupBys: this._getSelectionGroupBy(groupBys), }; if (!raw && state.hasData) { state.table = this._getTable(); @@ -540,6 +543,7 @@ var PivotModel = AbstractModel.extend({ * @param {string} [params.default_order] * @param {string} [params.timeRangeDescription=""] * @param {string} params.modelName + * @param {Object[]} params.groupableFields * @returns {Promise} */ load: function (params) { @@ -549,6 +553,7 @@ var PivotModel = AbstractModel.extend({ this.fields = params.fields; this.modelName = params.modelName; + this.groupableFields = params.groupableFields; this.data = { expandedRowGroupBys: [], expandedColGroupBys: [], @@ -661,6 +666,7 @@ var PivotModel = AbstractModel.extend({ * in-memory sort. * * @param {Object} sortedColumn + * @param {number[]} sortedColumn.groupId */ sortRows: function (sortedColumn) { var self = this; @@ -1041,7 +1047,7 @@ var PivotModel = AbstractModel.extend({ var groupId = column.groupId; var measure = column.measure; var isSorted = sortedColumn.measure === measure && - _.isEqual(sortedColumn.groupId, groupId); + _.isEqual(sortedColumn.groupId, groupId); var isSortedByOrigin = isSorted && !sortedColumn.originIndexes[1]; var isSortedByVariation = isSorted && sortedColumn.originIndexes[1]; @@ -1092,6 +1098,31 @@ var PivotModel = AbstractModel.extend({ } return origins; }, + + /** + * Get the selection needed to display the group by dropdown + * @returns {Object[]} + * @private + */ + _getSelectionGroupBy: function (groupBys) { + let groupedFieldNames = groupBys.rowGroupBys + .concat(groupBys.colGroupBys) + .map(function (g) { + return g.split(':')[0]; + }); + + var fields = Object.keys(this.groupableFields) + .map((fieldName, index) => { + return { + name: fieldName, + field: this.groupableFields[fieldName], + active: groupedFieldNames.includes(fieldName) + } + }) + .sort((left, right) => left.field.string < right.field.string ? -1 : 1); + return fields; + }, + /** * Returns a description of the pivot table. * @@ -1132,6 +1163,7 @@ var PivotModel = AbstractModel.extend({ title: "", width: 1, }); + // col groupby cells with group values /** * Recursive function that generates the header cells corresponding to @@ -1139,7 +1171,7 @@ var PivotModel = AbstractModel.extend({ * * @param {Object} tree */ - function generateTreeHeaders(tree) { + function generateTreeHeaders(tree, fields) { var group = tree.root; var rowIndex = group.values.length; var row = colGroupRows[rowIndex]; @@ -1150,6 +1182,7 @@ var PivotModel = AbstractModel.extend({ groupId: groupId, height: isLeaf ? (colGroupBys.length + 1 - rowIndex) : 1, isLeaf: isLeaf, + label: rowIndex === 0 ? undefined : fields[colGroupBys[rowIndex - 1].split(':')[0]].string, title: group.labels[group.labels.length - 1] || _t('Total'), width: leafCount * measureCount * (2 * originCount - 1), }; @@ -1159,10 +1192,11 @@ var PivotModel = AbstractModel.extend({ } _.values(tree.directSubTrees).forEach(function (subTree) { - generateTreeHeaders(subTree); + generateTreeHeaders(subTree, fields); }); } - generateTreeHeaders(this.colGroupTree); + + generateTreeHeaders(this.colGroupTree, this.fields); // blank top right cell for 'Total' group (if there is more that one leaf) if (leafCounts[JSON.stringify(this.colGroupTree.root.values)] > 1) { var groupId = [[], []]; @@ -1205,6 +1239,7 @@ var PivotModel = AbstractModel.extend({ var title = group.labels[group.labels.length - 1] || _t('Total'); var indent = group.labels.length; var isLeaf = _.isEmpty(tree.directSubTrees); + var rowGroupBys = this._getGroupBys().rowGroupBys; var subGroupMeasurements = columns.map(function (column) { var colGroupId = column.groupId; @@ -1226,6 +1261,7 @@ var PivotModel = AbstractModel.extend({ rows.push({ title: title, + label: indent === 0 ? undefined : this.fields[rowGroupBys[indent - 1].split(':')[0]].string, groupId: rowGroupId, indent: indent, isLeaf: isLeaf, @@ -1276,17 +1312,17 @@ var PivotModel = AbstractModel.extend({ _loadData: function () { var self = this; - this.rowGroupTree = {root: {labels: [], values: []}, directSubTrees: {}}; - this.colGroupTree = {root: {labels: [], values: []}, directSubTrees: {}}; + this.rowGroupTree = { root: { labels: [], values: [] }, directSubTrees: {} }; + this.colGroupTree = { root: { labels: [], values: [] }, directSubTrees: {} }; this.measurements = {}; this.counts = {}; - var key = JSON.stringify([[],[]]); + var key = JSON.stringify([[], []]); this.groupDomains = {}; this.groupDomains[key] = this.data.domains.slice(0); - var group = {rowValues: [], colValues: []}; + var group = { rowValues: [], colValues: [] }; var groupBys = this._getGroupBys(); var leftDivisors = sections(groupBys.rowGroupBys); var rightDivisors = sections(groupBys.colGroupBys); @@ -1305,7 +1341,6 @@ var PivotModel = AbstractModel.extend({ * * @private * @param {Object} group - TO DO * @param {Object[]} groupSubdivisions */ _prepareData: function (group, groupSubdivisions) { @@ -1447,7 +1482,7 @@ var PivotModel = AbstractModel.extend({ return this._getNumberedLabel(value, fieldName); } if (fieldName && this.fields[fieldName] && (this.fields[fieldName].type === 'selection')) { - var selected = _.where(this.fields[fieldName].selection, {0: value})[0]; + var selected = _.where(this.fields[fieldName].selection, { 0: value })[0]; return selected ? selected[1] : value; } return value; @@ -1487,7 +1522,11 @@ var PivotModel = AbstractModel.extend({ // if group is known to be empty for the given origin, // we don't need to fetch data fot that origin. if (!self.counts[key] || self.counts[key][originIndex] > 0) { - var subGroup = {rowValues: group.rowValues, colValues: group.colValues, originIndex: originIndex}; + var subGroup = { + rowValues: group.rowValues, + colValues: group.colValues, + originIndex: originIndex + }; divisors.forEach(function (divisor) { acc.push(self._getGroupSubdivision(subGroup, divisor[0], divisor[1])); }); diff --git a/addons/web/static/src/js/views/pivot/pivot_renderer.js b/addons/web/static/src/js/views/pivot/pivot_renderer.js index 3affc975225..6345d6ac0cb 100644 --- a/addons/web/static/src/js/views/pivot/pivot_renderer.js +++ b/addons/web/static/src/js/views/pivot/pivot_renderer.js @@ -1,319 +1,200 @@ odoo.define('web.PivotRenderer', function (require) { -"use strict"; + "use strict"; -var AbstractRenderer = require('web.AbstractRenderer'); -var config = require('web.config'); -var core = require('web.core'); -var dataComparisonUtils = require('web.dataComparisonUtils'); -var field_utils = require('web.field_utils'); - -var QWeb = core.qweb; - -var _t = core._t; - -var PivotRenderer = AbstractRenderer.extend({ - tagName: 'table', - className: 'table-hover table-sm table-bordered', - events: _.extend({}, AbstractRenderer.prototype.events, { - 'click td.o_pivot_cell_value': '_onCellValueClicked', - 'click .o_pivot_header_cell_opened': '_onOpenHeaderClick', - 'click .o_pivot_measure_row': '_onSpecialRowClick', - 'click .o_pivot_origin_row': '_onSpecialRowClick', - 'mouseenter thead tr:last th': '_onMouseenterCell', - 'mouseenter tbody td': '_onMouseenterCell', - 'mouseleave thead tr:last th': '_onMouseleaveCell', - 'mouseleave tbody td': '_onMouseleaveCell', - }), + const { useState, onMounted, onPatched } = owl.hooks; + const { useExternalListener } = require('web.custom_hooks'); + const OwlAbstractRenderer = require('web.AbstractRendererOwl'); + const field_utils = require('web.field_utils'); /** - * @override - * @param {Widget} parent - * @param {Object} state - * @param {Object} params - * @param {boolean} params.enableLinking configure the pivot view to allow - * opening a list view by clicking on a cell with some data. + * Here is a basic example of the structure of the Pivot Table: + * + * ┌─────────────────────────┬─────────────────────────────────────────────┬─────────────────┐ + * │ │ - web.PivotHeader │ │ + * │ ├──────────────────────┬──────────────────────┤ │ + * │ │ + web.PivotHeader │ + web.PivotHeader │ │ + * ├─────────────────────────┼──────────────────────┼──────────────────────┼─────────────────┤ + * │ │ web.PivotMeasure │ web.PivotMeasure │ │ + * ├─────────────────────────┼──────────────────────┼──────────────────────┼─────────────────┤ + * │ ─ web.PivotHeader │ │ │ │ + * ├─────────────────────────┼──────────────────────┼──────────────────────┼─────────────────┤ + * │ + web.PivotHeader │ │ │ │ + * ├─────────────────────────┼──────────────────────┼──────────────────────┼─────────────────┤ + * │ + web.PivotHeader │ │ │ │ + * └─────────────────────────┴──────────────────────┴──────────────────────┴─────────────────┘ + * */ - init: function (parent, state, params) { - this._super.apply(this, arguments); - this.enableLinking = params.enableLinking; - this.fieldWidgets = params.widgets || {}; - this.paddingLeftHeaderTabWidth = config.device.isMobile ? 5 : 30; - }, - //-------------------------------------------------------------------------- - // Private - //-------------------------------------------------------------------------- + class PivotRenderer extends OwlAbstractRenderer { + /** + * @override + * @param {boolean} props.disableLinking Disallow opening records by clicking on a cell + * @param {Object} props.widgets Widgets defined in the arch + */ + constructor(parent, props) { + super(...arguments); + + this.state = useState({ + activeNodeHeader: { + groupId: false, + isXAxis: false, + click: false + }, + }); - /** - * @private - * @param {string[]} groupBy list of 'fieldName[:period]' - * @returns {string[]} list of fields label - */ - _getGroupByLabels: function (groupBy) { - var self = this; - return _.map(groupBy, function (gb) { - return self.state.fields[gb.split(':')[0]].string; - }); - }, - /** - * @override - * @private - * @returns {Promise} - */ - _render: function () { - var hasContent = this.state.hasData && this.state.measures.length; - if (!hasContent) { - // display the nocontent helper - this._replaceElement(QWeb.render('View.NoContentHelper', { - description: _t("Try to add some records, or make sure that there is at least " + - "one measure and no active filter in the search bar."), - })); - return this._super.apply(this, arguments); - } + onMounted(() => this._updateTooltip()); - this.renderElement(); // in case we come from no content helper - this.$el.toggleClass('o_enable_linking', this.enableLinking); + onPatched(() => this._updateTooltip()); - var $thead = $('<thead>'); - var $tbody = $('<tbody>'); - this._renderHeaders($thead); - this._renderRows($tbody); - this.$el.empty().append($thead).append($tbody); - this.$('.o_pivot_header_cell_opened, .o_pivot_header_cell_closed').tooltip(); + if (!this.env.device.isMobile) { + useExternalListener(window, 'click', this._resetState); + } + } - return this._super.apply(this, arguments); - }, - /** - * @private - * @param {jQuery} $thead - */ - _renderHeaders: function ($thead) { - // groupbyLabels is the list of col groupby fields label - var groupbyLabels = this._getGroupByLabels(this.state.colGroupBys); + //---------------------------------------------------------------------- + // Private + //---------------------------------------------------------------------- + + /** + * Get the formatted value of the cell + * + * @private + * @param {Object} cell + * @returns {string} Formatted value + */ + _getFormattedValue(cell) { + const type = this.props.widgets[cell.measure] || + (this.props.fields[cell.measure].type === 'many2one' ? 'integer' : this.props.fields[cell.measure].type); + const formatter = field_utils.format[type]; + return formatter(cell.value, this.props.fields[cell.measure]); + } - this.state.table.headers.forEach(function (row, rowIndex) { - var $tr = $('<tr>'); - row.forEach(function (cell) { - var cellParams = { - text: cell.title, - colspan: cell.width, - rowspan: cell.height, - data: { - groupId: cell.groupId, - type: 'col', - } - } - var className; - if (cell.measure) { - if (cell.originIndexes) { - cellParams.data.originIndexes = cell.originIndexes - className = 'o_pivot_origin_row'; - } else { - className = 'o_pivot_measure_row'; - } - className += ' text-muted'; - if (cell.order) { - className += ' o_pivot_sort_order_' + cell.order; - if (cell.order === 'asc') { - cellParams['aria-sorted'] = 'ascending' - } else { - cellParams['aria-sorted'] = 'descending' - } - } - cellParams.data.measure = cell.measure; - } else if ('isLeaf' in cell) { - if (rowIndex > 0) { - cellParams.title = groupbyLabels[rowIndex - 1]; - } - className = 'o_pivot_header_cell' + (cell.isLeaf ? '_closed' : '_opened'); - } - cellParams.class = className; - - $tr.append($('<th>', cellParams)); - }); - $thead.append($tr); - }); - }, - /** - * @private - * @param {jQuery} $tbody - */ - _renderRows: function ($tbody) { - var self = this; + /** + * Get the formatted variation of a cell + * + * @private + * @param {Object} cell + * @returns {string} Formatted variation + */ + _getFormattedVariation(cell) { + const value = cell.value; + return isNaN(value) ? '-' : field_utils.format.percentage(value, this.props.fields[cell.measure]); + } - // measureTypes is a mapping from measure fields to their field type, - // with a special case for many2one fields which are mapped to the - // 'integer' type (as their group_operator is 'count_distinct') - var measureTypes = this.state.measures.reduce( - function (acc, measureName) { - var type = self.state.fields[measureName].type; - acc[measureName] = type === 'many2one' ? 'integer' : type; - return acc; - }, - {} - ); + /** + * Retrieves the padding of a left header + * + * @private + * @param {Object} cell + * @returns {Number} Padding + */ + _getPadding(cell) { + return 5 + cell.indent * 30; + } - // groupbyLabels is the list of row groupby fields label - var groupbyLabels = this._getGroupByLabels(this.state.rowGroupBys); + /** + * Compute if a cell is active (with its groupId) + * + * @private + * @param {Array} groupId GroupId of a cell + * @param {Boolean} isXAxis true if the cell is on the x axis + * @returns {Boolean} true if the cell is active + */ + _isClicked(groupId, isXAxis) { + return _.isEqual(groupId, this.state.activeNodeHeader.groupId) && this.state.activeNodeHeader.isXAxis === isXAxis; + } - this.state.table.rows.forEach(function (row) { - var $tr = $('<tr>'); - var paddingLeft = 5 + row.indent * self.paddingLeftHeaderTabWidth; - $tr.append($('<th>', { - text: row.title, - title: row.indent > 0 ? groupbyLabels[row.indent - 1] : null, - data: { - groupId: row.groupId, - type: 'row', - }, - css: { - 'padding-left': paddingLeft + 'px', - }, - class: 'o_pivot_header_cell_' + (row.isLeaf ? 'closed' : 'opened'), - })); + /** + * Reset the state of the node. + * + * @private + */ + _resetState() { + // This is a pourrav solution + if (this.isHeaderClicked) { + this.isHeaderClicked = false; + return; + } + this.state.activeNodeHeader = { + groupId: false, + isXAxis: false, + click: false + }; + } - row.subGroupMeasurements.forEach(function (measurement) { - var cellParams = { - data: { - groupId: measurement.groupId, - originIndexes: measurement.originIndexes, - }, - class: 'o_pivot_cell_value text-right', - }; - if (measurement.isBold) { - cellParams.css = { - 'font-weight': 'bold', - }; - } - if (measurement.value !== undefined) { - var measure = measurement.measure; - var measureField = self.state.fields[measure]; - var $value; - if (measurement.originIndexes.length > 1) { - $value = dataComparisonUtils.renderVariation(measurement.value, measureField); - } else { - var formatType = self.fieldWidgets[measure] || measureTypes[measure]; - var formatter = field_utils.format[formatType]; - $value = $('<div>', { - class: 'o_value', - html: formatter(measurement.value, measureField), - }); - } - cellParams.html = $value; - } else { - cellParams.class += ' o_empty' - } - $tr.append($('<td>', cellParams)); - }); - $tbody.append($tr); - }); - }, + /** + * Configure the tooltips on the headers. + * + * @private + */ + _updateTooltip() { + $(this.el).find('.o_pivot_header_cell_opened, .o_pivot_header_cell_closed').tooltip(); + } - //-------------------------------------------------------------------------- - // Handlers - //-------------------------------------------------------------------------- + //---------------------------------------------------------------------- + // Handlers + //---------------------------------------------------------------------- - /** - * When the user clicks on a non empty cell, and the view is configured to - * allow 'linking' (with enableLinking), we want to open a list view with - * the corresponding record. - * - * @private - * @param {MouseEvent} ev - */ - _onCellValueClicked: function (ev) { - ev.preventDefault(); - ev.stopPropagation(); - var $target = $(ev.currentTarget); - if ($target.hasClass('o_empty') || !this.enableLinking) { - return; + /** + * Handles a click on a menu item in the dropdown to select a groupby. + * + * @private + * @param {Object} field + * @param {string} interval + */ + _onClickMenuGroupBy(field, interval) { + this.trigger('groupby_menu_selection', { field, interval }); } - var context = _.omit(this.state.context, function (val, key) { - return key === 'group_by' || _.str.startsWith(key, 'search_default_'); - }); - - var groupId = $target.data('groupId'); - var originIndexes = $target.data('originIndexes'); - var group = { - rowValues: groupId[0], - colValues: groupId[1], - originIndex: originIndexes[0] - }; - - this.trigger_up('open_view', { - group: group, - context: context, - }); - }, - /** - * Highlight the column when hovering a cell. - * - * @private - * @param {MouseEvent} ev - */ - _onMouseenterCell: function (ev) { - var index = $(ev.currentTarget).index(); - if ($(ev.currentTarget).is('th')) { // header cell - index += 1; // increment by 1 to compensate the top left empty cell + /** + * Handles a click on a header node + * + * @private + * @param {Object} cell + * @param {string} type col or row + */ + _onHeaderClick(cell, type) { + const groupValues = cell.groupId[type === 'col' ? 1 : 0]; + const groupByLength = type === 'col' ? this.props.colGroupBys.length : this.props.rowGroupBys.length; + if (cell.isLeaf && groupValues.length >= groupByLength) { + this.isHeaderClicked = true; + this.state.activeNodeHeader = { + groupId: cell.groupId, + isXAxis: type === 'col', + click: 'leftClick' + }; + } + this.trigger(cell.isLeaf ? 'closed_header_click' : 'opened_header_click', { cell, type }); } - this.$("td").filter(":nth-child(" + (index + 1) + ")").addClass("o_cell_hover"); - }, - /** - * @private - */ - _onMouseleaveCell: function () { - this.$('.o_cell_hover').removeClass('o_cell_hover'); - }, - /** - * This method is called when someone clicks on an open header. When that - * happens, we want to close the header, then redisplay the view. - * - * @private - * @param {MouseEvent} ev - */ - _onOpenHeaderClick: function (ev) { - ev.preventDefault(); - ev.stopImmediatePropagation(); - var $target = $(ev.target); - var groupId = $target.data('groupId'); - var type = $target.data('type'); + /** + * Hover the column in which the mouse is. + * + * @private + * @param {MouseEvent} ev + */ + _onMouseEnter(ev) { + var index = [...ev.currentTarget.parentNode.children].indexOf(ev.currentTarget); + if (ev.currentTarget.tagName === 'TH') { + index += 1; + } + this.el.querySelectorAll('td:nth-child(' + (index + 1) + ')').forEach(elt => elt.classList.add('o_cell_hover')); + } - this.trigger_up('close_group', { - groupId: groupId, - type: type, - }); - }, - /** - * If the user clicks on a measure or origin row, we perform an in-memory sort. - * - * @private - * @param {MouseEvent} ev - */ - _onSpecialRowClick: function (ev) { - ev.preventDefault(); - ev.stopImmediatePropagation(); + /** + * Remove the hover on the columns. + * + * @private + */ + _onMouseLeave() { + this.el.querySelectorAll('.o_cell_hover').forEach(elt => elt.classList.remove('o_cell_hover')); + } + } - var $target = $(ev.target); - var groupId = $target.data('groupId'); - var measure = $target.data('measure'); - var originIndexes = $target.data('originIndexes'); - var isAscending = $target.hasClass('o_pivot_sort_order_asc'); - var order = isAscending ? 'desc' : 'asc'; + PivotRenderer.template = 'web.PivotRenderer'; - this.trigger_up('sort_rows', { - sortedColumn: { - groupId: groupId, - measure: measure, - order: order, - originIndexes: originIndexes, - } - }); - }, -}); + return PivotRenderer; -return PivotRenderer; }); diff --git a/addons/web/static/src/js/views/pivot/pivot_view.js b/addons/web/static/src/js/views/pivot/pivot_view.js index 1b878cf3530..17cc478ba9b 100644 --- a/addons/web/static/src/js/views/pivot/pivot_view.js +++ b/addons/web/static/src/js/views/pivot/pivot_view.js @@ -1,137 +1,150 @@ odoo.define('web.PivotView', function (require) { -"use strict"; - -/** - * The Pivot View is a view that represents data in a 'pivot grid' form. It - * aggregates data on 2 dimensions and displays the result, allows the user to - * 'zoom in' data. - */ - -var AbstractView = require('web.AbstractView'); -var config = require('web.config'); -var core = require('web.core'); -var PivotModel = require('web.PivotModel'); -var PivotController = require('web.PivotController'); -var PivotRenderer = require('web.PivotRenderer'); - -var _t = core._t; -var _lt = core._lt; - -var controlPanelViewParameters = require('web.controlPanelViewParameters'); -var GROUPABLE_TYPES = controlPanelViewParameters.GROUPABLE_TYPES; - -var PivotView = AbstractView.extend({ - display_name: _lt('Pivot'), - icon: 'fa-table', - config: _.extend({}, AbstractView.prototype.config,{ - Model: PivotModel, - Controller: PivotController, - Renderer: PivotRenderer, - }), - viewType: 'pivot', - searchMenuTypes: ['filter', 'groupBy', 'timeRange', 'favorite'], + "use strict"; /** - * @override - * @param {Object} params + * The Pivot View is a view that represents data in a 'pivot grid' form. It + * aggregates data on 2 dimensions and displays the result, allows the user to + * 'zoom in' data. */ - init: function (viewInfo, params) { - var self = this; - this._super.apply(this, arguments); - - var activeMeasures = []; - var colGroupBys = []; - var rowGroupBys = []; - - var measures = {}; - var groupableFields = {}; - var widgets = {}; - - this.fields.__count = {string: _t("Count"), type: "integer"}; - _.each(this.fields, function (field, name) { - if ((name !== 'id') && (field.store === true)) { - if (_.contains(['integer', 'float', 'monetary'], field.type) || - _.contains(params.additionalMeasures, name)) { + + const AbstractView = require('web.AbstractView'); + const config = require('web.config'); + const core = require('web.core'); + const PivotModel = require('web.PivotModel'); + const PivotController = require('web.PivotController'); + const PivotRenderer = require('web.PivotRenderer'); + const RendererWrapper = require('web.RendererWrapper'); + + const _t = core._t; + const _lt = core._lt; + + const controlPanelViewParameters = require('web.controlPanelViewParameters'); + const GROUPABLE_TYPES = controlPanelViewParameters.GROUPABLE_TYPES; + + const PivotView = AbstractView.extend({ + display_name: _lt('Pivot'), + icon: 'fa-table', + config: Object.assign({}, AbstractView.prototype.config, { + Model: PivotModel, + Controller: PivotController, + Renderer: PivotRenderer, + }), + viewType: 'pivot', + searchMenuTypes: ['filter', 'groupBy', 'timeRange', 'favorite'], + + /** + * @override + * @param {Object} params + * @param {Array} params.additionalMeasures + */ + init: function (viewInfo, params) { + this._super.apply(this, arguments); + + const activeMeasures = []; // Store the defined active measures + const colGroupBys = []; // Store the defined group_by used on cols + const rowGroupBys = []; // Store the defined group_by used on rows + const measures = {}; // All the available measures + const groupableFields = {}; // The fields which can be used to group data + const widgets = {}; // Wigdets defined in the arch + const additionalMeasures = params.additionalMeasures || []; + + this.fields.__count = { string: _t("Count"), type: "integer" }; + + //Compute the measures and the groupableFields + Object.keys(this.fields).forEach(name => { + const field = this.fields[name]; + if (name !== 'id' && field.store === true) { + if (['integer', 'float', 'monetary'].includes(field.type) || additionalMeasures.includes(name)) { measures[name] = field; + } + if (GROUPABLE_TYPES.includes(field.type)) { + groupableFields[name] = field; + } + } + }); + measures.__count = { string: _t("Count"), type: "integer" }; + + + this.arch.children.forEach(field => { + let name = field.attrs.name; + + // Remove invisible fields from the measures + if (field.attrs.invisible && py.eval(field.attrs.invisible)) { + delete measures[name]; + return; + } + if (field.attrs.interval) { + name += ':' + field.attrs.interval; + } + if (field.attrs.widget) { + widgets[name] = field.attrs.widget; + } + // add active measures to the measure list. This is very rarely + // necessary, but it can be useful if one is working with a + // functional field non stored, but in a model with an overrided + // read_group method. In this case, the pivot view could work, and + // the measure should be allowed. However, be careful if you define + // a measure in your pivot view: non stored functional fields will + // probably not work (their aggregate will always be 0). + if (field.attrs.type === 'measure' && !(name in measures)) { + measures[name] = this.fields[name]; + } + if (field.attrs.string) { + measures[name].string = field.attrs.string; + } + if (field.attrs.type === 'measure' || 'operator' in field.attrs) { + activeMeasures.push(name); + measures[name] = this.fields[name]; + } + if (field.attrs.type === 'col') { + colGroupBys.push(name); } - if (_.contains(GROUPABLE_TYPES, field.type)) { - groupableFields[name] = field; + if (field.attrs.type === 'row') { + rowGroupBys.push(name); } + }); + if ((!activeMeasures.length) || this.arch.attrs.display_quantity) { + activeMeasures.splice(0, 0, '__count'); } - }); - measures.__count = {string: _t("Count"), type: "integer"}; - this.arch.children.forEach(function (field) { - var name = field.attrs.name; - if (field.attrs.interval) { - name += ':' + field.attrs.interval; - } - if (field.attrs.widget) { - widgets[name] = field.attrs.widget; - } - if (field.attrs.invisible && py.eval(field.attrs.invisible)) { - delete measures[name]; - return; - } - // add active measures to the measure list. This is very rarely - // necessary, but it can be useful if one is working with a - // functional field non stored, but in a model with an overrided - // read_group method. In this case, the pivot view could work, and - // the measure should be allowed. However, be careful if you define - // a measure in your pivot view: non stored functional fields will - // probably not work (their aggregate will always be 0). - if (field.attrs.type === 'measure' && !(name in measures)) { - measures[name] = self.fields[name]; - } - if (field.attrs.string) { - measures[name].string = field.attrs.string; - } - if (field.attrs.type === 'measure' || 'operator' in field.attrs) { - activeMeasures.push(name); - measures[name] = self.fields[name]; + this.loadParams.measures = activeMeasures; + this.loadParams.colGroupBys = config.device.isMobile ? [] : colGroupBys; + this.loadParams.rowGroupBys = rowGroupBys; + this.loadParams.fields = this.fields; + this.loadParams.default_order = params.default_order || this.arch.attrs.default_order; + this.loadParams.groupableFields = groupableFields; + + this.rendererParams.widgets = widgets; + this.rendererParams.disableLinking = this.arch.attrs.disable_linking; + + this.controllerParams.title = params.title || this.arch.attrs.string || _t("Untitled"); + this.controllerParams.measures = measures; + + // retrieve form and list view ids from the action to open those views + // when a data cell of the pivot view is clicked + this.controllerParams.views = [ + _findView(params.actionViews, 'list'), + _findView(params.actionViews, 'form'), + ]; + + function _findView(views, viewType) { + const view = views.find(view => { + return view.type === viewType; + }); + return [view ? view.viewID : false, viewType]; } - if (field.attrs.type === 'col') { - colGroupBys.push(name); - } - if (field.attrs.type === 'row') { - rowGroupBys.push(name); - } - }); - if ((!activeMeasures.length) || this.arch.attrs.display_quantity) { - activeMeasures = ['__count'].concat(activeMeasures); - } - - this.loadParams.measures = activeMeasures; - this.loadParams.colGroupBys = colGroupBys; - this.loadParams.rowGroupBys = rowGroupBys; - this.loadParams.fields = this.fields; - this.loadParams.default_order = params.default_order || this.arch.attrs.default_order; - if (config.device.isMobile) { - this.loadParams.colGroupBys = []; - } - - - this.rendererParams.widgets = widgets; - this.rendererParams.enableLinking = !this.arch.attrs.disable_linking; - - this.controllerParams.title = params.title || this.arch.attrs.string || _t("Untitled"); - this.controllerParams.measures = measures; - this.controllerParams.groupableFields = groupableFields; - // retrieve form and list view ids from the action to open those views - // when a data cell of the pivot view is clicked - this.controllerParams.views = [ - _findView(params.actionViews, 'list'), - _findView(params.actionViews, 'form'), - ]; - function _findView(views, viewType) { - var view = _.find(views, function (view) { - return view.type === viewType; - }); - return [view ? view.viewID : false, viewType]; - } - }, -}); - -return PivotView; + }, + + /** + * + * @override + */ + getRenderer(parent, state) { + state = Object.assign(state || {}, this.rendererParams); + return new RendererWrapper(null, state, this.config.Renderer); + }, + }); + + return PivotView; }); |
