summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDidier (did) <did@odoo.com>2020-09-21 15:44:21 +0000
committerSébastien Theys <seb@odoo.com>2020-09-21 16:49:47 +0000
commitde1f701e7bd7ebb6f7ebea5c097d980039c4f904 (patch)
treed4b931794a6200ebce8809ff73400d65dc391e56
parentaaec45b9aece17f8c764cd7a89eb243a5fc11924 (diff)
[FIX] mail: restore the "to followers" feature
Before this commit, the "to followers" message inside the chatter composer was missing. Fix also some padding issue. This also restore the form dialog when you try to send a message to a suggested recipient that doesn't have a partner (someone in CC email for eg). But now, suggested recipient without partner are unchecked by default and the form open when the user try to check them. If the user discard the form dialog, the suggested recipient isn't checked. This is better since only a partner will receive a notification. task-2280157 closes odoo/odoo#58160 X-original-commit: 67d151cc36871d87b92676357a31a70de5a92fce Signed-off-by: Sébastien Theys (seb) <seb@odoo.com>
-rw-r--r--addons/mail/__manifest__.py2
-rw-r--r--addons/mail/static/src/components/chatter/chatter.xml1
-rw-r--r--addons/mail/static/src/components/composer/composer.js2
-rw-r--r--addons/mail/static/src/components/composer/composer.scss2
-rw-r--r--addons/mail/static/src/components/composer/composer.xml3
-rw-r--r--addons/mail/static/src/components/composer_suggested_recipient/composer_suggested_recipient.js163
-rw-r--r--addons/mail/static/src/components/composer_suggested_recipient/composer_suggested_recipient.scss5
-rw-r--r--addons/mail/static/src/components/composer_suggested_recipient/composer_suggested_recipient.xml41
-rw-r--r--addons/mail/static/src/components/composer_suggested_recipient_list/composer_suggested_recipient_list.js72
-rw-r--r--addons/mail/static/src/components/composer_suggested_recipient_list/composer_suggested_recipient_list.scss3
-rw-r--r--addons/mail/static/src/components/composer_suggested_recipient_list/composer_suggested_recipient_list.xml26
-rw-r--r--addons/mail/static/src/models/chatter/chatter.js1
-rw-r--r--addons/mail/static/src/models/composer/composer.js59
-rw-r--r--addons/mail/static/src/models/follower/follower.js2
-rw-r--r--addons/mail/static/src/models/suggested_recipient_info/suggested_recipient_info.js116
-rw-r--r--addons/mail/static/src/models/thread/thread.js56
-rw-r--r--addons/mail/static/tests/helpers/mock_server.js63
-rw-r--r--addons/mail/views/assets.xml5
18 files changed, 612 insertions, 10 deletions
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 @@
</t>
</b>
</small>
+ <ComposerSuggestedRecipientList
+ threadLocalId="composer.thread.localId"
+ />
</t>
</div>
</t>
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 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<templates xml:space="preserve">
+ <t t-name="mail.ComposerSuggestedRecipient" owl="1">
+ <div class="o_ComposerSuggestedRecipient" t-att-title="ADD_AS_RECIPIENT_AND_FOLLOWER_REASON">
+ <t t-if="suggestedRecipientInfo">
+ <div class="custom-control custom-checkbox">
+ <input t-attf-id="{{ id }}_checkbox" class="custom-control-input" type="checkbox" t-att-checked="suggestedRecipientInfo.isSelected ? 'checked' : undefined" t-on-change="_onChangeCheckbox" t-ref="checkbox" />
+ <label class="custom-control-label" t-attf-for="{{ id }}_checkbox">
+ <t t-if="suggestedRecipientInfo.name">
+ <t t-esc="suggestedRecipientInfo.name"/>
+ </t>
+ <t t-if="suggestedRecipientInfo.email">
+ (<t t-esc="suggestedRecipientInfo.email"/>)
+ </t>
+ </label>
+ </div>
+ <t t-if="!suggestedRecipientInfo.partner">
+ <FormViewDialogComponentAdapter
+ Component="FormViewDialog"
+ params="{
+ context: {
+ active_id: suggestedRecipientInfo.thread.id,
+ active_model: 'mail.compose.message',
+ default_email: suggestedRecipientInfo.email,
+ default_name: suggestedRecipientInfo.name,
+ force_email: true,
+ ref: 'compound_context',
+ },
+ disable_multiple_selection: true,
+ on_saved: _onDialogSaved,
+ res_id: false,
+ res_model: 'res.partner',
+ title: PLEASE_COMPLETE_CUSTOMER_S_INFORMATION,
+ }"
+ t-ref="dialog"
+ />
+ </t>
+ </t>
+ </div>
+ </t>
+</templates>
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 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<templates xml:space="preserve">
+ <t t-name="mail.ComposerSuggestedRecipientList" owl="1">
+ <div class="o_ComposerSuggestedRecipientList">
+ <t t-if="thread">
+ <t t-foreach="state.hasShowMoreButton ? thread.suggestedRecipientInfoList : thread.suggestedRecipientInfoList.slice(0,3)" t-as="recipientInfo" t-key="recipientInfo.localId">
+ <ComposerSuggestedRecipient
+ suggestedRecipientInfoLocalId="recipientInfo.localId"
+ />
+ </t>
+ <t t-if="thread.suggestedRecipientInfoList.length > 3">
+ <t t-if="!state.hasShowMoreButton" >
+ <button class="o_ComposerSuggestedRecipientList_showMore btn btn-sm btn-link" t-on-click="_onClickShowMore">
+ Show more
+ </button>
+ </t>
+ <t t-else="">
+ <button class="o_ComposerSuggestedRecipientList_showLess btn btn-sm btn-link" t-on-click="_onClickShowLess">
+ Show less
+ </button>
+ </t>
+ </t>
+ </t>
+ </div>
+ </t>
+</templates>
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
@@ -79,6 +79,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.
*/
async openFullComposer() {
@@ -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();
}
@@ -614,6 +634,33 @@ function factory(dependencies) {
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.
*/
subjectContent: attr({
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) {
@@ -419,6 +420,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}
*/
@@ -459,6 +495,19 @@ function factory(dependencies) {
}
/**
+ * 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.
*/
async follow() {
@@ -472,6 +521,7 @@ function factory(dependencies) {
},
}));
this.refreshFollowers();
+ this.fetchAndUpdateSuggestedRecipients();
}
/**
@@ -1594,6 +1644,12 @@ function factory(dependencies) {
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);
@@ -312,6 +319,17 @@ MockServer.include({
);
},
/**
+ * 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.
*
* @private
@@ -1346,6 +1364,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`.
*
* @private
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 @@
<script type="text/javascript" src="/mail/static/src/components/chatter_container/chatter_container.js"></script>
<script type="text/javascript" src="/mail/static/src/components/chatter_topbar/chatter_topbar.js"></script>
<script type="text/javascript" src="/mail/static/src/components/composer/composer.js"></script>
+ <script type="text/javascript" src="/mail/static/src/components/composer_suggested_recipient/composer_suggested_recipient.js"/>
+ <script type="text/javascript" src="/mail/static/src/components/composer_suggested_recipient_list/composer_suggested_recipient_list.js"/>
<script type="text/javascript" src="/mail/static/src/components/composer_text_input/composer_text_input.js"></script>
<script type="text/javascript" src="/mail/static/src/components/dialog/dialog.js"></script>
<script type="text/javascript" src="/mail/static/src/components/dialog_manager/dialog_manager.js"></script>
@@ -147,6 +149,7 @@
<script type="text/javascript" src="/mail/static/src/models/notification_group/notification_group.js"></script>
<script type="text/javascript" src="/mail/static/src/models/notification_group_manager/notification_group_manager.js"></script>
<script type="text/javascript" src="/mail/static/src/models/partner/partner.js"></script>
+ <script type="text/javascript" src="/mail/static/src/models/suggested_recipient_info/suggested_recipient_info.js"/>
<script type="text/javascript" src="/mail/static/src/models/thread/thread.js"></script>
<script type="text/javascript" src="/mail/static/src/models/thread_cache/thread_cache.js"></script>
<script type="text/javascript" src="/mail/static/src/models/thread_partner_seen_info/thread_partner_seen_info.js"></script>
@@ -183,6 +186,8 @@
<link rel="stylesheet" type="text/scss" href="/mail/static/src/components/chatter_container/chatter_container.scss"/>
<link rel="stylesheet" type="text/scss" href="/mail/static/src/components/chatter_topbar/chatter_topbar.scss"/>
<link rel="stylesheet" type="text/scss" href="/mail/static/src/components/composer/composer.scss"/>
+ <link rel="stylesheet" type="text/scss" href="/mail/static/src/components/composer_suggested_recipient/composer_suggested_recipient.scss"/>
+ <link rel="stylesheet" type="text/scss" href="/mail/static/src/components/composer_suggested_recipient_list/composer_suggested_recipient_list.scss"/>
<link rel="stylesheet" type="text/scss" href="/mail/static/src/components/composer_text_input/composer_text_input.scss"/>
<link rel="stylesheet" type="text/scss" href="/mail/static/src/components/dialog/dialog.scss"/>
<link rel="stylesheet" type="text/scss" href="/mail/static/src/components/discuss/discuss.scss"/>