[FIX] web: list: m2o doesn't trigger fieldChange when empty

Gives time to the user to enter an actual value in m2o in list views.

Before this commit, emptying a many2one field (selecting all > BACKSPACE or DELETE)
triggered instantly a field_change. This was an issue in multi edition as a field
changed triggered instantly a save on selected records without giving time to the
user to enter a new value.

Now, many2ones only trigger a fieldChange when selecting a new valid value.
This commit is contained in:
Julien Mougenot
2019-09-18 08:24:28 +00:00
parent bb85f963b5
commit d261d42d9b
3 changed files with 152 additions and 1 deletions
@@ -600,8 +600,10 @@ var FieldMany2One = AbstractField.extend({
no_create: !self.can_create,
on_selected: function (records) {
self.reinitialize(records[0]);
},
on_closed: function () {
self.activate();
}
},
})).open();
},
/**
@@ -770,6 +772,22 @@ var ListFieldMany2One = FieldMany2One.extend({
'focusin input': '_onInputFocusin',
}),
/**
* Should never be allowed to be opened while in readonly mode in a list
*
* @override
*/
init: function () {
this._super.apply(this, arguments);
// when we empty the input, we delay the setValue to prevent from
// triggering the 'fieldChanged' event twice when the user wants set
// another m2o value ; the following attribute is used to determine when
// we skipped the setValue, s.t. we can perform it later on if the user
// didn't select another value
this.mustSetValue = false;
this.m2oDialogFocused = false;
},
//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
@@ -785,6 +803,13 @@ var ListFieldMany2One = FieldMany2One.extend({
}
return this._super.apply(this, arguments);
},
/**
* @override
*/
reinitialize: function () {
this.mustSetValue = false;
return this._super.apply(this, arguments);
},
//--------------------------------------------------------------------------
// Private
@@ -796,6 +821,52 @@ var ListFieldMany2One = FieldMany2One.extend({
_renderReadonly: function () {
this.$el.text(this.m2o_value);
},
/**
* @override
* @private
*/
_searchCreatePopup: function () {
this.m2oDialogFocused = true;
return this._super.apply(this, arguments);
},
//--------------------------------------------------------------------------
// Handlers
//--------------------------------------------------------------------------
/**
* @private
*/
_onInputFocusin: function () {
this.m2oDialogFocused = false;
},
/**
* In list views, we don't want to try to trigger a fieldChange when the field
* is being emptied. Instead, it will be triggered as the user leaves the field
* while it is empty.
*
* @override
* @private
*/
_onInputFocusout: function () {
this._super.apply(this, arguments);
if (!this.m2oDialogFocused && this.$input.val() === "" && this.mustSetValue) {
this.reinitialize(false);
}
},
/**
* Prevents the triggering of an immediate _onFieldChanged when emptying the field.
*
* @override
* @private
*/
_onInputKeyup: function () {
if (this.$input.val() !== "") {
this._super.apply(this, arguments);
} else {
this.mustSetValue = true;
}
},
});
var KanbanFieldMany2One = AbstractField.extend({
@@ -328,6 +328,7 @@ var SelectCreateDialog = ViewDialog.extend({
this._super.apply(this, arguments);
_.defaults(this.options, { initial_view: 'search' });
this.on_selected = this.options.on_selected || (function () {});
this.on_closed = this.options.on_closed || (function () {});
this.initialIDs = this.options.initial_ids;
},
@@ -411,6 +412,10 @@ var SelectCreateDialog = ViewDialog.extend({
return fragment;
});
},
close: function () {
this._super.apply(this, arguments);
this.on_closed();
},
create_edit_record: function () {
var self = this;
var dialog = new FormViewDialog(this, _.extend({}, this.options, {
@@ -566,6 +566,81 @@ QUnit.module('fields', {}, function () {
form.destroy();
});
QUnit.test("many2one doesn't trigger field_change when being emptied", async function (assert) {
assert.expect(2);
const list = await createView({
arch: `
<tree editable="top">
<field name="trululu"/>
</tree>`,
data: this.data,
model: 'partner',
View: ListView,
});
// Select two records
await testUtils.dom.click(list.$('.o_data_row:eq(0) .o_list_record_selector input'));
await testUtils.dom.click(list.$('.o_data_row:eq(1) .o_list_record_selector input'));
await testUtils.dom.click(list.$('.o_data_row:first() .o_data_cell:first()'));
const $input = list.$('.o_field_widget[name=trululu] input');
await testUtils.fields.editInput($input, "");
await testUtils.dom.triggerEvents($input, ['keyup']);
assert.containsNone(document.body, '.modal',
"No save should be triggered when removing value");
await testUtils.fields.many2one.clickOpenDropdown('trululu');
await testUtils.fields.many2one.clickHighlightedItem('trululu');
assert.containsOnce(document.body, '.modal',
"Saving should be triggered when selecting a value");
await testUtils.dom.click($('.modal .btn-primary'));
list.destroy();
});
QUnit.test("focus tracking on a many2one in a list", async function (assert) {
assert.expect(4);
const list = await createView({
arch: '<tree editable="top"><field name="trululu"/></tree>',
archs: {
'partner,false,form': '<form string="Partners"><field name="foo"/></form>',
},
data: this.data,
model: 'partner',
View: ListView,
});
// Select two records
await testUtils.dom.click(list.$('.o_data_row:eq(0) .o_list_record_selector input'));
await testUtils.dom.click(list.$('.o_data_row:eq(1) .o_list_record_selector input'));
await testUtils.dom.click(list.$('.o_data_row:first() .o_data_cell:first()'));
const input = list.$('.o_data_row:first() .o_data_cell:first() input')[0];
assert.strictEqual(document.activeElement, input, "Input should be focused when activated");
await testUtils.fields.many2one.clickOpenDropdown('trululu');
await testUtils.fields.many2one.clickItem('trululu', 'Create');
// At this point, if the focus is correctly registered by the m2o, there
// should be only one modal (the "Create" one) and none for saving changes.
assert.containsOnce(document.body, '.modal', "There should be only one modal");
await testUtils.dom.click($('.modal .btn:not(.btn-primary)'));
assert.strictEqual(document.activeElement, input, "Input should be focused after dialog closes");
assert.strictEqual(input.value, "", "Input should be empty after discard");
list.destroy();
});
QUnit.test('many2one fields with option "no_open"', async function (assert) {
assert.expect(3);