From a75fa7e377829ac22f85eab3bfefd79705458236 Mon Sep 17 00:00:00 2001 From: Martin Geubelle Date: Wed, 20 Mar 2019 14:13:01 +0000 Subject: [PATCH] [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 --- .../src/js/views/list/list_controller.js | 87 +++++++++++++- .../static/src/js/views/list/list_model.js | 46 ++++++++ addons/web/static/tests/views/list_tests.js | 106 ++++++++++++++++++ 3 files changed, 237 insertions(+), 2 deletions(-) diff --git a/addons/web/static/src/js/views/list/list_controller.js b/addons/web/static/src/js/views/list/list_controller.js index c96b677e2ea..c4d1ca8959f 100644 --- a/addons/web/static/src/js/views/list/list_controller.js +++ b/addons/web/static/src/js/views/list/list_controller.js @@ -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); }, diff --git a/addons/web/static/src/js/views/list/list_model.js b/addons/web/static/src/js/views/list/list_model.js index 0be8f54e4f1..73c2d86864d 100644 --- a/addons/web/static/src/js/views/list/list_model.js +++ b/addons/web/static/src/js/views/list/list_model.js @@ -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 diff --git a/addons/web/static/tests/views/list_tests.js b/addons/web/static/tests/views/list_tests.js index cb6a1ef805d..83b5ebd25c5 100644 --- a/addons/web/static/tests/views/list_tests.js +++ b/addons/web/static/tests/views/list_tests.js @@ -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: '' + + '' + + '' + + '', + 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: '' + + '' + + '' + + '" , 2)]}\'/>' + + '', + 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);