[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:
@@ -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);
|
||||
|
||||
|
||||
+26
@@ -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',
|
||||
});
|
||||
+14
@@ -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="{
|
||||
|
||||
+26
@@ -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',
|
||||
});
|
||||
+11
@@ -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}
|
||||
*/
|
||||
|
||||
@@ -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 });
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user