summaryrefslogtreecommitdiff
path: root/addons/mail/static/src/components
diff options
context:
space:
mode:
authorJulien Giannone <jgi@odoo.com>2020-09-21 18:58:55 +0000
committerSébastien Theys <seb@odoo.com>2020-09-21 20:29:32 +0000
commit8bb3b09a2abf27011e4920a9dc045fc27e1ec628 (patch)
treede8b38bf7f933f28ae2f9d9f593d885bf8b0c676 /addons/mail/static/src/components
parentd40fd8e3e29d1954f60ebbd64a319ae564b7a639 (diff)
[IMP] mail: reintroduce channel mentions, canned responses, commands
task-2280308 closes odoo/odoo#58166 X-original-commit: b63d0d2ee5f3bff24a5e1e1cd962d59bebef082d Related: odoo/enterprise#13392 Signed-off-by: Sébastien Theys (seb) <seb@odoo.com>
Diffstat (limited to 'addons/mail/static/src/components')
-rw-r--r--addons/mail/static/src/components/chat_window/chat_window.js2
-rw-r--r--addons/mail/static/src/components/chat_window_manager/chat_window_manager_tests.js13
-rw-r--r--addons/mail/static/src/components/chatter/chatter_tests.js4
-rw-r--r--addons/mail/static/src/components/composer/composer.js9
-rw-r--r--addons/mail/static/src/components/composer/composer.xml1
-rw-r--r--addons/mail/static/src/components/composer/composer_tests.js668
-rw-r--r--addons/mail/static/src/components/composer_suggestion/composer_suggestion.js101
-rw-r--r--addons/mail/static/src/components/composer_suggestion/composer_suggestion.scss (renamed from addons/mail/static/src/components/partner_mention_suggestion/partner_mention_suggestion.scss)14
-rw-r--r--addons/mail/static/src/components/composer_suggestion/composer_suggestion.xml32
-rw-r--r--addons/mail/static/src/components/composer_suggestion/composer_suggestion_canned_response_tests.js154
-rw-r--r--addons/mail/static/src/components/composer_suggestion/composer_suggestion_channel_tests.js141
-rw-r--r--addons/mail/static/src/components/composer_suggestion/composer_suggestion_command_tests.js151
-rw-r--r--addons/mail/static/src/components/composer_suggestion/composer_suggestion_partner_tests.js (renamed from addons/mail/static/src/components/partner_mention_suggestion/partner_mention_suggestion_tests.js)67
-rw-r--r--addons/mail/static/src/components/composer_suggestion_list/composer_suggestion_list.js74
-rw-r--r--addons/mail/static/src/components/composer_suggestion_list/composer_suggestion_list.scss11
-rw-r--r--addons/mail/static/src/components/composer_suggestion_list/composer_suggestion_list.xml32
-rw-r--r--addons/mail/static/src/components/composer_text_input/composer_text_input.js66
-rw-r--r--addons/mail/static/src/components/composer_text_input/composer_text_input.xml35
-rw-r--r--addons/mail/static/src/components/discuss/tests/discuss_inbox_tests.js15
-rw-r--r--addons/mail/static/src/components/message_list/message_list.js4
-rw-r--r--addons/mail/static/src/components/partner_mention_suggestion/partner_mention_suggestion.js79
-rw-r--r--addons/mail/static/src/components/partner_mention_suggestion/partner_mention_suggestion.xml19
22 files changed, 1447 insertions, 245 deletions
diff --git a/addons/mail/static/src/components/chat_window/chat_window.js b/addons/mail/static/src/components/chat_window/chat_window.js
index a5d0b2c9a20..4480be9db11 100644
--- a/addons/mail/static/src/components/chat_window/chat_window.js
+++ b/addons/mail/static/src/components/chat_window/chat_window.js
@@ -313,7 +313,7 @@ class ChatWindow extends Component {
}
break;
case 'Escape':
- if (isEventHandled(ev, 'ComposerTextInput.closeMentionSuggestions')) {
+ if (isEventHandled(ev, 'ComposerTextInput.closeSuggestions')) {
break;
}
if (isEventHandled(ev, 'Composer.closeEmojisPopover')) {
diff --git a/addons/mail/static/src/components/chat_window_manager/chat_window_manager_tests.js b/addons/mail/static/src/components/chat_window_manager/chat_window_manager_tests.js
index e2d049d6f87..0401cef3809 100644
--- a/addons/mail/static/src/components/chat_window_manager/chat_window_manager_tests.js
+++ b/addons/mail/static/src/components/chat_window_manager/chat_window_manager_tests.js
@@ -653,16 +653,17 @@ QUnit.test('chat window: close on ESCAPE', async function (assert) {
);
await afterNextRender(() => {
- document.execCommand('insertText', false, "@test");
+ document.querySelector(`.o_ComposerTextInput_textarea`).focus();
+ document.execCommand('insertText', false, "@");
document.querySelector(`.o_ComposerTextInput_textarea`)
.dispatchEvent(new window.KeyboardEvent('keydown'));
document.querySelector(`.o_ComposerTextInput_textarea`)
.dispatchEvent(new window.KeyboardEvent('keyup'));
});
- assert.containsOnce(
- document.body,
- '.o_ComposerTextInput_mentionDropdownPropositionList',
- "mention suggestion should be opened after typing @test"
+ assert.hasClass(
+ document.querySelector('.o_ComposerSuggestionList_list'),
+ 'show',
+ "should display mention suggestions on typing '@'"
);
await afterNextRender(() => {
@@ -671,7 +672,7 @@ QUnit.test('chat window: close on ESCAPE', async function (assert) {
});
assert.containsNone(
document.body,
- '.o_ComposerTextInput_mentionDropdownPropositionList',
+ '.o_ComposerSuggestionList_list',
"mention suggestion should be closed after pressing escape on mention suggestion"
);
assert.containsOnce(
diff --git a/addons/mail/static/src/components/chatter/chatter_tests.js b/addons/mail/static/src/components/chatter/chatter_tests.js
index e44ccd81115..348291c14cd 100644
--- a/addons/mail/static/src/components/chatter/chatter_tests.js
+++ b/addons/mail/static/src/components/chatter/chatter_tests.js
@@ -323,12 +323,12 @@ QUnit.test('should not display user notification messages in chatter', async fun
assert.expect(1);
this.data['res.partner'].records.push({ id: 100 });
- this.data['mail.message'].records = [{
+ this.data['mail.message'].records.push({
id: 102,
message_type: 'user_notification',
model: 'res.partner',
res_id: 100,
- }];
+ });
await this.start();
const chatter = this.env.models['mail.chatter'].create({
threadId: 100,
diff --git a/addons/mail/static/src/components/composer/composer.js b/addons/mail/static/src/components/composer/composer.js
index bc46cd72d1d..95a09f7820f 100644
--- a/addons/mail/static/src/components/composer/composer.js
+++ b/addons/mail/static/src/components/composer/composer.js
@@ -264,6 +264,13 @@ class Composer extends Component {
/**
* @private
*/
+ _onComposerSuggestionClicked() {
+ this.focus();
+ }
+
+ /**
+ * @private
+ */
_onComposerTextInputSendShortcut() {
this._postMessage();
}
@@ -311,7 +318,7 @@ class Composer extends Component {
*/
_onKeydown(ev) {
if (ev.key === 'Escape') {
- if (isEventHandled(ev, 'ComposerTextInput.closeMentionSuggestions')) {
+ if (isEventHandled(ev, 'ComposerTextInput.closeSuggestions')) {
return;
}
if (isEventHandled(ev, 'Composer.closeEmojisPopover')) {
diff --git a/addons/mail/static/src/components/composer/composer.xml b/addons/mail/static/src/components/composer/composer.xml
index be947b72b3f..4aa1d9cfe99 100644
--- a/addons/mail/static/src/components/composer/composer.xml
+++ b/addons/mail/static/src/components/composer/composer.xml
@@ -80,6 +80,7 @@
hasMentionSuggestionsBelowPosition="props.hasMentionSuggestionsBelowPosition"
isCompact="props.isCompact"
sendShortcuts="props.textInputSendShortcuts"
+ t-on-o-composer-suggestion-clicked="_onComposerSuggestionClicked"
t-on-o-composer-text-input-send-shortcut="_onComposerTextInputSendShortcut"
t-on-paste="_onPasteTextInput"
t-ref="textInput"
diff --git a/addons/mail/static/src/components/composer/composer_tests.js b/addons/mail/static/src/components/composer/composer_tests.js
index 459e0d2c799..a3740f91f74 100644
--- a/addons/mail/static/src/components/composer/composer_tests.js
+++ b/addons/mail/static/src/components/composer/composer_tests.js
@@ -274,22 +274,602 @@ QUnit.test('add emoji replaces (keyboard) text selection', async function (asser
await nextAnimationFrame();
});
-QUnit.test('display partner mention suggestions on typing "@"', async function (assert) {
+QUnit.test('display canned response suggestions on typing ":"', async function (assert) {
+ assert.expect(2);
+
+ this.data['mail.shortcode'].records.push({
+ id: 11,
+ source: "hello",
+ substitution: "Hello! How are you?",
+ });
+
+ await this.start();
+ const composer = this.env.models['mail.composer'].create();
+ await this.createComposerComponent(composer);
+
+ assert.containsNone(
+ document.body,
+ '.o_ComposerSuggestionList_list',
+ "Canned responses suggestions list should not be present"
+ );
+ await afterNextRender(() => {
+ document.querySelector(`.o_ComposerTextInput_textarea`).focus();
+ document.execCommand('insertText', false, ":");
+ document.querySelector(`.o_ComposerTextInput_textarea`)
+ .dispatchEvent(new window.KeyboardEvent('keydown'));
+ document.querySelector(`.o_ComposerTextInput_textarea`)
+ .dispatchEvent(new window.KeyboardEvent('keyup'));
+ });
+ assert.hasClass(
+ document.querySelector('.o_ComposerSuggestionList_list'),
+ 'show',
+ "should display canned response suggestions on typing ':'"
+ );
+});
+
+QUnit.test('use a canned response', async function (assert) {
+ assert.expect(4);
+
+ this.data['mail.shortcode'].records.push({
+ id: 11,
+ source: "hello",
+ substitution: "Hello! How are you?",
+ });
+
+ await this.start();
+ const composer = this.env.models['mail.composer'].create();
+ await this.createComposerComponent(composer);
+
+ assert.containsNone(
+ document.body,
+ '.o_ComposerSuggestionList_list',
+ "canned response suggestions list should not be present"
+ );
+ assert.strictEqual(
+ document.querySelector(`.o_ComposerTextInput_textarea`).value,
+ "",
+ "text content of composer should be empty initially"
+ );
+ await afterNextRender(() => {
+ document.querySelector(`.o_ComposerTextInput_textarea`).focus();
+ document.execCommand('insertText', false, ":");
+ });
+ await afterNextRender(() => {
+ document.querySelector(`.o_ComposerTextInput_textarea`)
+ .dispatchEvent(new window.KeyboardEvent('keydown'));
+ document.querySelector(`.o_ComposerTextInput_textarea`)
+ .dispatchEvent(new window.KeyboardEvent('keyup'));
+ });
+ assert.containsOnce(
+ document.body,
+ '.o_ComposerSuggestion',
+ "should have a canned response suggestion"
+ );
+ await afterNextRender(() =>
+ document.querySelector('.o_ComposerSuggestion').click()
+ );
+ assert.strictEqual(
+ document.querySelector(`.o_ComposerTextInput_textarea`).value.replace(/\s/, " "),
+ "Hello! How are you? ",
+ "text content of composer should have canned response + additional whitespace afterwards"
+ );
+});
+
+QUnit.test('use a canned response some text', async function (assert) {
+ assert.expect(5);
+
+ this.data['mail.shortcode'].records.push({
+ id: 11,
+ source: "hello",
+ substitution: "Hello! How are you?",
+ });
+
+ await this.start();
+ const composer = this.env.models['mail.composer'].create();
+ await this.createComposerComponent(composer);
+
+ assert.containsNone(
+ document.body,
+ '.o_ComposerSuggestion',
+ "canned response suggestions list should not be present"
+ );
+ assert.strictEqual(
+ document.querySelector(`.o_ComposerTextInput_textarea`).value,
+ "",
+ "text content of composer should be empty initially"
+ );
+ document.querySelector(`.o_ComposerTextInput_textarea`).focus();
+ await afterNextRender(() =>
+ document.execCommand('insertText', false, "bluhbluh ")
+ );
+ assert.strictEqual(
+ document.querySelector(`.o_ComposerTextInput_textarea`).value,
+ "bluhbluh ",
+ "text content of composer should have content"
+ );
+ await afterNextRender(() =>
+ document.execCommand('insertText', false, ":")
+ );
+ await afterNextRender(() => {
+ document.querySelector(`.o_ComposerTextInput_textarea`)
+ .dispatchEvent(new window.KeyboardEvent('keydown'));
+ document.querySelector(`.o_ComposerTextInput_textarea`)
+ .dispatchEvent(new window.KeyboardEvent('keyup'));
+ });
+ assert.containsOnce(
+ document.body,
+ '.o_ComposerSuggestion',
+ "should have a canned response suggestion"
+ );
+ await afterNextRender(() =>
+ document.querySelector('.o_ComposerSuggestion').click()
+ );
+ assert.strictEqual(
+ document.querySelector(`.o_ComposerTextInput_textarea`).value.replace(/\s/, " "),
+ "bluhbluh Hello! How are you? ",
+ "text content of composer should have previous content + canned response substitution + additional whitespace afterwards"
+ );
+});
+
+QUnit.test('add an emoji after a canned response', async function (assert) {
+ assert.expect(5);
+
+ this.data['mail.shortcode'].records.push({
+ id: 11,
+ source: "hello",
+ substitution: "Hello! How are you?",
+ });
+
+ await this.start();
+ const composer = this.env.models['mail.composer'].create();
+ await this.createComposerComponent(composer);
+
+ assert.containsNone(
+ document.body,
+ '.o_ComposerSuggestion',
+ "canned response suggestions list should not be present"
+ );
+ assert.strictEqual(
+ document.querySelector(`.o_ComposerTextInput_textarea`).value,
+ "",
+ "text content of composer should be empty initially"
+ );
+ await afterNextRender(() => {
+ document.querySelector(`.o_ComposerTextInput_textarea`).focus();
+ document.execCommand('insertText', false, ":");
+ });
+ await afterNextRender(() => {
+ document.querySelector(`.o_ComposerTextInput_textarea`)
+ .dispatchEvent(new window.KeyboardEvent('keydown'));
+ document.querySelector(`.o_ComposerTextInput_textarea`)
+ .dispatchEvent(new window.KeyboardEvent('keyup'));
+ });
+ assert.containsOnce(
+ document.body,
+ '.o_ComposerSuggestion',
+ "should have a canned response suggestion"
+ );
+ await afterNextRender(() =>
+ document.querySelector('.o_ComposerSuggestion').click()
+ );
+ assert.strictEqual(
+ document.querySelector(`.o_ComposerTextInput_textarea`).value.replace(/\s/, " "),
+ "Hello! How are you? ",
+ "text content of composer should have previous content + canned response substitution + additional whitespace afterwards"
+ );
+
+ // select emoji
+ await afterNextRender(() =>
+ document.querySelector('.o_Composer_buttonEmojis').click()
+ );
+ await afterNextRender(() =>
+ document.querySelector('.o_EmojisPopover_emoji[data-unicode="😊"]').click()
+ );
+ assert.strictEqual(
+ document.querySelector(`.o_ComposerTextInput_textarea`).value.replace(/\s/, " "),
+ "Hello! How are you? 😊",
+ "text content of composer should have previous canned response substitution and selected emoji just after"
+ );
+ // ensure popover is closed
+ await nextAnimationFrame();
+});
+
+QUnit.test('display channel mention suggestions on typing "#"', async function (assert) {
+ assert.expect(2);
+
+ this.data['mail.channel'].records.push({
+ id: 7,
+ name: "General",
+ public: "groups",
+ });
+ await this.start();
+ const composer = this.env.models['mail.composer'].create();
+ await this.createComposerComponent(composer);
+
+ assert.containsNone(
+ document.body,
+ '.o_ComposerSuggestionList_list',
+ "channel mention suggestions list should not be present"
+ );
+ await afterNextRender(() => {
+ document.querySelector(`.o_ComposerTextInput_textarea`).focus();
+ document.execCommand('insertText', false, "#");
+ document.querySelector(`.o_ComposerTextInput_textarea`)
+ .dispatchEvent(new window.KeyboardEvent('keydown'));
+ document.querySelector(`.o_ComposerTextInput_textarea`)
+ .dispatchEvent(new window.KeyboardEvent('keyup'));
+ });
+ assert.hasClass(
+ document.querySelector('.o_ComposerSuggestionList_list'),
+ 'show',
+ "should display channel mention suggestions on typing '#'"
+ );
+});
+
+QUnit.test('mention a channel', async function (assert) {
+ assert.expect(4);
+
+ this.data['mail.channel'].records.push({
+ id: 7,
+ name: "General",
+ public: "groups",
+ });
+ await this.start();
+ const composer = this.env.models['mail.composer'].create();
+ await this.createComposerComponent(composer);
+
+ assert.containsNone(
+ document.body,
+ '.o_ComposerSuggestionList_list',
+ "channel mention suggestions list should not be present"
+ );
+ assert.strictEqual(
+ document.querySelector(`.o_ComposerTextInput_textarea`).value,
+ "",
+ "text content of composer should be empty initially"
+ );
+ await afterNextRender(() => {
+ document.querySelector(`.o_ComposerTextInput_textarea`).focus();
+ document.execCommand('insertText', false, "#");
+ });
+ await afterNextRender(() => {
+ document.querySelector(`.o_ComposerTextInput_textarea`)
+ .dispatchEvent(new window.KeyboardEvent('keydown'));
+ document.querySelector(`.o_ComposerTextInput_textarea`)
+ .dispatchEvent(new window.KeyboardEvent('keyup'));
+ });
+ assert.containsOnce(
+ document.body,
+ '.o_ComposerSuggestion',
+ "should have a channel mention suggestion"
+ );
+ await afterNextRender(() =>
+ document.querySelector('.o_ComposerSuggestion').click()
+ );
+ assert.strictEqual(
+ document.querySelector(`.o_ComposerTextInput_textarea`).value.replace(/\s/, " "),
+ "#General ",
+ "text content of composer should have mentioned channel + additional whitespace afterwards"
+ );
+});
+
+QUnit.test('mention a channel after some text', async function (assert) {
+ assert.expect(5);
+
+ this.data['mail.channel'].records.push({
+ id: 7,
+ name: "General",
+ public: "groups",
+ });
+ await this.start();
+ const composer = this.env.models['mail.composer'].create();
+ await this.createComposerComponent(composer);
+
+ assert.containsNone(
+ document.body,
+ '.o_ComposerSuggestion',
+ "channel mention suggestions list should not be present"
+ );
+ assert.strictEqual(
+ document.querySelector(`.o_ComposerTextInput_textarea`).value,
+ "",
+ "text content of composer should be empty initially"
+ );
+ document.querySelector(`.o_ComposerTextInput_textarea`).focus();
+ await afterNextRender(() =>
+ document.execCommand('insertText', false, "bluhbluh ")
+ );
+ assert.strictEqual(
+ document.querySelector(`.o_ComposerTextInput_textarea`).value,
+ "bluhbluh ",
+ "text content of composer should have content"
+ );
+ await afterNextRender(() =>
+ document.execCommand('insertText', false, "#")
+ );
+ await afterNextRender(() => {
+ document.querySelector(`.o_ComposerTextInput_textarea`)
+ .dispatchEvent(new window.KeyboardEvent('keydown'));
+ document.querySelector(`.o_ComposerTextInput_textarea`)
+ .dispatchEvent(new window.KeyboardEvent('keyup'));
+ });
+ assert.containsOnce(
+ document.body,
+ '.o_ComposerSuggestion',
+ "should have a channel mention suggestion"
+ );
+ await afterNextRender(() =>
+ document.querySelector('.o_ComposerSuggestion').click()
+ );
+ assert.strictEqual(
+ document.querySelector(`.o_ComposerTextInput_textarea`).value.replace(/\s/, " "),
+ "bluhbluh #General ",
+ "text content of composer should have previous content + mentioned channel + additional whitespace afterwards"
+ );
+});
+
+QUnit.test('add an emoji after a channel mention', async function (assert) {
+ assert.expect(5);
+
+ this.data['mail.channel'].records.push({
+ id: 7,
+ name: "General",
+ public: "groups",
+ });
+ await this.start();
+ const composer = this.env.models['mail.composer'].create();
+ await this.createComposerComponent(composer);
+
+ assert.containsNone(
+ document.body,
+ '.o_ComposerSuggestion',
+ "mention suggestions list should not be present"
+ );
+ assert.strictEqual(
+ document.querySelector(`.o_ComposerTextInput_textarea`).value,
+ "",
+ "text content of composer should be empty initially"
+ );
+ await afterNextRender(() => {
+ document.querySelector(`.o_ComposerTextInput_textarea`).focus();
+ document.execCommand('insertText', false, "#");
+ });
+ await afterNextRender(() => {
+ document.querySelector(`.o_ComposerTextInput_textarea`)
+ .dispatchEvent(new window.KeyboardEvent('keydown'));
+ document.querySelector(`.o_ComposerTextInput_textarea`)
+ .dispatchEvent(new window.KeyboardEvent('keyup'));
+ });
+ assert.containsOnce(
+ document.body,
+ '.o_ComposerSuggestion',
+ "should have a channel mention suggestion"
+ );
+ await afterNextRender(() =>
+ document.querySelector('.o_ComposerSuggestion').click()
+ );
+ assert.strictEqual(
+ document.querySelector(`.o_ComposerTextInput_textarea`).value.replace(/\s/, " "),
+ "#General ",
+ "text content of composer should have previous content + mentioned channel + additional whitespace afterwards"
+ );
+
+ // select emoji
+ await afterNextRender(() =>
+ document.querySelector('.o_Composer_buttonEmojis').click()
+ );
+ await afterNextRender(() =>
+ document.querySelector('.o_EmojisPopover_emoji[data-unicode="😊"]').click()
+ );
+ assert.strictEqual(
+ document.querySelector(`.o_ComposerTextInput_textarea`).value.replace(/\s/, " "),
+ "#General 😊",
+ "text content of composer should have previous channel mention and selected emoji just after"
+ );
+ // ensure popover is closed
+ await nextAnimationFrame();
+});
+
+QUnit.test('display command suggestions on typing "/"', async function (assert) {
assert.expect(2);
+ await this.start();
+ const composer = this.env.models['mail.composer'].create();
+ await this.createComposerComponent(composer);
+
+ assert.containsNone(
+ document.body,
+ '.o_ComposerSuggestionList_list',
+ "command suggestions list should not be present"
+ );
+ await afterNextRender(() => {
+ document.querySelector(`.o_ComposerTextInput_textarea`).focus();
+ document.execCommand('insertText', false, "/");
+ document.querySelector(`.o_ComposerTextInput_textarea`)
+ .dispatchEvent(new window.KeyboardEvent('keydown'));
+ document.querySelector(`.o_ComposerTextInput_textarea`)
+ .dispatchEvent(new window.KeyboardEvent('keyup'));
+ });
+ assert.hasClass(
+ document.querySelector('.o_ComposerSuggestionList_list'),
+ 'show',
+ "should display command suggestions on typing '/'"
+ );
+});
+
+QUnit.test('use a command', async function (assert) {
+ assert.expect(4);
+
+ await this.start();
+ const composer = this.env.models['mail.composer'].create();
+ await this.createComposerComponent(composer);
+
+ assert.containsNone(
+ document.body,
+ '.o_ComposerSuggestionList_list',
+ "command suggestions list should not be present"
+ );
+ assert.strictEqual(
+ document.querySelector(`.o_ComposerTextInput_textarea`).value,
+ "",
+ "text content of composer should be empty initially"
+ );
+ await afterNextRender(() => {
+ document.querySelector(`.o_ComposerTextInput_textarea`).focus();
+ document.execCommand('insertText', false, "/");
+ });
+ await afterNextRender(() => {
+ document.querySelector(`.o_ComposerTextInput_textarea`)
+ .dispatchEvent(new window.KeyboardEvent('keydown'));
+ document.querySelector(`.o_ComposerTextInput_textarea`)
+ .dispatchEvent(new window.KeyboardEvent('keyup'));
+ });
+ assert.containsOnce(
+ document.body,
+ '.o_ComposerSuggestion',
+ "should have a command suggestion"
+ );
+ await afterNextRender(() =>
+ document.querySelector('.o_ComposerSuggestion').click()
+ );
+ assert.strictEqual(
+ document.querySelector(`.o_ComposerTextInput_textarea`).value.replace(/\s/, " "),
+ "/who ",
+ "text content of composer should have used command + additional whitespace afterwards"
+ );
+});
+
+QUnit.test('use a command after some text', async function (assert) {
+ assert.expect(5);
+
+ await this.start();
+ const composer = this.env.models['mail.composer'].create();
+ await this.createComposerComponent(composer);
+
+ assert.containsNone(
+ document.body,
+ '.o_ComposerSuggestion',
+ "command suggestions list should not be present"
+ );
+ assert.strictEqual(
+ document.querySelector(`.o_ComposerTextInput_textarea`).value,
+ "",
+ "text content of composer should be empty initially"
+ );
+ document.querySelector(`.o_ComposerTextInput_textarea`).focus();
+ await afterNextRender(() =>
+ document.execCommand('insertText', false, "bluhbluh ")
+ );
+ assert.strictEqual(
+ document.querySelector(`.o_ComposerTextInput_textarea`).value,
+ "bluhbluh ",
+ "text content of composer should have content"
+ );
+ await afterNextRender(() =>
+ document.execCommand('insertText', false, "/")
+ );
+ await afterNextRender(() => {
+ document.querySelector(`.o_ComposerTextInput_textarea`)
+ .dispatchEvent(new window.KeyboardEvent('keydown'));
+ document.querySelector(`.o_ComposerTextInput_textarea`)
+ .dispatchEvent(new window.KeyboardEvent('keyup'));
+ });
+ assert.containsOnce(
+ document.body,
+ '.o_ComposerSuggestion',
+ "should have a command suggestion"
+ );
+ await afterNextRender(() =>
+ document.querySelector('.o_ComposerSuggestion').click()
+ );
+ assert.strictEqual(
+ document.querySelector(`.o_ComposerTextInput_textarea`).value.replace(/\s/, " "),
+ "bluhbluh /who ",
+ "text content of composer should have previous content + used command + additional whitespace afterwards"
+ );
+});
+
+QUnit.test('add an emoji after a command', async function (assert) {
+ assert.expect(5);
+
+ await this.start();
+ const composer = this.env.models['mail.composer'].create();
+ await this.createComposerComponent(composer);
+
+ assert.containsNone(
+ document.body,
+ '.o_ComposerSuggestion',
+ "command suggestions list should not be present"
+ );
+ assert.strictEqual(
+ document.querySelector(`.o_ComposerTextInput_textarea`).value,
+ "",
+ "text content of composer should be empty initially"
+ );
+ await afterNextRender(() => {
+ document.querySelector(`.o_ComposerTextInput_textarea`).focus();
+ document.execCommand('insertText', false, "/");
+ });
+ await afterNextRender(() => {
+ document.querySelector(`.o_ComposerTextInput_textarea`)
+ .dispatchEvent(new window.KeyboardEvent('keydown'));
+ document.querySelector(`.o_ComposerTextInput_textarea`)
+ .dispatchEvent(new window.KeyboardEvent('keyup'));
+ });
+ assert.containsOnce(
+ document.body,
+ '.o_ComposerSuggestion',
+ "should have a command suggestion"
+ );
+ await afterNextRender(() =>
+ document.querySelector('.o_ComposerSuggestion').click()
+ );
+ assert.strictEqual(
+ document.querySelector(`.o_ComposerTextInput_textarea`).value.replace(/\s/, " "),
+ "/who ",
+ "text content of composer should have previous content + used command + additional whitespace afterwards"
+ );
+
+ // select emoji
+ await afterNextRender(() =>
+ document.querySelector('.o_Composer_buttonEmojis').click()
+ );
+ await afterNextRender(() =>
+ document.querySelector('.o_EmojisPopover_emoji[data-unicode="😊"]').click()
+ );
+ assert.strictEqual(
+ document.querySelector(`.o_ComposerTextInput_textarea`).value.replace(/\s/, " "),
+ "/who 😊",
+ "text content of composer should have previous command and selected emoji just after"
+ );
+ // ensure popover is closed
+ await nextAnimationFrame();
+});
+
+QUnit.test('display partner mention suggestions on typing "@"', async function (assert) {
+ assert.expect(3);
+
this.data['res.partner'].records.push({
- email: "testpartnert@odoo.com",
id: 11,
+ email: "testpartner@odoo.com",
name: "TestPartner",
});
- this.data['res.users'].records.push({ partner_id: 11 });
+ this.data['res.partner'].records.push({
+ id: 12,
+ email: "testpartner2@odoo.com",
+ name: "TestPartner2",
+ });
+ this.data['res.users'].records.push({
+ partner_id: 11,
+ });
+
await this.start();
const composer = this.env.models['mail.composer'].create();
await this.createComposerComponent(composer);
assert.containsNone(
document.body,
- '.o_ComposerTextInput_mentionDropdownPropositionList',
+ '.o_ComposerSuggestionList_list',
"mention suggestions list should not be present"
);
await afterNextRender(() => {
@@ -301,28 +881,31 @@ QUnit.test('display partner mention suggestions on typing "@"', async function (
.dispatchEvent(new window.KeyboardEvent('keyup'));
});
assert.hasClass(
- document.querySelector('.o_ComposerTextInput_mentionDropdownPropositionList'),
+ document.querySelector('.o_ComposerSuggestionList_list'),
'show',
"should display mention suggestions on typing '@'"
);
+ assert.containsOnce(
+ document.body,
+ '.dropdown-divider',
+ "should have a separator"
+ );
});
QUnit.test('mention a partner', async function (assert) {
assert.expect(4);
this.data['res.partner'].records.push({
- email: "testpartnert@odoo.com",
- id: 11,
+ email: "testpartner@odoo.com",
name: "TestPartner",
});
- this.data['res.users'].records.push({ partner_id: 11 });
await this.start();
const composer = this.env.models['mail.composer'].create();
await this.createComposerComponent(composer);
assert.containsNone(
document.body,
- '.o_ComposerTextInput_mentionDropdownPropositionList',
+ '.o_ComposerSuggestionList_list',
"mention suggestions list should not be present"
);
assert.strictEqual(
@@ -332,9 +915,17 @@ QUnit.test('mention a partner', async function (assert) {
);
await afterNextRender(() => {
document.querySelector(`.o_ComposerTextInput_textarea`).focus();
- document.execCommand('insertText', false, "@Test");
- });
- await afterNextRender(() => {
+ document.execCommand('insertText', false, "@");
+ document.querySelector(`.o_ComposerTextInput_textarea`)
+ .dispatchEvent(new window.KeyboardEvent('keydown'));
+ document.querySelector(`.o_ComposerTextInput_textarea`)
+ .dispatchEvent(new window.KeyboardEvent('keyup'));
+ document.execCommand('insertText', false, "T");
+ document.querySelector(`.o_ComposerTextInput_textarea`)
+ .dispatchEvent(new window.KeyboardEvent('keydown'));
+ document.querySelector(`.o_ComposerTextInput_textarea`)
+ .dispatchEvent(new window.KeyboardEvent('keyup'));
+ document.execCommand('insertText', false, "e");
document.querySelector(`.o_ComposerTextInput_textarea`)
.dispatchEvent(new window.KeyboardEvent('keydown'));
document.querySelector(`.o_ComposerTextInput_textarea`)
@@ -342,11 +933,11 @@ QUnit.test('mention a partner', async function (assert) {
});
assert.containsOnce(
document.body,
- '.o_PartnerMentionSuggestion',
+ '.o_ComposerSuggestion',
"should have a mention suggestion"
);
await afterNextRender(() =>
- document.querySelector('.o_PartnerMentionSuggestion').click()
+ document.querySelector('.o_ComposerSuggestion').click()
);
assert.strictEqual(
document.querySelector(`.o_ComposerTextInput_textarea`).value.replace(/\s/, " "),
@@ -359,18 +950,16 @@ QUnit.test('mention a partner after some text', async function (assert) {
assert.expect(5);
this.data['res.partner'].records.push({
- email: "testpartnert@odoo.com",
- id: 11,
+ email: "testpartner@odoo.com",
name: "TestPartner",
});
- this.data['res.users'].records.push({ partner_id: 11 });
await this.start();
const composer = this.env.models['mail.composer'].create();
await this.createComposerComponent(composer);
assert.containsNone(
document.body,
- '.o_ComposerTextInput_mentionDropdownPropositionList',
+ '.o_ComposerSuggestion',
"mention suggestions list should not be present"
);
assert.strictEqual(
@@ -387,10 +976,19 @@ QUnit.test('mention a partner after some text', async function (assert) {
"bluhbluh ",
"text content of composer should have content"
);
- await afterNextRender(() =>
- document.execCommand('insertText', false, "@Test")
- );
await afterNextRender(() => {
+ document.querySelector(`.o_ComposerTextInput_textarea`).focus();
+ document.execCommand('insertText', false, "@");
+ document.querySelector(`.o_ComposerTextInput_textarea`)
+ .dispatchEvent(new window.KeyboardEvent('keydown'));
+ document.querySelector(`.o_ComposerTextInput_textarea`)
+ .dispatchEvent(new window.KeyboardEvent('keyup'));
+ document.execCommand('insertText', false, "T");
+ document.querySelector(`.o_ComposerTextInput_textarea`)
+ .dispatchEvent(new window.KeyboardEvent('keydown'));
+ document.querySelector(`.o_ComposerTextInput_textarea`)
+ .dispatchEvent(new window.KeyboardEvent('keyup'));
+ document.execCommand('insertText', false, "e");
document.querySelector(`.o_ComposerTextInput_textarea`)
.dispatchEvent(new window.KeyboardEvent('keydown'));
document.querySelector(`.o_ComposerTextInput_textarea`)
@@ -398,11 +996,11 @@ QUnit.test('mention a partner after some text', async function (assert) {
});
assert.containsOnce(
document.body,
- '.o_PartnerMentionSuggestion',
+ '.o_ComposerSuggestion',
"should have a mention suggestion"
);
await afterNextRender(() =>
- document.querySelector('.o_PartnerMentionSuggestion').click()
+ document.querySelector('.o_ComposerSuggestion').click()
);
assert.strictEqual(
document.querySelector(`.o_ComposerTextInput_textarea`).value.replace(/\s/, " "),
@@ -415,18 +1013,16 @@ QUnit.test('add an emoji after a partner mention', async function (assert) {
assert.expect(5);
this.data['res.partner'].records.push({
- email: "testpartnert@odoo.com",
- id: 11,
+ email: "testpartner@odoo.com",
name: "TestPartner",
});
- this.data['res.users'].records.push({ partner_id: 11 });
await this.start();
const composer = this.env.models['mail.composer'].create();
await this.createComposerComponent(composer);
assert.containsNone(
document.body,
- '.o_ComposerTextInput_mentionDropdownPropositionList',
+ '.o_ComposerSuggestion',
"mention suggestions list should not be present"
);
assert.strictEqual(
@@ -436,9 +1032,17 @@ QUnit.test('add an emoji after a partner mention', async function (assert) {
);
await afterNextRender(() => {
document.querySelector(`.o_ComposerTextInput_textarea`).focus();
- document.execCommand('insertText', false, "@Test");
- });
- await afterNextRender(() => {
+ document.execCommand('insertText', false, "@");
+ document.querySelector(`.o_ComposerTextInput_textarea`)
+ .dispatchEvent(new window.KeyboardEvent('keydown'));
+ document.querySelector(`.o_ComposerTextInput_textarea`)
+ .dispatchEvent(new window.KeyboardEvent('keyup'));
+ document.execCommand('insertText', false, "T");
+ document.querySelector(`.o_ComposerTextInput_textarea`)
+ .dispatchEvent(new window.KeyboardEvent('keydown'));
+ document.querySelector(`.o_ComposerTextInput_textarea`)
+ .dispatchEvent(new window.KeyboardEvent('keyup'));
+ document.execCommand('insertText', false, "e");
document.querySelector(`.o_ComposerTextInput_textarea`)
.dispatchEvent(new window.KeyboardEvent('keydown'));
document.querySelector(`.o_ComposerTextInput_textarea`)
@@ -446,11 +1050,11 @@ QUnit.test('add an emoji after a partner mention', async function (assert) {
});
assert.containsOnce(
document.body,
- '.o_PartnerMentionSuggestion',
+ '.o_ComposerSuggestion',
"should have a mention suggestion"
);
await afterNextRender(() =>
- document.querySelector('.o_PartnerMentionSuggestion').click()
+ document.querySelector('.o_ComposerSuggestion').click()
);
assert.strictEqual(
document.querySelector(`.o_ComposerTextInput_textarea`).value.replace(/\s/, " "),
diff --git a/addons/mail/static/src/components/composer_suggestion/composer_suggestion.js b/addons/mail/static/src/components/composer_suggestion/composer_suggestion.js
new file mode 100644
index 00000000000..caa324f1583
--- /dev/null
+++ b/addons/mail/static/src/components/composer_suggestion/composer_suggestion.js
@@ -0,0 +1,101 @@
+odoo.define('mail/static/src/components/composer_suggestion/composer_suggestion.js', function (require) {
+'use strict';
+
+const useStore = require('mail/static/src/component_hooks/use_store/use_store.js');
+
+const components = {
+ PartnerImStatusIcon: require('mail/static/src/components/partner_im_status_icon/partner_im_status_icon.js'),
+};
+
+const { Component } = owl;
+
+class ComposerSuggestion extends Component {
+
+ /**
+ * @override
+ */
+ constructor(...args) {
+ super(...args);
+ useStore(props => {
+ const record = this.env.models[props.modelName].get(props.recordLocalId);
+ return {
+ record: record ? record.__state : undefined,
+ };
+ });
+ }
+
+ //--------------------------------------------------------------------------
+ // Public
+ //--------------------------------------------------------------------------
+
+ /**
+ * @returns {mail.composer}
+ */
+ get composer() {
+ return this.env.models['mail.composer'].get(this.props.composerLocalId);
+ }
+
+ get isCannedResponse() {
+ return this.props.modelName === "mail.canned_response";
+ }
+
+ get isChannel() {
+ return this.props.modelName === "mail.thread";
+ }
+
+ get isCommand() {
+ return this.props.modelName === "mail.channel_command";
+ }
+
+ get isPartner() {
+ return this.props.modelName === "mail.partner";
+ }
+
+ get record() {
+ return this.env.models[this.props.modelName].get(this.props.recordLocalId);
+ }
+
+ //--------------------------------------------------------------------------
+ // Handlers
+ //--------------------------------------------------------------------------
+
+ /**
+ * @private
+ * @param {Event} ev
+ */
+ _onClick(ev) {
+ ev.preventDefault();
+ this.composer.insertSuggestion(this.record);
+ this.composer.closeSuggestions();
+ this.trigger('o-composer-suggestion-clicked');
+ }
+
+ /**
+ * @private
+ * @param {Event} ev
+ */
+ _onMouseOver(ev) {
+ this.composer.update({
+ [this.composer.activeSuggestedRecordName]: [['link', this.record]],
+ });
+ }
+
+}
+
+Object.assign(ComposerSuggestion, {
+ components,
+ defaultProps: {
+ isActive: false,
+ },
+ props: {
+ composerLocalId: String,
+ isActive: Boolean,
+ modelName: String,
+ recordLocalId: String,
+ },
+ template: 'mail.ComposerSuggestion',
+});
+
+return ComposerSuggestion;
+
+});
diff --git a/addons/mail/static/src/components/partner_mention_suggestion/partner_mention_suggestion.scss b/addons/mail/static/src/components/composer_suggestion/composer_suggestion.scss
index b93d245972c..6516672f38f 100644
--- a/addons/mail/static/src/components/partner_mention_suggestion/partner_mention_suggestion.scss
+++ b/addons/mail/static/src/components/composer_suggestion/composer_suggestion.scss
@@ -2,13 +2,13 @@
// Layout
// ------------------------------------------------------------------
-.o_PartnerMentionSuggestion {
+.o_ComposerSuggestion {
display: flex;
width: map-get($sizes, 100);
padding: map-get($spacers, 2) map-get($spacers, 4);
}
-.o_PartnerMentionSuggestion_name {
+.o_ComposerSuggestion_part1 {
padding-inline-end: map-get($spacers, 2);
}
@@ -16,11 +16,11 @@
// Style
// ------------------------------------------------------------------
-.o_PartnerMentionSuggestion_email {
- font-style: italic;
- color: $gray-600;
+.o_ComposerSuggestion_part1 {
+ font-weight: $font-weight-bold;
}
-.o_PartnerMentionSuggestion_name {
- font-weight: $font-weight-bold;
+.o_ComposerSuggestion_part2 {
+ font-style: italic;
+ color: $gray-600;
}
diff --git a/addons/mail/static/src/components/composer_suggestion/composer_suggestion.xml b/addons/mail/static/src/components/composer_suggestion/composer_suggestion.xml
new file mode 100644
index 00000000000..3397095e415
--- /dev/null
+++ b/addons/mail/static/src/components/composer_suggestion/composer_suggestion.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<templates xml:space="preserve">
+
+ <t t-name="mail.ComposerSuggestion" owl="1">
+ <a class="o_ComposerSuggestion dropdown-item" t-att-class="{ 'active': props.isActive }" href="#" role="menuitem" t-on-click="_onClick" t-on-mouseover="_onMouseOver">
+ <t t-if="record">
+ <t t-if="isCannedResponse">
+ <span class="o_ComposerSuggestion_part1"><t t-esc="record.source"/></span>
+ <span class="o_ComposerSuggestion_part2"><t t-esc="record.substitution"/></span>
+ </t>
+ <t t-if="isChannel">
+ <span class="o_ComposerSuggestion_part1"><t t-esc="record.name"/></span>
+ </t>
+ <t t-if="isCommand">
+ <span class="o_ComposerSuggestion_part1"><t t-esc="record.name"/></span>
+ <span class="o_ComposerSuggestion_part2"><t t-esc="record.help"/></span>
+ </t>
+ <t t-if="isPartner">
+ <PartnerImStatusIcon
+ hasBackground="false"
+ partnerLocalId="record.localId"
+ />
+ <span class="o_ComposerSuggestion_part1"><t t-esc="record.nameOrDisplayName"/></span>
+ <t t-if="record.email">
+ <span class="o_ComposerSuggestion_part2">(<t t-esc="record.email"/>)</span>
+ </t>
+ </t>
+ </t>
+ </a>
+ </t>
+
+</templates>
diff --git a/addons/mail/static/src/components/composer_suggestion/composer_suggestion_canned_response_tests.js b/addons/mail/static/src/components/composer_suggestion/composer_suggestion_canned_response_tests.js
new file mode 100644
index 00000000000..0e0f86859a6
--- /dev/null
+++ b/addons/mail/static/src/components/composer_suggestion/composer_suggestion_canned_response_tests.js
@@ -0,0 +1,154 @@
+odoo.define('mail/static/src/components/composer_suggestion/composer_suggestion_canned_response_tests.js', function (require) {
+'use strict';
+
+const components = {
+ ComposerSuggestion: require('mail/static/src/components/composer_suggestion/composer_suggestion.js'),
+};
+const {
+ afterEach,
+ beforeEach,
+ createRootComponent,
+ start,
+} = require('mail/static/src/utils/test_utils.js');
+
+QUnit.module('mail', {}, function () {
+QUnit.module('components', {}, function () {
+QUnit.module('composer_suggestion', {}, function () {
+QUnit.module('composer_suggestion_canned_response_tests.js', {
+ beforeEach() {
+ beforeEach(this);
+
+ this.createComposerSuggestion = async props => {
+ await createRootComponent(this, components.ComposerSuggestion, {
+ props,
+ target: this.widget.el,
+ });
+ };
+
+ this.start = async params => {
+ const { env, widget } = await start(Object.assign({}, params, {
+ data: this.data,
+ }));
+ this.env = env;
+ this.widget = widget;
+ };
+ },
+ afterEach() {
+ afterEach(this);
+ },
+});
+
+QUnit.test('canned response suggestion displayed', async function (assert) {
+ assert.expect(1);
+
+ this.data['mail.channel'].records.push({ id: 20 });
+ await this.start();
+ const thread = this.env.models['mail.thread'].findFromIdentifyingData({
+ id: 20,
+ model: 'mail.channel',
+ });
+ const cannedResponse = this.env.models['mail.canned_response'].create({
+ id: 7,
+ source: 'hello',
+ substitution: "Hello, how are you?",
+ });
+ await this.createComposerSuggestion({
+ composerLocalId: thread.composer.localId,
+ isActive: true,
+ modelName: 'mail.canned_response',
+ recordLocalId: cannedResponse.localId,
+ });
+
+ assert.containsOnce(
+ document.body,
+ `.o_ComposerSuggestion`,
+ "Canned response suggestion should be present"
+ );
+});
+
+QUnit.test('canned response suggestion correct data', async function (assert) {
+ assert.expect(5);
+
+ this.data['mail.channel'].records.push({ id: 20 });
+ await this.start();
+ const thread = this.env.models['mail.thread'].findFromIdentifyingData({
+ id: 20,
+ model: 'mail.channel',
+ });
+ const cannedResponse = this.env.models['mail.canned_response'].create({
+ id: 7,
+ source: 'hello',
+ substitution: "Hello, how are you?",
+ });
+ await this.createComposerSuggestion({
+ composerLocalId: thread.composer.localId,
+ isActive: true,
+ modelName: 'mail.canned_response',
+ recordLocalId: cannedResponse.localId,
+ });
+
+ assert.containsOnce(
+ document.body,
+ '.o_ComposerSuggestion',
+ "Canned response suggestion should be present"
+ );
+ assert.containsOnce(
+ document.body,
+ '.o_ComposerSuggestion_part1',
+ "Canned response source should be present"
+ );
+ assert.strictEqual(
+ document.querySelector(`.o_ComposerSuggestion_part1`).textContent,
+ "hello",
+ "Canned response source should be displayed"
+ );
+ assert.containsOnce(
+ document.body,
+ '.o_ComposerSuggestion_part2',
+ "Canned response substitution should be present"
+ );
+ assert.strictEqual(
+ document.querySelector(`.o_ComposerSuggestion_part2`).textContent,
+ "Hello, how are you?",
+ "Canned response substitution should be displayed"
+ );
+});
+
+QUnit.test('canned response suggestion active', async function (assert) {
+ assert.expect(2);
+
+ this.data['mail.channel'].records.push({ id: 20 });
+ await this.start();
+ const thread = this.env.models['mail.thread'].findFromIdentifyingData({
+ id: 20,
+ model: 'mail.channel',
+ });
+ const cannedResponse = this.env.models['mail.canned_response'].create({
+ id: 7,
+ source: 'hello',
+ substitution: "Hello, how are you?",
+ });
+ await this.createComposerSuggestion({
+ composerLocalId: thread.composer.localId,
+ isActive: true,
+ modelName: 'mail.canned_response',
+ recordLocalId: cannedResponse.localId,
+ });
+
+ assert.containsOnce(
+ document.body,
+ '.o_ComposerSuggestion',
+ "Canned response suggestion should be displayed"
+ );
+ assert.hasClass(
+ document.querySelector('.o_ComposerSuggestion'),
+ 'active',
+ "should be active initially"
+ );
+});
+
+});
+});
+});
+
+});
diff --git a/addons/mail/static/src/components/composer_suggestion/composer_suggestion_channel_tests.js b/addons/mail/static/src/components/composer_suggestion/composer_suggestion_channel_tests.js
new file mode 100644
index 00000000000..f8409de2da9
--- /dev/null
+++ b/addons/mail/static/src/components/composer_suggestion/composer_suggestion_channel_tests.js
@@ -0,0 +1,141 @@
+odoo.define('mail/static/src/components/composer_suggestion/composer_suggestion_channel_tests.js', function (require) {
+'use strict';
+
+const components = {
+ ComposerSuggestion: require('mail/static/src/components/composer_suggestion/composer_suggestion.js'),
+};
+const {
+ afterEach,
+ beforeEach,
+ createRootComponent,
+ start,
+} = require('mail/static/src/utils/test_utils.js');
+
+QUnit.module('mail', {}, function () {
+QUnit.module('components', {}, function () {
+QUnit.module('composer_suggestion', {}, function () {
+QUnit.module('composer_suggestion_channel_tests.js', {
+ beforeEach() {
+ beforeEach(this);
+
+ this.createComposerSuggestion = async props => {
+ await createRootComponent(this, components.ComposerSuggestion, {
+ props,
+ target: this.widget.el,
+ });
+ };
+
+ this.start = async params => {
+ const { env, widget } = await start(Object.assign({}, params, {
+ data: this.data,
+ }));
+ this.env = env;
+ this.widget = widget;
+ };
+ },
+ afterEach() {
+ afterEach(this);
+ },
+});
+
+QUnit.test('channel mention suggestion displayed', async function (assert) {
+ assert.expect(1);
+
+ this.data['mail.channel'].records.push({ id: 20 });
+ await this.start();
+ const thread = this.env.models['mail.thread'].findFromIdentifyingData({
+ id: 20,
+ model: 'mail.channel',
+ });
+ const channel = this.env.models['mail.thread'].create({
+ id: 7,
+ name: "General",
+ });
+ await this.createComposerSuggestion({
+ composerLocalId: thread.composer.localId,
+ isActive: true,
+ modelName: 'mail.thread',
+ recordLocalId: channel.localId,
+ });
+
+ assert.containsOnce(
+ document.body,
+ `.o_ComposerSuggestion`,
+ "Channel mention suggestion should be present"
+ );
+});
+
+QUnit.test('channel mention suggestion correct data', async function (assert) {
+ assert.expect(3);
+
+ this.data['mail.channel'].records.push({ id: 20 });
+ await this.start();
+ const thread = this.env.models['mail.thread'].findFromIdentifyingData({
+ id: 20,
+ model: 'mail.channel',
+ });
+ const channel = this.env.models['mail.thread'].create({
+ id: 7,
+ name: "General",
+ });
+ await this.createComposerSuggestion({
+ composerLocalId: thread.composer.localId,
+ isActive: true,
+ modelName: 'mail.thread',
+ recordLocalId: channel.localId,
+ });
+
+ assert.containsOnce(
+ document.body,
+ '.o_ComposerSuggestion',
+ "Channel mention suggestion should be present"
+ );
+ assert.containsOnce(
+ document.body,
+ '.o_ComposerSuggestion_part1',
+ "Channel name should be present"
+ );
+ assert.strictEqual(
+ document.querySelector(`.o_ComposerSuggestion_part1`).textContent,
+ "General",
+ "Channel name should be displayed"
+ );
+});
+
+QUnit.test('partner mention suggestion active', async function (assert) {
+ assert.expect(2);
+
+ this.data['mail.channel'].records.push({ id: 20 });
+ await this.start();
+ const thread = this.env.models['mail.thread'].findFromIdentifyingData({
+ id: 20,
+ model: 'mail.channel',
+ });
+ const channel = this.env.models['mail.thread'].create({
+ id: 7,
+ name: "General",
+ });
+ await this.createComposerSuggestion({
+ composerLocalId: thread.composer.localId,
+ isActive: true,
+ modelName: 'mail.thread',
+ recordLocalId: channel.localId,
+ });
+
+ assert.containsOnce(
+ document.body,
+ '.o_ComposerSuggestion',
+ "Channel mention suggestion should be displayed"
+ );
+ assert.hasClass(
+ document.querySelector('.o_ComposerSuggestion'),
+ 'active',
+ "should be active initially"
+ );
+});
+
+});
+});
+});
+
+});
diff --git a/addons/mail/static/src/components/composer_suggestion/composer_suggestion_command_tests.js b/addons/mail/static/src/components/composer_suggestion/composer_suggestion_command_tests.js
new file mode 100644
index 00000000000..8bbb3d45e1e
--- /dev/null
+++ b/addons/mail/static/src/components/composer_suggestion/composer_suggestion_command_tests.js
@@ -0,0 +1,151 @@
+odoo.define('mail/static/src/components/composer_suggestion/composer_suggestion_command_tests.js', function (require) {
+'use strict';
+
+const components = {
+ ComposerSuggestion: require('mail/static/src/components/composer_suggestion/composer_suggestion.js'),
+};
+const {
+ afterEach,
+ beforeEach,
+ createRootComponent,
+ start,
+} = require('mail/static/src/utils/test_utils.js');
+
+QUnit.module('mail', {}, function () {
+QUnit.module('components', {}, function () {
+QUnit.module('composer_suggestion', {}, function () {
+QUnit.module('composer_suggestion_command_tests.js', {
+ beforeEach() {
+ beforeEach(this);
+
+ this.createComposerSuggestion = async props => {
+ await createRootComponent(this, components.ComposerSuggestion, {
+ props,
+ target: this.widget.el,
+ });
+ };
+
+ this.start = async params => {
+ const { env, widget } = await start(Object.assign({}, params, {
+ data: this.data,
+ }));
+ this.env = env;
+ this.widget = widget;
+ };
+ },
+ afterEach() {
+ afterEach(this);
+ },
+});
+
+QUnit.test('command suggestion displayed', async function (assert) {
+ assert.expect(1);
+
+ this.data['mail.channel'].records.push({ id: 20 });
+ await this.start();
+ const thread = this.env.models['mail.thread'].findFromIdentifyingData({
+ id: 20,
+ model: 'mail.channel',
+ });
+ const command = this.env.models['mail.channel_command'].create({
+ name: 'whois',
+ help: "Displays who it is",
+ });
+ await this.createComposerSuggestion({
+ composerLocalId: thread.composer.localId,
+ isActive: true,
+ modelName: 'mail.channel_command',
+ recordLocalId: command.localId,
+ });
+
+ assert.containsOnce(
+ document.body,
+ `.o_ComposerSuggestion`,
+ "Command suggestion should be present"
+ );
+});
+
+QUnit.test('command suggestion correct data', async function (assert) {
+ assert.expect(5);
+
+ this.data['mail.channel'].records.push({ id: 20 });
+ await this.start();
+ const thread = this.env.models['mail.thread'].findFromIdentifyingData({
+ id: 20,
+ model: 'mail.channel',
+ });
+ const command = this.env.models['mail.channel_command'].create({
+ name: 'whois',
+ help: "Displays who it is",
+ });
+ await this.createComposerSuggestion({
+ composerLocalId: thread.composer.localId,
+ isActive: true,
+ modelName: 'mail.channel_command',
+ recordLocalId: command.localId,
+ });
+
+ assert.containsOnce(
+ document.body,
+ '.o_ComposerSuggestion',
+ "Command suggestion should be present"
+ );
+ assert.containsOnce(
+ document.body,
+ '.o_ComposerSuggestion_part1',
+ "Command name should be present"
+ );
+ assert.strictEqual(
+ document.querySelector(`.o_ComposerSuggestion_part1`).textContent,
+ "whois",
+ "Command name should be displayed"
+ );
+ assert.containsOnce(
+ document.body,
+ '.o_ComposerSuggestion_part2',
+ "Command help should be present"
+ );
+ assert.strictEqual(
+ document.querySelector(`.o_ComposerSuggestion_part2`).textContent,
+ "Displays who it is",
+ "Command help should be displayed"
+ );
+});
+
+QUnit.test('command suggestion active', async function (assert) {
+ assert.expect(2);
+
+ this.data['mail.channel'].records.push({ id: 20 });
+ await this.start();
+ const thread = this.env.models['mail.thread'].findFromIdentifyingData({
+ id: 20,
+ model: 'mail.channel',
+ });
+ const command = this.env.models['mail.channel_command'].create({
+ name: 'whois',
+ help: "Displays who it is",
+ });
+ await this.createComposerSuggestion({
+ composerLocalId: thread.composer.localId,
+ isActive: true,
+ modelName: 'mail.channel_command',
+ recordLocalId: command.localId,
+ });
+
+ assert.containsOnce(
+ document.body,
+ '.o_ComposerSuggestion',
+ "Command suggestion should be displayed"
+ );
+ assert.hasClass(
+ document.querySelector('.o_ComposerSuggestion'),
+ 'active',
+ "should be active initially"
+ );
+});
+
+});
+});
+});
+
+});
diff --git a/addons/mail/static/src/components/partner_mention_suggestion/partner_mention_suggestion_tests.js b/addons/mail/static/src/components/composer_suggestion/composer_suggestion_partner_tests.js
index 6b0b7f9d3bf..548fd6d7963 100644
--- a/addons/mail/static/src/components/partner_mention_suggestion/partner_mention_suggestion_tests.js
+++ b/addons/mail/static/src/components/composer_suggestion/composer_suggestion_partner_tests.js
@@ -1,8 +1,8 @@
-odoo.define('mail/static/src/components/partner_mention_suggestion/partner_mention_suggestion_tests.js', function (require) {
+odoo.define('mail/static/src/components/composer_suggestion/composer_suggestion_partner_tests.js', function (require) {
'use strict';
const components = {
- PartnerMentionSuggestion: require('mail/static/src/components/partner_mention_suggestion/partner_mention_suggestion.js'),
+ ComposerSuggestion: require('mail/static/src/components/composer_suggestion/composer_suggestion.js'),
};
const {
afterEach,
@@ -13,14 +13,14 @@ const {
QUnit.module('mail', {}, function () {
QUnit.module('components', {}, function () {
-QUnit.module('partner_mention_suggestion', {}, function () {
-QUnit.module('partner_mention_suggestion_tests.js', {
+QUnit.module('composer_suggestion', {}, function () {
+QUnit.module('composer_suggestion_partner_tests.js', {
beforeEach() {
beforeEach(this);
- this.createPartnerMentionSuggestion = async partner => {
- await createRootComponent(this, components.PartnerMentionSuggestion, {
- props: { isActive: true, partnerLocalId: partner.localId },
+ this.createComposerSuggestion = async props => {
+ await createRootComponent(this, components.ComposerSuggestion, {
+ props,
target: this.widget.el,
});
};
@@ -41,17 +41,27 @@ QUnit.module('partner_mention_suggestion_tests.js', {
QUnit.test('partner mention suggestion displayed', async function (assert) {
assert.expect(1);
+ this.data['mail.channel'].records.push({ id: 20 });
await this.start();
+ const thread = this.env.models['mail.thread'].findFromIdentifyingData({
+ id: 20,
+ model: 'mail.channel',
+ });
const partner = this.env.models['mail.partner'].create({
id: 7,
im_status: 'online',
name: "Demo User",
});
- await this.createPartnerMentionSuggestion(partner);
+ await this.createComposerSuggestion({
+ composerLocalId: thread.composer.localId,
+ isActive: true,
+ modelName: 'mail.partner',
+ recordLocalId: partner.localId,
+ });
assert.containsOnce(
document.body,
- `.o_PartnerMentionSuggestion`,
+ `.o_ComposerSuggestion`,
"Partner mention suggestion should be present"
);
});
@@ -59,18 +69,28 @@ QUnit.test('partner mention suggestion displayed', async function (assert) {
QUnit.test('partner mention suggestion correct data', async function (assert) {
assert.expect(6);
+ this.data['mail.channel'].records.push({ id: 20 });
await this.start();
+ const thread = this.env.models['mail.thread'].findFromIdentifyingData({
+ id: 20,
+ model: 'mail.channel',
+ });
const partner = this.env.models['mail.partner'].create({
email: "demo_user@odoo.com",
id: 7,
im_status: 'online',
name: "Demo User",
});
- await this.createPartnerMentionSuggestion(partner);
+ await this.createComposerSuggestion({
+ composerLocalId: thread.composer.localId,
+ isActive: true,
+ modelName: 'mail.partner',
+ recordLocalId: partner.localId,
+ });
assert.containsOnce(
document.body,
- '.o_PartnerMentionSuggestion',
+ '.o_ComposerSuggestion',
"Partner mention suggestion should be present"
);
assert.strictEqual(
@@ -80,21 +100,21 @@ QUnit.test('partner mention suggestion correct data', async function (assert) {
);
assert.containsOnce(
document.body,
- '.o_PartnerMentionSuggestion_name',
+ '.o_ComposerSuggestion_part1',
"Partner's name should be present"
);
assert.strictEqual(
- document.querySelector(`.o_PartnerMentionSuggestion_name`).textContent,
+ document.querySelector(`.o_ComposerSuggestion_part1`).textContent,
"Demo User",
"Partner's name should be displayed"
);
assert.containsOnce(
document.body,
- '.o_PartnerMentionSuggestion_email',
+ '.o_ComposerSuggestion_part2',
"Partner's email should be present"
);
assert.strictEqual(
- document.querySelector(`.o_PartnerMentionSuggestion_email`).textContent,
+ document.querySelector(`.o_ComposerSuggestion_part2`).textContent,
"(demo_user@odoo.com)",
"Partner's email should be displayed"
);
@@ -103,22 +123,31 @@ QUnit.test('partner mention suggestion correct data', async function (assert) {
QUnit.test('partner mention suggestion active', async function (assert) {
assert.expect(2);
+ this.data['mail.channel'].records.push({ id: 20 });
await this.start();
+ const thread = this.env.models['mail.thread'].findFromIdentifyingData({
+ id: 20,
+ model: 'mail.channel',
+ });
const partner = this.env.models['mail.partner'].create({
- email: "demo_user@odoo.com",
id: 7,
im_status: 'online',
name: "Demo User",
});
- await this.createPartnerMentionSuggestion(partner);
+ await this.createComposerSuggestion({
+ composerLocalId: thread.composer.localId,
+ isActive: true,
+ modelName: 'mail.partner',
+ recordLocalId: partner.localId,
+ });
assert.containsOnce(
document.body,
- '.o_PartnerMentionSuggestion',
+ '.o_ComposerSuggestion',
"Partner mention suggestion should be displayed"
);
assert.hasClass(
- document.querySelector('.o_PartnerMentionSuggestion'),
+ document.querySelector('.o_ComposerSuggestion'),
'active',
"should be active initially"
);
diff --git a/addons/mail/static/src/components/composer_suggestion_list/composer_suggestion_list.js b/addons/mail/static/src/components/composer_suggestion_list/composer_suggestion_list.js
new file mode 100644
index 00000000000..c033feb28ed
--- /dev/null
+++ b/addons/mail/static/src/components/composer_suggestion_list/composer_suggestion_list.js
@@ -0,0 +1,74 @@
+odoo.define('mail/static/src/components/composer_suggestion_list/composer_suggestion_list.js', function (require) {
+'use strict';
+
+const components = {
+ ComposerSuggestion: require('mail/static/src/components/composer_suggestion/composer_suggestion.js'),
+};
+const useStore = require('mail/static/src/component_hooks/use_store/use_store.js');
+
+const { Component } = owl;
+
+class ComposerSuggestionList extends Component {
+
+ /**
+ * @override
+ */
+ constructor(...args) {
+ super(...args);
+ useStore(props => {
+ const composer = this.env.models['mail.composer'].get(props.composerLocalId);
+ const activeSuggestedRecord = composer
+ ? composer.activeSuggestedRecord
+ : undefined;
+ const extraSuggestedRecordsList = composer
+ ? composer.extraSuggestedRecordsList
+ : [];
+ const mainSuggestedRecordsList = composer
+ ? composer.mainSuggestedRecordsList
+ : [];
+ return {
+ activeSuggestedRecord: activeSuggestedRecord ? activeSuggestedRecord.__state : undefined,
+ composer: composer ? composer.__state : undefined,
+ extraSuggestedRecordsList: extraSuggestedRecordsList
+ ? extraSuggestedRecordsList.map(record => record.__state)
+ : [],
+ mainSuggestedRecordsList: mainSuggestedRecordsList
+ ? mainSuggestedRecordsList.map(record => record.__state)
+ : [],
+ };
+ }, {
+ compareDepth: {
+ extraSuggestedRecordsList: 1,
+ mainSuggestedRecordsList: 1,
+ },
+ });
+ }
+
+ //--------------------------------------------------------------------------
+ // Public
+ //--------------------------------------------------------------------------
+
+ /**
+ * @returns {mail.composer}
+ */
+ get composer() {
+ return this.env.models['mail.composer'].get(this.props.composerLocalId);
+ }
+
+}
+
+Object.assign(ComposerSuggestionList, {
+ components,
+ defaultProps: {
+ isBelow: false,
+ },
+ props: {
+ composerLocalId: String,
+ isBelow: Boolean,
+ },
+ template: 'mail.ComposerSuggestionList',
+});
+
+return ComposerSuggestionList;
+
+});
diff --git a/addons/mail/static/src/components/composer_suggestion_list/composer_suggestion_list.scss b/addons/mail/static/src/components/composer_suggestion_list/composer_suggestion_list.scss
new file mode 100644
index 00000000000..9cb509c2380
--- /dev/null
+++ b/addons/mail/static/src/components/composer_suggestion_list/composer_suggestion_list.scss
@@ -0,0 +1,11 @@
+// ------------------------------------------------------------------
+// Layout
+// ------------------------------------------------------------------
+
+.o_ComposerSuggestionList {
+ position: absolute;
+
+ &.o-lowPosition {
+ bottom: 0;
+ }
+}
diff --git a/addons/mail/static/src/components/composer_suggestion_list/composer_suggestion_list.xml b/addons/mail/static/src/components/composer_suggestion_list/composer_suggestion_list.xml
new file mode 100644
index 00000000000..e38f9cca8c7
--- /dev/null
+++ b/addons/mail/static/src/components/composer_suggestion_list/composer_suggestion_list.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<templates xml:space="preserve">
+
+ <t t-name="mail.ComposerSuggestionList" owl="1">
+ <div class="o_ComposerSuggestionList" t-att-class="{ 'o-lowPosition': props.isBelow }">
+ <div t-att-class="{ 'dropdown': props.isBelow, 'dropup': !props.isBelow }">
+ <div class="o_ComposerSuggestionList_list dropdown-menu show">
+ <t t-foreach="composer.mainSuggestedRecordsList" t-as="record" t-key="record.localId">
+ <ComposerSuggestion
+ composerLocalId="props.composerLocalId"
+ isActive="record === composer.activeSuggestedRecord"
+ modelName="composer.suggestionModelName"
+ recordLocalId="record.localId"
+ />
+ </t>
+ <t t-if="composer.mainSuggestedRecordsList.length > 0 and composer.extraSuggestedRecordsList.length > 0">
+ <div role="separator" class="dropdown-divider"/>
+ </t>
+ <t t-foreach="composer.extraSuggestedRecordsList" t-as="record" t-key="record.localId">
+ <ComposerSuggestion
+ composerLocalId="props.composerLocalId"
+ isActive="record === composer.activeSuggestedRecord"
+ modelName="composer.suggestionModelName"
+ recordLocalId="record.localId"
+ />
+ </t>
+ </div>
+ </div>
+ </div>
+ </t>
+
+</templates>
diff --git a/addons/mail/static/src/components/composer_text_input/composer_text_input.js b/addons/mail/static/src/components/composer_text_input/composer_text_input.js
index ef21d023cb5..10cdd7ff0a7 100644
--- a/addons/mail/static/src/components/composer_text_input/composer_text_input.js
+++ b/addons/mail/static/src/components/composer_text_input/composer_text_input.js
@@ -4,7 +4,7 @@ odoo.define('mail/static/src/components/composer_text_input/composer_text_input.
const useStore = require('mail/static/src/component_hooks/use_store/use_store.js');
const components = {
- PartnerMentionSuggestion: require('mail/static/src/components/partner_mention_suggestion/partner_mention_suggestion.js'),
+ ComposerSuggestionList: require('mail/static/src/components/composer_suggestion_list/composer_suggestion_list.js'),
};
const { markEventHandled } = require('mail/static/src/utils/utils.js');
@@ -208,10 +208,10 @@ class ComposerTextInput extends Component {
_onKeydownTextarea(ev) {
switch (ev.key) {
case 'Escape':
- if (this.composer.hasSuggestedPartners) {
+ if (this.composer.hasSuggestions) {
ev.preventDefault();
- this.composer.closeMentionSuggestions();
- markEventHandled(ev, 'ComposerTextInput.closeMentionSuggestions');
+ this.composer.closeSuggestions();
+ markEventHandled(ev, 'ComposerTextInput.closeSuggestions');
}
break;
// UP, DOWN, TAB: prevent moving cursor if navigation in mention suggestions
@@ -222,7 +222,7 @@ class ComposerTextInput extends Component {
case 'Home':
case 'End':
case 'Tab':
- if (this.composer.hasSuggestedPartners) {
+ if (this.composer.hasSuggestions) {
// We use preventDefault here to avoid keys native actions but actions are handled in keyUp
ev.preventDefault();
}
@@ -239,7 +239,7 @@ class ComposerTextInput extends Component {
* @param {KeyboardEvent} ev
*/
_onKeydownTextareaEnter(ev) {
- if (this.composer.hasSuggestedPartners) {
+ if (this.composer.hasSuggestions) {
ev.preventDefault();
return;
}
@@ -292,72 +292,50 @@ class ComposerTextInput extends Component {
break;
// ENTER, HOME, END, UP, DOWN, PAGE UP, PAGE DOWN, TAB: check if navigation in mention suggestions
case 'Enter':
- if (this.composer.hasSuggestedPartners) {
- if (this.composer.activeSuggestedPartner) {
- this.composer.insertMentionedPartner(this.composer.activeSuggestedPartner);
- this.composer.closeMentionSuggestions();
- this.focus();
- }
+ if (this.composer.hasSuggestions) {
+ this.composer.insertSuggestion();
+ this.composer.closeSuggestions();
+ this.focus();
}
break;
case 'ArrowUp':
case 'PageUp':
- if (this.composer.hasSuggestedPartners) {
- this.composer.setPreviousSuggestedPartnerActive();
+ if (this.composer.hasSuggestions) {
+ this.composer.setPreviousSuggestionActive();
}
break;
case 'ArrowDown':
case 'PageDown':
- if (this.composer.hasSuggestedPartners) {
- this.composer.setNextSuggestedPartnerActive();
+ if (this.composer.hasSuggestions) {
+ this.composer.setNextSuggestionActive();
}
break;
case 'Home':
- if (this.composer.hasSuggestedPartners) {
- this.composer.setFirstSuggestedPartnerActive();
+ if (this.composer.hasSuggestions) {
+ this.composer.setFirstSuggestionActive();
}
break;
case 'End':
- if (this.composer.hasSuggestedPartners) {
- this.composer.setLastSuggestedPartnerActive();
+ if (this.composer.hasSuggestions) {
+ this.composer.setLastSuggestionActive();
}
break;
case 'Tab':
- if (this.composer.hasSuggestedPartners) {
+ if (this.composer.hasSuggestions) {
if (ev.shiftKey) {
- this.composer.setPreviousSuggestedPartnerActive();
+ this.composer.setPreviousSuggestionActive();
} else {
- this.composer.setNextSuggestedPartnerActive();
+ this.composer.setNextSuggestionActive();
}
}
break;
// Otherwise, check if a mention is typed
default:
this.saveStateInStore();
- this.composer._detectDelimiter();
+ this.composer.detectSuggestionDelimiter();
}
}
- /**
- * @private
- * @param {Event} ev
- */
- _onPartnerMentionSuggestionClicked(ev) {
- this.composer.insertMentionedPartner(ev.detail.partner);
- this.composer.closeMentionSuggestions();
- this.focus();
- }
-
- /**
- * @private
- * @param {MouseEvent} ev
- */
- _onPartnerMentionSuggestionMouseOver(ev) {
- this.composer.update({
- activeSuggestedPartner: [['link', ev.detail.partner]],
- });
- }
-
}
Object.assign(ComposerTextInput, {
diff --git a/addons/mail/static/src/components/composer_text_input/composer_text_input.xml b/addons/mail/static/src/components/composer_text_input/composer_text_input.xml
index 32fff1289eb..a834a61f1b7 100644
--- a/addons/mail/static/src/components/composer_text_input/composer_text_input.xml
+++ b/addons/mail/static/src/components/composer_text_input/composer_text_input.xml
@@ -4,37 +4,10 @@
<t t-name="mail.ComposerTextInput" owl="1">
<div class="o_ComposerTextInput">
<t t-if="composer">
- <t t-if="composer.hasSuggestedPartners">
- <div class="o_ComposerTextInput_mentionDropdown" t-att-class="{ 'o-lowPosition': props.hasMentionSuggestionsBelowPosition }">
- <div
- t-att-class="{
- 'dropdown': props.hasMentionSuggestionsBelowPosition,
- 'dropup': !props.hasMentionSuggestionsBelowPosition,
- }"
- >
- <div class="o_ComposerTextInput_mentionDropdownPropositionList dropdown-menu show">
- <t t-foreach="composer.mainSuggestedPartners" t-as="mainSuggestedPartner" t-key="mainSuggestedPartner.localId">
- <PartnerMentionSuggestion
- isActive="mainSuggestedPartner === composer.activeSuggestedPartner"
- partnerLocalId="mainSuggestedPartner.localId"
- t-on-o-partner-mention-suggestion-clicked="_onPartnerMentionSuggestionClicked"
- t-on-o-partner-mention-suggestion-mouse-over="_onPartnerMentionSuggestionMouseOver"
- />
- </t>
- <t t-if="composer.mainSuggestedPartners.length > 0 and composer.extraSuggestedPartners.length > 0">
- <div role="separator" class="dropdown-divider"/>
- </t>
- <t t-foreach="composer.extraSuggestedPartners" t-as="extraSuggestedPartner" t-key="extraSuggestedPartner.localId">
- <PartnerMentionSuggestion
- isActive="extraSuggestedPartner === composer.activeSuggestedPartner"
- partnerLocalId="extraSuggestedPartner.localId"
- t-on-o-partner-mention-suggestion-clicked="_onPartnerMentionSuggestionClicked"
- t-on-o-partner-mention-suggestion-mouse-over="_onPartnerMentionSuggestionMouseOver"
- />
- </t>
- </div>
- </div>
- </div>
+ <t t-if="composer.hasSuggestions">
+ <ComposerSuggestionList
+ composerLocalId="props.composerLocalId"
+ />
</t>
<textarea class="o_ComposerTextInput_textarea" t-att-class="{ 'o-composer-is-compact': props.isCompact }" t-esc="composer.textInputContent" t-att-placeholder="textareaPlaceholder" t-on-focusin="_onFocusinTextarea" t-on-focusout="_onFocusoutTextarea" t-on-keydown="_onKeydownTextarea" t-on-keyup="_onKeyupTextarea" t-on-input="_onInputTextarea" t-ref="textarea"/>
</t>
diff --git a/addons/mail/static/src/components/discuss/tests/discuss_inbox_tests.js b/addons/mail/static/src/components/discuss/tests/discuss_inbox_tests.js
index 5f24acec192..00cd5d2c310 100644
--- a/addons/mail/static/src/components/discuss/tests/discuss_inbox_tests.js
+++ b/addons/mail/static/src/components/discuss/tests/discuss_inbox_tests.js
@@ -90,15 +90,26 @@ QUnit.test('reply: discard on pressing escape', async function (assert) {
);
await afterNextRender(() => {
+ document.querySelector(`.o_ComposerTextInput_textarea`).focus();
document.execCommand('insertText', false, "@");
document.querySelector(`.o_ComposerTextInput_textarea`)
.dispatchEvent(new window.KeyboardEvent('keydown'));
document.querySelector(`.o_ComposerTextInput_textarea`)
.dispatchEvent(new window.KeyboardEvent('keyup'));
+ document.execCommand('insertText', false, "T");
+ document.querySelector(`.o_ComposerTextInput_textarea`)
+ .dispatchEvent(new window.KeyboardEvent('keydown'));
+ document.querySelector(`.o_ComposerTextInput_textarea`)
+ .dispatchEvent(new window.KeyboardEvent('keyup'));
+ document.execCommand('insertText', false, "e");
+ document.querySelector(`.o_ComposerTextInput_textarea`)
+ .dispatchEvent(new window.KeyboardEvent('keydown'));
+ document.querySelector(`.o_ComposerTextInput_textarea`)
+ .dispatchEvent(new window.KeyboardEvent('keyup'));
});
assert.containsOnce(
document.body,
- '.o_ComposerTextInput_mentionDropdownPropositionList',
+ '.o_ComposerSuggestion',
"mention suggestion should be opened after typing @"
);
@@ -108,7 +119,7 @@ QUnit.test('reply: discard on pressing escape', async function (assert) {
});
assert.containsNone(
document.body,
- '.o_ComposerTextInput_mentionDropdownPropositionList',
+ '.o_ComposerSuggestion',
"mention suggestion should be closed after pressing escape on mention suggestion"
);
assert.containsOnce(
diff --git a/addons/mail/static/src/components/message_list/message_list.js b/addons/mail/static/src/components/message_list/message_list.js
index 3189b97b627..bc1ab67bbfc 100644
--- a/addons/mail/static/src/components/message_list/message_list.js
+++ b/addons/mail/static/src/components/message_list/message_list.js
@@ -357,8 +357,8 @@ class MessageList extends Component {
*/
async _adjustFromCurrentPartnerJustPostedMessage(hint) {
const threadCache = this.threadView.threadCache;
- const { messageId } = hint.data;
- if (threadCache.isLoaded) {
+ if (threadCache.isLoaded && hint.data) {
+ const { messageId } = hint.data;
const threadCacheMessageIds = threadCache.messages.map(message => message.id);
if (threadCacheMessageIds.includes(messageId) && this.messageRefFromId(messageId)) {
if (this.props.hasScrollAdjust) {
diff --git a/addons/mail/static/src/components/partner_mention_suggestion/partner_mention_suggestion.js b/addons/mail/static/src/components/partner_mention_suggestion/partner_mention_suggestion.js
deleted file mode 100644
index 7c39f148264..00000000000
--- a/addons/mail/static/src/components/partner_mention_suggestion/partner_mention_suggestion.js
+++ /dev/null
@@ -1,79 +0,0 @@
-odoo.define('mail/static/src/components/partner_mention_suggestion/partner_mention_suggestion.js', function (require) {
-'use strict';
-
-const useStore = require('mail/static/src/component_hooks/use_store/use_store.js');
-
-const components = {
- PartnerImStatusIcon: require('mail/static/src/components/partner_im_status_icon/partner_im_status_icon.js'),
-};
-
-const { Component } = owl;
-
-class PartnerMentionSuggestion extends Component {
-
- /**
- * @override
- */
- constructor(...args) {
- super(...args);
- useStore(props => {
- const partner = this.env.models['mail.partner'].get(props.partnerLocalId);
- return {
- partner: partner ? partner.__state : undefined,
- };
- });
- }
-
- //--------------------------------------------------------------------------
- // Public
- //--------------------------------------------------------------------------
-
- /**
- * @returns {mail.partner}
- */
- get partner() {
- return this.env.models['mail.partner'].get(this.props.partnerLocalId);
- }
-
- //--------------------------------------------------------------------------
- // Handlers
- //--------------------------------------------------------------------------
-
- /**
- * @private
- * @param {Event} ev
- */
- _onClick(ev) {
- // avoid following dummy href
- ev.preventDefault();
- this.trigger('o-partner-mention-suggestion-clicked', {
- partner: this.partner,
- });
- }
-
- /**
- * @private
- * @param {Event} ev
- */
- _onMouseOver(ev) {
- this.trigger('o-partner-mention-suggestion-mouse-over', {
- partner: this.partner,
- });
- }
-}
-
-Object.assign(PartnerMentionSuggestion, {
- components,
- defaultProps: {
- isActive: false,
- },
- props: {
- isActive: Boolean,
- partnerLocalId: String,
- },
- template: 'mail.PartnerMentionSuggestion',
-});
-
-return PartnerMentionSuggestion;
-
-});
diff --git a/addons/mail/static/src/components/partner_mention_suggestion/partner_mention_suggestion.xml b/addons/mail/static/src/components/partner_mention_suggestion/partner_mention_suggestion.xml
deleted file mode 100644
index a45e991ab19..00000000000
--- a/addons/mail/static/src/components/partner_mention_suggestion/partner_mention_suggestion.xml
+++ /dev/null
@@ -1,19 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<templates xml:space="preserve">
-
- <t t-name="mail.PartnerMentionSuggestion" owl="1">
- <a class="o_PartnerMentionSuggestion dropdown-item" t-att-class="{ 'active': props.isActive }" href="#" role="menuitem" t-on-click="_onClick" t-on-mouseover="_onMouseOver">
- <t t-if="partner">
- <PartnerImStatusIcon
- hasBackground="false"
- partnerLocalId="partner.localId"
- />
- <span class="o_PartnerMentionSuggestion_name"><t t-esc="partner.nameOrDisplayName"/></span>
- <t t-if="partner.email">
- <span class="o_PartnerMentionSuggestion_email">(<t t-esc="partner.email"/>)</span>
- </t>
- </t>
- </a>
- </t>
-
-</templates>