[IMP] mail: avatar card preview v2
This commit changes the behavior of the avatar card preview so that it is now triggered on click instead of on hover and the previous behavior of the click event (open chat) is therefore removed. It also adds the functionality to the Message and Activity components of discuss so that clicking on the avatar inside these components will also show the card. It also makes sure that the id of the user is added to the persona even if nothing indicates that it should. task-3442819 closes odoo/odoo#131355 Related: odoo/enterprise#47084 Signed-off-by: Francois Georis (fge) <fge@odoo.com>
This commit is contained in:
@@ -42,7 +42,7 @@ class TestLeadConvertMass(crm_common.TestLeadConvertMassCommon):
|
||||
with self.assertQueryCount(user_sales_manager=0):
|
||||
test_leads = self.env['crm.lead'].browse(test_leads.ids)
|
||||
|
||||
with self.assertQueryCount(user_sales_manager=580): # crm 521 / com 516 / ent 516
|
||||
with self.assertQueryCount(user_sales_manager=581): # crm 521 / com 516 / ent 516
|
||||
test_leads._handle_salesmen_assignment(user_ids=user_ids, team_id=team_id)
|
||||
|
||||
self.assertEqual(test_leads.team_id, self.sales_team_convert)
|
||||
@@ -167,7 +167,7 @@ class TestLeadConvertMass(crm_common.TestLeadConvertMassCommon):
|
||||
user_ids = self.assign_users.ids
|
||||
|
||||
# randomness: at least 1 query
|
||||
with self.assertQueryCount(user_sales_manager=1797): # crm 1410 / com 1677 / ent 1685
|
||||
with self.assertQueryCount(user_sales_manager=1798): # crm 1410 / com 1677 / ent 1685
|
||||
mass_convert = self.env['crm.lead2opportunity.partner.mass'].with_context({
|
||||
'active_model': 'crm.lead',
|
||||
'active_ids': test_leads.ids,
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<templates xml:space="preserve">
|
||||
<t t-inherit="mail.AvatarCardPopover" t-inherit-mode="extension">
|
||||
<xpath expr="//div[hasclass('o_avatar_card_buttons')]" position="inside">
|
||||
<button class="btn btn-secondary btn-sm" t-on-click.stop="onClickViewEmployee"> View profile </button>
|
||||
<button class="btn btn-secondary btn-sm" t-if="user.employee_id" t-on-click.stop="onClickViewEmployee">View profile</button>
|
||||
</xpath>
|
||||
</t>
|
||||
|
||||
|
||||
@@ -59,34 +59,34 @@ QUnit.test("many2one_avatar_employee widget in list view", async function (asser
|
||||
document.querySelectorAll(".o_data_cell div[name='employee_id']")[2].innerText,
|
||||
"Mario"
|
||||
);
|
||||
|
||||
// TODO: avatar card employee
|
||||
// click on first employee
|
||||
dom.click(document.querySelector(".o_data_cell .o_m2o_avatar > img"));
|
||||
await contains(".o-mail-ChatWindow-name");
|
||||
assert.verifySteps([`read hr.employee.public ${employeeId_1}`]);
|
||||
assert.strictEqual(document.querySelector(".o-mail-ChatWindow-name").textContent, "Mario");
|
||||
// dom.click(document.querySelector(".o_data_cell .o_m2o_avatar > img"));
|
||||
// await contains(".o-mail-ChatWindow-name");
|
||||
// assert.verifySteps([`read hr.employee.public ${employeeId_1}`]);
|
||||
// assert.strictEqual(document.querySelector(".o-mail-ChatWindow-name").textContent, "Mario");
|
||||
|
||||
// click on second employee
|
||||
dom.click(document.querySelectorAll(".o_data_cell .o_m2o_avatar > img")[1]);
|
||||
await contains(".o-mail-ChatWindow-name", { count: 2 });
|
||||
assert.verifySteps([`read hr.employee.public ${employeeId_2}`]);
|
||||
assert.strictEqual(
|
||||
document.querySelectorAll(".o-mail-ChatWindow-name")[1].textContent,
|
||||
"Luigi"
|
||||
);
|
||||
// // click on second employee
|
||||
// dom.click(document.querySelectorAll(".o_data_cell .o_m2o_avatar > img")[1]);
|
||||
// await contains(".o-mail-ChatWindow-name", { count: 2 });
|
||||
// assert.verifySteps([`read hr.employee.public ${employeeId_2}`]);
|
||||
// assert.strictEqual(
|
||||
// document.querySelectorAll(".o-mail-ChatWindow-name")[1].textContent,
|
||||
// "Luigi"
|
||||
// );
|
||||
|
||||
// click on third employee (same as first)
|
||||
dom.click(document.querySelectorAll(".o_data_cell .o_m2o_avatar > img")[2]);
|
||||
assert.containsN(
|
||||
document.body,
|
||||
".o-mail-ChatWindow-name",
|
||||
2,
|
||||
"should still have only 2 chat windows because third is the same partner as first"
|
||||
);
|
||||
assert.verifySteps(
|
||||
[],
|
||||
"employee should not have been read again because we already know its partner"
|
||||
);
|
||||
// // click on third employee (same as first)
|
||||
// dom.click(document.querySelectorAll(".o_data_cell .o_m2o_avatar > img")[2]);
|
||||
// assert.containsN(
|
||||
// document.body,
|
||||
// ".o-mail-ChatWindow-name",
|
||||
// 2,
|
||||
// "should still have only 2 chat windows because third is the same partner as first"
|
||||
// );
|
||||
// assert.verifySteps(
|
||||
// [],
|
||||
// "employee should not have been read again because we already know its partner"
|
||||
// );
|
||||
});
|
||||
|
||||
QUnit.test("many2one_avatar_employee widget in kanban view", async function (assert) {
|
||||
@@ -158,10 +158,8 @@ QUnit.test(
|
||||
});
|
||||
await contains(".o_field_widget[name=employee_id] input", { value: "Mario" });
|
||||
await dom.click(document.querySelector(".o_m2o_avatar > img"));
|
||||
await contains(
|
||||
".o_notification.border-info:contains(You can only chat with employees that have a dedicated user.)"
|
||||
);
|
||||
assert.verifySteps([`web_read m2x.avatar.employee ${avatarId}`]);
|
||||
// Nothing should happen
|
||||
}
|
||||
);
|
||||
|
||||
@@ -218,12 +216,10 @@ QUnit.test("many2many_avatar_employee widget in form view", async function (asse
|
||||
await dom.click(
|
||||
document.querySelectorAll(".o_field_many2many_avatar_employee .o_tag .o_m2m_avatar")[1]
|
||||
);
|
||||
// TODO: avatar card employee
|
||||
assert.verifySteps([
|
||||
`web_read m2x.avatar.employee ${avatarId_1}`,
|
||||
`read hr.employee.public ${employeeId_1}`,
|
||||
`read hr.employee.public ${employeeId_2}`,
|
||||
]);
|
||||
await contains(".o-mail-ChatWindow-name", { count: 2 });
|
||||
});
|
||||
|
||||
QUnit.test("many2many_avatar_employee widget in list view", async function (assert) {
|
||||
@@ -265,21 +261,21 @@ QUnit.test("many2many_avatar_employee widget in list view", async function (asse
|
||||
2,
|
||||
"should have two avatar"
|
||||
);
|
||||
// TODO: avatar card employee
|
||||
// // click on first employee badge
|
||||
// dom.click(document.querySelector(".o_data_cell .o_m2m_avatar"));
|
||||
// await contains(".o-mail-ChatWindow-name");
|
||||
// assert.verifySteps([`read hr.employee.public ${employeeId_1}`]);
|
||||
// assert.strictEqual(document.querySelector(".o-mail-ChatWindow-name").textContent, "Mario");
|
||||
|
||||
// click on first employee badge
|
||||
dom.click(document.querySelector(".o_data_cell .o_m2m_avatar"));
|
||||
await contains(".o-mail-ChatWindow-name");
|
||||
assert.verifySteps([`read hr.employee.public ${employeeId_1}`]);
|
||||
assert.strictEqual(document.querySelector(".o-mail-ChatWindow-name").textContent, "Mario");
|
||||
|
||||
// click on second employee
|
||||
dom.click(document.querySelectorAll(".o_data_cell .o_m2m_avatar")[1]);
|
||||
await contains(".o-mail-ChatWindow-name", { count: 2 });
|
||||
assert.verifySteps([`read hr.employee.public ${employeeId_2}`]);
|
||||
assert.strictEqual(
|
||||
document.querySelectorAll(".o-mail-ChatWindow-name")[1].textContent,
|
||||
"Yoshi"
|
||||
);
|
||||
// // click on second employee
|
||||
// dom.click(document.querySelectorAll(".o_data_cell .o_m2m_avatar")[1]);
|
||||
// await contains(".o-mail-ChatWindow-name", { count: 2 });
|
||||
// assert.verifySteps([`read hr.employee.public ${employeeId_2}`]);
|
||||
// assert.strictEqual(
|
||||
// document.querySelectorAll(".o-mail-ChatWindow-name")[1].textContent,
|
||||
// "Yoshi"
|
||||
// );
|
||||
});
|
||||
|
||||
QUnit.test("many2many_avatar_employee widget in kanban view", async function (assert) {
|
||||
@@ -346,12 +342,9 @@ QUnit.test("many2many_avatar_employee widget in kanban view", async function (as
|
||||
`/web/image/hr.employee.public/${employeeId_1}/avatar_128`
|
||||
);
|
||||
|
||||
await dom.click(document.querySelectorAll(".o_kanban_record img.o_m2m_avatar")[1]);
|
||||
await dom.click(document.querySelectorAll(".o_kanban_record img.o_m2m_avatar")[0]);
|
||||
assert.verifySteps([
|
||||
`read hr.employee.public ${employeeId_1}`,
|
||||
`read hr.employee.public ${employeeId_2}`,
|
||||
]);
|
||||
await dom.click(document.querySelectorAll('.o_kanban_record img.o_m2m_avatar')[1]);
|
||||
await dom.click(document.querySelectorAll('.o_kanban_record img.o_m2m_avatar')[0]);
|
||||
// TODO: avatar card employee
|
||||
});
|
||||
|
||||
QUnit.test(
|
||||
@@ -400,14 +393,9 @@ QUnit.test(
|
||||
await dom.click(
|
||||
document.querySelectorAll(".o_field_many2many_avatar_employee .o_tag .o_m2m_avatar")[1]
|
||||
);
|
||||
await contains(
|
||||
".o_notification.border-info:contains(You can only chat with employees that have a dedicated user.)"
|
||||
);
|
||||
assert.verifySteps([
|
||||
`web_read m2x.avatar.employee ${employeeId_1}`,
|
||||
`read hr.employee.public ${employeeId_1}`,
|
||||
`read hr.employee.public ${employeeId_2}`,
|
||||
]);
|
||||
await contains(".o-mail-ChatWindow-name");
|
||||
// TODO: avtar card employee
|
||||
}
|
||||
);
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
/* @odoo-module */
|
||||
|
||||
import { startServer } from "@bus/../tests/helpers/mock_python_environment";
|
||||
|
||||
import { start } from "@mail/../tests/helpers/test_utils";
|
||||
import { patchWithCleanup, triggerEvent, getFixture, getNodesTextContent } from "@web/../tests/helpers/utils";
|
||||
import { startServer } from "@bus/../tests/helpers/mock_python_environment";
|
||||
import { click, patchWithCleanup, getFixture, getNodesTextContent } from "@web/../tests/helpers/utils";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { browser } from "@web/core/browser/browser";
|
||||
import { EventBus } from "@odoo/owl";
|
||||
@@ -71,6 +70,7 @@ QUnit.module("M2XAvatarUser", ({ beforeEach }) => {
|
||||
"email",
|
||||
"phone",
|
||||
"im_status",
|
||||
"share",
|
||||
"work_phone",
|
||||
"work_email",
|
||||
"job_title",
|
||||
@@ -107,13 +107,12 @@ QUnit.module("M2XAvatarUser", ({ beforeEach }) => {
|
||||
},
|
||||
});
|
||||
// Open card
|
||||
await triggerEvent(target, ".o_m2o_avatar > img", "mouseover");
|
||||
assert.verifySteps(["setTimeout of 350ms", "setTimeout of 250ms", "user read"]);
|
||||
await click(document, ".o_m2o_avatar > img");
|
||||
assert.verifySteps(["setTimeout of 250ms", "user read"]);
|
||||
assert.containsOnce(target, ".o_avatar_card");
|
||||
assert.deepEqual(getNodesTextContent(target.querySelectorAll(".o_card_user_infos > *")), ['Mario', 'sub manager', 'Managemment', ' Mario@odoo.pro', ' +585555555']);
|
||||
assert.deepEqual(getNodesTextContent(target.querySelectorAll(".o_card_user_infos > *")), ['Mario', 'sub manager', 'Managemment', 'Mario@odoo.pro', '+585555555']);
|
||||
// Close card
|
||||
await triggerEvent(target, ".o_control_panel", "mouseover");
|
||||
assert.verifySteps(["setTimeout of 400ms"]);
|
||||
await click(document, ".o_action_manager");
|
||||
assert.containsNone(target, ".o_avatar_card");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -32,7 +32,7 @@ class TestWorkEntryHolidaysPerformance(TestWorkEntryHolidaysBase):
|
||||
self.richard_emp.generate_work_entries(date(2018, 1, 1), date(2018, 1, 2))
|
||||
leave = self.create_leave(datetime(2018, 1, 1, 7, 0), datetime(2018, 1, 1, 18, 0))
|
||||
|
||||
with self.assertQueryCount(__system__=99, admin=102):
|
||||
with self.assertQueryCount(__system__=100, admin=103):
|
||||
leave.action_validate()
|
||||
leave.action_refuse()
|
||||
|
||||
@@ -48,7 +48,7 @@ class TestWorkEntryHolidaysPerformance(TestWorkEntryHolidaysBase):
|
||||
@users('__system__', 'admin')
|
||||
@warmup
|
||||
def test_performance_leave_create(self):
|
||||
with self.assertQueryCount(__system__=45, admin=45):
|
||||
with self.assertQueryCount(__system__=46, admin=46):
|
||||
leave = self.create_leave(datetime(2018, 1, 1, 7, 0), datetime(2018, 1, 1, 18, 0))
|
||||
leave.action_refuse()
|
||||
|
||||
@@ -57,7 +57,7 @@ class TestWorkEntryHolidaysPerformance(TestWorkEntryHolidaysBase):
|
||||
def test_performance_leave_confirm(self):
|
||||
leave = self.create_leave(datetime(2018, 1, 1, 7, 0), datetime(2018, 1, 1, 18, 0))
|
||||
leave.action_draft()
|
||||
with self.assertQueryCount(__system__=41, admin=40):
|
||||
with self.assertQueryCount(__system__=42, admin=41):
|
||||
leave.action_confirm()
|
||||
leave.state = 'refuse'
|
||||
|
||||
|
||||
@@ -78,7 +78,6 @@ Help your customers with this chat, and analyse their feedback.
|
||||
'im_livechat/static/src/core_ui/**/*',
|
||||
'im_livechat/static/src/discuss_app/**/*',
|
||||
'im_livechat/static/src/messaging_menu/**/*',
|
||||
'im_livechat/static/src/web/**/*',
|
||||
'im_livechat/static/src/scss/im_livechat_history.scss',
|
||||
'im_livechat/static/src/scss/im_livechat_form.scss',
|
||||
],
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
/* @odoo-module */
|
||||
|
||||
import { Message } from "@mail/core/common/message";
|
||||
import "@mail/discuss/core/web/message_patch"; // dependency ordering
|
||||
|
||||
import { patch } from "@web/core/utils/patch";
|
||||
|
||||
patch(Message.prototype, {
|
||||
hasOpenChatFeature() {
|
||||
return this.message.originThread?.channel?.channel_type === "livechat"
|
||||
? false
|
||||
: super.hasOpenChatFeature();
|
||||
},
|
||||
});
|
||||
@@ -1,30 +0,0 @@
|
||||
/* @odoo-module */
|
||||
|
||||
import { startServer } from "@bus/../tests/helpers/mock_python_environment";
|
||||
|
||||
import { Command } from "@mail/../tests/helpers/command";
|
||||
import { start } from "@mail/../tests/helpers/test_utils";
|
||||
|
||||
QUnit.module("message (patch)");
|
||||
|
||||
QUnit.test("redirect to author (open profile) in livechat", async (assert) => {
|
||||
const pyEnv = await startServer();
|
||||
const partnerId = pyEnv["res.partner"].create({ name: "Demo" });
|
||||
pyEnv["res.users"].create({ partner_id: partnerId });
|
||||
const channelId = pyEnv["discuss.channel"].create({
|
||||
channel_member_ids: [
|
||||
Command.create({ partner_id: pyEnv.currentPartnerId }),
|
||||
Command.create({ partner_id: partnerId }),
|
||||
],
|
||||
channel_type: "livechat",
|
||||
});
|
||||
pyEnv["mail.message"].create({
|
||||
author_id: partnerId,
|
||||
body: "not empty",
|
||||
model: "discuss.channel",
|
||||
res_id: channelId,
|
||||
});
|
||||
const { openDiscuss } = await start();
|
||||
await openDiscuss(channelId);
|
||||
assert.containsOnce($, ".o-mail-Message-avatarContainer[title='Open profile']");
|
||||
});
|
||||
@@ -58,6 +58,10 @@ class TestImLivechatMessage(HttpCase):
|
||||
'id': self.users[1].partner_id.id,
|
||||
'is_company': self.users[1].partner_id.is_company,
|
||||
'name': "test1",
|
||||
'user': {
|
||||
'id': self.users[1].id,
|
||||
'isInternalUser': self.users[1]._is_internal(),
|
||||
}
|
||||
},
|
||||
'body': message.body,
|
||||
'date': message.date,
|
||||
|
||||
@@ -876,11 +876,7 @@ class Message(models.Model):
|
||||
thread_ids_by_model_name[message.model].add(message.res_id)
|
||||
for vals in vals_list:
|
||||
message_sudo = self.browse(vals['id']).sudo().with_prefetch(self.ids)
|
||||
author = {
|
||||
'id': message_sudo.author_id.id,
|
||||
'is_company': message_sudo.author_id.is_company,
|
||||
'name': message_sudo.author_id.name,
|
||||
} if message_sudo.author_id else [('clear',)]
|
||||
author = message_sudo.author_id.mail_partner_format({'id': True, 'name': True, 'is_company': True, 'user': {"id": True}}).get(message_sudo.author_id) if message_sudo.author_id else [('clear',)]
|
||||
guestAuthor = {
|
||||
'id': message_sudo.author_guest_id.id,
|
||||
'name': message_sudo.author_guest_id.name,
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useAttachmentUploader } from "@mail/core/common/attachment_uploader_hoo
|
||||
import { ActivityMailTemplate } from "@mail/core/web/activity_mail_template";
|
||||
import { ActivityMarkAsDone } from "@mail/core/web/activity_markasdone_popover";
|
||||
import { computeDelay, getMsToTomorrow } from "@mail/utils/common/dates";
|
||||
import { AvatarCardPopover } from "@mail/discuss/web/avatar_card/avatar_card_popover";
|
||||
|
||||
import { Component, onMounted, onWillUnmount, useState } from "@odoo/owl";
|
||||
|
||||
@@ -34,6 +35,7 @@ export class Activity extends Component {
|
||||
this.threadService = useService("mail.thread");
|
||||
this.state = useState({ showDetails: false });
|
||||
this.popover = usePopover(ActivityMarkAsDone, { position: "right" });
|
||||
this.avatarCard = usePopover(AvatarCardPopover);
|
||||
onMounted(() => {
|
||||
this.updateDelayAtNight();
|
||||
});
|
||||
@@ -83,6 +85,15 @@ export class Activity extends Component {
|
||||
await this.threadService.fetchNewMessages(this.thread);
|
||||
}
|
||||
|
||||
onClickAvatar(ev) {
|
||||
const target = ev.currentTarget;
|
||||
if (!this.avatarCard.isOpen) {
|
||||
this.avatarCard.open(target, {
|
||||
id: this.props.data.user_id[0],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async edit() {
|
||||
const { id, res_model, res_id } = this.props.data;
|
||||
await this.env.services["mail.activity"].schedule(res_model, res_id, id);
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<t t-set="activity" t-value="props.data"/>
|
||||
<div class="o-mail-Activity d-flex px-3 py-1 mb-2">
|
||||
<div class="o-mail-Activity-sidebar flex-shrink-0 position-relative">
|
||||
<img class="w-100 h-100 rounded" t-attf-src="/web/image/res.users/{{activity.user_id[0]}}/avatar_128"/>
|
||||
<img class="w-100 h-100 rounded cursor-pointer" t-attf-src="/web/image/res.users/{{activity.user_id[0]}}/avatar_128" t-on-click.stop.prevent="onClickAvatar"/>
|
||||
<div
|
||||
class="o-mail-Activity-iconContainer position-absolute top-100 start-100 translate-middle d-flex align-items-center justify-content-center mt-n1 ms-n1 rounded-circle w-50 h-50"
|
||||
t-att-class="{
|
||||
|
||||
@@ -13,15 +13,16 @@ import {
|
||||
} from "@web/views/fields/formatters";
|
||||
import { formatFloat } from "@web/core/utils/numbers";
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
import { usePopover } from "@web/core/popover/popover_hook";
|
||||
import { patch } from "@web/core/utils/patch";
|
||||
import { useState } from "@odoo/owl";
|
||||
import { AvatarCardPopover } from "@mail/discuss/web/avatar_card/avatar_card_popover";
|
||||
|
||||
patch(Message.prototype, {
|
||||
setup() {
|
||||
super.setup(...arguments);
|
||||
this.action = useService("action");
|
||||
this.userService = useService("user");
|
||||
this.messaging = useState(useService("mail.messaging"));
|
||||
this.avatarCard = usePopover(AvatarCardPopover);
|
||||
},
|
||||
get authorAvatarAttClass() {
|
||||
return {
|
||||
@@ -30,18 +31,20 @@ patch(Message.prototype, {
|
||||
};
|
||||
},
|
||||
getAuthorText() {
|
||||
return this.hasAuthorClickable() ? _t("Open profile") : undefined;
|
||||
return this.hasAuthorClickable() ? _t("Open card") : undefined;
|
||||
},
|
||||
hasAuthorClickable() {
|
||||
return this.message.author && !this.message.isSelfAuthored;
|
||||
return this.message.author?.user;
|
||||
},
|
||||
onClickAuthor(ev) {
|
||||
if (this.hasAuthorClickable()) {
|
||||
markEventHandled(ev, "Message.ClickAuthor");
|
||||
this.messaging.openDocument({
|
||||
model: "res.partner",
|
||||
id: this.message.author.id,
|
||||
});
|
||||
const target = ev.currentTarget;
|
||||
if (!this.avatarCard.isOpen) {
|
||||
this.avatarCard.open(target, {
|
||||
id: this.message.author.user.id,
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
openRecord() {
|
||||
|
||||
@@ -29,11 +29,12 @@
|
||||
</xpath>
|
||||
<xpath expr="//div[hasclass('o-mail-Message-avatarContainer')]" position="attributes">
|
||||
<attribute name="t-att-class">{ 'cursor-pointer': hasAuthorClickable() }</attribute>
|
||||
<attribute name="t-att-title">getAuthorText()</attribute>
|
||||
<attribute name="t-att-aria-label">getAuthorText()</attribute>
|
||||
<attribute name="t-on-click">ev => this.onClickAuthor(ev)</attribute>
|
||||
</xpath>
|
||||
<xpath expr="//span[hasclass('o-mail-Message-author')]" position="attributes">
|
||||
<attribute name="t-att-title">getAuthorText()</attribute>
|
||||
<attribute name="t-att-class">{ 'cursor-pointer': hasAuthorClickable() }</attribute>
|
||||
<attribute name="t-att-aria-label">getAuthorText()</attribute>
|
||||
<attribute name="t-on-click">ev => this.onClickAuthor(ev)</attribute>
|
||||
</xpath>
|
||||
</t>
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
/* @odoo-module */
|
||||
|
||||
import { Message } from "@mail/core/common/message";
|
||||
import "@mail/core/web/message_patch"; // dependency ordering
|
||||
import { markEventHandled } from "@web/core/utils/misc";
|
||||
|
||||
import { _t } from "@web/core/l10n/translation";
|
||||
import { patch } from "@web/core/utils/patch";
|
||||
|
||||
patch(Message.prototype, {
|
||||
getAuthorText() {
|
||||
return this.hasOpenChatFeature() ? _t("Open chat") : super.getAuthorText();
|
||||
},
|
||||
hasAuthorClickable() {
|
||||
return (
|
||||
super.hasAuthorClickable() &&
|
||||
this.message.author.type !== "guest" &&
|
||||
this.message.originThread?.channel?.channel_type !== "chat"
|
||||
);
|
||||
},
|
||||
hasOpenChatFeature() {
|
||||
return this.hasAuthorClickable();
|
||||
},
|
||||
onClickAuthor(ev) {
|
||||
if (this.hasOpenChatFeature()) {
|
||||
markEventHandled(ev, "Message.ClickAuthor");
|
||||
this.threadService.openChat({ partnerId: this.message.author.id });
|
||||
return;
|
||||
}
|
||||
return super.onClickAuthor(ev);
|
||||
},
|
||||
});
|
||||
@@ -21,7 +21,7 @@ export class AvatarCardPopover extends Component {
|
||||
}
|
||||
|
||||
get fieldNames() {
|
||||
return ["name", "email", "phone", "im_status"];
|
||||
return ["name", "email", "phone", "im_status", "share"];
|
||||
}
|
||||
|
||||
get email() {
|
||||
|
||||
@@ -21,17 +21,17 @@
|
||||
<span class="fw-bold" t-esc="user.name"/>
|
||||
<t t-if="userInfoTemplate" t-call="{{userInfoTemplate}}"/>
|
||||
<a t-if="email" t-att-href="'mailto:'+email">
|
||||
<i class="fa fa-fw fa-envelope"/> <t t-esc="email"/>
|
||||
<i class="fa fa-fw fa-envelope me-1"/><t t-esc="email"/>
|
||||
</a>
|
||||
<a t-if="phone" t-att-href="'tel:'+phone">
|
||||
<i class="fa fa-fw fa-phone"/> <t t-esc="phone"/>
|
||||
<i class="fa fa-fw fa-phone me-1"/><t t-esc="phone"/>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-wrap gap-2 mt-2">
|
||||
<div class="justify-content-end d-flex align-items-baseline">
|
||||
<div class="d-flex gap-2 o_avatar_card_buttons">
|
||||
<button class="btn btn-secondary btn-sm" t-on-click.stop="onSendClick"> Send message </button>
|
||||
<button t-if="!user.share" class="btn btn-secondary btn-sm" t-on-click.stop="onSendClick">Send message</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+26
-35
@@ -1,12 +1,10 @@
|
||||
/* @odoo-module */
|
||||
|
||||
import { useOpenChat } from "@mail/core/web/open_chat_hook";
|
||||
import { useAssignUserCommand } from "@mail/views/web/fields/assign_user_command_hook";
|
||||
|
||||
import { registry } from "@web/core/registry";
|
||||
import { TagsList } from "@web/core/tags_list/tags_list";
|
||||
import { usePopover } from "@web/core/popover/popover_hook";
|
||||
import { browser } from "@web/core/browser/browser";
|
||||
import { AvatarCardPopover } from "@mail/discuss/web/avatar_card/avatar_card_popover";
|
||||
import {
|
||||
Many2ManyTagsAvatarField,
|
||||
@@ -21,30 +19,24 @@ import {
|
||||
export class Many2ManyAvatarUserTagsList extends TagsList {}
|
||||
Many2ManyAvatarUserTagsList.template = "mail.Many2ManyAvatarUserTagsList";
|
||||
|
||||
const WithUserChatter = (T) => class UserChatterMixin extends T {
|
||||
setup() {
|
||||
super.setup(...arguments);
|
||||
this.openChat = useOpenChat(this.relation);
|
||||
if (this.props.withCommand) {
|
||||
useAssignUserCommand();
|
||||
const WithUserChatter = (T) =>
|
||||
class UserChatterMixin extends T {
|
||||
setup() {
|
||||
super.setup(...arguments);
|
||||
if (this.props.withCommand) {
|
||||
useAssignUserCommand();
|
||||
}
|
||||
this.avatarCard = usePopover(AvatarCardPopover);
|
||||
}
|
||||
this.avatarCard = usePopover(AvatarCardPopover, {
|
||||
closeOnHoverAway: true,
|
||||
});
|
||||
this.openTimeout = false;
|
||||
this.lastOpenedId = 0;
|
||||
}
|
||||
|
||||
getTagProps(record) {
|
||||
return {
|
||||
...super.getTagProps(...arguments),
|
||||
onImageClicked: () => this.openChat(record.resId),
|
||||
openCard: (ev) => {
|
||||
if (this.env.isSmall || this.relation !== "res.users") {
|
||||
return;
|
||||
}
|
||||
const target = ev.currentTarget;
|
||||
this.openTimeout = browser.setTimeout(() => {
|
||||
getTagProps(record) {
|
||||
return {
|
||||
...super.getTagProps(...arguments),
|
||||
onImageClicked: (ev) => {
|
||||
if (this.env.isSmall || this.relation !== "res.users") {
|
||||
return;
|
||||
}
|
||||
const target = ev.currentTarget;
|
||||
if (
|
||||
!this.avatarCard.isOpen ||
|
||||
(this.lastOpenedId && record.resId !== this.lastOpenedId)
|
||||
@@ -54,15 +46,10 @@ const WithUserChatter = (T) => class UserChatterMixin extends T {
|
||||
});
|
||||
this.lastOpenedId = record.resId;
|
||||
}
|
||||
}, 350);
|
||||
},
|
||||
clearTimeout: () => {
|
||||
browser.clearTimeout(this.openTimeout);
|
||||
delete this.openTimeout;
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
export class Many2ManyTagsAvatarUserField extends WithUserChatter(Many2ManyTagsAvatarField) {
|
||||
static components = {
|
||||
@@ -83,7 +70,9 @@ export class KanbanMany2ManyAvatarUserTagsList extends KanbanMany2ManyTagsAvatar
|
||||
static template = "mail.KanbanMany2ManyAvatarUserTagsList";
|
||||
}
|
||||
|
||||
export class KanbanMany2ManyTagsAvatarUserField extends WithUserChatter(KanbanMany2ManyTagsAvatarField) {
|
||||
export class KanbanMany2ManyTagsAvatarUserField extends WithUserChatter(
|
||||
KanbanMany2ManyTagsAvatarField
|
||||
) {
|
||||
static template = "mail.KanbanMany2ManyTagsAvatarUserField";
|
||||
static components = {
|
||||
...KanbanMany2ManyTagsAvatarField.components,
|
||||
@@ -100,7 +89,9 @@ export const kanbanMany2ManyTagsAvatarUserField = {
|
||||
};
|
||||
registry.category("fields").add("kanban.many2many_avatar_user", kanbanMany2ManyTagsAvatarUserField);
|
||||
|
||||
export class ListMany2ManyTagsAvatarUserField extends WithUserChatter(ListMany2ManyTagsAvatarField) {
|
||||
export class ListMany2ManyTagsAvatarUserField extends WithUserChatter(
|
||||
ListMany2ManyTagsAvatarField
|
||||
) {
|
||||
static template = "mail.ListMany2ManyTagsAvatarUserField";
|
||||
static components = {
|
||||
...ListMany2ManyTagsAvatarField.components,
|
||||
|
||||
-10
@@ -1,24 +1,14 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates>
|
||||
<t t-name="mail.Many2ManyAvatarUserTagsList" t-inherit="web.TagsList" t-inherit-mode="primary">
|
||||
<xpath expr="//span[hasclass('o_tag')]" position="attributes">
|
||||
<attribute name="t-att-title"></attribute>
|
||||
</xpath>
|
||||
<img position="attributes">
|
||||
<attribute name="t-on-click.stop.prevent">tag.onImageClicked</attribute>
|
||||
<attribute name="t-on-mouseleave">tag.clearTimeout</attribute>
|
||||
<attribute name="t-on-mouseover.stop.prevent">tag.openCard</attribute>
|
||||
</img>
|
||||
</t>
|
||||
|
||||
<t t-name="mail.KanbanMany2ManyAvatarUserTagsList" t-inherit="web.KanbanMany2ManyTagsAvatarFieldTagsList" t-inherit-mode="primary">
|
||||
<xpath expr="//span[hasclass('o_tag')]" position="attributes">
|
||||
<attribute name="t-att-title"></attribute>
|
||||
</xpath>
|
||||
<img position="attributes">
|
||||
<attribute name="t-on-click.stop.prevent">tag.onImageClicked</attribute>
|
||||
<attribute name="t-on-mouseleave">tag.clearTimeout</attribute>
|
||||
<attribute name="t-on-mouseover.stop.prevent">tag.openCard</attribute>
|
||||
</img>
|
||||
</t>
|
||||
|
||||
|
||||
+7
-27
@@ -1,6 +1,5 @@
|
||||
/* @odoo-module */
|
||||
|
||||
import { useOpenChat } from "@mail/core/web/open_chat_hook";
|
||||
import { useAssignUserCommand } from "@mail/views/web/fields/assign_user_command_hook";
|
||||
|
||||
import { _t } from "@web/core/l10n/translation";
|
||||
@@ -12,50 +11,31 @@ import {
|
||||
kanbanMany2OneAvatarField,
|
||||
} from "@web/views/fields/many2one_avatar/many2one_avatar_field";
|
||||
import { usePopover } from "@web/core/popover/popover_hook";
|
||||
import { browser } from "@web/core/browser/browser";
|
||||
import { AvatarCardPopover } from "@mail/discuss/web/avatar_card/avatar_card_popover";
|
||||
|
||||
const WithUserChatter = (T) =>
|
||||
class extends T {
|
||||
setup() {
|
||||
super.setup(...arguments);
|
||||
this.openChat = useOpenChat(this.relation);
|
||||
if (this.props.withCommand) {
|
||||
useAssignUserCommand();
|
||||
}
|
||||
this.avatarCard = usePopover(AvatarCardPopover, {
|
||||
closeOnHoverAway: true,
|
||||
});
|
||||
this.openTimeout = false;
|
||||
this.avatarCard = usePopover(AvatarCardPopover);
|
||||
}
|
||||
|
||||
onClickAvatar() {
|
||||
onClickAvatar(ev) {
|
||||
const id = this.props.record.data[this.props.name][0] ?? false;
|
||||
if (id !== false) {
|
||||
this.openChat(id);
|
||||
}
|
||||
}
|
||||
|
||||
openCard(ev) {
|
||||
if (this.env.isSmall || this.relation !== "res.users") {
|
||||
return;
|
||||
}
|
||||
const target = ev.currentTarget;
|
||||
if (!target.querySelector(":scope > img")) {
|
||||
return;
|
||||
}
|
||||
this.openTimeout = browser.setTimeout(() => {
|
||||
if (this.env.isSmall || this.relation !== "res.users") {
|
||||
return;
|
||||
}
|
||||
const target = ev.currentTarget;
|
||||
if (!this.avatarCard.isOpen) {
|
||||
this.avatarCard.open(target, {
|
||||
id: this.props.record.data[this.props.name][0],
|
||||
});
|
||||
}
|
||||
}, 350);
|
||||
}
|
||||
|
||||
clearTimeout() {
|
||||
browser.clearTimeout(this.openTimeout);
|
||||
delete this.openTimeout;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
-10
@@ -1,24 +1,14 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates>
|
||||
<t t-name="mail.Many2OneAvatarUserField" t-inherit="web.Many2OneAvatarField" t-inherit-mode="primary">
|
||||
<xpath expr="//div" position="attributes">
|
||||
<attribute name="t-att-data-tooltip"></attribute>
|
||||
</xpath>
|
||||
<xpath expr="//span[hasclass('o_m2o_avatar')]" position="attributes">
|
||||
<attribute name="t-on-click.stop.prevent">onClickAvatar</attribute>
|
||||
<attribute name="t-on-mouseleave">clearTimeout</attribute>
|
||||
<attribute name="t-on-mouseover.stop.prevent">openCard</attribute>
|
||||
</xpath>
|
||||
</t>
|
||||
|
||||
<t t-name="mail.KanbanMany2OneAvatarUserField" t-inherit="web.KanbanMany2OneAvatarField" t-inherit-mode="primary">
|
||||
<xpath expr="//div" position="attributes">
|
||||
<attribute name="t-att-data-tooltip"></attribute>
|
||||
</xpath>
|
||||
<xpath expr="//span[hasclass('o_m2o_avatar')]" position="attributes">
|
||||
<attribute name="t-on-click.stop.prevent">onClickAvatar</attribute>
|
||||
<attribute name="t-on-mouseleave">clearTimeout</attribute>
|
||||
<attribute name="t-on-mouseover.stop.prevent">openCard</attribute>
|
||||
</xpath>
|
||||
<Many2OneField position="attributes">
|
||||
<attribute name="t-if">props.readonly and props.displayAvatarName</attribute>
|
||||
|
||||
@@ -156,7 +156,12 @@ QUnit.test(
|
||||
QUnit.test("Click on avatar opens its partner chat window", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const partnerId = pyEnv["res.partner"].create({ name: "testPartner" });
|
||||
pyEnv["res.users"].create({ partner_id: partnerId });
|
||||
pyEnv["res.users"].create({
|
||||
partner_id: partnerId,
|
||||
name: "testPartner",
|
||||
email: "test@partner.com",
|
||||
phone: "+45687468",
|
||||
});
|
||||
pyEnv["mail.message"].create({
|
||||
author_id: partnerId,
|
||||
body: "Test",
|
||||
@@ -168,7 +173,10 @@ QUnit.test("Click on avatar opens its partner chat window", async () => {
|
||||
await openFormView("res.partner", partnerId);
|
||||
await contains(".o-mail-Message-sidebar .o-mail-Message-avatarContainer img");
|
||||
await click(".o-mail-Message-sidebar .o-mail-Message-avatarContainer img");
|
||||
await contains(".o-mail-ChatWindow-name", { text: "testPartner" });
|
||||
await contains(".o_avatar_card");
|
||||
await contains(".o_card_user_infos > span:contains(testPartner)");
|
||||
await contains(".o_card_user_infos > a:contains(test@partner.com)");
|
||||
await contains(".o_card_user_infos > a:contains(+45687468)");
|
||||
});
|
||||
|
||||
QUnit.test("Can use channel command /who", async () => {
|
||||
|
||||
@@ -200,11 +200,17 @@ patch(MockServer.prototype, {
|
||||
const [author] = this.getRecords("res.partner", [["id", "=", message.author_id]], {
|
||||
active_test: false,
|
||||
});
|
||||
const [user] = this.getRecords("res.users", [
|
||||
["partner_id", "=", message.author_id],
|
||||
]);
|
||||
formattedAuthor = {
|
||||
id: author.id,
|
||||
is_company: author.is_company,
|
||||
name: author.name,
|
||||
};
|
||||
if (user) {
|
||||
formattedAuthor["user"] = { id: user.id };
|
||||
}
|
||||
} else {
|
||||
formattedAuthor = [["clear"]];
|
||||
}
|
||||
|
||||
@@ -665,10 +665,15 @@ QUnit.test("message comment of same author within 1min. should be squashed", asy
|
||||
await contains(".o-mail-Message:contains(body2) .o-mail-Message-sidebar .o-mail-Message-date");
|
||||
});
|
||||
|
||||
QUnit.test("redirect to author (open chat)", async () => {
|
||||
QUnit.test("open author avatar card", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const partnerId = pyEnv["res.partner"].create({ name: "Demo" });
|
||||
pyEnv["res.users"].create({ partner_id: partnerId });
|
||||
pyEnv["res.users"].create({
|
||||
partner_id: partnerId,
|
||||
name: "Demo",
|
||||
email: "demo@example.com",
|
||||
phone: "+5646548",
|
||||
});
|
||||
const [channelId_1] = pyEnv["discuss.channel"].create([
|
||||
{ name: "General" },
|
||||
{
|
||||
@@ -691,32 +696,10 @@ QUnit.test("redirect to author (open chat)", async () => {
|
||||
await contains(".o-mail-Discuss-content .o-mail-Message-avatarContainer img");
|
||||
|
||||
await click(".o-mail-Discuss-content .o-mail-Message-avatarContainer img");
|
||||
await contains(".o-mail-DiscussSidebarChannel.o-active", { text: "Demo" });
|
||||
});
|
||||
|
||||
QUnit.test("open chat from avatar should not work on self-authored messages", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const [channelId] = pyEnv["discuss.channel"].create([
|
||||
{ name: "General" },
|
||||
{
|
||||
channel_member_ids: [Command.create({ partner_id: pyEnv.currentPartnerId })],
|
||||
channel_type: "chat",
|
||||
},
|
||||
]);
|
||||
pyEnv["mail.message"].create({
|
||||
author_id: pyEnv.currentPartnerId,
|
||||
body: "not empty",
|
||||
model: "discuss.channel",
|
||||
res_id: channelId,
|
||||
});
|
||||
const { openDiscuss } = await start();
|
||||
openDiscuss(channelId);
|
||||
await contains(".o-mail-Message-avatarContainer:not(.cursor-pointer)");
|
||||
await contains(".o-mail-Message-author:not(.cursor-pointer)");
|
||||
await click(".o-mail-Message-avatar");
|
||||
// weak test, no guarantee that we waited long enough to open the chat/view
|
||||
await contains(".o-mail-DiscussSidebarChannel.o-active", { count: 0, text: "Mitchell Admin" });
|
||||
await contains(".breadcrumb", { count: 0, text: "Mitchell Admin" });
|
||||
await contains(".o_avatar_card");
|
||||
await contains(".o_card_user_infos > span:contains(Demo)");
|
||||
await contains(".o_card_user_infos > a:contains(demo@example.com)");
|
||||
await contains(".o_card_user_infos > a:contains(+5646548)");
|
||||
});
|
||||
|
||||
QUnit.test("toggle_star message", async (assert) => {
|
||||
@@ -1126,32 +1109,47 @@ QUnit.test("allow attachment image download on message", async () => {
|
||||
await contains(".o-mail-AttachmentImage .fa-download");
|
||||
});
|
||||
|
||||
QUnit.test("chat with author should be opened after clicking on their avatar", async (assert) => {
|
||||
const pyEnv = await startServer();
|
||||
const [partnerId_1, partnerId_2] = pyEnv["res.partner"].create([
|
||||
{ name: "Partner_1" },
|
||||
{ name: "Partner_2" },
|
||||
]);
|
||||
pyEnv["res.users"].create({ partner_id: partnerId_2 });
|
||||
pyEnv["mail.message"].create({
|
||||
author_id: partnerId_2,
|
||||
body: "not empty",
|
||||
model: "res.partner",
|
||||
res_id: partnerId_1,
|
||||
});
|
||||
const { openFormView } = await start();
|
||||
openFormView("res.partner", partnerId_1);
|
||||
await contains(".o-mail-Message-avatar");
|
||||
assert.hasClass($(".o-mail-Message-avatarContainer"), "cursor-pointer");
|
||||
await click(".o-mail-Message-avatar");
|
||||
await contains(".o-mail-ChatWindow-content");
|
||||
await contains(".o-mail-ChatWindow-name", { text: "Partner_2" });
|
||||
});
|
||||
QUnit.test(
|
||||
"avatar card from author should be opened after clicking on their avatar",
|
||||
async (assert) => {
|
||||
const pyEnv = await startServer();
|
||||
const [partnerId_1, partnerId_2] = pyEnv["res.partner"].create([
|
||||
{ name: "Partner_1" },
|
||||
{ name: "Partner_2" },
|
||||
]);
|
||||
pyEnv["res.users"].create({
|
||||
partner_id: partnerId_2,
|
||||
name: "Partner_2",
|
||||
email: "partner2@mail.com",
|
||||
phone: "+15968415",
|
||||
});
|
||||
pyEnv["mail.message"].create({
|
||||
author_id: partnerId_2,
|
||||
body: "not empty",
|
||||
model: "res.partner",
|
||||
res_id: partnerId_1,
|
||||
});
|
||||
const { openFormView } = await start();
|
||||
await openFormView("res.partner", partnerId_1);
|
||||
await contains(".o-mail-Message-avatar");
|
||||
assert.hasClass($(".o-mail-Message-avatarContainer"), "cursor-pointer");
|
||||
await click(".o-mail-Message-avatar");
|
||||
await contains(".o_avatar_card");
|
||||
await contains(".o_card_user_infos > span:contains(Partner_2)");
|
||||
await contains(".o_card_user_infos > a:contains(partner2@mail.com)");
|
||||
await contains(".o_card_user_infos > a:contains(+15968415)");
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test("chat with author should be opened after clicking on their name", async () => {
|
||||
QUnit.test("avatar card from author should be opened after clicking on their name", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const partnerId = pyEnv["res.partner"].create({ name: "Demo User" });
|
||||
pyEnv["res.users"].create({ partner_id: partnerId });
|
||||
const partnerId = pyEnv["res.partner"].create({ name: "Demo" });
|
||||
pyEnv["res.users"].create({
|
||||
partner_id: partnerId,
|
||||
name: "Demo",
|
||||
email: "demo@example.com",
|
||||
phone: "+5646548",
|
||||
});
|
||||
pyEnv["mail.message"].create({
|
||||
author_id: partnerId,
|
||||
body: "not empty",
|
||||
@@ -1159,12 +1157,14 @@ QUnit.test("chat with author should be opened after clicking on their name", asy
|
||||
res_id: partnerId,
|
||||
});
|
||||
const { openFormView } = await start();
|
||||
openFormView("res.partner", partnerId);
|
||||
await contains(".o-mail-Message span", { text: "Demo User" });
|
||||
await openFormView("res.partner", partnerId);
|
||||
await contains(".o-mail-Message span", { text: "Demo" });
|
||||
|
||||
await click(".o-mail-Message span", { text: "Demo User" });
|
||||
await contains(".o-mail-ChatWindow");
|
||||
await contains(".o-mail-ChatWindow-name", { text: "Demo User" });
|
||||
await click(".o-mail-Message span:contains(Demo)");
|
||||
await contains(".o_avatar_card");
|
||||
await contains(".o_card_user_infos > span:contains(Demo)");
|
||||
await contains(".o_card_user_infos > a:contains(demo@example.com)");
|
||||
await contains(".o_card_user_infos > a:contains(+5646548)");
|
||||
});
|
||||
|
||||
QUnit.test("subtype description should be displayed if it is different than body", async () => {
|
||||
@@ -1238,36 +1238,6 @@ QUnit.test("Chat with partner should be opened after clicking on their mention",
|
||||
await contains(".o-mail-ChatWindow-name", { text: "Test Partner" });
|
||||
});
|
||||
|
||||
QUnit.test(
|
||||
"open chat with author on avatar click should be disabled when currently chatting with the author",
|
||||
async (assert) => {
|
||||
const pyEnv = await startServer();
|
||||
const partnerId = pyEnv["res.partner"].create({ name: "test" });
|
||||
pyEnv["res.users"].create({ partner_id: partnerId });
|
||||
const channelId = pyEnv["discuss.channel"].create({
|
||||
name: "test",
|
||||
channel_member_ids: [
|
||||
Command.create({ partner_id: pyEnv.currentPartnerId }),
|
||||
Command.create({ partner_id: partnerId }),
|
||||
],
|
||||
channel_type: "chat",
|
||||
});
|
||||
pyEnv["mail.message"].create({
|
||||
author_id: partnerId,
|
||||
body: "not empty",
|
||||
model: "discuss.channel",
|
||||
res_id: channelId,
|
||||
});
|
||||
const { openDiscuss } = await start();
|
||||
openDiscuss(channelId);
|
||||
await contains(".o-mail-Message-avatar");
|
||||
assert.doesNotHaveClass($(".o-mail-Message-avatarContainer"), "cursor-pointer");
|
||||
await click(".o-mail-Message-avatar");
|
||||
// weak test, no guarantee that we waited long enough for the potential chat window to show
|
||||
await contains(".o-mail-ChatWindow", { count: 0 });
|
||||
}
|
||||
);
|
||||
|
||||
QUnit.test("Channel should be opened after clicking on its mention", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const partnerId = pyEnv["res.partner"].create({});
|
||||
|
||||
@@ -10,8 +10,8 @@ import { browser } from "@web/core/browser/browser";
|
||||
import { popoverService } from "@web/core/popover/popover_service";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { tooltipService } from "@web/core/tooltip/tooltip_service";
|
||||
import { patchWithCleanup, triggerHotkey, getNodesTextContent } from "@web/../tests/helpers/utils";
|
||||
import { click, contains, triggerEvents } from "@web/../tests/utils";
|
||||
import { patchWithCleanup, triggerHotkey } from "@web/../tests/helpers/utils";
|
||||
import { click, contains } from "@web/../tests/utils";
|
||||
|
||||
const fakeMultiTab = {
|
||||
start() {
|
||||
@@ -409,7 +409,7 @@ QUnit.test("avatar card preview", async (assert) => {
|
||||
});
|
||||
const mockRPC = (route, args) => {
|
||||
if (route === "/web/dataset/call_kw/res.users/read") {
|
||||
assert.deepEqual(args.args[1], ["name", "email", "phone", "im_status"]);
|
||||
assert.deepEqual(args.args[1], ["name", "email", "phone", "im_status", "share"]);
|
||||
assert.step("user read");
|
||||
}
|
||||
};
|
||||
@@ -440,17 +440,14 @@ QUnit.test("avatar card preview", async (assert) => {
|
||||
},
|
||||
});
|
||||
// Open card
|
||||
await triggerEvents(".o_m2o_avatar > img", ["mouseover"]);
|
||||
await click(".o_m2o_avatar > img");
|
||||
await contains(".o_avatar_card");
|
||||
assert.verifySteps(["setTimeout of 350ms", "setTimeout of 250ms", "user read"]);
|
||||
assert.deepEqual(getNodesTextContent(document.querySelectorAll(".o_card_user_infos > *")), [
|
||||
"Mario",
|
||||
" Mario@odoo.test",
|
||||
" +78786987",
|
||||
]);
|
||||
await contains(".o_card_user_infos > span:contains(Mario)");
|
||||
await contains(".o_card_user_infos > a:contains(Mario@odoo.test)");
|
||||
await contains(".o_card_user_infos > a:contains(+78786987)");
|
||||
assert.verifySteps(["setTimeout of 250ms", "user read"]);
|
||||
// Close card
|
||||
await triggerEvents(".o_control_panel", ["mouseover"]);
|
||||
assert.verifySteps(["setTimeout of 400ms"]);
|
||||
await click(".o_action_manager");
|
||||
await contains(".o_avatar_card", { count: 0 });
|
||||
});
|
||||
|
||||
@@ -478,7 +475,12 @@ QUnit.test("avatar_user widget displays the appropriate user image in form view"
|
||||
QUnit.test("many2one_avatar_user widget in list view", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const partnerId = pyEnv["res.partner"].create({ name: "Partner 1" });
|
||||
const userId = pyEnv["res.users"].create({ name: "Mario", partner_id: partnerId });
|
||||
const userId = pyEnv["res.users"].create({
|
||||
name: "Mario",
|
||||
partner_id: partnerId,
|
||||
email: "Mario@partner.com",
|
||||
phone: "+45687468",
|
||||
});
|
||||
pyEnv["m2x.avatar.user"].create({ user_id: userId });
|
||||
const views = {
|
||||
"m2x.avatar.user,false,list":
|
||||
@@ -490,13 +492,21 @@ QUnit.test("many2one_avatar_user widget in list view", async () => {
|
||||
views: [[false, "list"]],
|
||||
});
|
||||
await click(".o_data_cell .o_m2o_avatar > img");
|
||||
await contains(".o-mail-ChatWindow-name", { text: "Partner 1" });
|
||||
await contains(".o_avatar_card");
|
||||
await contains(".o_card_user_infos > span:contains(Mario)");
|
||||
await contains(".o_card_user_infos > a:contains(Mario@partner.com)");
|
||||
await contains(".o_card_user_infos > a:contains(+45687468)");
|
||||
});
|
||||
|
||||
QUnit.test("many2many_avatar_user widget in form view", async () => {
|
||||
const pyEnv = await startServer();
|
||||
const partnerId = pyEnv["res.partner"].create({ name: "Partner 1" });
|
||||
const userId = pyEnv["res.users"].create({ name: "Mario", partner_id: partnerId });
|
||||
const userId = pyEnv["res.users"].create({
|
||||
name: "Mario",
|
||||
partner_id: partnerId,
|
||||
email: "Mario@partner.com",
|
||||
phone: "+45687468",
|
||||
});
|
||||
const avatarUserId = pyEnv["m2x.avatar.user"].create({ user_ids: [userId] });
|
||||
const views = {
|
||||
"m2x.avatar.user,false,form": `
|
||||
@@ -511,5 +521,8 @@ QUnit.test("many2many_avatar_user widget in form view", async () => {
|
||||
views: [[false, "form"]],
|
||||
});
|
||||
await click(".o_field_many2many_avatar_user .o_avatar img");
|
||||
await contains(".o-mail-ChatWindow-name", { text: "Partner 1" });
|
||||
await contains(".o_avatar_card");
|
||||
await contains(".o_card_user_infos > span:contains(Mario)");
|
||||
await contains(".o_card_user_infos > a:contains(Mario@partner.com)");
|
||||
await contains(".o_card_user_infos > a:contains(+45687468)");
|
||||
});
|
||||
|
||||
@@ -401,7 +401,7 @@ class TestMailAPIPerformance(BaseMailPerformance):
|
||||
composer_form.attachment_ids.add(attachment)
|
||||
composer = composer_form.save()
|
||||
|
||||
with self.assertQueryCount(admin=53, employee=53): # tm+com 46/46
|
||||
with self.assertQueryCount(admin=54, employee=54): # tm+com 46/46
|
||||
composer._action_send_mail()
|
||||
|
||||
# notifications
|
||||
@@ -463,7 +463,7 @@ class TestMailAPIPerformance(BaseMailPerformance):
|
||||
'default_template_id': test_template.id,
|
||||
}).create({})
|
||||
|
||||
with self.assertQueryCount(admin=40, employee=40): # com 36/36
|
||||
with self.assertQueryCount(admin=41, employee=41): # com 36/36
|
||||
composer._action_send_mail()
|
||||
|
||||
# notifications
|
||||
@@ -487,7 +487,7 @@ class TestMailAPIPerformance(BaseMailPerformance):
|
||||
'default_template_id': test_template.id,
|
||||
}).create({})
|
||||
|
||||
with self.assertQueryCount(admin=49, employee=49): # com 43/43
|
||||
with self.assertQueryCount(admin=50, employee=50): # com 43/43
|
||||
composer._action_send_mail()
|
||||
|
||||
# notifications
|
||||
@@ -519,7 +519,7 @@ class TestMailAPIPerformance(BaseMailPerformance):
|
||||
)
|
||||
composer = composer_form.save()
|
||||
|
||||
with self.assertQueryCount(admin=48, employee=48): # com 44/44
|
||||
with self.assertQueryCount(admin=50, employee=50): # com 44/44
|
||||
composer._action_send_mail()
|
||||
|
||||
# notifications
|
||||
@@ -549,7 +549,7 @@ class TestMailAPIPerformance(BaseMailPerformance):
|
||||
)
|
||||
composer = composer_form.save()
|
||||
|
||||
with self.assertQueryCount(admin=67, employee=67): # com 61/61
|
||||
with self.assertQueryCount(admin=69, employee=69): # com 61/61
|
||||
composer._action_send_mail()
|
||||
|
||||
# notifications
|
||||
@@ -585,7 +585,7 @@ class TestMailAPIPerformance(BaseMailPerformance):
|
||||
@warmup
|
||||
def test_message_assignation_inbox(self):
|
||||
record = self.env['mail.test.track'].create({'name': 'Test'})
|
||||
with self.assertQueryCount(admin=22, employee=22): # com 19/9
|
||||
with self.assertQueryCount(admin=23, employee=23): # com 19/9
|
||||
record.write({
|
||||
'user_id': self.user_test_inbox.id,
|
||||
})
|
||||
@@ -671,7 +671,7 @@ class TestMailAPIPerformance(BaseMailPerformance):
|
||||
def test_message_post_one_inbox_notification(self):
|
||||
record = self.env['mail.test.simple'].create({'name': 'Test'})
|
||||
|
||||
with self.assertQueryCount(admin=20, employee=20): # com 18/18
|
||||
with self.assertQueryCount(admin=21, employee=21): # com 18/18
|
||||
record.message_post(
|
||||
body=Markup('<p>Test Post Performances with an inbox ping</p>'),
|
||||
partner_ids=self.user_test.partner_id.ids,
|
||||
@@ -1228,7 +1228,7 @@ class TestMailComplexPerformance(BaseMailPerformance):
|
||||
"""
|
||||
messages_all = self.messages_all.with_env(self.env)
|
||||
|
||||
with self.assertQueryCount(employee=25): # test_mail: 24
|
||||
with self.assertQueryCount(employee=26): # test_mail: 25
|
||||
res = messages_all.message_format()
|
||||
|
||||
self.assertEqual(len(res), 2*2)
|
||||
@@ -1241,7 +1241,7 @@ class TestMailComplexPerformance(BaseMailPerformance):
|
||||
def test_message_format_single(self):
|
||||
message = self.messages_all[0].with_env(self.env)
|
||||
|
||||
with self.assertQueryCount(employee=22): # test_mail: 21
|
||||
with self.assertQueryCount(employee=23): # test_mail: 22
|
||||
res = message.message_format()
|
||||
|
||||
self.assertEqual(len(res), 1)
|
||||
@@ -1262,14 +1262,14 @@ class TestMailComplexPerformance(BaseMailPerformance):
|
||||
'res_id': record.id
|
||||
} for record in records])
|
||||
|
||||
with self.assertQueryCount(employee=6):
|
||||
with self.assertQueryCount(employee=7):
|
||||
res = messages.message_format()
|
||||
self.assertEqual(len(res), 6)
|
||||
|
||||
self.env.flush_all()
|
||||
self.env.invalidate_all()
|
||||
|
||||
with self.assertQueryCount(employee=14):
|
||||
with self.assertQueryCount(employee=15):
|
||||
res = messages.message_format()
|
||||
self.assertEqual(len(res), 6)
|
||||
|
||||
@@ -1346,7 +1346,7 @@ class TestMailHeavyPerformancePost(BaseMailPerformance):
|
||||
attachments = self.env['ir.attachment'].with_user(self.env.user).create(self.test_attachments_vals)
|
||||
# enable_logging = self.cr._enable_logging() if self.warm else nullcontext()
|
||||
# with self.assertQueryCount(employee=63), enable_logging:
|
||||
with self.assertQueryCount(employee=65): # com 60
|
||||
with self.assertQueryCount(employee=66): # com 60
|
||||
record_container.with_context({}).message_post(
|
||||
body=Markup('<p>Test body <img src="cid:cid1"> <img src="cid:cid2"></p>'),
|
||||
subject='Test Subject',
|
||||
|
||||
@@ -91,7 +91,7 @@ class TestMailPerformance(FullBaseMailPerformance):
|
||||
record_ticket = self.env['mail.test.ticket.mc'].browse(self.record_ticket.ids)
|
||||
attachments = self.env['ir.attachment'].create(self.test_attachments_vals)
|
||||
|
||||
with self.assertQueryCount(employee=95): # test_mail_full: 88
|
||||
with self.assertQueryCount(employee=96): # test_mail_full: 88
|
||||
new_message = record_ticket.message_post(
|
||||
attachment_ids=attachments.ids,
|
||||
body=Markup('<p>Test Content</p>'),
|
||||
|
||||
@@ -172,10 +172,10 @@ class TestRatingPerformance(TestRatingCommon):
|
||||
@users('employee')
|
||||
@warmup
|
||||
def test_rating_last_value_perfs(self):
|
||||
with self.assertQueryCount(employee=1614): # tmf 1313 / com 1313
|
||||
with self.assertQueryCount(employee=1615): # tmf 1313 / com 1313
|
||||
self.create_ratings('mail.test.rating.thread')
|
||||
|
||||
with self.assertQueryCount(employee=2001): # tmf 1901
|
||||
with self.assertQueryCount(employee=2101): # tmf 1901
|
||||
self.apply_ratings(1)
|
||||
|
||||
with self.assertQueryCount(employee=1900): # tmf 1800
|
||||
@@ -184,10 +184,10 @@ class TestRatingPerformance(TestRatingCommon):
|
||||
@users('employee')
|
||||
@warmup
|
||||
def test_rating_last_value_perfs_with_rating_mixin(self):
|
||||
with self.assertQueryCount(employee=1721): # tmf 1419 / com 1419
|
||||
with self.assertQueryCount(employee=1722): # tmf 1419 / com 1419
|
||||
self.create_ratings('mail.test.rating')
|
||||
|
||||
with self.assertQueryCount(employee=2204): # tmf 2104
|
||||
with self.assertQueryCount(employee=2304): # tmf 2104
|
||||
self.apply_ratings(1)
|
||||
|
||||
with self.assertQueryCount(employee=2203): # tmf 2103
|
||||
|
||||
@@ -4,7 +4,6 @@ import { Component, onWillDestroy, useExternalListener, xml } from "@odoo/owl";
|
||||
import { useHotkey } from "../hotkeys/hotkey_hook";
|
||||
import { useChildRef } from "../utils/hooks";
|
||||
import { Popover } from "./popover";
|
||||
import { browser } from "../browser/browser";
|
||||
|
||||
export class PopoverController extends Component {
|
||||
static template = xml`
|
||||
@@ -17,7 +16,6 @@ export class PopoverController extends Component {
|
||||
"target",
|
||||
"close",
|
||||
"closeOnClickAway",
|
||||
"closeOnHoverAway",
|
||||
"component",
|
||||
"componentProps",
|
||||
"popoverProps",
|
||||
@@ -27,10 +25,6 @@ export class PopoverController extends Component {
|
||||
if (this.props.target.isConnected) {
|
||||
this.popoverRef = useChildRef();
|
||||
useExternalListener(window, "mousedown", this.onClickAway, { capture: true });
|
||||
if (this.props.closeOnHoverAway) {
|
||||
this.closePopoverTimeout = false;
|
||||
useExternalListener(window, "mouseover", this.onHoverAway, { capture: true });
|
||||
}
|
||||
useHotkey("escape", () => this.props.close());
|
||||
const targetObserver = new MutationObserver(this.onTargetMutate.bind(this));
|
||||
targetObserver.observe(this.props.target.parentElement, { childList: true });
|
||||
@@ -51,17 +45,6 @@ export class PopoverController extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
onHoverAway(ev) {
|
||||
const target = ev.composedPath()[0];
|
||||
if (!this.props.target.contains(target) && !this.popoverRef.el.contains(target)) {
|
||||
this.closePopoverTimeout = browser.setTimeout(() => {
|
||||
this.props.close();
|
||||
}, 400);
|
||||
} else {
|
||||
browser.clearTimeout(this.closePopoverTimeout);
|
||||
}
|
||||
}
|
||||
|
||||
onTargetMutate() {
|
||||
if (!this.props.target.isConnected) {
|
||||
this.props.close();
|
||||
|
||||
@@ -7,7 +7,6 @@ import { PopoverController } from "./popover_controller";
|
||||
/**
|
||||
* @typedef {{
|
||||
* closeOnClickAway?: boolean | (target: HTMLElement) => boolean;
|
||||
* closeOnHoverAway?: boolean;
|
||||
* onClose?: () => void;
|
||||
* popoverClass?: string;
|
||||
* position?: import("@web/core/position_hook").Options["position"];
|
||||
@@ -32,14 +31,12 @@ export const popoverService = {
|
||||
typeof options.closeOnClickAway === "function"
|
||||
? options.closeOnClickAway
|
||||
: () => options.closeOnClickAway ?? true;
|
||||
const closeOnHoverAway = options.closeOnHoverAway || false;
|
||||
const remove = overlay.add(
|
||||
PopoverController,
|
||||
{
|
||||
target,
|
||||
close: () => remove(),
|
||||
closeOnClickAway,
|
||||
closeOnHoverAway,
|
||||
component,
|
||||
componentProps: markRaw(props),
|
||||
popoverProps: {
|
||||
|
||||
Reference in New Issue
Block a user