From 48b8be29fa1e8c10f2ce652a768d3dcb581a53a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9ry=20Debongnie?= Date: Tue, 19 Sep 2017 15:53:24 +0200 Subject: [PATCH] [FIX] web: update x2manys when onchange add new line In this commit, we make sure that if an onchange in a x2many modifies another line, then the new data is properly shown. This is quite tricky, because the field x2many ignores the full extent of the changes. Also, we obviously want to preserve the editing state (focused field, ...) So, what we actually do is to rerender the full x2many (minus the new line) to make sure that we preserve the correct row order. Note: thanks to MGE for the test and some preliminary work. --- .../static/src/js/fields/relational_fields.js | 13 ++- .../js/views/list/list_editable_renderer.js | 53 +++++++++ .../tests/fields/relational_fields_tests.js | 102 +++++++++++++++++- 3 files changed, 158 insertions(+), 10 deletions(-) diff --git a/addons/web/static/src/js/fields/relational_fields.js b/addons/web/static/src/js/fields/relational_fields.js index 7df5b390a2c..90c2b08e1e5 100644 --- a/addons/web/static/src/js/fields/relational_fields.js +++ b/addons/web/static/src/js/fields/relational_fields.js @@ -681,17 +681,16 @@ var FieldX2Many = AbstractField.extend({ var command = ev.data.changes[this.name]; // Here, we only consider 'UPDATE' commands with data, which occur // with editable list view. In order to keep the current line in - // edition, we call confirmChange which will reset the widgets - // instead of re-rendering the list. 'UPDATE' commands with no data - // can be ignored: they occur in one2manys when the record is - // updated from a dialog and in this case, we can re-render the - // whole subview. + // edition, we call confirmUpdate which will try to reset the widgets + // of the line being edited, and rerender the rest of the list. + // 'UPDATE' commands with no data can be ignored: they occur in + // one2manys when the record is updated from a dialog and in this + // case, we can re-render the whole subview. if (command.operation === 'UPDATE' && command.data) { var state = record.data[this.name]; var fieldNames = state.getFieldNames(); this._reset(record, ev); - this.renderer.confirmChange(state, command.id, fieldNames, ev.initialEvent); - return $.when(); + return this.renderer.confirmUpdate(state, command.id, fieldNames, ev.initialEvent); } } return this._super.apply(this, arguments); diff --git a/addons/web/static/src/js/views/list/list_editable_renderer.js b/addons/web/static/src/js/views/list/list_editable_renderer.js index 7984ebf21e1..8379ab3a891 100644 --- a/addons/web/static/src/js/views/list/list_editable_renderer.js +++ b/addons/web/static/src/js/views/list/list_editable_renderer.js @@ -104,6 +104,59 @@ ListRenderer.include({ return widgets; }); }, + /** + * This is a specialized version of confirmChange, meant to be called when + * the change may have affected more than one line (so, for example, an + * onchange which add/remove a few lines in a x2many. This does not occur + * in a normal list view) + * + * The update is more difficult when other rows could have been changed. We + * need to potentially remove some lines, add some other lines, update some + * other lines and maybe reorder a few of them. This problem would neatly + * be solved by using a virtual dom, but we do not have this luxury yet. + * So, in the meantime, what we do is basically remove every current row + * except the 'main' one (the row which caused the update), then rerender + * every new row and add them before/after the main one. + * + * @param {Object} state + * @param {string} id + * @param {string[]} fields + * @param {OdooEvent} ev + * @returns {Deferred} resolved with the list of widgets + * that have been reset + */ + confirmUpdate: function (state, id, fields, ev) { + var self = this; + var oldRowIndex = _.findIndex(this.state.data, {id: id}); + this.state = state; + return this.confirmChange(state, id, fields, ev).then(function () { + // If no record with 'id' can be found in the state, the + // confirmChange method will have rerendered the whole view already, + // so no further work is necessary. + var record = _.findWhere(state.data, {id: id}); + if (!record) { + return; + } + var $row = self.$('.o_data_row:nth(' + oldRowIndex + ')'); + $row.nextAll('.o_data_row').remove(); + $row.prevAll().remove(); + var newRowIndex = _.findIndex(state.data, {id: id}); + _.each(state.data, function (record, index) { + if (index === newRowIndex) { + return; + } + var $newRow = self._renderRow(record); + if (index < newRowIndex) { + $newRow.insertBefore($row); + } else { + $newRow.insertAfter($row); + } + }); + if (self.currentRow !== null) { + self.currentRow = newRowIndex; + } + }); + }, /** * Edit a given record in the list * diff --git a/addons/web/static/tests/fields/relational_fields_tests.js b/addons/web/static/tests/fields/relational_fields_tests.js index b59df79f0f0..31b42d7d4f1 100644 --- a/addons/web/static/tests/fields/relational_fields_tests.js +++ b/addons/web/static/tests/fields/relational_fields_tests.js @@ -3536,6 +3536,93 @@ QUnit.module('relational_fields', { form.destroy(); }); + QUnit.test('one2many with CREATE onchanges correctly refreshed', function (assert) { + assert.expect(4); + + var deactiveOnchange = true; + + this.data.partner.records[0].turtles = []; + this.data.partner.onchanges = { + turtles: function (obj) { + if (deactiveOnchange) { return; } + // the onchange will either: + // - create a second line if there is only one line + // - edit the second line if there are two lines + if (obj.turtles.length === 1) { + obj.turtles = [ + [5], // delete all + [0, obj.turtles[0][1], { + display_name: "first", + turtle_int: obj.turtles[0][2].turtle_int, + }], + [0, 0, { + display_name: "second", + turtle_int: -obj.turtles[0][2].turtle_int, + }], + ]; + } else if (obj.turtles.length === 2) { + obj.turtles = [ + [5], // delete all + [0, obj.turtles[0][1], { + display_name: "first", + turtle_int: obj.turtles[0][2].turtle_int, + }], + [0, obj.turtles[1][1], { + display_name: "second", + turtle_int: -obj.turtles[0][2].turtle_int, + }], + ]; + } + }, + }; + + var form = createView({ + View: FormView, + model: 'partner', + data: this.data, + arch:'
' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '', + res_id: 1, + viewOptions: { + mode: 'edit', + }, + }); + + assert.strictEqual(form.$('.o_data_row').length, 0, + "o2m shouldnn't contain any row"); + + form.$('.o_field_x2many_list_row_add a').click(); + // trigger the first onchange + deactiveOnchange = false; + form.$('input[name="turtle_int"]').val('10').trigger('input'); + // put the list back in non edit mode + form.$('input[name="foo"]').click(); + assert.strictEqual(form.$('.o_data_row').text(), "first10second-10", + "should correctly refresh the records"); + + // trigger the second onchange + form.$('.o_field_x2many_list tbody tr:first td:first').click(); + form.$('input[name="turtle_int"]').val('20').trigger('input'); + + form.$('input[name="foo"]').click(); + assert.strictEqual(form.$('.o_data_row').text(), "first20second-20", + "should correctly refresh the records"); + + form.$buttons.find('.o_form_button_save').click(); + + assert.strictEqual(form.$('.o_data_row').text(), "first20second-20", + "should correctly refresh the records after save"); + + form.destroy(); + }); + QUnit.test('one2many list (editable): readonly domain is evaluated', function (assert) { assert.expect(2); @@ -4731,15 +4818,17 @@ QUnit.module('relational_fields', { '', }); + // add new line (first, xpad) form.$('.o_field_x2many_list_row_add a').click(); form.$('input[name="display_name"]').val('first').trigger('input'); form.$('div[name="product_id"] input').click(); - $('li.ui-menu-item a').click(); + // the onchange won't be generated + $('li.ui-menu-item a:contains(xpad)').trigger('mouseenter').click(); assert.strictEqual(form.$('.o_field_many2manytags.o_input').length, 1, 'should display the line in editable mode'); assert.strictEqual(form.$('.o_field_many2one input').val(), "xpad", - 'should display the product'); + 'should display the product xpad'); assert.strictEqual(form.$('.o_field_many2manytags.o_input .o_badge_text').text(), "first record", 'should display the tag from the onchange'); @@ -4750,6 +4839,7 @@ QUnit.module('relational_fields', { assert.strictEqual(form.$('.o_field_many2manytags:not(.o_input) .o_badge_text').text(), "first record", 'should display the tag in readonly'); + // enable the many2many onchange and generate it form.$('input.o_field_integer[name="int_field"]').val('10').trigger('input'); assert.strictEqual(form.$('.o_data_cell.o_required_modifier').text(), "xphonexenomorphe", @@ -4757,17 +4847,22 @@ QUnit.module('relational_fields', { assert.strictEqual(form.$('.o_data_row').text().replace(/\s+/g, ' '), "new linexphone first record firstxenomorphe second record ", 'should display the name, one2many and many2many value'); + // disable the many2many onchange form.$('input.o_field_integer[name="int_field"]').val('0').trigger('input'); + // delete and start over form.$('.o_list_record_delete:first span').click(); form.$('.o_list_record_delete:first span').click(); + // enable the many2many onchange form.$('input.o_field_integer[name="int_field"]').val('10').trigger('input'); + // add new line (first, xenomorphe) form.$('.o_field_x2many_list_row_add a').click(); form.$('input[name="display_name"]').val('first').trigger('input'); form.$('div[name="product_id"] input').click(); - $('li.ui-menu-item a').click(); + // generate the onchange + $('li.ui-menu-item a:contains(xenomorphe)').trigger('mouseenter').click(); assert.strictEqual(form.$('.o_field_many2manytags.o_input').length, 1, 'should display the line in editable mode'); @@ -4776,6 +4871,7 @@ QUnit.module('relational_fields', { assert.strictEqual(form.$('.o_field_many2manytags.o_input .o_badge_text').text(), "second record", 'should display the tag from the onchange'); + // put list in readonly mode form.$('input.o_field_integer[name="int_field"]').click(); assert.strictEqual(form.$('.o_data_cell.o_required_modifier').text(), "xphonexenomorphe",