[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:
@@ -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) {
|
||||
|
||||
@@ -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()),
|
||||
|
||||
@@ -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];
|
||||
|
||||
Reference in New Issue
Block a user