[IMP] mail: move the activity handler inside a model view

part of task-2579306

closes odoo/odoo#79538

Related: odoo/enterprise#22506
Signed-off-by: Alexandre Kühn (aku) <aku@odoo.com>
Co-authored-by: Sébastien Theys <seb@odoo.com>
This commit is contained in:
Didier (did)
2021-12-20 16:52:47 +00:00
committed by Sébastien Theys
co-authored by Sébastien Theys
parent c53ebf8244
commit 84adbcaffa
23 changed files with 320 additions and 287 deletions
+2 -3
View File
@@ -39,8 +39,7 @@ If you need to manage your meetings, you should install the CRM module.
'auto_install': False,
'assets': {
'web.assets_backend': [
'calendar/static/src/models/activity/activity.js',
'calendar/static/src/components/activity/activity.js',
'calendar/static/src/models/*/*.js',
'calendar/static/src/scss/calendar.scss',
'calendar/static/src/js/base_calendar.js',
'calendar/static/src/js/calendar_renderer.js',
@@ -55,7 +54,7 @@ If you need to manage your meetings, you should install the CRM module.
],
'web.assets_qweb': [
'calendar/static/src/xml/base_calendar.xml',
'calendar/static/src/components/activity/activity.xml',
'calendar/static/src/components/*/*.xml',
],
},
'license': 'LGPL-3',
@@ -1,35 +0,0 @@
/** @odoo-module */
import { Activity } from '@mail/components/activity/activity';
import { patch } from 'web.utils';
import Dialog from 'web.Dialog';
import core from 'web.core';
const _t = core._t;
patch(Activity.prototype, 'calendar/static/src/components/activity/activity.js', {
//--------------------------------------------------------------------------
// Handlers
//--------------------------------------------------------------------------
/**
* Small override that asks for confirmation in case there is a meeting linked to this activity.
*
* @override
*/
_onClickCancel(ev) {
const superMethod = this._super;
if (!this.activity.calendar_event_id){
this._super(ev);
} else {
Dialog.confirm(
this,
_t("The activity is linked to a meeting. Deleting it will remove the meeting as well. Do you want to proceed ?"), {
confirm_callback: function () {
superMethod(ev);
},
}
);
}
}
});
@@ -3,11 +3,11 @@
<t t-inherit="mail.Activity" t-inherit-mode="extension">
<xpath expr="//button[hasclass('o_Activity_editButton')]" position="attributes">
<attribute name="t-if">!activity.calendar_event_id</attribute>
<attribute name="t-if">!activityView.activity.calendar_event_id</attribute>
</xpath>
<xpath expr="//button[hasclass('o_Activity_editButton')]" position="after">
<t t-if="activity.calendar_event_id">
<button class="o_Activity_toolButton o_Activity_editButton btn btn-link" t-on-click="_onClickEdit">
<t t-if="activityView.activity.calendar_event_id">
<button class="o_Activity_toolButton o_Activity_editButton btn btn-link" t-on-click="activityView.onClickEdit">
<i class="fa fa-calendar"/> Reschedule
</button>
</t>
@@ -0,0 +1,28 @@
/** @odoo-module **/
import { patchRecordMethods } from '@mail/model/model_core';
// ensure that the model definition is loaded before the patch
import '@mail/models/activity_view/activity_view';
import Dialog from 'web.Dialog';
patchRecordMethods('ActivityView', {
/**
* @override
*/
async onClickCancel(ev) {
if (this.activity.calendar_event_id) {
await new Promise(resolve => {
Dialog.confirm(
this,
this.env._t("The activity is linked to a meeting. Deleting it will remove the meeting as well. Do you want to proceed?"),
{ confirm_callback: resolve },
);
});
}
if (!this.exists()) {
return;
}
await this._super();
}
});
@@ -1,6 +1,7 @@
/** @odoo-module **/
import { registerMessagingComponent } from '@mail/utils/messaging_component';
import { useRefToModel } from '@mail/component_hooks/use_ref_to_model/use_ref_to_model';
import {
auto_str_to_date,
@@ -8,8 +9,7 @@ import {
getLangDatetimeFormat,
} from 'web.time';
const { Component, useState } = owl;
const { useRef } = owl.hooks;
const { Component } = owl;
export class Activity extends Component {
@@ -18,15 +18,7 @@ export class Activity extends Component {
*/
setup() {
super.setup();
this.state = useState({
areDetailsVisible: false,
});
/**
* Reference of the file uploader.
* Useful to programmatically prompts the browser file uploader.
*/
this._fileUploaderRef = useRef('fileUploader');
this._onAttachmentCreated = this._onAttachmentCreated.bind(this);
useRefToModel({ fieldName: 'fileUploaderRef', modelName: 'ActivityView', propNameAsRecordLocalId: 'activityViewLocalId', refName: 'fileUploader' });
}
//--------------------------------------------------------------------------
@@ -34,17 +26,17 @@ export class Activity extends Component {
//--------------------------------------------------------------------------
/**
* @returns {Activity}
* @returns {ActivityView}
*/
get activity() {
return this.messaging && this.messaging.models['Activity'].get(this.props.activityLocalId);
get activityView() {
return this.messaging && this.messaging.models['ActivityView'].get(this.props.activityViewLocalId);
}
/**
* @returns {string}
*/
get assignedUserText() {
return _.str.sprintf(this.env._t("for %s"), this.activity.assignee.nameOrDisplayName);
return _.str.sprintf(this.env._t("for %s"), this.activityView.activity.assignee.nameOrDisplayName);
}
/**
@@ -52,7 +44,7 @@ export class Activity extends Component {
*/
get delayLabel() {
const today = moment().startOf('day');
const momentDeadlineDate = moment(auto_str_to_date(this.activity.dateDeadline));
const momentDeadlineDate = moment(auto_str_to_date(this.activityView.activity.dateDeadline));
// true means no rounding
const diff = momentDeadlineDate.diff(today, 'days', true);
if (diff === 0) {
@@ -72,7 +64,7 @@ export class Activity extends Component {
* @returns {string}
*/
get formattedCreateDatetime() {
const momentCreateDate = moment(auto_str_to_date(this.activity.dateCreate));
const momentCreateDate = moment(auto_str_to_date(this.activityView.activity.dateCreate));
const datetimeFormat = getLangDatetimeFormat();
return momentCreateDate.format(datetimeFormat);
}
@@ -81,7 +73,7 @@ export class Activity extends Component {
* @returns {string}
*/
get formattedDeadlineDate() {
const momentDeadlineDate = moment(auto_str_to_date(this.activity.dateDeadline));
const momentDeadlineDate = moment(auto_str_to_date(this.activityView.activity.dateDeadline));
const datetimeFormat = getLangDateFormat();
return momentDeadlineDate.format(datetimeFormat);
}
@@ -97,79 +89,14 @@ export class Activity extends Component {
* @returns {string}
*/
get summary() {
return _.str.sprintf(this.env._t("“%s”"), this.activity.summary);
}
//--------------------------------------------------------------------------
// Handlers
//--------------------------------------------------------------------------
/**
* @private
* @param {Object} detail
* @param {Attachment} detail.attachment
*/
_onAttachmentCreated(detail) {
this.activity.markAsDone({ attachments: [detail.attachment] });
}
/**
* @private
* @param {MouseEvent} ev
*/
_onClick(ev) {
if (
ev.target.tagName === 'A' &&
ev.target.dataset.oeId &&
ev.target.dataset.oeModel
) {
this.messaging.openProfile({
id: Number(ev.target.dataset.oeId),
model: ev.target.dataset.oeModel,
});
// avoid following dummy href
ev.preventDefault();
}
}
/**
* @private
* @param {MouseEvent} ev
*/
async _onClickCancel(ev) {
ev.preventDefault();
await this.activity.deleteServerRecord();
this.trigger('reload', { keepChanges: true });
}
/**
* @private
*/
_onClickDetailsButton() {
this.state.areDetailsVisible = !this.state.areDetailsVisible;
}
/**
* @private
* @param {MouseEvent} ev
*/
_onClickEdit(ev) {
this.activity.edit();
}
/**
* @private
* @param {MouseEvent} ev
*/
_onClickUploadDocument(ev) {
this._fileUploaderRef.comp.openBrowserFileUploader();
return _.str.sprintf(this.env._t("“%s”"), this.activityView.activity.summary);
}
}
Object.assign(Activity, {
props: {
activityLocalId: String,
activityViewLocalId: String,
},
template: 'mail.Activity',
});
@@ -2,21 +2,21 @@
<templates xml:space="preserve">
<t t-name="mail.Activity" owl="1">
<div class="o_Activity" t-attf-class="{{ className }}" t-on-click="_onClick" t-ref="root">
<t t-if="activity">
<div class="o_Activity" t-attf-class="{{ className }}" t-on-click="activityView.onClickActivity" t-ref="root">
<t t-if="activityView">
<div class="o_Activity_sidebar">
<div class="o_Activity_user">
<t t-if="activity.assignee">
<img class="o_Activity_userAvatar" t-attf-src="/web/image/res.users/{{ activity.assignee.id }}/avatar_128" t-att-alt="activity.assignee.nameOrDisplayName"/>
<t t-if="activityView.activity.assignee">
<img class="o_Activity_userAvatar" 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"
t-att-class="{
'bg-success': activity.state === 'planned',
'bg-warning': activity.state === 'today',
'bg-danger': activity.state === 'overdue',
'bg-success': activityView.activity.state === 'planned',
'bg-warning': activityView.activity.state === 'today',
'bg-danger': activityView.activity.state === 'overdue',
}"
>
<i class="o_Activity_icon fa" t-attf-class="{{ activity.icon }}"/>
<i class="o_Activity_icon fa" t-attf-class="{{ activityView.activity.icon }}"/>
</div>
</div>
</div>
@@ -24,68 +24,68 @@
<div class="o_Activity_info">
<div class="o_Activity_dueDateText"
t-att-class="{
'o-default': activity.state === 'default',
'o-overdue': activity.state === 'overdue',
'o-planned': activity.state === 'planned',
'o-today': activity.state === 'today',
'o-default': activityView.activity.state === 'default',
'o-overdue': activityView.activity.state === 'overdue',
'o-planned': activityView.activity.state === 'planned',
'o-today': activityView.activity.state === 'today',
}"
>
<t t-esc="delayLabel"/>
</div>
<t t-if="activity.summary">
<t t-if="activityView.activity.summary">
<div class="o_Activity_summary">
<t t-esc="summary"/>
</div>
</t>
<t t-elif="activity.type">
<t t-elif="activityView.activity.type">
<div class="o_Activity_summary o_Activity_type">
<t t-esc="activity.type.displayName"/>
<t t-esc="activityView.activity.type.displayName"/>
</div>
</t>
<t t-if="activity.assignee">
<t t-if="activityView.activity.assignee">
<div class="o_Activity_userName">
<t t-esc="assignedUserText"/>
</div>
</t>
<a class="o_Activity_detailsButton btn btn-link" t-on-click="_onClickDetailsButton" role="button">
<a class="o_Activity_detailsButton btn btn-link" t-on-click="activityView.onClickDetailsButton" role="button">
<i class="fa fa-info-circle" role="img" title="Info"/>
</a>
</div>
<t t-if="state.areDetailsVisible">
<t t-if="activityView.areDetailsVisible">
<div class="o_Activity_details">
<dl class="dl-horizontal">
<t t-if="activity.type">
<t t-if="activityView.activity.type">
<dt>Activity type</dt>
<dd class="o_Activity_type">
<t t-esc="activity.type.displayName"/>
<t t-esc="activityView.activity.type.displayName"/>
</dd>
</t>
<t t-if="activity.creator">
<t t-if="activityView.activity.creator">
<dt>Created</dt>
<dd class="o_Activity_detailsCreation">
<t t-esc="formattedCreateDatetime"/>
<img class="o_Activity_detailsUserAvatar o_Activity_detailsCreatorAvatar" t-attf-src="/web/image/res.users/{{ activity.creator.id }}/avatar_128" t-att-title="activity.creator.nameOrDisplayName" t-att-alt="activity.creator.nameOrDisplayName"/>
<img class="o_Activity_detailsUserAvatar o_Activity_detailsCreatorAvatar" 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"/>
<span class="o_Activity_detailsCreator">
<t t-esc="activity.creator.nameOrDisplayName"/>
<t t-esc="activityView.activity.creator.nameOrDisplayName"/>
</span>
</dd>
</t>
<t t-if="activity.assignee">
<t t-if="activityView.activity.assignee">
<dt>Assigned to</dt>
<dd class="o_Activity_detailsAssignation">
<img class="o_Activity_detailsUserAvatar o_Activity_detailsAssignationUserAvatar" t-attf-src="/web/image/res.users/{{ activity.assignee.id }}/avatar_128" t-att-title="activity.assignee.nameOrDisplayName" t-att-alt="activity.assignee.nameOrDisplayName"/>
<t t-esc="activity.assignee.nameOrDisplayName"/>
<img class="o_Activity_detailsUserAvatar o_Activity_detailsAssignationUserAvatar" 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"/>
<t t-esc="activityView.activity.assignee.nameOrDisplayName"/>
</dd>
</t>
<dt>Due on</dt>
<dd class="o_Activity_detailsDueDate">
<span class="o_Activity_deadlineDateText"
t-att-class="{
'o-default': activity.state === 'default',
'o-overdue': activity.state === 'overdue',
'o-planned': activity.state === 'planned',
'o-today': activity.state === 'today',
'o-default': activityView.activity.state === 'default',
'o-overdue': activityView.activity.state === 'overdue',
'o-planned': activityView.activity.state === 'planned',
'o-today': activityView.activity.state === 'today',
}"
>
<t t-esc="formattedDeadlineDate"/>
@@ -95,51 +95,51 @@
</div>
</t>
<t t-if="activity.note">
<t t-if="activityView.activity.note">
<div class="o_Activity_note">
<t t-raw="activity.note"/>
<t t-raw="activityView.activity.note"/>
</div>
</t>
<t t-if="activity.mailTemplates.length > 0">
<t t-if="activityView.activity.mailTemplates.length > 0">
<div class="o_Activity_mailTemplates">
<t t-foreach="activity.mailTemplates" t-as="mailTemplate" t-key="mailTemplate.localId">
<t t-foreach="activityView.activity.mailTemplates" t-as="mailTemplate" t-key="mailTemplate.localId">
<MailTemplate
className="'o_Activity_mailTemplate'"
activityLocalId="activity.localId"
activityLocalId="activityView.activity.localId"
mailTemplateLocalId="mailTemplate.localId"
/>
</t>
</div>
</t>
<t t-if="activity.canWrite">
<t t-if="activityView.activity.canWrite">
<div name="tools" class="o_Activity_tools">
<t t-if="activity.category !== 'upload_file'">
<t t-if="activityView.activity.category !== 'upload_file'">
<Popover position="'right'" title="MARK_DONE">
<button class="o_Activity_toolButton o_Activity_markDoneButton btn btn-link" t-att-title="MARK_DONE">
<i class="fa fa-check"/> Mark Done
</button>
<t t-set="opened">
<ActivityMarkDonePopover activityLocalId="props.activityLocalId"/>
<ActivityMarkDonePopover activityLocalId="activityView.activity.localId"/>
</t>
</Popover>
</t>
<t t-else="">
<button class="o_Activity_toolButton o_Activity_uploadButton btn btn-link" t-on-click="_onClickUploadDocument">
<button class="o_Activity_toolButton o_Activity_uploadButton btn btn-link" t-on-click="activityView.onClickUploadDocument">
<i class="fa fa-upload"/> Upload Document
</button>
<FileUploader
t-if="activity.thread"
threadLocalId="activity.thread.localId"
onAttachmentCreated="_onAttachmentCreated"
t-if="activityView.activity.thread"
threadLocalId="activityView.activity.thread.localId"
onAttachmentCreated="activityView.onAttachmentCreated"
t-ref="fileUploader"
/>
</t>
<button class="o_Activity_toolButton o_Activity_editButton btn btn-link" t-on-click="_onClickEdit">
<button class="o_Activity_toolButton o_Activity_editButton btn btn-link" t-on-click="activityView.onClickEdit">
<i class="fa fa-pencil"/> Edit
</button>
<button class="o_Activity_toolButton o_Activity_cancelButton btn btn-link" t-on-click="_onClickCancel" >
<button class="o_Activity_toolButton o_Activity_cancelButton btn btn-link" t-on-click="activityView.onClickCancel" >
<i class="fa fa-times"/> Cancel
</button>
</div>
@@ -557,7 +557,7 @@ QUnit.test('activity details layout', async function (assert) {
assert.strictEqual(
document.querySelector('.o_Activity_details .o_Activity_type').textContent,
"Email",
"activity details type should be 'Fake type'"
"activity details type should be 'Email'"
);
assert.strictEqual(
document.querySelectorAll('.o_Activity_detailsCreation').length,
@@ -14,7 +14,7 @@ export class ActivityBox extends Component {
* @returns {Chatter}
*/
get activityBoxView() {
return this.messaging.models['ActivityBoxView'].get(this.props.activityBoxViewLocalId);
return this.messaging && this.messaging.models['ActivityBoxView'].get(this.props.activityBoxViewLocalId);
}
}
@@ -33,8 +33,8 @@
</a>
<t t-if="activityBoxView.isActivityListVisible">
<div class="o_ActivityBox_activityList">
<t t-foreach="activityBoxView.chatter.thread.activities" t-as="activity" t-key="activity.localId">
<Activity className="'o_ActivityBox_activity'" activityLocalId="activity.localId"/>
<t t-foreach="activityBoxView.activityViews" t-as="activityView" t-key="activityView.localId">
<Activity className="'o_ActivityBox_activity'" activityViewLocalId="activityView.localId"/>
</t>
</div>
</t>
@@ -1,6 +1,7 @@
/** @odoo-module **/
import { registerMessagingComponent } from '@mail/utils/messaging_component';
import { useComponentToModel } from '@mail/component_hooks/use_component_to_model/use_component_to_model';
import { useUpdate } from '@mail/component_hooks/use_update/use_update';
import { useRefToModel } from '@mail/component_hooks/use_ref_to_model/use_ref_to_model';
@@ -15,6 +16,7 @@ export class Chatter extends Component {
setup() {
super.setup();
useUpdate({ func: () => this._update() });
useComponentToModel({ fieldName: 'component', modelName: 'Chatter', propNameAsRecordLocalId: 'chatterLocalId' });
useRefToModel({ fieldName: 'threadRef', modelName: 'Chatter', propNameAsRecordLocalId: 'chatterLocalId', refName: 'thread' });
/**
* Reference of the scroll Panel (Real scroll element). Useful to pass the Scroll element to
@@ -1,24 +1,11 @@
/** @odoo-module **/
import { registerMessagingComponent } from '@mail/utils/messaging_component';
import { useComponentToModel } from '@mail/component_hooks/use_component_to_model/use_component_to_model';
const { Component } = owl;
export class ChatterTopbar extends Component {
/**
* @override
*/
setup() {
super.setup();
useComponentToModel({ fieldName: 'componentChatterTopbar', modelName: 'Chatter', propNameAsRecordLocalId: 'chatterLocalId' });
}
//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
/**
* @returns {Chatter}
*/
@@ -3,20 +3,22 @@
<t t-name="mail.FollowButton" owl="1">
<div class="o_FollowButton" t-attf-class="{{ className }}" t-ref="root">
<t t-if="thread.isCurrentPartnerFollowing">
<button class="o_FollowButton_unfollow btn btn-link" t-att-class="{ 'o-following': !state.isUnfollowButtonHighlighted, 'o-unfollow': state.isUnfollowButtonHighlighted }" t-att-disabled="props.isDisabled" t-on-click="_onClickUnfollow" t-on-mouseenter="_onMouseEnterUnfollow" t-on-mouseleave="_onMouseLeaveUnfollow">
<t t-if="state.isUnfollowButtonHighlighted">
<i class="fa fa-times"/> Unfollow
</t>
<t t-else="">
<i class="fa fa-check"/> Following
</t>
</button>
</t>
<t t-else="">
<button class="o_FollowButton_follow btn btn-link" t-att-disabled="props.isDisabled" t-on-click="_onClickFollow">
Follow
</button>
<t t-if="thread">
<t t-if="thread.isCurrentPartnerFollowing">
<button class="o_FollowButton_unfollow btn btn-link" t-att-class="{ 'o-following': !state.isUnfollowButtonHighlighted, 'o-unfollow': state.isUnfollowButtonHighlighted }" t-att-disabled="props.isDisabled" t-on-click="_onClickUnfollow" t-on-mouseenter="_onMouseEnterUnfollow" t-on-mouseleave="_onMouseLeaveUnfollow">
<t t-if="state.isUnfollowButtonHighlighted">
<i class="fa fa-times"/> Unfollow
</t>
<t t-else="">
<i class="fa fa-check"/> Following
</t>
</button>
</t>
<t t-else="">
<button class="o_FollowButton_follow btn btn-link" t-att-disabled="props.isDisabled" t-on-click="_onClickFollow">
Follow
</button>
</t>
</t>
</div>
</t>
@@ -3,32 +3,34 @@
<t t-name="mail.FollowerListMenu" owl="1">
<div class="o_FollowerListMenu" t-attf-class="{{ className }}" t-on-keydown="_onKeydown" t-ref="root">
<div class="o_FollowerListMenu_followers" t-ref="dropdown">
<button class="o_FollowerListMenu_buttonFollowers btn btn-link" t-att-disabled="props.isDisabled" t-on-click="_onClickFollowersButton" title="Show Followers">
<i class="fa fa-user"/>
<span class="o_FollowerListMenu_buttonFollowersCount pl-1" t-esc="thread.followers.length"/>
</button>
<t t-if="thread">
<div class="o_FollowerListMenu_followers" t-ref="dropdown">
<button class="o_FollowerListMenu_buttonFollowers btn btn-link" t-att-disabled="props.isDisabled" t-on-click="_onClickFollowersButton" title="Show Followers">
<i class="fa fa-user"/>
<span class="o_FollowerListMenu_buttonFollowersCount pl-1" t-esc="thread.followers.length"/>
</button>
<t t-if="state.isDropdownOpen">
<div class="o_FollowerListMenu_dropdown dropdown-menu dropdown-menu-right" role="menu">
<t t-if="thread.channel_type !== 'chat'">
<a class="o_FollowerListMenu_addFollowersButton dropdown-item" href="#" role="menuitem" t-on-click="_onClickAddFollowers">
Add Followers
</a>
</t>
<t t-if="thread.followers.length > 0">
<div role="separator" class="dropdown-divider"/>
<t t-foreach="thread.followers" t-as="follower" t-key="follower.localId">
<Follower
className="'o_FollowerMenu_follower dropdown-item'"
followerLocalId="follower.localId"
onClick="_onClickFollower"
/>
<t t-if="state.isDropdownOpen">
<div class="o_FollowerListMenu_dropdown dropdown-menu dropdown-menu-right" role="menu">
<t t-if="thread.channel_type !== 'chat'">
<a class="o_FollowerListMenu_addFollowersButton dropdown-item" href="#" role="menuitem" t-on-click="_onClickAddFollowers">
Add Followers
</a>
</t>
</t>
</div>
</t>
</div>
<t t-if="thread.followers.length > 0">
<div role="separator" class="dropdown-divider"/>
<t t-foreach="thread.followers" t-as="follower" t-key="follower.localId">
<Follower
className="'o_FollowerMenu_follower dropdown-item'"
followerLocalId="follower.localId"
onClick="_onClickFollower"
/>
</t>
</t>
</div>
</t>
</div>
</t>
</div>
</t>
+2 -1
View File
@@ -65,7 +65,7 @@ export function addLifecycleHooks(modelName, hooks) {
assertIsFunction(handler);
assertIsValidHookName(name);
assertSectionDoesNotHaveKey('lifecycleHooks', name, definition);
}, `Cannot add lifecycle hook "${name}" to model "${modelName}":`);
}, `Cannot add lifecycle hook "${name}" to model "${modelName}": `);
definition.get('lifecycleHooks').set(name, handler);
}
}
@@ -281,6 +281,7 @@ export function patchLifecycleHooks(modelName, hooks) {
for (const [name, handler] of Object.entries(hooks)) {
addContextToErrors(() => {
assertIsFunction(handler);
assertIsValidHookName(name);
assertSectionHasKey('lifecycleHooks', name, definition);
}, `Cannot patch lifecycle hook "${name}" on model "${modelName}": `);
const hookBeforePatch = definition.get('lifecycleHooks').get(name);
@@ -1,7 +1,7 @@
/** @odoo-module **/
import { registerModel } from '@mail/model/model_core';
import { attr, many2many, many2one } from '@mail/model/model_field';
import { attr, many2many, many2one, one2many } from '@mail/model/model_field';
import { clear, insert, unlink, unlinkAll } from '@mail/model/model_field_command';
registerModel({
@@ -232,6 +232,10 @@ registerModel({
},
},
fields: {
activityViews: one2many('ActivityView', {
inverse: 'activity',
isCausal: true,
}),
assignee: many2one('User'),
attachments: many2many('Attachment', {
inverse: 'activities',
@@ -1,7 +1,8 @@
/** @odoo-module **/
import { registerModel } from '@mail/model/model_core';
import { attr, one2one } from '@mail/model/model_field';
import { attr, one2many, one2one } from '@mail/model/model_field';
import { insertAndReplace, replace } from '@mail/model/model_field_command';
registerModel({
name: 'ActivityBoxView',
@@ -19,8 +20,22 @@ registerModel({
onClickActivityBoxTitle() {
this.update({ isActivityListVisible: !this.isActivityListVisible });
},
/**
* @private
* @returns {FieldCommand}
*/
_computeActivityViews() {
return insertAndReplace(this.chatter.thread.activities.map(activity => {
return { activity: replace(activity) };
}));
},
},
fields: {
activityViews: one2many('ActivityView', {
compute: '_computeActivityViews',
inverse: 'activityBoxView',
isCausal: true,
}),
chatter: one2one('Chatter', {
inverse: 'activityBoxView',
readonly: true,
@@ -29,6 +44,6 @@ registerModel({
isActivityListVisible: attr({
default: true,
}),
},
},
});
@@ -0,0 +1,96 @@
/** @odoo-module **/
import { registerModel } from '@mail/model/model_core';
import { attr, many2one } from '@mail/model/model_field';
registerModel({
name: 'ActivityView',
identifyingFields: ['activityBoxView', 'activity'],
lifecycleHooks: {
_created() {
this.onAttachmentCreated = this.onAttachmentCreated.bind(this);
this.onClickActivity = this.onClickActivity.bind(this);
this.onClickCancel = this.onClickCancel.bind(this);
this.onClickDetailsButton = this.onClickDetailsButton.bind(this);
this.onClickEdit = this.onClickEdit.bind(this);
this.onClickUploadDocument = this.onClickUploadDocument.bind(this);
}
},
recordMethods: {
/**
* @param {Object} detail
* @param {Attachment} detail.attachment
*/
onAttachmentCreated(detail) {
this.activity.markAsDone({ attachments: [detail.attachment] });
},
/**
* Handles the click on a link inside the activity.
*
* @param {MouseEvent} ev
*/
onClickActivity(ev) {
if (
ev.target.tagName === 'A' &&
ev.target.dataset.oeId &&
ev.target.dataset.oeModel
) {
this.messaging.openProfile({
id: Number(ev.target.dataset.oeId),
model: ev.target.dataset.oeModel,
});
// avoid following dummy href
ev.preventDefault();
}
},
/**
* Handles the click on the cancel button
*/
async onClickCancel() {
const { chatter } = this.activityBoxView; // save value before deleting activity
await this.activity.deleteServerRecord();
chatter.reloadParentView();
},
/**
* Handles the click on the detail button
*/
onClickDetailsButton() {
this.update({ areDetailsVisible: !this.areDetailsVisible });
},
/**
* Handles the click on the edit button
*/
onClickEdit() {
this.activity.edit();
},
/**
* Handles the click on the upload document button. This open the file
* explorer for upload.
*/
onClickUploadDocument() {
this.fileUploaderRef.comp.openBrowserFileUploader();
},
},
fields: {
activity: many2one('Activity', {
inverse: 'activityViews',
required: true,
readonly: true,
}),
activityBoxView: many2one('ActivityBoxView', {
inverse: 'activityViews',
readonly: true,
required: true,
}),
/**
* Determines whether the details are visible.
*/
areDetailsVisible: attr({
default: false,
}),
/**
* States the OWL FileUploader component of this activity view.
*/
fileUploaderRef: attr(),
},
});
@@ -60,7 +60,7 @@ registerModel({
* @param {MouseEvent} ev
*/
onClickChatterTopbarClose(ev) {
this.componentChatterTopbar.trigger('o-close-chatter');
this.component.trigger('o-close-chatter');
},
/**
* Handles click on "log note" button.
@@ -96,12 +96,7 @@ registerModel({
return this.env.bus.trigger('do-action', {
action,
options: {
on_close: () => {
if (!this.componentChatterTopbar) {
return;
}
this.componentChatterTopbar.trigger('reload', { keepChanges: true });
},
on_close: () => this.reloadParentView(),
},
});
},
@@ -140,6 +135,11 @@ registerModel({
this.thread.refresh();
}
},
reloadParentView() {
if (this.component) {
this.component.trigger('reload', { keepChanges: true });
}
},
showLogNote() {
this.update({ composerView: insertAndReplace() });
this.composerView.composer.update({ isLog: true });
@@ -296,9 +296,9 @@ registerModel({
readonly: true,
}),
/**
* States the OWL component of this chatter top bar.
* States the OWL Chatter component of this chatter.
*/
componentChatterTopbar: attr(),
component: attr(),
/**
* Determines the composer view used to post in this chatter (if any).
*/
+3 -3
View File
@@ -67,9 +67,9 @@ Featuring
'web.assets_backend': [
'website_slides/static/src/scss/rating_rating_views.scss',
'website_slides/static/src/scss/slide_views.scss',
'website_slides/static/src/components/activity/activity.js',
'website_slides/static/src/js/slide_category_one2many.js',
'website_slides/static/src/js/rating_field_backend.js',
'website_slides/static/src/models/*/*.js',
],
'web.assets_frontend': [
'website_slides/static/src/scss/website_slides.scss',
@@ -114,10 +114,10 @@ Featuring
'website_slides/static/src/tests/helpers/mock_models.js',
],
'web.qunit_suite_tests': [
'website_slides/static/src/components/activity/activity_tests.js',
'website_slides/static/src/components/*/tests/*.js',
],
'web.assets_qweb': [
'website_slides/static/src/components/activity/activity.xml',
'website_slides/static/src/components/*/*.xml',
],
},
'license': 'LGPL-3',
@@ -1,43 +0,0 @@
/** @odoo-module **/
import { Activity } from '@mail/components/activity/activity';
import { patch } from 'web.utils';
patch(Activity.prototype, 'website_slides/static/src/components/activity/activity.js', {
//--------------------------------------------------------------------------
// Handlers
//--------------------------------------------------------------------------
/**
* @private
*/
async _onGrantAccess(ev) {
await this.env.services.rpc({
model: 'slide.channel',
method: 'action_grant_access',
args: [[this.activity.thread.id]],
kwargs: { partner_id: this.activity.requestingPartner.id },
});
if (this.activity) {
this.activity.delete();
}
this.trigger('reload', { keepChanges: true });
},
/**
* @private
*/
async _onRefuseAccess(ev) {
await this.env.services.rpc({
model: 'slide.channel',
method: 'action_refuse_access',
args: [[this.activity.thread.id]],
kwargs: { partner_id: this.activity.requestingPartner.id },
});
if (this.activity) {
this.activity.delete();
}
this.trigger('reload', { keepChanges: true });
},
});
@@ -2,12 +2,12 @@
<templates xml:space="preserve">
<t t-inherit="mail.Activity" t-inherit-mode="extension">
<xpath expr="//*[@name='tools']" position="replace">
<t t-if="activity.requestingPartner and activity.thread.model === 'slide.channel'">
<t t-if="activityView.activity.requestingPartner and activityView.activity.thread.model === 'slide.channel'">
<div class="o_Activity_tools">
<button class="o_Activity_toolButton o_Activity_grantAccessButton btn btn-link" t-on-click="_onGrantAccess">
<button class="o_Activity_toolButton o_Activity_grantAccessButton btn btn-link" t-on-click="activityView.onGrantAccess">
<i class="fa fa-check"/> Grant Access
</button>
<button class="o_Activity_toolButton o_Activity_refuseAccessButton btn btn-link" t-on-click="_onRefuseAccess">
<button class="o_Activity_toolButton o_Activity_refuseAccessButton btn btn-link" t-on-click="activityView.onRefuseAccess">
<i class="fa fa-times"/> Refuse Access
</button>
</div>
@@ -0,0 +1,48 @@
/** @odoo-module **/
import { addRecordMethods, patchLifecycleHooks } from '@mail/model/model_core';
// ensure that the model definition is loaded before the patch
import '@mail/models/activity_view/activity_view';
patchLifecycleHooks('ActivityView', {
_created() {
this._super();
this.onGrantAccess = this.onGrantAccess.bind(this);
this.onRefuseAccess = this.onRefuseAccess.bind(this);
},
});
addRecordMethods('ActivityView', {
/**
* Handles click on the "grant access" button.
*/
async onGrantAccess(ev) {
const { chatter } = this.activityBoxView; // save value before deleting activity
await this.env.services.rpc({
model: 'slide.channel',
method: 'action_grant_access',
args: [[this.activity.thread.id]],
kwargs: { partner_id: this.activity.requestingPartner.id },
});
if (this.activity) {
this.activity.delete();
}
chatter.reloadParentView();
},
/**
* Handles click on the "refuse access" button.
*/
async onRefuseAccess(ev) {
const { chatter } = this.activityBoxView; // save value before deleting activity
await this.env.services.rpc({
model: 'slide.channel',
method: 'action_refuse_access',
args: [[this.activity.thread.id]],
kwargs: { partner_id: this.activity.requestingPartner.id },
});
if (this.activity) {
this.activity.delete();
}
chatter.reloadParentView();
},
});