[FIX] web: InputField: don't re-render if dirty

Let's assume that we have a form view with a one2many field
displayed as a list containing two fields A and B (an input field,
e.g. a char field). An onchange on the one2many is triggered as
soon as A changes. This triggers a reset of the one2many which
thus redraws the corresponding line by resetting its widgets.

A problem occured when the user updated A, and directly (before
the onchange returned) updated the input field B, as in this case
the new value in B was erased when the onchanged returned, as the
widget was reset with its former value (or with the value returned
by the onchange, if any). Note that the model was aware of the
change on B, so the model and the UI were actually desynchronized.

This was quite hard to reproduce in practice, by hand, but it
occured in the tour testing 'the flow', in the form view of
account.bank_statement, with fields partner_id and amount of the
line_ids one2many.

With this rev., we don't re-render the input fields if there are
pending changes that haven't been acknowledged by the model.
This commit is contained in:
Adrien Dieudonne
2017-07-27 13:58:43 +02:00
parent ecdcc65a90
commit a71f223702
2 changed files with 146 additions and 3 deletions
@@ -831,6 +831,122 @@ QUnit.module('basic_fields', {
form.destroy();
});
QUnit.test('input field: change value before pending onchange returns', function (assert) {
assert.expect(3);
this.data.partner.onchanges = {
product_id: function () {},
};
var def;
var form = createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form>' +
'<sheet>' +
'<field name="p">' +
'<tree editable="bottom">' +
'<field name="product_id"/>' +
'<field name="foo"/>' +
'</tree>' +
'</field>' +
'</sheet>' +
'</form>',
res_id: 1,
mockRPC: function (route, args) {
var result = this._super.apply(this, arguments);
if (args.method === "onchange") {
return $.when(def).then(function () {
return result;
});
} else {
return result;
}
},
viewOptions: {
mode: 'edit',
},
});
form.$('.o_field_x2many_list_row_add a').click();
assert.strictEqual(form.$('input[name="foo"]').val(), 'My little Foo Value',
'should contain the default value');
def = $.Deferred();
form.$('.o_field_many2one input').click();
var $dropdown = form.$('.o_field_many2one input').autocomplete('widget');
$dropdown.find('li:first()').click();
// set foo before onchange
form.$('input[name="foo"]').val("tralala").trigger('input');
assert.strictEqual(form.$('input[name="foo"]').val(), 'tralala',
'input should contain tralala');
// complete the onchange
def.resolve();
assert.strictEqual(form.$('input[name="foo"]').val(), 'tralala',
'input should contain the same value as before onchange');
form.destroy();
});
QUnit.test('input field: change value before pending onchange renaming', function (assert) {
assert.expect(3);
this.data.partner.onchanges = {
product_id: function (obj) {
obj.foo = 'on change value';
},
};
var def = $.Deferred();
var form = createView({
View: FormView,
model: 'partner',
data: this.data,
arch: '<form>' +
'<sheet>' +
'<field name="product_id"/>' +
'<field name="foo"/>' +
'</sheet>' +
'</form>',
res_id: 1,
mockRPC: function (route, args) {
var result = this._super.apply(this, arguments);
if (args.method === "onchange") {
return $.when(def).then(function () {
return result;
});
} else {
return result;
}
},
viewOptions: {
mode: 'edit',
},
});
assert.strictEqual(form.$('input[name="foo"]').val(), 'yop',
'should contain the correct value');
form.$('.o_field_many2one input').click();
var $dropdown = form.$('.o_field_many2one input').autocomplete('widget');
$dropdown.find('li:first()').click();
// set foo before onchange
form.$('input[name="foo"]').val("tralala").trigger('input');
assert.strictEqual(form.$('input[name="foo"]').val(), 'tralala',
'input should contain tralala');
// complete the onchange
def.resolve();
assert.strictEqual(form.$('input[name="foo"]').val(), 'tralala',
'input should contain the same value as before onchange');
form.destroy();
});
QUnit.module('UrlWidget');
QUnit.test('url widget in form view', function (assert) {