[FIX] mail: remove regex from mentions

That should fix all detection issues once and for all.

task-2366680

closes odoo/odoo#61528

X-original-commit: cf6961fba38ed619bf7bfb908c3aadde0f694648
Signed-off-by: Alexandre Kühn (aku) <aku@odoo.com>
This commit is contained in:
Sébastien Theys
2020-11-09 10:49:34 +00:00
parent 4995c6b76e
commit dd7241c890
2 changed files with 190 additions and 66 deletions
@@ -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"
);
});
@@ -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);
}
substrings.push(body.substring(startIndex, body.length));
return substrings.join('');
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);
}
return body;
}
/**