diff --git a/addons/im_livechat/models/mail_channel.py b/addons/im_livechat/models/mail_channel.py index b62a7c570aa..adb8d59ef0b 100644 --- a/addons/im_livechat/models/mail_channel.py +++ b/addons/im_livechat/models/mail_channel.py @@ -87,7 +87,9 @@ class MailChannel(models.Model): def _execute_command_help_message_extra(self): msg = super(MailChannel, self)._execute_command_help_message_extra() - return msg + _("Type :shortcut to insert a canned response in your message.
") + if self.channel_type == 'livechat': + return msg + _("Type :shortcut to insert a canned response in your message.
") + return msg def execute_command_history(self, **kwargs): self.env['bus.bus']._sendone(self.uuid, 'im_livechat.history_command', {'id': self.id}) diff --git a/addons/im_livechat/static/src/composer/suggestion_service_patch.js b/addons/im_livechat/static/src/composer/suggestion_service_patch.js new file mode 100644 index 00000000000..83adf2ba677 --- /dev/null +++ b/addons/im_livechat/static/src/composer/suggestion_service_patch.js @@ -0,0 +1,72 @@ +/* @odoo-module */ + +import { patch } from "@web/core/utils/patch"; +import { SuggestionService } from "@mail/composer/suggestion_service"; +import { cleanTerm } from "@mail/utils/format"; +import { _t } from "@web/core/l10n/translation"; + +patch(SuggestionService.prototype, "im_livechat", { + getSupportedDelimiters(thread) { + return thread?.type === "livechat" + ? [...this._super(...arguments), ":"] + : this._super(...arguments); + }, + /** + * Returns suggestions that match the given search term from specified type. + * + * @param {Object} [param0={}] + * @param {String} [param0.delimiter] can be one one of the following: ["@", ":", "#", "/"] + * @param {String} [param0.term] + * @param {Object} [options={}] + * @param {Integer} [options.thread] prioritize and/or restrict + * result in the context of given thread + * @returns {[mainSuggestion[], extraSuggestion[]]} + */ + searchSuggestions({ delimiter, term }, { thread } = {}, sort = false) { + if (delimiter === ":") { + return this.searchCannedResponseSuggestions(cleanTerm(term), sort); + } + return this._super(...arguments); + }, + + searchCannedResponseSuggestions(cleanedSearchTerm, sort) { + const cannedResponses = this.store.cannedResponses + .filter((cannedResponse) => { + return cleanTerm(cannedResponse.name).includes(cleanedSearchTerm); + }) + .map(({ id, name, substitution }) => { + return { + id, + name, + substitution: _t(substitution), + }; + }); + const sortFunc = (c1, c2) => { + const cleanedName1 = cleanTerm(c1.name ?? ""); + const cleanedName2 = cleanTerm(c2.name ?? ""); + if ( + cleanedName1.startsWith(cleanedSearchTerm) && + !cleanedName2.startsWith(cleanedSearchTerm) + ) { + return -1; + } + if ( + !cleanedName1.startsWith(cleanedSearchTerm) && + cleanedName2.startsWith(cleanedSearchTerm) + ) { + return 1; + } + if (cleanedName1 < cleanedName2) { + return -1; + } + if (cleanedName1 > cleanedName2) { + return 1; + } + return c1.id - c2.id; + }; + return { + type: "CannedResponse", + mainSuggestions: sort ? cannedResponses.sort(sortFunc) : cannedResponses, + }; + }, +}); diff --git a/addons/im_livechat/static/tests/composer_patch_tests.js b/addons/im_livechat/static/tests/composer_patch_tests.js index 51b8fa263b5..b2d2808e892 100644 --- a/addons/im_livechat/static/tests/composer_patch_tests.js +++ b/addons/im_livechat/static/tests/composer_patch_tests.js @@ -1,7 +1,13 @@ /** @odoo-module */ import { startServer } from "@bus/../tests/helpers/mock_python_environment"; -import { afterNextRender, click, dragenterFiles, start } from "@mail/../tests/helpers/test_utils"; +import { + afterNextRender, + insertText, + click, + dragenterFiles, + start, +} from "@mail/../tests/helpers/test_utils"; import { editInput, nextTick, triggerHotkey } from "@web/../tests/helpers/utils"; QUnit.module("composer (patch)"); @@ -87,3 +93,120 @@ QUnit.test('Receives visitor typing status "is typing"', async (assert) => { ); assert.containsOnce($, ".o-mail-Typing:contains(Visitor 20 is typing...)"); }); + +QUnit.test('display canned response suggestions on typing ":"', async (assert) => { + const pyEnv = await startServer(); + const channelId = pyEnv["mail.channel"].create({ + anonymous_name: "Mario", + channel_member_ids: [ + [0, 0, { partner_id: pyEnv.currentPartnerId }], + [0, 0, { partner_id: pyEnv.publicPartnerId }], + ], + channel_type: "livechat", + livechat_operator_id: pyEnv.currentPartnerId, + }); + pyEnv["mail.shortcode"].create({ + source: "hello", + substitution: "Hello! How are you?", + }); + const { openDiscuss } = await start(); + await openDiscuss(channelId); + assert.containsNone($, ".o-mail-Composer-suggestionList .o-open"); + await insertText(".o-mail-Composer-input", ":"); + assert.containsOnce($, ".o-mail-Composer-suggestionList .o-open"); +}); + +QUnit.test("use a canned response", async (assert) => { + const pyEnv = await startServer(); + const channelId = pyEnv["mail.channel"].create({ + anonymous_name: "Mario", + channel_member_ids: [ + [0, 0, { partner_id: pyEnv.currentPartnerId }], + [0, 0, { partner_id: pyEnv.publicPartnerId }], + ], + channel_type: "livechat", + livechat_operator_id: pyEnv.currentPartnerId, + }); + pyEnv["mail.shortcode"].create({ + source: "hello", + substitution: "Hello! How are you?", + }); + const { openDiscuss } = await start(); + await openDiscuss(channelId); + assert.containsNone($, ".o-mail-Composer-suggestionList .o-open"); + assert.strictEqual($(".o-mail-Composer-input").val(), ""); + await insertText(".o-mail-Composer-input", ":"); + assert.containsOnce($, ".o-mail-Composer-suggestion"); + await click(".o-mail-Composer-suggestion"); + assert.strictEqual( + $(".o-mail-Composer-input").val().replace(/\s/, " "), + "Hello! How are you? ", + "canned response + additional whitespace afterwards" + ); +}); + +QUnit.test("use a canned response some text", async (assert) => { + const pyEnv = await startServer(); + const channelId = pyEnv["mail.channel"].create({ + anonymous_name: "Mario", + channel_member_ids: [ + [0, 0, { partner_id: pyEnv.currentPartnerId }], + [0, 0, { partner_id: pyEnv.publicPartnerId }], + ], + channel_type: "livechat", + livechat_operator_id: pyEnv.currentPartnerId, + }); + pyEnv["mail.shortcode"].create({ + source: "hello", + substitution: "Hello! How are you?", + }); + const { openDiscuss } = await start(); + await openDiscuss(channelId); + assert.containsNone($, ".o-mail-Composer-suggestion"); + assert.strictEqual($(".o-mail-Composer-input").val(), ""); + await insertText(".o-mail-Composer-input", "bluhbluh "); + assert.strictEqual($(".o-mail-Composer-input").val(), "bluhbluh "); + await insertText(".o-mail-Composer-input", ":"); + assert.containsOnce($, ".o-mail-Composer-suggestion"); + await click(".o-mail-Composer-suggestion"); + assert.strictEqual( + $(".o-mail-Composer-input").val().replace(/\s/, " "), + "bluhbluh Hello! How are you? ", + "previous content + canned response substitution + additional whitespace afterwards" + ); +}); + +QUnit.test("add an emoji after a canned response", async (assert) => { + const pyEnv = await startServer(); + const channelId = pyEnv["mail.channel"].create({ + anonymous_name: "Visitor 20", + channel_member_ids: [ + [0, 0, { partner_id: pyEnv.currentPartnerId }], + [0, 0, { partner_id: pyEnv.publicPartnerId }], + ], + channel_type: "livechat", + livechat_operator_id: pyEnv.currentPartnerId, + }); + pyEnv["mail.shortcode"].create({ + source: "hello", + substitution: "Hello! How are you?", + }); + const { openDiscuss } = await start(); + await openDiscuss(channelId); + assert.containsNone($, ".o-mail-Composer-suggestion"); + assert.strictEqual($(".o-mail-Composer-input").val(), ""); + await insertText(".o-mail-Composer-input", ":"); + assert.containsOnce($, ".o-mail-Composer-suggestion"); + await click(".o-mail-Composer-suggestion"); + assert.strictEqual( + $(".o-mail-Composer-input").val().replace(/\s/, " "), + "Hello! How are you? ", + "previous content + canned response substitution + additional whitespace afterwards" + ); + await click("button[aria-label='Emojis']"); + await click(".o-mail-Emoji:contains(😊)"); + assert.strictEqual( + $(".o-mail-Composer-input").val().replace(/\s/, " "), + "Hello! How are you? 😊" + ); +}); diff --git a/addons/mail/static/src/composer/suggestion_hook.js b/addons/mail/static/src/composer/suggestion_hook.js index a6eec7b3e51..e696e8fc547 100644 --- a/addons/mail/static/src/composer/suggestion_hook.js +++ b/addons/mail/static/src/composer/suggestion_hook.js @@ -38,7 +38,6 @@ export function useSuggestion() { if (selectionStart > 0) { candidatePositions.push(selectionStart - 1); } - const suggestionDelimiters = ["@", ":", "#", "/"]; for (const candidatePosition of candidatePositions) { if (candidatePosition < 0 || candidatePosition >= content.length) { continue; @@ -47,7 +46,11 @@ export function useSuggestion() { if (candidateChar === "/" && candidatePosition !== 0) { continue; } - if (!suggestionDelimiters.includes(candidateChar)) { + if ( + !suggestionService + .getSupportedDelimiters(comp.props.composer.thread) + .includes(candidateChar) + ) { continue; } const charBeforeCandidate = content[candidatePosition - 1]; diff --git a/addons/mail/static/src/composer/suggestion_service.js b/addons/mail/static/src/composer/suggestion_service.js index abd5d2bc7ac..0d59344ab0c 100644 --- a/addons/mail/static/src/composer/suggestion_service.js +++ b/addons/mail/static/src/composer/suggestion_service.js @@ -2,7 +2,6 @@ import { cleanTerm } from "@mail/utils/format"; import { registry } from "@web/core/registry"; -import { _t } from "@web/core/l10n/translation"; const commandRegistry = registry.category("mail.channel_commands"); @@ -19,6 +18,10 @@ export class SuggestionService { this.channelMemberService = services["mail.channel.member"]; } + getSupportedDelimiters(thread) { + return ["@", "#", "/"]; + } + async fetchSuggestions({ delimiter, term }, { thread, onFetched } = {}) { const cleanedSearchTerm = cleanTerm(term); switch (delimiter) { @@ -26,8 +29,6 @@ export class SuggestionService { this.fetchPartners(cleanedSearchTerm, thread).then(onFetched); break; } - case ":": - break; case "#": this.fetchThreads(cleanedSearchTerm).then(onFetched); break; @@ -79,7 +80,7 @@ export class SuggestionService { * Returns suggestions that match the given search term from specified type. * * @param {Object} [param0={}] - * @param {String} [param0.delimiter] can be one one of the following: ["@", ":", "#", "/"] + * @param {String} [param0.delimiter] can be one one of the following: ["@", "#", "/"] * @param {String} [param0.term] * @param {Object} [options={}] * @param {Integer} [options.thread] prioritize and/or restrict @@ -92,8 +93,6 @@ export class SuggestionService { case "@": { return this.searchPartnerSuggestions(cleanedSearchTerm, thread, sort); } - case ":": - return this.searchCannedResponseSuggestions(cleanedSearchTerm, sort); case "#": return this.searchChannelSuggestions(cleanedSearchTerm, thread, sort); case "/": @@ -285,47 +284,6 @@ export class SuggestionService { }; } - searchCannedResponseSuggestions(cleanedSearchTerm, sort) { - const cannedResponses = this.store.cannedResponses - .filter((cannedResponse) => { - return cleanTerm(cannedResponse.name).includes(cleanedSearchTerm); - }) - .map(({ id, name, substitution }) => { - return { - id, - name, - substitution: _t(substitution), - }; - }); - const sortFunc = (c1, c2) => { - const cleanedName1 = cleanTerm(c1.name ?? ""); - const cleanedName2 = cleanTerm(c2.name ?? ""); - if ( - cleanedName1.startsWith(cleanedSearchTerm) && - !cleanedName2.startsWith(cleanedSearchTerm) - ) { - return -1; - } - if ( - !cleanedName1.startsWith(cleanedSearchTerm) && - cleanedName2.startsWith(cleanedSearchTerm) - ) { - return 1; - } - if (cleanedName1 < cleanedName2) { - return -1; - } - if (cleanedName1 > cleanedName2) { - return 1; - } - return c1.id - c2.id; - }; - return { - type: "CannedResponse", - mainSuggestions: sort ? cannedResponses.sort(sortFunc) : cannedResponses, - }; - } - searchChannelSuggestions(cleanedSearchTerm, thread, sort) { let threads; if ( diff --git a/addons/mail/static/tests/composer/composer_tests.js b/addons/mail/static/tests/composer/composer_tests.js index 72f25014727..e9405d594b7 100644 --- a/addons/mail/static/tests/composer/composer_tests.js +++ b/addons/mail/static/tests/composer/composer_tests.js @@ -393,36 +393,6 @@ QUnit.test("add an emoji after a command", async (assert) => { assert.strictEqual($(".o-mail-Composer-input").val().replace(/\s/, " "), "/who 😊"); }); -QUnit.test("add an emoji after a canned response", async (assert) => { - const pyEnv = await startServer(); - const channelId = pyEnv["mail.channel"].create({ - name: "Mario Party", - }); - pyEnv["mail.shortcode"].create({ - source: "hello", - substitution: "Hello! How are you?", - }); - const { openDiscuss } = await start(); - await openDiscuss(channelId); - assert.containsNone($, ".o-mail-Composer-suggestion"); - assert.strictEqual($(".o-mail-Composer-input").val(), ""); - await insertText(".o-mail-Composer-input", ":"); - assert.containsOnce($, ".o-mail-Composer-suggestion"); - await click(".o-mail-Composer-suggestion"); - assert.strictEqual( - $(".o-mail-Composer-input").val().replace(/\s/, " "), - "Hello! How are you? ", - "previous content + canned response substitution + additional whitespace afterwards" - ); - - await click("button[aria-label='Emojis']"); - await click(".o-mail-Emoji:contains(😊)"); - assert.strictEqual( - $(".o-mail-Composer-input").val().replace(/\s/, " "), - "Hello! How are you? 😊" - ); -}); - QUnit.test("add an emoji after a partner mention", async (assert) => { const pyEnv = await startServer(); const partnerId = pyEnv["res.partner"].create({ diff --git a/addons/mail/static/tests/suggestion/suggestion_tests.js b/addons/mail/static/tests/suggestion/suggestion_tests.js index bff5f6a4561..ddc256a2735 100644 --- a/addons/mail/static/tests/suggestion/suggestion_tests.js +++ b/addons/mail/static/tests/suggestion/suggestion_tests.js @@ -177,64 +177,6 @@ QUnit.test( } ); -QUnit.test('display canned response suggestions on typing ":"', async (assert) => { - const pyEnv = await startServer(); - const channelId = pyEnv["mail.channel"].create({ name: "Mario Party" }); - pyEnv["mail.shortcode"].create({ - source: "hello", - substitution: "Hello! How are you?", - }); - const { openDiscuss } = await start(); - await openDiscuss(channelId); - assert.containsNone($, ".o-mail-Composer-suggestionList .o-open"); - await insertText(".o-mail-Composer-input", ":"); - assert.containsOnce($, ".o-mail-Composer-suggestionList .o-open"); -}); - -QUnit.test("use a canned response", async (assert) => { - const pyEnv = await startServer(); - const channelId = pyEnv["mail.channel"].create({ name: "Mario Party" }); - pyEnv["mail.shortcode"].create({ - source: "hello", - substitution: "Hello! How are you?", - }); - const { openDiscuss } = await start(); - await openDiscuss(channelId); - assert.containsNone($, ".o-mail-Composer-suggestionList .o-open"); - assert.strictEqual($(".o-mail-Composer-input").val(), ""); - await insertText(".o-mail-Composer-input", ":"); - assert.containsOnce($, ".o-mail-Composer-suggestion"); - await click(".o-mail-Composer-suggestion"); - assert.strictEqual( - $(".o-mail-Composer-input").val().replace(/\s/, " "), - "Hello! How are you? ", - "canned response + additional whitespace afterwards" - ); -}); - -QUnit.test("use a canned response some text", async (assert) => { - const pyEnv = await startServer(); - const channelId = pyEnv["mail.channel"].create({ name: "Mario Party" }); - pyEnv["mail.shortcode"].create({ - source: "hello", - substitution: "Hello! How are you?", - }); - const { openDiscuss } = await start(); - await openDiscuss(channelId); - assert.containsNone($, ".o-mail-Composer-suggestion"); - assert.strictEqual($(".o-mail-Composer-input").val(), ""); - await insertText(".o-mail-Composer-input", "bluhbluh "); - assert.strictEqual($(".o-mail-Composer-input").val(), "bluhbluh "); - await insertText(".o-mail-Composer-input", ":"); - assert.containsOnce($, ".o-mail-Composer-suggestion"); - await click(".o-mail-Composer-suggestion"); - assert.strictEqual( - $(".o-mail-Composer-input").val().replace(/\s/, " "), - "bluhbluh Hello! How are you? ", - "previous content + canned response substitution + additional whitespace afterwards" - ); -}); - QUnit.test('display channel mention suggestions on typing "#"', async (assert) => { const pyEnv = await startServer(); const channelId = pyEnv["mail.channel"].create({