Initial commit: Core packages

This commit is contained in:
Ernad Husremovic 2025-08-29 15:20:45 +02:00
commit 12c29a983b
9512 changed files with 8379910 additions and 0 deletions

View file

@ -0,0 +1,26 @@
/** @odoo-module **/
import { useRefToModel } from '@mail/component_hooks/use_ref_to_model';
import { registerMessagingComponent } from '@mail/utils/messaging_component';
const { Component } = owl;
export class ActivityButtonView extends Component {
setup() {
super.setup();
useRefToModel({ fieldName: 'buttonRef', refName: 'button' });
}
get activityButtonView() {
return this.props.record;
}
}
Object.assign(ActivityButtonView, {
props: { record: Object },
template: 'mail.ActivityButtonView',
});
registerMessagingComponent(ActivityButtonView);

View file

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="mail.ActivityButtonView" owl="1">
<a class="o_ActivityButtonView" role="button" t-on-click.prevent="activityButtonView.onClick" t-ref="button">
<i class="o_ActivityButtonView_icon fa fa-fw fa-lg" t-att-class="activityButtonView.buttonClass" role="img"/>
</a>
</t>
</templates>

View file

@ -0,0 +1,28 @@
/** @odoo-module **/
import { registry } from "@web/core/registry";
import { standardFieldProps } from "@web/views/fields/standard_field_props";
const { Component } = owl;
class ActivityException extends Component {
get textClass() {
if (this.props.value) {
return 'text-' + this.props.value + ' fa ' + this.props.record.data.activity_exception_icon;
}
return undefined;
}
}
Object.assign(ActivityException, {
props: standardFieldProps,
template: 'mail.ActivityException',
fieldDependencies: {
activity_exception_icon: { type: 'char' },
},
noLabel: true,
});
registry.category('fields').add('activity_exception', ActivityException);

View file

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="mail.ActivityException" owl="1">
<div
t-if="props.value"
class="o_ActivityException float-end mt-1"
t-att-class="textClass"
title="This record has an exception activity."
></div>
</t>
</templates>

View file

@ -0,0 +1,20 @@
/** @odoo-module **/
import { registerMessagingComponent } from '@mail/utils/messaging_component';
const { Component } = owl;
export class ActivityListView extends Component {
get activityListView() {
return this.props.record;
}
}
Object.assign(ActivityListView, {
props: { record: Object },
template: 'mail.ActivityListView',
});
registerMessagingComponent(ActivityListView);

View file

@ -0,0 +1,8 @@
.o_ActivityListView {
width: #{"min(95vw, 300px)"};
max-height: #{"min(95vh, 350px)"};
}
.o_ActivityListView_activityList {
overflow-y: auto;
}

View file

@ -0,0 +1,45 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="mail.ActivityListView" owl="1">
<div class="o_ActivityListView d-flex flex-column" t-ref="root">
<div class="o_ActivityListView_activityList d-flex flex-column flex-grow-1">
<t t-if="activityListView.activityListViewItems.length === 0">
<span class="p-3 text-center fst-italic text-500 border-bottom">Schedule activities to help you get things done.</span>
</t>
<t t-if="activityListView.overdueActivityListViewItems.length > 0">
<div class="d-flex bg-100 py-2 border-bottom">
<span class="text-danger fw-bold mx-3">Overdue</span>
<span class="flex-grow-1"/>
<span class="badge rounded-pill text-bg-danger mx-3 align-self-center" t-esc="activityListView.overdueActivityListViewItems.length"/>
</div>
<t t-foreach="activityListView.overdueActivityListViewItems" t-as="activityListViewItem" t-key="activityListViewItem">
<ActivityListViewItem record="activityListViewItem"/>
</t>
</t>
<t t-if="activityListView.todayActivityListViewItems.length > 0">
<div class="d-flex bg-100 py-2 border-bottom">
<span class="text-warning fw-bold mx-3">Today</span>
<span class="flex-grow-1"/>
<span class="badge rounded-pill text-bg-warning mx-3 align-self-center" t-esc="activityListView.todayActivityListViewItems.length"/>
</div>
<t t-foreach="activityListView.todayActivityListViewItems" t-as="activityListViewItem" t-key="activityListViewItem">
<ActivityListViewItem record="activityListViewItem"/>
</t>
</t>
<t t-if="activityListView.plannedActivityListViewItems.length > 0">
<div class="d-flex bg-100 py-2 border-bottom">
<span class="text-success fw-bold mx-3">Planned</span>
<span class="flex-grow-1"/>
<span class="badge rounded-pill text-bg-success mx-3 align-self-center" t-esc="activityListView.plannedActivityListViewItems.length"/>
</div>
<t t-foreach="activityListView.plannedActivityListViewItems" t-as="activityListViewItem" t-key="activityListViewItem">
<ActivityListViewItem record="activityListViewItem"/>
</t>
</t>
</div>
<button class="o_ActivityListView_addActivityButton btn btn-secondary p-3 text-center" t-on-click="activityListView.onClickAddActivityButton">
<i class="fa fa-plus fa-fw"></i><strong>Schedule an activity</strong>
</button>
</div>
</t>
</templates>

View file

@ -0,0 +1,20 @@
/** @odoo-module **/
import { registerMessagingComponent } from '@mail/utils/messaging_component';
const { Component } = owl;
export class ActivityListViewItem extends Component {
get activityListViewItem() {
return this.props.record;
}
}
Object.assign(ActivityListViewItem, {
props: { record: Object },
template: 'mail.ActivityListViewItem',
});
registerMessagingComponent(ActivityListViewItem);

View file

@ -0,0 +1,16 @@
.o_ActivityListViewItem_actionLink {
@include o-hover-text-color($text-muted, map-get($theme-colors, 'success'));
@include o-hover-opacity(0.5, 1);
}
.o_ActivityListViewItem_editButton {
opacity: 0.5;
}
.o_ActivityListViewItem:hover .o_ActivityListViewItem_editButton {
opacity: 1;
}
.o_ActivityListViewItem_container {
min-width: 0;
}

View file

@ -0,0 +1,38 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="mail.ActivityListViewItem" owl="1">
<div class="o_ActivityListViewItem d-flex flex-column border-bottom py-2">
<div class="o_ActivityListViewItem_container d-flex align-items-baseline ms-3 me-1">
<i t-if="activityListViewItem.activity.icon" class="fa small me-2" t-attf-class="{{ activityListViewItem.activity.icon }}" role="img"/>
<t t-if="activityListViewItem.activity.summary">
<b class="text-900 me-2 text-truncate flex-grow-1 flex-basis-0" t-esc="activityListViewItem.activity.summary"/>
</t>
<t t-if="!activityListViewItem.activity.summary and activityListViewItem.activity.type">
<b class="text-900 me-2 text-truncate flex-grow-1" t-esc="activityListViewItem.activity.type.displayName"/>
</t>
<button t-if="activityListViewItem.hasEditButton" class="o_ActivityListViewItem_editButton btn btn-sm btn-link" t-on-click="activityListViewItem.onClickEditActivityButton">
<i class="fa fa-pencil"/>
</button>
<t t-if="activityListViewItem.activity.canWrite">
<button t-if="activityListViewItem.fileUploader" class="o_ActivityListViewItem_actionLink btn btn-link shadow-none fs-4 fa fa-upload" title="Upload file" aria-label="Upload File" t-on-click="activityListViewItem.onClickUploadDocument"/>
<button t-if="activityListViewItem.hasMarkDoneButton" class="o_ActivityListViewItem_actionLink o_ActivityListViewItem_markAsDone btn btn-link shadow-none fs-4 fa fa-check-circle" title="Mark as done" aria-label="Mark as done" t-on-click="activityListViewItem.onClickMarkAsDone" t-ref="markDoneButton"/>
</t>
</div>
<div t-if="activityListViewItem.activity.state !== 'today'" class="d-flex align-items-baseline flex-wrap mx-3">
<i class="fa fa-clock-o me-2 text-muted" role="img" aria-label="Deadline" title="Deadline"/>
<t t-if="!activityListViewItem.activity.isCurrentPartnerAssignee and activityListViewItem.activity.assignee">
<small class="text-truncate" t-esc="activityListViewItem.activity.assignee.displayName"/>
<small class="mx-1">-</small>
</t>
<small t-att-title="activityListViewItem.activity.dateDeadline" t-esc="activityListViewItem.delayLabel"/>
</div>
<ActivityMarkDonePopoverContent t-if="activityListViewItem.markDoneView" record="activityListViewItem.markDoneView"/>
<div t-if="activityListViewItem.mailTemplateViews.length > 0" class="mx-3 mt-2">
<MailTemplate
t-foreach="activityListViewItem.mailTemplateViews" t-as="mailTemplateView" t-key="mailTemplateView"
record="mailTemplateView"
/>
</div>
</div>
</t>
</templates>

View file

@ -0,0 +1,20 @@
/** @odoo-module **/
import { registerMessagingComponent } from '@mail/utils/messaging_component';
const { Component } = owl;
export class KanbanFieldActivityView extends Component {
get kanbanFieldActivityView() {
return this.props.record;
}
}
Object.assign(KanbanFieldActivityView, {
props: { record: Object },
template: 'mail.KanbanFieldActivityView',
});
registerMessagingComponent(KanbanFieldActivityView);

View file

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="mail.KanbanFieldActivityView" owl="1">
<ActivityButtonView record="kanbanFieldActivityView.activityButtonView"/>
</t>
</templates>

View file

@ -0,0 +1,98 @@
/** @odoo-module **/
// ensure components are registered beforehand.
import '@mail/backend_components/kanban_field_activity_view/kanban_field_activity_view';
import { getMessagingComponent } from '@mail/utils/messaging_component';
import { registry } from "@web/core/registry";
import { standardFieldProps } from "@web/views/fields/standard_field_props";
const { Component, onWillDestroy, onWillUpdateProps } = owl;
const getNextId = (function () {
let tmpId = 0;
return () => {
tmpId += 1;
return tmpId;
};
})();
/**
* Container for messaging component KanbanFieldActivityView ensuring messaging
* records are ready before rendering KanbanFieldActivityView component.
*/
export class KanbanFieldActivityViewContainer extends Component {
/**
* @override
*/
setup() {
super.setup();
this.kanbanFieldActivityView = undefined;
this.kanbanFieldActivityViewId = getNextId();
this._insertFromProps(this.props);
onWillUpdateProps(nextProps => this._insertFromProps(nextProps));
onWillDestroy(() => this._deleteRecord());
}
/**
* @private
*/
_deleteRecord() {
if (this.kanbanFieldActivityView) {
if (this.kanbanFieldActivityView.exists()) {
this.kanbanFieldActivityView.delete();
}
this.kanbanFieldActivityView = undefined;
}
}
/**
* @private
*/
async _insertFromProps(props) {
const messaging = await this.env.services.messaging.get();
if (owl.status(this) === "destroyed") {
this._deleteRecord();
return;
}
const kanbanFieldActivityView = messaging.models['KanbanFieldActivityView'].insert({
id: this.kanbanFieldActivityViewId,
thread: {
activities: props.value.records.map(activityData => {
return {
id: activityData.resId,
};
}),
hasActivities: true,
id: props.record.resId,
model: props.record.resModel,
},
webRecord: props.record,
});
if (kanbanFieldActivityView !== this.kanbanFieldActivityView) {
this._deleteRecord();
this.kanbanFieldActivityView = kanbanFieldActivityView;
}
this.render();
}
}
Object.assign(KanbanFieldActivityViewContainer, {
components: { KanbanFieldActivityView: getMessagingComponent('KanbanFieldActivityView') },
fieldDependencies: {
activity_exception_decoration: { type: 'selection' },
activity_exception_icon: { type: 'char' },
activity_state: { type: 'selection' },
activity_summary: { type: 'char' },
activity_type_icon: { type: 'char' },
activity_type_id: { type: 'many2one', relation: 'mail.activity.type' },
},
props: {
...standardFieldProps,
},
template: 'mail.KanbanFieldActivityViewContainer',
});
registry.category('fields').add('kanban_activity', KanbanFieldActivityViewContainer);

View file

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="mail.KanbanFieldActivityViewContainer" owl="1">
<KanbanFieldActivityView t-if="kanbanFieldActivityView" record="kanbanFieldActivityView"/>
</t>
</templates>

View file

@ -0,0 +1,20 @@
/** @odoo-module **/
import { registerMessagingComponent } from '@mail/utils/messaging_component';
const { Component } = owl;
export class ListFieldActivityView extends Component {
get listFieldActivityView() {
return this.props.record;
}
}
Object.assign(ListFieldActivityView, {
props: { record: Object },
template: 'mail.ListFieldActivityView',
});
registerMessagingComponent(ListFieldActivityView);

