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