[MERGE][FIX] mail: several fixes in discuss, chat window, chatter

This merge fixes a crash in discuss, in addition to re-introduce some _quality of life_
features that have been mistakenly removed from the chatter and from chat windows:

### 1. No crash when searching messages

This was caused by the addition of moderated channels. In short, fetching twice
the same message was performing a re-render of the thread due to a mis-consideration
of a moderation status change. When the fetch reaches the fetch limit, the re-render is
considered as a "load more" fetch, which only works when there is at least one message
registered for the current search.

This is fixed by re-render when there is a change of moderation status only.

Task-ID 1910180

### 2. Easily mention followers and employees in chatter

When mentioning a user from the chatter in a document, it was always
fetching all partners in a arbitrary order. As a result, it was hard to mention
followers and employees.

This is fixed by re-introducing the previous behaviour on mentions, which
groups them by followers, then employees, then all partners.

Task-ID 1910111

### 3. Show message document link in chat window

When a message is linked to a document, it usually shows a
link next to the message. This still works in the Discuss app,
but not in chat windows.

This erroneous regression was caused by a refactoring.
This is fixed by re-introduced document links on messages in chat
windows.

Task-ID 1910119

closes odoo/odoo#28744
This commit is contained in:
Robot Odoo
2018-11-16 17:53:55 +01:00
committed by GitHub
10 changed files with 302 additions and 23 deletions
@@ -211,7 +211,8 @@ var BasicComposer = Widget.extend({
* displayed to the user. If none of them match, then it will fetch for more
* partner suggestions (@see _mentionFetchPartners).
*
* @param {$.Deferred<Object[]>} prefetchedPartners
* @param {$.Deferred<Object[]>} prefetchedPartners list of list of
* prefetched partners.
*/
mentionSetPrefetchedPartners: function (prefetchedPartners) {
this._mentionPrefetchedPartners = prefetchedPartners;
@@ -302,21 +303,23 @@ var BasicComposer = Widget.extend({
*/
_mentionFetchPartners: function (search) {
var self = this;
return $.when(this._mentionPrefetchedPartners).then(function (partners) {
return $.when(this._mentionPrefetchedPartners).then(function (prefetchedPartners) {
// filter prefetched partners with the given search string
var suggestions = [];
var limit = self.options.mentionFetchLimit;
var searchRegexp = new RegExp(_.str.escapeRegExp(mailUtils.unaccent(search)), 'i');
if (limit > 0) {
var filteredPartners = _.filter(partners, function (partner) {
return partner.email && searchRegexp.test(partner.email) ||
partner.name && searchRegexp.test(mailUtils.unaccent(partner.name));
});
if (filteredPartners.length) {
suggestions.push(filteredPartners.slice(0, limit));
limit -= filteredPartners.length;
_.each(prefetchedPartners, function (partners) {
if (limit > 0) {
var filteredPartners = _.filter(partners, function (partner) {
return partner.email && searchRegexp.test(partner.email) ||
partner.name && searchRegexp.test(mailUtils.unaccent(partner.name));
});
if (filteredPartners.length) {
suggestions.push(filteredPartners.slice(0, limit));
limit -= filteredPartners.length;
}
}
}
});
if (!suggestions.length && !self.options.mentionPartnersRestricted) {
// no result found among prefetched partners, fetch other suggestions
suggestions = self._mentionFetchThrottled(
@@ -458,6 +458,9 @@ var Message = AbstractMessage.extend(Mixins.EventDispatcherMixin, ServicesMixin
*/
setModerationStatus: function (newModerationStatus, options) {
var self = this;
if (newModerationStatus === this._moderationStatus) {
return;
}
this._moderationStatus = newModerationStatus;
if (newModerationStatus === 'accepted' && options) {
_.each(options.additionalThreadIDs, function (threadID) {
@@ -179,7 +179,8 @@ var Channel = SearchableThread.extend(ThreadTypingMixin, {
/**
* Get listeners of a channel
*
* @returns {$.Promise<Object[]>} resolved with list of channel listeners
* @returns {$.Promise<Array<Object[]>>} resolved with list of list of
* channel listeners.
*/
getMentionPartnerSuggestions: function () {
var self = this;
@@ -193,7 +194,7 @@ var Channel = SearchableThread.extend(ThreadTypingMixin, {
})
.then(function (members) {
self._members = members;
return members;
return [members];
});
}
return this._membersDef;
@@ -37,7 +37,8 @@ var Livechat = TwoUserChannel.extend({
* display of a user that is typing.
*
* @override
* @returns {$.Promise<Object[]>} resolved with list of livechat members
* @returns {$.Promise<Array<Object[]>>} resolved with list of list of
* livechat members.
*/
getMentionPartnerSuggestions: function () {
var self = this;
@@ -49,7 +50,7 @@ var Livechat = TwoUserChannel.extend({
name: self._WEBSITE_USER_NAME,
});
}
return self._members;
return [self._members];
});
},
/**
@@ -15,7 +15,8 @@ var ServicesMixin = require('web.ServicesMixin');
* In particular, channels and mailboxes are two different kinds of threads.
*/
var Thread = AbstractThread.extend(ServicesMixin, {
// max number of fetched messages from the server
_FETCH_LIMIT: 30,
/**
* @override
* @param {Object} params
@@ -33,8 +34,6 @@ var Thread = AbstractThread.extend(ServicesMixin, {
// means that there is no message in this channel.
this._previewed = false;
this._type = params.data.type || params.data.channel_type;
// max number of fetched messages from the server
this._FETCH_LIMIT = 30;
},
//--------------------------------------------------------------------------
@@ -111,7 +110,7 @@ var Thread = AbstractThread.extend(ServicesMixin, {
* By default, a thread has not listener.
*
* @abstract
* @returns {$.Promise<Object[]>}
* @returns {$.Promise<Array<Object[]>>}
*/
getMentionPartnerSuggestions: function () {
return $.when([]);
@@ -199,7 +199,7 @@ var MailManager = AbstractService.extend({
* Get partners as mentions from a chatter
* Typically all employees as partner suggestions.
*
* @returns {Array<Object[]>}
* @returns {Array<Array<Object[]>>}
*/
getMentionPartnerSuggestions: function () {
return this._mentionPartnerSuggestions;
@@ -1195,8 +1195,8 @@ var MailManager = AbstractService.extend({
*
* @private
* @param {Object} result data from server on mail/init_messaging rpc
* @param {Object[]} result.mention_partner_suggestions list of suggestions
* with all the employees
* @param {Array<Object[]>} result.mention_partner_suggestions list of
* suggestions.
* @param {integer} result.menu_id the menu ID of discuss app
*/
_updateInternalStateFromServer: function (result) {
@@ -79,7 +79,6 @@ var AbstractThreadWindow = Widget.extend({
this.$header = this.$('.o_thread_window_header');
this._threadWidget = new ThreadWidget(this, {
displayDocumentLinks: false,
displayMarkAsRead: false,
displayStars: this.options.displayStars,
});
+118
View File
@@ -2450,6 +2450,124 @@ QUnit.test('chatter: suggested partner auto-follow on message post', function (a
form.destroy();
});
QUnit.test('chatter: mention prefetched partners (followers & employees)', function (assert) {
// Note: employees are in prefeteched partner for mentions in chatter when
// the module hr is installed.
assert.expect(10);
var followerSuggestions = [{
id: 1,
name: 'FollowerUser1',
email: 'follower-user1@example.com',
}, {
id: 2,
name: 'FollowerUser2',
email: 'follower-user2@example.com',
}];
var nonFollowerSuggestions = [{
id: 3,
name: 'NonFollowerUser1',
email: 'non-follower-user1@example.com',
}, {
id: 4,
name: 'NonFollowerUser2',
email: 'non-follower-user2@example.com',
}];
// link followers
this.data.partner.records[0].message_follower_ids = [10, 20];
// prefetched partners
this.data.initMessaging = {
mention_partner_suggestions: [followerSuggestions.concat(nonFollowerSuggestions)],
};
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_follower_ids" widget="mail_followers"/>' +
'<field name="message_ids" widget="mail_thread" options="{\'display_log_button\': True}"/>' +
'</div>' +
'</form>',
res_id: 2,
mockRPC: function (route, args) {
if (route === '/mail/read_followers') {
return $.when({
followers: [{
id: 10,
name: 'FollowerUser1',
email: 'follower-user1@example.com',
res_model: 'res.partner',
res_id: 1,
}, {
id: 20,
name: 'FollowerUser2',
email: 'follower-user2@example.com',
res_model: 'res.partner',
res_id: 2,
}],
subtypes: [],
});
}
if (args.method === 'message_get_suggested_recipients') {
return $.when({2: []});
}
if (args.method === 'get_mention_suggestions') {
throw new Error('should not fetch partners for mentions');
}
return this._super(route, args);
},
session: {},
});
assert.strictEqual(form.$('.o_followers_count').text(), '2',
"should have two followers of this document");
assert.strictEqual(form.$('.o_followers_list > .o_partner').text().replace(/\s+/g, ''),
'FollowerUser1FollowerUser2',
"should have correct follower names");
assert.strictEqual(form.$('.o_composer_mention_dropdown').length, 0,
"should not show the mention suggestion dropdown");
form.$('.o_chatter_button_new_message').click();
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');
assert.strictEqual(form.$('.o_composer_mention_dropdown').length, 1,
"should show the mention suggestion dropdown");
assert.strictEqual(form.$('.o_mention_proposition').length, 4,
"should show 4 mention suggestions");
assert.strictEqual(form.$('.o_mention_proposition').eq(0).text().replace(/\s+/g, ''),
"FollowerUser1(follower-user1@example.com)",
"should display correct 1st mention suggestion");
assert.strictEqual(form.$('.o_mention_proposition').eq(1).text().replace(/\s+/g, ''),
"FollowerUser2(follower-user2@example.com)",
"should display correct 2nd mention suggestion");
assert.ok(form.$('.o_mention_proposition').eq(1).next().hasClass('dropdown-divider'),
"should have a mention separator after last follower mention suggestion");
assert.strictEqual(form.$('.o_mention_proposition').eq(2).text().replace(/\s+/g, ''),
"NonFollowerUser1(non-follower-user1@example.com)",
"should display correct 3rd mention suggestion");
assert.strictEqual(form.$('.o_mention_proposition').eq(3).text().replace(/\s+/g, ''),
"NonFollowerUser2(non-follower-user2@example.com)",
"should display correct 4th mention suggestion");
//cleanup
form.destroy();
});
QUnit.module('FieldMany2ManyTagsEmail', {
beforeEach: function () {
this.data = {
@@ -1,6 +1,7 @@
odoo.define('mail.discuss_moderation_tests', function (require) {
"use strict";
var Thread = require('mail.model.Thread');
var mailTestUtils = require('mail.testUtils');
var createDiscuss = mailTestUtils.createDiscuss;
@@ -760,5 +761,84 @@ QUnit.test('author: sent message rejected in moderated channel', function (asser
});
});
QUnit.test('no crash when load-more fetching "accepted" message twice', function (assert) {
// This tests requires discuss not loading more messages due to having less
// messages to fetch than available height. This justifies we simply do not
// patch FETCH_LIMIT to 1, as it would detect that more messages could fit
// the empty space (it behaviour is linked to "auto load more").
var done = assert.async();
assert.expect(2);
var FETCH_LIMIT = Thread.prototype._FETCH_LIMIT;
// FETCH LIMIT + 30 should be enough to cover the whole available space in
// the thread of discuss app.
var messageData = [];
_.each(_.range(1, FETCH_LIMIT+31), function (num) {
messageData.push({
id: num,
body: "<p>test" + num + "</p>",
author_id: [100, "Someone"],
channel_ids: [1],
model: 'mail.channel',
res_id: 1,
moderation_status: 'accepted',
}
);
});
this.data['mail.message'].records = messageData;
this.data.initMessaging = {
channel_slots: {
channel_channel: [{
id: 1,
channel_type: "channel",
name: "general",
}],
},
};
var count = 0;
createDiscuss({
id: 1,
context: {},
params: {},
data: this.data,
services: this.services,
session: { partner_id: 3 },
mockRPC: function (route, args) {
if (args.method === 'message_fetch') {
count++;
if (count === 1) {
// inbox message_fetch
return $.when([]);
}
// general message_fetch
return $.when(messageData);
}
return this._super.apply(this, arguments);
},
})
.then(function (discuss) {
var $general = discuss.$('.o_mail_discuss_sidebar')
.find('.o_mail_discuss_item[data-thread-id=1]');
assert.strictEqual($general.length, 1,
"should have the channel item with id 1");
assert.strictEqual($general.attr('title'), 'general',
"should have the title 'general'");
// click on general
$general.click();
// simulate search
discuss.trigger_up('search', {
domains: [['author_id', '=', 100]],
});
discuss.destroy();
done();
});
});
});
});
@@ -353,6 +353,81 @@ QUnit.test('do not mark as read the newly open thread window from received messa
parent.destroy();
});
QUnit.test('show document link of message linked to a document', function (assert) {
assert.expect(6);
this.data['mail.channel'] = {
fields: {
name: {
string: "Name",
type: "char",
required: true,
},
channel_type: {
string: "Channel Type",
type: "selection",
},
channel_message_ids: {
string: "Messages",
type: "many2many",
relation: 'mail.message'
},
message_unread_counter: {
string: "Amount of Unread Messages",
type: "integer"
},
},
records: [{
id: 2,
name: "R&D Tasks",
channel_type: "channel",
}],
};
this.data['mail.message'].records.push({
author_id: [5, "Someone else"],
body: "<p>Test message</p>",
id: 40,
model: 'some.document',
record_name: 'Some Document',
res_id: 10,
channel_ids: [2],
});
this.data.initMessaging.channel_slots.channel_channel.push({
id: 2,
name: "R&D Tasks",
channel_type: "public",
});
var parent = this.createParent({
data: this.data,
services: this.services,
session: { partner_id: 3 },
});
assert.strictEqual($('.o_thread_window').length, 0,
"no thread window should be open initially");
// get channel instance to link to thread window
var channel = parent.call('mail_service', 'getChannel', 2);
channel.detach();
var $threadWindow = $('.o_thread_window');
assert.strictEqual($threadWindow.length, 1,
"a thread window should be open");
assert.strictEqual($threadWindow.find('.o_thread_window_title').text().trim(),
"#R&D Tasks",
"should be thread window of correct channel");
assert.strictEqual($threadWindow.find('.o_thread_message').length, 1,
"should contain a single message in thread window");
assert.ok($threadWindow.find('.o_mail_info').text().replace(/\s/g, "").indexOf('Someoneelse') !== -1,
"message should be from 'Someone else' user");
assert.ok($threadWindow.find('.o_mail_info').text().replace(/\s/g, "").indexOf('onSomeDocument') !== -1,
"message should link to 'Some Document'");
parent.destroy();
});
});
});
});