[IMP] mail: introduce PopoverManager, and some code cleaning

1. Remove useless props category on component `ChannelMemberListCategory`
2. Make fields of model `DiscussSidebarCategory` more declarative,
   i.e. by defining values from compute of field rather than imperative
   code somewhere in the code of discuss.
3. Introduce a new model `PopoverManager`, which eases use of `PopoverView`s
   as they are automatically added in the DOM in a similar way than `Dialog`s
   with `DialogManager` or `ChatWindow`s with `ChatWindowManager`.
   Doing so also make these popover view no longer deep child in component tree:
   this fixes an issue in which popover views could be cut due to DOM tree.

Task-2871688

closes odoo/odoo#92896

Signed-off-by: Alexandre Kühn (aku) <aku@odoo.com>
This commit is contained in:
Alexandre Kühn
2022-06-03 18:34:53 +02:00
parent fc115ebdf7
commit 315a0d6b5b
19 changed files with 265 additions and 36 deletions
@@ -1,6 +1,6 @@
/** @odoo-module **/
import { addFields, patchIdentifyingFields } from '@mail/model/model_core';
import { addFields, patchIdentifyingFields, patchRecordMethods } from '@mail/model/model_core';
import { one } from '@mail/model/model_field';
// ensure that the model definition is loaded before the patch
import '@mail/models/discuss_sidebar_category';
@@ -15,3 +15,33 @@ addFields('DiscussSidebarCategory', {
patchIdentifyingFields('DiscussSidebarCategory', identifyingFields => {
identifyingFields[0].push('discussAsLivechat');
});
patchRecordMethods('DiscussSidebarCategory', {
/**
* @override
*/
_computeName() {
if (this.discussAsLivechat) {
return this.env._t("Livechat");
}
return this._super();
},
/**
* @override
*/
_computeSortComputeMethod() {
if (this.discussAsLivechat) {
return 'last_action';
}
return this._super();
},
/**
* @override
*/
_computeSupportedChannelTypes() {
if (this.discussAsLivechat) {
return ['livechat'];
}
return this._super();
},
});
@@ -15,10 +15,7 @@ patchRecordMethods('MessagingInitializer', {
this.messaging.discuss.update({
categoryLivechat: insertAndReplace({
isServerOpen: is_discuss_sidebar_category_livechat_open,
name: this.env._t("Livechat"),
serverStateKey: 'is_discuss_sidebar_category_livechat_open',
sortComputeMethod: 'last_action',
supportedChannelTypes: ['livechat'],
}),
});
this._super(...arguments);
@@ -122,7 +122,6 @@
<button class="o_Activity_toolButton o_Activity_markDoneButton btn btn-link btn-primary pt-0 pl-0" t-att-title="activityView.markDoneText" t-ref="markDoneButton" t-on-click="activityView.onClickMarkDoneButton">
<i class="fa fa-check"/> Mark Done
</button>
<PopoverView t-if="activityView.markDonePopoverView" record="activityView.markDonePopoverView"/>
<t t-if="activityView.fileUploader">
<button class="o_Activity_toolButton o_Activity_uploadButton btn btn-link btn-primary pt-0 pl-0" t-on-click="activityView.onClickUploadDocument">
<i class="fa fa-upload"/> Upload Document
@@ -5,10 +5,10 @@
<t t-if="channelMemberListView">
<div class="o_ChannelMemberList d-flex flex-column overflow-auto bg-light" t-attf-class="{{ className }}" t-ref="root">
<t t-if="channelMemberListView.onlineCategoryView">
<ChannelMemberListCategory record="channelMemberListView.onlineCategoryView" category="'online'" channelMemberListView="channelMemberListView"/>
<ChannelMemberListCategory record="channelMemberListView.onlineCategoryView" channelMemberListView="channelMemberListView"/>
</t>
<t t-if="channelMemberListView.offlineCategoryView">
<ChannelMemberListCategory record="channelMemberListView.offlineCategoryView" category="'offline'" channelMemberListView="channelMemberListView"/>
<ChannelMemberListCategory record="channelMemberListView.offlineCategoryView" channelMemberListView="channelMemberListView"/>
</t>
<t t-if="channelMemberListView.channel.unknownMemberCount === 1">
<span class="mx-2 mt-2">And 1 other member.</span>
@@ -29,10 +29,10 @@ export class ChannelMemberListCategory extends Component {
if (!this.channelMemberListView.exists()) {
return [];
}
if (this.props.category === 'online') {
if (this.record.channelMemberListViewOwnerAsOnline) {
return this.channelMemberListView.channel.orderedOnlineMembers;
}
if (this.props.category === 'offline') {
if (this.record.channelMemberListViewOwnerAsOffline) {
return this.channelMemberListView.channel.orderedOfflineMembers;
}
return [];
@@ -43,10 +43,10 @@ export class ChannelMemberListCategory extends Component {
*/
get title() {
let categoryText = "";
if (this.props.category === 'online') {
if (this.record.channelMemberListViewOwnerAsOnline) {
categoryText = this.env._t("Online");
}
if (this.props.category === 'offline') {
if (this.record.channelMemberListViewOwnerAsOffline) {
categoryText = this.env._t("Offline");
}
return sprintf(
@@ -62,7 +62,6 @@ export class ChannelMemberListCategory extends Component {
Object.assign(ChannelMemberListCategory, {
props: {
category: String,
channelMemberListView: Object,
record: Object,
},
@@ -107,7 +107,6 @@
>
<i class="fa fa-smile-o"/>
</button>
<PopoverView t-if="composerView.emojisPopoverView" record="composerView.emojisPopoverView"/>
<button class="o_Composer_button o_Composer_buttonAttachment o_Composer_toolButton btn btn-light fa fa-paperclip mx-1 border-0 rounded-pill bg-white" t-att-class="{ 'o-isDeviceSmall': messaging.device.isSmall }" title="Add attachment" type="button" t-on-click="composerView.onClickAddAttachment"/>
</div>
<t t-if="composerView.isExpandable">
@@ -4,7 +4,6 @@
<t t-if="messageActionList">
<div class="o_MessageActionList d-flex border rounded-sm bg-white" t-att-class="{ 'o-isDeviceSmall': messaging.device.isSmall }" t-attf-class="{{ className }}" t-on-click="messageActionList.onClick" t-ref="root">
<i t-if="messageActionList.message.hasReactionIcon" class="o_MessageActionList_action o_MessageActionList_actionReaction fa fa-lg fa-smile-o o_cursor_pointer" t-attf-class="{{ messaging.device.isSmall ? 'pl-3 py-3 pr-2' : 'pl-2 py-2 pr-1' }}" t-att-title="messageActionList.addReactionText" t-on-click="messageActionList.onClickActionReaction" t-ref="actionReaction"/>
<PopoverView t-if="messageActionList.reactionPopoverView" record="messageActionList.reactionPopoverView"/>
<span t-if="messageActionList.message.canStarBeToggled" class="o_MessageActionList_action o_MessageActionList_actionStar o_cursor_pointer" t-attf-class="{{ messaging.device.isSmall ? 'px-2 py-3' : 'px-1 py-2' }}" t-att-class="{
'o_MessageActionList_actionStar_active': messageActionList.message.isStarred,
'fa fa-lg fa-star': messageActionList.message.isStarred,
@@ -0,0 +1,19 @@
/** @odoo-module **/
import { registerMessagingComponent } from '@mail/utils/messaging_component';
const { Component } = owl;
export class PopoverManager extends Component {
get popoverManager() {
return this.props.record;
}
}
Object.assign(PopoverManager, {
props: { record: Object },
template: 'mail.PopoverManager',
});
registerMessagingComponent(PopoverManager);
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="mail.PopoverManager" owl="1">
<div class="o_PopoverManager" t-attf-class="{{ className }}" t-ref="root">
<t t-foreach="popoverManager.popoverViews" t-as="popoverView" t-key="popoverView.localId">
<PopoverView
className="'o_PopoverManager_popover'"
record="popoverView"
/>
</t>
</div>
</t>
</templates>
@@ -0,0 +1,28 @@
/** @odoo-module **/
import { useModels } from '@mail/component_hooks/use_models';
// ensure components are registered beforehand.
import '@mail/components/popover_manager/popover_manager';
import { getMessagingComponent } from "@mail/utils/messaging_component";
const { Component } = owl;
export class PopoverManagerContainer extends Component {
/**
* @override
*/
setup() {
useModels();
super.setup();
}
get messaging() {
return this.env.services.messaging.modelManager.messaging;
}
}
Object.assign(PopoverManagerContainer, {
components: { PopoverManager: getMessagingComponent('PopoverManager') },
template: 'mail.PopoverManagerContainer',
});
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="mail.PopoverManagerContainer" owl="1">
<PopoverManager t-if="messaging and messaging.popoverManager" record="messaging.popoverManager"/>
</t>
</templates>
@@ -61,9 +61,6 @@
<button class="o_ThreadViewTopbar_inviteButton o_ThreadViewTopbar_button btn px-2 border-none rounded shadow-none" t-attf-class="{{ threadViewTopbar.invitePopoverView ? '' : '' }}" title="Add users" t-on-click="threadViewTopbar.onClickInviteButton" t-ref="inviteButton">
<i class="fa fa-lg fa-user-plus" t-attf-class="{{ threadViewTopbar.invitePopoverView ? 'text-500' : 'text-700' }}"/>
</button>
<t t-if="threadViewTopbar.invitePopoverView">
<PopoverView className="'o_ThreadViewTopbar_invitePopoverView'" record="threadViewTopbar.invitePopoverView"/>
</t>
</t>
<!-- FIXME: guests should be able to see members but there currently is no route for it, so hide it for now -->
<t t-if="!messaging.isCurrentUserGuest and threadViewTopbar.thread and threadViewTopbar.thread.hasMemberListFeature and threadViewTopbar.threadView.hasMemberList and !threadViewTopbar.threadView.isMemberListOpened">
+2
View File
@@ -3,6 +3,7 @@
import { ChatWindowManagerContainer } from '@mail/components/chat_window_manager_container/chat_window_manager_container';
import { DialogManagerContainer } from '@mail/components/dialog_manager_container/dialog_manager_container';
import { DiscussContainer } from '@mail/components/discuss_container/discuss_container';
import { PopoverManagerContainer } from '@mail/components/popover_manager_container/popover_manager_container';
import { messagingService } from '@mail/services/messaging_service';
import { systrayService } from '@mail/services/systray_service';
import { makeMessagingToLegacyEnv } from '@mail/utils/make_messaging_to_legacy_env';
@@ -25,3 +26,4 @@ registry.category('actions').add('mail.action_discuss', DiscussContainer);
registry.category('main_components').add('DialogManagerContainer', { Component: DialogManagerContainer });
registry.category('main_components').add('ChatWindowManagerContainer', { Component: ChatWindowManagerContainer });
registry.category('main_components').add('PopoverManagerContainer', { Component: PopoverManagerContainer });
@@ -73,6 +73,32 @@ registerModel({
}
return clear();
},
/**
* @private
* @returns {string|FieldCommand}
*/
_computeAutocompleteMethod() {
if (this.discussAsChannel) {
return 'channel';
}
if (this.discussAsChat) {
return 'chat';
}
return clear();
},
/**
* @private
* @returns {string|FieldCommand}
*/
_computeCommandAddTitleText() {
if (this.discussAsChannel) {
return this.env._t("Add or join a channel");
}
if (this.discussAsChat) {
return this.env._t("Start a conversation");
}
return clear();
},
/**
* @private
* @returns {FieldCommand}
@@ -89,6 +115,29 @@ registerModel({
}
return replace(categoryItems);
},
/**
* @private
* @returns {boolean|FieldCommand}
*/
_computeHasAddCommand() {
if (this.discussAsChannel) {
return true;
}
if (this.discussAsChat) {
return true;
}
return clear();
},
/**
* @private
* @returns {boolean|FieldCommand}
*/
_computeHasViewCommand() {
if (this.discussAsChannel) {
return true;
}
return clear();
},
/**
* @private
* @returns {boolean}
@@ -96,6 +145,58 @@ registerModel({
_computeIsOpen() {
return this.isPendingOpen !== undefined ? this.isPendingOpen : this.isServerOpen;
},
/**
* @private
* @returns {string|FieldCommand}
*/
_computeName() {
if (this.discussAsChannel) {
return this.env._t("Channels");
}
if (this.discussAsChat) {
return this.env._t("Direct Messages");
}
return clear();
},
/**
* @private
* @returns {string|FieldCommand}
*/
_computeNewItemPlaceholderText() {
if (this.discussAsChannel) {
return this.env._t("Find or create a channel...");
}
if (this.discussAsChat) {
return this.env._t("Find or start a conversation...");
}
return clear();
},
/**
* @private
* @returns {string|FieldCommand}
*/
_computeSortComputeMethod() {
if (this.discussAsChannel) {
return 'name';
}
if (this.discussAsChat) {
return 'last_action';
}
return clear();
},
/**
* @private
* @returns {string[]|FieldCommand}
*/
_computeSupportedChannelTypes() {
if (this.discussAsChannel) {
return ['channel'];
}
if (this.discussAsChat) {
return ['chat', 'group'];
}
return clear();
},
/**
* @private
* @returns {Array[]}
@@ -213,11 +314,17 @@ registerModel({
* Determines how the autocomplete of this category should behave.
* Must be one of: 'channel', 'chat'.
*/
autocompleteMethod: attr(),
autocompleteMethod: attr({
compute: '_computeAutocompleteMethod',
default: '',
}),
/**
* The title text in UI for command `add`
*/
commandAddTitleText: attr(),
commandAddTitleText: attr({
compute: '_computeCommandAddTitleText',
default: '',
}),
/**
* Determines the discuss sidebar category items that are displayed by
* this discuss sidebar category.
@@ -255,17 +362,22 @@ registerModel({
/**
* Display name of the category.
*/
name: attr(),
name: attr({
compute: '_computeName',
default: '',
}),
/**
* Boolean that determines whether this category has a 'add' command.
*/
hasAddCommand: attr({
compute: '_computeHasAddCommand',
default: false,
}),
/**
* Boolean that determines whether this category has a 'view' command.
*/
hasViewCommand: attr({
compute: '_computeHasViewCommand',
default: false,
}),
/**
@@ -295,7 +407,9 @@ registerModel({
/**
* The placeholder text used when a new item is being added in UI.
*/
newItemPlaceholderText: attr(),
newItemPlaceholderText: attr({
compute: '_computeNewItemPlaceholderText',
}),
/**
* The key used in the server side for the category state
*/
@@ -308,12 +422,14 @@ registerModel({
* Must be one of: 'name', 'last_action'.
*/
sortComputeMethod: attr({
compute: '_computeSortComputeMethod',
required: true,
}),
/**
* Channel type which is supported by the category.
*/
supportedChannelTypes: attr({
compute: '_computeSupportedChannelTypes',
required: true,
readonly: true,
}),
@@ -461,6 +461,11 @@ registerModel({
default: 0,
}),
partnerRoot: one('Partner'),
popoverManager: one('PopoverManager', {
default: insertAndReplace(),
isCausal: true,
readonly: true,
}),
/**
* Determines which partners should be considered the public partners,
* which are special partners notably used in livechat.
@@ -247,27 +247,12 @@ registerModel({
});
this.messaging.discuss.update({
categoryChannel: insertAndReplace({
autocompleteMethod: 'channel',
commandAddTitleText: this.env._t("Add or join a channel"),
hasAddCommand: true,
hasViewCommand: true,
isServerOpen: is_discuss_sidebar_category_channel_open,
name: this.env._t("Channels"),
newItemPlaceholderText: this.env._t("Find or create a channel..."),
serverStateKey: 'is_discuss_sidebar_category_channel_open',
sortComputeMethod: 'name',
supportedChannelTypes: ['channel'],
}),
categoryChat: insertAndReplace({
autocompleteMethod: 'chat',
commandAddTitleText: this.env._t("Start a conversation"),
hasAddCommand: true,
isServerOpen: is_discuss_sidebar_category_chat_open,
name: this.env._t("Direct Messages"),
newItemPlaceholderText: this.env._t("Find or start a conversation..."),
serverStateKey: 'is_discuss_sidebar_category_chat_open',
sortComputeMethod: 'last_action',
supportedChannelTypes: ['chat', 'group'],
}),
});
},
@@ -0,0 +1,16 @@
/** @odoo-module **/
import { registerModel } from '@mail/model/model_core';
import { many } from '@mail/model/model_field';
registerModel({
name: 'PopoverManager',
identifyingFields: ['messaging'],
fields: {
// FIXME: dependent on implementation that uses insert order in relations!!
popoverViews: many('PopoverView', {
inverse: 'manager',
isCausal: true,
}),
},
});
@@ -123,6 +123,16 @@ registerModel({
}
return clear();
},
/**
* @private
* @returns {FieldCommand}
*/
_computeManager() {
if (this.messaging.popoverManager) {
return replace(this.messaging.popoverManager);
}
return clear();
},
/**
* @private
* @returns {string}
@@ -231,6 +241,11 @@ registerModel({
isCausal: true,
readonly: true,
}),
manager: one('PopoverManager', {
compute: '_computeManager',
inverse: 'popoverViews',
readonly: true,
}),
/**
* If set, this popover view is owned by a message action list.
*/
@@ -7,6 +7,7 @@ import BusService from 'bus.BusService';
import { ChatWindowManagerContainer } from '@mail/components/chat_window_manager_container/chat_window_manager_container';
import { DialogManagerContainer } from '@mail/components/dialog_manager_container/dialog_manager_container';
import { DiscussContainer } from '@mail/components/discuss_container/discuss_container';
import { PopoverManagerContainer } from '@mail/components/popover_manager_container/popover_manager_container';
import { messagingService } from '@mail/services/messaging_service';
import { makeMessagingToLegacyEnv } from '@mail/utils/make_messaging_to_legacy_env';
@@ -36,6 +37,7 @@ const SERVICES_PARAMETER_NAMES = new Set([
mainComponentRegistry.add('ChatWindowManagerContainer', { Component: ChatWindowManagerContainer });
mainComponentRegistry.add('DialogManagerContainer', { Component: DialogManagerContainer });
registry.category('actions').add('mail.action_discuss', DiscussContainer);
mainComponentRegistry.add('PopoverManagerContainer', { Component: PopoverManagerContainer });
}
/**