[FIX] mail, web: properly add messaging menu to the systray

This commit intends to fix two strange bugs encountered while trying to
push a new component into the systray menu:
- MessagingMenuWidget and RtcActivityNoticeWidget were removing their
parent node via DOM manipulations when attached in the DOM. This caused
a crash when adding other items to the systray menu since the deleted
nodes were actually managed by OWL.
- The t-foreach directive in the navbar used indexes as the t-key, which
led mapping items subsequently added to wrong templates.

closes odoo/odoo#78784

X-original-commit: 5f0f80da4fc951954d253c9e45defd591605f556
Related: odoo/enterprise#21829
Signed-off-by: Sébastien Theys (seb) <seb@odoo.com>
Signed-off-by: Ooming <oomsveta@users.noreply.github.com>
This commit is contained in:
Louis Wicket (wil)
2021-10-21 18:02:06 +00:00
parent 3739d74afe
commit 7c76f29111
18 changed files with 200 additions and 234 deletions
@@ -15,15 +15,17 @@ QUnit.module('chat_window_manager_tests.js', {
beforeEach(this);
this.start = async params => {
const { afterEvent, env, widget } = await start(Object.assign(
{ hasChatWindow: true, hasMessagingMenu: true },
const res = await start(Object.assign(
{ hasChatWindow: true },
params,
{ data: this.data }
));
const { afterEvent, env, widget } = res;
this.debug = params && params.debug;
this.afterEvent = afterEvent;
this.env = env;
this.widget = widget;
return res;
};
},
afterEach() {
@@ -48,7 +50,8 @@ QUnit.test('closing a chat window with no message from admin side unpins it', as
uuid: 'channel-10-uuid',
},
);
await this.start();
const { createMessagingMenuComponent } = await this.start();
await createMessagingMenuComponent();
await afterNextRender(() => document.querySelector(`.o_MessagingMenu_toggler`).click());
await afterNextRender(() => document.querySelector(`.o_NotificationList_preview`).click());
@@ -15,12 +15,13 @@ QUnit.module('messaging_menu_tests.js', {
beforeEach(this);
this.start = async params => {
let { env, widget } = await start(Object.assign({}, params, {
const res = await start(Object.assign({}, params, {
data: this.data,
hasMessagingMenu: true,
}));
const { env, widget } = res;
this.env = env;
this.widget = widget;
return res;
};
},
afterEach() {
@@ -38,7 +39,8 @@ QUnit.test('livechats should be in "chat" filter', async function (assert) {
livechat_operator_id: this.data.currentPartnerId,
members: [this.data.currentPartnerId, this.data.publicPartnerId],
});
await this.start();
const { createMessagingMenuComponent } = await this.start();
await createMessagingMenuComponent();
assert.containsOnce(
document.body,
'.o_MessagingMenu',
@@ -21,15 +21,17 @@ QUnit.module('chat_window_manager_tests.js', {
beforeEach(this);
this.start = async params => {
const { afterEvent, env, widget } = await start(Object.assign(
{ hasChatWindow: true, hasMessagingMenu: true },
const res = await start(Object.assign(
{ hasChatWindow: true },
params,
{ data: this.data }
));
const { afterEvent, env, widget } = res;
this.debug = params && params.debug;
this.afterEvent = afterEvent;
this.env = env;
this.widget = widget;
return res;
};
},
afterEach() {
@@ -84,7 +86,8 @@ QUnit.test('initial mount', async function (assert) {
QUnit.test('chat window new message: basic rendering', async function (assert) {
assert.expect(10);
await this.start();
const { createMessagingMenuComponent } = await this.start();
await createMessagingMenuComponent();
await afterNextRender(() =>
document.querySelector(`.o_MessagingMenu_toggler`).click()
);
@@ -146,7 +149,8 @@ QUnit.test('chat window new message: basic rendering', async function (assert) {
QUnit.test('chat window new message: focused on open [REQUIRE FOCUS]', async function (assert) {
assert.expect(2);
await this.start();
const { createMessagingMenuComponent } = await this.start();
await createMessagingMenuComponent();
await afterNextRender(() =>
document.querySelector(`.o_MessagingMenu_toggler`).click()
);
@@ -167,7 +171,8 @@ QUnit.test('chat window new message: focused on open [REQUIRE FOCUS]', async fun
QUnit.test('chat window new message: close', async function (assert) {
assert.expect(1);
await this.start();
const { createMessagingMenuComponent } = await this.start();
await createMessagingMenuComponent();
await afterNextRender(() =>
document.querySelector(`.o_MessagingMenu_toggler`).click()
);
@@ -187,7 +192,8 @@ QUnit.test('chat window new message: close', async function (assert) {
QUnit.test('chat window new message: fold', async function (assert) {
assert.expect(6);
await this.start();
const { createMessagingMenuComponent } = await this.start();
await createMessagingMenuComponent();
await afterNextRender(() => document.querySelector(`.o_MessagingMenu_toggler`).click());
await afterNextRender(() =>
document.querySelector(`.o_MessagingMenu_newMessageButton`).click()
@@ -251,7 +257,7 @@ QUnit.test('open chat from "new message" chat window should open chat in place o
{ is_minimized: true },
);
const imSearchDef = makeDeferred();
await this.start({
const { createMessagingMenuComponent } = await this.start({
env: {
browser: {
innerWidth: 1920,
@@ -265,6 +271,7 @@ QUnit.test('open chat from "new message" chat window should open chat in place o
return res;
}
});
await createMessagingMenuComponent();
assert.containsN(
document.body,
'.o_ChatWindow',
@@ -365,7 +372,7 @@ QUnit.test('new message chat window should close on selecting the user if chat w
state: 'open',
});
const imSearchDef = makeDeferred();
await this.start({
const { createMessagingMenuComponent } = await this.start({
async mockRPC(route, args) {
const res = await this._super(...arguments);
if (args.method === 'im_search') {
@@ -374,6 +381,7 @@ QUnit.test('new message chat window should close on selecting the user if chat w
return res;
},
});
await createMessagingMenuComponent();
// open "new message" chat window
await afterNextRender(() => document.querySelector(`.o_MessagingMenu_toggler`).click());
@@ -411,7 +419,7 @@ QUnit.test('new message autocomplete should automatically select first result',
this.data['res.partner'].records.push({ id: 131, name: "Partner 131" });
this.data['res.users'].records.push({ partner_id: 131 });
const imSearchDef = makeDeferred();
await this.start({
const { createMessagingMenuComponent } = await this.start({
async mockRPC(route, args) {
const res = await this._super(...arguments);
if (args.method === 'im_search') {
@@ -420,6 +428,7 @@ QUnit.test('new message autocomplete should automatically select first result',
return res;
},
});
await createMessagingMenuComponent();
// open "new message" chat window
await afterNextRender(() =>
@@ -453,7 +462,8 @@ QUnit.test('chat window: basic rendering', async function (assert) {
// channel that is expected to be found in the messaging menu
// with random unique id and name that will be asserted during the test
this.data['mail.channel'].records.push({ id: 20, name: "General" });
await this.start();
const { createMessagingMenuComponent } = await this.start();
await createMessagingMenuComponent();
await afterNextRender(() =>
document.querySelector(`.o_MessagingMenu_toggler`).click()
@@ -543,7 +553,7 @@ QUnit.test('chat window: fold', async function (assert) {
// channel that is expected to be found in the messaging menu
// with random UUID, will be asserted during the test
this.data['mail.channel'].records.push({ uuid: 'channel-uuid' });
await this.start({
const { createMessagingMenuComponent } = await this.start({
mockRPC(route, args) {
if (args.method === 'channel_fold') {
assert.step(`rpc:${args.method}/${args.kwargs.state}`);
@@ -551,6 +561,7 @@ QUnit.test('chat window: fold', async function (assert) {
return this._super(...arguments);
},
});
await createMessagingMenuComponent();
// Open Thread
await afterNextRender(() => document.querySelector(`.o_MessagingMenu_toggler`).click());
await afterNextRender(() =>
@@ -597,7 +608,7 @@ QUnit.test('chat window: open / close', async function (assert) {
// channel that is expected to be found in the messaging menu
// with random UUID, will be asserted during the test
this.data['mail.channel'].records.push({ uuid: 'channel-uuid' });
await this.start({
const { createMessagingMenuComponent } = await this.start({
mockRPC(route, args) {
if (args.method === 'channel_fold') {
assert.step(`rpc:channel_fold/${args.kwargs.state}`);
@@ -605,6 +616,7 @@ QUnit.test('chat window: open / close', async function (assert) {
return this._super(...arguments);
},
});
await createMessagingMenuComponent();
assert.containsNone(
document.body,
'.o_ChatWindow',
@@ -659,13 +671,14 @@ QUnit.test('Mobile: opening a chat window should not update channel state on the
id: 20,
state: 'closed',
});
await this.start({
const { createMessagingMenuComponent } = await this.start({
env: {
device: {
isMobile: true,
},
},
});
await createMessagingMenuComponent();
await afterNextRender(() => document.querySelector(`.o_MessagingMenu_toggler`).click());
await afterNextRender(() => document.querySelector(`.o_NotificationList_preview`).click());
assert.containsOnce(
@@ -692,13 +705,14 @@ QUnit.test('Mobile: closing a chat window should not update channel state on the
id: 20,
state: 'open',
});
await this.start({
const { createMessagingMenuComponent } = await this.start({
env: {
device: {
isMobile: true,
},
},
});
await createMessagingMenuComponent();
await afterNextRender(() => document.querySelector(`.o_MessagingMenu_toggler`).click());
await afterNextRender(() => document.querySelector(`.o_NotificationList_preview`).click());
assert.containsOnce(
@@ -916,7 +930,8 @@ QUnit.test('chat window: composer state conservation on toggle discuss', async f
// channel that is expected to be found in the messaging menu
// with random unique id that is needed to link messages
this.data['mail.channel'].records.push({ id: 20 });
await this.start();
const { createMessagingMenuComponent } = await this.start();
await createMessagingMenuComponent();
await afterNextRender(() => document.querySelector(`.o_MessagingMenu_toggler`).click());
await afterNextRender(() =>
document.querySelector(`.o_MessagingMenu_dropdownMenu .o_NotificationList_preview`).click()
@@ -990,7 +1005,8 @@ QUnit.test('chat window: scroll conservation on toggle discuss', async function
res_id: 20,
});
}
await this.start();
const { createMessagingMenuComponent } = await this.start();
await createMessagingMenuComponent();
await afterNextRender(() => document.querySelector(`.o_MessagingMenu_toggler`).click());
await this.afterEvent({
eventName: 'o-component-message-list-scrolled',
@@ -1065,13 +1081,14 @@ QUnit.test('open 2 different chat windows: enough screen width [REQUIRE FOCUS]',
// 2 channels are expected to be found in the messaging menu, each with a
// random unique id that will be referenced in the test
this.data['mail.channel'].records.push({ id: 10 }, { id: 20 });
await this.start({
const { createMessagingMenuComponent } = await this.start({
env: {
browser: {
innerWidth: 1920, // enough to fit at least 2 chat windows
},
},
});
await createMessagingMenuComponent();
await afterNextRender(() => document.querySelector(`.o_MessagingMenu_toggler`).click());
await afterNextRender(() =>
document.querySelector(`
@@ -1184,7 +1201,8 @@ QUnit.test('open 2 chat windows: check shift operations are available', async fu
// 2 channels are expected to be found in the messaging menu
// only their existence matters, data are irrelevant
this.data['mail.channel'].records.push({}, {});
await this.start();
const { createMessagingMenuComponent } = await this.start();
await createMessagingMenuComponent();
await afterNextRender(() => {
document.querySelector('.o_MessagingMenu_toggler').click();
@@ -1410,13 +1428,14 @@ QUnit.test('open 3 different chat windows: not enough screen width', async funct
// 3 channels are expected to be found in the messaging menu, each with a
// random unique id that will be referenced in the test
this.data['mail.channel'].records.push({ id: 1 }, { id: 2 }, { id: 3 });
await this.start({
const { createMessagingMenuComponent } = await this.start({
env: {
browser: {
innerWidth: 900, // enough to fit 2 chat windows but not 3
},
},
});
await createMessagingMenuComponent();
// open, from systray menu, chat windows of channels with Id 1, 2, then 3
await afterNextRender(() =>
@@ -1554,7 +1573,8 @@ QUnit.test('chat window: switch on TAB', async function (assert) {
{ id: 1, name: "channel1" },
{ id: 2, name: "channel2" }
);
await this.start();
const { createMessagingMenuComponent } = await this.start();
await createMessagingMenuComponent();
await afterNextRender(() =>
document.querySelector(`.o_MessagingMenu_toggler`).click()
@@ -1759,7 +1779,8 @@ QUnit.test('chat window with a thread: keep scroll position in message list on f
res_id: 20,
});
}
await this.start();
const { createMessagingMenuComponent } = await this.start();
await createMessagingMenuComponent();
await afterNextRender(() => document.querySelector(`.o_MessagingMenu_toggler`).click());
await this.afterEvent({
eventName: 'o-component-message-list-scrolled',
@@ -1872,13 +1893,14 @@ QUnit.test('chat window: post message on non-mailing channel with "CTRL-Enter" k
id: 20,
is_minimized: true,
});
await this.start({
const { createMessagingMenuComponent } = await this.start({
env: {
device: {
isMobile: true, // here isMobile is used for the small screen size, not actually for the mobile devices
},
},
});
await createMessagingMenuComponent();
await afterNextRender(() => document.querySelector(`.o_MessagingMenu_toggler`).click());
await afterNextRender(() =>
@@ -1913,7 +1935,8 @@ QUnit.test('chat window with a thread: keep scroll position in message list on t
res_id: 20,
});
}
await this.start();
const { createMessagingMenuComponent } = await this.start();
await createMessagingMenuComponent();
await afterNextRender(() => document.querySelector(`.o_MessagingMenu_toggler`).click());
await this.afterEvent({
eventName: 'o-component-message-list-scrolled',
@@ -1983,7 +2006,8 @@ QUnit.test('chat window with a thread: keep scroll position in message list on t
res_id: 20,
});
}
await this.start();
const { createMessagingMenuComponent } = await this.start();
await createMessagingMenuComponent();
await afterNextRender(() => document.querySelector(`.o_MessagingMenu_toggler`).click());
await this.afterEvent({
eventName: 'o-component-message-list-scrolled',
@@ -30,6 +30,16 @@ export class MessagingMenu extends Component {
*/
_constructor() {}
/**
* @override
*/
setup() {
// for now, the legacy env is needed for internal functions such as
// `useModels` to work
this.env = owl.Component.env;
super.setup();
}
mounted() {
document.addEventListener('click', this._onClickCaptureGlobal, true);
}
@@ -18,13 +18,14 @@ QUnit.module('messaging_menu_tests.js', {
beforeEach(this);
this.start = async params => {
let { discussWidget, env, widget } = await start(Object.assign({}, params, {
const res = await start(Object.assign({}, params, {
data: this.data,
hasMessagingMenu: true,
}));
const { discussWidget, env, widget } = res;
this.discussWidget = discussWidget;
this.env = env;
this.widget = widget;
return res;
};
},
afterEach() {
@@ -44,10 +45,11 @@ QUnit.test('[technical] messaging not created then becomes created', async funct
assert.expect(2);
const messagingBeforeCreationDeferred = makeTestPromise();
await this.start({
const { createMessagingMenuComponent } = await this.start({
messagingBeforeCreationDeferred,
waitUntilMessagingCondition: 'none',
});
await createMessagingMenuComponent();
assert.containsOnce(
document.body,
'.o_MessagingMenu',
@@ -78,10 +80,11 @@ QUnit.test('[technical] no crash on attempting opening messaging menu when messa
*/
assert.expect(2);
await this.start({
const { createMessagingMenuComponent } = await this.start({
messagingBeforeCreationDeferred: new Promise(() => {}), // keep messaging not created
waitUntilMessagingCondition: 'none',
});
await createMessagingMenuComponent();
assert.containsOnce(
document.body,
'.o_MessagingMenu',
@@ -107,7 +110,7 @@ QUnit.test('[technical] no crash on attempting opening messaging menu when messa
QUnit.test('messaging not initialized', async function (assert) {
assert.expect(2);
await this.start({
const { createMessagingMenuComponent } = await this.start({
async mockRPC(route) {
if (route === '/mail/init_messaging') {
// simulate messaging never initialized
@@ -117,6 +120,7 @@ QUnit.test('messaging not initialized', async function (assert) {
},
waitUntilMessagingCondition: 'created',
});
await createMessagingMenuComponent();
assert.strictEqual(
document.querySelectorAll('.o_MessagingMenu_loading').length,
1,
@@ -136,7 +140,7 @@ QUnit.test('messaging becomes initialized', async function (assert) {
const messagingInitializedProm = makeTestPromise();
await this.start({
const { createMessagingMenuComponent } = await this.start({
async mockRPC(route) {
const _super = this._super.bind(this, ...arguments); // limitation of class.js
if (route === '/mail/init_messaging') {
@@ -146,6 +150,7 @@ QUnit.test('messaging becomes initialized', async function (assert) {
},
waitUntilMessagingCondition: 'created',
});
await createMessagingMenuComponent();
await afterNextRender(() => document.querySelector(`.o_MessagingMenu_toggler`).click());
// simulate messaging becomes initialized
@@ -164,7 +169,8 @@ QUnit.test('messaging becomes initialized', async function (assert) {
QUnit.test('basic rendering', async function (assert) {
assert.expect(21);
await this.start();
const { createMessagingMenuComponent } = await this.start();
await createMessagingMenuComponent();
assert.strictEqual(
document.querySelectorAll('.o_MessagingMenu').length,
1,
@@ -303,7 +309,8 @@ QUnit.test('counter is taking into account failure notification', async function
mail_message_id: 11, // id of the related message
notification_status: 'exception', // necessary value to have a failure
});
await this.start();
const { createMessagingMenuComponent } = await this.start();
await createMessagingMenuComponent();
assert.containsOnce(
document.body,
@@ -320,7 +327,8 @@ QUnit.test('counter is taking into account failure notification', async function
QUnit.test('switch tab', async function (assert) {
assert.expect(15);
await this.start();
const { createMessagingMenuComponent } = await this.start();
await createMessagingMenuComponent();
await afterNextRender(() => document.querySelector(`.o_MessagingMenu_toggler`).click());
assert.strictEqual(
@@ -427,9 +435,10 @@ QUnit.test('switch tab', async function (assert) {
QUnit.test('new message', async function (assert) {
assert.expect(3);
await this.start({
const { createMessagingMenuComponent } = await this.start({
hasChatWindow: true,
});
await createMessagingMenuComponent();
await afterNextRender(() =>
document.querySelector(`.o_MessagingMenu_toggler`).click()
@@ -456,10 +465,11 @@ QUnit.test('new message', async function (assert) {
QUnit.test('no new message when discuss is open', async function (assert) {
assert.expect(3);
await this.start({
const { createMessagingMenuComponent } = await this.start({
autoOpenDiscuss: true,
hasDiscuss: true,
});
await createMessagingMenuComponent();
await afterNextRender(() =>
document.querySelector(`.o_MessagingMenu_toggler`).click()
@@ -506,7 +516,8 @@ QUnit.test('channel preview: basic rendering', async function (assert) {
model: 'mail.channel', // necessary to link message to channel
res_id: 20, // id of related channel
});
await this.start();
const { createMessagingMenuComponent } = await this.start();
await createMessagingMenuComponent();
await afterNextRender(() => document.querySelector(`.o_MessagingMenu_toggler`).click());
assert.strictEqual(
@@ -603,7 +614,8 @@ QUnit.test('filtered previews', async function (assert) {
res_id: 20, // id of related channel
},
);
await this.start();
const { createMessagingMenuComponent } = await this.start();
await createMessagingMenuComponent();
await afterNextRender(() =>
document.querySelector(`.o_MessagingMenu_toggler`).click()
@@ -754,9 +766,10 @@ QUnit.test('open chat window from preview', async function (assert) {
// channel expected to be found in the menu, only its existence matters, data are irrelevant
this.data['mail.channel'].records.push({});
await this.start({
const { createMessagingMenuComponent } = await this.start({
hasChatWindow: true,
});
await createMessagingMenuComponent();
await afterNextRender(() =>
document.querySelector(`.o_MessagingMenu_toggler`).click()
@@ -780,7 +793,8 @@ QUnit.test('no code injection in message body preview', async function (assert)
model: "mail.channel",
res_id: 11,
});
await this.start();
const { createMessagingMenuComponent } = await this.start();
await createMessagingMenuComponent();
await afterNextRender(() => {
document.querySelector(`.o_MessagingMenu_toggler`).click();
@@ -822,7 +836,8 @@ QUnit.test('no code injection in message body preview from sanitized message', a
model: "mail.channel",
res_id: 11,
});
await this.start();
const { createMessagingMenuComponent } = await this.start();
await createMessagingMenuComponent();
await afterNextRender(() => {
document.querySelector(`.o_MessagingMenu_toggler`).click();
@@ -864,7 +879,8 @@ QUnit.test('<br/> tags in message body preview are transformed in spaces', async
model: "mail.channel",
res_id: 11,
});
await this.start();
const { createMessagingMenuComponent } = await this.start();
await createMessagingMenuComponent();
await afterNextRender(() => {
document.querySelector(`.o_MessagingMenu_toggler`).click();
@@ -894,7 +910,7 @@ QUnit.test('<br/> tags in message body preview are transformed in spaces', async
QUnit.test('rendering with OdooBot has a request (default)', async function (assert) {
assert.expect(4);
await this.start({
const { createMessagingMenuComponent } = await this.start({
env: {
browser: {
Notification: {
@@ -903,6 +919,7 @@ QUnit.test('rendering with OdooBot has a request (default)', async function (ass
},
},
});
await createMessagingMenuComponent();
assert.ok(
document.querySelector('.o_MessagingMenu_counter'),
@@ -932,7 +949,7 @@ QUnit.test('rendering with OdooBot has a request (default)', async function (ass
QUnit.test('rendering without OdooBot has a request (denied)', async function (assert) {
assert.expect(2);
await this.start({
const { createMessagingMenuComponent } = await this.start({
env: {
browser: {
Notification: {
@@ -941,6 +958,7 @@ QUnit.test('rendering without OdooBot has a request (denied)', async function (a
},
},
});
await createMessagingMenuComponent();
assert.containsNone(
document.body,
@@ -961,7 +979,7 @@ QUnit.test('rendering without OdooBot has a request (denied)', async function (a
QUnit.test('rendering without OdooBot has a request (accepted)', async function (assert) {
assert.expect(2);
await this.start({
const { createMessagingMenuComponent } = await this.start({
env: {
browser: {
Notification: {
@@ -970,6 +988,7 @@ QUnit.test('rendering without OdooBot has a request (accepted)', async function
},
},
});
await createMessagingMenuComponent();
assert.containsNone(
document.body,
@@ -990,7 +1009,7 @@ QUnit.test('rendering without OdooBot has a request (accepted)', async function
QUnit.test('respond to notification prompt (denied)', async function (assert) {
assert.expect(4);
await this.start({
const { createMessagingMenuComponent } = await this.start({
env: {
browser: {
Notification: {
@@ -1012,6 +1031,7 @@ QUnit.test('respond to notification prompt (denied)', async function (assert) {
}
},
});
await createMessagingMenuComponent();
await afterNextRender(() =>
document.querySelector('.o_MessagingMenu_toggler').click()
@@ -1047,7 +1067,8 @@ QUnit.test('Group chat should be displayed inside the chat section of the messag
channel_type: 'group',
is_pinned: true,
});
await this.start();
const { createMessagingMenuComponent } = await this.start();
await createMessagingMenuComponent();
await afterNextRender(() =>
document.querySelector('.o_MessagingMenu_toggler').click()
@@ -3,28 +3,30 @@
<t t-name="mail.NotificationRequest" owl="1">
<div class="o_NotificationRequest" t-on-click="_onClick">
<div class="o_NotificationRequest_sidebar">
<div class="o_NotificationRequest_imageContainer o_NotificationRequest_sidebarItem">
<img class="o_NotificationRequest_image rounded-circle" src="/mail/static/src/img/odoobot.png" alt="Avatar of OdooBot"/>
<PartnerImStatusIcon
class="o_NotificationRequest_partnerImStatusIcon"
t-att-class="{ 'o-mobile': messaging.device.isMobile }"
partnerLocalId="messaging.partnerRoot.localId"
/>
<t t-if="messaging">
<div class="o_NotificationRequest_sidebar">
<div class="o_NotificationRequest_imageContainer o_NotificationRequest_sidebarItem">
<img class="o_NotificationRequest_image rounded-circle" src="/mail/static/src/img/odoobot.png" alt="Avatar of OdooBot"/>
<PartnerImStatusIcon
class="o_NotificationRequest_partnerImStatusIcon"
t-att-class="{ 'o-mobile': messaging.device.isMobile }"
partnerLocalId="messaging.partnerRoot.localId"
/>
</div>
</div>
</div>
<div class="o_NotificationRequest_content">
<div class="o_NotificationRequest_header">
<span class="o_NotificationRequest_name text-truncate" t-att-class="{ 'o-mobile': messaging.device.isMobile }">
<t t-esc="getHeaderText()"/>
</span>
<div class="o_NotificationRequest_content">
<div class="o_NotificationRequest_header">
<span class="o_NotificationRequest_name text-truncate" t-att-class="{ 'o-mobile': messaging.device.isMobile }">
<t t-esc="getHeaderText()"/>
</span>
</div>
<div class="o_NotificationRequest_core">
<span class="o_NotificationRequest_coreItem o_NotificationRequest_inlineText text-truncate">
Enable desktop notifications to chat.
</span>
</div>
</div>
<div class="o_NotificationRequest_core">
<span class="o_NotificationRequest_coreItem o_NotificationRequest_inlineText text-truncate">
Enable desktop notifications to chat.
</span>
</div>
</div>
</t>
</div>
</t>
@@ -6,6 +6,16 @@ const { Component } = owl;
export class RtcActivityNotice extends Component {
/**
* @override
*/
setup() {
// for now, the legacy env is needed for internal functions such as
// `useModels` to work
this.env = owl.Component.env;
super.setup();
}
//--------------------------------------------------------------------------
// Handlers
//--------------------------------------------------------------------------
@@ -25,12 +25,14 @@ QUnit.module('thread_needaction_preview_tests.js', {
};
this.start = async params => {
const { afterEvent, env, widget } = await start(Object.assign({}, params, {
const res = await start(Object.assign({}, params, {
data: this.data,
}));
const { afterEvent, env, widget } = res;
this.afterEvent = afterEvent;
this.env = env;
this.widget = widget;
return res;
};
},
afterEach() {
@@ -54,9 +56,8 @@ QUnit.test('mark as read', async function (assert) {
notification_type: 'inbox',
res_partner_id: this.data.currentPartnerId,
});
await this.start({
const { createMessagingMenuComponent } = await this.start({
hasChatWindow: true,
hasMessagingMenu: true,
async mockRPC(route, args) {
if (route.includes('mark_all_as_read')) {
assert.step('mark_all_as_read');
@@ -72,6 +73,7 @@ QUnit.test('mark as read', async function (assert) {
return this._super(...arguments);
},
});
await createMessagingMenuComponent();
await afterNextRender(() => this.afterEvent({
eventName: 'o-thread-cache-loaded-messages',
func: () => document.querySelector('.o_MessagingMenu_toggler').click(),
@@ -116,9 +118,8 @@ QUnit.test('click on preview should mark as read and open the thread', async fun
notification_type: 'inbox',
res_partner_id: this.data.currentPartnerId,
});
await this.start({
const { createMessagingMenuComponent } = await this.start({
hasChatWindow: true,
hasMessagingMenu: true,
async mockRPC(route, args) {
if (route.includes('mark_all_as_read')) {
assert.step('mark_all_as_read');
@@ -134,6 +135,7 @@ QUnit.test('click on preview should mark as read and open the thread', async fun
return this._super(...arguments);
},
});
await createMessagingMenuComponent();
await afterNextRender(() => this.afterEvent({
eventName: 'o-thread-cache-loaded-messages',
func: () => document.querySelector('.o_MessagingMenu_toggler').click(),
@@ -197,11 +199,11 @@ QUnit.test('click on expand from chat window should close the chat window and op
notification_type: 'inbox',
res_partner_id: this.data.currentPartnerId,
});
await this.start({
const { createMessagingMenuComponent } = await this.start({
env: { bus },
hasChatWindow: true,
hasMessagingMenu: true,
});
await createMessagingMenuComponent();
await afterNextRender(() => this.afterEvent({
eventName: 'o-thread-cache-loaded-messages',
func: () => document.querySelector('.o_MessagingMenu_toggler').click(),
@@ -261,9 +263,8 @@ QUnit.test('[technical] opening a non-channel chat window should not call channe
notification_type: 'inbox',
res_partner_id: this.data.currentPartnerId,
});
await this.start({
const { createMessagingMenuComponent } = await this.start({
hasChatWindow: true,
hasMessagingMenu: true,
async mockRPC(route, args) {
if (route.includes('channel_fold')) {
const message = "should not call channel_fold when opening a non-channel chat window";
@@ -274,6 +275,7 @@ QUnit.test('[technical] opening a non-channel chat window should not call channe
return this._super(...arguments);
},
});
await createMessagingMenuComponent();
await afterNextRender(() => this.afterEvent({
eventName: 'o-thread-cache-loaded-messages',
func: () => document.querySelector('.o_MessagingMenu_toggler').click(),
@@ -332,10 +334,8 @@ QUnit.test('preview should display last needaction message preview even if there
notification_type: 'inbox',
res_partner_id: this.data.currentPartnerId,
});
await this.start({
hasChatWindow: true,
hasMessagingMenu: true,
});
const { createMessagingMenuComponent } = await this.start({ hasChatWindow: true });
await createMessagingMenuComponent();
await afterNextRender(() => this.afterEvent({
eventName: 'o-thread-cache-loaded-messages',
func: () => document.querySelector('.o_MessagingMenu_toggler').click(),
@@ -375,10 +375,8 @@ QUnit.test('chat window header should not have unread counter for non-channel th
notification_type: 'inbox',
res_partner_id: this.data.currentPartnerId,
});
await this.start({
hasChatWindow: true,
hasMessagingMenu: true,
});
const { createMessagingMenuComponent } = await this.start({ hasChatWindow: true });
await createMessagingMenuComponent();
await afterNextRender(() => this.afterEvent({
eventName: 'o-thread-cache-loaded-messages',
func: () => document.querySelector('.o_MessagingMenu_toggler').click(),
+2 -14
View File
@@ -3,27 +3,15 @@
import { ChatWindowService } from '@mail/services/chat_window_service/chat_window_service';
import { DialogService } from '@mail/services/dialog_service/dialog_service';
import { MessagingService } from '@mail/services/messaging/messaging';
import { SystrayService } from '@mail/services/systray_service/systray_service';
import { DiscussWidget } from '@mail/widgets/discuss/discuss';
import { MessagingMenuWidget } from '@mail/widgets/messaging_menu/messaging_menu';
import { action_registry } from 'web.core';
import { serviceRegistry } from 'web.core';
import SystrayMenu from 'web.SystrayMenu';
serviceRegistry.add('chat_window', ChatWindowService);
serviceRegistry.add('dialog', DialogService);
serviceRegistry.add('messaging', MessagingService);
serviceRegistry.add('systray_service', SystrayService);
action_registry.add('mail.widgets.discuss', DiscussWidget);
// Systray menu items display order matches order in the list
// lower index comes first, and display is from right to left.
// For messagin menu, it should come before activity menu, if any
// otherwise, it is the next systray item.
const activityMenuIndex = SystrayMenu.Items.findIndex(SystrayMenuItem =>
SystrayMenuItem.prototype.name === 'activity_menu');
if (activityMenuIndex > 0) {
SystrayMenu.Items.splice(activityMenuIndex, 0, MessagingMenuWidget);
} else {
SystrayMenu.Items.push(MessagingMenuWidget);
}
@@ -0,0 +1,24 @@
/** @odoo-module **/
import { getMessagingComponent } from '@mail/utils/messaging_component';
import AbstractService from 'web.AbstractService';
import { registry } from '@web/core/registry';
const systrayRegistry = registry.category('systray');
export const SystrayService = AbstractService.extend({
dependencies: ['messaging'],
/**
* @override {web.AbstractService}
*/
async start() {
await owl.Component.env.services.messaging.modelManager.messagingCreatedPromise;
systrayRegistry.add('mail.MessagingMenu', {
Component: getMessagingComponent('MessagingMenu'),
});
systrayRegistry.add('mail.RtcActivityNotice', {
Component: getMessagingComponent('RtcActivityNotice'),
});
},
});
+10 -35
View File
@@ -14,7 +14,6 @@ import { DialogService } from '@mail/services/dialog_service/dialog_service';
import { getMessagingComponent } from '@mail/utils/messaging_component';
import { nextTick } from '@mail/utils/utils';
import { DiscussWidget } from '@mail/widgets/discuss/discuss';
import { MessagingMenuWidget } from '@mail/widgets/messaging_menu/messaging_menu';
import { MockModels } from '@mail/../tests/helpers/mock_models';
import AbstractStorageService from 'web.AbstractStorageService';
@@ -154,33 +153,6 @@ function _useDiscuss(callbacks) {
});
}
/**
* @private
* @param {Object} callbacks
* @param {function[]} callbacks.init
* @param {function[]} callbacks.mount
* @param {function[]} callbacks.destroy
* @param {function[]} callbacks.return
* @returns {Object} update callbacks
*/
function _useMessagingMenu(callbacks) {
const {
mount: prevMount,
return: prevReturn,
} = callbacks;
let messagingMenuWidget;
return Object.assign({}, callbacks, {
mount: prevMount.concat(async ({ selector, widget }) => {
messagingMenuWidget = new MessagingMenuWidget(widget, {});
await messagingMenuWidget.appendTo($(selector));
await messagingMenuWidget.on_attach_callback();
}),
return: prevReturn.concat(result => {
Object.assign(result, { messagingMenuWidget });
}),
});
}
//------------------------------------------------------------------------------
// Public: rendering timers
//------------------------------------------------------------------------------
@@ -463,6 +435,15 @@ function getCreateMessageComponent({ components, env, modelManager, widget }) {
};
}
function getCreateMessagingMenuComponent({ components, env, widget }) {
return async function createMessagingMenuComponent() {
await createRootMessagingComponent({ components, env }, 'MessagingMenu', {
props: {},
target: widget.el,
});
};
}
function getCreateThreadViewComponent({ afterEvent, components, env, widget }) {
return async function createThreadViewComponent(threadView, otherProps = {}, { isFixedSize = false, waitUntilMessagesLoaded = true } = {}) {
let target;
@@ -524,8 +505,6 @@ function getCreateThreadViewComponent({ afterEvent, components, env, widget }) {
* @param {boolean} [param0.hasChatWindow=false] if set, mount chat window
* service.
* @param {boolean} [param0.hasDiscuss=false] if set, mount discuss app.
* @param {boolean} [param0.hasMessagingMenu=false] if set, mount messaging
* menu.
* @param {boolean} [param0.hasTimeControl=false] if set, all flow of time
* with `env.browser.setTimeout` are fully controlled by test itself.
* @see addTimeControlToEnv that adds `advanceTime` function in
@@ -581,7 +560,6 @@ async function start(param0 = {}) {
hasChatWindow = false,
hasDialog = false,
hasDiscuss = false,
hasMessagingMenu = false,
hasTimeControl = false,
hasView = false,
loadingBaseDelayDuration = 0,
@@ -596,7 +574,6 @@ async function start(param0 = {}) {
delete param0.hasWebClient;
delete param0.hasChatWindow;
delete param0.hasDiscuss;
delete param0.hasMessagingMenu;
delete param0.hasTimeControl;
delete param0.hasView;
if (hasChatWindow) {
@@ -608,9 +585,6 @@ async function start(param0 = {}) {
if (hasDiscuss) {
callbacks = _useDiscuss(callbacks);
}
if (hasMessagingMenu) {
callbacks = _useMessagingMenu(callbacks);
}
const messagingBus = new EventBus();
const {
init: initCallbacks,
@@ -814,6 +788,7 @@ async function start(param0 = {}) {
createComposerComponent: getCreateComposerComponent({ components, env: testEnv, modelManager, widget }),
createComposerSuggestionComponent: getCreateComposerSuggestionComponent({ components, env: testEnv, modelManager, widget }),
createMessageComponent: getCreateMessageComponent({ components, env: testEnv, modelManager, widget }),
createMessagingMenuComponent: getCreateMessagingMenuComponent({ components, env: testEnv, widget }),
createThreadViewComponent: getCreateThreadViewComponent({ afterEvent, components, env: testEnv, widget }),
};
}
@@ -1,36 +0,0 @@
/** @odoo-module **/
import { getMessagingComponent } from "@mail/utils/messaging_component";
import Widget from 'web.Widget';
/**
* Odoo Widget, necessary to instantiate component.
*/
export const MessagingMenuWidget = Widget.extend({
template: 'mail.widgets.MessagingMenu',
/**
* @override
*/
init() {
this._super(...arguments);
this.component = undefined;
},
/**
* @override
*/
destroy() {
if (this.component) {
this.component.destroy();
}
this._super(...arguments);
},
async on_attach_callback() {
const MessagingMenu = getMessagingComponent("MessagingMenu");
this.component = new MessagingMenu(null);
await this.component.mount(this.el);
// unwrap
this.el.parentNode.insertBefore(this.component.el, this.el);
this.el.parentNode.removeChild(this.el);
},
});
@@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="mail.widgets.MessagingMenu">
<li class="o_widget_SystrayMessagingItem"/>
</t>
</templates>
@@ -1,40 +0,0 @@
/** @odoo-module **/
import { getMessagingComponent } from '@mail/utils/messaging_component';
import SystrayMenu from 'web.SystrayMenu';
import Widget from 'web.Widget';
/**
* Odoo Widget, necessary to instantiate component.
*/
export const RtcActivityNoticeWidget = Widget.extend({
template: 'mail.widgets.RtcActivityNotice',
sequence: 99, // position in the systray, right to left.
/**
* @override
*/
init() {
this._super(...arguments);
this.component = undefined;
},
/**
* @override
*/
destroy() {
if (this.component) {
this.component.destroy();
}
this._super(...arguments);
},
async on_attach_callback() {
const RtcActivityNotice = getMessagingComponent('RtcActivityNotice');
this.component = new RtcActivityNotice(null);
await this.component.mount(this.el);
// unwrap
this.el.parentNode.insertBefore(this.component.el, this.el);
this.el.parentNode.removeChild(this.el);
},
});
SystrayMenu.Items.push(RtcActivityNoticeWidget);
@@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="mail.widgets.RtcActivityNotice">
<div class="o_widget_RtcActivityNotice"/>
</t>
</templates>
@@ -234,9 +234,8 @@ QUnit.test('activity menu widget: activity view icon', async function (assert) {
QUnit.test('activity menu widget: close on messaging menu click', async function (assert) {
assert.expect(2);
const { widget } = await start({
const { createMessagingMenuComponent, widget } = await start({
data: this.data,
hasMessagingMenu: true,
async mockRPC(route, args) {
if (args.method === 'systray_get_activities') {
return [];
@@ -244,6 +243,7 @@ QUnit.test('activity menu widget: close on messaging menu click', async function
return this._super(route, args);
},
});
await createMessagingMenuComponent();
const activityMenu = new ActivityMenu(widget);
await activityMenu.appendTo($('#qunit-fixture'));
await testUtils.nextTick();
@@ -92,7 +92,8 @@ export class NavBar extends Component {
get systrayItems() {
return systrayRegistry
.getAll()
.getEntries()
.map(([key, value]) => ({ key, ...value }))
.filter((item) => ("isDisplayed" in item ? item.isDisplayed(this.env) : true))
.reverse();
}
@@ -30,7 +30,7 @@
<!-- Systray -->
<div class="o_menu_systray" role="menu">
<t t-foreach="systrayItems" t-as="item" t-key="item_index">
<t t-foreach="systrayItems" t-as="item" t-key="item.key">
<NotUpdatable>
<ErrorHandler onError="error => handleItemError(error, item)">
<t t-component="item.Component" t-props="item.props" />