From a54ca01096daeb69d442b70903f921909bead4a3 Mon Sep 17 00:00:00 2001 From: Jairo Llopis Date: Tue, 21 Aug 2018 13:45:09 +0100 Subject: [PATCH 1/3] [FIX] web_widget_x2many_2d_matrix: Fix linters --- web_widget_x2many_2d_matrix/README.rst | 2 +- .../static/src/js/2d_matrix_renderer.js | 903 ++++++++++-------- .../static/src/js/widget_x2many_2d_matrix.js | 343 +++---- 3 files changed, 666 insertions(+), 582 deletions(-) diff --git a/web_widget_x2many_2d_matrix/README.rst b/web_widget_x2many_2d_matrix/README.rst index 52eb81b1..aba2f90a 100644 --- a/web_widget_x2many_2d_matrix/README.rst +++ b/web_widget_x2many_2d_matrix/README.rst @@ -154,7 +154,7 @@ Contributors * Artem Kostyuk * Simone Orsi * Timon Tschanz - +* Jairo Llopis Maintainer ---------- diff --git a/web_widget_x2many_2d_matrix/static/src/js/2d_matrix_renderer.js b/web_widget_x2many_2d_matrix/static/src/js/2d_matrix_renderer.js index 092278a5..6cb66ac2 100644 --- a/web_widget_x2many_2d_matrix/static/src/js/2d_matrix_renderer.js +++ b/web_widget_x2many_2d_matrix/static/src/js/2d_matrix_renderer.js @@ -2,422 +2,491 @@ * License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). */ odoo.define('web_widget_x2many_2d_matrix.X2Many2dMatrixRenderer', function (require) { - "use strict"; - - // heavily inspired by Odoo's `ListRenderer` - var BasicRenderer = require('web.BasicRenderer'); - var config = require('web.config'); - var field_utils = require('web.field_utils'); - var utils = require('web.utils'); - var core = require('web.core'); - var _t = core._t - var FIELD_CLASSES = { - // copied from ListRenderer - float: 'o_list_number', - integer: 'o_list_number', - monetary: 'o_list_number', - text: 'o_list_text', - }; - - var X2Many2dMatrixRenderer = BasicRenderer.extend({ - - init: function (parent, state, params) { - this._super.apply(this, arguments); - this.editable = params.editable; - this.columns = params.matrix_data.columns; - this.rows = params.matrix_data.rows; - this.matrix_data = params.matrix_data; - }, - /** - * Main render function for the matrix widget. It is rendered as a table. For now, - * this method does not wait for the field widgets to be ready. - * - * @override - * @private - * returns {Deferred} this deferred is resolved immediately - */ - _renderView: function () { - var self = this; - - // Display a nice message if there's no data to display - this.$el.empty(); - if (!self.rows.length){ - var $alert = $('
', {'class': 'alert alert-info'}); - $alert.text(_t('Sorry no matrix data to display.')); - this.$el.append($alert); - return this._super(); - } - - var $table = $('').addClass('o_list_view table table-condensed table-striped'); - this.$el - .addClass('table-responsive') - .append($table); - - this._computeColumnAggregates(); - this._computeRowAggregates(); - - $table - .append(this._renderHeader()) - .append(this._renderBody()); - if (self.matrix_data.show_column_totals) { - $table.append(this._renderFooter()); - } - return this._super(); - }, - /** - * Render the table body. Looks for the table body and renders the rows in it. - * Also it sets the tabindex on every input element. - * - * @private - * return {jQueryElement} The table body element that was just filled. - */ - _renderBody: function () { - var $body = $('').append(this._renderRows()); - _.each($body.find('input'), function (td, i) { - $(td).attr('tabindex', i); - }); - return $body; - }, - /** - * Render the table head of our matrix. Looks for the first table head - * and inserts the header into it. - * - * @private - * @return {jQueryElement} The thead element that was inserted into. - */ - _renderHeader: function () { - var $tr = $('').append('').append($tr); - }, - /** - * Render a single header cell. Creates a th and adds the description as text. - * - * @private - * @param {jQueryElement} node - * @returns {jQueryElement} the created . - * If aggregate is set on the row it also will generate the aggregate cell. - * - * @private - * @param {Object} row: The row that will be rendered. - * @returns {jQueryElement} the element that has been rendered. - */ - _renderRow: function (row) { - var self = this; - var $tr = $('', {class: 'o_data_row'}); - $tr = $tr.append(self._renderLabelCell(row.data[0])); - var $cells = _.map(this.columns, function (node, index) { - var record = row.data[index]; - // make the widget use our field value for each cell - node.attrs.name = self.matrix_data.field_value; - return self._renderBodyCell(record, node, index, {mode:''}); - }); - $tr = $tr.append($cells); - if (row.aggregate) { - $tr.append(self._renderAggregateRowCell(row)); - } - return $tr; - }, - /** - * Renders the label for a specific row. - * - * @private - * @params {Object} record: Contains the information about the record. - * @params {jQueryElement} the cell that was rendered. - */ - _renderLabelCell: function(record) { - var $td = $('').append($('').append('
'); - $tr= $tr.append(_.map(this.columns, this._renderHeaderCell.bind(this))); - if (this.matrix_data.show_row_totals) { - $tr.append($('', {class: 'total'})); - } - return $('
node. - */ - _renderHeaderCell: function (node) { - var name = node.attrs.name; - var field = this.state.fields[name]; - var $th = $(''); - if (!field) { - return $th; - } - var description; - if (node.attrs.widget) { - description = this.state.fieldsInfo.list[name].Widget.prototype.description; - } - if (description === undefined) { - description = node.attrs.string || field.string; - } - $th.text(description).data('name', name); - - if (field.type === 'float' || field.type === 'integer' || field.type === 'monetary') { - $th.addClass('text-right'); - } - - if (config.debug) { - var fieldDescr = { - field: field, - name: name, - string: description || name, - record: this.state, - attrs: node.attrs, - }; - this._addFieldTooltip(fieldDescr, $th); - } - return $th; - }, - /** - * Proxy call to function rendering single row. - * - * @private - * @returns {String} a string with the generated html. - * - */ - - _renderRows: function () { - return _.map(this.rows, this._renderRow.bind(this)); - }, - /** - * Render a single row with all its columns. Renders all the cells and then wraps them with a
'); - var value = record.data[this.matrix_data.field_y_axis]; - if (value.type == 'record') { - // we have a related record - value = value.data.display_name; - } - // get 1st column filled w/ Y label - $td.text(value); - return $td; - }, - /** - * Create a cell and fill it with the aggregate value. - * - * @private - * @param {Object} row: the row object to aggregate. - * @returns {jQueryElement} The rendered cell. - */ - _renderAggregateRowCell: function (row) { - var $cell = $('', {class: 'row-total text-right'}); - this._apply_aggregate_value($cell, row.aggregate); - return $cell; - }, - /** - * Render a single body Cell. - * Gets the field and renders the widget. We force the edit mode, since - * we always want the widget to be editable. - * - * @private - * @param {Object} record: Contains the data for this cell - * @param {jQueryElement} node: The HTML of the field. - * @param {int} colIndex: The index of the current column. - * @param {Object} options: The obtions used for the widget - * @returns {jQueryElement} the rendered cell. - */ - _renderBodyCell: function (record, node, colIndex, options) { - var tdClassName = 'o_data_cell'; - if (node.tag === 'button') { - tdClassName += ' o_list_button'; - } else if (node.tag === 'field') { - var typeClass = FIELD_CLASSES[this.state.fields[node.attrs.name].type]; - if (typeClass) { - tdClassName += (' ' + typeClass); - } - if (node.attrs.widget) { - tdClassName += (' o_' + node.attrs.widget + '_cell'); - } - } - // TODO roadmap: here we should collect possible extra params - // the user might want to attach to each single cell. - var $td = $('', { - 'class': tdClassName, - 'data-form-id': record.id, - 'data-id': record.data.id, - }); - // We register modifiers on the element so that it gets the correct - // modifiers classes (for styling) - var modifiers = this._registerModifiers(node, record, $td, _.pick(options, 'mode')); - // If the invisible modifiers is true, the element is left empty. - // Indeed, if the modifiers was to change the whole cell would be - // rerendered anyway. - if (modifiers.invisible && !(options && options.renderInvisible)) { - return $td; - } - options.mode = this.getParent().mode; // enforce mode of the parent - var widget = this._renderFieldWidget(node, record, _.pick(options, 'mode')); - this._handleAttributes(widget.$el, node); - return $td.append(widget.$el); - }, - /** - * Wraps the column aggregate with a tfoot element - * - * @private - * @returns {jQueryElement} The footer element with the cells in it. - */ - _renderFooter: function () { - var $cells = this._renderAggregateColCells(); - if ($cells) { - return $('
').append($cells)); - } - return; - }, - /** - * Render the Aggregate cells for the column. - * - * @private - * @returns {List} the rendered cells - */ - _renderAggregateColCells: function () { - var self = this; - return _.map(this.columns, function (column, index) { - var $cell = $('', {class: 'col-total text-right'}); - if (column.aggregate) { - self._apply_aggregate_value($cell, column.aggregate); - } - return $cell; - }); - }, - /** - * Compute the column aggregates. - * This function is called everytime the value is changed. - * - * @private - */ - _computeColumnAggregates: function () { - if (!this.matrix_data.show_column_totals) { - return; - } - var self = this, - fname = this.matrix_data.field_value, - field = this.state.fields[fname]; - if (!field) { return; } - var type = field.type; - if (type !== 'integer' && type !== 'float' && type !== 'monetary') { - return; - } - _.each(self.columns, function (column, index) { - column.aggregate = { - fname: fname, - ftype: type, - // TODO: translate - help: 'Sum', - value: 0 - }; - _.each(self.rows, function (row) { - // var record = _.findWhere(self.state.data, {id: col.data.id}); - column.aggregate.value += row.data[index].data[fname]; - }); - }); - }, + "use strict"; + + // heavily inspired by Odoo's `ListRenderer` + var BasicRenderer = require('web.BasicRenderer'); + var config = require('web.config'); + var core = require('web.core'); + var field_utils = require('web.field_utils'); + var _t = core._t; + var FIELD_CLASSES = { + // copied from ListRenderer + float: 'o_list_number', + integer: 'o_list_number', + monetary: 'o_list_number', + text: 'o_list_text', + }; + + var X2Many2dMatrixRenderer = BasicRenderer.extend({ + + /** + * @override + */ + init: function (parent, state, params) { + this._super.apply(this, arguments); + this.editable = params.editable; + this.columns = params.matrix_data.columns; + this.rows = params.matrix_data.rows; + this.matrix_data = params.matrix_data; + }, + + /** + * Main render function for the matrix widget. + * + * It is rendered as a table. For now, + * this method does not wait for the field widgets to be ready. + * + * @override + * @private + * @returns {Deferred} this deferred is resolved immediately + */ + _renderView: function () { + var self = this; + + // Display a nice message if there's no data to display + this.$el.empty(); + if (!self.rows.length) { + var $alert = $('
', {'class': 'alert alert-info'}); + $alert.text(_t('Sorry no matrix data to display.')); + this.$el.append($alert); + return this._super(); + } + + var $table = $('').addClass( + 'o_list_view table table-condensed table-striped' + ); + this.$el + .addClass('table-responsive') + .append($table); + + this._computeColumnAggregates(); + this._computeRowAggregates(); + + $table + .append(this._renderHeader()) + .append(this._renderBody()); + if (self.matrix_data.show_column_totals) { + $table.append(this._renderFooter()); + } + return this._super(); + }, + + /** + * Render the table body. + * + * Looks for the table body and renders the rows in it. + * Also it sets the tabindex on every input element. + * + * @private + * @returns {jQueryElement} The table body element just filled. + */ + _renderBody: function () { + var $body = $('').append(this._renderRows()); + _.each($body.find('input'), function (td, i) { + $(td).attr('tabindex', i); + }); + return $body; + }, + /** - * Compute the row aggregates. - * This function is called everytime the value is changed. - * - * @private - */ - _computeRowAggregates: function () { - if (!this.matrix_data.show_row_totals) { - return; - } - var self = this, - fname = this.matrix_data.field_value, - field = this.state.fields[fname]; - if (!field) { return; } - var type = field.type; - if (type !== 'integer' && type !== 'float' && type !== 'monetary') { - return; - } - _.each(self.rows, function (row) { - row.aggregate = { - fname: fname, - ftype: type, - // TODO: translate - help: 'Sum', - value: 0 - }; - _.each(row.data, function (col) { - row.aggregate.value += col.data[fname]; - }); - }); - }, - /** - * Takes the given Value, formats it and adds it to the given cell. - * - * @private - * @param {jQueryElement} $cell: The Cell where the aggregate should be added. - * @param {Object} aggregate: The object which contains the information about the aggregate value - */ - _apply_aggregate_value: function ($cell, aggregate) { - var field = this.state.fields[aggregate.fname], - formatter = field_utils.format[field.type]; - var formattedValue = formatter(aggregate.value, field, {escape: true, }); - $cell.addClass('total').attr('title', aggregate.help).html(formattedValue); - }, - /** - * Check if the change was successful and then update the grid. - * This function is required on relational fields. - * - * @params {Object} state: Contains the current state of the field & all the data - * @params {String} id: the id of the updated object. - * @params {Array} fields: The fields we have in the view. - * @params {Object} ev: The event object. - * @returns {Deferred} The deferred object thats gonna be resolved when the change is made. - */ - confirmUpdate: function (state, id, fields, ev) { - var self = this; - this.state = state; - return this.confirmChange(state, id, fields, ev).then(function () { - self._refresh(id); - }); - }, - /** - * Refresh our grid. - * - * @private - */ - _refresh: function (id) { - this._updateRow(id); - this._refreshColTotals(); - this._refreshRowTotals(); - }, - /** - *Update row data in our internal rows. - * - * @params {String} id: The id of the row that needs to be updated. - */ - _updateRow: function (id) { - var self = this, - record = _.findWhere(self.state.data, {id: id}); - _.each(self.rows, function(row) { - _.each(row.data, function(col, i) { - if (col.id == id) { - row.data[i] = record; - } - }); - }); - }, - /** - * Update the row total. - */ - _refreshColTotals: function () { - this._computeColumnAggregates(); - this.$('tfoot').replaceWith(this._renderFooter()); - }, - /** - * Update the column total. - */ - _refreshRowTotals: function () { - var self = this; - this._computeRowAggregates(); - var $rows = self.$el.find('tr.o_data_row'); - _.each(self.rows, function(row, i) { - if (row.aggregate) { - $($rows[i]).find('.row-total') - .replaceWith(self._renderAggregateRowCell(row)); - } - }); - }, - /* - x2m fields expect this - */ - getEditableRecordID: function (){ return false;} - - }); - - return X2Many2dMatrixRenderer; + * Render the table head of our matrix. Looks for the first table head + * and inserts the header into it. + * + * @private + * @returns {jQueryElement} The thead element that was inserted into. + */ + _renderHeader: function () { + var $tr = $('').append('').append($tr); + }, + + /** + * Render a single header cell. + * + * Creates a th and adds the description as text. + * + * @private + * @param {jQueryElement} node + * @returns {jQueryElement} the created . + * If aggregate is set on the row it also will generate + * the aggregate cell. + * + * @private + * @param {Object} row: The row that will be rendered. + * @returns {jQueryElement} the element that has been rendered. + */ + _renderRow: function (row) { + var self = this; + var $tr = $('', {class: 'o_data_row'}); + $tr = $tr.append(self._renderLabelCell(row.data[0])); + var $cells = _.map(this.columns, function (node, index) { + var record = row.data[index]; + // make the widget use our field value for each cell + node.attrs.name = self.matrix_data.field_value; + return self._renderBodyCell(record, node, index, {mode:''}); + }); + $tr = $tr.append($cells); + if (row.aggregate) { + $tr.append(self._renderAggregateRowCell(row)); + } + return $tr; + }, + + /** + * Renders the label for a specific row. + * + * @private + * @param {Object} record Contains the information about the record. + * @returns {jQueryElement} the cell that was rendered. + */ + _renderLabelCell: function (record) { + var $td = $('').append( + $('').append(' element that has been rendered. */ _renderRow: function (row) { - var self = this; - var $tr = $('', {class: 'o_data_row'}); - $tr = $tr.append(self._renderLabelCell(row.data[0])); + var $tr = $('', {class: 'o_data_row'}), + _data = _.without(row.data, undefined); + $tr = $tr.append(this._renderLabelCell(_data[0])); var $cells = _.map(this.columns, function (node, index) { var record = row.data[index]; // Make the widget use our field value for each cell - node.attrs.name = self.matrix_data.field_value; - return self._renderBodyCell(record, node, index, {mode:''}); - }); + node.attrs.name = this.matrix_data.field_value; + return this._renderBodyCell(record, node, index, {mode:''}); + }.bind(this)); $tr = $tr.append($cells); if (row.aggregate) { - $tr.append(self._renderAggregateRowCell(row)); + $tr.append(this._renderAggregateRowCell(row)); } return $tr; }, @@ -223,7 +223,7 @@ odoo.define('web_widget_x2many_2d_matrix.X2Many2dMatrixRenderer', function (requ * Create a cell and fill it with the aggregate value. * * @private - * @param {Object} row: the row object to aggregate. + * @param {Object} row the row object to aggregate. * @returns {jQueryElement} The rendered cell. */ _renderAggregateRowCell: function (row) { @@ -238,10 +238,10 @@ odoo.define('web_widget_x2many_2d_matrix.X2Many2dMatrixRenderer', function (requ * we always want the widget to be editable. * * @private - * @param {Object} record: Contains the data for this cell - * @param {jQueryElement} node: The HTML of the field. - * @param {int} colIndex: The index of the current column. - * @param {Object} options: The obtions used for the widget + * @param {Object} record Contains the data for this cell + * @param {jQueryElement} node The HTML of the field. + * @param {int} colIndex The index of the current column. + * @param {Object} options The obtions used for the widget * @returns {jQueryElement} the rendered cell. */ _renderBodyCell: function (record, node, colIndex, options) { @@ -263,6 +263,12 @@ odoo.define('web_widget_x2many_2d_matrix.X2Many2dMatrixRenderer', function (requ // the user might want to attach to each single cell. var $td = $('
'); + $tr = $tr.append(_.map( + this.columns, + this._renderHeaderCell.bind(this) + )); + if (this.matrix_data.show_row_totals) { + $tr.append($('', {class: 'total'})); + } + return $('
node. + */ + _renderHeaderCell: function (node) { + var name = node.attrs.name; + var field = this.state.fields[name]; + var $th = $(''); + if (!field) { + return $th; + } + var description = null; + if (node.attrs.widget) { + description = this.state.fieldsInfo.list[name] + .Widget.prototype.description; + } + if (_.isNull(description)) { + description = node.attrs.string || field.string; + } + $th.text(description).data('name', name); + + if ( + field.type === 'float' || field.type === 'integer' || + field.type === 'monetary' + ) { + $th.addClass('text-right'); + } + + if (config.debug) { + var fieldDescr = { + field: field, + name: name, + string: description || name, + record: this.state, + attrs: node.attrs, + }; + this._addFieldTooltip(fieldDescr, $th); + } + return $th; + }, + + /** + * Proxy call to function rendering single row. + * + * @private + * @returns {String} a string with the generated html. + * + */ + _renderRows: function () { + return _.map(this.rows, this._renderRow.bind(this)); + }, + + /** + * Render a single row with all its columns. + * Renders all the cells and then wraps them with a
'); + var value = record.data[this.matrix_data.field_y_axis]; + if (value.type === 'record') { + // we have a related record + value = value.data.display_name; + } + // get 1st column filled w/ Y label + $td.text(value); + return $td; + }, + + /** + * Create a cell and fill it with the aggregate value. + * + * @private + * @param {Object} row: the row object to aggregate. + * @returns {jQueryElement} The rendered cell. + */ + _renderAggregateRowCell: function (row) { + var $cell = $('', {class: 'row-total text-right'}); + this._apply_aggregate_value($cell, row.aggregate); + return $cell; + }, + + /** + * Render a single body Cell. + * Gets the field and renders the widget. We force the edit mode, since + * we always want the widget to be editable. + * + * @private + * @param {Object} record: Contains the data for this cell + * @param {jQueryElement} node: The HTML of the field. + * @param {int} colIndex: The index of the current column. + * @param {Object} options: The obtions used for the widget + * @returns {jQueryElement} the rendered cell. + */ + _renderBodyCell: function (record, node, colIndex, options) { + var tdClassName = 'o_data_cell'; + if (node.tag === 'button') { + tdClassName += ' o_list_button'; + } else if (node.tag === 'field') { + var typeClass = FIELD_CLASSES[ + this.state.fields[node.attrs.name].type + ]; + if (typeClass) { + tdClassName += ' ' + typeClass; + } + if (node.attrs.widget) { + tdClassName += ' o_' + node.attrs.widget + '_cell'; + } + } + // TODO roadmap: here we should collect possible extra params + // the user might want to attach to each single cell. + var $td = $('', { + 'class': tdClassName, + 'data-form-id': record.id, + 'data-id': record.data.id, + }); + // We register modifiers on the element so that it gets + // the correct modifiers classes (for styling) + var modifiers = this._registerModifiers( + node, + record, + $td, + _.pick(options, 'mode') + ); + // If the invisible modifiers is true, the element is + // left empty. Indeed, if the modifiers was to change the + // whole cell would be rerendered anyway. + if (modifiers.invisible && !(options && options.renderInvisible)) { + return $td; + } + // enforce mode of the parent + options.mode = this.getParent().mode; + var widget = this._renderFieldWidget( + node, record, _.pick(options, 'mode') + ); + this._handleAttributes(widget.$el, node); + return $td.append(widget.$el); + }, + + /** + * Wraps the column aggregate with a tfoot element + * + * @private + * @returns {jQueryElement} The footer element with the cells in it. + */ + _renderFooter: function () { + var $cells = this._renderAggregateColCells(); + if ($cells) { + return $('
').append($cells) + ); + } + }, + + /** + * Render the Aggregate cells for the column. + * + * @private + * @returns {List} the rendered cells + */ + _renderAggregateColCells: function () { + var self = this; + return _.map(this.columns, function (column) { + var $cell = $('', {class: 'col-total text-right'}); + if (column.aggregate) { + self._apply_aggregate_value($cell, column.aggregate); + } + return $cell; + }); + }, + + /** + * Compute the column aggregates. + * This function is called everytime the value is changed. + * + * @private + */ + _computeColumnAggregates: function () { + if (!this.matrix_data.show_column_totals) { + return; + } + var fname = this.matrix_data.field_value, + field = this.state.fields[fname]; + if (!field) { + return; + } + var type = field.type; + if (!_.inArray(type, ['integer', 'float', 'monetary'])) { + return; + } + _.each(this.columns, function (column, index) { + column.aggregate = { + fname: fname, + ftype: type, + help: _t('Sum'), + value: 0, + }; + _.each(this.rows, function (row) { + column.aggregate.value += row.data[index].data[fname]; + }); + }); + }, + + /** + * Compute the row aggregates. + * + * This function is called everytime the value is changed. + * + * @private + */ + _computeRowAggregates: function () { + if (!this.matrix_data.show_row_totals) { + return; + } + var fname = this.matrix_data.field_value, + field = this.state.fields[fname]; + if (!field) { + return; + } + var type = field.type; + if (!_.inArray(type, ['integer', 'float', 'monetary'])) { + return; + } + _.each(this.rows, function (row) { + row.aggregate = { + fname: fname, + ftype: type, + help: _t('Sum'), + value: 0, + }; + _.each(row.data, function (col) { + row.aggregate.value += col.data[fname]; + }); + }); + }, + + /** + * Takes the given Value, formats it and adds it to the given cell. + * + * @private + * + * @param {jQueryElement} $cell + * The Cell where the aggregate should be added. + * + * @param {Object} aggregate + * The object which contains the information about the aggregate value + */ + _apply_aggregate_value: function ($cell, aggregate) { + var field = this.state.fields[aggregate.fname], + formatter = field_utils.format[field.type]; + var formattedValue = formatter( + aggregate.value, field, {escape: true} + ); + $cell.addClass('total').attr('title', aggregate.help) + .html(formattedValue); + }, + + /** + * Check if the change was successful and then update the grid. + * This function is required on relational fields. + * + * @param {Object} state + * Contains the current state of the field & all the data + * + * @param {String} id + * the id of the updated object. + * + * @param {Array} fields + * The fields we have in the view. + * + * @param {Object} ev + * The event object. + * + * @returns {Deferred} + * The deferred object thats gonna be resolved when the change is made. + */ + confirmUpdate: function (state, id, fields, ev) { + var self = this; + this.state = state; + return this.confirmChange(state, id, fields, ev).then(function () { + self._refresh(id); + }); + }, + + /** + * Refresh our grid. + * + * @private + * @param {String} id Datapoint ID + */ + _refresh: function (id) { + this._updateRow(id); + this._refreshColTotals(); + this._refreshRowTotals(); + }, + + /** + *Update row data in our internal rows. + * + * @param {String} id: The id of the row that needs to be updated. + */ + _updateRow: function (id) { + var record = _.findWhere(this.state.data, {id: id}); + _.each(this.rows, function (row) { + _.each(row.data, function (col, i) { + if (col.id === id) { + row.data[i] = record; + } + }); + }); + }, + + /** + * Update the row total. + */ + _refreshColTotals: function () { + this._computeColumnAggregates(); + this.$('tfoot').replaceWith(this._renderFooter()); + }, + + /** + * Update the column total. + */ + _refreshRowTotals: function () { + var self = this; + this._computeRowAggregates(); + var $rows = self.$el.find('tr.o_data_row'); + _.each(self.rows, function (row, i) { + if (row.aggregate) { + $($rows[i]).find('.row-total') + .replaceWith(self._renderAggregateRowCell(row)); + } + }); + }, + + /** + * X2many fields expect this + * + * @returns {null} + */ + getEditableRecordID: function () { + return null; + }, + + }); + + return X2Many2dMatrixRenderer; }); diff --git a/web_widget_x2many_2d_matrix/static/src/js/widget_x2many_2d_matrix.js b/web_widget_x2many_2d_matrix/static/src/js/widget_x2many_2d_matrix.js index 56d2598f..420c624c 100644 --- a/web_widget_x2many_2d_matrix/static/src/js/widget_x2many_2d_matrix.js +++ b/web_widget_x2many_2d_matrix/static/src/js/widget_x2many_2d_matrix.js @@ -4,177 +4,192 @@ * License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). */ odoo.define('web_widget_x2many_2d_matrix.widget', function (require) { - "use strict"; + "use strict"; - var core = require('web.core'); - // var FieldManagerMixin = require('web.FieldManagerMixin'); - var field_registry = require('web.field_registry'); - var relational_fields = require('web.relational_fields'); - var weContext = require('web_editor.context'); - // var Helpers = require('web_widget_x2many_2d_matrix.helpers'); - var AbstractField = require('web.AbstractField'); - var X2Many2dMatrixRenderer = require('web_widget_x2many_2d_matrix.X2Many2dMatrixRenderer'); + var field_registry = require('web.field_registry'); + var relational_fields = require('web.relational_fields'); + var X2Many2dMatrixRenderer = require( + 'web_widget_x2many_2d_matrix.X2Many2dMatrixRenderer' + ); - var WidgetX2Many2dMatrix = relational_fields.FieldOne2Many.extend({ - widget_class: 'o_form_field_x2many_2d_matrix', - /** - * Initialize the widget & parameters. - * - * @param {Object} parent: contains the form view. - * @param {String} name: the name of the field. - * @param {Object} record: Contains the information about the database records. - * @param {Object} options: Contains the view options. - */ - init: function (parent, name, record, options) { - this._super(parent, name, record, options); - this.init_params(); - }, + var WidgetX2Many2dMatrix = relational_fields.FieldOne2Many.extend({ + widget_class: 'o_form_field_x2many_2d_matrix', - /** - * Initialize the widget specific parameters. - * Sets the axis and the values. - */ - init_params: function () { - var node = this.attrs; - this.by_x_axis = {}; - this.by_y_axis = {}; - this.field_x_axis = node.field_x_axis || this.field_x_axis; - this.field_y_axis = node.field_y_axis || this.field_y_axis; - this.field_label_x_axis = node.field_label_x_axis || this.field_x_axis; - this.field_label_y_axis = node.field_label_y_axis || this.field_y_axis; - this.x_axis_clickable = this.parse_boolean(node.x_axis_clickable || '1'); - this.y_axis_clickable = this.parse_boolean(node.y_axis_clickable || '1'); - this.field_value = node.field_value || this.field_value; - // TODO: is this really needed? Holger? - for (var property in node) { - if (property.startsWith("field_att_")) { - this.fields_att[property.substring(10)] = node[property]; - } - } - // and this? - this.field_editability = node.field_editability || this.field_editability; - this.show_row_totals = this.parse_boolean(node.show_row_totals || '1'); - this.show_column_totals = this.parse_boolean(node.show_column_totals || '1'); + /** + * Initialize the widget & parameters. + * + * @param {Object} parent contains the form view. + * @param {String} name the name of the field. + * @param {Object} record information about the database records. + * @param {Object} options view options. + */ + init: function (parent, name, record, options) { + this._super(parent, name, record, options); + this.init_params(); + }, - }, - /** - * Initializes the Value matrix. - * Puts the values in the grid. If we have related items we use the display name. - */ - init_matrix: function(){ - var self = this, - records = self.recordData[this.name].data; - // Wipe the content if something still exists - this.by_x_axis = {}; - this.by_y_axis = {}; - _.each(records, function(record) { - var x = record.data[self.field_x_axis], - y = record.data[self.field_y_axis]; - if (x.type == 'record') { - // we have a related record - x = x.data.display_name; - } - if (y.type == 'record') { - // we have a related record - y = y.data.display_name; - } - self.by_x_axis[x] = self.by_x_axis[x] || {}; - self.by_y_axis[y] = self.by_y_axis[y] || {}; - self.by_x_axis[x][y] = record; - self.by_y_axis[y][x] = record; - }); - // init columns - self.columns = []; - $.each(self.by_x_axis, function(x){ - self.columns.push(self._make_column(x)); - }); - self.rows = []; - $.each(self.by_y_axis, function(y){ - self.rows.push(self._make_row(y)); - }); - self.matrix_data = { - 'field_value': self.field_value, - 'field_x_axis': self.field_x_axis, - 'field_y_axis': self.field_y_axis, - 'columns': self.columns, - 'rows': self.rows, - 'show_row_totals': self.show_row_totals, - 'show_column_totals': self.show_column_totals - }; + /** + * Initialize the widget specific parameters. + * Sets the axis and the values. + */ + init_params: function () { + var node = this.attrs; + this.by_x_axis = {}; + this.by_y_axis = {}; + this.field_x_axis = node.field_x_axis || this.field_x_axis; + this.field_y_axis = node.field_y_axis || this.field_y_axis; + this.field_label_x_axis = + node.field_label_x_axis || this.field_x_axis; + this.field_label_y_axis = + node.field_label_y_axis || this.field_y_axis; + this.x_axis_clickable = this.parse_boolean( + node.x_axis_clickable || '1' + ); + this.y_axis_clickable = this.parse_boolean( + node.y_axis_clickable || '1' + ); + this.field_value = node.field_value || this.field_value; + // TODO: is this really needed? Holger? + for (var property in node) { + if (property.startsWith("field_att_")) { + this.fields_att[property.substring(10)] = + node[property]; + } + } + // and this? + this.field_editability = + node.field_editability || this.field_editability; + this.show_row_totals = + this.parse_boolean(node.show_row_totals || '1'); + this.show_column_totals = + this.parse_boolean(node.show_column_totals || '1'); + }, - }, - /** - * Create scaffold for a column. - * - * @params {String} x: The string used as a column title - */ - _make_column: function(x){ - return { - // simulate node parsed on xml arch - 'tag': 'field', - 'attrs': { - 'name': this.field_x_axis, - 'string': x - } - }; - }, - /** - * Create scaffold for a row. - * - * @params {String} x: The string used as a row title - */ - _make_row: function(y){ - var self = this; - // use object so that we can attach more data if needed - var row = {'data': []}; - $.each(self.by_x_axis, function(x) { - row.data.push(self.by_y_axis[y][x]); - }); - return row; - }, - /** - *Parse a String containing a Python bool or 1 and convert it to a proper bool. - * - * @params {String} val: the string to be parsed. - * @returns {Boolean} The parsed boolean. - */ - parse_boolean: function(val) { - if (val.toLowerCase() === 'true' || val === '1') { - return true; - } - return false; - }, - /** - *Create the matrix renderer and add its output to our element - * - * @returns {Deferred} A deferred object to be completed when it finished rendering. - */ - _render: function () { - if (!this.view) { - return this._super(); - } - // Ensure widget is re initiated when rendering - this.init_matrix(); - var arch = this.view.arch, - viewType = 'list'; - this.renderer = new X2Many2dMatrixRenderer(this, this.value, { - arch: arch, - editable: true, - viewType: viewType, - matrix_data: this.matrix_data - }); - this.$el.addClass('o_field_x2many o_field_x2many_2d_matrix'); - // Remove previous rendered and add the newly created one - this.$el.find('div:not(.o_x2m_control_panel)').remove(); - return this.renderer.appendTo(this.$el); + /** + * Initializes the Value matrix. + * + * Puts the values in the grid. + * If we have related items we use the display name. + */ + init_matrix: function () { + var records = this.recordData[this.name].data; + // Wipe the content if something still exists + this.by_x_axis = {}; + this.by_y_axis = {}; + _.each(records, function (record) { + var x = record.data[this.field_x_axis], + y = record.data[this.field_y_axis]; + if (x.type === 'record') { + // we have a related record + x = x.data.display_name; + } + if (y.type === 'record') { + // we have a related record + y = y.data.display_name; + } + this.by_x_axis[x] = this.by_x_axis[x] || {}; + this.by_y_axis[y] = this.by_y_axis[y] || {}; + this.by_x_axis[x][y] = record; + this.by_y_axis[y][x] = record; + }.bind(this)); + // init columns + this.columns = []; + $.each(this.by_x_axis, function (x) { + this.columns.push(this._make_column(x)); + }.bind(this)); + this.rows = []; + $.each(this.by_y_axis, function (y) { + this.rows.push(this._make_row(y)); + }.bind(this)); + this.matrix_data = { + 'field_value': this.field_value, + 'field_x_axis': this.field_x_axis, + 'field_y_axis': this.field_y_axis, + 'columns': this.columns, + 'rows': this.rows, + 'show_row_totals': this.show_row_totals, + 'show_column_totals': this.show_column_totals, + }; + }, - } + /** + * Create scaffold for a column. + * + * @param {String} x The string used as a column title + * @returns {Object} + */ + _make_column: function (x) { + return { + // simulate node parsed on xml arch + 'tag': 'field', + 'attrs': { + 'name': this.field_x_axis, + 'string': x, + }, + }; + }, - }); + /** + * Create scaffold for a row. + * + * @param {String} y The string used as a row title + * @returns {Object} + */ + _make_row: function (y) { + var self = this; + // use object so that we can attach more data if needed + var row = {'data': []}; + $.each(self.by_x_axis, function (x) { + row.data.push(self.by_y_axis[y][x]); + }); + return row; + }, - field_registry.add('x2many_2d_matrix', WidgetX2Many2dMatrix); + /** + * Parse a String containing a bool and convert it to a JS bool. + * + * @param {String} val: the string to be parsed. + * @returns {Boolean} The parsed boolean. + */ + parse_boolean: function (val) { + if (val.toLowerCase() === 'true' || val === '1') { + return true; + } + return false; + }, - return { - WidgetX2Many2dMatrix: WidgetX2Many2dMatrix - }; + /** + * Create the matrix renderer and add its output to our element + * + * @returns {Deferred} + * A deferred object to be completed when it finished rendering. + */ + _render: function () { + if (!this.view) { + return this._super(); + } + // Ensure widget is re initiated when rendering + this.init_matrix(); + var arch = this.view.arch, + viewType = 'list'; + this.renderer = new X2Many2dMatrixRenderer(this, this.value, { + arch: arch, + editable: true, + viewType: viewType, + matrix_data: this.matrix_data, + }); + this.$el.addClass('o_field_x2many o_field_x2many_2d_matrix'); + // Remove previous rendered and add the newly created one + this.$el.find('div:not(.o_x2m_control_panel)').remove(); + return this.renderer.appendTo(this.$el); + + }, + + }); + + field_registry.add('x2many_2d_matrix', WidgetX2Many2dMatrix); + + return { + WidgetX2Many2dMatrix: WidgetX2Many2dMatrix, + }; }); From 78beda054659fb4c3554ac64112e61fbca0944e2 Mon Sep 17 00:00:00 2001 From: Jairo Llopis Date: Tue, 21 Aug 2018 12:08:28 +0100 Subject: [PATCH 2/3] [FIX] web_widget_x2many_2d_matrix: Enable keyboard navigation --- web_widget_x2many_2d_matrix/README.rst | 115 ++++++++++-------- .../static/src/js/2d_matrix_renderer.js | 72 +++++++++-- .../static/src/js/widget_x2many_2d_matrix.js | 54 +++++--- 3 files changed, 161 insertions(+), 80 deletions(-) diff --git a/web_widget_x2many_2d_matrix/README.rst b/web_widget_x2many_2d_matrix/README.rst index aba2f90a..44f5920c 100644 --- a/web_widget_x2many_2d_matrix/README.rst +++ b/web_widget_x2many_2d_matrix/README.rst @@ -39,16 +39,18 @@ Use this widget by saying:: This assumes that my_field refers to a model with the fields `x`, `y` and `value`. If your fields are named differently, pass the correct names as -attributes:: +attributes: - - - - - - - - +.. code-block:: xml + + + + + + + + + You can pass the following parameters: @@ -80,49 +82,53 @@ You need a data structure already filled with values. Let's assume we want to use this widget in a wizard that lets the user fill in planned hours for one task per project per user. In this case, we can use ``project.task`` as our data model and point to it from our wizard. The crucial part is that we fill -the field in the default function:: - - from odoo import fields, models - - class MyWizard(models.TransientModel): - _name = 'my.wizard' - - def _default_task_ids(self): - # your list of project should come from the context, some selection - # in a previous wizard or wherever else - projects = self.env['project.project'].browse([1, 2, 3]) - # same with users - users = self.env['res.users'].browse([1, 2, 3]) - return [ - (0, 0, { - 'name': 'Sample task name', - 'project_id': p.id, - 'user_id': u.id, - 'planned_hours': 0, - 'message_needaction': False, - 'date_deadline': fields.Date.today(), - }) - # if the project doesn't have a task for the user, create a new one - if not p.task_ids.filtered(lambda x: x.user_id == u) else - # otherwise, return the task - (4, p.task_ids.filtered(lambda x: x.user_id == u)[0].id) - for p in projects - for u in users - ] - - task_ids = fields.Many2many('project.task', default=_default_task_ids) - -Now in our wizard, we can use:: - - - - - - - - - - +the field in the default function: + +.. code-block:: python + + from odoo import fields, models + + class MyWizard(models.TransientModel): + _name = 'my.wizard' + + def _default_task_ids(self): + # your list of project should come from the context, some selection + # in a previous wizard or wherever else + projects = self.env['project.project'].browse([1, 2, 3]) + # same with users + users = self.env['res.users'].browse([1, 2, 3]) + return [ + (0, 0, { + 'name': 'Sample task name', + 'project_id': p.id, + 'user_id': u.id, + 'planned_hours': 0, + 'message_needaction': False, + 'date_deadline': fields.Date.today(), + }) + # if the project doesn't have a task for the user, + # create a new one + if not p.task_ids.filtered(lambda x: x.user_id == u) else + # otherwise, return the task + (4, p.task_ids.filtered(lambda x: x.user_id == u)[0].id) + for p in projects + for u in users + ] + + task_ids = fields.Many2many('project.task', default=_default_task_ids) + +Now in our wizard, we can use: + +.. code-block:: xml + + + + + + + + + Known issues / Roadmap ====================== @@ -134,6 +140,11 @@ Known issues / Roadmap * Support limit total records in the matrix. Ref: https://github.com/OCA/web/issues/901 +* Support cell traversal through keyboard arrows. + +* Entering the widget from behind by pressing ``Shift+TAB`` in your keyboard + will enter into the 1st cell until https://github.com/odoo/odoo/pull/26490 + is merged. Bug Tracker =========== diff --git a/web_widget_x2many_2d_matrix/static/src/js/2d_matrix_renderer.js b/web_widget_x2many_2d_matrix/static/src/js/2d_matrix_renderer.js index 6cb66ac2..dcbc44ad 100644 --- a/web_widget_x2many_2d_matrix/static/src/js/2d_matrix_renderer.js +++ b/web_widget_x2many_2d_matrix/static/src/js/2d_matrix_renderer.js @@ -4,14 +4,14 @@ odoo.define('web_widget_x2many_2d_matrix.X2Many2dMatrixRenderer', function (require) { "use strict"; - // heavily inspired by Odoo's `ListRenderer` + // Heavily inspired by Odoo's `ListRenderer` var BasicRenderer = require('web.BasicRenderer'); var config = require('web.config'); var core = require('web.core'); var field_utils = require('web.field_utils'); var _t = core._t; var FIELD_CLASSES = { - // copied from ListRenderer + // Copied from ListRenderer float: 'o_list_number', integer: 'o_list_number', monetary: 'o_list_number', @@ -26,9 +26,18 @@ odoo.define('web_widget_x2many_2d_matrix.X2Many2dMatrixRenderer', function (requ init: function (parent, state, params) { this._super.apply(this, arguments); this.editable = params.editable; - this.columns = params.matrix_data.columns; - this.rows = params.matrix_data.rows; - this.matrix_data = params.matrix_data; + this._saveMatrixData(params.matrix_data); + }, + + /** + * Update matrix data in current renderer instance. + * + * @param {Object} matrixData Contains the matrix data + */ + _saveMatrixData: function (matrixData) { + this.columns = matrixData.columns; + this.rows = matrixData.rows; + this.matrix_data = matrixData; }, /** @@ -159,7 +168,6 @@ odoo.define('web_widget_x2many_2d_matrix.X2Many2dMatrixRenderer', function (requ * * @private * @returns {String} a string with the generated html. - * */ _renderRows: function () { return _.map(this.rows, this._renderRow.bind(this)); @@ -181,7 +189,7 @@ odoo.define('web_widget_x2many_2d_matrix.X2Many2dMatrixRenderer', function (requ $tr = $tr.append(self._renderLabelCell(row.data[0])); var $cells = _.map(this.columns, function (node, index) { var record = row.data[index]; - // make the widget use our field value for each cell + // Make the widget use our field value for each cell node.attrs.name = self.matrix_data.field_value; return self._renderBodyCell(record, node, index, {mode:''}); }); @@ -203,10 +211,10 @@ odoo.define('web_widget_x2many_2d_matrix.X2Many2dMatrixRenderer', function (requ var $td = $(''); var value = record.data[this.matrix_data.field_y_axis]; if (value.type === 'record') { - // we have a related record + // We have a related record value = value.data.display_name; } - // get 1st column filled w/ Y label + // Get 1st column filled w/ Y label $td.text(value); return $td; }, @@ -272,7 +280,7 @@ odoo.define('web_widget_x2many_2d_matrix.X2Many2dMatrixRenderer', function (requ if (modifiers.invisible && !(options && options.renderInvisible)) { return $td; } - // enforce mode of the parent + // Enforce mode of the parent options.mode = this.getParent().mode; var widget = this._renderFieldWidget( node, record, _.pick(options, 'mode') @@ -329,7 +337,7 @@ odoo.define('web_widget_x2many_2d_matrix.X2Many2dMatrixRenderer', function (requ return; } var type = field.type; - if (!_.inArray(type, ['integer', 'float', 'monetary'])) { + if (!~['integer', 'float', 'monetary'].indexOf(type)) { return; } _.each(this.columns, function (column, index) { @@ -342,7 +350,45 @@ odoo.define('web_widget_x2many_2d_matrix.X2Many2dMatrixRenderer', function (requ _.each(this.rows, function (row) { column.aggregate.value += row.data[index].data[fname]; }); - }); + }.bind(this)); + }, + + /** + * @override + */ + updateState: function (state, params) { + if (params.matrix_data) { + this._saveMatrixData(params.matrix_data); + } + return this._super.apply(this, arguments); + }, + + /** + * Traverse the fields matrix with the keyboard + * + * @override + * @private + * @param {OdooEvent} event "navigation_move" event + */ + _onNavigationMove: function (event) { + var widgets = this.__parentedChildren, + index = widgets.indexOf(event.target), + first = index === 0, + last = index === widgets.length - 1, + move = 0; + // Guess if we have to move the focus + if (event.data.direction === "next" && !last) { + move = 1; + } else if (event.data.direction === "previous" && !first) { + move = -1; + } + // Move focus + if (move) { + var target = widgets[index + move]; + index = this.allFieldWidgets[target.record.id].indexOf(target); + this._activateFieldWidget(target.record, index, {inc: 0}); + event.stopPropagation(); + } }, /** @@ -362,7 +408,7 @@ odoo.define('web_widget_x2many_2d_matrix.X2Many2dMatrixRenderer', function (requ return; } var type = field.type; - if (!_.inArray(type, ['integer', 'float', 'monetary'])) { + if (!~['integer', 'float', 'monetary'].indexOf(type)) { return; } _.each(this.rows, function (row) { diff --git a/web_widget_x2many_2d_matrix/static/src/js/widget_x2many_2d_matrix.js b/web_widget_x2many_2d_matrix/static/src/js/widget_x2many_2d_matrix.js index 420c624c..638c080a 100644 --- a/web_widget_x2many_2d_matrix/static/src/js/widget_x2many_2d_matrix.js +++ b/web_widget_x2many_2d_matrix/static/src/js/widget_x2many_2d_matrix.js @@ -16,9 +16,9 @@ odoo.define('web_widget_x2many_2d_matrix.widget', function (require) { widget_class: 'o_form_field_x2many_2d_matrix', /** - * Initialize the widget & parameters. + *Initialize the widget & parameters. * - * @param {Object} parent contains the form view. + *@param {Object} parent contains the form view. * @param {String} name the name of the field. * @param {Object} record information about the database records. * @param {Object} options view options. @@ -29,7 +29,7 @@ odoo.define('web_widget_x2many_2d_matrix.widget', function (require) { }, /** - * Initialize the widget specific parameters. + *Initialize the widget specific parameters. * Sets the axis and the values. */ init_params: function () { @@ -56,7 +56,7 @@ odoo.define('web_widget_x2many_2d_matrix.widget', function (require) { node[property]; } } - // and this? + // And this? this.field_editability = node.field_editability || this.field_editability; this.show_row_totals = @@ -80,11 +80,11 @@ odoo.define('web_widget_x2many_2d_matrix.widget', function (require) { var x = record.data[this.field_x_axis], y = record.data[this.field_y_axis]; if (x.type === 'record') { - // we have a related record + // We have a related record x = x.data.display_name; } if (y.type === 'record') { - // we have a related record + // We have a related record y = y.data.display_name; } this.by_x_axis[x] = this.by_x_axis[x] || {}; @@ -92,7 +92,7 @@ odoo.define('web_widget_x2many_2d_matrix.widget', function (require) { this.by_x_axis[x][y] = record; this.by_y_axis[y][x] = record; }.bind(this)); - // init columns + // Init columns this.columns = []; $.each(this.by_x_axis, function (x) { this.columns.push(this._make_column(x)); @@ -120,7 +120,7 @@ odoo.define('web_widget_x2many_2d_matrix.widget', function (require) { */ _make_column: function (x) { return { - // simulate node parsed on xml arch + // Simulate node parsed on xml arch 'tag': 'field', 'attrs': { 'name': this.field_x_axis, @@ -137,7 +137,7 @@ odoo.define('web_widget_x2many_2d_matrix.widget', function (require) { */ _make_row: function (y) { var self = this; - // use object so that we can attach more data if needed + // Use object so that we can attach more data if needed var row = {'data': []}; $.each(self.by_x_axis, function (x) { row.data.push(self.by_y_axis[y][x]); @@ -170,21 +170,45 @@ odoo.define('web_widget_x2many_2d_matrix.widget', function (require) { } // Ensure widget is re initiated when rendering this.init_matrix(); - var arch = this.view.arch, - viewType = 'list'; + var arch = this.view.arch; + // Update existing renderer + if (!_.isUndefined(this.renderer)) { + return this.renderer.updateState(this.value, { + matrix_data: this.matrix_data, + }); + } + // Create a new matrix renderer this.renderer = new X2Many2dMatrixRenderer(this, this.value, { arch: arch, - editable: true, - viewType: viewType, + editable: this.mode === 'edit' && arch.attrs.editable, + viewType: "list", matrix_data: this.matrix_data, }); this.$el.addClass('o_field_x2many o_field_x2many_2d_matrix'); - // Remove previous rendered and add the newly created one - this.$el.find('div:not(.o_x2m_control_panel)').remove(); return this.renderer.appendTo(this.$el); + }, + /** + * Activate the widget. + * + * @override + */ + activate: function (options) { + // Won't work fine without https://github.com/odoo/odoo/pull/26490 + this._backwards = options.event.data.direction === "previous"; + var result = this._super.apply(this, arguments); + delete this._backwards; + return result; }, + /** + * Get first element to focus. + * + * @override + */ + getFocusableElement: function () { + return this.$(".o_input:" + (this._backwards ? "last" : "first")); + }, }); field_registry.add('x2many_2d_matrix', WidgetX2Many2dMatrix); From 0efe75de7c79ef1c58336cf2e14f8f1ef0f9faa5 Mon Sep 17 00:00:00 2001 From: Jairo Llopis Date: Fri, 24 Aug 2018 11:12:46 +0100 Subject: [PATCH 3/3] [FIX] web_widget_x2many_2d_matrix: Allow empty cells Before this commit, if a matrix was lacking one element, some ugly errors appeared to the user. Now, it just displays the empty cell and lets the user go on. Also, acknowledge another limitation of the widget and add it to roadmap. --- web_widget_x2many_2d_matrix/README.rst | 2 + .../static/src/js/2d_matrix_renderer.js | 51 ++++++++++++------- .../static/src/js/widget_x2many_2d_matrix.js | 13 +++-- 3 files changed, 45 insertions(+), 21 deletions(-) diff --git a/web_widget_x2many_2d_matrix/README.rst b/web_widget_x2many_2d_matrix/README.rst index 44f5920c..52b2f3cf 100644 --- a/web_widget_x2many_2d_matrix/README.rst +++ b/web_widget_x2many_2d_matrix/README.rst @@ -146,6 +146,8 @@ Known issues / Roadmap will enter into the 1st cell until https://github.com/odoo/odoo/pull/26490 is merged. +* Support extra invisible fields inside each cell. + Bug Tracker =========== diff --git a/web_widget_x2many_2d_matrix/static/src/js/2d_matrix_renderer.js b/web_widget_x2many_2d_matrix/static/src/js/2d_matrix_renderer.js index dcbc44ad..d3c98afa 100644 --- a/web_widget_x2many_2d_matrix/static/src/js/2d_matrix_renderer.js +++ b/web_widget_x2many_2d_matrix/static/src/js/2d_matrix_renderer.js @@ -180,22 +180,22 @@ odoo.define('web_widget_x2many_2d_matrix.X2Many2dMatrixRenderer', function (requ * the aggregate cell. * * @private - * @param {Object} row: The row that will be rendered. + * @param {Object} row The row that will be rendered. * @returns {jQueryElement} the
', { 'class': tdClassName, + }); + if (_.isUndefined(record)) { + // Without record, nothing elese to do + return $td; + } + $td.attr({ 'data-form-id': record.id, 'data-id': record.data.id, }); @@ -348,7 +354,12 @@ odoo.define('web_widget_x2many_2d_matrix.X2Many2dMatrixRenderer', function (requ value: 0, }; _.each(this.rows, function (row) { - column.aggregate.value += row.data[index].data[fname]; + // TODO Use only one _.propertyOf in underscore 1.9.0+ + try { + column.aggregate.value += row.data[index].data[fname]; + } catch (error) { + // Nothing to do + } }); }.bind(this)); }, @@ -419,7 +430,12 @@ odoo.define('web_widget_x2many_2d_matrix.X2Many2dMatrixRenderer', function (requ value: 0, }; _.each(row.data, function (col) { - row.aggregate.value += col.data[fname]; + // TODO Use _.property in underscore 1.9+ + try { + row.aggregate.value += col.data[fname]; + } catch (error) { + // Nothing to do + } }); }); }, @@ -490,10 +506,11 @@ odoo.define('web_widget_x2many_2d_matrix.X2Many2dMatrixRenderer', function (requ * @param {String} id: The id of the row that needs to be updated. */ _updateRow: function (id) { - var record = _.findWhere(this.state.data, {id: id}); + var record = _.findWhere(this.state.data, {id: id}), + _id = _.property("id"); _.each(this.rows, function (row) { _.each(row.data, function (col, i) { - if (col.id === id) { + if (_id(col) === id) { row.data[i] = record; } }); diff --git a/web_widget_x2many_2d_matrix/static/src/js/widget_x2many_2d_matrix.js b/web_widget_x2many_2d_matrix/static/src/js/widget_x2many_2d_matrix.js index 638c080a..addf7cd4 100644 --- a/web_widget_x2many_2d_matrix/static/src/js/widget_x2many_2d_matrix.js +++ b/web_widget_x2many_2d_matrix/static/src/js/widget_x2many_2d_matrix.js @@ -16,9 +16,9 @@ odoo.define('web_widget_x2many_2d_matrix.widget', function (require) { widget_class: 'o_form_field_x2many_2d_matrix', /** - *Initialize the widget & parameters. + * Initialize the widget & parameters. * - *@param {Object} parent contains the form view. + * @param {Object} parent contains the form view. * @param {String} name the name of the field. * @param {Object} record information about the database records. * @param {Object} options view options. @@ -29,7 +29,7 @@ odoo.define('web_widget_x2many_2d_matrix.widget', function (require) { }, /** - *Initialize the widget specific parameters. + * Initialize the widget specific parameters. * Sets the axis and the values. */ init_params: function () { @@ -195,7 +195,12 @@ odoo.define('web_widget_x2many_2d_matrix.widget', function (require) { */ activate: function (options) { // Won't work fine without https://github.com/odoo/odoo/pull/26490 - this._backwards = options.event.data.direction === "previous"; + // TODO Use _.propertyOf in underscore 1.9+ + try { + this._backwards = options.event.data.direction === "previous"; + } catch (error) { + this._backwards = false; + } var result = this._super.apply(this, arguments); delete this._backwards; return result;