From 71076550deb8a49ebc683c82011caa67d832cdd9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Theys?= Date: Mon, 29 Nov 2021 13:20:45 +0000 Subject: [PATCH] [IMP] mail: add systray item containers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit To simplify messaging components in the future, they all need to have a similar shape. It is done for messaging menu and activity notice in this commit, which requires to move the waiting on "messaging becoming created" to a parent non-messaging component. closes odoo/odoo#80482 Signed-off-by: Alexandre Kühn (aku) --- .../component_hooks/use_models/use_models.js | 8 +++ .../use_rendered_values.js | 15 +++++- .../messaging_menu/messaging_menu.js | 8 +-- .../tests/messaging_menu_tests.js | 50 ++----------------- .../messaging_menu_container.js | 26 ++++++++++ .../messaging_menu_container.xml | 14 ++++++ .../rtc_activity_notice.js | 10 ---- .../rtc_activity_notice.xml | 2 +- .../rtc_activity_notice_container.js | 26 ++++++++++ .../rtc_activity_notice_container.xml | 11 ++++ .../static/src/models/messaging/messaging.js | 5 +- .../systray_service/systray_service.js | 12 ++--- .../static/src/utils/messaging_component.js | 10 +--- addons/mail/static/src/utils/test_utils.js | 39 +++++++++------ 14 files changed, 141 insertions(+), 95 deletions(-) create mode 100644 addons/mail/static/src/components/messaging_menu_container/messaging_menu_container.js create mode 100644 addons/mail/static/src/components/messaging_menu_container/messaging_menu_container.xml create mode 100644 addons/mail/static/src/components/rtc_activity_notice_container/rtc_activity_notice_container.js create mode 100644 addons/mail/static/src/components/rtc_activity_notice_container/rtc_activity_notice_container.xml diff --git a/addons/mail/static/src/component_hooks/use_models/use_models.js b/addons/mail/static/src/component_hooks/use_models/use_models.js index 25583efaa37..c555afa8e14 100644 --- a/addons/mail/static/src/component_hooks/use_models/use_models.js +++ b/addons/mail/static/src/component_hooks/use_models/use_models.js @@ -7,10 +7,18 @@ const { useComponent } = owl.hooks; /** * This hook provides support for automatically re-rendering when used records * or fields changed. + * + * Components that use this hook must be instantiated after messaging service is + * started. However there is no restriction on the messaging record (coming from + * the modelManager of the messaging service) being already initialized or even + * created. */ export function useModels() { const component = useComponent(); const { modelManager } = component.env.services.messaging; + Object.defineProperty(component, 'messaging', { + get: () => modelManager.messaging, + }); const listener = new Listener({ isLocking: false, // unfortunately __render has side effects such as children components updating their reference to their corresponding model name: `useModels() of ${component}`, diff --git a/addons/mail/static/src/component_hooks/use_rendered_values/use_rendered_values.js b/addons/mail/static/src/component_hooks/use_rendered_values/use_rendered_values.js index 930f16ecb29..525e00efe6f 100644 --- a/addons/mail/static/src/component_hooks/use_rendered_values/use_rendered_values.js +++ b/addons/mail/static/src/component_hooks/use_rendered_values/use_rendered_values.js @@ -1,5 +1,7 @@ /** @odoo-module **/ +import { Listener } from '@mail/model/model_listener'; + const { Component } = owl; const { onMounted, onPatched } = owl.hooks; @@ -16,10 +18,16 @@ export function useRenderedValues(selector) { const component = Component.current; let renderedValues; let patchedValues; - + const { modelManager } = component.env.services.messaging; + const listener = new Listener({ + name: `useRenderedValues() of ${component}`, + onChange: () => component.render(), + }); const __render = component.__render.bind(component); component.__render = function () { + modelManager.startListening(listener); renderedValues = selector(); + modelManager.stopListening(listener); return __render(...arguments); }; onMounted(onUpdate); @@ -27,5 +35,10 @@ export function useRenderedValues(selector) { function onUpdate() { patchedValues = renderedValues; } + const __destroy = component.__destroy; + component.__destroy = parent => { + modelManager.removeListener(listener); + __destroy.call(component, parent); + }; return () => patchedValues; } 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 c1519c3de4c..f183b4e2640 100644 --- a/addons/mail/static/src/components/messaging_menu/messaging_menu.js +++ b/addons/mail/static/src/components/messaging_menu/messaging_menu.js @@ -18,16 +18,12 @@ export class MessagingMenu extends Component { * item is not considered as a click away from messaging menu in mobile. */ this.id = _.uniqueId('o_messagingMenu_'); - // bind since passed as props this._onMobileNewMessageInputSelect = this._onMobileNewMessageInputSelect.bind(this); this._onMobileNewMessageInputSource = this._onMobileNewMessageInputSource.bind(this); this._onClickCaptureGlobal = this._onClickCaptureGlobal.bind(this); this._onHideMobileNewMessage = this._onHideMobileNewMessage.bind(this); this._onSelectMobileNavbarTab = this._onSelectMobileNavbarTab.bind(this); - // for now, the legacy env is needed for internal functions such as - // `useModels` to work - this.env = owl.Component.env; onMounted(() => this._mounted()); onWillUnmount(() => this._willUnmount()); } @@ -48,7 +44,7 @@ export class MessagingMenu extends Component { * @returns {mail.messaging_menu} */ get messagingMenu() { - return this.messaging && this.messaging.messagingMenu; + return this.messaging && this.messaging.models['mail.messaging_menu'].get(this.props.localId); } /** @@ -187,7 +183,7 @@ export class MessagingMenu extends Component { } Object.assign(MessagingMenu, { - props: {}, + props: { localId: String }, template: 'mail.MessagingMenu', }); 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 6a717ce7a45..6c21de3398f 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 @@ -52,61 +52,19 @@ QUnit.test('[technical] messaging not created then becomes created', async funct await createMessagingMenuComponent(); assert.containsOnce( document.body, - '.o_MessagingMenu', - "should have messaging menu even when messaging is not yet created" + '.o_MessagingMenuContainer_spinner', + "messaging menu container should have spinner when messaging is not yet created" ); // simulate messaging becoming created - messagingBeforeCreationDeferred.resolve(); - await nextAnimationFrame(); + await afterNextRender(() => messagingBeforeCreationDeferred.resolve()); assert.containsOnce( document.body, '.o_MessagingMenu', - "should still contain messaging menu after messaging has been created" + "messaging menu container should contain messaging menu after messaging has been created" ); }); -QUnit.test('[technical] no crash on attempting opening messaging menu when messaging not created', async function (assert) { - /** - * Creation of messaging in env is async due to generation of models being - * async. Generation of models is async because it requires parsing of all - * JS modules that contain pieces of model definitions. - * - * Time of having no messaging is very short, almost imperceptible by user - * on UI, but the display should not crash during this critical time period. - * - * Messaging menu is not expected to be open on click because state of - * messaging menu requires messaging being created. - */ - assert.expect(2); - - const { createMessagingMenuComponent } = await this.start({ - messagingBeforeCreationDeferred: new Promise(() => {}), // keep messaging not created - waitUntilMessagingCondition: 'none', - }); - await createMessagingMenuComponent(); - assert.containsOnce( - document.body, - '.o_MessagingMenu', - "should have messaging menu even when messaging is not yet created" - ); - - let error; - try { - document.querySelector('.o_MessagingMenu_toggler').click(); - await nextAnimationFrame(); - } catch (err) { - error = err; - } - assert.notOk( - !!error, - "Should not crash on attempt to open messaging menu when messaging not created" - ); - if (error) { - throw error; - } -}); - QUnit.test('messaging not initialized', async function (assert) { assert.expect(2); diff --git a/addons/mail/static/src/components/messaging_menu_container/messaging_menu_container.js b/addons/mail/static/src/components/messaging_menu_container/messaging_menu_container.js new file mode 100644 index 00000000000..bacdb041c6c --- /dev/null +++ b/addons/mail/static/src/components/messaging_menu_container/messaging_menu_container.js @@ -0,0 +1,26 @@ +/** @odoo-module **/ + +import { useModels } from "@mail/component_hooks/use_models/use_models"; +import { getMessagingComponent } from "@mail/utils/messaging_component"; + +const { Component } = owl; + +export class MessagingMenuContainer extends Component { + + /** + * @override + */ + setup() { + // for now, the legacy env is needed for internal functions such as + // `useModels` to work + this.env = owl.Component.env; + useModels(); + super.setup(); + } + +} + +Object.assign(MessagingMenuContainer, { + components: { MessagingMenu: getMessagingComponent('MessagingMenu') }, + template: 'mail.MessagingMenuContainer', +}); diff --git a/addons/mail/static/src/components/messaging_menu_container/messaging_menu_container.xml b/addons/mail/static/src/components/messaging_menu_container/messaging_menu_container.xml new file mode 100644 index 00000000000..13d4b86b73d --- /dev/null +++ b/addons/mail/static/src/components/messaging_menu_container/messaging_menu_container.xml @@ -0,0 +1,14 @@ + + + + +
+ + + + + + +
+
+
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 695a7e6a193..09f02c40d19 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,16 +6,6 @@ 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/rtc_activity_notice/rtc_activity_notice.xml b/addons/mail/static/src/components/rtc_activity_notice/rtc_activity_notice.xml index 871fd7151e4..9ed259b04a9 100644 --- a/addons/mail/static/src/components/rtc_activity_notice/rtc_activity_notice.xml +++ b/addons/mail/static/src/components/rtc_activity_notice/rtc_activity_notice.xml @@ -7,7 +7,7 @@ Open conference: - +
+ + + +
+ + + +
+
+
diff --git a/addons/mail/static/src/models/messaging/messaging.js b/addons/mail/static/src/models/messaging/messaging.js index 973a3713334..d2d38248399 100644 --- a/addons/mail/static/src/models/messaging/messaging.js +++ b/addons/mail/static/src/models/messaging/messaging.js @@ -27,7 +27,10 @@ registerModel({ */ async start() { this.env.services['bus_service'].on('window_focus', null, this._handleGlobalWindowFocus); - await this.async(() => this.initializer.start()); + await this.initializer.start(); + if (!this.exists()) { + return; + } this.notificationHandler.start(); this.update({ isInitialized: true }); this.initializedPromise.resolve(); diff --git a/addons/mail/static/src/services/systray_service/systray_service.js b/addons/mail/static/src/services/systray_service/systray_service.js index c2644575938..2ae1fd6ea2b 100644 --- a/addons/mail/static/src/services/systray_service/systray_service.js +++ b/addons/mail/static/src/services/systray_service/systray_service.js @@ -1,6 +1,7 @@ /** @odoo-module **/ -import { getMessagingComponent } from '@mail/utils/messaging_component'; +import { MessagingMenuContainer } from '@mail/components/messaging_menu_container/messaging_menu_container'; +import { RtcActivityNoticeContainer } from '@mail/components/rtc_activity_notice_container/rtc_activity_notice_container'; import AbstractService from 'web.AbstractService'; import { registry } from '@web/core/registry'; @@ -13,12 +14,7 @@ export const SystrayService = AbstractService.extend({ * @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'), - }); + systrayRegistry.add('mail.MessagingMenuContainer', { Component: MessagingMenuContainer }); + systrayRegistry.add('mail.RtcActivityNoticeContainer', { Component: RtcActivityNoticeContainer }); }, }); diff --git a/addons/mail/static/src/utils/messaging_component.js b/addons/mail/static/src/utils/messaging_component.js index 415b1898b46..a8cc1718f53 100644 --- a/addons/mail/static/src/utils/messaging_component.js +++ b/addons/mail/static/src/utils/messaging_component.js @@ -28,14 +28,11 @@ export function registerMessagingComponent(ComponentClass, { propsCompareDepth = // Defining the class in an object and immediately taking it out so that it // has "decoratedName" as its class name in stack traces and stuff. const MessagingClass = { [decoratedName]: class extends ComponentClass { - setup(...args) { + setup() { this.root = useRef('root'); - super.setup(...args); - // useModels must be defined after useRenderedValues, indeed records and - // fields accessed during useRenderedValues should be observed by - // useModels as if they were part of the OWL rendering itself. useModels(); useShouldUpdateBasedOnProps({ propsCompareDepth }); + super.setup(); } get className() { let res = ''; @@ -51,9 +48,6 @@ export function registerMessagingComponent(ComponentClass, { propsCompareDepth = } return res; } - get messaging() { - return this.env.services.messaging.modelManager.messaging; - } /** * @returns {string} */ diff --git a/addons/mail/static/src/utils/test_utils.js b/addons/mail/static/src/utils/test_utils.js index 959ec5d22a1..50aba2eb99a 100644 --- a/addons/mail/static/src/utils/test_utils.js +++ b/addons/mail/static/src/utils/test_utils.js @@ -2,6 +2,7 @@ import BusService from 'bus.BusService'; +import { MessagingMenuContainer } from '@mail/components/messaging_menu_container/messaging_menu_container'; import { addMessagingToEnv, addTimeControlToEnv, @@ -373,8 +374,28 @@ function getAfterEvent({ messagingBus }) { } /** - * Creates and returns a new root Component with the given props and mounts it - * on target. + * Creates a new root Component, with the given props, and mounts it on target. + * Assumes that self.env is set to the correct value. + * Components created this way are automatically registered for clean up after + * the test, which will happen when `afterEach` is called. + * + * @param {Object} self the current QUnit instance + * @param {Object} Component the class of the component to create + * @param {Object} param2 + * @param {Object} [param2.props={}] forwarded to component constructor + * @param {DOM.Element} param2.target mount target for the component + */ +async function createRootComponent(self, Component, { props = {}, target }) { + Component.env = self.env; + const component = new Component(null, props); + delete Component.env; + self.components.push(component); + await afterNextRender(() => component.mount(target)); +} + +/** + * Creates and returns a new root messaging component, based on the given + * componentName and with the given props, and mounts it on target. * Assumes that self.env is set to the correct value. * Components created this way are automatically registered for clean up after * the test, which will happen when `afterEach` is called. @@ -384,16 +405,9 @@ function getAfterEvent({ messagingBus }) { * @param {Object} param2 * @param {Object} [param2.props={}] forwarded to component constructor * @param {DOM.Element} param2.target mount target for the component - * @returns {owl.Component} the new component instance */ async function createRootMessagingComponent(self, componentName, { props = {}, target }) { - const Component = getMessagingComponent(componentName); - Component.env = self.env; - const component = new Component(null, props); - delete Component.env; - self.components.push(component); - await afterNextRender(() => component.mount(target)); - return component; + await createRootComponent(self, getMessagingComponent(componentName), { props, target }); } function getCreateComposerComponent({ components, env, modelManager, widget }) { @@ -439,10 +453,7 @@ function getCreateMessageComponent({ components, env, modelManager, widget }) { function getCreateMessagingMenuComponent({ components, env, widget }) { return async function createMessagingMenuComponent() { - await createRootMessagingComponent({ components, env }, 'MessagingMenu', { - props: {}, - target: widget.el, - }); + await createRootComponent({ components, env }, MessagingMenuContainer, { target: widget.el }); }; }