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",