summaryrefslogtreecommitdiff
path: root/addons/web/static
diff options
context:
space:
mode:
Diffstat (limited to 'addons/web/static')
-rw-r--r--addons/web/static/src/js/fields/relational_fields.js13
-rw-r--r--addons/web/static/src/js/views/list/list_editable_renderer.js53
-rw-r--r--addons/web/static/tests/fields/relational_fields_tests.js102
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
@@ -105,6 +105,59 @@ ListRenderer.include({
});
},
/**
+ * 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<AbstractField[]>} 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
*
* @param {string} recordID
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:'<form string="Partners">' +
+ '<field name="foo"/>' +
+ '<field name="turtles">' +
+ '<tree editable="bottom">' +
+ '<field name="display_name"/>' +
+ '<field name="turtle_int"/>' +
+ '</tree>' +
+ '</field>' +
+ '</form>',
+ 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', {
'</form>',
});
+ // 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",