summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSébastien Theys <seb@odoo.com>2020-11-06 11:54:26 +0000
committerSébastien Theys <seb@odoo.com>2020-11-06 11:54:26 +0000
commitcf6961fba38ed619bf7bfb908c3aadde0f694648 (patch)
tree3df2ddf96be24c2e6b637a9a6460c84072442105
parent46be4866adacafdd147c799d1cc91e27a88a99d0 (diff)
[FIX] mail: remove regex from mentions
That should fix all detection issues once and for all. task-2366680 closes odoo/odoo#60593 Signed-off-by: Alexandre Kühn (aku) <aku@odoo.com>
-rw-r--r--addons/mail/static/src/components/thread_view/thread_view_tests.js144
-rw-r--r--addons/mail/static/src/models/composer/composer.js112
2 files changed, 190 insertions, 66 deletions
diff --git a/addons/mail/static/src/components/thread_view/thread_view_tests.js b/addons/mail/static/src/components/thread_view/thread_view_tests.js
index dcdfee9d8fd..f8d421bfb5c 100644
--- a/addons/mail/static/src/components/thread_view/thread_view_tests.js
+++ b/addons/mail/static/src/components/thread_view/thread_view_tests.js
@@ -1166,7 +1166,7 @@ QUnit.test('Post a message containing an email address followed by a mention on
.dispatchEvent(new window.KeyboardEvent('keydown'));
document.querySelector(`.o_ComposerTextInput_textarea`)
.dispatchEvent(new window.KeyboardEvent('keyup'));
- })
+ });
});
await afterNextRender(() =>
document.querySelector('.o_ComposerSuggestion').click()
@@ -1181,6 +1181,66 @@ QUnit.test('Post a message containing an email address followed by a mention on
);
});
+QUnit.test('mention 2 different partners that have the same name', async function (assert) {
+ assert.expect(3);
+
+ this.data['mail.channel'].records.push({ id: 11 });
+ this.data['res.partner'].records.push(
+ {
+ id: 25,
+ email: "partner1@example.com",
+ name: "TestPartner",
+ }, {
+ id: 26,
+ email: "partner2@example.com",
+ name: "TestPartner",
+ },
+ );
+ await this.start();
+ const thread = this.env.models['mail.thread'].findFromIdentifyingData({
+ id: 11,
+ model: 'mail.channel',
+ });
+ const threadViewer = this.env.models['mail.thread_viewer'].create({
+ hasThreadView: true,
+ thread: [['link', thread]],
+ });
+ await this.createThreadViewComponent(threadViewer.threadView, { hasComposer: true });
+ document.querySelector('.o_ComposerTextInput_textarea').focus();
+ await afterNextRender(() => {
+ ["@", "T", "e"].forEach((char)=>{
+ document.execCommand('insertText', false, char);
+ document.querySelector(`.o_ComposerTextInput_textarea`)
+ .dispatchEvent(new window.KeyboardEvent('keydown'));
+ document.querySelector(`.o_ComposerTextInput_textarea`)
+ .dispatchEvent(new window.KeyboardEvent('keyup'));
+ });
+ });
+ await afterNextRender(() => document.querySelectorAll('.o_ComposerSuggestion')[0].click());
+ await afterNextRender(() => {
+ ["@", "T", "e"].forEach((char)=>{
+ document.execCommand('insertText', false, char);
+ document.querySelector(`.o_ComposerTextInput_textarea`)
+ .dispatchEvent(new window.KeyboardEvent('keydown'));
+ document.querySelector(`.o_ComposerTextInput_textarea`)
+ .dispatchEvent(new window.KeyboardEvent('keyup'));
+ });
+ });
+ await afterNextRender(() => document.querySelectorAll('.o_ComposerSuggestion')[1].click());
+ await afterNextRender(() => document.querySelector('.o_Composer_buttonSend').click());
+ assert.containsOnce(document.body, '.o_Message_content', 'should have one message after posting it');
+ assert.containsOnce(
+ document.querySelector(`.o_Message_content`),
+ `.o_mail_redirect[data-oe-id="25"][data-oe-model="res.partner"]:contains("@TestPartner")`,
+ "message should contain the first partner mention"
+ );
+ assert.containsOnce(
+ document.querySelector(`.o_Message_content`),
+ `.o_mail_redirect[data-oe-id="26"][data-oe-model="res.partner"]:contains("@TestPartner")`,
+ "message should also contain the second partner mention"
+ );
+});
+
QUnit.test('mention a channel with space in the name', async function (assert) {
assert.expect(2);
@@ -1216,12 +1276,12 @@ QUnit.test('mention a channel with space in the name', async function (assert) {
assert.containsOnce(
document.querySelector('.o_Message_content'),
'.o_channel_redirect',
- "message must contain a link the the mentionned channel"
+ "message must contain a link to the mentioned channel"
);
assert.strictEqual(
document.querySelector('.o_channel_redirect').textContent,
'#General good boy',
- "link to the channel must containt # + the channel name"
+ "link to the channel must contains # + the channel name"
);
});
@@ -1260,16 +1320,16 @@ QUnit.test('mention a channel with "&" in the name', async function (assert) {
assert.containsOnce(
document.querySelector('.o_Message_content'),
'.o_channel_redirect',
- "message should contain a link the the mentionned channel"
+ "message should contain a link to the mentioned channel"
);
assert.strictEqual(
document.querySelector('.o_channel_redirect').textContent,
'#General & good',
- "link to the channel must containt # + the channel name"
+ "link to the channel must contains # + the channel name"
);
});
-QUnit.test('start to mention a channel and add a new line after the #', async function (assert) {
+QUnit.test('mention a channel on a second line when the first line contains #', async function (assert) {
assert.expect(2);
this.data['mail.channel'].records.push({
@@ -1289,7 +1349,7 @@ QUnit.test('start to mention a channel and add a new line after the #', async fu
await afterNextRender(() => {
document.querySelector(`.o_ComposerTextInput_textarea`).focus();
- document.execCommand('insertText', false, "#\n #");
+ document.execCommand('insertText', false, "#blabla\n#");
document.querySelector(`.o_ComposerTextInput_textarea`)
.dispatchEvent(new window.KeyboardEvent('keydown'));
document.querySelector(`.o_ComposerTextInput_textarea`)
@@ -1304,16 +1364,16 @@ QUnit.test('start to mention a channel and add a new line after the #', async fu
assert.containsOnce(
document.querySelector('.o_Message_content'),
'.o_channel_redirect',
- "message should contain a link the the mentionned channel"
+ "message should contain a link to the mentioned channel"
);
assert.strictEqual(
document.querySelector('.o_channel_redirect').textContent,
'#General good',
- "link to the channel must containt # + the channel name"
+ "link to the channel must contains # + the channel name"
);
});
-QUnit.test('mention a channel and add a new line and text after the mention', async function (assert) {
+QUnit.test('mention a channel when replacing the space after the mention by another char', async function (assert) {
assert.expect(2);
this.data['mail.channel'].records.push({
@@ -1345,7 +1405,7 @@ QUnit.test('mention a channel and add a new line and text after the mention', as
await afterNextRender(() => {
const text = document.querySelector(`.o_ComposerTextInput_textarea`).value;
document.querySelector(`.o_ComposerTextInput_textarea`).value = text.slice(0, -1);
- document.execCommand('insertText', false, "\ntest");
+ document.execCommand('insertText', false, ", test");
document.querySelector(`.o_ComposerTextInput_textarea`)
.dispatchEvent(new window.KeyboardEvent('keydown'));
document.querySelector(`.o_ComposerTextInput_textarea`)
@@ -1357,12 +1417,70 @@ QUnit.test('mention a channel and add a new line and text after the mention', as
assert.containsOnce(
document.querySelector('.o_Message_content'),
'.o_channel_redirect',
- "message should contain a link the the mentionned channel"
+ "message should contain a link to the mentioned channel"
);
assert.strictEqual(
document.querySelector('.o_channel_redirect').textContent,
'#General good',
- "link to the channel must containt # + the channel name"
+ "link to the channel must contains # + the channel name"
+ );
+});
+
+QUnit.test('mention 2 different channels that have the same name', async function (assert) {
+ assert.expect(3);
+
+ this.data['mail.channel'].records.push(
+ {
+ id: 11,
+ name: "my channel",
+ },
+ {
+ id: 12,
+ name: "my channel",
+ },
+ );
+ await this.start();
+ const thread = this.env.models['mail.thread'].findFromIdentifyingData({
+ id: 11,
+ model: 'mail.channel',
+ });
+ const threadViewer = this.env.models['mail.thread_viewer'].create({
+ hasThreadView: true,
+ thread: [['link', thread]],
+ });
+ await this.createThreadViewComponent(threadViewer.threadView, { hasComposer: true });
+ document.querySelector('.o_ComposerTextInput_textarea').focus();
+ await afterNextRender(() => {
+ ["#", "m", "y"].forEach((char)=>{
+ document.execCommand('insertText', false, char);
+ document.querySelector(`.o_ComposerTextInput_textarea`)
+ .dispatchEvent(new window.KeyboardEvent('keydown'));
+ document.querySelector(`.o_ComposerTextInput_textarea`)
+ .dispatchEvent(new window.KeyboardEvent('keyup'));
+ });
+ });
+ await afterNextRender(() => document.querySelectorAll('.o_ComposerSuggestion')[0].click());
+ await afterNextRender(() => {
+ ["#", "m", "y"].forEach((char)=>{
+ document.execCommand('insertText', false, char);
+ document.querySelector(`.o_ComposerTextInput_textarea`)
+ .dispatchEvent(new window.KeyboardEvent('keydown'));
+ document.querySelector(`.o_ComposerTextInput_textarea`)
+ .dispatchEvent(new window.KeyboardEvent('keyup'));
+ });
+ });
+ await afterNextRender(() => document.querySelectorAll('.o_ComposerSuggestion')[1].click());
+ await afterNextRender(() => document.querySelector('.o_Composer_buttonSend').click());
+ assert.containsOnce(document.body, '.o_Message_content', 'should have one message after posting it');
+ assert.containsOnce(
+ document.querySelector(`.o_Message_content`),
+ `.o_channel_redirect[data-oe-id="11"][data-oe-model="mail.channel"]:contains("#my channel")`,
+ "message should contain the first channel mention"
+ );
+ assert.containsOnce(
+ document.querySelector(`.o_Message_content`),
+ `.o_channel_redirect[data-oe-id="12"][data-oe-model="mail.channel"]:contains("#my channel")`,
+ "message should also contain the second channel mention"
);
});
diff --git a/addons/mail/static/src/models/composer/composer.js b/addons/mail/static/src/models/composer/composer.js
index cfec9805308..a5e37ec7da4 100644
--- a/addons/mail/static/src/models/composer/composer.js
+++ b/addons/mail/static/src/models/composer/composer.js
@@ -161,7 +161,7 @@ function factory(dependencies) {
recordReplacement = this[this.activeSuggestedRecordName].substitution;
break;
case 'activeSuggestedChannel':
- recordReplacement = this[this.activeSuggestedRecordName].name.replace(/ /g, '\u00a0');
+ recordReplacement = this[this.activeSuggestedRecordName].name;
this.update({
mentionedChannels: [['link', this[this.activeSuggestedRecordName]]],
});
@@ -170,7 +170,7 @@ function factory(dependencies) {
recordReplacement = this[this.activeSuggestedRecordName].name;
break;
case 'activeSuggestedPartner':
- recordReplacement = this[this.activeSuggestedRecordName].name.replace(/ /g, '\u00a0');
+ recordReplacement = this[this.activeSuggestedRecordName].name;
this.update({
mentionedPartners: [['link', this[this.activeSuggestedRecordName]]],
});
@@ -510,15 +510,18 @@ function factory(dependencies) {
* @returns {mail.partner[]}
*/
_computeMentionedPartners() {
- const inputMentions = this.textInputContent.replace(/\n/g, " \n ").match(
- new RegExp("@[^ ]+(?= |&nbsp;|$)", 'g')
- ) || [];
const unmentionedPartners = [];
+ // ensure the same mention is not used multiple times if multiple
+ // partners have the same name
+ const namesIndex = {};
for (const partner of this.mentionedPartners) {
- let inputMention = inputMentions.find(item => {
- return item === ("@" + partner.name).replace(/ /g, '\u00a0');
- });
- if (!inputMention) {
+ const fromIndex = namesIndex[partner.name] !== undefined
+ ? namesIndex[partner.name] + 1 :
+ 0;
+ const index = this.textInputContent.indexOf(`@${partner.name}`, fromIndex);
+ if (index !== -1) {
+ namesIndex[partner.name] = index;
+ } else {
unmentionedPartners.push(partner);
}
}
@@ -533,15 +536,18 @@ function factory(dependencies) {
* @returns {mail.partner[]}
*/
_computeMentionedChannels() {
- const inputMentions = this.textInputContent.replace(/\n/g, " \n ").match(
- new RegExp("#[^ ]+(?= |&nbsp;|$)", 'g')
- ) || [];
const unmentionedChannels = [];
+ // ensure the same mention is not used multiple times if multiple
+ // channels have the same name
+ const namesIndex = {};
for (const channel of this.mentionedChannels) {
- let inputMention = inputMentions.find(item => {
- return item === ("#" + channel.name).replace(/ /g, '\u00a0');
- });
- if (!inputMention) {
+ const fromIndex = namesIndex[channel.name] !== undefined
+ ? namesIndex[channel.name] + 1 :
+ 0;
+ const index = this.textInputContent.indexOf(`#${channel.name}`, fromIndex);
+ if (index !== -1) {
+ namesIndex[channel.name] = index;
+ } else {
unmentionedChannels.push(channel);
}
}
@@ -577,45 +583,45 @@ function factory(dependencies) {
* @returns {string}
*/
_generateMentionsLinks(body) {
- if (this.mentionedPartners.length === 0 && this.mentionedChannels.length === 0) {
- return body;
+ // List of mention data to insert in the body.
+ // Useful to do the final replace after parsing to avoid using the
+ // same tag twice if two different mentions have the same name.
+ const mentions = [];
+ for (const partner of this.mentionedPartners) {
+ const placeholder = `@-mention-partner-${partner.id}`;
+ const text = `@${owl.utils.escape(partner.name)}`;
+ mentions.push({
+ class: 'o_mail_redirect',
+ id: partner.id,
+ model: 'res.partner',
+ placeholder,
+ text,
+ });
+ body = body.replace(text, placeholder);
}
- const inputMentions = body.replace(new RegExp('<br/>', 'g'), " <br/> ")
- .match(new RegExp("(@|#)" + '[^ ]+(?= |&nbsp;|$)', 'g'))
- .filter(match => !match.endsWith("<br/>"));
- const substrings = [];
- let startIndex = 0;
- for (const match of inputMentions) {
- const suggestionDelimiter = match[0];
- const matchNameEscaped = match.substring(1).replace(new RegExp('\u00a0', 'g'), ' ');
- const endIndex = body.indexOf(match, startIndex) + match.length;
- 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 =>
- owl.utils.escape(mention.name) === matchNameEscaped
- );
- let mentionLink = suggestionDelimiter + matchNameEscaped;
- if (mention) {
- const baseHREF = this.env.session.url('/web');
- 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} >${suggestionDelimiter}${matchNameEscaped}</a>`;
- }
- substrings.push(body.substring(startIndex, body.indexOf(match, startIndex)));
- substrings.push(mentionLink);
- startIndex = endIndex;
+ for (const channel of this.mentionedChannels) {
+ const placeholder = `#-mention-channel-${channel.id}`;
+ const text = `#${owl.utils.escape(channel.name)}`;
+ mentions.push({
+ class: 'o_channel_redirect',
+ id: channel.id,
+ model: 'mail.channel',
+ placeholder,
+ text,
+ });
+ body = body.replace(text, placeholder);
+ }
+ const baseHREF = this.env.session.url('/web');
+ for (const mention of mentions) {
+ const href = `href='${baseHREF}#model=${mention.model}&id=${mention.id}'`;
+ const attClass = `class='${mention.class}'`;
+ const dataOeId = `data-oe-id='${mention.id}'`;
+ const dataOeModel = `data-oe-model='${mention.model}'`;
+ const target = `target='_blank'`;
+ const link = `<a ${href} ${attClass} ${dataOeId} ${dataOeModel} ${target}>${mention.text}</a>`;
+ body = body.replace(mention.placeholder, link);
}
- substrings.push(body.substring(startIndex, body.length));
- return substrings.join('');
+ return body;
}
/**