From 7c76f29111ff8bd9cec8ac107b979cdd414191b7 Mon Sep 17 00:00:00 2001 From: "Louis Wicket (wil)" Date: Thu, 7 Oct 2021 18:09:03 +0000 Subject: [PATCH] [FIX] mail, web: properly add messaging menu to the systray MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) Signed-off-by: Ooming --- .../tests/chat_window_manager_tests.js | 9 ++- .../tests/messaging_menu_tests.js | 8 ++- .../tests/chat_window_manager_tests.js | 72 ++++++++++++------- .../messaging_menu/messaging_menu.js | 10 +++ .../tests/messaging_menu_tests.js | 65 +++++++++++------ .../notification_request.xml | 42 +++++------ .../rtc_activity_notice.js | 10 +++ .../tests/thread_needaction_preview_tests.js | 32 ++++----- addons/mail/static/src/main.js | 16 +---- .../systray_service/systray_service.js | 24 +++++++ addons/mail/static/src/utils/test_utils.js | 45 +++--------- .../widgets/messaging_menu/messaging_menu.js | 36 ---------- .../widgets/messaging_menu/messaging_menu.xml | 8 --- .../rtc_activity_notice.js | 40 ----------- .../rtc_activity_notice.xml | 8 --- .../systray/systray_activity_menu_tests.js | 4 +- .../web/static/src/webclient/navbar/navbar.js | 3 +- .../static/src/webclient/navbar/navbar.xml | 2 +- 18 files changed, 200 insertions(+), 234 deletions(-) create mode 100644 addons/mail/static/src/services/systray_service/systray_service.js delete mode 100644 addons/mail/static/src/widgets/messaging_menu/messaging_menu.js delete mode 100644 addons/mail/static/src/widgets/messaging_menu/messaging_menu.xml delete mode 100644 addons/mail/static/src/widgets/rtc_activity_notice/rtc_activity_notice.js delete mode 100644 addons/mail/static/src/widgets/rtc_activity_notice/rtc_activity_notice.xml diff --git a/addons/im_livechat/static/src/components/chat_window_manager/tests/chat_window_manager_tests.js b/addons/im_livechat/static/src/components/chat_window_manager/tests/chat_window_manager_tests.js index 7ad7f9a0f87..73ce465f094 100644 --- a/addons/im_livechat/static/src/components/chat_window_manager/tests/chat_window_manager_tests.js +++ b/addons/im_livechat/static/src/components/chat_window_manager/tests/chat_window_manager_tests.js @@ -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()); diff --git a/addons/im_livechat/static/src/components/messaging_menu/tests/messaging_menu_tests.js b/addons/im_livechat/static/src/components/messaging_menu/tests/messaging_menu_tests.js index c5ef06a0975..6b988ee921f 100644 --- a/addons/im_livechat/static/src/components/messaging_menu/tests/messaging_menu_tests.js +++ b/addons/im_livechat/static/src/components/messaging_menu/tests/messaging_menu_tests.js @@ -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', diff --git a/addons/mail/static/src/components/chat_window_manager/tests/chat_window_manager_tests.js b/addons/mail/static/src/components/chat_window_manager/tests/chat_window_manager_tests.js index 4c697ddb06f..c5be8007fb0 100644 --- a/addons/mail/static/src/components/chat_window_manager/tests/chat_window_manager_tests.js +++ b/addons/mail/static/src/components/chat_window_manager/tests/chat_window_manager_tests.js @@ -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', diff --git a/addons/mail/static/src/components/messaging_menu/messaging_menu.js b/addons/mail/static/src/components/messaging_menu/messaging_menu.js index 032e9a082fc..0d5629fcdd9 100644 --- a/addons/mail/static/src/components/messaging_menu/messaging_menu.js +++ b/addons/mail/static/src/components/messaging_menu/messaging_menu.js @@ -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); } diff --git a/addons/mail/static/src/components/messaging_menu/tests/messaging_menu_tests.js b/addons/mail/static/src/components/messaging_menu/tests/messaging_menu_tests.js index 756c193a224..6a717ce7a45 100644 --- a/addons/mail/static/src/components/messaging_menu/tests/messaging_menu_tests.js +++ b/addons/mail/static/src/components/messaging_menu/tests/messaging_menu_tests.js @@ -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('
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('
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() diff --git a/addons/mail/static/src/components/notification_request/notification_request.xml b/addons/mail/static/src/components/notification_request/notification_request.xml index f238f4f5c48..6849c2134d2 100644 --- a/addons/mail/static/src/components/notification_request/notification_request.xml +++ b/addons/mail/static/src/components/notification_request/notification_request.xml @@ -3,28 +3,30 @@
-
-
- Avatar of OdooBot - + +
+
+ Avatar of OdooBot + +
-
-
-
- - - +
+
+ + + +
+
+ + Enable desktop notifications to chat. + +
-
- - Enable desktop notifications to chat. - -
-
+
diff --git a/addons/mail/static/src/components/rtc_activity_notice/rtc_activity_notice.js b/addons/mail/static/src/components/rtc_activity_notice/rtc_activity_notice.js index 09f02c40d19..695a7e6a193 100644 --- a/addons/mail/static/src/components/rtc_activity_notice/rtc_activity_notice.js +++ b/addons/mail/static/src/components/rtc_activity_notice/rtc_activity_notice.js @@ -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 //-------------------------------------------------------------------------- diff --git a/addons/mail/static/src/components/thread_needaction_preview/tests/thread_needaction_preview_tests.js b/addons/mail/static/src/components/thread_needaction_preview/tests/thread_needaction_preview_tests.js index 1aa833697a3..d91469ed06b 100644 --- a/addons/mail/static/src/components/thread_needaction_preview/tests/thread_needaction_preview_tests.js +++ b/addons/mail/static/src/components/thread_needaction_preview/tests/thread_needaction_preview_tests.js @@ -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(), diff --git a/addons/mail/static/src/main.js b/addons/mail/static/src/main.js index b37869d11dc..491b8c7adcc 100644 --- a/addons/mail/static/src/main.js +++ b/addons/mail/static/src/main.js @@ -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); -} diff --git a/addons/mail/static/src/services/systray_service/systray_service.js b/addons/mail/static/src/services/systray_service/systray_service.js new file mode 100644 index 00000000000..c2644575938 --- /dev/null +++ b/addons/mail/static/src/services/systray_service/systray_service.js @@ -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'), + }); + }, +}); diff --git a/addons/mail/static/src/utils/test_utils.js b/addons/mail/static/src/utils/test_utils.js index 62069a6cab6..b42e65ce14e 100644 --- a/addons/mail/static/src/utils/test_utils.js +++ b/addons/mail/static/src/utils/test_utils.js @@ -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 }), }; } diff --git a/addons/mail/static/src/widgets/messaging_menu/messaging_menu.js b/addons/mail/static/src/widgets/messaging_menu/messaging_menu.js deleted file mode 100644 index f43bc025661..00000000000 --- a/addons/mail/static/src/widgets/messaging_menu/messaging_menu.js +++ /dev/null @@ -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); - }, -}); diff --git a/addons/mail/static/src/widgets/messaging_menu/messaging_menu.xml b/addons/mail/static/src/widgets/messaging_menu/messaging_menu.xml deleted file mode 100644 index 308c1f31134..00000000000 --- a/addons/mail/static/src/widgets/messaging_menu/messaging_menu.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - -
  • - - - diff --git a/addons/mail/static/src/widgets/rtc_activity_notice/rtc_activity_notice.js b/addons/mail/static/src/widgets/rtc_activity_notice/rtc_activity_notice.js deleted file mode 100644 index a7a9e86ed65..00000000000 --- a/addons/mail/static/src/widgets/rtc_activity_notice/rtc_activity_notice.js +++ /dev/null @@ -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); diff --git a/addons/mail/static/src/widgets/rtc_activity_notice/rtc_activity_notice.xml b/addons/mail/static/src/widgets/rtc_activity_notice/rtc_activity_notice.xml deleted file mode 100644 index d5eb221310e..00000000000 --- a/addons/mail/static/src/widgets/rtc_activity_notice/rtc_activity_notice.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - -
    - - - diff --git a/addons/mail/static/tests/systray/systray_activity_menu_tests.js b/addons/mail/static/tests/systray/systray_activity_menu_tests.js index 1a0f0f7b25c..44daf93b912 100644 --- a/addons/mail/static/tests/systray/systray_activity_menu_tests.js +++ b/addons/mail/static/tests/systray/systray_activity_menu_tests.js @@ -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(); diff --git a/addons/web/static/src/webclient/navbar/navbar.js b/addons/web/static/src/webclient/navbar/navbar.js index d5771b7b2fc..7fc1c3bf4f3 100644 --- a/addons/web/static/src/webclient/navbar/navbar.js +++ b/addons/web/static/src/webclient/navbar/navbar.js @@ -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(); } diff --git a/addons/web/static/src/webclient/navbar/navbar.xml b/addons/web/static/src/webclient/navbar/navbar.xml index 4f1043d9034..31d067766d6 100644 --- a/addons/web/static/src/webclient/navbar/navbar.xml +++ b/addons/web/static/src/webclient/navbar/navbar.xml @@ -30,7 +30,7 @@