[IMP] mail: reintroduce channel mentions, canned responses, commands
task-2280308 closes odoo/odoo#58167 X-original-commit: b63d0d2ee5f3bff24a5e1e1cd962d59bebef082d Related: odoo/enterprise#13393 Signed-off-by: Sébastien Theys (seb) <seb@odoo.com>
This commit is contained in:
committed by
Sébastien Theys
parent
043dcba4e0
commit
9fc3fe1dba
@@ -29,7 +29,7 @@ QUnit.module('composer_tests.js', {
|
||||
};
|
||||
|
||||
this.start = async params => {
|
||||
let { env, widget } = await start(Object.assign({}, params, {
|
||||
const { env, widget } = await start(Object.assign({}, params, {
|
||||
data: this.data,
|
||||
}));
|
||||
this.env = env;
|
||||
@@ -38,13 +38,6 @@ QUnit.module('composer_tests.js', {
|
||||
},
|
||||
afterEach() {
|
||||
afterEach(this);
|
||||
if (this.component) {
|
||||
this.component.destroy();
|
||||
}
|
||||
if (this.widget) {
|
||||
this.widget.destroy();
|
||||
}
|
||||
this.env = undefined;
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -77,6 +77,8 @@
|
||||
'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_suggestion/composer_suggestion.xml',
|
||||
'static/src/components/composer_suggestion_list/composer_suggestion_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',
|
||||
@@ -109,7 +111,6 @@
|
||||
'static/src/components/notification_popover/notification_popover.xml',
|
||||
'static/src/components/notification_request/notification_request.xml',
|
||||
'static/src/components/partner_im_status_icon/partner_im_status_icon.xml',
|
||||
'static/src/components/partner_mention_suggestion/partner_mention_suggestion.xml',
|
||||
'static/src/components/thread_icon/thread_icon.xml',
|
||||
'static/src/components/thread_needaction_preview/thread_needaction_preview.xml',
|
||||
'static/src/components/thread_preview/thread_preview.xml',
|
||||
|
||||
@@ -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')) {
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -261,6 +261,13 @@ class Composer extends Component {
|
||||
this._postMessage();
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_onComposerSuggestionClicked() {
|
||||
this.focus();
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
@@ -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')) {
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -274,56 +274,56 @@ 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['res.partner'].records.push({
|
||||
email: "testpartnert@odoo.com",
|
||||
this.data['mail.shortcode'].records.push({
|
||||
id: 11,
|
||||
name: "TestPartner",
|
||||
source: "hello",
|
||||
substitution: "Hello! How are you?",
|
||||
});
|
||||
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',
|
||||
"mention suggestions list should not be present"
|
||||
'.o_ComposerSuggestionList_list',
|
||||
"Canned responses suggestions list should not be present"
|
||||
);
|
||||
await afterNextRender(() => {
|
||||
document.querySelector(`.o_ComposerTextInput_textarea`).focus();
|
||||
document.execCommand('insertText', false, "@");
|
||||
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_ComposerTextInput_mentionDropdownPropositionList'),
|
||||
document.querySelector('.o_ComposerSuggestionList_list'),
|
||||
'show',
|
||||
"should display mention suggestions on typing '@'"
|
||||
"should display canned response suggestions on typing ':'"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test('mention a partner', async function (assert) {
|
||||
QUnit.test('use a canned response', async function (assert) {
|
||||
assert.expect(4);
|
||||
|
||||
this.data['res.partner'].records.push({
|
||||
email: "testpartnert@odoo.com",
|
||||
this.data['mail.shortcode'].records.push({
|
||||
id: 11,
|
||||
name: "TestPartner",
|
||||
source: "hello",
|
||||
substitution: "Hello! How are you?",
|
||||
});
|
||||
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',
|
||||
"mention suggestions list should not be present"
|
||||
'.o_ComposerSuggestionList_list',
|
||||
"canned response suggestions list should not be present"
|
||||
);
|
||||
assert.strictEqual(
|
||||
document.querySelector(`.o_ComposerTextInput_textarea`).value,
|
||||
@@ -332,7 +332,7 @@ QUnit.test('mention a partner', async function (assert) {
|
||||
);
|
||||
await afterNextRender(() => {
|
||||
document.querySelector(`.o_ComposerTextInput_textarea`).focus();
|
||||
document.execCommand('insertText', false, "@Test");
|
||||
document.execCommand('insertText', false, ":");
|
||||
});
|
||||
await afterNextRender(() => {
|
||||
document.querySelector(`.o_ComposerTextInput_textarea`)
|
||||
@@ -342,36 +342,36 @@ QUnit.test('mention a partner', async function (assert) {
|
||||
});
|
||||
assert.containsOnce(
|
||||
document.body,
|
||||
'.o_PartnerMentionSuggestion',
|
||||
"should have a mention suggestion"
|
||||
'.o_ComposerSuggestion',
|
||||
"should have a canned response suggestion"
|
||||
);
|
||||
await afterNextRender(() =>
|
||||
document.querySelector('.o_PartnerMentionSuggestion').click()
|
||||
document.querySelector('.o_ComposerSuggestion').click()
|
||||
);
|
||||
assert.strictEqual(
|
||||
document.querySelector(`.o_ComposerTextInput_textarea`).value.replace(/\s/, " "),
|
||||
"@TestPartner ",
|
||||
"text content of composer should have mentioned partner + additional whitespace afterwards"
|
||||
"Hello! How are you? ",
|
||||
"text content of composer should have canned response + additional whitespace afterwards"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test('mention a partner after some text', async function (assert) {
|
||||
QUnit.test('use a canned response some text', async function (assert) {
|
||||
assert.expect(5);
|
||||
|
||||
this.data['res.partner'].records.push({
|
||||
email: "testpartnert@odoo.com",
|
||||
this.data['mail.shortcode'].records.push({
|
||||
id: 11,
|
||||
name: "TestPartner",
|
||||
source: "hello",
|
||||
substitution: "Hello! How are you?",
|
||||
});
|
||||
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',
|
||||
"mention suggestions list should not be present"
|
||||
'.o_ComposerSuggestion',
|
||||
"canned response suggestions list should not be present"
|
||||
);
|
||||
assert.strictEqual(
|
||||
document.querySelector(`.o_ComposerTextInput_textarea`).value,
|
||||
@@ -388,7 +388,7 @@ QUnit.test('mention a partner after some text', async function (assert) {
|
||||
"text content of composer should have content"
|
||||
);
|
||||
await afterNextRender(() =>
|
||||
document.execCommand('insertText', false, "@Test")
|
||||
document.execCommand('insertText', false, ":")
|
||||
);
|
||||
await afterNextRender(() => {
|
||||
document.querySelector(`.o_ComposerTextInput_textarea`)
|
||||
@@ -398,11 +398,609 @@ QUnit.test('mention a partner after some text', async function (assert) {
|
||||
});
|
||||
assert.containsOnce(
|
||||
document.body,
|
||||
'.o_PartnerMentionSuggestion',
|
||||
'.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({
|
||||
id: 11,
|
||||
email: "testpartner@odoo.com",
|
||||
name: "TestPartner",
|
||||
});
|
||||
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_ComposerSuggestionList_list',
|
||||
"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 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: "testpartner@odoo.com",
|
||||
name: "TestPartner",
|
||||
});
|
||||
await this.start();
|
||||
const composer = this.env.models['mail.composer'].create();
|
||||
await this.createComposerComponent(composer);
|
||||
|
||||
assert.containsNone(
|
||||
document.body,
|
||||
'.o_ComposerSuggestionList_list',
|
||||
"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, "@");
|
||||
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_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/, " "),
|
||||
"@TestPartner ",
|
||||
"text content of composer should have mentioned partner + additional whitespace afterwards"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test('mention a partner after some text', async function (assert) {
|
||||
assert.expect(5);
|
||||
|
||||
this.data['res.partner'].records.push({
|
||||
email: "testpartner@odoo.com",
|
||||
name: "TestPartner",
|
||||
});
|
||||
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"
|
||||
);
|
||||
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.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_ComposerSuggestion',
|
||||
"should have a mention suggestion"
|
||||
);
|
||||
await afterNextRender(() =>
|
||||
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/, " "),
|
||||
|
||||
@@ -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;
|
||||
|
||||
});
|
||||
+7
-7
@@ -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 {
|
||||
.o_ComposerSuggestion_part1 {
|
||||
font-weight: $font-weight-bold;
|
||||
}
|
||||
|
||||
.o_ComposerSuggestion_part2 {
|
||||
font-style: italic;
|
||||
color: $gray-600;
|
||||
}
|
||||
|
||||
.o_PartnerMentionSuggestion_name {
|
||||
font-weight: $font-weight-bold;
|
||||
}
|
||||
@@ -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>
|
||||
+154
@@ -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"
|
||||
);
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
+141
@@ -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"
|
||||
);
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
+151
@@ -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"
|
||||
);
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
+48
-19
@@ -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"
|
||||
);
|
||||
+74
@@ -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;
|
||||
|
||||
});
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
// ------------------------------------------------------------------
|
||||
// Layout
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
.o_ComposerSuggestionList {
|
||||
position: absolute;
|
||||
|
||||
&.o-lowPosition {
|
||||
bottom: 0;
|
||||
}
|
||||
}
|
||||
+32
@@ -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>
|
||||
@@ -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, {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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) {
|
||||
|
||||
-79
@@ -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;
|
||||
|
||||
});
|
||||
-19
@@ -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>
|
||||
@@ -0,0 +1,31 @@
|
||||
odoo.define('mail/static/src/models/canned_response/canned_response.js', function (require) {
|
||||
'use strict';
|
||||
|
||||
const { registerNewModel } = require('mail/static/src/model/model_core.js');
|
||||
const { attr } = require('mail/static/src/model/model_field.js');
|
||||
|
||||
function factory(dependencies) {
|
||||
|
||||
class CannedResponse extends dependencies['mail.model'] {}
|
||||
|
||||
CannedResponse.fields = {
|
||||
id: attr(),
|
||||
/**
|
||||
* The keyword to use a specific canned response.
|
||||
*/
|
||||
source: attr(),
|
||||
/**
|
||||
* The canned response itself which will replace the keyword previously
|
||||
* entered.
|
||||
*/
|
||||
substitution: attr(),
|
||||
};
|
||||
|
||||
CannedResponse.modelName = 'mail.canned_response';
|
||||
|
||||
return CannedResponse;
|
||||
}
|
||||
|
||||
registerNewModel('mail.canned_response', factory);
|
||||
|
||||
});
|
||||
@@ -0,0 +1,33 @@
|
||||
odoo.define('mail/static/src/models/channel_command/channel_command.js', function (require) {
|
||||
'use strict';
|
||||
|
||||
const { registerNewModel } = require('mail/static/src/model/model_core.js');
|
||||
const { attr } = require('mail/static/src/model/model_field.js');
|
||||
|
||||
function factory(dependencies) {
|
||||
|
||||
class ChannelCommand extends dependencies['mail.model'] {}
|
||||
|
||||
ChannelCommand.fields = {
|
||||
/**
|
||||
* FIXME use this value task-2343850
|
||||
*/
|
||||
channel_types: attr(),
|
||||
/**
|
||||
* The command that will be executed.
|
||||
*/
|
||||
help: attr(),
|
||||
/**
|
||||
* The keyword to use a specific command.
|
||||
*/
|
||||
name: attr(),
|
||||
};
|
||||
|
||||
ChannelCommand.modelName = 'mail.channel_command';
|
||||
|
||||
return ChannelCommand;
|
||||
}
|
||||
|
||||
registerNewModel('mail.channel_command', factory);
|
||||
|
||||
});
|
||||
@@ -20,14 +20,78 @@ function factory(dependencies) {
|
||||
// Public
|
||||
//----------------------------------------------------------------------
|
||||
|
||||
closeMentionSuggestions() {
|
||||
closeSuggestions() {
|
||||
if (this.activeSuggestedRecordName) {
|
||||
this.update({
|
||||
[this.activeSuggestedRecordName]: [['unlink']],
|
||||
});
|
||||
}
|
||||
if (this.extraSuggestedRecordsListName) {
|
||||
this.update({
|
||||
[this.extraSuggestedRecordsListName]: [['unlink-all']],
|
||||
});
|
||||
}
|
||||
if (this.mainSuggestedRecordsListName) {
|
||||
this.update({
|
||||
[this.mainSuggestedRecordsListName]: [['unlink-all']],
|
||||
});
|
||||
}
|
||||
this.update({
|
||||
activeSuggestedPartner: [['unlink']],
|
||||
extraSuggestedPartners: [['unlink-all']],
|
||||
mainSuggestedPartners: [['unlink-all']],
|
||||
activeSuggestedRecordName: "",
|
||||
extraSuggestedRecordsListName: "",
|
||||
mainSuggestedRecordsListName: "",
|
||||
suggestionDelimiter: "",
|
||||
});
|
||||
}
|
||||
|
||||
detectSuggestionDelimiter() {
|
||||
const lastInputChar = this.textInputContent.substring(this.textInputCursorStart - 1, this.textInputCursorStart);
|
||||
const suggestionDelimiters = ['@', ':', '#', '/'];
|
||||
if (suggestionDelimiters.includes(lastInputChar) && !this.hasSuggestions) {
|
||||
this.update({ suggestionDelimiter: lastInputChar });
|
||||
}
|
||||
const mentionKeyword = this._validateMentionKeyword(false);
|
||||
if (mentionKeyword !== false) {
|
||||
switch (this.suggestionDelimiter) {
|
||||
case '@':
|
||||
this.update({
|
||||
activeSuggestedRecordName: "activeSuggestedPartner",
|
||||
extraSuggestedRecordsListName: "extraSuggestedPartners",
|
||||
mainSuggestedRecordsListName: "mainSuggestedPartners",
|
||||
suggestionModelName: "mail.partner",
|
||||
});
|
||||
this._updateSuggestedPartners(mentionKeyword);
|
||||
break;
|
||||
case ':':
|
||||
this.update({
|
||||
activeSuggestedRecordName: "activeSuggestedCannedResponse",
|
||||
mainSuggestedRecordsListName: "suggestedCannedResponses",
|
||||
suggestionModelName: "mail.canned_response",
|
||||
});
|
||||
this._updateSuggestedCannedResponses(mentionKeyword);
|
||||
break;
|
||||
case '/':
|
||||
this.update({
|
||||
activeSuggestedRecordName: "activeSuggestedChannelCommand",
|
||||
mainSuggestedRecordsListName: "suggestedChannelCommands",
|
||||
suggestionModelName: "mail.channel_command",
|
||||
});
|
||||
this._updateSuggestedChannelCommands(mentionKeyword);
|
||||
break;
|
||||
case '#':
|
||||
this.update({
|
||||
activeSuggestedRecordName: "activeSuggestedChannel",
|
||||
mainSuggestedRecordsListName: "suggestedChannels",
|
||||
suggestionModelName: "mail.thread",
|
||||
});
|
||||
this._updateSuggestedChannels(mentionKeyword);
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
this.closeSuggestions();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Hides the composer, which only makes sense if the composer is
|
||||
* currently used as a Discuss Inbox reply composer.
|
||||
@@ -56,25 +120,51 @@ function factory(dependencies) {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {mail.partner} partner
|
||||
*/
|
||||
insertMentionedPartner(partner) {
|
||||
insertSuggestion() {
|
||||
const cursorPosition = this.textInputCursorStart;
|
||||
const textLeft = this.textInputContent.substring(
|
||||
let textLeft = this.textInputContent.substring(
|
||||
0,
|
||||
this.textInputContent.substring(0, cursorPosition).lastIndexOf('@') + 1
|
||||
this.textInputContent.substring(0, cursorPosition).lastIndexOf(this.suggestionDelimiter) + 1
|
||||
);
|
||||
const textRight = this.textInputContent.substring(
|
||||
let textRight = this.textInputContent.substring(
|
||||
cursorPosition,
|
||||
this.textInputContent.length
|
||||
);
|
||||
const partnerName = partner.name.replace(/ /g, '\u00a0');
|
||||
if (this.suggestionDelimiter === ':') {
|
||||
textLeft = this.textInputContent.substring(
|
||||
0,
|
||||
this.textInputContent.substring(0, cursorPosition).lastIndexOf(this.suggestionDelimiter)
|
||||
);
|
||||
textRight = this.textInputContent.substring(
|
||||
cursorPosition,
|
||||
this.textInputContent.length
|
||||
);
|
||||
}
|
||||
let recordReplacement = "";
|
||||
switch (this.activeSuggestedRecordName) {
|
||||
case 'activeSuggestedCannedResponse':
|
||||
recordReplacement = this[this.activeSuggestedRecordName].substitution;
|
||||
break;
|
||||
case 'activeSuggestedChannel':
|
||||
recordReplacement = this[this.activeSuggestedRecordName].name;
|
||||
this.update({
|
||||
mentionedChannels: [['link', this[this.activeSuggestedRecordName]]],
|
||||
});
|
||||
break;
|
||||
case 'activeSuggestedChannelCommand':
|
||||
recordReplacement = this[this.activeSuggestedRecordName].name;
|
||||
break;
|
||||
case 'activeSuggestedPartner':
|
||||
recordReplacement = this[this.activeSuggestedRecordName].name.replace(/ /g, '\u00a0');
|
||||
this.update({
|
||||
mentionedPartners: [['link', this[this.activeSuggestedRecordName]]],
|
||||
});
|
||||
break;
|
||||
}
|
||||
this.update({
|
||||
mentionedPartners: [['link', partner]],
|
||||
textInputContent: textLeft + partnerName + ' ' + textRight,
|
||||
textInputCursorEnd: textLeft.length + partnerName.length + 1,
|
||||
textInputCursorStart: textLeft.length + partnerName.length + 1,
|
||||
textInputContent: textLeft + recordReplacement + ' ' + textRight,
|
||||
textInputCursorEnd: textLeft.length + recordReplacement.length + 1,
|
||||
textInputCursorStart: textLeft.length + recordReplacement.length + 1,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -150,6 +240,7 @@ function factory(dependencies) {
|
||||
let postData = {
|
||||
attachment_ids: this.attachments.map(attachment => attachment.id),
|
||||
body,
|
||||
channel_ids: this.mentionedChannels.map(channel => channel.id),
|
||||
context: {
|
||||
mail_post_autofollow: true,
|
||||
},
|
||||
@@ -223,71 +314,91 @@ function factory(dependencies) {
|
||||
}
|
||||
}
|
||||
|
||||
setFirstSuggestedPartnerActive() {
|
||||
if (!this.allSuggestedPartners[0]) {
|
||||
return;
|
||||
setFirstSuggestionActive() {
|
||||
if (!this[this.mainSuggestedRecordsListName][0]) {
|
||||
if (!this[this.extraSuggestedRecordsListName][0]) {
|
||||
return;
|
||||
}
|
||||
this.update({
|
||||
[this.activeSuggestedRecordName]: [['link', this[this.extraSuggestedRecordsListName][0]]],
|
||||
});
|
||||
} else {
|
||||
this.update({
|
||||
[this.activeSuggestedRecordName]: [['link', this[this.mainSuggestedRecordsListName][0]]],
|
||||
});
|
||||
}
|
||||
this.update({
|
||||
activeSuggestedPartner: [['link', this.allSuggestedPartners[0]]],
|
||||
});
|
||||
}
|
||||
|
||||
setLastSuggestedPartnerActive() {
|
||||
if (this.allSuggestedPartners.length === 0) {
|
||||
return;
|
||||
setLastSuggestionActive() {
|
||||
if (this[this.extraSuggestedRecordsListName].length === 0) {
|
||||
if (this[this.mainSuggestedRecordsListName].length === 0) {
|
||||
return;
|
||||
}
|
||||
this.update({
|
||||
[this.activeSuggestedRecordName]: [[
|
||||
'link',
|
||||
this[this.mainSuggestedRecordsListName][this[this.mainSuggestedRecordsListName].length - 1]
|
||||
]],
|
||||
});
|
||||
}
|
||||
this.update({
|
||||
activeSuggestedPartner: [[
|
||||
[this.activeSuggestedRecordName]: [[
|
||||
'link',
|
||||
this.allSuggestedPartners[this.allSuggestedPartners.length - 1]
|
||||
this[this.extraSuggestedRecordsListName][this[this.extraSuggestedRecordsListName].length - 1]
|
||||
]],
|
||||
});
|
||||
}
|
||||
|
||||
setNextSuggestedPartnerActive() {
|
||||
if (this.allSuggestedPartners.length === 0) {
|
||||
setNextSuggestionActive() {
|
||||
const fullList = this.extraSuggestedRecordsListName ?
|
||||
this[this.mainSuggestedRecordsListName].concat(this[this.extraSuggestedRecordsListName]) :
|
||||
this[this.mainSuggestedRecordsListName];
|
||||
if (fullList.length === 0) {
|
||||
return;
|
||||
}
|
||||
const activeSuggestedPartnerIndex = this.allSuggestedPartners.findIndex(
|
||||
suggestedPartner => suggestedPartner === this.activeSuggestedPartner
|
||||
const activeElementIndex = fullList.findIndex(
|
||||
suggestion => suggestion === this[this.activeSuggestedRecordName]
|
||||
);
|
||||
if (activeSuggestedPartnerIndex !== this.allSuggestedPartners.length - 1) {
|
||||
if (activeElementIndex !== fullList.length - 1) {
|
||||
this.update({
|
||||
activeSuggestedPartner: [[
|
||||
[this.activeSuggestedRecordName]: [[
|
||||
'link',
|
||||
this.allSuggestedPartners[activeSuggestedPartnerIndex + 1]
|
||||
fullList[activeElementIndex + 1]
|
||||
]],
|
||||
});
|
||||
} else {
|
||||
this.update({
|
||||
activeSuggestedPartner: [['link', this.allSuggestedPartners[0]]],
|
||||
[this.activeSuggestedRecordName]: [['link', fullList[0]]],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
setPreviousSuggestedPartnerActive() {
|
||||
if (this.allSuggestedPartners.length === 0) {
|
||||
setPreviousSuggestionActive() {
|
||||
const fullList = this.extraSuggestedRecordsListName ?
|
||||
this[this.mainSuggestedRecordsListName].concat(this[this.extraSuggestedRecordsListName]) :
|
||||
this[this.mainSuggestedRecordsListName];
|
||||
if (fullList.length === 0) {
|
||||
return;
|
||||
}
|
||||
const activeSuggestedPartnerIndex = this.allSuggestedPartners.findIndex(
|
||||
suggestedPartner => suggestedPartner === this.activeSuggestedPartner
|
||||
const activeElementIndex = fullList.findIndex(
|
||||
suggestion => suggestion === this[this.activeSuggestedRecordName]
|
||||
);
|
||||
if (activeSuggestedPartnerIndex === -1) {
|
||||
if (activeElementIndex === -1) {
|
||||
this.update({
|
||||
activeSuggestedPartner: [['link', this.allSuggestedPartners[0]]]
|
||||
[this.activeSuggestedRecordName]: [['link', fullList[0]]]
|
||||
});
|
||||
} else if (activeSuggestedPartnerIndex !== 0) {
|
||||
} else if (activeElementIndex !== 0) {
|
||||
this.update({
|
||||
activeSuggestedPartner: [[
|
||||
[this.activeSuggestedRecordName]: [[
|
||||
'link',
|
||||
this.allSuggestedPartners[activeSuggestedPartnerIndex - 1]
|
||||
fullList[activeElementIndex - 1]
|
||||
]],
|
||||
});
|
||||
} else {
|
||||
this.update({
|
||||
activeSuggestedPartner: [[
|
||||
[this.activeSuggestedRecordName]: [[
|
||||
'link',
|
||||
this.allSuggestedPartners[this.allSuggestedPartners.length - 1]
|
||||
fullList[fullList.length - 1]
|
||||
]],
|
||||
});
|
||||
}
|
||||
@@ -299,10 +410,10 @@ function factory(dependencies) {
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @returns {mail.partner[]}
|
||||
* @returns {mail.model}
|
||||
*/
|
||||
_computeAllSuggestedPartners() {
|
||||
return [['replace', this.mainSuggestedPartners.concat(this.extraSuggestedPartners)]];
|
||||
_computeActiveSuggestedRecord() {
|
||||
return this[this.activeSuggestedRecordName];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -319,9 +430,7 @@ function factory(dependencies) {
|
||||
/**
|
||||
* Ensure extraSuggestedPartners does not contain any partner already
|
||||
* present in mainSuggestedPartners. This is necessary for the
|
||||
* consistency of suggestion list because allSuggestedPartners is the
|
||||
* union of both but without duplicates (because it is a relational
|
||||
* field).
|
||||
* consistency of suggestion list.
|
||||
*
|
||||
* @private
|
||||
* @returns {mail.partner[]}
|
||||
@@ -330,12 +439,24 @@ function factory(dependencies) {
|
||||
return [['unlink', this.mainSuggestedPartners]];
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @returns {mail.model[]}
|
||||
*/
|
||||
_computeExtraSuggestedRecordsList() {
|
||||
return this.extraSuggestedRecordsListName
|
||||
? this[this.extraSuggestedRecordsListName]
|
||||
: [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @return {boolean}
|
||||
*/
|
||||
_computeHasSuggestedPartners() {
|
||||
return this.allSuggestedPartners.length > 0;
|
||||
_computeHasSuggestions() {
|
||||
const hasMainSuggestedRecordsList = this.mainSuggestedRecordsListName ? this[this.mainSuggestedRecordsListName].length > 0 : false;
|
||||
const hasExtraSuggestedRecordsList = this.extraSuggestedRecordsListName ? this[this.extraSuggestedRecordsListName].length > 0 : false;
|
||||
return hasMainSuggestedRecordsList || hasExtraSuggestedRecordsList;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -346,6 +467,16 @@ function factory(dependencies) {
|
||||
return this.attachments.some(attachment => attachment.isTemporary);
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @returns {mail.model[]}
|
||||
*/
|
||||
_computeMainSuggestedRecordsList() {
|
||||
return this.mainSuggestedRecordsListName
|
||||
? this[this.mainSuggestedRecordsListName]
|
||||
: [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Detects if mentioned partners are still in the composer text input content
|
||||
* and removes them if not.
|
||||
@@ -370,18 +501,26 @@ function factory(dependencies) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Detects if mentions suggestions should be displayed when user is typing
|
||||
* and searches partners based on user's research.
|
||||
* Detects if mentioned channels are still in the composer text input content
|
||||
* and removes them if not.
|
||||
*
|
||||
* @private
|
||||
* @returns {mail.partner[]}
|
||||
*/
|
||||
_detectDelimiter() {
|
||||
const mentionKeyword = this._validateMentionKeyword('@', false);
|
||||
if (mentionKeyword !== false) {
|
||||
this._getSuggestedPartners(mentionKeyword);
|
||||
} else {
|
||||
this.closeMentionSuggestions();
|
||||
_computeMentionedChannels() {
|
||||
const inputMentions = this.textInputContent.match(
|
||||
new RegExp("#[^ ]+(?= | |$)", 'g')
|
||||
) || [];
|
||||
const unmentionedChannels = [];
|
||||
for (const channel of this.mentionedChannels) {
|
||||
let inputMention = inputMentions.find(item => {
|
||||
return item === ("#" + channel.name).replace(/ /g, '\u00a0');
|
||||
});
|
||||
if (!inputMention) {
|
||||
unmentionedChannels.push(channel);
|
||||
}
|
||||
}
|
||||
return [['unlink', unmentionedChannels]];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -413,28 +552,36 @@ function factory(dependencies) {
|
||||
* @returns {string}
|
||||
*/
|
||||
_generateMentionsLinks(body) {
|
||||
if (this.mentionedPartners.length === 0) {
|
||||
if (this.mentionedPartners.length === 0 && this.mentionedChannels.length === 0) {
|
||||
return body;
|
||||
}
|
||||
const inputMentions = body.match(new RegExp("@"+'[^ ]+(?= | |$)', 'g'));
|
||||
const inputMentions = body.match(new RegExp("(@|#)" + '[^ ]+(?= | |$)', 'g'));
|
||||
const substrings = [];
|
||||
let startIndex = 0;
|
||||
for (const i in inputMentions) {
|
||||
const match = inputMentions[i];
|
||||
for (const match of inputMentions) {
|
||||
const suggestionDelimiter = match[0];
|
||||
const matchName = owl.utils.escape(match.substring(1).replace(new RegExp('\u00a0', 'g'), ' '));
|
||||
const endIndex = body.indexOf(match, startIndex) + match.length;
|
||||
const partner = this.mentionedPartners.find(partner =>
|
||||
partner.name === matchName
|
||||
let field = "mentionedPartners";
|
||||
let model = "res.partner";
|
||||
let cssClass = "o_mail_redirect";
|
||||
if (suggestionDelimiter === "#") {
|
||||
field = "mentionedChannels";
|
||||
model = "mail.channel";
|
||||
cssClass = "o_channel_redirect";
|
||||
}
|
||||
const mention = this[field].find(mention =>
|
||||
mention.name === matchName
|
||||
);
|
||||
let mentionLink = "@" + matchName;
|
||||
if (partner) {
|
||||
let mentionLink = suggestionDelimiter + matchName;
|
||||
if (mention) {
|
||||
const baseHREF = this.env.session.url('/web');
|
||||
const href = `href='${baseHREF}#model=res.partner&id=${partner.id}'`;
|
||||
const attClass = `class='o_mail_redirect'`;
|
||||
const dataOeId = `data-oe-id='${partner.id}'`;
|
||||
const dataOeModel = `data-oe-model='res.partner'`;
|
||||
const href = `href='${baseHREF}#model=${model}&id=${mention.id}'`;
|
||||
const attClass = `class='${cssClass}'`;
|
||||
const dataOeId = `data-oe-id='${mention.id}'`;
|
||||
const dataOeModel = `data-oe-model='${model}'`;
|
||||
const target = `target='_blank'`;
|
||||
mentionLink = `<a ${href} ${attClass} ${dataOeId} ${dataOeModel} ${target} >@${matchName}</a>`;
|
||||
mentionLink = `<a ${href} ${attClass} ${dataOeId} ${dataOeModel} ${target} >${suggestionDelimiter}${matchName}</a>`;
|
||||
}
|
||||
substrings.push(body.substring(startIndex, body.indexOf(match, startIndex)));
|
||||
substrings.push(mentionLink);
|
||||
@@ -456,11 +603,109 @@ function factory(dependencies) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_reset() {
|
||||
this.closeSuggestions();
|
||||
this.update({
|
||||
attachments: [['unlink-all']],
|
||||
mentionedChannels: [['unlink-all']],
|
||||
mentionedPartners: [['unlink-all']],
|
||||
subjectContent: "",
|
||||
textInputContent: '',
|
||||
textInputCursorEnd: 0,
|
||||
textInputCursorStart: 0,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @param {string} mentionKeyword
|
||||
*/
|
||||
async _getSuggestedPartners(mentionKeyword) {
|
||||
_updateSuggestedCannedResponses(mentionKeyword) {
|
||||
this.update({
|
||||
suggestedCannedResponses: [['replace', this.env.messaging.cannedResponses.filter(
|
||||
cannedResponse => cannedResponse.source.includes(mentionKeyword)
|
||||
)]],
|
||||
});
|
||||
|
||||
if (this.suggestedCannedResponses[0]) {
|
||||
this.update({
|
||||
activeSuggestedCannedResponse: [['link', this.suggestedCannedResponses[0]]],
|
||||
});
|
||||
} else {
|
||||
this.update({
|
||||
activeSuggestedCannedResponse: [['unlink']],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @param {string} mentionKeyword
|
||||
*/
|
||||
async _updateSuggestedChannels(mentionKeyword) {
|
||||
const mentions = await this.async(() => this.env.services.rpc(
|
||||
{
|
||||
model: 'mail.channel',
|
||||
method: 'get_mention_suggestions',
|
||||
kwargs: {
|
||||
limit: 8,
|
||||
search: mentionKeyword,
|
||||
},
|
||||
},
|
||||
{ shadow: true }
|
||||
));
|
||||
|
||||
this.update({
|
||||
suggestedChannels: [[
|
||||
'insert-and-replace',
|
||||
mentions.map(data =>
|
||||
this.env.models['mail.thread'].convertData(data))
|
||||
]],
|
||||
});
|
||||
|
||||
if (this.suggestedChannels[0]) {
|
||||
this.update({
|
||||
activeSuggestedChannel: [['link', this.suggestedChannels[0]]],
|
||||
});
|
||||
} else {
|
||||
this.update({
|
||||
activeSuggestedChannel: [['unlink']],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} mentionKeyword
|
||||
*/
|
||||
_updateSuggestedChannelCommands(mentionKeyword) {
|
||||
this.update({
|
||||
suggestedChannelCommands: [[
|
||||
'replace',
|
||||
this.env.messaging.commands.filter(
|
||||
command => command.name.includes(mentionKeyword)
|
||||
)
|
||||
]],
|
||||
});
|
||||
|
||||
if (this.suggestedChannelCommands[0]) {
|
||||
this.update({
|
||||
activeSuggestedChannelCommand: [['link', this.suggestedChannelCommands[0]]],
|
||||
});
|
||||
} else {
|
||||
this.update({
|
||||
activeSuggestedChannelCommand: [['unlink']],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @param {string} mentionKeyword
|
||||
*/
|
||||
async _updateSuggestedPartners(mentionKeyword) {
|
||||
const mentions = await this.async(() => this.env.services.rpc(
|
||||
{
|
||||
model: 'res.partner',
|
||||
@@ -489,9 +734,13 @@ function factory(dependencies) {
|
||||
]],
|
||||
});
|
||||
|
||||
if (this.allSuggestedPartners[0]) {
|
||||
if (this.mainSuggestedPartners[0]) {
|
||||
this.update({
|
||||
activeSuggestedPartner: [['link', this.allSuggestedPartners[0]]],
|
||||
activeSuggestedPartner: [['link', this.mainSuggestedPartners[0]]],
|
||||
});
|
||||
} else if (this.extraSuggestedPartners[0]) {
|
||||
this.update({
|
||||
activeSuggestedPartner: [['link', this.extraSuggestedPartners[0]]],
|
||||
});
|
||||
} else {
|
||||
this.update({
|
||||
@@ -501,46 +750,30 @@ function factory(dependencies) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_reset() {
|
||||
this.closeMentionSuggestions();
|
||||
this.update({
|
||||
attachments: [['unlink-all']],
|
||||
mentionedPartners: [['unlink-all']],
|
||||
subjectContent: "",
|
||||
textInputContent: "",
|
||||
textInputCursorStart: 0,
|
||||
textInputCursorEnd: 0,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates user's current typing as a correct mention keyword
|
||||
* in order to trigger mentions suggestions display.
|
||||
* Returns the mention keyword without the delimiter if it has been validated
|
||||
* and false if not
|
||||
* Validates user's current typing as a correct mention keyword in order
|
||||
* to trigger mentions suggestions display.
|
||||
* Returns the mention keyword without the suggestion delimiter if it
|
||||
* has been validated and false if not.
|
||||
*
|
||||
* @private
|
||||
* @param {string} delimiter
|
||||
* @param {boolean} beginningOnly
|
||||
* @returns {string|boolean}
|
||||
*/
|
||||
_validateMentionKeyword(delimiter, beginningOnly) {
|
||||
_validateMentionKeyword(beginningOnly) {
|
||||
const leftString = this.textInputContent.substring(0, this.textInputCursorStart);
|
||||
|
||||
// use position before delimiter because there should be whitespaces
|
||||
// or line feed/carriage return before the delimiter
|
||||
const beforeDelimiterPosition = leftString.lastIndexOf(delimiter) - 1;
|
||||
if (beginningOnly && beforeDelimiterPosition > 0) {
|
||||
// use position before suggestion delimiter because there should be whitespaces
|
||||
// or line feed/carriage return before the suggestion delimiter
|
||||
const beforeSuggestionDelimiterPosition = leftString.lastIndexOf(this.suggestionDelimiter) - 1;
|
||||
if (beginningOnly && beforeSuggestionDelimiterPosition > 0) {
|
||||
return false;
|
||||
}
|
||||
let searchStr = this.textInputContent.substring(
|
||||
beforeDelimiterPosition,
|
||||
beforeSuggestionDelimiterPosition,
|
||||
this.textInputCursorStart
|
||||
);
|
||||
// regex string start with delimiter or whitespace then delimiter
|
||||
const pattern = "^"+delimiter+"|^\\s"+delimiter;
|
||||
// regex string start with suggestion delimiter or whitespace then suggestion delimiter
|
||||
const pattern = "^" + this.suggestionDelimiter + "|^\\s" + this.suggestionDelimiter;
|
||||
const regexStart = new RegExp(pattern, 'g');
|
||||
// trim any left whitespaces or the left line feed/ carriage return
|
||||
// at the beginning of the string
|
||||
@@ -548,7 +781,7 @@ function factory(dependencies) {
|
||||
if (regexStart.test(searchStr) && searchStr.length) {
|
||||
searchStr = searchStr.replace(pattern, '');
|
||||
return !searchStr.includes(' ') && !/[\r\n]/.test(searchStr)
|
||||
? searchStr.replace(delimiter, '')
|
||||
? searchStr.replace(this.suggestionDelimiter, '')
|
||||
: false;
|
||||
}
|
||||
return false;
|
||||
@@ -556,14 +789,23 @@ function factory(dependencies) {
|
||||
}
|
||||
|
||||
Composer.fields = {
|
||||
activeSuggestedCannedResponse: many2one('mail.canned_response'),
|
||||
activeSuggestedChannel: many2one('mail.thread'),
|
||||
activeSuggestedChannelCommand: many2one('mail.channel_command'),
|
||||
activeSuggestedPartner: many2one('mail.partner'),
|
||||
allSuggestedPartners: many2many('mail.partner', {
|
||||
compute: '_computeAllSuggestedPartners',
|
||||
activeSuggestedRecord: attr({
|
||||
compute: '_computeActiveSuggestedRecord',
|
||||
dependencies: [
|
||||
'extraSuggestedPartners',
|
||||
'mainSuggestedPartners',
|
||||
'activeSuggestedCannedResponse',
|
||||
'activeSuggestedChannel',
|
||||
'activeSuggestedChannelCommand',
|
||||
'activeSuggestedPartner',
|
||||
'activeSuggestedRecordName',
|
||||
],
|
||||
}),
|
||||
activeSuggestedRecordName: attr({
|
||||
default: "",
|
||||
}),
|
||||
attachments: many2many('mail.attachment', {
|
||||
inverse: 'composers',
|
||||
}),
|
||||
@@ -601,6 +843,20 @@ function factory(dependencies) {
|
||||
'mainSuggestedPartners',
|
||||
],
|
||||
}),
|
||||
extraSuggestedRecordsList: attr({
|
||||
compute: '_computeExtraSuggestedRecordsList',
|
||||
dependencies: [
|
||||
'extraSuggestedPartners',
|
||||
'extraSuggestedRecordsListName',
|
||||
],
|
||||
}),
|
||||
/**
|
||||
* Allows to have different model types of mentions through a dynamic process
|
||||
* RPC can provide 2 lists and the second is defined as "extra"
|
||||
*/
|
||||
extraSuggestedRecordsListName: attr({
|
||||
default: "",
|
||||
}),
|
||||
/**
|
||||
* This field determines whether some attachments linked to this
|
||||
* composer are being uploaded.
|
||||
@@ -615,10 +871,16 @@ function factory(dependencies) {
|
||||
hasFocus: attr({
|
||||
default: false,
|
||||
}),
|
||||
hasSuggestedPartners: attr({
|
||||
compute: '_computeHasSuggestedPartners',
|
||||
hasSuggestions: attr({
|
||||
compute: '_computeHasSuggestions',
|
||||
dependencies: [
|
||||
'allSuggestedPartners',
|
||||
'extraSuggestedRecordsListName',
|
||||
'extraSuggestedPartners',
|
||||
'mainSuggestedRecordsListName',
|
||||
'mainSuggestedPartners',
|
||||
'suggestedCannedResponses',
|
||||
'suggestedChannelCommands',
|
||||
'suggestedChannels',
|
||||
],
|
||||
default: false,
|
||||
}),
|
||||
@@ -628,7 +890,28 @@ function factory(dependencies) {
|
||||
isLog: attr({
|
||||
default: false,
|
||||
}),
|
||||
mainSuggestedRecordsList: attr({
|
||||
compute: '_computeMainSuggestedRecordsList',
|
||||
dependencies: [
|
||||
'mainSuggestedPartners',
|
||||
'mainSuggestedRecordsListName',
|
||||
'suggestedCannedResponses',
|
||||
'suggestedChannelCommands',
|
||||
'suggestedChannels',
|
||||
],
|
||||
}),
|
||||
/**
|
||||
* Allows to have different model types of mentions through a dynamic process
|
||||
* RPC can provide 2 lists and the first is defined as "main"
|
||||
*/
|
||||
mainSuggestedRecordsListName: attr({
|
||||
default: "",
|
||||
}),
|
||||
mainSuggestedPartners: many2many('mail.partner'),
|
||||
mentionedChannels: many2many('mail.thread', {
|
||||
compute: '_computeMentionedChannels',
|
||||
dependencies: ['textInputContent'],
|
||||
}),
|
||||
mentionedPartners: many2many('mail.partner', {
|
||||
compute: '_computeMentionedPartners',
|
||||
dependencies: ['textInputContent'],
|
||||
@@ -666,13 +949,26 @@ function factory(dependencies) {
|
||||
subjectContent: attr({
|
||||
default: "",
|
||||
}),
|
||||
suggestedCannedResponses: many2many('mail.canned_response'),
|
||||
suggestedChannelCommands: many2many('mail.channel_command'),
|
||||
suggestedChannels: many2many('mail.thread'),
|
||||
/**
|
||||
* Special character used to trigger different kinds of suggestions
|
||||
* such as canned responses (:), channels (#), commands (/) and partners (@)
|
||||
*/
|
||||
suggestionDelimiter: attr({
|
||||
default: "",
|
||||
}),
|
||||
suggestionModelName: attr({
|
||||
default: "",
|
||||
}),
|
||||
textInputContent: attr({
|
||||
default: "",
|
||||
}),
|
||||
textInputCursorStart: attr({
|
||||
textInputCursorEnd: attr({
|
||||
default: 0,
|
||||
}),
|
||||
textInputCursorEnd: attr({
|
||||
textInputCursorStart: attr({
|
||||
default: 0,
|
||||
}),
|
||||
thread: one2one('mail.thread', {
|
||||
|
||||
@@ -2,7 +2,7 @@ odoo.define('mail/static/src/models/messaging/messaging.js', function (require)
|
||||
'use strict';
|
||||
|
||||
const { registerNewModel } = require('mail/static/src/model/model_core.js');
|
||||
const { attr, many2one, one2one } = require('mail/static/src/model/model_field.js');
|
||||
const { attr, many2one, one2many, one2one } = require('mail/static/src/model/model_field.js');
|
||||
|
||||
function factory(dependencies) {
|
||||
|
||||
@@ -158,17 +158,13 @@ function factory(dependencies) {
|
||||
}
|
||||
|
||||
Messaging.fields = {
|
||||
cannedResponses: attr({
|
||||
default: {},
|
||||
}),
|
||||
cannedResponses: one2many('mail.canned_response'),
|
||||
chatWindowManager: one2one('mail.chat_window_manager', {
|
||||
default: [['create']],
|
||||
inverse: 'messaging',
|
||||
isCausal: true,
|
||||
}),
|
||||
commands: attr({
|
||||
default: {},
|
||||
}),
|
||||
commands: one2many('mail.channel_command'),
|
||||
currentPartner: one2one('mail.partner'),
|
||||
currentUser: one2one('mail.user'),
|
||||
device: one2one('mail.device', {
|
||||
|
||||
@@ -128,20 +128,12 @@ function factory(dependencies) {
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @param {Object[]} shortcodes
|
||||
* @param {Object[]} cannedResponsesData
|
||||
*/
|
||||
_initCannedResponses(shortcodes) {
|
||||
const messaging = this.messaging;
|
||||
const cannedResponses = shortcodes
|
||||
.map(s => {
|
||||
const { id, source, substitution } = s;
|
||||
return { id, source, substitution };
|
||||
})
|
||||
.reduce((obj, cr) => {
|
||||
obj[cr.id] = cr;
|
||||
return obj;
|
||||
}, {});
|
||||
messaging.update({ cannedResponses });
|
||||
_initCannedResponses(cannedResponsesData) {
|
||||
this.messaging.update({
|
||||
cannedResponses: [['insert', cannedResponsesData]],
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -172,18 +164,9 @@ function factory(dependencies) {
|
||||
* @param {Object[]} commandsData
|
||||
*/
|
||||
_initCommands(commandsData) {
|
||||
const messaging = this.messaging;
|
||||
const commands = commandsData
|
||||
.map(command => {
|
||||
return Object.assign({
|
||||
id: command.name,
|
||||
}, command);
|
||||
})
|
||||
.reduce((obj, command) => {
|
||||
obj[command.id] = command;
|
||||
return obj;
|
||||
}, {});
|
||||
messaging.update({ commands });
|
||||
this.messaging.update({
|
||||
commands: [['insert', commandsData]],
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -561,7 +561,11 @@ function factory(dependencies) {
|
||||
return;
|
||||
}
|
||||
this.update({ pendingSeenMessageId: messageId });
|
||||
return this.env.models['mail.thread'].performRpcChannelSeen({ ids: [this.id], lastMessageId: messageId });
|
||||
return this.env.models['mail.thread'].performRpcChannelSeen({
|
||||
ids: [this.id],
|
||||
// commands have fake message id that is not integer
|
||||
lastMessageId: Math.floor(messageId),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -170,6 +170,13 @@ class MockModels {
|
||||
},
|
||||
records: [],
|
||||
},
|
||||
'mail.shortcode': {
|
||||
fields: {
|
||||
source: { type: 'char' },
|
||||
substitution: { type: 'char' },
|
||||
},
|
||||
records: [],
|
||||
},
|
||||
'res.country': {
|
||||
fields: {
|
||||
code: { string: "Code", type: 'char' },
|
||||
|
||||
@@ -233,8 +233,13 @@ MockServer.include({
|
||||
return this._mockMailMessageUnstarAll();
|
||||
}
|
||||
// res.partner methods
|
||||
if (args.model === 'res.partner' && args.method === 'get_mention_suggestions') {
|
||||
return this._mockResPartnerGetMentionSuggestions(args);
|
||||
if (args.method === 'get_mention_suggestions') {
|
||||
if (args.model === 'mail.channel') {
|
||||
return this._mockMailChannelGetMentionSuggestions(args);
|
||||
}
|
||||
if (args.model === 'res.partner') {
|
||||
return this._mockResPartnerGetMentionSuggestions(args);
|
||||
}
|
||||
}
|
||||
if (args.model === 'res.partner' && args.method === 'im_search') {
|
||||
const name = args.args[0] || args.kwargs.search;
|
||||
@@ -388,6 +393,8 @@ MockServer.include({
|
||||
|
||||
const mailFailures = this._mockMailMessageMessageFetchFailed();
|
||||
|
||||
const shortcodes = this._getRecords('mail.shortcode', []);
|
||||
|
||||
const starredCounter = this._getRecords('mail.message', [
|
||||
['starred_partner_ids', 'in', this.currentPartnerId],
|
||||
]).length;
|
||||
@@ -398,7 +405,11 @@ MockServer.include({
|
||||
channel_direct_message: directMessageInfos,
|
||||
channel_private_group: privateGroupInfos,
|
||||
},
|
||||
commands: [],
|
||||
commands: [{
|
||||
channel_types: ["channel", "chat"],
|
||||
help: "List users in the current channel",
|
||||
name: "who",
|
||||
}],
|
||||
current_partner: currentPartnerFormat,
|
||||
current_user_id: this.currentUserId,
|
||||
mail_failures: mailFailures,
|
||||
@@ -409,7 +420,7 @@ MockServer.include({
|
||||
needaction_inbox_counter,
|
||||
partner_root: partnerRootFormat,
|
||||
public_partner: publicPartnerFormat,
|
||||
shortcodes: [],
|
||||
shortcodes,
|
||||
starred_counter: starredCounter,
|
||||
};
|
||||
},
|
||||
@@ -681,7 +692,6 @@ MockServer.include({
|
||||
// is supposed to return this existing chat. But the mock is currently
|
||||
// always creating a new chat, because no test is relying on receiving
|
||||
// an existing chat.
|
||||
|
||||
const id = this._mockCreate('mail.channel', {
|
||||
channel_type: 'chat',
|
||||
mass_mailing: false,
|
||||
@@ -853,6 +863,56 @@ MockServer.include({
|
||||
}
|
||||
throw new Error(`mail/mock_server: the route execute_command doesn't implement the command "${commandName}"`);
|
||||
},
|
||||
/**
|
||||
* Simulates `get_mention_suggestions` on `mail.channel`.
|
||||
*
|
||||
* @private
|
||||
* @returns {Array[]}
|
||||
*/
|
||||
_mockMailChannelGetMentionSuggestions(args) {
|
||||
const search = args.kwargs.search || '';
|
||||
const limit = args.kwargs.limit || 8;
|
||||
|
||||
/**
|
||||
* Returns the given list of channels after filtering it according to
|
||||
* the logic of the Python method `get_mention_suggestions` for the
|
||||
* given search term. The result is truncated to the given limit and
|
||||
* formatted as expected by the original method.
|
||||
*
|
||||
* @param {Object[]} channels
|
||||
* @param {string} search
|
||||
* @param {integer} limit
|
||||
* @returns {Object[]}
|
||||
*/
|
||||
const mentionSuggestionsFilter = function (channels, search, limit) {
|
||||
const matchingChannels = channels
|
||||
.filter(channel => {
|
||||
// no search term is considered as return all
|
||||
if (!search) {
|
||||
return true;
|
||||
}
|
||||
// otherwise name or email must match search term
|
||||
if (channel.name && channel.name.includes(search)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}).map(channel => {
|
||||
// expected format
|
||||
return {
|
||||
id: channel.id,
|
||||
name: channel.name,
|
||||
public: channel.public,
|
||||
};
|
||||
});
|
||||
// reduce results to max limit
|
||||
matchingChannels.length = Math.min(matchingChannels.length, limit);
|
||||
return matchingChannels;
|
||||
};
|
||||
|
||||
const mentionSuggestions = mentionSuggestionsFilter(this.data['mail.channel'].records, search, limit);
|
||||
|
||||
return mentionSuggestions;
|
||||
},
|
||||
/**
|
||||
* Simulates `message_post` on `mail.channel`.
|
||||
*
|
||||
|
||||
@@ -77,6 +77,8 @@
|
||||
<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_suggestion/composer_suggestion.js"/>
|
||||
<script type="text/javascript" src="/mail/static/src/components/composer_suggestion_list/composer_suggestion_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>
|
||||
@@ -109,7 +111,6 @@
|
||||
<script type="text/javascript" src="/mail/static/src/components/notification_popover/notification_popover.js"></script>
|
||||
<script type="text/javascript" src="/mail/static/src/components/notification_request/notification_request.js"/>
|
||||
<script type="text/javascript" src="/mail/static/src/components/partner_im_status_icon/partner_im_status_icon.js"></script>
|
||||
<script type="text/javascript" src="/mail/static/src/components/partner_mention_suggestion/partner_mention_suggestion.js"></script>
|
||||
<script type="text/javascript" src="/mail/static/src/components/thread_icon/thread_icon.js"></script>
|
||||
<script type="text/javascript" src="/mail/static/src/components/thread_needaction_preview/thread_needaction_preview.js"></script>
|
||||
<script type="text/javascript" src="/mail/static/src/components/thread_preview/thread_preview.js"></script>
|
||||
@@ -124,6 +125,8 @@
|
||||
<script type="text/javascript" src="/mail/static/src/models/activity_type/activity_type.js"></script>
|
||||
<script type="text/javascript" src="/mail/static/src/models/attachment/attachment.js"></script>
|
||||
<script type="text/javascript" src="/mail/static/src/models/attachment_viewer/attachment_viewer.js"></script>
|
||||
<script type="text/javascript" src="/mail/static/src/models/canned_response/canned_response.js"/>
|
||||
<script type="text/javascript" src="/mail/static/src/models/channel_command/channel_command.js"/>
|
||||
<script type="text/javascript" src="/mail/static/src/models/chat_window/chat_window.js"></script>
|
||||
<script type="text/javascript" src="/mail/static/src/models/chat_window_manager/chat_window_manager.js"></script>
|
||||
<script type="text/javascript" src="/mail/static/src/models/chatter/chatter.js"></script>
|
||||
@@ -188,6 +191,8 @@
|
||||
<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_suggestion/composer_suggestion.scss"/>
|
||||
<link rel="stylesheet" type="text/scss" href="/mail/static/src/components/composer_suggestion_list/composer_suggestion_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"/>
|
||||
@@ -214,7 +219,6 @@
|
||||
<link rel="stylesheet" type="text/scss" href="/mail/static/src/components/notification_popover/notification_popover.scss"/>
|
||||
<link rel="stylesheet" type="text/scss" href="/mail/static/src/components/notification_request/notification_request.scss"/>
|
||||
<link rel="stylesheet" type="text/scss" href="/mail/static/src/components/partner_im_status_icon/partner_im_status_icon.scss"/>
|
||||
<link rel="stylesheet" type="text/scss" href="/mail/static/src/components/partner_mention_suggestion/partner_mention_suggestion.scss"/>
|
||||
<link rel="stylesheet" type="text/scss" href="/mail/static/src/components/thread_icon/thread_icon.scss"/>
|
||||
<link rel="stylesheet" type="text/scss" href="/mail/static/src/components/thread_needaction_preview/thread_needaction_preview.scss"/>
|
||||
<link rel="stylesheet" type="text/scss" href="/mail/static/src/components/thread_preview/thread_preview.scss"/>
|
||||
@@ -270,6 +274,10 @@
|
||||
<script type="text/javascript" src="/mail/static/src/components/chatter/chatter_tests.js"></script>
|
||||
<script type="text/javascript" src="/mail/static/src/components/chatter_topbar/chatter_topbar_tests.js"></script>
|
||||
<script type="text/javascript" src="/mail/static/src/components/composer/composer_tests.js"></script>
|
||||
<script type="text/javascript" src="/mail/static/src/components/composer_suggestion/composer_suggestion_canned_response_tests.js"/>
|
||||
<script type="text/javascript" src="/mail/static/src/components/composer_suggestion/composer_suggestion_channel_tests.js"/>
|
||||
<script type="text/javascript" src="/mail/static/src/components/composer_suggestion/composer_suggestion_command_tests.js"/>
|
||||
<script type="text/javascript" src="/mail/static/src/components/composer_suggestion/composer_suggestion_partner_tests.js"/>
|
||||
<script type="text/javascript" src="/mail/static/src/components/dialog_manager/dialog_manager_tests.js"></script>
|
||||
<script type="text/javascript" src="/mail/static/src/components/discuss/tests/discuss_inbox_tests.js"></script>
|
||||
<script type="text/javascript" src="/mail/static/src/components/discuss/tests/discuss_moderation_tests.js"></script>
|
||||
@@ -286,7 +294,6 @@
|
||||
<script type="text/javascript" src="/mail/static/src/components/notification_list/notification_list_notification_group_tests.js"></script>
|
||||
<script type="text/javascript" src="/mail/static/src/components/notification_list/notification_list_tests.js"></script>
|
||||
<script type="text/javascript" src="/mail/static/src/components/partner_im_status_icon/partner_im_status_icon_tests.js"></script>
|
||||
<script type="text/javascript" src="/mail/static/src/components/partner_mention_suggestion/partner_mention_suggestion_tests.js"></script>
|
||||
<script type="text/javascript" src="/mail/static/src/components/thread_icon/thread_icon_tests.js"></script>
|
||||
<script type="text/javascript" src="/mail/static/src/components/thread_needaction_preview/thread_needaction_preview_tests.js"></script>
|
||||
<script type="text/javascript" src="/mail/static/src/components/thread_preview/thread_preview_tests.js"/>
|
||||
|
||||
Reference in New Issue
Block a user