[IMP] web: allow mass update in editable lists

From an editable list view, if you select several lines then change
a field, you are now asked if you want to apply the same change on
all selected lines. If you accept, a write is directly performed
(without applying onchange, as the records are reloaded and the view
re-rendered). If you decline, the classic behavior still occurs.

Part of task 1915702
This commit is contained in:
Martin Geubelle
2019-04-02 11:55:57 +00:00
committed by Aaron Bohy
parent 015958dc17
commit a75fa7e377
3 changed files with 237 additions and 2 deletions
@@ -342,6 +342,15 @@ var ListController = BasicController.extend({
var record = this.model.get(this.handle);
return _.extend(env, {domain: record.getDomain()});
},
/**
* @private
* @returns {boolean}
*/
_inMultipleRecordEdition: function (recordId) {
var record = this.model.get(recordId, { raw: true });
var recordIds = _.union([recordId], this.selectedRecords);
return recordIds.length > 1 && record.res_id;
},
/**
* Only display the pager when there are data to display.
*
@@ -352,6 +361,57 @@ var ListController = BasicController.extend({
var state = this.model.get(this.handle, {raw: true});
return !!state.count;
},
/**
* @private
* @param {string} recordId
* @param {Object} node
* @param {Object} changes
*/
_saveMultipleRecords: function (recordId, node, changes) {
var self = this;
var value = Object.values(changes)[0];
var recordIds = _.union([recordId], this.selectedRecords);
var validRecordIds = recordIds.reduce(function (result, recordId) {
var record = self.model.get(recordId);
var modifiers = self.renderer._registerModifiers(node, record);
if (!modifiers.readonly && (!modifiers.required || value)) {
result.push(recordId);
}
return result;
}, []);
var message = _.str.sprintf(
_t('Do you want to set the value on the %d valid selected records?'),
validRecordIds.length);
if (recordIds.length !== validRecordIds.length) {
var nbInvalid = recordIds.length - validRecordIds.length;
message += ' ' + _.str.sprintf(_t('(%d invalid)'), nbInvalid);
}
Dialog.confirm(this, message, {
confirm_callback: function () {
self.model.saveRecords(recordId, validRecordIds)
.then(function () {
self._updateButtons('readonly');
var state = self.model.get(self.handle);
self.renderer.updateState(state, {});
});
},
});
},
/**
* Overridden to deal with edition of multiple line.
*
* @override
* @param {string} recordId
*/
_saveRecord: function (recordId) {
var record = this.model.get(recordId, { raw: true });
if (record.isDirty() && this._inMultipleRecordEdition(recordId)) {
// do not save the record (see _saveMultipleRecords)
return Promise.resolve();
}
return this._super.apply(this, arguments);
},
/**
* Allows to change the mode of a single row.
*
@@ -545,6 +605,30 @@ var ListController = BasicController.extend({
flags: {mode: 'edit'},
});
},
/**
* Overridden to deal with the edition of multiple records.
*
* Note that we don't manage saving multiple records on saveLine
* because we don't want the onchanges to be applied.
*
* @private
* @override
*/
_onFieldChanged: function (ev) {
ev.stopPropagation();
var self = this;
if (this._inMultipleRecordEdition(ev.data.dataPointID)) {
// deal with edition of multiple lines
var _onSuccess = ev.data.onSuccess;
ev.data.onSuccess = function () {
Promise.resolve(_onSuccess()).then(function () {
self._saveMultipleRecords(ev.data.dataPointID, ev.target.__node, ev.data.changes);
});
};
}
this._super.apply(this, arguments);
},
/**
* Force a resequence of the records curently on this page.
*
@@ -579,8 +663,7 @@ var ListController = BasicController.extend({
* @param {OdooEvent} ev
*/
_onSaveLine: function (ev) {
var recordID = ev.data.recordID;
this.saveRecord(recordID)
this.saveRecord(ev.data.recordID)
.then(ev.data.onSuccess)
.guardedCatch(ev.data.onFailure);
},
@@ -33,6 +33,52 @@ odoo.define('web.ListModel', function (require) {
}
return result;
},
/**
* For a list of records, performs a write with all changes and fetches
* all data.
*
* @param {string} referenceRecordId the record datapoint used to
* generate the changes to apply to recordIds
* @param {string[]} recordIds a list of record datapoint ids
*/
saveRecords: function (referenceRecordId, recordIds) {
var self = this;
var referenceRecord = this.localData[referenceRecordId];
var list = this.localData[referenceRecord.parentID];
var changes = this._generateChanges(referenceRecord, {});
var records = recordIds.map(function (recordId) {
return self.localData[recordId];
});
var model = records[0].model;
var recordResIds = _.pluck(records, 'res_id');
var fieldNames = records[0].getFieldNames();
return this._rpc({
model: model,
method: 'write',
args: [recordResIds, changes],
context: records[0].getContext(),
}).then(function () {
return self._rpc({
model: model,
method: 'read',
args: [recordResIds, fieldNames],
});
}).then(function (results) {
results.forEach(function (data) {
var record = _.findWhere(records, {res_id: data.id});
record.data = _.extend({}, record.data, data);
record._changes = {};
record._isDirty = false;
self._parseServerData(fieldNames, record, record.data);
});
}).then(function () {
return Promise.all([
self._fetchX2ManysBatched(list),
self._fetchReferencesBatched(list)
]);
});
},
//--------------------------------------------------------------------------
// Private
+106
View File
@@ -3833,6 +3833,112 @@ QUnit.module('Views', {
list.destroy();
});
QUnit.test('editable list view: multi edition', async function (assert) {
assert.expect(19);
var list = await createView({
View: ListView,
model: 'foo',
data: this.data,
arch: '<tree editable="bottom">' +
'<field name="foo"/>' +
'<field name="int_field"/>' +
'</tree>',
mockRPC: function (route, args) {
assert.step(args.method || route);
if (args.method === 'write') {
assert.deepEqual(args.args, [[1, 2], { int_field: 666 }],
"should write on multi records");
} else if (args.method === 'read') {
if (args.args[0].length !== 1) {
assert.deepEqual(args.args, [[1, 2], ['foo', 'int_field']],
"should batch the read");
}
}
return this._super.apply(this, arguments);
},
});
assert.verifySteps(['/web/dataset/search_read']);
// 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'));
// edit a line witout modifying a field
await testUtils.dom.click(list.$('.o_data_row:eq(0) .o_data_cell:eq(1)'));
assert.hasClass(list.$('.o_data_row:eq(0)'), 'o_selected_row',
"the first row should be selected");
await testUtils.dom.click('body');
assert.containsNone(list, '.o_selected_row', "no row should be selected");
// create a record and edit its value
await testUtils.dom.click($('.o_list_button_add'));
assert.verifySteps(['default_get']);
await testUtils.fields.editInput(list.$('.o_selected_row .o_field_widget[name=int_field]'), 123);
assert.containsNone($, '.modal', "the multi edition should not be triggered during creation");
await testUtils.dom.click($('.o_list_button_save'));
assert.verifySteps(['create', 'read']);
// edit a field
await testUtils.dom.click(list.$('.o_data_row:eq(0) .o_data_cell:eq(1)'));
await testUtils.fields.editInput(list.$('.o_field_widget[name=int_field]'), 666);
assert.containsOnce($, '.modal', "there should be an opened modal");
assert.ok($('.modal').text().includes('2 valid'), "the number of records should be correctly displayed");
await testUtils.dom.click($('.modal .btn-primary'));
assert.verifySteps(['write', 'read']);
assert.strictEqual(list.$('.o_data_row:eq(0) .o_data_cell').text(), "yop666",
"the first row should be updated");
assert.strictEqual(list.$('.o_data_row:eq(1) .o_data_cell').text(), "blip666",
"the second row should be updated");
list.destroy();
});
QUnit.test('editable list view: multi edition with readonly modifiers', async function (assert) {
assert.expect(5);
var list = await createView({
View: ListView,
model: 'foo',
data: this.data,
arch: '<tree editable="top">' +
'<field name="id"/>' +
'<field name="foo"/>' +
'<field name="int_field" attrs=\'{"readonly": [("id", ">" , 2)]}\'/>' +
'</tree>',
mockRPC: function (route, args) {
if (args.method === 'write') {
assert.deepEqual(args.args, [[1, 2], { int_field: 666 }],
"should only write on the valid records");
}
return this._super.apply(this, arguments);
},
});
// select all records
await testUtils.dom.click(list.$('th.o_list_record_selector input'));
// edit a field
await testUtils.dom.click(list.$('.o_data_row:eq(0) .o_data_cell:eq(1)'));
await testUtils.fields.editInput(list.$('.o_field_widget[name=int_field]'), 666);
assert.ok($('.modal').text().includes('2 valid'),
"the number of records should be correctly displayed (only 2 not readonly)");
assert.ok($('.modal').text().includes('2 invalid'),
"should display the number of invalid records");
await testUtils.dom.click($('.modal .btn-primary'));
assert.strictEqual(list.$('.o_data_row:eq(0) .o_data_cell').text(), "1yop666",
"the first row should be updated");
assert.strictEqual(list.$('.o_data_row:eq(1) .o_data_cell').text(), "2blip666",
"the second row should be updated");
list.destroy();
});
QUnit.test('list grouped by date:month', async function (assert) {
assert.expect(1);