View file

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="mail.ListFieldActivityView" owl="1">
<ActivityButtonView record="listFieldActivityView.activityButtonView"/>
<span class="o_ListFieldActivityView_summary" t-out="listFieldActivityView.summaryText"/>
</t>
</templates>

View file

@ -0,0 +1,98 @@
/** @odoo-module **/
// ensure components are registered beforehand.
import '@mail/backend_components/list_field_activity_view/list_field_activity_view';
import { getMessagingComponent } from '@mail/utils/messaging_component';
import { registry } from "@web/core/registry";
import { standardFieldProps } from "@web/views/fields/standard_field_props";
const { Component, onWillDestroy, onWillUpdateProps } = owl;
const getNextId = (function () {
let tmpId = 0;
return () => {
tmpId += 1;
return tmpId;
};
})();
/**
* Container for messaging component ListFieldActivityView ensuring messaging
* records are ready before rendering ListFieldActivityView component.
*/
export class ListFieldActivityViewContainer extends Component {
/**
* @override
*/
setup() {
super.setup();
this.listFieldActivityView = undefined;
this.listFieldActivityViewId = getNextId();
this._insertFromProps(this.props);
onWillUpdateProps(nextProps => this._insertFromProps(nextProps));
onWillDestroy(() => this._deleteRecord());
}
/**
* @private
*/
_deleteRecord() {
if (this.listFieldActivityView) {
if (this.listFieldActivityView.exists()) {
this.listFieldActivityView.delete();
}
this.listFieldActivityView = undefined;
}
}
/**
* @private
*/
async _insertFromProps(props) {
const messaging = await this.env.services.messaging.get();
if (owl.status(this) === "destroyed") {
this._deleteRecord();
return;
}
const listFieldActivityView = messaging.models['ListFieldActivityView'].insert({
id: this.listFieldActivityViewId,
thread: {
activities: props.value.records.map(activityData => {
return {
id: activityData.resId,
};
}),
hasActivities: true,
id: props.record.resId,
model: props.record.resModel,
},
webRecord: props.record,
});
if (listFieldActivityView !== this.listFieldActivityView) {
this._deleteRecord();
this.listFieldActivityView = listFieldActivityView;
}
this.render();
}
}
Object.assign(ListFieldActivityViewContainer, {
components: { ListFieldActivityView: getMessagingComponent('ListFieldActivityView') },
fieldDependencies: {
activity_exception_decoration: { type: 'selection' },
activity_exception_icon: { type: 'char' },
activity_state: { type: 'selection' },
activity_summary: { type: 'char' },
activity_type_icon: { type: 'char' },
activity_type_id: { type: 'many2one', relation: 'mail.activity.type' },
},
props: {
...standardFieldProps,
},
template: 'mail.ListFieldActivityViewContainer',
});
registry.category('fields').add('list_activity', ListFieldActivityViewContainer);

View file

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="mail.ListFieldActivityViewContainer" owl="1">
<ListFieldActivityView t-if="listFieldActivityView" record="listFieldActivityView"/>
</t>
</templates>

View file

@ -0,0 +1,26 @@
/** @odoo-module **/
import { clear } from '@mail/model/model_field_command';
const { onWillUpdateProps, useComponent } = owl;
/**
* This hook provides support for saving the reference of the component directly
* into the field of a record, and appropriately updates it when necessary
* (props change or destroy).
*
* @param {Object} param0
* @param {string} param0.fieldName Name of the field on the target record.
*/
export function useComponentToModel({ fieldName }) {
const component = useComponent();
component.props.record.update({ [fieldName]: component });
onWillUpdateProps(nextProps => {
const currentRecord = component.props.record;
const nextRecord = nextProps.record;
if (currentRecord.exists() && currentRecord !== nextRecord) {
currentRecord.update({ [fieldName]: clear() });
}
nextRecord.update({ [fieldName]: component });
});
}

View file

@ -0,0 +1,35 @@
/** @odoo-module **/
import { Listener } from '@mail/model/model_listener';
const { onRendered, onWillDestroy, onWillRender, useComponent } = owl;
/**
* 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 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}`,
onChange: () => component.render(),
});
onWillRender(() => {
component.env.services.messaging.modelManager.startListening(listener);
});
onRendered(() => {
component.env.services.messaging.modelManager.stopListening(listener);
});
onWillDestroy(() => {
component.env.services.messaging.modelManager.removeListener(listener);
});
component.env.services.messaging.modelManager.messagingCreatedPromise.then(() => {
component.render();
});
}

View file

@ -0,0 +1,28 @@
/** @odoo-module **/
import { clear } from '@mail/model/model_field_command';
const { onWillUpdateProps, useComponent, useRef } = owl;
/**
* This hook provides support for saving the result of useRef directly into the
* field of a record, and appropriately updates it when necessary (props change
* or destroy).
*
* @param {Object} param0
* @param {string} param0.fieldName Name of the field on the target record.
* @param {string} param0.refName Name of the t-ref on this component.
*/
export function useRefToModel({ fieldName, refName }) {
const component = useComponent();
const ref = useRef(refName);
component.props.record.update({ [fieldName]: ref });
onWillUpdateProps(nextProps => {
const currentRecord = component.props.record;
const nextRecord = nextProps.record;
if (currentRecord.exists() && currentRecord !== nextRecord) {
currentRecord.update({ [fieldName]: clear() });
}
nextRecord.update({ [fieldName]: ref });
});
}

View file

@ -0,0 +1,16 @@
/** @odoo-module **/
const { useComponent } = owl;
/**
* This hook provides support for dynamic-refs.
*
* @returns {function} returns object whose keys are t-ref values of active refs.
* and values are refs.
*/
export function useRefs() {
const component = useComponent();
return function () {
return component.__owl__.refs || {};
};
}

View file

@ -0,0 +1,36 @@
/** @odoo-module **/
import { Listener } from '@mail/model/model_listener';
const { onMounted, onPatched, onWillDestroy, onWillRender, useComponent } = owl;
/**
* This hooks provides support for accessing the values returned by the given
* selector at the time of the last render. The values will be updated after
* every mount/patch.
*
* @param {function} selector function that will be executed at the time of the
* render and of which the result will be stored for future reference.
* @returns {function} function to call to retrieve the last rendered values.
*/
export function useRenderedValues(selector) {
const component = useComponent();
let renderedValues;
let patchedValues;
const listener = new Listener({
name: `useRenderedValues() of ${component}`,
onChange: () => component.render(),
});
onWillRender(() => {
component.env.services.messaging.modelManager.startListening(listener);
renderedValues = selector();
component.env.services.messaging.modelManager.stopListening(listener);
});
onMounted(onUpdate);
onPatched(onUpdate);
function onUpdate() {
patchedValues = renderedValues;
}
onWillDestroy(() => component.env.services.messaging.modelManager.removeListener(listener));
return () => patchedValues;
}

View file

@ -0,0 +1,30 @@
/** @odoo-module **/
import { Listener } from '@mail/model/model_listener';
const { onMounted, onPatched, onWillDestroy, useComponent } = owl;
/**
* This hook provides support for executing code after update (render or patch).
*
* @param {Object} param0
* @param {function} param0.func the function to execute after the update.
*/
export function useUpdate({ func }) {
const component = useComponent();
const listener = new Listener({
isLocking: false, // unfortunately onUpdate methods often have side effect
name: `useUpdate() of ${component}`,
onChange: () => component.render(),
});
function onUpdate() {
component.env.services.messaging.modelManager.startListening(listener);
func();
component.env.services.messaging.modelManager.stopListening(listener);
}
onMounted(onUpdate);
onPatched(onUpdate);
onWillDestroy(() => {
component.env.services.messaging.modelManager.removeListener(listener);
});
}

View file

@ -0,0 +1,19 @@
/** @odoo-module **/
import { useUpdate } from '@mail/component_hooks/use_update';
const { useComponent } = owl;
/**
* This hook provides support for binding the onMounted/onPatched hooks to the
* method of a target record.
*
* @param {Object} param0
* @param {string} param0.methodName Name of the method on the target record.
*/
export function useUpdateToModel({ methodName }) {
const component = useComponent();
useUpdate({ func: () => {
component.props.record[methodName]();
} });
}

View file

@ -0,0 +1,36 @@
/** @odoo-module **/
import { useComponentToModel } from '@mail/component_hooks/use_component_to_model';
import { useRefToModel } from '@mail/component_hooks/use_ref_to_model';
import { registerMessagingComponent } from '@mail/utils/messaging_component';
import Popover from "web.Popover";
import { LegacyComponent } from "@web/legacy/legacy_component";
export class Activity extends LegacyComponent {
/**
* @override
*/
setup() {
super.setup();
useComponentToModel({ fieldName: 'component' });
useRefToModel({ fieldName: 'markDoneButtonRef', refName: 'markDoneButton', });
}
/**
* @returns {ActivityView}
*/
get activityView() {
return this.props.record;
}
}
Object.assign(Activity, {
props: { record: Object },
template: 'mail.Activity',
components: { Popover },
});
registerMessagingComponent(Activity);

View file

@ -0,0 +1,48 @@
// ------------------------------------------------------------------
// Layout
// ------------------------------------------------------------------
.o_Activity_core {
min-width: 0;
}
.o_Activity_detailsUserAvatar {
object-fit: cover;
height: 18px;
width: 18px;
}
.o_Activity_iconContainer {
@include o-position-absolute($top: auto, $left: auto, $bottom: -16%, $right: -5%);
}
.o_Activity_note p {
margin-bottom: map-get($spacers, 0);
}
.o_Activity_sidebar {
width: $o-mail-thread-avatar-size;
min-width: $o-mail-thread-avatar-size;
height: $o-mail-thread-avatar-size;
}
// From python template
.o_mail_note_title {
margin-top: map-get($spacers, 2);
}
.o_mail_note_title + div p {
margin-bottom: map-get($spacers, 0);
}
// ------------------------------------------------------------------
// Style
// ------------------------------------------------------------------
.o_Activity_detailsButton {
@include o-hover-text-color($default-color: $o-main-color-muted);
}
.o_Activity_iconContainer {
box-shadow: 0 0 0 2px $o-view-background-color;
}

View file

