mirror of
https://github.com/bringout/oca-ocb-core.git
synced 2026-04-20 06:31:59 +02:00
Initial commit: Core packages
This commit is contained in:
commit
12c29a983b
9512 changed files with 8379910 additions and 0 deletions
BIN
odoo-bringout-oca-ocb-mail/mail/static/src/audio/call_02_in_.mp3
Normal file
BIN
odoo-bringout-oca-ocb-mail/mail/static/src/audio/call_02_in_.mp3
Normal file
Binary file not shown.
BIN
odoo-bringout-oca-ocb-mail/mail/static/src/audio/call_02_in_.ogg
Normal file
BIN
odoo-bringout-oca-ocb-mail/mail/static/src/audio/call_02_in_.ogg
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
odoo-bringout-oca-ocb-mail/mail/static/src/audio/dm_02.mp3
Normal file
BIN
odoo-bringout-oca-ocb-mail/mail/static/src/audio/dm_02.mp3
Normal file
Binary file not shown.
BIN
odoo-bringout-oca-ocb-mail/mail/static/src/audio/dm_02.ogg
Normal file
BIN
odoo-bringout-oca-ocb-mail/mail/static/src/audio/dm_02.ogg
Normal file
Binary file not shown.
BIN
odoo-bringout-oca-ocb-mail/mail/static/src/audio/mute_1.mp3
Normal file
BIN
odoo-bringout-oca-ocb-mail/mail/static/src/audio/mute_1.mp3
Normal file
Binary file not shown.
BIN
odoo-bringout-oca-ocb-mail/mail/static/src/audio/mute_1.ogg
Normal file
BIN
odoo-bringout-oca-ocb-mail/mail/static/src/audio/mute_1.ogg
Normal file
Binary file not shown.
BIN
odoo-bringout-oca-ocb-mail/mail/static/src/audio/ptt_push_1.mp3
Normal file
BIN
odoo-bringout-oca-ocb-mail/mail/static/src/audio/ptt_push_1.mp3
Normal file
Binary file not shown.
BIN
odoo-bringout-oca-ocb-mail/mail/static/src/audio/ptt_push_1.ogg
Normal file
BIN
odoo-bringout-oca-ocb-mail/mail/static/src/audio/ptt_push_1.ogg
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
odoo-bringout-oca-ocb-mail/mail/static/src/audio/share_02.mp3
Normal file
BIN
odoo-bringout-oca-ocb-mail/mail/static/src/audio/share_02.mp3
Normal file
Binary file not shown.
BIN
odoo-bringout-oca-ocb-mail/mail/static/src/audio/share_02.ogg
Normal file
BIN
odoo-bringout-oca-ocb-mail/mail/static/src/audio/share_02.ogg
Normal file
Binary file not shown.
BIN
odoo-bringout-oca-ocb-mail/mail/static/src/audio/ting.mp3
Normal file
BIN
odoo-bringout-oca-ocb-mail/mail/static/src/audio/ting.mp3
Normal file
Binary file not shown.
BIN
odoo-bringout-oca-ocb-mail/mail/static/src/audio/ting.ogg
Normal file
BIN
odoo-bringout-oca-ocb-mail/mail/static/src/audio/ting.ogg
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
odoo-bringout-oca-ocb-mail/mail/static/src/audio/unmute_1.mp3
Normal file
BIN
odoo-bringout-oca-ocb-mail/mail/static/src/audio/unmute_1.mp3
Normal file
Binary file not shown.
BIN
odoo-bringout-oca-ocb-mail/mail/static/src/audio/unmute_1.ogg
Normal file
BIN
odoo-bringout-oca-ocb-mail/mail/static/src/audio/unmute_1.ogg
Normal file
Binary file not shown.
|
|
@ -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);
|
||||
|
|
@ -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>
|
||||
|
|
@ -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);
|
||||
|
|
@ -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>
|
||||
|
|
@ -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);
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
.o_ActivityListView {
|
||||
width: #{"min(95vw, 300px)"};
|
||||
max-height: #{"min(95vh, 350px)"};
|
||||
}
|
||||
|
||||
.o_ActivityListView_activityList {
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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);
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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);
|
||||
|
|
@ -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>
|
||||
|
|
@ -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);
|
||||
|
|
@ -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>
|
||||
|
|
@ -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);
|
||||
|
|
@ -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>
|
||||
|
|
@ -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);
|
||||
|
|
@ -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>
|
||||
|
|
@ -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 });
|
||||
});
|
||||
}
|
||||
|
|
@ -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();
|
||||
});
|
||||
}
|
||||
|
|
@ -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 });
|
||||
});
|
||||
}
|
||||
|
|
@ -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 || {};
|
||||
};
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
|
|
@ -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]();
|
||||
} });
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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);
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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);
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
// ------------------------------------------------------------------
|
||||
// Layout
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
.o_ActivityMarkDonePopoverContent_feedback {
|
||||
min-height: 70px;
|
||||
}
|
||||
|
|
@ -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 & 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>
|
||||
|
|
@ -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',
|
||||
});
|
||||
|
|
@ -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>
|
||||
|
|
@ -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);
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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);
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
// ------------------------------------------------------------------
|
||||
// Style
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
.o_AttachmentBox_dashedLine {
|
||||
border-top: $border-width dashed $border-color;
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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);
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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);
|
||||
|
|
@ -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>
|
||||
|
|
@ -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);
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
// ------------------------------------------------------------------
|
||||
// Layout
|
||||
// ------------------------------------------------------------------
|
||||
.o_AttachmentImage {
|
||||
min-width: 20px;
|
||||
min-height: 20px;
|
||||
|
||||
img {
|
||||
object-fit: contain;
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Style
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
.o_AttachmentImage {
|
||||
cursor: zoom-in;
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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);
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
.o_AttachmentList {
|
||||
max-width: var(--Chatter-max-width, none);
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
// = Attachment Viewer View
|
||||
// ============================================================================
|
||||
// No CSS hacks, variables overrides only
|
||||
|
||||
.o_AttachmentViewer_toolbarButton {
|
||||
--AttachmentViewer_toolbarButton-background-color: #{$o-gray-200};
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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);
|
||||
|
|
@ -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>
|
||||
|
|
@ -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};
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
.o_CallActionList_button:not(.btn-danger) {
|
||||
background-color: var(--CallActionList_button-background-color, #{$o-gray-800});
|
||||
color: #FFFFFF;
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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);
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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);
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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);
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
// ------------------------------------------------------------------
|
||||
// Layout
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
.o_CallInviteRequestPopupList {
|
||||
z-index: $zindex-modal;
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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);
|
||||
|
|
@ -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
Loading…
Add table
Add a link
Reference in a new issue