[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:
Julien Carion (juca)
2023-09-15 16:45:50 +00:00
parent 30650af183
commit 1a44537095
30 changed files with 245 additions and 392 deletions
@@ -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'
-1
View File
@@ -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']");
});
+4
View File
@@ -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,
+1 -5
View File
@@ -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);
+1 -1
View File
@@ -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>
@@ -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,
@@ -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>
@@ -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;
}
}
};
@@ -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)");
});
+12 -12
View File
@@ -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>'),
+4 -4
View File
@@ -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: {