@ -0,0 +1,145 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="mail.Activity" owl="1">
<t t-if="activityView">
<div class="o_Activity d-flex py-2 px-3" t-attf-class="{{ className }}" t-on-click="activityView.onClickActivity" t-ref="root">
<div class="o_Activity_sidebar me-3">
<div class="o_Activity_user position-relative h-100 w-100">
<t t-if="activityView.activity.assignee">
<img class="o_Activity_userAvatar rounded-circle h-100 w-100 o_object_fit_cover" t-attf-src="/web/image/res.users/{{ activityView.activity.assignee.id }}/avatar_128" t-att-alt="activityView.activity.assignee.nameOrDisplayName"/>
</t>
<div class="o_Activity_iconContainer d-flex align-items-center justify-content-center rounded-circle w-50 h-50"
t-att-class="{
'text-bg-success': activityView.activity.state === 'planned',
'text-bg-warning': activityView.activity.state === 'today',
'text-bg-danger': activityView.activity.state === 'overdue',
}"
>
<i class="o_Activity_icon fa small" t-attf-class="{{ activityView.activity.icon }}"/>
</div>
</div>
</div>
<div class="o_Activity_core">
<div class="o_Activity_info d-flex align-items-baseline">
<div class="o_Activity_dueDateText me-2"
t-att-class="{
'text-danger': activityView.activity.state === 'overdue',
'text-success': activityView.activity.state === 'planned',
'text-warning': activityView.activity.state === 'today',
}"
>
<b t-esc="activityView.delayLabel"/>
</div>
<t t-if="activityView.activity.summary">
<b class="o_Activity_summary text-900 me-2">
<t t-esc="activityView.summary"/>
</b>
</t>
<t t-elif="activityView.activity.type">
<b class="o_Activity_summary o_Activity_type text-900 me-2">
<t t-esc="activityView.activity.type.displayName"/>
</b>
</t>
<t t-if="activityView.activity.assignee">
<div class="o_Activity_userName">
<t t-esc="activityView.assignedUserText"/>
</div>
</t>
<a
href="#"
class="o_Activity_detailsButton btn py-0"
t-att-class="activityView.areDetailsVisible ? 'text-primary' : 'btn-link btn-primary'"
t-att-aria-expanded="activityView.areDetailsVisible ? 'true' : 'false'"
t-on-click="activityView.onClickDetailsButton"
role="button"
>
<i class="fa fa-info-circle" role="img" title="Info" aria-label="Info"/>
</a>
</div>
<t t-if="activityView.areDetailsVisible">
<div class="o_Activity_details">
<div class="d-md-table table table-sm mt-2 mb-3">
<div t-if="activityView.activity.type" class="d-md-table-row mb-3">
<div class="d-md-table-cell fw-bold text-md-end m-0 py-md-1 px-md-4">Activity type</div>
<div class="o_Activity_type d-md-table-cell py-md-1 pe-4">
<t t-esc="activityView.activity.type.displayName"/>
</div>
</div>
<div t-if="activityView.activity.creator" class="d-md-table-row mb-3">
<div class="d-md-table-cell fw-bold text-md-end m-0 py-md-1 px-md-4">Created</div>
<div class="o_Activity_detailsCreation d-md-table-cell py-md-1 pe-4">
<t t-esc="activityView.formattedCreateDatetime"/>, <br t-if="messaging.device.isSmall"/>by
<img class="o_Activity_detailsUserAvatar o_Activity_detailsCreatorAvatar ms-1 me-1 rounded-circle align-text-bottom p-0" t-attf-src="/web/image/res.users/{{ activityView.activity.creator.id }}/avatar_128" t-att-title="activityView.activity.creator.nameOrDisplayName" t-att-alt="activityView.activity.creator.nameOrDisplayName"/>
<b class="o_Activity_detailsCreator">
<t t-esc="activityView.activity.creator.nameOrDisplayName"/>
</b>
</div>
</div>
<div t-if="activityView.activity.assignee" class="d-md-table-row mb-3">
<div class="d-md-table-cell fw-bold text-md-end m-0 py-md-1 px-md-4">Assigned to</div>
<div class="o_Activity_detailsAssignation d-md-table-cell py-md-1 pe-4">
<img class="o_Activity_detailsUserAvatar o_Activity_detailsAssignationUserAvatar me-1 rounded-circle align-text-bottom p-0" t-attf-src="/web/image/res.users/{{ activityView.activity.assignee.id }}/avatar_128" t-att-title="activityView.activity.assignee.nameOrDisplayName" t-att-alt="activityView.activity.assignee.nameOrDisplayName"/>
<b t-esc="activityView.activity.assignee.nameOrDisplayName"/>
</div>
</div>
<div class="d-md-table-row">
<div class="d-md-table-cell fw-bold text-md-end m-0 py-md-1 px-md-4">Due on</div>
<div class="o_Activity_detailsDueDate d-md-table-cell py-md-1 pe-4">
<span class="o_Activity_deadlineDateText"
t-att-class="{
'text-danger': activityView.activity.state === 'overdue',
'text-success': activityView.activity.state === 'planned',
'text-warning': activityView.activity.state === 'today',
}"
>
<t t-esc="activityView.formattedDeadlineDate"/>
</span>
</div>
</div>
</div>
</div>
</t>
<t t-if="activityView.activity.note">
<div class="o_Activity_note">
<t t-out="activityView.activity.noteAsMarkup"/>
</div>
</t>
<t t-if="activityView.mailTemplateViews.length > 0">
<div class="o_Activity_mailTemplates">
<t t-foreach="activityView.mailTemplateViews" t-as="mailTemplateView" t-key="mailTemplateView.localId">
<MailTemplate
className="'o_Activity_mailTemplate'"
record="mailTemplateView"
/>
</t>
</div>
</t>
<t t-if="activityView.activity.canWrite">
<div name="tools" class="o_Activity_tools d-flex">
<button class="o_Activity_toolButton o_Activity_markDoneButton btn btn-link btn-primary pt-0 ps-0" t-att-title="activityView.markDoneText" t-ref="markDoneButton" t-on-click="activityView.onClickMarkDoneButton">
<i class="fa fa-check"/> Mark Done
</button>
<t t-if="activityView.fileUploader">
<button class="o_Activity_toolButton o_Activity_uploadButton btn btn-link btn-primary pt-0 ps-0" t-on-click="activityView.onClickUploadDocument">
<i class="fa fa-upload"/> Upload Document
</button>
</t>
<button class="o_Activity_toolButton o_Activity_editButton btn btn-link btn-primary pt-0" t-on-click="activityView.onClickEdit">
<i class="fa fa-pencil"/> Edit
</button>
<button class="o_Activity_toolButton o_Activity_cancelButton btn btn-link btn-primary pt-0" t-on-click="activityView.onClickCancel" >
<i class="fa fa-times"/> Cancel
</button>
</div>
</t>
</div>
</div>
</t>
</t>
</templates>

View file

@ -0,0 +1,23 @@
/** @odoo-module **/
import { registerMessagingComponent } from '@mail/utils/messaging_component';
const { Component } = owl;
export class ActivityBox extends Component {
/**
* @returns {ActivityBoxView}
*/
get activityBoxView() {
return this.props.record;
}
}
Object.assign(ActivityBox, {
props: { record: Object },
template: 'mail.ActivityBox',
});
registerMessagingComponent(ActivityBox);

View file

