[IMP] mail: introduce action panel

This commit uniformises the different discuss option
panel by introducing a dedicated component.

Before this change, every single discuss panel would
look different: font size/weight, text in lower/upper
case, margins...

Moreover, in the chat window, the close panel button
was absolute positionned which could overlap with the
content of the panel. Moreover, the cross icon looked
strange with the one of the chat window header.

This commit introduces the `ActionPanel` component in
order to solve those inconsistencies and replaces the
cross icon by an arrow left one.

part of task-3332872

closes odoo/odoo#128742

Signed-off-by: Alexandre Kühn (aku) <aku@odoo.com>
This commit is contained in:
tsm-odoo
2023-07-18 19:28:01 +02:00
parent 3fa87c8c47
commit d94fd7ef27
20 changed files with 128 additions and 72 deletions
@@ -59,6 +59,7 @@ export class ChatWindow extends Component {
this.contentRef = useRef("content");
this.threadActions = useThreadActions();
useChildSubEnv({
closeActionPanel: () => this.threadActions.activeAction?.close(),
inChatWindow: true,
messageHighlight: this.messageHighlight,
});
@@ -55,9 +55,10 @@
</t>
</div>
<div t-if="!props.chatWindow.folded or ui.isSmall" class="o-mail-ChatWindow-content bg-view d-flex flex-column h-100 overflow-auto position-relative" t-ref="content">
<button t-if="threadActions.activeAction" class="o-mail-ChatWindow-closePanel btn position-absolute top-0 end-0 btn opacity-75 opacity-100-hover" title="Close panel" t-on-click.stop="() => threadActions.activeAction.close()"><i class="fa fa-close"/></button>
<t t-if="thread" name="thread content">
<t t-if="threadActions.activeAction?.componentCondition" t-component="threadActions.activeAction.component" thread="thread" t-props="threadActions.activeAction.componentProps"/>
<div t-if="threadActions.activeAction?.componentCondition" class="h-100" t-attf-class="{{ threadActions.activeAction.panelOuterClass }}">
<t t-component="threadActions.activeAction.component" t-props="{ ...threadActions.activeAction.componentProps, thread }"/>
</div>
<t t-else="">
<Thread isInChatWindow="true" thread="thread" t-key="thread.localId" jumpPresent="state.jumpThreadPresent" messageEdition="messageEdition" messageToReplyTo="messageToReplyTo"/>
<Composer composer="thread.composer" autofocus="props.chatWindow.autofocus" mode="'compact'" messageEdition="messageEdition" messageToReplyTo="messageToReplyTo" onPostCallback.bind="() => this.state.jumpThreadPresent++" dropzoneRef="contentRef" type="composerType"/>
@@ -5,7 +5,8 @@
}
.o-mail-Discuss-inspector {
width: $o-mail-Discuss-inspector;
flex-basis: $o-mail-Discuss-inspector;
flex-shrink: 0;
}
.o-mail-Discuss-selfAvatar {
@@ -44,7 +45,7 @@
transform: translate(-50%, -50%);
opacity: 0;
}
&:hover {
background-color: rgba($black, 0.5);
i {
@@ -65,7 +65,9 @@
<Thread thread="thread" t-key="thread.localId" jumpPresent="state.jumpThreadPresent" messageEdition="messageEdition" messageToReplyTo="messageToReplyTo"/>
<Composer t-if="thread.type !== 'mailbox' or messageToReplyTo.thread === thread" t-key="thread.localId" composer="thread.composer" autofocus="true" messageEdition="messageEdition" messageToReplyTo="messageToReplyTo" onDiscardCallback="() => messageToReplyTo.cancel()" onPostCallback.bind="() => this.state.jumpThreadPresent++" dropzoneRef="contentRef" type="messageToReplyTo?.message ? (messageToReplyTo.message.isNote ? 'note' : 'message') : undefined"/>
</div>
<t t-if="threadActions.activeAction?.componentCondition" t-component="threadActions.activeAction.component" thread="thread" className="'o-mail-Discuss-inspector border-start'" t-props="threadActions.activeAction.componentProps"/>
<div t-if="threadActions.activeAction?.componentCondition" t-attf-class="{{ threadActions.activeAction.panelOuterClass }}" class="h-100 border-start o-mail-Discuss-inspector">
<t t-component="threadActions.activeAction.component" thread="thread" t-props="threadActions.activeAction.componentProps"/>
</div>
</div>
</div>
<div t-if="!thread and (!ui.isSmall or store.discuss.activeTab === 'mailbox')" class="d-flex flex-grow-1 align-items-center justify-content-center w-100 bg-view">
@@ -108,6 +108,7 @@ function transformAction(component, id, action) {
}
action.open?.(component, this);
},
panelOuterClass: action.panelOuterClass,
/** Determines whether this is a popover linked to this action. */
popover: null,
/** Determines the order of this action (smaller first). */
@@ -1,14 +1,16 @@
/* @odoo-module */
import { useRtc } from "@mail/discuss/call/common/rtc_hook";
import { ActionPanel } from "@mail/discuss/core/common/action_panel";
import { Component, onWillStart, useExternalListener, useState } from "@odoo/owl";
import { browser } from "@web/core/browser/browser";
import { _t } from "@web/core/l10n/translation";
import { browser } from "@web/core/browser/browser";
import { useService } from "@web/core/utils/hooks";
export class CallSettings extends Component {
static components = { ActionPanel };
static template = "discuss.CallSettings";
static props = ["thread", "className?"];
@@ -120,4 +122,8 @@ export class CallSettings extends Component {
onChangeEdgeBlurAmount(ev) {
this.userSettings.edgeBlurAmount = Number(ev.target.value);
}
get title() {
return _t("Voice Settings");
}
}
@@ -2,9 +2,8 @@
<templates xml:space="preserve">
<t t-name="discuss.CallSettings" owl="1">
<div class="o-discuss-CallSettings flex-grow-1 py-2 user-select-none bg-view" t-attf-class="{{ props.className }}" t-ref="root">
<ActionPanel title="title">
<div class="d-flex flex-column px-3 overflow-auto">
<div class="py-2 fw-bolder text-700 text-truncate text-uppercase">Voice Settings</div>
<div class="mb-3 d-flex align-items-center flex-wrap">
<label class="d-flex align-items-center flex-wrap mw-100 cursor-pointer" title="Input device" aria-label="Input device">
<span class="me-2 text-truncate">Input device</span>
@@ -101,7 +100,7 @@
<button class="btn btn-primary" t-att-disabled="rtc.state.logs.size === 0" t-on-click="onClickDownloadLogs">Download logs</button>
</div>
</div>
</div>
</ActionPanel>
</t>
</templates>
@@ -37,6 +37,7 @@ threadActionsRegistry
(!component.props.chatWindow || component.props.chatWindow.isOpen)
);
},
panelOuterClass: "o-discuss-CallSettings",
icon: "fa fa-fw fa-gear",
iconLarge: "fa fa-fw fa-lg fa-gear",
name: _t("Show Call Settings"),
@@ -0,0 +1,17 @@
/* @odoo-module */
import { Component } from "@odoo/owl";
/**
* @typedef {Object} Props
* @prop {string} title
* @prop {Object} [slots]
* @extends {Component<Props, Env>}
*/
export class ActionPanel extends Component {
static template = "mail.ActionPanel";
static props = {
title: String,
slots: { type: Object, optional: true },
};
}
@@ -0,0 +1,3 @@
.o-mail-ActionPanel-backButton {
color: $o-brand-odoo;
}
@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="mail.ActionPanel" owl="1">
<div class="overflow-auto d-flex flex-column flex-shrink-0 bg-view position-relative p-3 pt-0 h-100">
<div class="position-sticky top-0 py-3 d-flex align-items-baseline bg-view" style="z-index: 1;" >
<button t-if="env.closeActionPanel" class="o-mail-ActionPanel-backButton btn opacity-75 opacity-100-hover ps-0 py-0 fs-5" title="Close panel" t-on-click.stop="env.closeActionPanel">
<i class="oi oi-arrow-left"/>
</button>
<p class="fs-6 fw-bold text-uppercase m-0 text-700 flex-grow-1" t-esc="props.title"/>
</div>
<t t-slot="default"/>
</div>
</t>
</templates>
@@ -1,6 +1,7 @@
/* @odoo-module */
import { ImStatus } from "@mail/core/common/im_status";
import { ActionPanel } from "@mail/discuss/core/common/action_panel";
import { useMessaging, useStore } from "@mail/core/common/messaging_hook";
import { Component, onMounted, onWillStart, useRef, useState } from "@odoo/owl";
@@ -9,7 +10,7 @@ import { _t } from "@web/core/l10n/translation";
import { useService } from "@web/core/utils/hooks";
export class ChannelInvitation extends Component {
static components = { ImStatus };
static components = { ImStatus, ActionPanel };
static defaultProps = { hasSizeConstraints: false };
static props = ["hasSizeConstraints?", "thread", "close", "className?"];
static template = "discuss.ChannelInvitation";
@@ -140,4 +141,8 @@ export class ChannelInvitation extends Component {
}
return _t("Invite");
}
get title() {
return _t("Invite people");
}
}
@@ -2,54 +2,50 @@
<templates xml:space="preserve">
<t t-name="discuss.ChannelInvitation" owl="1">
<div class="o-discuss-ChannelInvitation bg-view d-flex flex-column" t-att-class="{ 'o-discuss-ChannelInvitation-has-size-constraints': props.hasSizeConstraints }" t-attf-class="{{ props.className }}">
<t t-if="store.user">
<h3 class="mx-3 mt-3 mb-2">Invite people</h3>
<div class="mx-3 my-2">
<ActionPanel title="title">
<div t-att-class="{ 'o-discuss-ChannelInvitation-has-size-constraints': props.hasSizeConstraints }">
<t t-if="store.user">
<input class="o-discuss-ChannelInvitation-search border form-control" t-ref="input" placeholder="Type the name of a person" t-on-input="onInput"/>
</div>
<div class="d-flex flex-column flex-grow-1 mx-0 py-2 overflow-auto">
<t t-foreach="state.selectablePartners" t-as="selectablePartner" t-key="selectablePartner.id">
<div class="o-discuss-ChannelInvitation-selectable o_object_fit_cover d-flex align-items-center px-3 py-1 btn" t-on-click="() => this.onClickSelectablePartner(selectablePartner)">
<div class="d-flex align-items-center p-2">
<div class="o-discuss-ChannelInvitation-avatar position-relative d-flex flex-shrink-0">
<img class="w-100 h-100 rounded o_object_fit_cover"
t-att-src="threadService.avatarUrl(selectablePartner, props.thread)"/>
<ImStatus persona="selectablePartner" className="'position-absolute top-100 start-100 translate-middle bg-view mt-n1 ms-n1'"/>
<div class="d-flex flex-column mx-0 py-2 overflow-auto">
<t t-foreach="state.selectablePartners" t-as="selectablePartner" t-key="selectablePartner.id">
<div class="o-discuss-ChannelInvitation-selectable o_object_fit_cover d-flex align-items-center px-3 py-1 btn" t-on-click="() => this.onClickSelectablePartner(selectablePartner)">
<div class="d-flex align-items-center p-2">
<div class="o-discuss-ChannelInvitation-avatar position-relative d-flex flex-shrink-0">
<img class="w-100 h-100 rounded o_object_fit_cover"
t-att-src="threadService.avatarUrl(selectablePartner, props.thread)"/>
<ImStatus persona="selectablePartner" className="'position-absolute top-100 start-100 translate-middle bg-view mt-n1 ms-n1'"/>
</div>
</div>
<span class="flex-grow-1 mx-2 text-truncate text-start fs-6" t-esc="selectablePartner.name"/>
<input class="form-check-input flex-shrink-0" type="checkbox" t-att-checked="state.selectedPartners.includes(selectablePartner) ? 'checked' : undefined"/>
</div>
<span class="flex-grow-1 mx-2 text-truncate text-start fs-6" t-esc="selectablePartner.name"/>
<input class="form-check-input flex-shrink-0" type="checkbox" t-att-checked="state.selectedPartners.includes(selectablePartner) ? 'checked' : undefined"/>
</div>
</t>
<div t-if="state.selectablePartners.length === 0" class="mx-3">No user found that is not already a member of this channel.</div>
<div t-if="state.searchResultCount > state.selectablePartners.length" class="mx-3">
Showing
<t t-esc="state.selectablePartners.length"/>
results out of
<t t-esc="state.searchResultCount"/>
. Narrow your search to see more choices.
</div>
</div>
<div t-if="state.selectedPartners.length > 0" class="mx-3 mt-3">
<h4>Selected users:</h4>
<div class="o-discuss-ChannelInvitation-selectedList d-flex flex-wrap gap-1 overflow-auto">
<t t-foreach="state.selectedPartners" t-as="selectedPartner" t-key="selectedPartner.id">
<button class="btn btn-secondary" t-on-click="() => this.onClickSelectedPartner(selectedPartner)">
<t t-esc="selectedPartner.name"/> <i class="fa fa-times"/>
</button>
</t>
<div t-if="state.selectablePartners.length === 0">No user found that is not already a member of this channel.</div>
<div t-if="state.searchResultCount > state.selectablePartners.length">
Showing
<t t-esc="state.selectablePartners.length"/>
results out of
<t t-esc="state.searchResultCount"/>
. Narrow your search to see more choices.
</div>
</div>
</div>
<div class="mx-3 mt-2 mb-3">
<button class="btn btn-primary w-100" t-att-disabled="this.state.selectedPartners.length === 0" t-att-title="invitationButtonText" t-on-click="onClickInvite">
<t t-esc="invitationButtonText"/>
</button>
</div>
</t>
<t t-if="props.thread.invitationLink">
<h4 class="mx-3 mt-3 mb-2">Invitation Link</h4>
<div class="mx-3 mt-2 mb-3">
<div t-if="state.selectedPartners.length > 0" class="mt-3">
<h4>Selected users:</h4>
<div class="o-discuss-ChannelInvitation-selectedList d-flex flex-wrap gap-1 overflow-auto">
<t t-foreach="state.selectedPartners" t-as="selectedPartner" t-key="selectedPartner.id">
<button class="btn btn-secondary" t-on-click="() => this.onClickSelectedPartner(selectedPartner)">
<t t-esc="selectedPartner.name"/> <i class="fa fa-times"/>
</button>
</t>
</div>
</div>
<div class="mt-2">
<button class="btn btn-primary w-100" t-att-disabled="this.state.selectedPartners.length === 0" t-att-title="invitationButtonText" t-on-click="onClickInvite">
<t t-esc="invitationButtonText"/>
</button>
</div>
</t>
<div t-if="props.thread.invitationLink" class="mt-3">
<div class="input-group">
<input class="border form-control" type="text" t-att-value="props.thread.invitationLink" readonly="" t-on-focus="onFocusInvitationLinkInput"/>
<button class="btn btn-primary" t-on-click="onClickCopy">
@@ -58,8 +54,8 @@
</div>
<div t-if="props.thread.accessRestrictedToGroupText" class="mt-2" t-esc="props.thread.accessRestrictedToGroupText"/>
</div>
</t>
</div>
</div>
</ActionPanel>
</t>
</templates>
@@ -1,14 +1,16 @@
/* @odoo-module */
import { ImStatus } from "@mail/core/common/im_status";
import { ActionPanel } from "@mail/discuss/core/common/action_panel";
import { useMessaging, useStore } from "@mail/core/common/messaging_hook";
import { Component, onWillUpdateProps, onWillStart, useState } from "@odoo/owl";
import { _t } from "@web/core/l10n/translation";
import { useService } from "@web/core/utils/hooks";
export class ChannelMemberList extends Component {
static components = { ImStatus };
static components = { ImStatus, ActionPanel };
static props = ["thread", "className?"];
static template = "discuss.ChannelMemberList";
@@ -44,4 +46,8 @@ export class ChannelMemberList extends Component {
}
this.threadService.openChat({ partnerId: member.persona.id });
}
get title() {
return _t("Member List");
}
}
@@ -2,16 +2,16 @@
<templates xml:space="preserve">
<t t-name="discuss.ChannelMemberList" owl="1">
<div class="o-discuss-ChannelMemberList d-flex flex-column overflow-auto bg-view" t-att-class="props.className">
<ActionPanel title="title">
<t t-if="props.thread.onlineMembers.length > 0">
<h6 class="m-2">
<h6 class="mx-3 text-700">
Online -
<t t-esc="props.thread.onlineMembers.length"/>
</h6>
<t t-foreach="props.thread.onlineMembers" t-as="member" t-key="member.id" t-call="discuss.channel_member"/>
</t>
<t t-if="props.thread.offlineMembers.length > 0">
<h6 class="m-2">
<h6 class="mx-3 text-700">
Offline -
<t t-esc="props.thread.offlineMembers.length"/>
</h6>
@@ -22,7 +22,7 @@
<div t-if="!props.thread.areAllMembersLoaded" class="mx-2 my-1">
<button class="btn btn-secondary" title="Load more" t-on-click.stop="() => threadService.fetchChannelMembers(props.thread)">Load more</button>
</div>
</div>
</ActionPanel>
</t>
<t t-name="discuss.channel_member" owl="1">
@@ -24,6 +24,7 @@ threadActionsRegistry
(!component.props.chatWindow || component.props.chatWindow.isOpen)
);
},
panelOuterClass: "o-discuss-ChannelInvitation",
icon: "fa fa-fw fa-user-plus",
iconLarge: "fa fa-fw fa-lg fa-user-plus",
name: _t("Add Users"),
@@ -40,6 +41,7 @@ threadActionsRegistry
if (!component.props.chatWindow) {
action.popover = usePopover(ChannelInvitation, {
onClose: () => action.close(),
popoverClass: action.panelOuterClass,
});
}
},
@@ -53,6 +55,7 @@ threadActionsRegistry
(!component.props.chatWindow || component.props.chatWindow.isOpen)
);
},
panelOuterClass: "o-discuss-ChannelMemberList",
icon: "fa fa-fw fa-users",
iconLarge: "fa fa-fw fa-lg fa-users",
name: _t("Show Member List"),
@@ -2,6 +2,7 @@
import { Message } from "@mail/core/common/message";
import { useMessagePinService } from "@mail/discuss/message_pin/common/message_pin_service";
import { ActionPanel } from "@mail/discuss/core/common/action_panel";
import { Component, onWillStart, onWillUpdateProps, useState, useSubEnv } from "@odoo/owl";
@@ -11,6 +12,7 @@ import { useService } from "@web/core/utils/hooks";
export class PinnedMessagesPanel extends Component {
static components = {
Message,
ActionPanel,
};
static props = ["thread", "className?"];
static template = "discuss.PinnedMessagesPanel";
@@ -71,4 +73,8 @@ export class PinnedMessagesPanel extends Component {
return _t("This conversation doesn't have any pinned messages.");
}
}
get title() {
return _t("Pinned Messages");
}
}
@@ -2,10 +2,6 @@
flex-basis: 425px;
}
.o-discuss-PinnedMessagesPanel-title {
z-index: 1;
}
.o-discuss-PinnedMessagesPanel-jump {
color: white;
}
@@ -1,14 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="discuss.PinnedMessagesPanel" owl="1">
<div class="o-discuss-PinnedMessagesPanel overflow-auto d-flex flex-column flex-shrink-0 bg-view position-relative" t-att-class="{
'flex-grow-1': env.inChatWindow,
}" t-attf-class="{{ props.className }}">
<p class="o-discuss-PinnedMessagesPanel-title position-sticky top-0 py-3 mb-0 fw-bolder text-center text-uppercase text-700">
Pinned messages
</p>
<ActionPanel title="title">
<div class="d-flex flex-column" t-att-class="{ 'justify-content-center flex-grow-1': !messagePinService.hasPinnedMessages(props.thread) }" t-ref="message-list">
<div class="card m-3 mt-0" t-foreach="messagePinService.getPinnedMessages(props.thread)" t-as="message" t-key="message.id">
<div class="card mb-3" t-foreach="messagePinService.getPinnedMessages(props.thread)" t-as="message" t-key="message.id">
<div class="card-body">
<div class="d-flex justify-content-end">
<button class="o-discuss-PinnedMessagesPanel-jump btn rounded bg-400 badge opacity-0 flex-shrink-0" t-att-class="{ 'opacity-100 py-1 px-2': ui.isSmall }" t-on-click="() => this.onClickJump(message)">Jump</button>
@@ -21,6 +16,6 @@
</div>
<p t-if="!messagePinService.hasPinnedMessages(props.thread)" t-esc="emptyMessage" class="text-center fst-italic text-500 fs-6"/>
</div>
</div>
</ActionPanel>
</t>
</templates>
@@ -15,6 +15,7 @@ threadActionsRegistry.add("pinned-messages", {
(!component.props.chatWindow || component.props.chatWindow.isOpen)
);
},
panelOuterClass: "o-discuss-PinnedMessagesPanel",
icon: "fa fa-fw fa-thumb-tack",
iconLarge: "fa fa-fw fa-lg fa-thumb-tack",
name: _t("Pinned Messages"),