[IMP] web: list multi edition confirmation modal

Changed the information dipslayed by the confirmation modal in multi edition
in list views.

Before this commit, the modal simply showed a one-line message with the amount of valid records.

Now, there are 3 elements:
- If all of the fields cannot be changed, a first line warns the amount of valid records.
- A second (or first if all fields can be updated) line ensuring that the user is aware of
the occuring changes.
- A table displaying the affected field and the updated value (readonly widget with the new value).

Part of task 1967602
This commit is contained in:
Julien Mougenot
2019-09-25 07:20:36 +00:00
committed by Aaron Bohy
parent 6fea5b42d6
commit f328b9ca8c
6 changed files with 158 additions and 45 deletions
@@ -0,0 +1,78 @@
odoo.define('web.ListConfirmDialog', function (require) {
"use strict";
const core = require('web.core');
const Dialog = require('web.Dialog');
const _t = core._t;
const qweb = core.qweb;
/**
* Multi edition confirmation modal for list views.
*
* Handles the display of the amount of changed records (+ valid ones) and
* of the widget representing the new value.
*
* @class
*/
const ListConfirmDialog = Dialog.extend({
/**
* @constructor
* @override
* @param {Widget} parent
* @param {Object} record edited record with updated value
* @param {Object} changes changes registered by the list controller
* @param {string} changes.fieldLabel label of the changed field
* @param {string} changes.fieldName technical name of the changed field
* @param {number} changes.nbRecords number of records (total)
* @param {number} changes.nbValidRecords number of valid records
* @param {Object} [options]
*/
init: function (parent, record, changes, options) {
options = Object.assign({}, options, {
$content: $(qweb.render('ListView.confirmModal', { changes })),
buttons: options.buttons || [{
text: _t("Ok"),
classes: 'btn-primary',
close: true,
click: options.confirm_callback,
}, {
text: _t("Cancel"),
close: true,
click: options.cancel_callback,
}],
onForceClose: options.cancel_callback,
size: options.size || 'medium',
title: options.title || _t("Confirmation"),
});
this._super(parent, options);
const Widget = record.fieldsInfo.list[changes.fieldName].Widget;
this.fieldWidget = new Widget(this, changes.fieldName, record, {
mode: 'readonly',
viewType: 'list',
noOpen: true,
});
},
/**
* @override
*/
willStart: function () {
return Promise.all([
this.fieldWidget.appendTo(document.createDocumentFragment()),
this._super.apply(this, arguments)
]);
},
/**
* @override
*/
start: function () {
this.$content.find('.o_changes_widget').replaceWith(this.fieldWidget.$el);
return this._super.apply(this, arguments);
},
});
return ListConfirmDialog;
});
@@ -11,6 +11,7 @@ var core = require('web.core');
var BasicController = require('web.BasicController');
var DataExport = require('web.DataExport');
var Dialog = require('web.Dialog');
var ListConfirmDialog = require('web.ListConfirmDialog');
var Sidebar = require('web.Sidebar');
var _t = core._t;
@@ -396,62 +397,54 @@ var ListController = BasicController.extend({
* @returns {Promise}
*/
_saveMultipleRecords: function (recordId, node, changes) {
var self = this;
var fieldName = Object.keys(changes)[0];
var value = Object.values(changes)[0];
var recordIds = _.union([recordId], this.selectedRecords);
var validRecordIds = recordIds.reduce(function (result, nextRecordId) {
var record = self.model.get(nextRecordId);
var modifiers = self.renderer._registerModifiers(node, record);
var validRecordIds = recordIds.reduce((result, nextRecordId) => {
var record = this.model.get(nextRecordId);
var modifiers = this.renderer._registerModifiers(node, record);
if (!modifiers.readonly && (!modifiers.required || value)) {
result.push(nextRecordId);
}
return result;
}, []);
const nbInvalid = recordIds.length - validRecordIds.length;
return new Promise((resolve, reject) => {
const rejectAndDiscard = () => {
const discardAndReject = () => {
this.model.discardChanges(recordId);
return this._confirmSave(recordId).then(reject);
this._confirmSave(recordId).then(() => {
this.renderer.focusCell(recordId, node);
reject();
});
};
let dialog;
if (validRecordIds.length > 0) {
let message;
if (nbInvalid === 0) {
message = _.str.sprintf(
_t("Do you want to set the value on the %d selected records?"),
validRecordIds.length);
} else {
message = _.str.sprintf(
_t("Do you want to set the value on the %d valid selected records? (%d invalid)"),
validRecordIds.length, nbInvalid);
}
dialog = Dialog.confirm(this, message, {
const dialogOptions = {
confirm_callback: () => {
return this.model.saveRecords(recordId, validRecordIds, fieldName)
this.model.saveRecords(recordId, validRecordIds, fieldName)
.then(async () => {
this._updateButtons('readonly');
const state = this.model.get(this.handle);
await this.renderer.updateState(state, {keepWidths: true});
await this.renderer.updateState(state, { keepWidths: true });
await this.renderer.focusCell(recordId, node);
resolve(Object.keys(changes));
})
.guardedCatch(rejectAndDiscard);
.guardedCatch(discardAndReject);
},
cancel_callback: rejectAndDiscard,
});
cancel_callback: discardAndReject,
};
const record = this.model.get(recordId);
const dialogChanges = {
fieldLabel: node.attrs.string || record.fields[fieldName].string,
fieldName: node.attrs.name,
nbRecords: recordIds.length,
nbValidRecords: validRecordIds.length,
};
new ListConfirmDialog(this, record, dialogChanges, dialogOptions)
.open({ shouldFocusButtons: true });
} else {
dialog = Dialog.alert(this, _t("No valid record to save"), {
confirm_callback: rejectAndDiscard,
Dialog.alert(this, _t("No valid record to save"), {
confirm_callback: discardAndReject,
});
}
dialog.on('closed', this, async () => {
// we need to wait for the dialog to be actually closed, but
// the 'closed' event is triggered just before, and it prevents
// from focussing the cell
await Promise.resolve();
this.renderer.focusCell(recordId, node);
});
});
},
/**
@@ -750,14 +743,14 @@ var ListController = BasicController.extend({
* @param {OdooEvent} ev
*/
_onSetDirty: function (ev) {
var self = this;
var recordId = ev.data.dataPointID;
if (this.renderer.inMultipleRecordEdition(recordId)) {
ev.stopPropagation();
Dialog.alert(this, _t("No valid record to save"), {
confirm_callback: function () {
self.model.discardChanges(recordId);
self._confirmSave(recordId);
confirm_callback: async () => {
this.model.discardChanges(recordId);
await this._confirmSave(recordId);
this.renderer.focusCell(recordId, ev.target.__node);
},
});
} else {
+11
View File
@@ -27,6 +27,17 @@
.o_view_nocontent {
position: unset;
}
.o_modal_changes td {
&:first-child {
padding-right: 10px;
vertical-align: top;
white-space: nowrap;
}
&:not(:first-child) {
width: 100%;
}
}
}
.modal-footer {
+28
View File
@@ -281,6 +281,34 @@
</div>
</t>
<t t-name="ListView.confirmModal">
<main role="alert">
<p>
<t t-if="changes.nbRecords != changes.nbValidRecords">
Among the <t t-raw="changes.nbRecords"/> selected records,
<t t-raw="changes.nbValidRecords"/> are valid for this update.<br/>
</t>
Are you sure you want to perform the following update on those <t t-raw="changes.nbValidRecords"/> records ?
</p>
<div class="table-responsive">
<table class="o_modal_changes">
<tbody>
<tr>
<td>Field:</td>
<td><t t-raw="changes.fieldLabel"/></td>
</tr>
<tr>
<td>Update to:</td>
<td>
<div class="o_changes_widget"/>
</td>
</tr>
</tbody>
</table>
</div>
</main>
</t>
<t t-name="FormView.buttons">
<div class="o_form_buttons_view" role="toolbar" aria-label="Main actions">
<button t-if="widget.is_action_enabled('edit')" type="button"
+9 -7
View File
@@ -5430,7 +5430,7 @@ QUnit.module('Views', {
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);
await testUtils.dom.click(list.$('.o_data_row:eq(1) .o_data_cell:eq(0)'));
assert.ok($('.modal').text().includes('2 selected records'), "the number of records should be correctly displayed");
assert.ok($('.modal').text().includes('those 2 records'), "the number of records should be correctly displayed");
await testUtils.dom.click($('.modal .btn-primary'));
assert.containsNone(list, '.o_data_cell input.o_field_widget', "no field should be editable anymore");
assert.verifySteps(['write', 'read']);
@@ -5660,7 +5660,7 @@ QUnit.module('Views', {
});
QUnit.test('editable list view: multi edition with readonly modifiers', async function (assert) {
assert.expect(5);
assert.expect(4);
var list = await createView({
View: ListView,
@@ -5686,17 +5686,19 @@ QUnit.module('Views', {
// 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");
const modalText = $('.modal-body').text()
.split(" ").filter(w => w.trim() !== '').join(" ")
.split("\n").join('');
assert.strictEqual(modalText,
"Among the 4 selected records, 2 are valid for this update. Are you sure you want to " +
"perform the following update on those 2 records ? Field: int_field Update to: 666");
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();
});
+1
View File
@@ -351,6 +351,7 @@
<script type="text/javascript" src="/web/static/src/js/views/list/list_renderer.js"></script>
<script type="text/javascript" src="/web/static/src/js/views/list/list_view.js"></script>
<script type="text/javascript" src="/web/static/src/js/views/list/list_controller.js"></script>
<script type="text/javascript" src="/web/static/src/js/views/list/list_confirm_dialog.js"></script>
<script type="text/javascript" src="/web/static/src/js/views/pivot/pivot_model.js"></script>
<script type="text/javascript" src="/web/static/src/js/views/pivot/pivot_controller.js"></script>
<script type="text/javascript" src="/web/static/src/js/views/pivot/pivot_renderer.js"></script>