[IMP] mail: add systray item containers

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) <aku@odoo.com>
This commit is contained in:
Sébastien Theys
2021-12-13 13:17:46 +00:00
parent f3e71d01ee
commit 71076550de
14 changed files with 141 additions and 95 deletions
@@ -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}`,
@@ -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;
}
@@ -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',
});
@@ -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);
@@ -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',
});
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="mail.MessagingMenuContainer" owl="1">
<div class="MessagingMenuContainer">
<t t-if="messaging and messaging.messagingMenu">
<MessagingMenu localId="messaging.messagingMenu.localId"/>
</t>
<t t-else="">
<i class="o_MessagingMenuContainer_spinner fa fa-circle-o-notch fa-spin mx-2" title="Please wait..."/>
</t>
</div>
</t>
</templates>
@@ -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
//--------------------------------------------------------------------------
@@ -7,7 +7,7 @@
<RtcInvitations/>
<t t-if="messaging.rtc.channel">
<t t-set="title">Open conference: <t t-esc="messaging.rtc.channel.displayName"/></t>
<a class="o_RtcActivityNotice_button" t-att-title="title" role="button" t-on-click="_onClick">
<a class="o_RtcActivityNotice_button dropdown-toggle o-no-caret o-dropdown--narrow" t-att-title="title" role="button" t-on-click="_onClick">
<div class="o_RtcActivityNotice_buttonContent">
<span class="o_RtcActivityNotice_buttonTitle" t-esc="messaging.rtc.channel.displayName"/>
<i class="o_RtcActivityNotice_outputIndicator fa" t-att-class="{
@@ -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 RtcActivityNoticeContainer 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(RtcActivityNoticeContainer, {
components: { RtcActivityNotice: getMessagingComponent('RtcActivityNotice') },
template: 'mail.RtcActivityNoticeContainer',
});
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="mail.RtcActivityNoticeContainer" owl="1">
<div class="RtcActivityNoticeContainer">
<t t-if="messaging">
<RtcActivityNotice/>
</t>
</div>
</t>
</templates>
@@ -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();
@@ -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 });
},
});
@@ -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}
*/
+25 -14
View File
@@ -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 });
};
}