@ -0,0 +1,19 @@
// ------------------------------------------------------------------
// Layout
// ------------------------------------------------------------------
.o_ActivityBox_activityList {
max-width: var(--Chatter-max-width, none);
}
// ------------------------------------------------------------------
// Style
// ------------------------------------------------------------------
.o_ActivityBox_title {
margin-top: var(--ActivityBox_title-margin, #{map-get($spacers, 4)});
}
.o_ActivityBox_titleLine {
border-top: $border-width dashed $border-color;
}

View file

@ -0,0 +1,45 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="mail.ActivityBox" owl="1">
<t t-if="activityBoxView">
<div class="o_ActivityBox" t-attf-class="{{ className }}" t-ref="root">
<a href="#" role="button" class="o_ActivityBox_title btn d-flex align-items-center p-0 w-100 fw-bold" t-att-aria-expanded="activityBoxView.isActivityListVisible ? 'true' : 'false'" t-on-click="activityBoxView.onClickActivityBoxTitle">
<hr class="o_ActivityBox_titleLine w-auto flex-grow-1 me-3" />
<span class="o_ActivityBox_titleText">
<i class="fa fa-fw" t-att-class="activityBoxView.isActivityListVisible ? 'fa-caret-down' : 'fa-caret-right'"/>
Planned activities
</span>
<t t-if="!activityBoxView.isActivityListVisible">
<span class="o_ActivityBox_titleBadges ms-2">
<t t-if="activityBoxView.chatter.thread.overdueActivities.length > 0">
<span class="o_ActivityBox_titleBadge me-1 badge text-bg-danger">
<t t-esc="activityBoxView.chatter.thread.overdueActivities.length"/>
</span>
</t>
<t t-if="activityBoxView.chatter.thread.todayActivities.length > 0">
<span class="o_ActivityBox_titleBadge me-1 badge text-bg-warning">
<t t-esc="activityBoxView.chatter.thread.todayActivities.length"/>
</span>
</t>
<t t-if="activityBoxView.chatter.thread.futureActivities.length > 0">
<span class="o_ActivityBox_titleBadge me-1 badge text-bg-success">
<t t-esc="activityBoxView.chatter.thread.futureActivities.length"/>
</span>
</t>
</span>
</t>
<hr class="o_ActivityBox_titleLine w-auto flex-grow-1 ms-3"/>
</a>
<t t-if="activityBoxView.isActivityListVisible">
<div class="o_ActivityBox_activityList">
<t t-foreach="activityBoxView.activityViews" t-as="activityView" t-key="activityView.localId">
<Activity className="'o_ActivityBox_activity'" record="activityView"/>
</t>
</div>
</t>
</div>
</t>
</t>
</templates>

View file

@ -0,0 +1,48 @@
/** @odoo-module **/
import { useComponentToModel } from '@mail/component_hooks/use_component_to_model';
import { useRefToModel } from '@mail/component_hooks/use_ref_to_model';
import { registerMessagingComponent } from '@mail/utils/messaging_component';
import { LegacyComponent } from "@web/legacy/legacy_component";
const { onMounted, useRef } = owl;
export class ActivityMarkDonePopoverContent extends LegacyComponent {
/**
* @override
*/
setup() {
super.setup();
useComponentToModel({ fieldName: 'component' });
useRefToModel({ fieldName: 'feedbackTextareaRef', refName: 'feedbackTextarea' });
this._feedbackTextareaRef = useRef('feedbackTextarea');
onMounted(() => this._mounted());
}
//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
_mounted() {
this._feedbackTextareaRef.el.focus();
if (this.activityMarkDonePopoverContentView.activity.feedbackBackup) {
this._feedbackTextareaRef.el.value = this.activityMarkDonePopoverContentView.activity.feedbackBackup;
}
}
/**
* @returns {ActivityMarkDonePopoverContentView}
*/
get activityMarkDonePopoverContentView() {
return this.props.record;
}
}
Object.assign(ActivityMarkDonePopoverContent, {
props: { record: Object },
template: 'mail.ActivityMarkDonePopoverContent',
});
registerMessagingComponent(ActivityMarkDonePopoverContent);

View file

@ -0,0 +1,7 @@
// ------------------------------------------------------------------
// Layout
// ------------------------------------------------------------------
.o_ActivityMarkDonePopoverContent_feedback {
min-height: 70px;
}

View file

@ -0,0 +1,28 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="mail.ActivityMarkDonePopoverContent" owl="1">
<t t-if="activityMarkDonePopoverContentView">
<div class="o_ActivityMarkDonePopoverContent" t-attf-class="{{ className }}" t-on-keydown="activityMarkDonePopoverContentView.onKeydown" t-ref="root">
<h6 t-if="activityMarkDonePopoverContentView.hasHeader" class="o_ActivityMarkDonePopoverContent_header p-2 fw-bolder bg-200 border-bottom" t-esc="activityMarkDonePopoverContentView.headerText"/>
<div class="o_ActivityMarkDonePopoverContent_content py-2 px-3">
<textarea class="form-control o_ActivityMarkDonePopoverContent_feedback" rows="3" placeholder="Write Feedback" t-on-blur="activityMarkDonePopoverContentView.onBlur" t-ref="feedbackTextarea"/>
<div class="o_ActivityMarkDonePopoverContent_buttons mt-2">
<button type="button" class="o_ActivityMarkDonePopoverContent_doneScheduleNextButton btn btn-sm btn-primary" t-on-click="activityMarkDonePopoverContentView.onClickDoneAndScheduleNext">
Done &amp; Schedule Next
</button>
<t t-if="activityMarkDonePopoverContentView.activity.chaining_type === 'suggest'">
<button type="button" class="o_ActivityMarkDonePopoverContent_doneButton btn btn-sm btn-primary mx-2" t-on-click="activityMarkDonePopoverContentView.onClickDone">
Done
</button>
</t>
<button type="button" class="o_ActivityMarkDonePopoverContent_discardButton btn btn-sm btn-link" t-on-click="activityMarkDonePopoverContentView.onClickDiscard">
Discard
</button>
</div>
</div>
</div>
</t>
</t>
</templates>

View file

@ -0,0 +1,27 @@
/** @odoo-module **/
// ensure components are registered beforehand.
import '@mail/components/activity_menu_view/activity_menu_view';
import { getMessagingComponent } from "@mail/utils/messaging_component";
const { Component } = owl;
export class ActivityMenuContainer extends Component {
/**
* @override
*/
setup() {
super.setup();
this.env.services.messaging.modelManager.messagingCreatedPromise.then(() => {
this.activityMenuView = this.env.services.messaging.modelManager.messaging.models['ActivityMenuView'].insert();
this.render();
});
}
}
Object.assign(ActivityMenuContainer, {
components: { ActivityMenuView: getMessagingComponent('ActivityMenuView') },
template: 'mail.ActivityMenuContainer',
});

View file

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="mail.ActivityMenuContainer" owl="1">
<t t-if="activityMenuView">
<ActivityMenuView record="activityMenuView"/>
</t>
</t>
</templates>

View file

@ -0,0 +1,29 @@
/** @odoo-module **/
import { useComponentToModel } from '@mail/component_hooks/use_component_to_model';
import { registerMessagingComponent } from '@mail/utils/messaging_component';
const { Component } = owl;
export class ActivityMenuView extends Component {
/**
* @override
*/
setup() {
super.setup();
useComponentToModel({ fieldName: 'component' });
}
/**
* @returns {ActivityMenuView}
*/
get activityMenuView() {
return this.props.record;
}
}
Object.assign(ActivityMenuView, {
props: { record: Object },
template: 'mail.ActivityMenuView',
});
registerMessagingComponent(ActivityMenuView);

View file

@ -0,0 +1,136 @@
.o_ActivityMenuView_activityGroups {
flex: 0 1 auto;
max-height: 400px;
min-height: 50px;
overflow-y: auto;
@include media-breakpoint-down(md) {
max-height: none;
padding-bottom: 52px; // leave space for tabs
}
}
.o_ActivityMenuView_activityGroup {
display: flex;
background-color: transparent;
color: $o-main-text-color;
cursor: pointer;
overflow: hidden;
position: relative;
&:hover {
background-color: map-get($theme-colors, 'light');
.o_ActivityMenuView_activityGroupName {
color: $headings-color;
}
}
&:not(:last-child) {
border-bottom: 1px solid map-get($grays, '400');
}
@include media-breakpoint-down(md) {
padding: $o-mail-chatter-mobile-gap;
}
@include media-breakpoint-up(md) {
min-height: 50px;
padding: 5px;
}
}
.o_ActivityMenuView_activityGroupActionButtons {
display: flex;
flex: 1 1 auto;
flex-flow: row-reverse wrap;
}
.o_ActivityMenuView_activityGroupActionButton {
padding-top: 0px;
padding-bottom: 0px;
padding-right: 0px;
}
.o_ActivityMenuView_activityGroupFilterButton {
padding: 2px;
}
.o_ActivityMenuView_activityGroupIconContainer {
display: flex;
align-items: center;
flex: 0 0 auto;
position: relative;
width: 40px;
height: 40px;
object-fit: cover;
> img {
max-width: 100%;
max-height: 100%;
object-fit: cover;
border-radius: 2px;
}
@include media-breakpoint-up(md) {
.fa-circle-o {
display: none;
}
}
}
.o_ActivityMenuView_activityGroupInfo {
flex: 1 1 100%;
overflow: hidden;
@include media-breakpoint-down(md) {
margin-left: $o-mail-chatter-mobile-gap;
}
@include media-breakpoint-up(md) {
margin-left: 10px;
}
}
.o_ActivityMenuView_activityGroupName {
flex: 0 1 auto;
@include o-text-overflow;
@include media-breakpoint-down(md) {
font-size: 1.1em;
}
}
.o_ActivityMenuView_activityGroupNoCount {
cursor: initial;
align-items: center;
opacity: 0.5;
padding: 3px;
min-height: inherit;
}
.o_ActivityMenuView_activityGroupTitle {
align-items: center;
display: flex;
}
.o_ActivityMenuView_dropdownMenu {
direction: ltr;
width: 350px;
padding: 0;
@include media-breakpoint-down(md) {
position: fixed;
top: $o-mail-chat-window-header-height-mobile;
bottom: 0;
left: 0;
right: 0;
width: 100%;
margin: 0;
max-height: none;
}
}
.o_ActivityMenuView_noActivity {
cursor: initial;
align-items: center;
color: grey;
opacity: 0.5;
padding: 3px;
min-height: inherit;
}

View file

@ -0,0 +1,57 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="mail.ActivityMenuView" owl="1">
<div class="o_ActivityMenuView dropdown" t-ref="root">
<a class="o_ActivityMenuView_dropdownToggle dropdown-toggle o-no-caret o-dropdown--narrow" t-att-aria-expanded="activityMenuView.isOpen ? 'true' : 'false'" title="Activities" href="#" role="button" t-on-click="activityMenuView.onClickDropdownToggle">
<i class="fa fa-lg fa-clock-o" role="img" aria-label="Activities"/> <span t-if="activityMenuView.counter > 0" class="o_ActivityMenuView_counter badge" t-esc="activityMenuView.counter"/>
</a>
<div t-if="activityMenuView.isOpen" class="o_ActivityMenuView_dropdownMenu o-dropdown-menu dropdown-menu-end show bg-view" role="menu">
<div class="o_ActivityMenuView_activityGroups">
<t t-if="activityMenuView.activityGroupViews.length === 0">
<div class="o_ActivityMenuView_noActivity dropdown-item-text text-center d-flex justify-content-center">
<span>Congratulations, you're done with your activities.</span>
</div>
</t>
<t t-foreach="activityMenuView.activityGroupViews" t-as="activityGroupView" t-key="activityGroupView.localId" name="activityGroupLoop">
<div class="o_ActivityMenuView_activityGroup" t-att-data-res_model="activityGroupView.activityGroup.irModel.model" t-att-data-model_name="activityGroupView.activityGroup.irModel.name" t-att-data-domain="activityGroupView.activityGroup.domain" data-filter='my' t-att-data-activity-group-view-local-id="activityGroupView.localId" t-on-click="activityGroupView.onClickFilterButton">
<div t-if="activityGroupView.activityGroup.irModel.iconUrl" class="o_ActivityMenuView_activityGroupIconContainer">
<img t-att-src="activityGroupView.activityGroup.irModel.iconUrl" alt="Activity"/>
</div>
<div class="o_ActivityMenuView_activityGroupInfo">
<div class="o_ActivityMenuView_activityGroupTitle">
<span class="o_ActivityMenuView_activityGroupName">
<t t-esc="activityGroupView.activityGroup.irModel.name"/>
</span>
<div t-if="activityGroupView.activityGroup.actions" class="o_ActivityMenuView_activityGroupActionButtons">
<t t-foreach="activityGroupView.activityGroup.actions" t-as="action" t-key="action.name">
<button type="button"
t-att-title="action.name"
t-att-class="'o_ActivityMenuView_activityGroupActionButton btn btn-link fa ' + action.icon"
t-att-data-action_xmlid="action.action_xmlid"
t-att-data-res_model="activityGroupView.activityGroup.irModel.model"
t-att-data-model_name="activityGroupView.activityGroup.irModel.name"
t-att-data-domain="activityGroupView.activityGroup.domain"
t-on-click="activityGroupView.onClick"
>
</button>
</t>
</div>
</div>
<div t-if="activityGroupView.activityGroup.type == 'activity'">
<button t-if="activityGroupView.activityGroup.overdue_count" type="button" class="o_ActivityMenuView_activityGroupFilterButton btn btn-link mr16" t-att-data-res_model="activityGroupView.activityGroup.irModel.model" t-att-data-model_name="activityGroupView.activityGroup.irModel.name" data-filter='overdue'><t t-esc="activityGroupView.activityGroup.overdue_count"/> Late </button>
<span t-if="!activityGroupView.activityGroup.overdue_count" class="o_ActivityMenuView_activityGroupNoCount mr16 text-muted">0 Late </span>
<button t-if="activityGroupView.activityGroup.today_count" type="button" class="o_ActivityMenuView_activityGroupFilterButton btn btn-link mr16" t-att-data-res_model="activityGroupView.activityGroup.irModel.model" t-att-data-model_name="activityGroupView.activityGroup.irModel.name" data-filter='today'> <t t-esc="activityGroupView.activityGroup.today_count"/> Today </button>
<span t-if="!activityGroupView.activityGroup.today_count" class="o_ActivityMenuView_activityGroupNoCount mr16 text-muted">0 Today </span>
<button t-if="activityGroupView.activityGroup.planned_count" type="button" class="o_ActivityMenuView_activityGroupFilterButton btn btn-link float-end" t-att-data-res_model="activityGroupView.activityGroup.irModel.model" t-att-data-model_name="activityGroupView.activityGroup.irModel.name" data-filter='upcoming_all'> <t t-esc="activityGroupView.activityGroup.planned_count"/> Future </button>
<span t-if="!activityGroupView.activityGroup.planned_count" class="o_ActivityMenuView_activityGroupNoCount float-end text-muted">0 Future</span>
</div>
</div>
</div>
</t>
</div>
</div>
</div>
</t>
</templates>

View file

@ -0,0 +1,36 @@
/** @odoo-module **/
import { useComponentToModel } from '@mail/component_hooks/use_component_to_model';
import { registerMessagingComponent } from '@mail/utils/messaging_component';
const { Component } = owl;
export class AttachmentBox extends Component {
/**
* @override
*/
setup() {
super.setup();
useComponentToModel({ fieldName: 'component' });
}
//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
/**
* @returns {AttachmentBoxView|undefined}
*/
get attachmentBoxView() {
return this.props.record;
}
}
Object.assign(AttachmentBox, {
props: { record: Object },
template: 'mail.AttachmentBox',
});
registerMessagingComponent(AttachmentBox);

View file

@ -0,0 +1,7 @@
// ------------------------------------------------------------------
// Style
// ------------------------------------------------------------------
.o_AttachmentBox_dashedLine {
border-top: $border-width dashed $border-color;
}

View file

@ -0,0 +1,30 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="mail.AttachmentBox" owl="1">
<t t-if="attachmentBoxView">
<div class="o_AttachmentBox position-relative" t-attf-class="{{ className }}" t-ref="root">
<div class="o_AttachmentBox_title d-flex align-items-center">
<hr class="o_AttachmentBox_dashedLine flex-grow-1"/>
<span class="o_AttachmentBox_titleText p-3 fw-bold">
Files
</span>
<hr class="o_AttachmentBox_dashedLine flex-grow-1"/>
</div>
<div class="o_AttachmentBox_content d-flex flex-column">
<t t-if="attachmentBoxView.attachmentList">
<AttachmentList
className="'o_attachmentBox_attachmentList'"
record="attachmentBoxView.attachmentList"
/>
</t>
<button class="o_AttachmentBox_buttonAdd btn btn-link" type="button" t-on-click="attachmentBoxView.onClickAddAttachment" t-att-disabled="!attachmentBoxView.chatter.isTemporary and !attachmentBoxView.chatter.hasWriteAccess">
<i class="fa fa-plus-square"/>
Attach files
</button>
</div>
</div>
</t>
</t>
</templates>

View file

@ -0,0 +1,23 @@
/** @odoo-module **/
import { registerMessagingComponent } from '@mail/utils/messaging_component';
const { Component } = owl;
export class AttachmentCard extends Component {
/**
* @returns {AttachmentCard}
*/
get attachmentCard() {
return this.props.record;
}
}
Object.assign(AttachmentCard, {
props: { record: Object },
template: 'mail.AttachmentCard',
});
registerMessagingComponent(AttachmentCard);

View file

@ -0,0 +1,37 @@
// ------------------------------------------------------------------
// Layout
// ------------------------------------------------------------------
.o_AttachmentCard:hover .o_AttachmentCard_asideItemUnlink.o-pretty {
transform: translateX(0);
}
.o_AttachmentCard_action {
min-width: 20px;
}
.o_AttachmentCard_aside {
&:not(.o-hasMultipleActions) {
min-width: 50px;
}
&.o-hasMultipleActions {
min-width: 30px;
}
}
.o_AttachmentCard_asideItemUnlink.o-pretty {
transform: translateX(100%);
}
.o_AttachmentCard_details {
min-width: 0; /* This allows the text ellipsis in the flex element */
}
// ------------------------------------------------------------------
// Style
// ------------------------------------------------------------------
.o_AttachmentCard_image.o-attachment-viewable {
cursor: zoom-in;
}

View file

@ -0,0 +1,66 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="mail.AttachmentCard" owl="1">
<t t-if="attachmentCard">
<div t-attf-class="{{ className }}" t-ref="root">
<div class="o_AttachmentCard o-has-card-details d-flex rounded bg-300"
t-att-class="{
'o-isUploading': attachmentCard.attachment.isUploading,
'o-viewable': attachmentCard.attachment.isViewable,
}" t-att-title="attachmentCard.attachment.displayName ? attachmentCard.attachment.displayName : undefined" role="menu" t-att-aria-label="attachmentCard.attachment.displayName" t-att-data-id="attachmentCard.attachment.localId"
>
<!-- Image style-->
<!-- o_image from mimetype.scss -->
<div class="o_AttachmentCard_image o_image flex-shrink-0 m-1" t-on-click="attachmentCard.onClickImage" t-att-class="{'o-attachment-viewable opacity-75-hover': attachmentCard.attachment.isViewable,}" role="menuitem" aria-label="Preview" t-att-tabindex="attachmentCard.attachment.isViewable ? 0 : -1" t-att-aria-disabled="!attachmentCard.attachment.isViewable" t-att-data-mimetype="attachmentCard.attachment.mimetype">
</div>
<!-- Attachment details -->
<div class="o_AttachmentCard_details d-flex justify-content-center flex-column px-1">
<t t-if="attachmentCard.attachment.displayName">
<div class="o_AttachmentCard_filename text-truncate">
<t t-esc="attachmentCard.attachment.displayName"/>
</div>
</t>
<t t-if="attachmentCard.attachment.extension">
<small class="o_AttachmentCard_extension text-uppercase">
<t t-esc="attachmentCard.attachment.extension"/>
</small>
</t>
</div>
<!-- Attachment aside -->
<div class="o_AttachmentCard_aside position-relative rounded-end overflow-hidden" t-att-class="{ 'o-hasMultipleActions d-flex flex-column': attachmentCard.hasMultipleActions }">
<!-- Uploading icon -->
<t t-if="attachmentCard.attachment.isUploading and attachmentCard.attachmentList.composerViewOwner">
<div class="o_AttachmentCard_asideItem o_AttachmentCard_asideItemUploading d-flex justify-content-center align-items-center w-100 h-100" title="Uploading">
<i class="fa fa-spin fa-spinner"/>
</div>
</t>
<!-- Uploaded icon -->
<t t-if="!attachmentCard.attachment.isUploading and attachmentCard.attachmentList.composerViewOwner">
<div class="o_AttachmentCard_asideItem o_AttachmentCard_asideItemUploaded d-flex justify-content-center align-items-center w-100 h-100 text-primary" title="Uploaded">
<i class="fa fa-check"/>
</div>
</t>
<!-- Remove button -->
<t t-if="attachmentCard.attachment.isDeletable">
<button class="o_AttachmentCard_asideItem o_AttachmentCard_asideItemUnlink btn top-0 justify-content-center align-items-center d-flex w-100 h-100 rounded-0" t-attf-class="{{ attachmentCard.attachmentList.composerViewOwner ? 'o-pretty position-absolute btn-primary transition-base' : 'bg-300' }}" t-on-click="attachmentCard.onClickUnlink" title="Remove">
<i class="fa fa-trash" role="img" aria-label="Remove"/>
</button>
</t>
<!-- Open link button -->
<t t-if="attachmentCard.attachment.type === 'url'">
<a class="o_AttachmentCard_asideItem o_AttachmentCard_asideItemOpenLink btn d-flex justify-content-center align-items-center w-100 h-100 rounded-0 bg-300" t-att-href="attachmentCard.attachment.url" target='_blank' title="Open Link">
<i class="fa fa-external-link" role="img" aria-label="Open Link"/>
</a>
</t>
<!-- Download button -->
<t t-elif="!attachmentCard.attachmentList.composerViewOwner and !attachmentCard.attachment.isUploading">
<button class="o_AttachmentCard_asideItem o_AttachmentCard_asideItemDownload btn d-flex justify-content-center align-items-center w-100 h-100 rounded-0 bg-300" t-on-click="attachmentCard.attachment.onClickDownload" title="Download">
<i class="fa fa-download" role="img" aria-label="Download"/>
</button>
</t>
</div>
</div>
</div>
</t>
</t>
</templates>

