[IMP] web, *: discard unchanged record in editable list
* sale_product_matrix Before this commit, if we created a new row in an editable list view, do not "touched" it and clicked out of the list then the record tried to be saved and threw error notifications. Now, that flow will discard the record to make it consistent with the form view which discards the record when we leave the form. task-2431691 closes odoo/odoo#68382 Signed-off-by: Aaron Bohy (aab) <aab@odoo.com>
This commit is contained in:
committed by
Michael Mattiello (mcm)
parent
f25d55bc3a
commit
8b6063ac16
@@ -69,7 +69,8 @@ tour.register('sale_matrix_tour', {
|
||||
run: 'click' // apply the matrix
|
||||
}, {
|
||||
trigger: ".o_form_editable .o_field_many2one[name='partner_id'] input",
|
||||
extra_trigger: ".o_sale_order",
|
||||
// wait for qty to be 1
|
||||
extra_trigger: '.o_sale_order .o_field_cell.o_data_cell.o_list_number:contains("1.00")',
|
||||
run: 'text Agrolait'
|
||||
}, {
|
||||
trigger: ".ui-menu-item > a",
|
||||
|
||||
@@ -582,6 +582,7 @@ var BasicModel = AbstractModel.extend({
|
||||
getFieldNames: element.getFieldNames,
|
||||
id: element.id,
|
||||
isDirty: element.isDirty,
|
||||
isNew: element.isNew,
|
||||
limit: element.limit,
|
||||
model: element.model,
|
||||
offset: element.offset,
|
||||
@@ -640,6 +641,7 @@ var BasicModel = AbstractModel.extend({
|
||||
groupsOffset: element.groupsOffset,
|
||||
id: element.id,
|
||||
isDirty: element.isDirty,
|
||||
isNew: element.isNew,
|
||||
isOpen: element.isOpen,
|
||||
isSample: this.isSampleModel,
|
||||
limit: element.limit,
|
||||
@@ -4129,6 +4131,7 @@ var BasicModel = AbstractModel.extend({
|
||||
dataPoint.getDomain = this._getDomain.bind(this, dataPoint);
|
||||
dataPoint.getFieldNames = this._getFieldNames.bind(this, dataPoint);
|
||||
dataPoint.isDirty = this.isDirty.bind(this, dataPoint.id);
|
||||
dataPoint.isNew = this.isNew.bind(this, dataPoint.id);
|
||||
|
||||
this.localData[dataPoint.id] = dataPoint;
|
||||
|
||||
|
||||
@@ -83,7 +83,7 @@ var ListController = BasicController.extend({
|
||||
*/
|
||||
canBeRemoved: async function () {
|
||||
const _super = this._super.bind(this);
|
||||
await this.renderer.unselectRow();
|
||||
await this.renderer.unselectRow({ canDiscard: true });
|
||||
return _super(...arguments);
|
||||
},
|
||||
/*
|
||||
@@ -415,7 +415,7 @@ var ListController = BasicController.extend({
|
||||
return null;
|
||||
}
|
||||
return Object.assign(this._super(...arguments), {
|
||||
validate: () => this.renderer.unselectRow(),
|
||||
validate: () => this.renderer.unselectRow({ canDiscard: true }),
|
||||
});
|
||||
},
|
||||
/**
|
||||
|
||||
@@ -495,11 +495,14 @@ ListRenderer.include({
|
||||
* prevent subsequent editions. These edits would be lost, because the list
|
||||
* view only saves records when unselecting a row.
|
||||
*
|
||||
* @param {Object} [options]
|
||||
* @param {boolean} [options.canDiscard=false] when set to true,
|
||||
* discard the record if it is new and not dirty otherwise save it
|
||||
* @returns {Promise} The promise resolves if the row was unselected (and
|
||||
* possibly removed). If may be rejected, when the row is dirty and the
|
||||
* user refuses to discard its changes.
|
||||
*/
|
||||
unselectRow: function () {
|
||||
unselectRow: function (options={}) {
|
||||
// Protect against calling this method when no row is selected
|
||||
if (this.currentRow === null) {
|
||||
return Promise.resolve();
|
||||
@@ -514,7 +517,23 @@ ListRenderer.include({
|
||||
}
|
||||
|
||||
toggleWidgets(true);
|
||||
return new Promise((resolve, reject) => {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
const record = this._getRecord(recordID);
|
||||
if (options.canDiscard && record.isNew()) {
|
||||
await this.commitChanges(recordID);
|
||||
if (!record.isDirty()) {
|
||||
this.trigger_up('mutexify', {
|
||||
action: () => {
|
||||
return this.trigger_up('discard_changes', {
|
||||
recordID: recordID,
|
||||
onSuccess: resolve,
|
||||
onFailure: reject,
|
||||
});
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
this.trigger_up('save_line', {
|
||||
recordID: recordID,
|
||||
onSuccess: resolve,
|
||||
@@ -1325,7 +1344,7 @@ ListRenderer.include({
|
||||
var recordId = this._getRecordID(rowIndex);
|
||||
// To select a row, the currently selected one must be unselected first
|
||||
var self = this;
|
||||
return this.unselectRow().then((selectNextRow = true) => {
|
||||
return this.unselectRow({ canDiscard: true }).then((selectNextRow = true) => {
|
||||
if (!selectNextRow) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
@@ -1853,7 +1872,11 @@ ListRenderer.include({
|
||||
return;
|
||||
}
|
||||
|
||||
this.unselectRow();
|
||||
this.unselectRow({
|
||||
// if save button is clicked then save the record forcefully even if
|
||||
// it is non dirty
|
||||
canDiscard: ![...event.target.classList].includes('o_list_button_save')
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -2880,6 +2880,7 @@ QUnit.module('fields', {}, function () {
|
||||
arch: '<form>' +
|
||||
'<field name="p">' +
|
||||
'<tree editable="bottom">' +
|
||||
'<field name="foo"/>' +
|
||||
'<field name="trululu"/>' +
|
||||
'</tree>' +
|
||||
'</field>' +
|
||||
@@ -2908,6 +2909,8 @@ QUnit.module('fields', {}, function () {
|
||||
domain = [['id', 'in', [10]]]; // domain for subrecord 1
|
||||
await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a'));
|
||||
await testUtils.dom.click(form.$('.o_field_widget[name=trululu] input'));
|
||||
// add some value to foo field to make record dirty
|
||||
await testUtils.fields.editInput(form.$('tr.o_selected_row input[name="foo"]'), 'new value');
|
||||
|
||||
// add a second row with another domain for the m2o
|
||||
domain = [['id', 'in', [5]]]; // domain for subrecord 2
|
||||
@@ -2916,7 +2919,7 @@ QUnit.module('fields', {}, function () {
|
||||
|
||||
// check again the first row to ensure that the domain hasn't change
|
||||
domain = [['id', 'in', [10]]]; // domain for subrecord 1 should have been kept
|
||||
await testUtils.dom.click(form.$('.o_data_row:first .o_data_cell'));
|
||||
await testUtils.dom.click(form.$('.o_data_row:first .o_data_cell:eq(1)'));
|
||||
await testUtils.dom.click(form.$('.o_field_widget[name=trululu] input'));
|
||||
|
||||
form.destroy();
|
||||
|
||||
@@ -2669,9 +2669,10 @@ QUnit.module('fields', {}, function () {
|
||||
res_id: 1,
|
||||
});
|
||||
|
||||
// add a record, then click in form view to confirm it
|
||||
// add a record, add value to turtle_foo then click in form view to confirm it
|
||||
await testUtils.form.clickEdit(form);
|
||||
await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a'));
|
||||
await testUtils.fields.editInput(form.$('input[name="turtle_foo"]'), 'nora');
|
||||
await testUtils.dom.click(form.$el);
|
||||
|
||||
assert.strictEqual(form.$('.o_field_widget[name=turtles] .o_pager').text().trim(), '1-4 / 5',
|
||||
@@ -2871,11 +2872,12 @@ QUnit.module('fields', {}, function () {
|
||||
res_id: 1,
|
||||
});
|
||||
|
||||
// edit mode, then click on Add an item 2 times
|
||||
// edit mode, then click on Add an item, enter value in turtle_foo and Add an item again
|
||||
assert.containsOnce(form, 'tr.o_data_row',
|
||||
"should have 1 data rows");
|
||||
await testUtils.form.clickEdit(form);
|
||||
await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a'));
|
||||
await testUtils.fields.editInput(form.$('input[name="turtle_foo"]'), 'nora');
|
||||
await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a'));
|
||||
assert.containsN(form, 'tr.o_data_row', 3,
|
||||
"should have 3 data rows");
|
||||
@@ -3006,7 +3008,8 @@ QUnit.module('fields', {}, function () {
|
||||
// the record currently being added should not count in the pager
|
||||
assert.containsNone(form, '.o_field_widget[name=turtles] .o_pager');
|
||||
|
||||
// unselect the row
|
||||
// enter value in turtle_foo field and click outside to unselect the row
|
||||
await testUtils.fields.editInput(form.$('input[name="turtle_foo"]'), 'nora');
|
||||
await testUtils.dom.click(form.$el);
|
||||
assert.containsNone(form, '.o_selected_row');
|
||||
assert.containsNone(form, '.o_field_widget[name=turtles] .o_pager');
|
||||
@@ -3147,8 +3150,11 @@ QUnit.module('fields', {}, function () {
|
||||
// add 4 records (to have more records than the limit)
|
||||
await testUtils.form.clickEdit(form);
|
||||
await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a'));
|
||||
await testUtils.fields.editInput(form.$('input[name="turtle_foo"]'), 'nora');
|
||||
await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a'));
|
||||
await testUtils.fields.editInput(form.$('input[name="turtle_foo"]'), 'nora');
|
||||
await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a'));
|
||||
await testUtils.fields.editInput(form.$('input[name="turtle_foo"]'), 'nora');
|
||||
await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a'));
|
||||
|
||||
assert.containsN(form, 'tr.o_data_row', 5);
|
||||
@@ -8069,9 +8075,11 @@ QUnit.module('fields', {}, function () {
|
||||
assert.strictEqual($('.o_data_cell').text(), "");
|
||||
|
||||
// click add pizza
|
||||
// save the modal
|
||||
// press enter to save the record
|
||||
// check it's pizza
|
||||
await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a:eq(1)'));
|
||||
const $input = form.$('.o_field_widget[name="p"] .o_selected_row .o_field_widget[name="display_name"]');
|
||||
await testUtils.fields.triggerKeydown($input, 'enter');
|
||||
// click add pasta
|
||||
await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a:eq(2)'));
|
||||
await testUtils.form.clickSave(form);
|
||||
|
||||
@@ -539,7 +539,7 @@ QUnit.module('relational_fields', {
|
||||
});
|
||||
|
||||
QUnit.test('one2many, onchange, edition and multipage...', async function (assert) {
|
||||
assert.expect(7);
|
||||
assert.expect(8);
|
||||
|
||||
this.data.partner.onchanges = {
|
||||
turtles: function (obj) {
|
||||
@@ -570,6 +570,7 @@ QUnit.module('relational_fields', {
|
||||
},
|
||||
});
|
||||
await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a'));
|
||||
await testUtils.fields.editInput(form.$('input[name="turtle_foo"]'), 'nora');
|
||||
await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a'));
|
||||
|
||||
assert.verifySteps([
|
||||
@@ -577,6 +578,7 @@ QUnit.module('relational_fields', {
|
||||
'read turtle',
|
||||
'onchange turtle',
|
||||
'onchange partner',
|
||||
"onchange partner",
|
||||
'onchange turtle',
|
||||
'onchange partner',
|
||||
]);
|
||||
|
||||
@@ -5110,7 +5110,7 @@ QUnit.module('Views', {
|
||||
model: 'foo',
|
||||
data: this.data,
|
||||
arch: '<tree string="Phonecalls" editable="top">' +
|
||||
'<field name="date"/>' +
|
||||
'<field name="foo"/>' +
|
||||
'</tree>',
|
||||
mockRPC: function (route, args) {
|
||||
if (args.method === 'create') {
|
||||
@@ -5121,16 +5121,19 @@ QUnit.module('Views', {
|
||||
});
|
||||
|
||||
await testUtils.dom.click(list.$buttons.find('.o_list_button_add'));
|
||||
await testUtils.fields.editInput(list.$('tr.o_selected_row input[name="foo"]'), 'new value');
|
||||
await testUtils.dom.click(list.$('.o_list_view'));
|
||||
|
||||
assert.strictEqual(createCount, 1, "should have created a record");
|
||||
|
||||
await testUtils.dom.click(list.$buttons.find('.o_list_button_add'));
|
||||
await testUtils.fields.editInput(list.$('tr.o_selected_row input[name="foo"]'), 'new value');
|
||||
await testUtils.dom.click(list.$('tfoot'));
|
||||
|
||||
assert.strictEqual(createCount, 2, "should have created a record");
|
||||
|
||||
await testUtils.dom.click(list.$buttons.find('.o_list_button_add'));
|
||||
await testUtils.fields.editInput(list.$('tr.o_selected_row input[name="foo"]'), 'new value');
|
||||
await testUtils.dom.click(list.$('tbody tr').last());
|
||||
|
||||
assert.strictEqual(createCount, 3, "should have created a record");
|
||||
@@ -6343,6 +6346,7 @@ QUnit.module('Views', {
|
||||
|
||||
// Press 'Tab' -> should go to next line
|
||||
await testUtils.fields.triggerKeydown(form.$('.o_field_widget[name=o2m] .o_selected_row input'), 'tab');
|
||||
await testUtils.nextTick(); // wait for discard
|
||||
assert.hasClass(form.$('.o_field_widget[name=o2m] .o_data_row:nth(1)'),'o_selected_row',
|
||||
"second row should be in edition");
|
||||
|
||||
@@ -7249,7 +7253,7 @@ QUnit.module('Views', {
|
||||
list.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('list with handle widget, create, move and discard', async function (assert) {
|
||||
QUnit.test('list with handle widget, create and move should keep empty lines at last', async function (assert) {
|
||||
// When there are less than 4 records in the table, empty lines are added
|
||||
// to have at least 4 rows. This test ensures that the empty line added
|
||||
// when a new record is discarded is correctly added on the bottom of
|
||||
@@ -7587,6 +7591,100 @@ QUnit.module('Views', {
|
||||
list.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('editable list view: non dirty record with required fields', async function (assert) {
|
||||
assert.expect(10);
|
||||
|
||||
const list = await createView({
|
||||
arch: `
|
||||
<tree editable="top">
|
||||
<field name="foo" required="1"/>
|
||||
<field name="int_field"/>
|
||||
</tree>`,
|
||||
data: this.data,
|
||||
model: 'foo',
|
||||
View: ListView,
|
||||
});
|
||||
|
||||
await testUtils.dom.click($('.o_list_button_add'));
|
||||
assert.containsOnce(list, '.o_selected_row');
|
||||
|
||||
// do not change anything and then click outside should discard record
|
||||
await testUtils.dom.click('body');
|
||||
assert.containsNone(list, '.o_selected_row');
|
||||
|
||||
await testUtils.dom.click($('.o_list_button_add'));
|
||||
assert.containsOnce(list, '.o_selected_row');
|
||||
// do not change anything and then click save button should not allow to discard record
|
||||
await testUtils.dom.click($('.o_list_button_save'));
|
||||
assert.containsOnce(list, '.o_selected_row');
|
||||
|
||||
// selecting some other row should discard non dirty record
|
||||
assert.strictEqual(list.$('.o_selected_row').data('id'), 'foo_7',
|
||||
"foo_7 record should be currently selected");
|
||||
await testUtils.dom.click(list.$('.o_data_row:eq(1) .o_data_cell:eq(0)'));
|
||||
assert.strictEqual(list.$('.o_selected_row').data('id'), 'foo_2',
|
||||
"foo_2 record should be currently selected");
|
||||
|
||||
// click somewhere else to discard currently selected row
|
||||
await testUtils.dom.click('body');
|
||||
await testUtils.dom.click($('.o_list_button_add'));
|
||||
assert.containsOnce(list, '.o_selected_row');
|
||||
// do not change anything and press Enter key should not allow to discard record
|
||||
await testUtils.fields.triggerKeydown(list.$('tr.o_selected_row input.o_field_widget[name="foo"]'), 'enter');
|
||||
assert.containsOnce(list, '.o_selected_row');
|
||||
|
||||
// discard row and create new record and keep required field empty and click anywhere
|
||||
await testUtils.dom.click(list.$buttons.find('.o_list_button_discard'));
|
||||
await testUtils.dom.click($('.o_list_button_add'));
|
||||
assert.containsOnce(list, '.o_selected_row', "row should be selected");
|
||||
await testUtils.fields.editInput(list.$('.o_selected_row .o_field_widget[name=int_field]'), 123);
|
||||
await testUtils.dom.click('body');
|
||||
assert.containsOnce(list, '.o_selected_row', "row should still be selected");
|
||||
|
||||
list.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('editable list view: do not commit changes twice', async function (assert) {
|
||||
assert.expect(5);
|
||||
|
||||
this.data.foo.onchanges = {
|
||||
bar: function (obj) {
|
||||
obj.foo = 'changed';
|
||||
},
|
||||
};
|
||||
|
||||
const list = await createView({
|
||||
View: ListView,
|
||||
model: 'foo',
|
||||
data: this.data,
|
||||
arch: `
|
||||
<tree editable="top">
|
||||
<field name="foo"/>
|
||||
<field name="bar"/>
|
||||
</tree>`,
|
||||
mockRPC(route, { method, model }) {
|
||||
if (model === 'foo' && method === 'onchange') {
|
||||
assert.step('onchange');
|
||||
}
|
||||
return this._super(...arguments);
|
||||
},
|
||||
});
|
||||
testUtils.mock.intercept(list, "field_changed", function (ev) {
|
||||
assert.deepEqual(ev.data.changes, { bar: false });
|
||||
}, true);
|
||||
|
||||
assert.strictEqual(list.$('.o_field_cell[name="foo"]')[0].textContent, 'yop');
|
||||
|
||||
await testUtils.dom.click(list.$('.o_field_cell[name="bar"]')[0]);
|
||||
await testUtils.dom.click(list.$('.o_field_cell[name="bar"] label')[0]);
|
||||
|
||||
await testUtils.dom.click(list.$('.o_list_button_save')[0]);
|
||||
assert.strictEqual(list.$('.o_field_cell[name="foo"]')[0].textContent, 'changed');
|
||||
assert.verifySteps([ 'onchange' ]);
|
||||
|
||||
list.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('editable list view: multi edition', async function (assert) {
|
||||
assert.expect(26);
|
||||
|
||||
@@ -8543,6 +8641,7 @@ QUnit.module('Views', {
|
||||
await testUtils.dom.click(form.$('.o_field_x2many_list_row_add > a'));
|
||||
assert.containsOnce(form, '.o_data_row');
|
||||
assert.hasClass(form.$('.o_data_row'), 'o_selected_row');
|
||||
await testUtils.fields.editInput(form.$('.o_field_x2many[name="o2m"] .o_selected_row input[name="display_name"]'), 'new value');
|
||||
|
||||
// click outside to unselect the row
|
||||
await testUtils.dom.click(document.body);
|
||||
@@ -10453,6 +10552,7 @@ QUnit.module('Views', {
|
||||
|
||||
await testUtils.dom.click(list.$('.o_group_header:first')); // open group
|
||||
await testUtils.dom.click(list.$('.o_group_field_row_add a')); // add a new row
|
||||
await testUtils.fields.editInput(list.$('input[name="foo"]'), 'xyz'); // make record dirty
|
||||
await testUtils.dom.click($('body')); // unselect row
|
||||
assert.verifySteps(['1']);
|
||||
assert.strictEqual(list.$('.o_data_row .o_data_cell:eq(1)').text(), 'Low',
|
||||
|
||||
Reference in New Issue
Block a user