[FIX] mail: warn user on record reload before post message

Before this commit, the user could lost some unsaved changes in a record when
posting a message in the chatter.

This is due to reloading the data on the record, which is necessary because
a `message_post` may make some changes on the record [1].

With this commit, a dialog will warn the user that the record will be reloaded
in the following cases:

    a) posting a message in the basic composer that will reload the record
    b) opening the full-composer when the record is dirty

Indeed, any operation after opening the full-composer will always refresh the
record (even closing the dialog).

Known issues:

    - a record is not considered as dirty if we only make changes in its
      description from a pad.

      > the content of the pad will be erased without any warning...

[1] https://github.com/odoo-dev/odoo/commit/803d5cf04835fcf8e7ddb143e1ab140408beef93

opw-1825005
This commit is contained in:
Alexandre Kühn
2018-06-15 17:46:32 +02:00
parent b170a753e1
commit b74d1e90ef
3 changed files with 361 additions and 13 deletions
+65 -5
View File
@@ -24,6 +24,7 @@ var QWeb = core.qweb;
var Chatter = Widget.extend({
template: 'mail.Chatter',
custom_events: {
discard_record_changes: '_onDiscardRecordChanges',
reload_mail_fields: '_onReloadMailFields',
},
events: {
@@ -154,6 +155,37 @@ var Chatter = Widget.extend({
this.composer.clear_composer();
}
},
/**
* Discard changes on the record.
*
* @private
* @returns {$.Deferred} resolved if successfully discarding changes on
* the record, rejected otherwise
*/
_discardChanges: function () {
var def = $.Deferred();
this.trigger_up('discard_changes', {
recordID: this.record.id,
onSuccess: def.resolve.bind(def),
onFailure: def.reject.bind(def),
});
return def;
},
/**
* Discard changes on the record if the message will reload the record
* after posting it
*
* @private
* @param {Object} message
* @return {$.Deferred} resolved if no reload or proceed to discard the
* changes on the record, rejected otherwise
*/
_discardOnReload: function (message) {
if (this._reloadAfterPost(message)) {
return this._discardChanges();
}
return $.when();
},
/**
* @private
*/
@@ -199,11 +231,13 @@ var Chatter = Widget.extend({
self.composer.focus();
}
self.composer.on('post_message', self, function (message) {
self.fields.thread.postMessage(message).then(function () {
self._closeComposer(true);
if (self.postRefresh === 'always' || (self.postRefresh === 'recipients' && message.partner_ids.length)) {
self.trigger_up('reload');
}
self._discardOnReload(message).then(function () {
self.fields.thread.postMessage(message).then(function () {
self._closeComposer(true);
if (self._reloadAfterPost(message)) {
self.trigger_up('reload');
}
});
});
});
self.composer.on('need_refresh', self, self.trigger_up.bind(self, 'reload'));
@@ -215,6 +249,24 @@ var Chatter = Widget.extend({
self.$('.o_chatter_button_log_note').toggleClass('o_active', self.composer.options.is_log);
});
},
/**
* State if the record will be reloaded after posting a message.
* Useful to warn the user of unsaved changes if the record is dirty.
*
* @private
* @param {Object} message
* @param {Array} [message.partner_ids] list of recipients of a message
* @return {boolean} true if record will be reloaded after posting the
* message, false otherwise
*/
_reloadAfterPost: function (message) {
return this.postRefresh === 'always' ||
(
this.postRefresh === 'recipients' &&
message.partner_ids &&
message.partner_ids.length
);
},
/**
* @private
* @param {Deferred} def
@@ -314,8 +366,16 @@ var Chatter = Widget.extend({
//--------------------------------------------------------------------------
/**
* Discard changes on the record.
* This is notified by the composer, when opening the full-composer.
*
* @private
* @param {OdooEvent} ev
* @param {function} ev.data.proceed callback to tell to proceed
*/
_onDiscardRecordChanges: function (ev) {
this._discardChanges().then(ev.data.proceed);
},
_onOpenComposerMessage: function () {
var self = this;
if (!this.suggested_partners_def) {
+17 -8
View File
@@ -201,14 +201,23 @@ var ChatterComposer = composer.BasicComposer.extend({
}
var self = this;
var recipient_done = $.Deferred();
if (this.options.is_log) {
recipient_done.resolve([]);
} else {
var checked_suggested_partners = this.get_checked_suggested_partners();
recipient_done = this.check_suggested_partners(checked_suggested_partners);
}
recipient_done.then(function (partner_ids) {
var recipientDoneDef = $.Deferred();
// any operation on the full-composer will reload the record, so
// warn the user that any unsaved changes on the record will be lost.
this.trigger_up('discard_record_changes', {
proceed: function () {
if (self.options.is_log) {
recipientDoneDef.resolve([]);
} else {
var checked_suggested_partners = self.get_checked_suggested_partners();
self.check_suggested_partners(checked_suggested_partners)
.then(recipientDoneDef.resolve.bind(recipientDoneDef));
}
},
});
recipientDoneDef.then(function (partner_ids) {
var context = {
default_parent_id: self.id,
default_body: utils.get_text2html(self.$input.val()),
+279
View File
@@ -609,6 +609,285 @@ QUnit.test('chatter: post a message and switch in edit mode', function (assert)
form.destroy();
});
QUnit.test('chatter: discard changes on message post with post_refresh "always"', function (assert) {
// After posting a message that always reloads the record, if the record
// is dirty (= has some unsaved changes), we should warn the user that
// these changes will be lost if he proceeds.
assert.expect(2);
var form = createView({
View: FormView,
model: 'partner',
data: this.data,
services: this.services,
arch: '<form string="Partners">' +
'<sheet>' +
'<field name="foo"/>' +
'</sheet>' +
'<div class="oe_chatter">' +
'<field name="message_ids" widget="mail_thread"' +
' options="{\'display_log_button\': True, \'post_refresh\': \'always\'}"/>' +
'</div>' +
'</form>',
res_id: 2,
session: {},
mockRPC: function (route, args) {
if (route === "/web/dataset/call_kw/partner/message_get_suggested_recipients") {
return $.when({2: []});
}
return this._super(route, args);
},
viewOptions: {
mode: 'edit',
},
});
// Make record dirty
form.$('.o_form_sheet input').val('trululu').trigger('input');
// Send a message
form.$('.o_chatter_button_new_message').click();
form.$('.oe_chatter .o_composer_text_field:first()').val("My first message");
form.$('.oe_chatter .o_composer_button_send').click();
var $modal = $('.modal-dialog');
assert.strictEqual($modal.length, 1, "should have a modal opened");
assert.strictEqual($modal.find('.modal-body').text(),
"The record has been modified, your changes will be discarded. Do you want to proceed?",
"should warn the user that any unsaved changes will be lost");
form.destroy();
});
QUnit.test('chatter: discard changes on message post without post_refresh', function (assert) {
// After posting a message, if the record is dirty and there are no
// post_refresh rule, it will not discard the changes on the record.
assert.expect(2);
var hasDiscardChanges = false; // set if `discard_changes` has been triggered up
var messages = [];
var form = createView({
View: FormView,
model: 'partner',
data: this.data,
services: this.services,
arch: '<form string="Partners">' +
'<sheet>' +
'<field name="foo"/>' +
'</sheet>' +
'<div class="oe_chatter">' +
'<field name="message_ids" widget="mail_thread"' +
' options="{\'display_log_button\': True}"/>' +
'</div>' +
'</form>',
res_id: 2,
session: {},
mockRPC: function (route, args) {
if (route === "/web/dataset/call_kw/partner/message_get_suggested_recipients") {
return $.when({2: []});
}
if (args.method === 'message_format') {
var requested_msgs = _.filter(messages, function (msg) {
return _.contains(args.args[0], msg.id);
});
return $.when(requested_msgs);
}
if (args.method === 'message_post') {
messages.push({
attachment_ids: [],
author_id: ["42", "Me"],
body: args.kwargs.body,
date: moment().format('YYYY-MM-DD HH:MM:SS'), // now
displayed_author: "Me",
id: 42,
is_note: args.kwargs.subtype === 'mail.mt_note',
is_starred: false,
model: 'partner',
res_id: 2,
});
return $.when(42);
}
return this._super(route, args);
},
intercepts: {
discard_changes: function () {
hasDiscardChanges = true; // should not do that
},
},
viewOptions: {
mode: 'edit',
},
});
// Make record dirty
form.$('.o_form_sheet input').val('trululu').trigger('input');
// Send a message
form.$('.o_chatter_button_new_message').click();
form.$('.oe_chatter .o_composer_text_field:first()').val("My first message");
form.$('.oe_chatter .o_composer_button_send').click();
var $modal = $('.modal-dialog');
assert.strictEqual($modal.length, 0, "should have no modal opened");
assert.notOk(hasDiscardChanges);
form.destroy();
});
QUnit.test('chatter: discard changes on message post with post_refresh "recipients"', function (assert) {
// After posting a message with mentions, the record will be reloaded,
// as the rpc `message_post` may make changes on some fields of the record.
// If the record is dirty (= has some unsaved changes), we should warn the
// user that these changes will be lost if he proceeds.
assert.expect(2);
var done = assert.async();
var getSuggestionsDef = $.Deferred();
var messages = [];
var bus = new Bus();
var form = createView({
View: FormView,
model: 'partner',
data: this.data,
services: this.services,
arch: '<form string="Partners">' +
'<sheet>' +
'<field name="foo"/>' +
'</sheet>' +
'<div class="oe_chatter">' +
'<field name="message_ids" widget="mail_thread"' +
' options="{\'display_log_button\': True, \'post_refresh\': \'recipients\'}"/>' +
'</div>' +
'</form>',
res_id: 2,
session: {},
mockRPC: function (route, args) {
if (route === "/web/dataset/call_kw/partner/message_get_suggested_recipients") {
return $.when({2: [[42, "Me"]]});
}
if (args.method === 'get_mention_suggestions') {
getSuggestionsDef.resolve();
return $.when([{email: "me@odoo.com", id: 42, name: "Me"}]);
}
if (args.method === 'message_format') {
var requested_msgs = _.filter(messages, function (msg) {
return _.contains(args.args[0], msg.id);
});
return $.when(requested_msgs);
}
if (args.method === 'message_post') {
messages.push({
attachment_ids: [],
author_id: ["42", "Me"],
body: args.kwargs.body,
date: moment().format('YYYY-MM-DD HH:MM:SS'), // now
displayed_author: "Me",
id: 42,
is_note: args.kwargs.subtype === 'mail.mt_note',
is_starred: false,
model: 'partner',
res_id: 2,
});
return $.when(42);
}
return this._super(route, args);
},
viewOptions: {
mode: 'edit',
},
});
// Make record dirty
form.$('.o_form_sheet input').val('trululu').trigger('input');
// create a new message
form.$('.o_chatter_button_new_message').click();
// Add a user as mention
form.$('.oe_chatter .o_composer_text_field:first()').val("@");
var $input = form.$('.oe_chatter .o_composer_text_field:first()');
$input.val('@');
// the cursor position must be set for the mention manager to detect that we are mentionning
$input[0].selectionStart = 1;
$input[0].selectionEnd = 1;
$input.trigger('keyup');
getSuggestionsDef
.then(concurrency.delay.bind(concurrency, 0))
.then(function () {
// click on mention
$input.trigger($.Event('keyup', {which: $.ui.keyCode.ENTER}));
// untick recipient as follower (prompts a res.partner form otherwise)
form.$('.o_checkbox input').prop('checked', false);
// send message
form.$('.oe_chatter .o_composer_button_send').click();
var $modal = $('.modal-dialog');
assert.strictEqual($modal.length, 1, "should have a modal opened");
assert.strictEqual($modal.find('.modal-body').text(),
"The record has been modified, your changes will be discarded. Do you want to proceed?",
"should warn the user that any unsaved changes will be lost");
form.destroy();
done();
});
});
QUnit.test('chatter: discard changes on opening full-composer', function (assert) {
// When we open the full-composer, any following operations by the user
// will reload the record (even closing the full-composer). Therefore,
// we should warn the user when we open the full-composer if the record
// is dirty (= has some unsaved changes).
assert.expect(2);
var form = createView({
View: FormView,
model: 'partner',
data: this.data,
services: this.services,
arch: '<form string="Partners">' +
'<sheet>' +
'<field name="foo"/>' +
'</sheet>' +
'<div class="oe_chatter">' +
'<field name="message_ids" widget="mail_thread"' +
' options="{\'display_log_button\': True,' +
' \'post_refresh\': \'always\'}"/>' +
'</div>' +
'</form>',
res_id: 2,
session: {},
mockRPC: function (route, args) {
if (route === "/web/dataset/call_kw/partner/message_get_suggested_recipients") {
return $.when({2: []});
}
return this._super(route, args);
},
viewOptions: {
mode: 'edit',
},
});
// Make record dirty
form.$('.o_form_sheet input').val('trululu').trigger('input');
// Open full-composer
form.$('.o_chatter_button_new_message').click();
form.$('.o_composer_button_full_composer').click();
var $modal = $('.modal-dialog');
assert.strictEqual($modal.length, 1, "should have a modal opened");
assert.strictEqual($modal.find('.modal-body').text(),
"The record has been modified, your changes will be discarded. Do you want to proceed?",
"should warn the user that any unsaved changes will be lost");
form.destroy();
});
QUnit.test('chatter: Attachment viewer', function (assert) {
assert.expect(6);
this.data.partner.records[0].message_ids = [1];