View file

@ -0,0 +1,31 @@
/** @odoo-module **/
import { useComponentToModel } from '@mail/component_hooks/use_component_to_model';
import { registerMessagingComponent } from '@mail/utils/messaging_component';
const { Component } = owl;
export class AttachmentDeleteConfirm extends Component {
/**
* @override
*/
setup() {
useComponentToModel({ fieldName: 'component' });
}
/**
* @returns {AttachmentDeleteConfirmView}
*/
get attachmentDeleteConfirmView() {
return this.props.record;
}
}
Object.assign(AttachmentDeleteConfirm, {
props: { record: Object },
template: 'mail.AttachmentDeleteConfirm',
});
registerMessagingComponent(AttachmentDeleteConfirm);

View file

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="mail.AttachmentDeleteConfirm" owl="1">
<t t-if="attachmentDeleteConfirmView">
<div class="o_AttachmentDeleteConfirm card bg-view" t-attf-class="{{ className }}" t-ref="root">
<h4 class="m-3">Confirmation</h4>
<hr class="mt-0 mb-3"/>
<p class="o_AttachmentDeleteConfirm_mainText mx-3 mb-3" t-esc="attachmentDeleteConfirmView.body"/>
<hr class="mt-0 mb-3"/>
<div class="o_AttachmentDeleteConfirm_buttons mx-3 mb-3">
<button class="o_AttachmentDeleteConfirm_confirmButton btn btn-primary me-2" t-on-click="attachmentDeleteConfirmView.onClickOk">Ok</button>
<button class="o_AttachmentDeleteConfirm_cancelButton btn btn-secondary me-2" t-on-click="attachmentDeleteConfirmView.onClickCancel">Cancel</button>
</div>
</div>
</t>
</t>
</templates>

View file

@ -0,0 +1,23 @@
/** @odoo-module **/
import { registerMessagingComponent } from '@mail/utils/messaging_component';
const { Component } = owl;
export class AttachmentImage extends Component {
/**
* @returns {AttachmentImage}
*/
get attachmentImage() {
return this.props.record;
}
}
Object.assign(AttachmentImage, {
props: { record: Object },
template: 'mail.AttachmentImage',
});
registerMessagingComponent(AttachmentImage);

View file

@ -0,0 +1,19 @@
// ------------------------------------------------------------------
// Layout
// ------------------------------------------------------------------
.o_AttachmentImage {
min-width: 20px;
min-height: 20px;
img {
object-fit: contain;
}
}
// ------------------------------------------------------------------
// Style
// ------------------------------------------------------------------
.o_AttachmentImage {
cursor: zoom-in;
}

View file

@ -0,0 +1,38 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="mail.AttachmentImage" owl="1">
<t t-if="attachmentImage">
<div t-attf-class="{{ className }}" role="menu" t-att-aria-label="attachmentImage.attachment.displayName" t-ref="root">
<div class="o_AttachmentImage d-flex position-relative flex-shrink-0"
t-att-class="{
'o-isUploading': attachmentImage.attachment.isUploading,
}"
t-att-title="attachmentImage.attachment.displayName ? attachmentImage.attachment.displayName : undefined"
t-att-data-id="attachmentImage.attachment.localId"
tabindex="0"
aria-label="View image"
role="menuitem"
t-on-click="attachmentImage.onClickImage"
t-att-data-mimetype="attachmentImage.attachment.mimetype"
>
<t t-if="!attachmentImage.attachment.isUploading">
<img class="img img-fluid my-0 mx-auto" t-att-src="attachmentImage.imageUrl" t-att-alt="attachmentImage.attachment.name" t-attf-style="max-width: min(100%, {{ attachmentImage.width }}px); max-height: {{ attachmentImage.height }}px;"/>
</t>
<t t-if="attachmentImage.attachment.isUploading">
<div class="o_AttachmentImageUploading position-absolute top-0 bottom-0 start-0 end-0 d-flex align-items-center justify-content-center" title="Uploading">
<i class="fa fa-spin fa-spinner"/>
</div>
</t>
<div class="o_AttachmentImage_imageOverlay position-absolute top-0 bottom-0 start-0 end-0 p-2 text-white opacity-0 opacity-100-hover d-flex align-items-end flax-wrap flex-column">
<div t-if="attachmentImage.attachment.isDeletable" class="o_AttachmentImage_action o_AttachmentImage_actionUnlink btn btn-sm btn-dark rounded opacity-75 opacity-100-hover" t-att-class="{'o-pretty': attachmentImage.attachmentList.composerViewOwner}" tabindex="0" aria-label="Remove" role="menuitem" t-on-click="attachmentImage.onClickUnlink" title="Remove">
<i class="fa fa-trash"/>
</div>
<div t-if="attachmentImage.hasDownloadButton" class="o_AttachmentImage_action o_AttachmentImage_actionDownload btn btn-sm btn-dark rounded opacity-75 opacity-100-hover mt-auto" t-on-click="attachmentImage.onClickDownload" title="Download">
<i class="fa fa-download"/>
</div>
</div>
</div>
</div>
</t>
</t>
</templates>

View file

@ -0,0 +1,23 @@
/** @odoo-module **/
import { registerMessagingComponent } from '@mail/utils/messaging_component';
const { Component } = owl;
export class AttachmentList extends Component {
/**
* @returns {AttachmentList}
*/
get attachmentList() {
return this.props.record;
}
}
Object.assign(AttachmentList, {
props: { record: Object },
template: 'mail.AttachmentList',
});
registerMessagingComponent(AttachmentList);

View file

@ -0,0 +1,3 @@
.o_AttachmentList {
max-width: var(--Chatter-max-width, none);
}

View file

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="mail.AttachmentList" owl="1">
<t t-if="attachmentList">
<div class="o_AttachmentList d-flex flex-column mt-1" t-att-class="{ 'me-2 pe-4': attachmentList.isInChatWindowAndIsAlignedLeft and !attachmentList.composerViewOwner, 'ms-2 ps-4': attachmentList.isInChatWindowAndIsAlignedRight and !attachmentList.composerViewOwner }" t-attf-class="{{ className }}" t-ref="root">
<div t-if="attachmentList.attachmentImages.length > 0" class="o_AttachmentList_partialList o_AttachmentList_partialListImages d-flex flex-grow-1 flex-wrap" t-att-class="{ 'justify-content-end': attachmentList.isInChatWindowAndIsAlignedRight and !attachmentList.composerViewOwner }">
<t t-foreach="attachmentList.attachmentImages" t-as="attachmentImage" t-key="attachmentImage.localId">
<AttachmentImage className="'o_AttachmentList_attachment mw-100 mb-1'" classNameObj="{ 'ms-1': attachmentList.isInChatWindowAndIsAlignedRight, 'me-1': !attachmentList.isInChatWindowAndIsAlignedRight }" record="attachmentImage"/>
</t>
</div>
<div t-if="attachmentList.attachmentCards.length > 0" class="o_AttachmentList_partialList o_AttachmentList_partialListNonImages d-flex flex-grow-1 flex-wrap mt-1" t-att-class="{ 'justify-content-end': attachmentList.isInChatWindowAndIsAlignedRight and !attachmentList.composerViewOwner }">
<t t-foreach="attachmentList.attachmentCards" t-as="attachmentCard" t-key="attachmentCard.localId">
<AttachmentCard className="'o_AttachmentList_attachment mw-100 mb-1'" classNameObj="{ 'ms-1': attachmentList.isInChatWindowAndIsAlignedRight, 'me-1': !attachmentList.isInChatWindowAndIsAlignedRight }" record="attachmentCard"/>
</t>
</div>
</div>
</t>
</t>
</templates>

View file

@ -0,0 +1,7 @@
// = Attachment Viewer View
// ============================================================================
// No CSS hacks, variables overrides only
.o_AttachmentViewer_toolbarButton {
--AttachmentViewer_toolbarButton-background-color: #{$o-gray-200};
}

View file

