[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:
committed by
Aaron Bohy
parent
015958dc17
commit
a75fa7e377
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user