[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:
committed by
Alexandre Kühn
parent
044f2a9c58
commit
6705c2f4fd
@@ -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({
|
||||
|
||||
Reference in New Issue
Block a user