@ -0,0 +1,359 @@
/** @odoo-module **/
import { useComponentToModel } from '@mail/component_hooks/use_component_to_model';
import { useRefs } from '@mail/component_hooks/use_refs';
import { registerMessagingComponent } from '@mail/utils/messaging_component';
import { hidePDFJSButtons } from '@web/legacy/js/libs/pdfjs';
const { Component, onMounted, onPatched, onWillUnmount, useRef } = owl;
const MIN_SCALE = 0.5;
const SCROLL_ZOOM_STEP = 0.1;
const ZOOM_STEP = 0.5;
export class AttachmentViewer extends Component {
/**
* @override
*/
setup() {
super.setup();
useComponentToModel({ fieldName: 'component' });
this.MIN_SCALE = MIN_SCALE;
/**
* Used to ensure that the ref is always up to date, which seems to be needed if the element
* has a t-key, which was added to force the rendering of a new element when the src of the image changes.
* This was made to remove the display of the previous image as soon as the src changes.
*/
this._getRefs = useRefs();
/**
* Reference of the zoomer node. Useful to apply translate
* transformation on image visualisation.
*/
this._zoomerRef = useRef('zoomer');
/**
* Reference of the IFRAME node when the attachment is a PDF.
*/
this._iframeViewerPdfRef = useRef('iframeViewerPdf');
/**
* Tracked translate transformations on image visualisation. This is
* not observed for re-rendering because they are used to compute zoomer
* style, and this is changed directly on zoomer for performance
* reasons (overhead of making vdom is too significant for each mouse
* position changes while dragging)
*/
this._translate = { x: 0, y: 0, dx: 0, dy: 0 };
this._onClickGlobal = this._onClickGlobal.bind(this);
onMounted(() => this._mounted());
onPatched(() => this._patched());
onWillUnmount(() => this._willUnmount());
}
_mounted() {
if (!this.root.el) {
return;
}
this.root.el.focus();
this._handleImageLoad();
this._hideUnwantedPdfJsButtons();
document.addEventListener('click', this._onClickGlobal);
}
/**
* When a new image is displayed, show a spinner until it is loaded.
*/
_patched() {
this._handleImageLoad();
this._hideUnwantedPdfJsButtons();
}
_willUnmount() {
document.removeEventListener('click', this._onClickGlobal);
}
//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
/**
* @returns {AttachmentViewer}
*/
get attachmentViewer() {
return this.props.record;
}
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
/**
* Determine whether the current image is rendered for the 1st time, and if
* that's the case, display a spinner until loaded.
*
* @private
*/
_handleImageLoad() {
if (!this.attachmentViewer.exists() || !this.attachmentViewer.attachmentViewerViewable) {
return;
}
const refs = this._getRefs();
const image = refs[`image_${this.attachmentViewer.attachmentViewerViewable.localId}`];
if (
this.attachmentViewer.attachmentViewerViewable.isImage &&
(!image || !image.complete)
) {
this.attachmentViewer.update({ isImageLoading: true });
}
}
/**
* @see 'hidePDFJSButtons'
*
* @private
*/
_hideUnwantedPdfJsButtons() {
if (this._iframeViewerPdfRef.el) {
hidePDFJSButtons(this._iframeViewerPdfRef.el);
}
}
/**
* Stop dragging interaction of the user.
*
* @private
*/
_stopDragging() {
this.attachmentViewer.update({ isDragging: false });
this._translate.x += this._translate.dx;
this._translate.y += this._translate.dy;
this._translate.dx = 0;
this._translate.dy = 0;
this._updateZoomerStyle();
}
/**
* Update the style of the zoomer based on translate transformation. Changes
* are directly applied on zoomer, instead of triggering re-render and
* defining them in the template, for performance reasons.
*
* @private
* @returns {string}
*/
_updateZoomerStyle() {
const attachmentViewer = this.attachmentViewer;
const refs = this._getRefs();
const image = refs[`image_${this.attachmentViewer.attachmentViewerViewable.localId}`];
// some actions are too fast that sometimes this function is called
// before setting the refs, so we just do nothing when image is null
if (!image) {
return;
}
const tx = image.offsetWidth * attachmentViewer.scale > this._zoomerRef.el.offsetWidth
? this._translate.x + this._translate.dx
: 0;
const ty = image.offsetHeight * attachmentViewer.scale > this._zoomerRef.el.offsetHeight
? this._translate.y + this._translate.dy
: 0;
if (tx === 0) {
this._translate.x = 0;
}
if (ty === 0) {
this._translate.y = 0;
}
this._zoomerRef.el.style = `transform: ` +
`translate(${tx}px, ${ty}px)`;
}
/**
* Zoom in the image.
*
* @private
* @param {Object} [param0={}]
* @param {boolean} [param0.scroll=false]
*/
_zoomIn({ scroll = false } = {}) {
this.attachmentViewer.update({
scale: this.attachmentViewer.scale + (scroll ? SCROLL_ZOOM_STEP : ZOOM_STEP),
});
this._updateZoomerStyle();
}
/**
* Zoom out the image.
*
* @private
* @param {Object} [param0={}]
* @param {boolean} [param0.scroll=false]
*/
_zoomOut({ scroll = false } = {}) {
if (this.attachmentViewer.scale === MIN_SCALE) {
return;
}
const unflooredAdaptedScale = (
this.attachmentViewer.scale -
(scroll ? SCROLL_ZOOM_STEP : ZOOM_STEP)
);
this.attachmentViewer.update({
scale: Math.max(MIN_SCALE, unflooredAdaptedScale),
});
this._updateZoomerStyle();
}
/**
* Reset the zoom scale of the image.
*
* @private
*/
_zoomReset() {
this.attachmentViewer.update({ scale: 1 });
this._updateZoomerStyle();
}
//--------------------------------------------------------------------------
// Handlers
//--------------------------------------------------------------------------
/**
* @private
* @param {MouseEvent} ev
*/
_onClickGlobal(ev) {
if (!this.attachmentViewer.exists()) {
return;
}
if (!this.attachmentViewer.isDragging) {
return;
}
ev.stopPropagation();
this._stopDragging();
}
/**
* Called when clicking on zoom in icon.
*
* @private
* @param {MouseEvent} ev
*/
_onClickZoomIn(ev) {
ev.stopPropagation();
this._zoomIn();
}
/**
* Called when clicking on zoom out icon.
*
* @private
* @param {MouseEvent} ev
*/
_onClickZoomOut(ev) {
ev.stopPropagation();
this._zoomOut();
}
/**
* Called when clicking on reset zoom icon.
*
* @private
* @param {MouseEvent} ev
*/
_onClickZoomReset(ev) {
ev.stopPropagation();
this._zoomReset();
}
/**
* @private
* @param {KeyboardEvent} ev
*/
_onKeydown(ev) {
switch (ev.key) {
case 'ArrowRight':
this.attachmentViewer.next();
break;
case 'ArrowLeft':
this.attachmentViewer.previous();
break;
case 'Escape':
this.attachmentViewer.close();
break;
case 'q':
this.attachmentViewer.close();
break;
case 'r':
this.attachmentViewer.rotate();
break;
case '+':
this._zoomIn();
break;
case '-':
this._zoomOut();
break;
case '0':
this._zoomReset();
break;
default:
return;
}
ev.stopPropagation();
}
/**
* @private
* @param {DragEvent} ev
*/
_onMousedownImage(ev) {
if (!this.attachmentViewer.exists()) {
return;
}
if (this.attachmentViewer.isDragging) {
return;
}
if (ev.button !== 0) {
return;
}
ev.stopPropagation();
this.attachmentViewer.update({ isDragging: true });
this._dragstartX = ev.clientX;
this._dragstartY = ev.clientY;
}
/**
* @private
* @param {DragEvent}
*/
_onMousemoveView(ev) {
if (!this.attachmentViewer.exists()) {
return;
}
if (!this.attachmentViewer.isDragging) {
return;
}
this._translate.dx = ev.clientX - this._dragstartX;
this._translate.dy = ev.clientY - this._dragstartY;
this._updateZoomerStyle();
}
/**
* @private
* @param {Event} ev
*/
_onWheelImage(ev) {
ev.stopPropagation();
if (!this.root.el) {
return;
}
if (ev.deltaY > 0) {
this._zoomOut({ scroll: true });
} else {
this._zoomIn({ scroll: true });
}
}
}
Object.assign(AttachmentViewer, {
props: { record: Object },
template: 'mail.AttachmentViewer',
});
registerMessagingComponent(AttachmentViewer);

View file

