diff --git a/addons/mail/__manifest__.py b/addons/mail/__manifest__.py index 3ded08fdda0..0bfb543d904 100644 --- a/addons/mail/__manifest__.py +++ b/addons/mail/__manifest__.py @@ -75,6 +75,8 @@ 'static/src/components/chatter_container/chatter_container.xml', 'static/src/components/chatter_topbar/chatter_topbar.xml', 'static/src/components/composer/composer.xml', + 'static/src/components/composer_suggested_recipient/composer_suggested_recipient.xml', + 'static/src/components/composer_suggested_recipient_list/composer_suggested_recipient_list.xml', 'static/src/components/composer_text_input/composer_text_input.xml', 'static/src/components/dialog/dialog.xml', 'static/src/components/dialog_manager/dialog_manager.xml', diff --git a/addons/mail/static/src/components/chatter/chatter.xml b/addons/mail/static/src/components/chatter/chatter.xml index 53550da516d..911851106ad 100644 --- a/addons/mail/static/src/components/chatter/chatter.xml +++ b/addons/mail/static/src/components/chatter/chatter.xml @@ -13,6 +13,7 @@ class="o_Chatter_composer" t-att-class="{ 'o-bordered': chatter.hasExternalBorder }" composerLocalId="chatter.thread.composer.localId" + hasFollowers="true" hasMentionSuggestionsBelowPosition="true" isCompact="false" isExpandable="true" diff --git a/addons/mail/static/src/components/composer/composer.js b/addons/mail/static/src/components/composer/composer.js index 510a5ac6038..bc46cd72d1d 100644 --- a/addons/mail/static/src/components/composer/composer.js +++ b/addons/mail/static/src/components/composer/composer.js @@ -3,6 +3,7 @@ odoo.define('mail/static/src/components/composer/composer.js', function (require const components = { AttachmentList: require('mail/static/src/components/attachment_list/attachment_list.js'), + ComposerSuggestedRecipientList: require('mail/static/src/components/composer_suggested_recipient_list/composer_suggested_recipient_list.js'), DropZone: require('mail/static/src/components/drop_zone/drop_zone.js'), EmojisPopover: require('mail/static/src/components/emojis_popover/emojis_popover.js'), FileUploader: require('mail/static/src/components/file_uploader/file_uploader.js'), @@ -190,7 +191,6 @@ class Composer extends Component { } return; } - // TODO: take suggested recipients into account (task-2283356) await this.composer.postMessage(); // TODO: we might need to remove trigger and use the store to wait for the post rpc to be done // task-2252858 diff --git a/addons/mail/static/src/components/composer/composer.scss b/addons/mail/static/src/components/composer/composer.scss index 7ba50371577..37250a2ffd7 100644 --- a/addons/mail/static/src/components/composer/composer.scss +++ b/addons/mail/static/src/components/composer/composer.scss @@ -13,7 +13,7 @@ &.o-has-current-partner-avatar { grid-template-columns: 50px 1fr; - padding: map-get($spacers, 0) map-get($spacers, 3) map-get($spacers, 1) map-get($spacers, 1); + padding: map-get($spacers, 3) map-get($spacers, 3) map-get($spacers, 4) map-get($spacers, 1); &:not(.o-has-footer) { padding-bottom: 20px; diff --git a/addons/mail/static/src/components/composer/composer.xml b/addons/mail/static/src/components/composer/composer.xml index 61694e5aaf1..be947b72b3f 100644 --- a/addons/mail/static/src/components/composer/composer.xml +++ b/addons/mail/static/src/components/composer/composer.xml @@ -46,6 +46,9 @@ + diff --git a/addons/mail/static/src/components/composer_suggested_recipient/composer_suggested_recipient.js b/addons/mail/static/src/components/composer_suggested_recipient/composer_suggested_recipient.js new file mode 100644 index 00000000000..cbc215a3b7e --- /dev/null +++ b/addons/mail/static/src/components/composer_suggested_recipient/composer_suggested_recipient.js @@ -0,0 +1,163 @@ +odoo.define('mail/static/src/components/composer_suggested_recipient/composer_suggested_recipient.js', function (require) { +'use strict'; + +const useStore = require('mail/static/src/component_hooks/use_store/use_store.js'); + +const { FormViewDialog } = require('web.view_dialogs'); +const { ComponentAdapter } = require('web.OwlCompatibility'); + +const { Component } = owl; +const { useRef } = owl.hooks; + +class FormViewDialogComponentAdapter extends ComponentAdapter { + + renderWidget() { + // Ensure the dialog is properly reconstructed. Without this line, it is + // impossible to open the dialog again after having it closed a first + // time, because the DOM of the dialog has disappeared. + return this.willStart(); + } + +} + +const components = { + FormViewDialogComponentAdapter, +}; + +class ComposerSuggestedRecipient extends Component { + + constructor(...args) { + super(...args); + this.id = _.uniqueId('o_ComposerSuggestedRecipient_'); + + useStore(props => { + const suggestedRecipientInfo = this.env.models['mail.suggested_recipient_info'].get(props.suggestedRecipientLocalId); + const partner = suggestedRecipientInfo && suggestedRecipientInfo.partner; + return { + partner: partner && partner.__state, + suggestedRecipientInfo: suggestedRecipientInfo && suggestedRecipientInfo.__state, + }; + }); + /** + * Form view dialog class. Useful to reference it in the template. + */ + this.FormViewDialog = FormViewDialog; + /** + * Reference of the checkbox. Useful to know whether it was checked or + * not, to properly update the corresponding state in the record or to + * prompt the user with the partner creation dialog. + */ + this._checkboxRef = useRef('checkbox'); + /** + * Reference of the partner creation dialog. Useful to open it, for + * compatibility with old code. + */ + this._dialogRef = useRef('dialog'); + /** + * Whether the dialog is currently open. `_dialogRef` cannot be trusted + * to know if the dialog is open due to manually calling `open` and + * potential out of sync with component adapter. + */ + this._isDialogOpen = false; + this._onDialogSaved = this._onDialogSaved.bind(this); + } + + mounted() { + this._update(); + } + + patched() { + this._update(); + } + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + /** + * @returns {string|undefined} + */ + get ADD_AS_RECIPIENT_AND_FOLLOWER_REASON() { + if (!this.suggestedRecipientInfo) { + return undefined; + } + return this.env._t(_.str.sprintf( + "Add as recipient and follower (reason: %s)", + this.suggestedRecipientInfo.reason + )); + } + + /** + * @returns {string} + */ + get PLEASE_COMPLETE_CUSTOMER_S_INFORMATION() { + return this.env._t("Please complete customer's information"); + } + + /** + * @returns {mail.suggested_recipient_info} + */ + get suggestedRecipientInfo() { + return this.env.models['mail.suggested_recipient_info'].get(this.props.suggestedRecipientInfoLocalId); + } + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * @private + */ + _update() { + if (this._checkboxRef.el && this.suggestedRecipientInfo) { + this._checkboxRef.el.checked = this.suggestedRecipientInfo.isSelected; + } + } + + //-------------------------------------------------------------------------- + // Handler + //-------------------------------------------------------------------------- + + /** + * @private + */ + _onChangeCheckbox() { + const isChecked = this._checkboxRef.el.checked; + this.suggestedRecipientInfo.update({ isSelected: isChecked }); + if (!this.suggestedRecipientInfo.partner) { + // Recipients must always be partners. On selecting a suggested + // recipient that does not have a partner, the partner creation form + // should be opened. + if (isChecked && this._dialogRef && !this._isDialogOpen) { + this._isDialogOpen = true; + this._dialogRef.comp.widget.on('closed', this, () => { + this._isDialogOpen = false; + }); + this._dialogRef.comp.widget.open(); + } + } + } + + /** + * @private + */ + _onDialogSaved() { + const thread = this.suggestedRecipientInfo && this.suggestedRecipientInfo.thread; + if (!thread) { + return; + } + thread.fetchAndUpdateSuggestedRecipients(); + } +} + +Object.assign(ComposerSuggestedRecipient, { + components, + props: { + suggestedRecipientInfoLocalId: String, + }, + template: 'mail.ComposerSuggestedRecipient', +}); + +return ComposerSuggestedRecipient; + +}); diff --git a/addons/mail/static/src/components/composer_suggested_recipient/composer_suggested_recipient.scss b/addons/mail/static/src/components/composer_suggested_recipient/composer_suggested_recipient.scss new file mode 100644 index 00000000000..c8b6b5adaba --- /dev/null +++ b/addons/mail/static/src/components/composer_suggested_recipient/composer_suggested_recipient.scss @@ -0,0 +1,5 @@ +// Dirty fix: clear modal-body padding, otherwise it create space inside the +// suggested_recipient list. +.o_ComposerSuggestedRecipient .modal-body { + padding: 0; +} diff --git a/addons/mail/static/src/components/composer_suggested_recipient/composer_suggested_recipient.xml b/addons/mail/static/src/components/composer_suggested_recipient/composer_suggested_recipient.xml new file mode 100644 index 00000000000..ae65b2bb77c --- /dev/null +++ b/addons/mail/static/src/components/composer_suggested_recipient/composer_suggested_recipient.xml @@ -0,0 +1,41 @@ + + + + + + + + + + + + + () + + + + + + + + + + diff --git a/addons/mail/static/src/components/composer_suggested_recipient_list/composer_suggested_recipient_list.js b/addons/mail/static/src/components/composer_suggested_recipient_list/composer_suggested_recipient_list.js new file mode 100644 index 00000000000..330072960bd --- /dev/null +++ b/addons/mail/static/src/components/composer_suggested_recipient_list/composer_suggested_recipient_list.js @@ -0,0 +1,72 @@ +odoo.define('mail/static/src/components/composer_suggested_recipient_list/composer_suggested_recipient_list.js', function (require) { +'use strict'; + +const useStore = require('mail/static/src/component_hooks/use_store/use_store.js'); + +const { Component } = owl; +const { useState } = owl.hooks; + +const components = { + ComposerSuggestedRecipient: require('mail/static/src/components/composer_suggested_recipient/composer_suggested_recipient.js'), +}; + +class ComposerSuggestedRecipientList extends Component { + + /** + * @override + */ + constructor(...args) { + super(...args); + this.state = useState({ + hasShowMoreButton: false, + }); + + useStore(props => { + const thread = this.env.models['mail.thread'].get(props.threadLocalId); + return { + thread: thread ? thread.__state : undefined, + }; + }); + } + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + /** + * @returns {mail.thread} + */ + get thread() { + return this.env.models['mail.thread'].get(this.props.threadLocalId); + } + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * @private + */ + _onClickShowLess(ev) { + this.state.hasShowMoreButton = false; + } + + /** + * @private + */ + _onClickShowMore(ev) { + this.state.hasShowMoreButton = true; + } + +} + +Object.assign(ComposerSuggestedRecipientList, { + components, + props: { + threadLocalId: String, + }, + template: 'mail.ComposerSuggestedRecipientList', +}); + +return ComposerSuggestedRecipientList; +}); diff --git a/addons/mail/static/src/components/composer_suggested_recipient_list/composer_suggested_recipient_list.scss b/addons/mail/static/src/components/composer_suggested_recipient_list/composer_suggested_recipient_list.scss new file mode 100644 index 00000000000..4e1b4dd76ea --- /dev/null +++ b/addons/mail/static/src/components/composer_suggested_recipient_list/composer_suggested_recipient_list.scss @@ -0,0 +1,3 @@ +.o_ComposerSuggestedRecipientList { + margin-bottom: map-get($spacers, 2); +} diff --git a/addons/mail/static/src/components/composer_suggested_recipient_list/composer_suggested_recipient_list.xml b/addons/mail/static/src/components/composer_suggested_recipient_list/composer_suggested_recipient_list.xml new file mode 100644 index 00000000000..c78570dbe24 --- /dev/null +++ b/addons/mail/static/src/components/composer_suggested_recipient_list/composer_suggested_recipient_list.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + + Show more + + + + + Show less + + + + + + + diff --git a/addons/mail/static/src/models/chatter/chatter.js b/addons/mail/static/src/models/chatter/chatter.js index a94731910bc..23e98a956a9 100644 --- a/addons/mail/static/src/models/chatter/chatter.js +++ b/addons/mail/static/src/models/chatter/chatter.js @@ -193,6 +193,7 @@ function factory(dependencies) { ) { if (this.thread) { this.thread.refreshFollowers(); + this.thread.fetchAndUpdateSuggestedRecipients(); } } if ( diff --git a/addons/mail/static/src/models/composer/composer.js b/addons/mail/static/src/models/composer/composer.js index d51f8c37249..230541c77ef 100644 --- a/addons/mail/static/src/models/composer/composer.js +++ b/addons/mail/static/src/models/composer/composer.js @@ -78,6 +78,22 @@ function factory(dependencies) { }); } + /** + * @private + * @returns {mail.partner[]} + */ + _computeRecipients() { + const recipients = [...this.mentionedPartners]; + if (this.thread) { + for (const recipient of this.thread.suggestedRecipientInfoList) { + if (recipient.isSelected) { + recipients.push(recipient.partner); + } + } + } + return [['replace', recipients]]; + } + /** * Open the full composer modal. */ @@ -89,8 +105,7 @@ function factory(dependencies) { default_body: mailUtils.escapeAndCompactTextContent(this.textInputContent), default_is_log: this.isLog, default_model: this.thread.model, - /* FIXME would need to use suggested_partners here task-2280157 */ - // default_partner_ids: partnerIds, + default_partner_ids: this.recipients.map(partner => partner.id), default_res_id: this.thread.id, mail_post_autofollow: true, }; @@ -135,8 +150,11 @@ function factory(dependencies) { let postData = { attachment_ids: this.attachments.map(attachment => attachment.id), body, - partner_ids: this.mentionedPartners.map(partner => partner.id), + context: { + mail_post_autofollow: true, + }, message_type: 'comment', + partner_ids: this.recipients.map(partner => partner.id), }; if (this.subjectContent) { postData.subject = this.subjectContent; @@ -152,7 +170,7 @@ function factory(dependencies) { model: 'mail.channel', method: command ? 'execute_command' : 'message_post', args: [thread.id], - kwargs: postData + kwargs: postData, })); } else { Object.assign(postData, { @@ -162,12 +180,12 @@ function factory(dependencies) { model: thread.model, method: 'message_post', args: [thread.id], - kwargs: postData + kwargs: postData, })); const [messageData] = await this.async(() => this.env.services.rpc({ model: 'mail.message', method: 'message_format', - args: [[messageId]] + args: [[messageId]], })); this.env.models['mail.message'].insert(Object.assign( {}, @@ -184,6 +202,8 @@ function factory(dependencies) { for (const threadView of this.thread.threadViews) { threadView.addComponentHint('current-partner-just-posted-message', { messageId }); } + thread.refreshFollowers(); + thread.fetchAndUpdateSuggestedRecipients(); this._reset(); } @@ -613,6 +633,33 @@ function factory(dependencies) { compute: '_computeMentionedPartners', dependencies: ['textInputContent'], }), + /** + * Determines the extra `mail.partner` (on top of existing followers) + * that will receive the message being composed by `this`, and that will + * also be added as follower of `this.thread`. + */ + recipients: many2many('mail.partner', { + compute: '_computeRecipients', + dependencies: [ + 'mentionedPartners', + 'threadSuggestedRecipientInfoListIsSelected', + // FIXME thread.suggestedRecipientInfoList.partner should be a + // dependency, but it is currently impossible to have a related + // m2o through a m2m. task-2261221 + ] + }), + /** + * Serves as compute dependency. + */ + threadSuggestedRecipientInfoList: many2many('mail.suggested_recipient_info', { + related: 'thread.suggestedRecipientInfoList', + }), + /** + * Serves as compute dependency. + */ + threadSuggestedRecipientInfoListIsSelected: attr({ + related: 'threadSuggestedRecipientInfoList.isSelected', + }), /** * Composer subject input content. */ diff --git a/addons/mail/static/src/models/follower/follower.js b/addons/mail/static/src/models/follower/follower.js index 6bc6c7f579f..183c20e4684 100644 --- a/addons/mail/static/src/models/follower/follower.js +++ b/addons/mail/static/src/models/follower/follower.js @@ -89,7 +89,9 @@ function factory(dependencies) { method: 'message_unsubscribe', args: [[this.followedThread.id], partner_ids, channel_ids] })); + const followedThread = this.followedThread; this.delete(); + followedThread.fetchAndUpdateSuggestedRecipients(); } /** diff --git a/addons/mail/static/src/models/suggested_recipient_info/suggested_recipient_info.js b/addons/mail/static/src/models/suggested_recipient_info/suggested_recipient_info.js new file mode 100644 index 00000000000..c8f128569a6 --- /dev/null +++ b/addons/mail/static/src/models/suggested_recipient_info/suggested_recipient_info.js @@ -0,0 +1,116 @@ +odoo.define('mail/static/src/models/suggested_recipient_info/suggested_recipient_info.js', function (require) { +'use strict'; + +const { registerNewModel } = require('mail/static/src/model/model_core.js'); +const { attr, many2one } = require('mail/static/src/model/model_field.js'); + +function factory(dependencies) { + + class SuggestedRecipientInfo extends dependencies['mail.model'] { + + //---------------------------------------------------------------------- + // private + //---------------------------------------------------------------------- + + /** + * @private + * @returns {string} + */ + _computeEmail() { + return this.partner && this.partner.email || this.email; + } + + /** + * Prevents selecting a recipient that does not have a partner. + * + * @private + * @returns {boolean} + */ + _computeIsSelected() { + return this.partner ? this.isSelected : false; + } + + /** + * @private + * @returns {string} + */ + _computeName() { + return this.partner && this.partner.nameOrDisplayName || this.name; + } + + } + + SuggestedRecipientInfo.fields = { + /** + * Determines the email of `this`. It serves as visual clue when + * displaying `this`, and also serves as default partner email when + * creating a new partner from `this`. + */ + email: attr({ + compute: '_computeEmail', + dependencies: [ + 'email', + 'partnerEmail', + ], + }), + /** + * Determines whether `this` will be added to recipients when posting a + * new message on `this.thread`. + */ + isSelected: attr({ + compute: '_computeIsSelected', + default: true, + dependencies: [ + 'isSelected', + 'partner', + ], + }), + /** + * Determines the name of `this`. It serves as visual clue when + * displaying `this`, and also serves as default partner name when + * creating a new partner from `this`. + */ + name: attr({ + compute: '_computeName', + dependencies: [ + 'name', + 'partnerNameOrDisplayName', + ], + }), + /** + * Determines the optional `mail.partner` associated to `this`. + */ + partner: many2one('mail.partner'), + /** + * Serves as compute dependency. + */ + partnerEmail: attr({ + related: 'partner.email' + }), + /** + * Serves as compute dependency. + */ + partnerNameOrDisplayName: attr({ + related: 'partner.nameOrDisplayName' + }), + /** + * Determines why `this` is a suggestion for `this.thread`. It serves as + * visual clue when displaying `this`. + */ + reason: attr(), + /** + * Determines the `mail.thread` concerned by `this.` + */ + thread: many2one('mail.thread', { + inverse: 'suggestedRecipientInfoList', + }), + }; + + SuggestedRecipientInfo.modelName = 'mail.suggested_recipient_info'; + + return SuggestedRecipientInfo; +} + +registerNewModel('mail.suggested_recipient_info', factory); + +}); diff --git a/addons/mail/static/src/models/thread/thread.js b/addons/mail/static/src/models/thread/thread.js index 75a53f67259..6eaed09ad8b 100644 --- a/addons/mail/static/src/models/thread/thread.js +++ b/addons/mail/static/src/models/thread/thread.js @@ -5,6 +5,7 @@ const { registerNewModel } = require('mail/static/src/model/model_core.js'); const { attr, many2many, many2one, one2many, one2one } = require('mail/static/src/model/model_field.js'); const throttle = require('mail/static/src/utils/throttle/throttle.js'); const Timer = require('mail/static/src/utils/timer/timer.js'); +const mailUtils = require('mail.utils'); function factory(dependencies) { @@ -418,6 +419,41 @@ function factory(dependencies) { ); } + /** + * Performs RPC on the route `/mail/get_suggested_recipients`. + * + * @static + * @param {Object} param0 + * @param {string} param0.model + * @param {integer[]} param0.res_id + */ + static async performRpcMailGetSuggestedRecipients({ model, res_ids }) { + const data = await this.env.services.rpc({ + route: '/mail/get_suggested_recipients', + params: { + model, + res_ids, + }, + }); + for (const id in data) { + const recipientInfoList = data[id].map(recipientInfoData => { + const [partner_id, emailInfo, reason] = recipientInfoData; + const [name, email] = emailInfo && mailUtils.parseEmail(emailInfo); + return { + email, + name, + partner: [partner_id ? ['insert', { id: partner_id }] : ['unlink']], + reason, + }; + }); + this.insert({ + id: parseInt(id), + model, + suggestedRecipientInfoList: [['insert-and-replace', recipientInfoList]], + }); + } + } + /** * @param {string} [stringifiedDomain='[]'] * @returns {mail.thread_cache} @@ -458,6 +494,19 @@ function factory(dependencies) { this.update({ areAttachmentsLoaded: true }); } + /** + * Fetches suggested recipients. + */ + async fetchAndUpdateSuggestedRecipients() { + if (this.isTemporary) { + return; + } + return this.env.models['mail.thread'].performRpcMailGetSuggestedRecipients({ + model: this.model, + res_ids: [this.id], + }); + } + /** * Add current user to provided thread's followers. */ @@ -472,6 +521,7 @@ function factory(dependencies) { }, })); this.refreshFollowers(); + this.fetchAndUpdateSuggestedRecipients(); } /** @@ -1593,6 +1643,12 @@ function factory(dependencies) { serverMessageUnreadCounter: attr({ default: 0, }), + /** + * Determines the `mail.suggested_recipient_info` concerning `this`. + */ + suggestedRecipientInfoList: one2many('mail.suggested_recipient_info', { + inverse: 'thread', + }), /** * Members that are currently typing something in the composer of this * thread, including current partner. diff --git a/addons/mail/static/tests/helpers/mock_server.js b/addons/mail/static/tests/helpers/mock_server.js index a58a685040c..28618fcd6cd 100644 --- a/addons/mail/static/tests/helpers/mock_server.js +++ b/addons/mail/static/tests/helpers/mock_server.js @@ -105,6 +105,11 @@ MockServer.include({ const context = args.context; return this._mockRouteMailChatPost(uuid, message_content, context); } + if (route === '/mail/get_suggested_recipients') { + const model = args.model; + const res_ids = args.res_ids; + return this._mockRouteMailGetSuggestedRecipient(model, res_ids); + } if (route === '/mail/init_messaging') { return this._mockRouteMailInitMessaging(); } @@ -186,7 +191,8 @@ MockServer.include({ if (args.model === 'mail.channel' && args.method === 'message_post') { const id = args.args[0]; const kwargs = args.kwargs; - const context = args.context; + const context = kwargs.context; + delete kwargs.context; return this._mockMailChannelMessagePost(id, kwargs, context); } if (args.model === 'mail.channel' && args.method === 'notify_typing') { @@ -252,7 +258,8 @@ MockServer.include({ if (args.method === 'message_post') { const id = args.args[0]; const kwargs = args.kwargs; - const context = args.kwargs.context; + const context = kwargs.context; + delete kwargs.context; return this._mockMailThreadMessagePost(args.model, [id], kwargs, context); } return this._super(route, args); @@ -311,6 +318,17 @@ MockServer.include({ context ); }, + /** + * Simulates `/mail/get_suggested_recipients` route. + * + * @private + * @returns {string} model + * @returns {integer[]} res_ids + * @returns {Object} + */ + _mockRouteMailGetSuggestedRecipient(model, res_ids) { + return this._mockMailThread_MessageGetSuggestedRecipients(model, res_ids); + }, /** * Simulates the `/mail/init_messaging` route. * @@ -1345,6 +1363,47 @@ MockServer.include({ return [author_id, email_from]; }, + /** + * Simulates `_message_add_suggested_recipient` on `mail.thread`. + * + * @private + * @param {string} model + * @param {integer[]} ids + * @param {Object} result + * @param {Object} [param3={}] + * @param {string} [param3.email] + * @param {integer} [param3.partner] + * @param {string} [param3.reason] + * @returns {Object} + */ + _mockMailThread_MessageAddSuggestedRecipient(model, ids, result, { email, partner, reason = '' } = {}) { + const record = this._getRecords(model, [['id', 'in', 'ids']])[0]; + // for simplicity + result[record.id].push([partner, email, reason]); + return result; + }, + /** + * Simulates `_message_get_suggested_recipients` on `mail.thread`. + * + * @private + * @param {string} model + * @param {integer[]} ids + * @returns {Object} + */ + _mockMailThread_MessageGetSuggestedRecipients(model, ids) { + const result = ids.reduce((result, id) => result[id] = [], {}); + const records = this._getRecords(model, [['id', 'in', ids]]); + for (const record in records) { + if (record.user_id) { + const user = this._getRecords('res.users', [['id', '=', record.user_id]]); + if (user.partner_id) { + const reason = this.data[model].fields['user_id'].string; + this._mockMailThread_MessageAddSuggestedRecipient(result, user.partner_id, reason); + } + } + } + return result; + }, /** * Simulates `message_post` on `mail.thread`. * diff --git a/addons/mail/views/assets.xml b/addons/mail/views/assets.xml index b560d870313..e1744fe11e7 100644 --- a/addons/mail/views/assets.xml +++ b/addons/mail/views/assets.xml @@ -75,6 +75,8 @@ + + @@ -147,6 +149,7 @@ + @@ -183,6 +186,8 @@ + +