diff options
| author | Sébastien Theys <seb@odoo.com> | 2020-11-06 11:54:26 +0000 |
|---|---|---|
| committer | Sébastien Theys <seb@odoo.com> | 2020-11-06 11:54:26 +0000 |
| commit | cf6961fba38ed619bf7bfb908c3aadde0f694648 (patch) | |
| tree | 3df2ddf96be24c2e6b637a9a6460c84072442105 | |
| parent | 46be4866adacafdd147c799d1cc91e27a88a99d0 (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.js | 144 | ||||
| -rw-r--r-- | addons/mail/static/src/models/composer/composer.js | 112 |
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("@[^ ]+(?= | |$)", '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("#[^ ]+(?= | |$)", '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("(@|#)" + '[^ ]+(?= | |$)', '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; } /** |