@ -0,0 +1,69 @@
// ------------------------------------------------------------------
// Layout
// ------------------------------------------------------------------
.o_AttachmentViewer {
z-index: -1;
}
.o_AttachmentViewer_buttonNavigation {
width: 40px;
height: 40px;
}
.o_AttachmentViewer_buttonNavigationNextIcon {
margin: 1px 0 0 1px; // not correctly centered for some reasons
}
.o_AttachmentViewer_buttonNavigationPreviousIcon {
margin: 1px 1px 0 0; // not correctly centered for some reasons
}
.o_AttachmentViewer_header {
height: $o-navbar-height;
}
.o_AttachmentViewer_main {
z-index: -1;
padding: ($o-navbar-height * 1.125) 0;
}
.o_AttachmentViewer_name {
min-width: 0;
}
.o_AttachmentViewer_zoomer {
padding: ($o-navbar-height * 1.125) 0;
}
// ------------------------------------------------------------------
// Style
// ------------------------------------------------------------------
.o_AttachmentViewer {
outline: none;
}
.o_AttachmentViewer_headerItemButton:hover {
background-color: rgba($white, 0.1);
color: lighten($gray-400, 15%);
}
.o_AttachmentViewer_toolbarButton {
background-color: var(--AttachmentViewer_toolbarButton-background-color, #{$o-gray-800});
color: #fff;
&:hover {
filter: brightness(1.3);
}
}
.o_AttachmentViewer_view {
background-color: #000000;
box-shadow: 0 0 40px #000000;
outline: none;
&.o_AttachmentViewer_isText {
background: $white;
}
}

View file

@ -0,0 +1,98 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="mail.AttachmentViewer" owl="1">
<t t-if="attachmentViewer">
<div class="o_AttachmentViewer flex-column align-items-center d-flex w-100 h-100" t-attf-class="{{ className }}" t-on-click="attachmentViewer.onClick" t-on-keydown="_onKeydown" tabindex="0" t-ref="root">
<div class="o_AttachmentViewer_header d-flex w-100 bg-black-75 text-400" t-on-click="attachmentViewer.onClickHeader">
<t t-if="attachmentViewer.attachmentViewerViewable.isViewable">
<div class="o_AttachmentViewer_headerItem o_AttachmentViewer_icon d-flex align-items-center ms-4 me-2">
<t t-if="attachmentViewer.attachmentViewerViewable.isImage">
<i class="fa fa-picture-o" role="img" title="Image"/>
</t>
<t t-if="attachmentViewer.attachmentViewerViewable.isPdf">
<i class="fa fa-file-text" role="img" title="PDF file"/>
</t>
<t t-if="attachmentViewer.attachmentViewerViewable.isText">
<i class="fa fa-file-text" role="img" title="Text file"/>
</t>
<t t-if="attachmentViewer.attachmentViewerViewable.isVideo">
<i class="fa fa-video-camera" role="img" title="Video"/>
</t>
</div>
</t>
<div class="o_AttachmentViewer_headerItem o_AttachmentViewer_name d-flex align-items-center mx-2">
<span class="o_AttachmentViewer_nameText text-truncate" t-esc="attachmentViewer.attachmentViewerViewable.displayName"/>
</div>
<div class="flex-grow-1"/>
<div class="o_AttachmentViewer_buttonDownload o_AttachmentViewer_headerItem o_AttachmentViewer_headerItemButton d-flex align-items-center px-3 cursor-pointer" t-on-click="attachmentViewer.onClickDownload" role="button" title="Download">
<i class="o_AttachmentViewer_headerItemButtonIcon fa fa-download fa-fw" t-att-class="{ 'o-hasLabel me-2': messaging.device.sizeClass > messaging.device.sizeClasses.MD }" role="img"/>
<t t-if="messaging.device.sizeClass > messaging.device.sizeClasses.MD">
<span>Download</span>
</t>
</div>
<div class="o_AttachmentViewer_headerItem o_AttachmentViewer_headerItemButton o_AttachmentViewer_headerItemButtonClose d-flex align-items-center mb-0 px-3 h4 text-reset cursor-pointer" t-on-click="attachmentViewer.onClickClose" role="button" title="Close (Esc)" aria-label="Close">
<i class="fa fa-fw fa-times" role="img"/>
</div>
</div>
<div class="o_AttachmentViewer_main position-absolute top-0 bottom-0 start-0 end-0 align-items-center justify-content-center d-flex" t-att-class="{ 'o_with_img overflow-hidden': attachmentViewer.attachmentViewerViewable.isImage }" t-on-mousemove="_onMousemoveView">
<t t-if="attachmentViewer.attachmentViewerViewable.isImage">
<div class="o_AttachmentViewer_zoomer position-absolute align-items-center justify-content-center d-flex w-100 h-100" t-ref="zoomer">
<t t-if="attachmentViewer.isImageLoading">
<div class="o_AttachmentViewer_loading position-absolute">
<i class="fa fa-3x fa-circle-o-notch fa-fw fa-spin text-white" role="img" title="Loading"/>
</div>
</t>
<img class="o_AttachmentViewer_view o_AttachmentViewer_viewImage mw-100 mh-100 transition-base" t-on-click="attachmentViewer.onClickImage" t-on-mousedown="_onMousedownImage" t-on-wheel="_onWheelImage" t-on-load="attachmentViewer.onLoadImage" t-att-src="attachmentViewer.attachmentViewerViewable.imageUrl" t-att-style="attachmentViewer.imageStyle" draggable="false" alt="Viewer" t-key="'image_' + attachmentViewer.attachmentViewerViewable.localId" t-ref="image_{{ attachmentViewer.attachmentViewerViewable.localId }}"/>
</div>
</t>
<t t-if="attachmentViewer.attachmentViewerViewable.isPdf">
<iframe class="o_AttachmentViewer_view o_AttachmentViewer_viewIframe o_AttachmentViewer_viewPdf w-75 h-100 border-0" t-ref="iframeViewerPdf" t-att-class="{ 'w-100': messaging.device.isSmall }" t-att-src="attachmentViewer.attachmentViewerViewable.defaultSource"/>
</t>
<t t-if="attachmentViewer.attachmentViewerViewable.isText">
<iframe class="o_AttachmentViewer_view o_AttachmentViewer_viewIframe o_AttachmentViewer_isText o_text w-75 h-100 border-0" t-att-src="attachmentViewer.attachmentViewerViewable.defaultSource"/>
</t>
<t t-if="attachmentViewer.attachmentViewerViewable.isUrlYoutube">
<iframe allow="autoplay; encrypted-media" class="o_AttachmentViewer_view o_AttachmentViewer_viewIframe o_AttachmentViewer_youtube w-75 h-100 border-0" t-att-src="attachmentViewer.attachmentViewerViewable.defaultSource" height="315" width="560"/>
</t>
<t t-if="attachmentViewer.attachmentViewerViewable.isVideo">
<video class="o_AttachmentViewer_view o_AttachmentViewer_viewVideo w-75 h-75" t-att-class="{ 'w-100 h-100': messaging.device.isSmall }" t-on-click="attachmentViewer.onClickVideo" controls="controls">
<source t-att-data-type="attachmentViewer.attachmentViewerViewable.mimetype" t-att-src="attachmentViewer.attachmentViewerViewable.defaultSource"/>
</video>
</t>
</div>
<t t-if="attachmentViewer.attachmentViewerViewable.isImage">
<div class="o_AttachmentViewer_toolbar position-absolute bottom-0 d-flex" role="toolbar">
<div class="o_AttachmentViewer_toolbarButton p-3 rounded-0" t-on-click="_onClickZoomIn" title="Zoom In (+)" role="button">
<i class="fa fa-fw fa-plus" role="img"/>
</div>
<div class="o_AttachmentViewer_toolbarButton p-3 rounded-0" t-att-class="{ 'o_disabled opacity-50': attachmentViewer.scale === 1 }" t-on-click="_onClickZoomReset" role="button" title="Reset Zoom (0)">
<i class="fa fa-fw fa-search" role="img"/>
</div>
<div class="o_AttachmentViewer_toolbarButton p-3 rounded-0" t-att-class="{ 'o_disabled opacity-50': attachmentViewer.scale === MIN_SCALE }" t-on-click="_onClickZoomOut" title="Zoom Out (-)" role="button">
<i class="fa fa-fw fa-minus" role="img"/>
</div>
<div class="o_AttachmentViewer_toolbarButton p-3 rounded-0" t-on-click="attachmentViewer.onClickRotate" title="Rotate (r)" role="button">
<i class="fa fa-fw fa-repeat" role="img"/>
</div>
<div class="o_AttachmentViewer_toolbarButton p-3 rounded-0" t-on-click="attachmentViewer.onClickPrint" title="Print" role="button">
<i class="fa fa-fw fa-print" role="img"/>
</div>
<div class="o_AttachmentViewer_buttonDownload o_AttachmentViewer_toolbarButton p-3 rounded-0 cursor-pointer" t-on-click="attachmentViewer.onClickDownload" title="Download" role="button">
<i class="fa fa-download fa-fw" role="img"/>
</div>
</div>
</t>
<t t-if="attachmentViewer.attachmentViewerViewables.length > 1">
<div class="o_AttachmentViewer_buttonNavigation o_AttachmentViewer_buttonNavigationPrevious o_AttachmentViewer_buttonNavigationPreviousIcon position-absolute top-0 bottom-0 start-0 align-items-center justify-content-center d-flex my-auto ms-3 rounded-circle bg-dark text-white" t-on-click="attachmentViewer.onClickPrevious" title="Previous (Left-Arrow)" role="button">
<span class="fa fa-chevron-left" role="img"/>
</div>
<div class="o_AttachmentViewer_buttonNavigation o_AttachmentViewer_buttonNavigationNext o_AttachmentViewer_buttonNavigationNextIcon position-absolute top-0 bottom-0 end-0 align-items-center justify-content-center d-flex my-auto me-3 rounded-circle bg-dark text-white" t-on-click="attachmentViewer.onClickNext" title="Next (Right-Arrow)" role="button">
<span class="fa fa-chevron-right" role="img"/>
</div>
</t>
</div>
</t>
</t>
</templates>

View file

@ -0,0 +1,77 @@
/** @odoo-module **/
import { useComponentToModel } from '@mail/component_hooks/use_component_to_model';
import { registerMessagingComponent } from '@mail/utils/messaging_component';
const { Component, onMounted, onWillUnmount } = owl;
export class AutocompleteInputView extends Component {
/**
* @override
*/
setup() {
super.setup();
useComponentToModel({ fieldName: 'component' });
onMounted(() => this._mounted());
onWillUnmount(() => this._willUnmount());
}
_mounted() {
if (!this.root.el) {
return;
}
if (this.autocompleteInputView.isFocusOnMount) {
this.root.el.focus();
}
const args = {
autoFocus: true,
select: (ev, ui) => {
if (this.autocompleteInputView) {
this.autocompleteInputView.onSelect(ev, ui);
}
},
source: (req, res) => {
if (this.autocompleteInputView) {
this.autocompleteInputView.onSource(req, res);
}
},
html: this.autocompleteInputView.isHtml,
};
if (this.autocompleteInputView.customClass) {
args.classes = { 'ui-autocomplete': this.autocompleteInputView.customClass };
}
const autoCompleteElem = $(this.root.el).autocomplete(args);
// Resize the autocomplete dropdown options to handle the long strings
// By setting the width of dropdown based on the width of the input element.
autoCompleteElem.data('ui-autocomplete')._resizeMenu = function () {
const ul = this.menu.element;
ul.outerWidth(this.element.outerWidth());
};
}
_willUnmount() {
if (!this.root.el) {
return;
}
$(this.root.el).autocomplete('destroy');
}
/**
* @returns {AutocompleteInputView}
*/
get autocompleteInputView() {
return this.props.record;
}
}
Object.assign(AutocompleteInputView, {
props: { record: Object },
template: 'mail.AutocompleteInputView',
});
registerMessagingComponent(AutocompleteInputView);

View file

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="mail.AutocompleteInputView" owl="1">
<input class="o_AutocompleteInputView form-control" t-attf-class="{{ className }}" t-on-blur="autocompleteInputView.onBlur" t-on-focusin="autocompleteInputView.onFocusin" t-on-keydown="autocompleteInputView.onKeydown" t-att-placeholder="autocompleteInputView.placeholder" t-ref="root"/>
</t>
</templates>

View file

@ -0,0 +1,7 @@
// = Call Action List View
// ============================================================================
// No CSS hacks, variables overrides only
.o_CallActionList_button:not(.btn-danger) {
--CallActionList_button-background-color: #{$o-gray-200};
}

View file

@ -0,0 +1,32 @@
/** @odoo-module **/
import { useRefToModel } from '@mail/component_hooks/use_ref_to_model';
import { registerMessagingComponent } from '@mail/utils/messaging_component';
const { Component } = owl;
export class CallActionList extends Component {
/**
* @override
*/
setup() {
super.setup();
useRefToModel({ fieldName: 'moreButtonRef', refName: 'moreButton' });
}
/**
* @returns {CallActionListView}
*/
get callActionListView() {
return this.props.record;
}
}
Object.assign(CallActionList, {
props: { record: Object },
template: 'mail.CallActionList',
});
registerMessagingComponent(CallActionList);

View file

@ -0,0 +1,4 @@
.o_CallActionList_button:not(.btn-danger) {
background-color: var(--CallActionList_button-background-color, #{$o-gray-800});
color: #FFFFFF;
}

View file

@ -0,0 +1,144 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="mail.CallActionList" owl="1">
<t t-if="callActionListView">
<div class="o_CallActionList d-flex justify-content-between" t-attf-class="{{ className }}" t-ref="root">
<div class="o_CallActionList_buttons d-flex align-items-center flex-wrap">
<t t-if="callActionListView.thread.rtc and messaging.rtc.currentRtcSession">
<button class="o_CallActionList_button btn d-flex m-1 border-0 rounded-circle shadow-none opacity-100 opacity-75-hover"
t-att-class="{ 'o-isActive': !messaging.rtc.currentRtcSession.isMute, 'o-isSmall p-2': callActionListView.isSmall, 'p-3': !callActionListView.isSmall }"
t-att-aria-label="callActionListView.microphoneButtonTitle"
t-att-title="callActionListView.microphoneButtonTitle"
t-on-click="callActionListView.onClickMicrophone">
<div class="o_CallActionList_buttonIconWrapper fa-stack" t-att-class="{ 'o-isSmall': callActionListView.isSmall }">
<i class="fa fa-stack-1x" t-att-class="{
'fa-lg': !callActionListView.isSmall,
'fa-microphone': !messaging.rtc.currentRtcSession.isMute,
'fa-microphone-slash': messaging.rtc.currentRtcSession.isMute,
'text-danger': messaging.rtc.currentRtcSession.isMute,
}"/>
</div>
</button>
<button class="o_CallActionList_button btn d-flex m-1 border-0 rounded-circle shadow-none opacity-100 opacity-75-hover"
t-att-class="{ 'o-isActive': !messaging.rtc.currentRtcSession.isDeaf, 'o-isSmall p-2': callActionListView.isSmall, 'p-3': !callActionListView.isSmall }"
t-att-aria-label="callActionListView.headphoneButtonTitle"
t-att-title="callActionListView.headphoneButtonTitle"
t-on-click="callActionListView.onClickDeafen">
<div class="o_CallActionList_buttonIconWrapper fa-stack" t-att-class="{ 'o-isSmall': callActionListView.isSmall }">
<i class="fa fa-stack-1x" t-att-class="{
'fa-lg': !callActionListView.isSmall,
'fa-headphones': !messaging.rtc.currentRtcSession.isDeaf,
'fa-deaf': messaging.rtc.currentRtcSession.isDeaf,
'text-danger': messaging.rtc.currentRtcSession.isDeaf,
}"/>
</div>
</button>
<button class="o_CallActionList_button o_CallActionList_videoButton btn d-flex m-1 border-0 rounded-circle shadow-none opacity-100 opacity-75-hover"
t-att-class="{
'o-isActive': messaging.rtc.sendUserVideo,
'o-isSmall p-2': callActionListView.isSmall,
'p-3': !callActionListView.isSmall,
}"
t-att-aria-label="callActionListView.cameraButtonTitle"
t-att-title="callActionListView.cameraButtonTitle"
t-on-click="callActionListView.onClickCamera">
<div class="o_CallActionList_buttonIconWrapper fa-stack" t-att-class="{ 'o-isSmall': callActionListView.isSmall }">
<i class="fa fa-video-camera fa-stack-1x" t-att-class="{ 'fa-lg': !callActionListView.isSmall, 'text-success': messaging.rtc.sendUserVideo }"/>
</div>
</button>
<t t-if="!messaging.device.isMobileDevice">
<button class="o_CallActionList_button o_CallActionList_videoButton btn d-flex m-1 border-0 rounded-circle shadow-none opacity-100 opacity-75-hover"
t-att-class="{
'o-isActive': messaging.rtc.sendDisplay,
'o-isSmall p-2': callActionListView.isSmall,
'p-3': !callActionListView.isSmall,
}"
t-att-aria-label="callActionListView.screenSharingButtonTitle"
t-att-title="callActionListView.screenSharingButtonTitle"
t-on-click="callActionListView.onClickScreen">
<div class="o_CallActionList_buttonIconWrapper fa-stack" t-att-class="{ 'o-isSmall': callActionListView.isSmall }">
<i class="fa fa-desktop fa-stack-1x" t-att-class="{ 'fa-lg': !callActionListView.isSmall, 'text-success': messaging.rtc.sendDisplay }"/>
</div>
</button>
</t>
<t t-if="!callActionListView.callView.isFullScreen">
<button class="o_CallActionList_button btn d-flex m-1 border-0 rounded-circle shadow-none opacity-100 opacity-75-hover"
aria-label="Activate Full Screen"
title="Activate Full Screen"
t-att-class="{ 'o-isSmall p-2': callActionListView.isSmall, 'p-3': !callActionListView.isSmall }"
t-on-click="callActionListView.callView.activateFullScreen"
>
<div class="o_CallActionList_buttonIconWrapper fa-stack" t-att-class="{ 'o-isSmall': callActionListView.isSmall }">
<i class="fa fa-arrows-alt fa-stack-1x" t-att-class="{ 'fa-lg': !callActionListView.isSmall }"/>
</div>
</button>
</t>
<t t-if="callActionListView.callView.isFullScreen">
<button class="o_CallActionList_button btn d-flex m-1 border-0 rounded-circle shadow-none opacity-100 opacity-75-hover"
aria-label="Deactivate Full Screen"
title="Deactivate Full Screen"
t-att-class="{ 'o-isSmall p-2': callActionListView.isSmall, 'p-3': !callActionListView.isSmall }"
t-on-click="callActionListView.callView.deactivateFullScreen"
>
<div class="o_CallActionList_buttonIconWrapper fa-stack" t-att-class="{ 'o-isSmall': callActionListView.isSmall }">
<i class="fa fa-compress fa-stack-1x" t-att-class="{ 'fa-lg': !callActionListView.isSmall }"/>
</div>
</button>
</t>
<t t-if="messaging.modelManager.isDebug">
<button class="o_CallActionList_button btn d-flex m-1 border-0 rounded-circle shadow-none opacity-100 opacity-75-hover"
aria-label="More"
title="More"
t-att-class="{ 'o-isSmall p-2': callActionListView.isSmall, 'p-3': !callActionListView.isSmall }"
t-on-click="callActionListView.onClickMore"
t-ref="moreButton"
>
<div class="o_CallActionList_buttonIconWrapper fa-stack" t-att-class="{ 'o-isSmall': callActionListView.isSmall }">
<i class="fa fa-ellipsis-h fa-stack-1x" t-att-class="{ 'fa-lg': !callActionListView.isSmall }"/>
</div>
</button>
</t>
</t>
<t t-if="!callActionListView.thread">
<button class="o_CallActionList_button o_CallActionList_callToggle btn btn-success d-flex m-1 border-0 rounded-circle shadow-none"
t-att-class="{ 'o-isSmall p-2': callActionListView.isSmall, 'p-3': !callActionListView.isSmall }"
aria-label="Join Video Call"
title="Join Video Call"
t-att-disabled="callActionListView.thread.hasPendingRtcRequest"
t-on-click="callActionListView.onClickToggleVideoCall">
<div class="o_CallActionList_buttonIconWrapper fa-stack" t-att-class="{ 'o-isSmall': callActionListView.isSmall }">
<i class="fa fa-video-camera fa-stack-1x" t-att-class="{ 'fa-lg': !callActionListView.isSmall }"/>
</div>
</button>
</t>
<t t-if="callActionListView.thread">
<t t-if="callActionListView.thread.rtcInvitingSession and !callActionListView.thread.rtc">
<button class="o_CallActionList_button o_CallActionList_callToggle o-isActive btn btn-danger d-flex m-1 border-0 rounded-circle shadow-none"
t-att-class="{ 'o-isSmall p-2': callActionListView.isSmall, 'p-3': !callActionListView.isSmall }"
aria-label="Reject"
title="Reject"
t-att-disabled="callActionListView.thread.hasPendingRtcRequest"
t-on-click="callActionListView.onClickRejectCall">
<div class="o_CallActionList_buttonIconWrapper fa-stack" t-att-class="{ 'o-isSmall': callActionListView.isSmall }">
<i class="fa fa-phone fa-stack-1x" t-att-class="{ 'fa-lg': !callActionListView.isSmall }"/>
</div>
</button>
</t>
<button class="o_CallActionList_button o_CallActionList_callToggle btn d-flex m-1 border-0 rounded-circle shadow-none"
t-att-aria-label="callActionListView.callButtonTitle"
t-att-class="{ 'o-isActive btn-danger': !!callActionListView.thread.rtc, 'o-isSmall p-2': callActionListView.isSmall, 'p-3': !callActionListView.isSmall, 'btn-success': !callActionListView.thread.rtc }"
t-att-disabled="callActionListView.thread.hasPendingRtcRequest"
t-att-title="callActionListView.callButtonTitle"
t-on-click="callActionListView.onClickToggleAudioCall">
<div class="o_CallActionList_buttonIconWrapper fa-stack" t-att-class="{ 'o-isSmall': callActionListView.isSmall }">
<i class="fa fa-phone fa-stack-1x" t-att-class="{ 'fa-lg': !callActionListView.isSmall }"/>
</div>
</button>
</t>
</div>
</div>
</t>
</t>
</templates>

