[IMP] mail,im_livechat: no canned reply in mail

Canned response should only enabled in livechat thread.
task-3208438

closes odoo/odoo#116388

Signed-off-by: Alexandre Kühn (aku) <aku@odoo.com>
This commit is contained in:
Zelong Lin
2023-04-11 18:49:24 +02:00
committed by Alexandre Kühn
parent 044f2a9c58
commit 6705c2f4fd
7 changed files with 209 additions and 139 deletions
+3 -1
View File
@@ -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 <b>:shortcut</b> to insert a canned response in your message.<br>")
if self.channel_type == 'livechat':
return msg + _("Type <b>:shortcut</b> to insert a canned response in your message.<br>")
return msg
def execute_command_history(self, **kwargs):
self.env['bus.bus']._sendone(self.uuid, 'im_livechat.history_command', {'id': self.id})
@@ -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,
};
},
});
@@ -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? 😊"
);
});
@@ -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];
@@ -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 (
@@ -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({
@@ -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({