summaryrefslogtreecommitdiff
path: root/addons/web/static/src/js/views/pivot/pivot_renderer.js
diff options
context:
space:
mode:
authorPierre Rousseau <pro@odoo.com>2019-12-03 09:58:52 +0000
committerVincent Schippefilt <vsc@odoo.com>2019-12-09 12:53:24 +0000
commit46cf65d68e9c13193dff4b563b9b0bbb2199dfc1 (patch)
tree4338abceab39a837283a4075587b9c12d616ba67 /addons/web/static/src/js/views/pivot/pivot_renderer.js
parenteef10097aefe04c3a89d5226727a45d540bf8f63 (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/pivot/pivot_renderer.js')
-rw-r--r--addons/web/static/src/js/views/pivot/pivot_renderer.js463
1 files changed, 172 insertions, 291 deletions
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;
});