View file

@ -0,0 +1,32 @@
/** @odoo-module **/
import { useRefToModel } from '@mail/component_hooks/use_ref_to_model';
import { registerMessagingComponent } from '@mail/utils/messaging_component';
const { Component } = owl;
export class CallDemoView extends Component {
/**
* @override
*/
setup() {
super.setup();
useRefToModel({ fieldName: 'audioRef', refName: 'audio' });
useRefToModel({ fieldName: 'videoRef', refName: 'video' });
}
/**
* @returns {CallDemoView}
*/
get callDemoView() {
return this.props.record;
}
}
Object.assign(CallDemoView, {
props: { record: Object },
template: 'mail.CallDemoView',
});
registerMessagingComponent(CallDemoView);

View file

@ -0,0 +1,18 @@
.o_CallDemoView {
max-width: max-content;
}
.o_CallDemoView_mediaDevicesStatus {
@include o-position-absolute($bottom: 50%);
}
.o_CallDemoView_buttonsContainer {
@include o-position-absolute($bottom: 0);
}
.o_CallDemoView_button {
height: 4rem;
width: 4rem;
font-size: 1.5em;
line-height: 4rem;
}

View file

@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="mail.CallDemoView" owl="1">
<t t-if="callDemoView">
<div class="o_CallDemoView position-relative d-flex justify-content-center" t-attf-class="{{ className }}" t-ref="root">
<video class="o_CallDemoView_videoDisplay shadow rounded bg-dark" height="480" width="640" autoplay="" t-ref="video"/>
<p t-if="callDemoView.doesBrowserSupportMediaDevices and !callDemoView.isVideoEnabled" class="o_CallDemoView_mediaDevicesStatus position-absolute text-light">
Camera is off
</p>
<p t-if="!callDemoView.doesBrowserSupportMediaDevices" class="o_CallDemoView_mediaDevicesStatus text-light">
Your browser does not support videoconference
</p>
<div class="o_CallDemoView_buttonsContainer">
<button t-if="!callDemoView.isMicrophoneEnabled" class="o_CallDemoView_enableMicrophoneButton o_CallDemoView_button btn btn-danger btn-lg rounded-circle p-0 m-3 fa fa-microphone-slash" t-on-click="callDemoView.onClickEnableMicrophoneButton"/>
<button t-if="callDemoView.isMicrophoneEnabled" class="o_CallDemoView_disableMicrophoneButton o_CallDemoView_button btn btn-dark btn-lg p-0 m-3 rounded-circle border-light fa fa-microphone" t-on-click="callDemoView.onClickDisableMicrophoneButton"/>
<button t-if="!callDemoView.isVideoEnabled" class="o_CallDemoView_enableVideoButton o_CallDemoView_button btn btn-danger btn-lg p-0 m-3 rounded-circle fa fa-eye-slash" t-on-click="callDemoView.onClickEnableVideoButton"/>
<button t-if="callDemoView.isVideoEnabled" class="o_CallDemoView_disableVideoButton o_CallDemoView_button btn btn-dark btn-lg p-0 m-3 rounded-circle border-light fa fa-video-camera" t-on-click="callDemoView.onClickDisableVideoButton"/>
</div>
<audio class="o_CallDemoView_audioPlayer" autoplay="" t-ref="audio"/>
</div>
</t>
</t>
</templates>

View file

@ -0,0 +1,27 @@
/** @odoo-module **/
import { registerMessagingComponent } from '@mail/utils/messaging_component';
const { Component } = owl;
export class CallInviteRequestPopup extends Component {
//--------------------------------------------------------------------------
// Getters / Setters
//--------------------------------------------------------------------------
/**
* @returns {CallInviteRequestPopup}
*/
get callInviteRequestPopup() {
return this.props.record;
}
}
Object.assign(CallInviteRequestPopup, {
props: { record: Object },
template: 'mail.CallInviteRequestPopup',
});
registerMessagingComponent(CallInviteRequestPopup);

View file

@ -0,0 +1,24 @@
// ------------------------------------------------------------------
// Layout
// ------------------------------------------------------------------
.o_CallInviteRequestPopup_partnerInfo {
width: 130px;
}
.o_CallInviteRequestPopup_partnerInfoImage {
width: 70%;
height: 70%;
}
// ------------------------------------------------------------------
// Style
// ------------------------------------------------------------------
.o_CallInviteRequestPopup_buttonListButton {
user-select: none;
}
.o_CallInviteRequestPopup_partnerInfoImage {
border: 3px solid gray;
}

View file

@ -0,0 +1,35 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="mail.CallInviteRequestPopup" owl="1">
<t t-if="callInviteRequestPopup">
<div class="o_CallInviteRequestPopup d-flex flex-column m-2 p-5 border border-dark rounded-1 bg-900" t-attf-class="{{ className }}" t-ref="root">
<t t-if="callInviteRequestPopup.thread.rtcInvitingSession">
<div class="o_CallInviteRequestPopup_partnerInfo d-flex flex-column justify-content-around align-items-center text-nowrap">
<img class="o_CallInviteRequestPopup_partnerInfoImage mb-2 rounded-circle cursor-pointer"
t-att-src="callInviteRequestPopup.thread.rtcInvitingSession.channelMember.avatarUrl"
t-on-click="callInviteRequestPopup.onClickAvatar"
alt="Avatar"/>
<span class="o_CallInviteRequestPopup_partnerInfoName w-100 fw-bolder text-truncate text-center overflow-hidden" t-esc="callInviteRequestPopup.thread.rtcInvitingSession.channelMember.persona.name"/>
<span class="o_CallInviteRequestPopup_partnerInfoText fst-italic opacity-75">Incoming Call...</span>
</div>
</t>
<div class="o_CallInviteRequestPopup_buttonList d-flex justify-content-around align-items-center w-100 mt-4">
<button class="o_CallInviteRequestPopup_buttonListButton o_CallInviteRequestPopup_buttonListRefuse p-2 rounded-circle border-0 bg-danger"
aria-label="Refuse"
title="Refuse"
t-on-click="callInviteRequestPopup.onClickRefuse">
<i class="o_CallInviteRequestPopup_buttonListButtonIcon fa fa-lg fa-times m-3"/>
</button>
<button class="o_CallInviteRequestPopup_buttonListButton o_CallInviteRequestPopup_buttonListAccept p-2 rounded-circle border-0 bg-success"
aria-label="Accept"
title="Accept"
t-on-click="callInviteRequestPopup.onClickAccept">
<i class="o_CallInviteRequestPopup_buttonListButtonIcon fa fa-lg fa-phone m-3"/>
</button>
</div>
</div>
</t>
</t>
</templates>

View file

@ -0,0 +1,14 @@
/** @odoo-module **/
import { registerMessagingComponent } from '@mail/utils/messaging_component';
const { Component } = owl;
export class CallInviteRequestPopupList extends Component {}
Object.assign(CallInviteRequestPopupList, {
props: {},
template: 'mail.CallInviteRequestPopupList',
});
registerMessagingComponent(CallInviteRequestPopupList);

View file

@ -0,0 +1,7 @@
// ------------------------------------------------------------------
// Layout
// ------------------------------------------------------------------
.o_CallInviteRequestPopupList {
z-index: $zindex-modal;
}

View file

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="mail.CallInviteRequestPopupList" owl="1">
<div class="o_CallInviteRequestPopupList position-absolute top-0 end-0 d-flex flex-column p-2" t-attf-class="{{ className }}" t-ref="root">
<t t-if="messaging.ringingThreads">
<t t-foreach="messaging.callInviteRequestPopups" t-as="callInviteRequestPopup" t-key="callInviteRequestPopup.localId">
<CallInviteRequestPopup record="callInviteRequestPopup"/>
</t>
</t>
</div>
</t>
</templates>

View file

@ -0,0 +1,44 @@
/** @odoo-module **/
import { useComponentToModel } from '@mail/component_hooks/use_component_to_model';
import { useUpdateToModel } from '@mail/component_hooks/use_update_to_model';
import { useRefToModel } from '@mail/component_hooks/use_ref_to_model';
import { registerMessagingComponent } from '@mail/utils/messaging_component';
const { Component, onMounted, onWillUnmount } = owl;
export class CallMainView extends Component {
/**
* @override
*/
setup() {
super.setup();
useComponentToModel({ fieldName: 'component' });
useRefToModel({ fieldName: 'tileContainerRef', refName: 'tileContainer', });
useUpdateToModel({ methodName: 'onComponentUpdate' });
onMounted(() => {
this.resizeObserver = new ResizeObserver(() => this.callMainView.onResize());
this.resizeObserver.observe(this.root.el);
});
onWillUnmount(() => this.resizeObserver.disconnect());
}
//--------------------------------------------------------------------------
// Getters / Setters
//--------------------------------------------------------------------------
/**
* @returns {CallMainView}
*/
get callMainView() {
return this.props.record;
}
}
Object.assign(CallMainView, {
props: { record: Object },
template: 'mail.CallMainView',
});
registerMessagingComponent(CallMainView);

View file

@ -0,0 +1,34 @@
// ------------------------------------------------------------------
// Layout
// ------------------------------------------------------------------
.o_CallMainView {
min-width: 0;
}
.o_CallMainView_gridParticipantCard {
width: var(--width);
height: var(--height);
min-width: var(--width);
min-height: var(--height);
aspect-ratio: 16/9;
}
.o_CallMainView_sidebarButton {
font-size: 1.2rem;
padding: map-get($spacers, 2);
top: 50%;
transform: translateY(-50%);
right: 0;
}
// ------------------------------------------------------------------
// Style
// ------------------------------------------------------------------
.o_CallMainView_sidebarButton {
color: white;
border-radius: 10px 0px 0px 10px;
border-right: 0px;
background-color: lighten(black, 10%);
}

Some files were not shown because too many files have changed in this diff Show more