[IMP] mail, *: remove legacy code from activity systray

* = calendar, crm, im_livechat, mass_mailing_sms, note, test_discuss_full,
    test_mail

task-2920645

closes odoo/odoo#96114

Related: odoo/enterprise#29794
Signed-off-by: Alexandre Kühn (aku) <aku@odoo.com>
This commit is contained in:
Sébastien Theys
2022-07-27 02:18:15 +02:00
parent 0099b8ee36
commit b401ba6e2d
52 changed files with 1038 additions and 1022 deletions
-1
View File
@@ -47,7 +47,6 @@ If you need to manage your meetings, you should install the CRM module.
'calendar/static/src/js/calendar_controller.js',
'calendar/static/src/js/calendar_model.js',
'calendar/static/src/js/calendar_view.js',
'calendar/static/src/js/systray_activity_menu.js',
'calendar/static/src/js/services/calendar_notification_service.js',
],
'web.qunit_suite_tests': [
+2 -1
View File
@@ -44,11 +44,12 @@ class Users(models.Model):
if meetings_lines:
meeting_label = _("Today's Meetings")
meetings_systray = {
'id': self.env['ir.model']._get('calendar.event').id,
'type': 'meeting',
'name': meeting_label,
'model': 'calendar.event',
'icon': modules.module.get_module_icon(self.env['calendar.event']._original_module),
'meetings': meetings_lines,
'meetings': [('insert-and-replace', meetings_lines)],
}
res.insert(0, meetings_systray)
@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-inherit="mail.ActivityMenuView" t-inherit-mode="extension">
<xpath expr="//*[hasclass('o_ActivityMenuView_activityGroupTitle')]" position="after">
<div t-if="activityGroupView.activityGroup.type == 'meeting'">
<t t-set="is_next_meeting" t-value="true"/>
<t t-foreach="activityGroupView.activityGroup.meetings" t-as="meeting" t-key="meeting.localId">
<div class="d-flex">
<span class="flex-grow-1" t-att-class="!meeting.allday and is_next_meeting ? 'o_meeting_filter o_meeting_bold' : 'o_meeting_filter'" t-att-data-res_model="activityGroupView.activityGroup.irModel.model" t-att-data-res_id="meeting.id" t-att-data-model_name="activityGroupView.activityGroup.irModel.name" t-att-title="meeting.name">
<span><t t-esc="meeting.name"/></span>
</span>
<span t-if="meeting.formattedStart">
<t t-if="meeting.allday">All Day</t>
<t t-else=''>
<t t-set="is_next_meeting" t-value="false"/>
<t t-esc="meeting.formattedStart"/>
</t>
</span>
</div>
</t>
</div>
</xpath>
</t>
</templates>
@@ -1,53 +0,0 @@
/** @odoo-module **/
import ActivityMenu from '@mail/js/systray/systray_activity_menu';
import fieldUtils from 'web.field_utils';
ActivityMenu.include({
//-----------------------------------------
// Private
//-----------------------------------------
/**
* parse date to server value
*
* @private
* @override
*/
_getActivityData: function () {
var self = this;
return this._super.apply(this, arguments).then(function () {
var meeting = _.find(self._activities, {type: 'meeting'});
if (meeting && meeting.meetings) {
_.each(meeting.meetings, function (res) {
res.start = fieldUtils.parse.datetime(res.start, false, {isUTC: true});
});
}
});
},
//-----------------------------------------
// Handlers
//-----------------------------------------
/**
* @private
* @override
*/
_onActivityFilterClick: function (ev) {
var $el = $(ev.currentTarget);
var data = _.extend({}, $el.data());
if (data.res_model === "calendar.event" && data.filter === "my") {
this.do_action('calendar.action_calendar_event', {
additional_context: {
default_mode: 'day',
search_default_mymeetings: 1,
},
clear_breadcrumbs: true,
});
} else {
this._super.apply(this, arguments);
}
},
});
@@ -0,0 +1,40 @@
/** @odoo-module **/
import { addFields, addOnChanges, addRecordMethods, patchModelMethods } from '@mail/model/model_core';
import { many } from '@mail/model/model_field';
import { OnChange } from '@mail/model/model_onchange';
// ensure that the model definition is loaded before the patch
import '@mail/models/activity_group';
patchModelMethods('ActivityGroup', {
/**
* @override
*/
convertData(data) {
const data2 = this._super(data);
data2.meetings = data.meetings;
return data2;
},
});
addRecordMethods('ActivityGroup', {
/**
* @private
*/
_onChangeMeetings() {
if (this.type === 'meeting' && this.meetings.length === 0) {
this.delete();
}
},
});
addFields('ActivityGroup', {
meetings: many('calendar.event'),
});
addOnChanges('ActivityGroup', [
new OnChange({
dependencies: ['meetings', 'type'],
methodName: '_onChangeMeetings',
}),
]);
@@ -0,0 +1,27 @@
/** @odoo-module **/
import { patchRecordMethods } from '@mail/model/model_core';
// ensure that the model definition is loaded before the patch
import '@mail/models/activity_group_view';
patchRecordMethods('ActivityGroupView', {
/**
* @override
*/
onClickFilterButton(ev) {
const $el = $(ev.currentTarget);
const data = _.extend({}, $el.data());
if (data.res_model === "calendar.event" && data.filter === "my") {
this.activityMenuViewOwner.update({ isOpen: false });
this.env.services['action'].doAction('calendar.action_calendar_event', {
additionalContext: {
default_mode: 'day',
search_default_mymeetings: 1,
},
clearBreadcrumbs: true,
});
} else {
this._super.apply(this, arguments);
}
},
});
@@ -0,0 +1,34 @@
/** @odoo-module **/
import { registerModel } from '@mail/model/model_core';
import { attr } from '@mail/model/model_field';
import { clear } from '@mail/model/model_field_command';
import fieldUtils from 'web.field_utils';
import { getLangTimeFormat } from 'web.time';
registerModel({
name: 'calendar.event',
identifyingFields: ['id'],
recordMethods: {
_computeFormattedStart() {
if (!this.start) {
return clear();
}
return moment(fieldUtils.parse.datetime(this.start, false, { isUTC: true })).local().format(getLangTimeFormat());
},
},
fields: {
allday: attr(),
attendee_status: attr(),
formattedStart: attr({
compute: '_computeFormattedStart',
}),
id: attr({
readonly: true,
required: true,
}),
name: attr(),
start: attr(),
},
});
@@ -57,7 +57,7 @@
}
.o_meeting_filter {
@include o-text-overflow();
width: 64%;
min-width: 0;
color: grey;
vertical-align: top;
&.o_meeting_bold {
@@ -25,28 +25,6 @@
</div>
<t t-extend="mail.systray.ActivityMenu.Previews">
<t t-jquery="div.o_preview_title" t-operation="after">
<div t-if="activity and activity.type == 'meeting'">
<t t-set="is_next_meeting" t-value="true"/>
<t t-foreach="activity.meetings" t-as="meeting">
<div>
<span t-att-class="!meeting.allday and is_next_meeting ? 'o_meeting_filter o_meeting_bold' : 'o_meeting_filter'" t-att-data-res_model="activity.model" t-att-data-res_id="meeting.id" t-att-data-model_name="activity.name" t-att-title="meeting.name">
<span><t t-esc="meeting.name"/></span>
</span>
<span t-if="meeting.start" class="float-end">
<t t-if="meeting.allday">All Day</t>
<t t-else=''>
<t t-set="is_next_meeting" t-value="false"/>
<t t-esc="moment(meeting.start).local().format(Time.strftime_to_moment_format(_t.database.parameters.time_format))"/>
</t>
</span>
</div>
</t>
</div>
</t>
</t>
<t t-name="Calendar.attendee.status.popover" t-extend="CalendarView.event.popover">
<t t-jquery=".o_cw_popover_delete" t-operation="after">
<div t-if="widget.displayAttendeeAnswerChoice()" class="btn-group o-calendar-attendee-status ms-2">
@@ -81,7 +81,8 @@ patch(MockServer.prototype, 'calendar', {
).filter(meetingLine => meetingLine['attendee_status'] !== 'declined');
if (meetingsLines.length) {
activities.unshift({
meetings: meetingsLines,
id: 'calendar.event', // for simplicity
meetings: [['insert-and-replace', meetingsLines]],
model: 'calendar.event',
name: 'Today\'s Meetings',
type: 'meeting',
@@ -2,7 +2,6 @@
import { start, startServer } from '@mail/../tests/helpers/test_utils';
import testUtils from 'web.test_utils';
import { patchDate, patchWithCleanup } from "@web/../tests/helpers/utils";
QUnit.module('calendar', {}, function () {
@@ -28,21 +27,22 @@ QUnit.test('activity menu widget:today meetings', async function (assert) {
attendee_ids: [calendarAttendeeId1],
},
]);
const { env } = await start();
assert.containsOnce(document.body, '.o_mail_systray_item', 'should contain an instance of widget');
const { click, env } = await start();
assert.containsOnce(document.body, '.o_ActivityMenuView', 'should contain an instance of widget');
await testUtils.dom.click(document.querySelector('.dropdown-toggle[title="Activities"]'));
await click('.dropdown-toggle[title="Activities"]');
patchWithCleanup(env.services.action, {
doAction(action) {
assert.strictEqual(action, "calendar.action_calendar_event", 'should open meeting calendar view in day mode');
},
});
await testUtils.dom.click(document.querySelector('.o_mail_preview'));
assert.ok(document.querySelector('.o_meeting_filter'), "should be a meeting");
assert.containsN(document.body, '.o_meeting_filter', 2, 'there should be 2 meetings');
assert.hasClass(document.querySelector('.o_meeting_filter'), 'o_meeting_bold', 'this meeting is yet to start');
assert.doesNotHaveClass(document.querySelectorAll('.o_meeting_filter')[1], 'o_meeting_bold', 'this meeting has been started');
await click('.o_ActivityMenuView_activityGroup');
});
});
+3 -1
View File
@@ -70,6 +70,9 @@
'installable': True,
'application': True,
'assets': {
'mail.assets_messaging': [
'crm/static/src/models/*.js',
],
'web.assets_qweb': [
'crm/static/src/xml/crm_kanban.xml',
'crm/static/src/xml/forecast_kanban.xml',
@@ -78,7 +81,6 @@
'crm/static/src/js/crm_form.js',
'crm/static/src/js/crm_kanban.js',
'crm/static/src/js/forecast/*',
'crm/static/src/js/systray_activity_menu.js',
'crm/static/src/js/tours/crm.js',
'crm/static/src/scss/crm.scss',
'crm/static/src/scss/crm_team_member_views.scss',
@@ -1,55 +0,0 @@
/** @odoo-module **/
import ActivityMenu from '@mail/js/systray/systray_activity_menu';
ActivityMenu.include({
//--------------------------------------------------
// Private
//--------------------------------------------------
/**
* @override
*/
_getViewsList(model) {
if (model === "crm.lead") {
return [[false, 'list'], [false, 'kanban'],
[false, 'form'], [false, 'calendar'],
[false, 'pivot'], [false, 'graph'],
[false, 'activity']
];
}
return this._super(...arguments);
},
//-----------------------------------------
// Handlers
//-----------------------------------------
/**
* @private
* @override
*/
_onActivityFilterClick: function (event) {
// fetch the data from the button otherwise fetch the ones from the parent (.o_mail_preview).
var data = _.extend({}, $(event.currentTarget).data(), $(event.target).data());
var context = {};
if (data.res_model === "crm.lead") {
if (data.filter === 'my') {
context['search_default_activities_overdue'] = 1;
context['search_default_activities_today'] = 1;
} else {
context['search_default_activities_' + data.filter] = 1;
}
// Necessary because activity_ids of mail.activity.mixin has auto_join
// So, duplicates are faking the count and "Load more" doesn't show up
context['force_search_count'] = 1;
this.do_action('crm.crm_lead_action_my_activities', {
additional_context: context,
clear_breadcrumbs: true,
});
} else {
this._super.apply(this, arguments);
}
},
});
@@ -0,0 +1,34 @@
/** @odoo-module **/
import { patchRecordMethods } from '@mail/model/model_core';
// ensure that the model definition is loaded before the patch
import '@mail/models/activity_group_view';
patchRecordMethods('ActivityGroupView', {
/**
* @override
*/
onClickFilterButton(ev) {
// fetch the data from the button otherwise fetch the ones from the parent (.o_ActivityMenuView_activityGroup).
var data = _.extend({}, $(ev.currentTarget).data(), $(ev.target).data());
var context = {};
if (data.res_model === "crm.lead") {
this.activityMenuViewOwner.update({ isOpen: false });
if (data.filter === 'my') {
context['search_default_activities_overdue'] = 1;
context['search_default_activities_today'] = 1;
} else {
context['search_default_activities_' + data.filter] = 1;
}
// Necessary because activity_ids of mail.activity.mixin has auto_join
// So, duplicates are faking the count and "Load more" doesn't show up
context['force_search_count'] = 1;
this.env.services['action'].doAction('crm.crm_lead_action_my_activities', {
additionalContext: context,
clearBreadcrumbs: true,
});
} else {
this._super.apply(this, arguments);
}
},
});
+24
View File
@@ -0,0 +1,24 @@
/** @odoo-module **/
import { patchRecordMethods } from "@mail/model/model_core";
import "@mail/models/ir_model"; // ensure the model definition is loaded before the patch
patchRecordMethods("ir.model", {
/**
* @override
*/
_computeAvailableWebViews() {
if (this.model === "crm.lead") {
return [
'list',
'kanban',
'form',
'calendar',
'pivot',
'graph',
'activity',
];
}
return this._super();
},
});
@@ -96,7 +96,7 @@ $o-mail-thread-window-zindex: $zindex-modal + 1 !default;
.o_thread_window_buttons {
flex: 0 0 auto;
.o_thread_window_close, .o_thread_window_expand {
.o_thread_window_close {
color: white;
padding: 0px 3px;
margin-left: 5px;
@@ -161,7 +161,7 @@ $o-mail-thread-window-zindex: $zindex-modal + 1 !default;
font-size: smaller;
}
}
&.show .o_thread_window_dropdown_toggler .o_total_unread_counter, .o_thread_window_expand {
&.show .o_thread_window_dropdown_toggler .o_total_unread_counter {
display: none;
}
@@ -193,10 +193,6 @@ $o-mail-thread-window-zindex: $zindex-modal + 1 !default;
z-index: 1101; // blockUI's z-index is 1100
}
.o_no_thread_window .o_thread_window {
display: none;
}
//-------------------------------------
// legacy: mail/thread.scss
//-------------------------------------
+1
View File
@@ -226,6 +226,7 @@ class Users(models.Model):
module = self.env[activity['model']]._original_module
icon = module and modules.module.get_module_icon(module)
user_activities[activity['model']] = {
'id': activity['id'],
'name': model_names[activity['id']],
'model': activity['model'],
'type': 'activity',
@@ -1,26 +1,27 @@
/** @odoo-module **/
import ActivityMenu from '@mail/js/systray/systray_activity_menu';
import { ComponentAdapter } from "web.OwlCompatibility";
// ensure components are registered beforehand.
import '@mail/components/activity_menu_view/activity_menu_view';
import { getMessagingComponent } from "@mail/utils/messaging_component";
const { Component } = owl;
class ActivityMenuAdapter extends ComponentAdapter {
setup() {
this.env = owl.Component.env;
super.setup();
}
}
export class ActivityMenuContainer extends Component {
get activityMenuWidget() {
return ActivityMenu;
}
}
/**
* @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: { ActivityMenuAdapter },
components: { ActivityMenuView: getMessagingComponent('ActivityMenuView') },
template: 'mail.ActivityMenuContainer',
});
@@ -1,6 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="mail.ActivityMenuContainer" owl="1">
<ActivityMenuAdapter Component="activityMenuWidget"/>
<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,137 @@
.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;
color: grey;
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" 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">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">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">0 Future</span>
</div>
</div>
</div>
</t>
</div>
</div>
</div>
</t>
</templates>
@@ -1,199 +0,0 @@
/** @odoo-module **/
import { qweb as QWeb } from 'web.core';
import config from "web.config";
import session from 'web.session';
import Widget from 'web.Widget';
import Time from 'web.time';
const { Component } = owl;
/**
* Menu item appended in the systray part of the navbar, redirects to the next
* activities of all app
*/
var ActivityMenu = Widget.extend({
name: 'activity_menu',
template:'mail.systray.ActivityMenu',
events: {
'click .o_mail_activity_action': '_onActivityActionClick',
'click .o_mail_preview': '_onActivityFilterClick',
'show.bs.dropdown': '_onActivityMenuShow',
'hide.bs.dropdown': '_onActivityMenuHide',
},
start: function () {
if (config.device.isMobile) {
this.el.querySelector('.dropdown-toggle').dataset.bsDisplay = "static";
}
this._$activitiesPreview = this.$('.o_mail_systray_dropdown_items');
Component.env.bus.on('activity_updated', this, this._updateCounter);
this._updateCounter();
this._updateActivityPreview();
return this._super();
},
//--------------------------------------------------
// Private
//--------------------------------------------------
/**
* Make RPC and get current user's activity details
* @private
*/
_getActivityData: function () {
var self = this;
return self._rpc({
model: 'res.users',
method: 'systray_get_activities',
args: [],
kwargs: {context: session.user_context},
}).then(function (data) {
self._activities = data;
self.activityCounter = _.reduce(data, function (total_count, p_data) { return total_count + p_data.total_count || 0; }, 0);
self.$('.o_notification_counter').text(self.activityCounter);
self.$el.toggleClass('o_no_notification', !self.activityCounter);
});
},
/**
* Get particular model view to redirect on click of activity scheduled on that model.
* @private
* @param {string} model
*/
_getActivityModelViewID: function (model) {
return this._rpc({
model: model,
method: 'get_activity_view_id'
});
},
/**
* Return views to display when coming from systray depending on the model.
*
* @private
* @param {string} model
* @returns {Array[]} output the list of views to display.
*/
_getViewsList(model) {
return [[false, 'kanban'], [false, 'list'], [false, 'form']];
},
/**
* Update(render) activity system tray view on activity updation.
* @private
*/
_updateActivityPreview: function () {
var self = this;
self._getActivityData().then(function (){
self._$activitiesPreview.html(QWeb.render('mail.systray.ActivityMenu.Previews', {
widget: self,
Time: Time
}));
});
},
/**
* update counter based on activity status(created or Done)
* @private
* @param {Object} [data] key, value to decide activity created or deleted
* @param {String} [data.type] notification type
* @param {Boolean} [data.activity_deleted] when activity deleted
* @param {Boolean} [data.activity_created] when activity created
*/
_updateCounter: function (data) {
if (data) {
if (data.activity_created) {
this.activityCounter ++;
}
if (data.activity_deleted && this.activityCounter > 0) {
this.activityCounter --;
}
this.$('.o_notification_counter').text(this.activityCounter);
this.$el.toggleClass('o_no_notification', !this.activityCounter);
}
},
//------------------------------------------------------------
// Handlers
//------------------------------------------------------------
/**
* Redirect to specific action given its xml id or to the activity
* view of the current model if no xml id is provided
*
* @private
* @param {MouseEvent} ev
*/
_onActivityActionClick: function (ev) {
ev.stopPropagation();
this.$('.dropdown-toggle').dropdown('toggle');
var targetAction = $(ev.currentTarget);
var actionXmlid = targetAction.data('action_xmlid');
if (actionXmlid) {
this.do_action(actionXmlid);
} else {
var domain = [['activity_ids.user_id', '=', session.uid]]
if (targetAction.data('domain')) {
domain = domain.concat(targetAction.data('domain'))
}
this.do_action({
type: 'ir.actions.act_window',
name: targetAction.data('model_name'),
views: [[false, 'activity'], [false, 'kanban'], [false, 'list'], [false, 'form']],
view_mode: 'activity',
res_model: targetAction.data('res_model'),
domain: domain,
}, {
clear_breadcrumbs: true,
});
}
},
/**
* Redirect to particular model view
* @private
* @param {MouseEvent} event
*/
_onActivityFilterClick: function (event) {
// fetch the data from the button otherwise fetch the ones from the parent (.o_mail_preview).
var data = _.extend({}, $(event.currentTarget).data(), $(event.target).data());
var context = {};
if (data.filter === 'my') {
context['search_default_activities_overdue'] = 1;
context['search_default_activities_today'] = 1;
} else {
context['search_default_activities_' + data.filter] = 1;
}
// Necessary because activity_ids of mail.activity.mixin has auto_join
// So, duplicates are faking the count and "Load more" doesn't show up
context['force_search_count'] = 1;
var domain = [['activity_ids.user_id', '=', session.uid]]
if (data.domain) {
domain = domain.concat(data.domain)
}
this.do_action({
type: 'ir.actions.act_window',
name: data.model_name,
res_model: data.res_model,
views: this._getViewsList(data.res_model),
search_view_id: [false],
domain: domain,
context:context,
}, {
clear_breadcrumbs: true,
});
},
/**
* @private
*/
_onActivityMenuShow: function () {
document.body.classList.add('modal-open');
this._updateActivityPreview();
},
/**
* @private
*/
_onActivityMenuHide: function () {
document.body.classList.remove('modal-open');
},
});
export default ActivityMenu;
@@ -895,6 +895,9 @@ export class ModelManager {
listener,
reason: `first call on ${record}`,
});
if (!record.exists()) {
break; // onChange might have deleted the record, other onChange shouldn't be executed
}
}
}
}
+3 -1
View File
@@ -265,7 +265,9 @@ registerModel({
inverse: 'activity',
isCausal: true,
}),
assignee: one('User'),
assignee: one('User', {
inverse: 'activitiesAsAssignee',
}),
attachments: many('Attachment', {
inverse: 'activities',
}),
@@ -0,0 +1,72 @@
/** @odoo-module **/
import { registerModel } from '@mail/model/model_core';
import { attr, many, one } from '@mail/model/model_field';
import { insertAndReplace } from '@mail/model/model_field_command';
import { OnChange } from '@mail/model/model_onchange';
registerModel({
name: 'ActivityGroup',
identifyingFields: ['irModel'],
modelMethods: {
convertData(data) {
return {
actions: data.actions,
domain: data.domain,
irModel: insertAndReplace({
iconUrl: data.icon,
id: data.id,
model: data.model,
name: data.name,
}),
overdue_count: data.overdue_count,
planned_count: data.planned_count,
today_count: data.today_count,
total_count: data.total_count,
type: data.type,
};
},
},
recordMethods: {
/**
* @private
*/
_onChangeTotalCount() {
if (this.type === 'activity' && this.total_count === 0) {
this.delete();
}
},
},
fields: {
actions: attr(),
activityGroupViews: many('ActivityGroupView', {
inverse: 'activityGroup',
isCausal: true,
}),
domain: attr(),
irModel: one('ir.model', {
inverse: 'activityGroup',
readonly: true,
required: true,
}),
overdue_count: attr({
default: 0,
}),
planned_count: attr({
default: 0,
}),
today_count: attr({
default: 0,
}),
total_count: attr({
default: 0,
}),
type: attr(),
},
onChanges: [
new OnChange({
dependencies: ['total_count', 'type'],
methodName: '_onChangeTotalCount',
}),
],
});
@@ -0,0 +1,91 @@
/** @odoo-module **/
import { registerModel } from '@mail/model/model_core';
import { one } from '@mail/model/model_field';
import session from 'web.session';
registerModel({
name: 'ActivityGroupView',
identifyingFields: ['activityMenuViewOwner', 'activityGroup'],
recordMethods: {
/**
* @param {MouseEvent} ev
*/
onClick(ev) {
ev.stopPropagation();
this.activityMenuViewOwner.update({ isOpen: false });
const targetAction = $(ev.currentTarget);
const actionXmlid = targetAction.data('action_xmlid');
if (actionXmlid) {
this.env.services.action.doAction(actionXmlid);
} else {
let domain = [['activity_ids.user_id', '=', session.uid]];
if (targetAction.data('domain')) {
domain = domain.concat(targetAction.data('domain'));
}
this.env.services['action'].doAction(
{
domain,
name: targetAction.data('model_name'),
res_model: targetAction.data('res_model'),
type: 'ir.actions.act_window',
views: this.activityGroup.irModel.availableWebViews.map(viewName => [false, viewName]),
},
{
clearBreadcrumbs: true,
viewType: 'activity',
},
);
}
},
/**
* @param {MouseEvent} ev
*/
onClickFilterButton(ev) {
this.activityMenuViewOwner.update({ isOpen: false });
// fetch the data from the button otherwise fetch the ones from the parent (.o_ActivityMenuView_activityGroup).
const data = _.extend({}, $(ev.currentTarget).data(), $(ev.target).data());
const context = {};
if (data.filter === 'my') {
context['search_default_activities_overdue'] = 1;
context['search_default_activities_today'] = 1;
} else {
context['search_default_activities_' + data.filter] = 1;
}
// Necessary because activity_ids of mail.activity.mixin has auto_join
// So, duplicates are faking the count and "Load more" doesn't show up
context['force_search_count'] = 1;
let domain = [['activity_ids.user_id', '=', session.uid]];
if (data.domain) {
domain = domain.concat(data.domain);
}
this.env.services['action'].doAction(
{
context,
domain,
name: data.model_name,
res_model: data.res_model,
search_view_id: [false],
type: 'ir.actions.act_window',
views: this.activityGroup.irModel.availableWebViews.map(viewName => [false, viewName]),
},
{
clearBreadcrumbs: true,
},
);
},
},
fields: {
activityGroup: one('ActivityGroup', {
inverse: 'activityGroupViews',
readonly: true,
required: true,
}),
activityMenuViewOwner: one('ActivityMenuView', {
inverse: 'activityGroupViews',
readonly: true,
required: true,
}),
},
});
@@ -0,0 +1,119 @@
/** @odoo-module **/
import { registerModel } from '@mail/model/model_core';
import { attr, many } from '@mail/model/model_field';
import { insertAndReplace, replace } from '@mail/model/model_field_command';
import session from 'web.session';
registerModel({
name: 'ActivityMenuView',
identifyingFields: [],
lifecycleHooks: {
_created() {
this.fetchData();
document.addEventListener('click', this._onClickCaptureGlobal, true);
},
_willDelete() {
document.removeEventListener('click', this._onClickCaptureGlobal, true);
},
},
recordMethods: {
close() {
this.update({ isOpen: false });
},
async fetchData() {
const data = await this.messaging.rpc({
model: 'res.users',
method: 'systray_get_activities',
args: [],
kwargs: { context: session.user_context },
});
this.update({
activityGroups: insertAndReplace(data.map(vals => this.messaging.models['ActivityGroup'].convertData(vals))),
extraCount: 0,
});
},
/**
* @param {MouseEvent} ev
*/
onClickDropdownToggle(ev) {
ev.preventDefault();
if (this.isOpen) {
this.update({ isOpen: false });
} else {
this.update({ isOpen: true });
this.fetchData();
}
},
/**
* Closes the menu when clicking outside, if appropriate.
*
* @private
* @param {MouseEvent} ev
*/
_onClickCaptureGlobal(ev) {
if (!this.exists()) {
return;
}
if (!this.component || !this.component.root.el) {
return;
}
if (this.component.root.el.contains(ev.target)) {
return;
}
this.close();
},
/**
* @private
* @returns {FieldCommand}
*/
_computeActivityGroupViews() {
return insertAndReplace(this.activityGroups.map(activityGroup => {
return {
activityGroup: replace(activityGroup),
};
}));
},
/**
* @private
*/
_computeCounter() {
return this.activityGroups.reduce((total, group) => total + group.total_count, this.extraCount);
},
/**
* @private
* @returns {Array[]}
*/
_sortActivityGroups() {
return [
['smaller-first', 'irModel.id'],
];
},
},
fields: {
activityGroups: many('ActivityGroup', {
sort: '_sortActivityGroups',
}),
activityGroupViews: many('ActivityGroupView', {
compute: '_computeActivityGroupViews',
inverse: 'activityMenuViewOwner',
isCausal: true,
}),
component: attr(),
counter: attr({
compute: '_computeCounter',
}),
/**
* Determines the number of activities that have been added in the
* system but not yet taken into account in each activity group counter.
*
* @deprecated this field should be replaced by directly updating the
* counter of each group.
*/
extraCount: attr(),
isOpen: attr({
default: false,
}),
},
});
+39
View File
@@ -0,0 +1,39 @@
/** @odoo-module **/
import { registerModel } from '@mail/model/model_core';
import { attr, one } from '@mail/model/model_field';
registerModel({
name: 'ir.model',
identifyingFields: ['id'],
recordMethods: {
/**
* @private
* @returns {string[]}
*/
_computeAvailableWebViews() {
return ['kanban', 'list', 'form', 'activity'];
},
},
fields: {
/**
* Determines the name of the views that are available for this model.
*/
availableWebViews: attr({
compute: '_computeAvailableWebViews',
}),
activityGroup: one('ActivityGroup', {
inverse: 'irModel',
isCausal: true,
}),
iconUrl: attr(),
id: attr({
required: true,
readonly: true,
}),
model: attr({
required: true,
}),
name: attr(),
},
});
@@ -89,8 +89,17 @@ registerModel({
return this._handleNotificationChannelLeave(message.payload);
case 'res.users/connection':
return this._handleNotificationPartnerUserConnection(message.payload);
case 'mail.activity/updated':
return owl.Component.env.bus.trigger('activity_updated', message.payload);
case 'mail.activity/updated': {
for (const activityMenuView of this.messaging.models['ActivityMenuView'].all()) {
if (message.payload.activity_created) {
activityMenuView.update({ extraCount: increment() });
}
if (message.payload.activity_deleted) {
activityMenuView.update({ extraCount: decrement() });
}
}
return;
}
case 'mail.channel/unpin':
return this._handleNotificationChannelUnpin(message.payload);
case 'mail.channel/joined':
+4 -1
View File
@@ -1,7 +1,7 @@
/** @odoo-module **/
import { registerModel } from '@mail/model/model_core';
import { attr, one } from '@mail/model/model_field';
import { attr, many, one } from '@mail/model/model_field';
import { clear, insert } from '@mail/model/model_field_command';
registerModel({
@@ -173,6 +173,9 @@ registerModel({
},
},
fields: {
activitiesAsAssignee: many('Activity', {
inverse: 'assignee',
}),
id: attr({
readonly: true,
required: true,
@@ -19,9 +19,6 @@
font-size: 1.3rem;
font-family: emojifont;
}
.o_mail_preview .o_mail_emoji {
font-size: 100%;
}
@mixin o-viewer-black-btn {
background-color: rgba(black, 0.4);
-188
View File
@@ -1,188 +0,0 @@
// ------------------------------------------------------------------
// Layout
// ------------------------------------------------------------------
// ------------------------------------------------------------------
// Style
// ------------------------------------------------------------------
.o_mail_user_status {
font-size: 1em;
position: relative;
&.o_user_online {
color: $o-enterprise-primary-color;
}
&.fa-stack {
width: 1em;
height: 1em;
line-height: 1em;
}
}
// ------------------------------------------------------------------
// Thread preview: shared between discuss (mobile) and systray
// ------------------------------------------------------------------
.o_mail_preview {
display: flex;
background-color: map-get($theme-colors, 'light');
color: $o-main-text-color;
cursor: pointer;
overflow: hidden;
position: relative;
&:hover {
background-color: map-get($grays, '300');
.o_preview_name {
color: $headings-color;
}
.o_discuss_icon {
opacity: 1;
}
}
&:not(:last-child) {
border-bottom: 1px solid map-get($grays, '400');
}
.o_mail_preview_image {
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%;
border-radius: 50%;
object-fit: cover;
}
&.o_mail_preview_app > img {
border-radius: 2px;
}
.o_mail_user_status {
@include o-position-absolute($bottom: 0px, $right: 0px);
}
}
.o_preview_info {
flex: 1 1 100%;
overflow: hidden;
.o_preview_title {
align-items: center;
display: flex;
.o_preview_name {
flex: 0 1 auto;
@include o-text-overflow;
}
.o_mail_activity_action_buttons {
display: flex;
flex: 1 1 auto;
flex-flow: row-reverse wrap;
}
.o_mail_activity_action {
padding-top: 0px;
padding-bottom: 0px;
padding-right: 0px;
}
.o_preview_counter {
flex: 0 1 auto;
}
.o_thread_window_expand {
margin: 0px 6px;
}
.o_last_message_date {
flex: 0 0 auto;
color: $o-main-color-muted;
font-weight: 500;
}
}
.o_last_message_preview {
width: 94%;
max-height: 20px;
color: $o-main-color-muted;
@include o-text-overflow;
}
}
.o_discuss_icon {
opacity: 0;
}
&.o_preview_unread {
background-color: transparent;
&:hover {
background-color: map-get($theme-colors, 'light');
}
.o_preview_info {
.o_preview_title {
.o_preview_name, .o_preview_counter {
font-weight: 700;
}
.o_last_message_date {
color: $o-brand-primary;
}
}
}
}
&.o_systray_activity {
background-color: transparent;
&:hover {
background-color: map-get($theme-colors, 'light');
}
}
}
@include media-breakpoint-down(md) {
.o_main_navbar > ul.o_menu_systray > li .dropdown-menu.show {
border: none;
}
.o_mail_preview {
padding: $o-mail-chatter-mobile-gap;
.o_preview_info, .o_last_message_date {
margin-left: $o-mail-chatter-mobile-gap;
}
.o_preview_name {
font-size: 1.1em;
}
.o_last_message_date {
font-size: 0.9em;
}
.o_last_message_preview {
margin-top: $o-mail-chatter-mobile-gap*0.5;
}
}
.o_mail_mobile_tabs {
display: flex;
box-shadow: 0 0 8px map-get($grays, '400');
.o_mail_mobile_tab {
display: flex;
flex: 1 1 auto;
width: 20%;
flex-flow: column nowrap;
justify-content: space-between;
padding: $o-mail-chatter-mobile-gap $o-mail-chatter-mobile-gap*2;
box-shadow: 1px 0 0 map-get($grays, '400');
text-align: center;
> span {
display: block;
font-weight: 500;
font-size: 10px;
&.fa {
padding-bottom: $o-mail-chatter-mobile-gap*2;
font-size: 1.3em;
}
}
&.active > span {
color: $o-brand-primary;
}
}
}
}
-125
View File
@@ -1,125 +0,0 @@
// Systray icon and dropdown
.o_mail_systray_item {
&.o_no_notification .o_notification_counter {
display: none;
}
&.show .o_mail_systray_dropdown {
display: flex;
flex-flow: column nowrap;
}
.o_mail_systray_dropdown {
direction: ltr;
width: 350px;
padding: 0;
.o_spinner {
display: flex;
align-items: center;
justify-content: center;
color: $o-main-text-color;
height: 50px;
}
.o_mail_systray_dropdown_top {
display: flex;
flex: 0 0 auto;
justify-content: space-between;
border-bottom: 1px solid map-get($grays, '400');
box-shadow: 0 0 2px map-get($grays, '400');
.o_filter_button, .o_new_message {
padding: 5px;
}
.o_filter_button {
color: $o-main-color-muted;
&:hover, &.active {
color: $o-brand-primary;
}
&.active {
cursor: default;
font-weight: bold;
}
}
}
.o_mail_systray_dropdown_items {
flex: 0 1 auto;
max-height: 400px;
min-height: 50px;
overflow-y: auto;
@include media-breakpoint-up(md) {
.o_mail_preview {
min-height: 50px;
padding: 5px;
.o_mail_preview_image .fa-circle-o {
display: none;
}
.o_preview_info {
margin-left: 10px;
.o_preview_title {
.o_last_message_date {
padding-top: 2px;
font-size: x-small;
margin-left: 10px;
}
}
}
}
}
}
.o_activity_filter_button {
padding: 2px;
}
.o_no_activity {
cursor: initial;
align-items: center;
color: grey;
opacity: 0.5;
padding: 3px;
min-height: inherit;
}
}
}
.o_no_thread_window .o_mail_systray_dropdown .o_new_message {
display: none; // hide 'new message' button if chat windows are disabled
}
// Mobile rules
// Goal: mock the design of Discuss in mobile
@include media-breakpoint-down(md) {
.o_mail_systray_item {
.o_mail_systray_dropdown {
position: fixed;
top: $o-mail-chat-window-header-height-mobile;
bottom: 0;
left: 0;
right: 0;
width: 100%;
margin: 0;
max-height: none;
.o_mail_systray_dropdown_top {
padding: 5px;
}
.o_mail_systray_mobile_header {
padding: 5px;
height: 44px;
border-bottom: 1px solid #ebebeb;
box-shadow: 0 0 2px map-get($grays, '400');
}
.o_mail_systray_dropdown_items {
max-height: none;
padding-bottom: 52px; // leave space for tabs
}
.o_mail_mobile_tabs {
position: fixed;
bottom: 0px;
left: 0px;
right: 0px;
background-color: white;
color: $o-main-text-color;
}
}
}
}
-9
View File
@@ -38,15 +38,6 @@
.o_updatable_im_status {
width: $o-mail-thread-avatar-size;
}
.o_mail_user_status {
position: absolute;
bottom: 0;
right: 0;
&.fa-circle-o {
display: none;
}
}
}
@include media-breakpoint-down(md) {
-25
View File
@@ -1,25 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<!--
AKU FIXME: use mail/static/src/components/persona_im_status_icon/persona_im_status_icon.js component instead
@param {string} status
-->
<t t-name="mail.widgets.UserStatus">
<span>
<t t-if="status == 'online'">
<i class="o_mail_user_status o_user_online fa fa-circle" title="Online" role="img" aria-label="User is online"/>
</t>
<t t-if="status == 'away'">
<i class="fa fa-circle o_mail_user_status o_user_idle text-warning" title="Idle" role="img" aria-label="User is idle"/>
</t>
<t t-if="status == 'offline'">
<i class="o_mail_user_status fa fa-circle-o" title="Offline" role="img" aria-label="User is offline"/>
</t>
<t t-if="status == 'bot'">
<i class="o_mail_user_status o_user_online fa fa-heart" title="Bot" role="img" aria-label="User is a bot"/>
</t>
</span>
</t>
</templates>
-61
View File
@@ -1,61 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates>
<!--
@param {mail.systray.ActivityMenu} widget
@param {Object[]} widget.activities
-->
<t t-name="mail.systray.ActivityMenu.Previews">
<t t-set="activities" t-value="widget._activities"/>
<t t-if="_.isEmpty(activities)">
<div class="dropdown-item-text text-center o_no_activity d-flex justify-content-center">
<span>Congratulations, you're done with your activities.</span>
</div>
</t>
<t t-foreach="activities" t-as="activity">
<div class="o_mail_preview o_systray_activity" t-att-data-res_model="activity.model" t-att-data-model_name="activity.name" t-att-data-domain="activity.domain" data-filter='my'>
<div t-if="activity.icon" class="o_mail_preview_image o_mail_preview_app">
<img t-att-src="activity.icon" alt="Activity"/>
</div>
<div class="o_preview_info">
<div class="o_preview_title">
<span class="o_preview_name">
<t t-esc="activity.name"/>
</span>
<div t-if="activity.actions" class="o_mail_activity_action_buttons">
<t t-foreach="activity.actions" t-as="action">
<button type="button"
t-att-title="action.name"
t-att-class="'o_mail_activity_action btn btn-link fa ' + action.icon"
t-att-data-action_xmlid="action.action_xmlid"
t-att-data-res_model="activity.model"
t-att-data-model_name="activity.name"
t-att-data-domain="activity.domain">
</button>
</t>
</div>
</div>
<div t-if="activity and activity.type == 'activity'">
<button t-if="activity.overdue_count" type="button" class="btn btn-link o_activity_filter_button mr16" t-att-data-res_model="activity.model" t-att-data-model_name="activity.name" data-filter='overdue'><t t-esc="activity.overdue_count"/> Late </button>
<span t-if="!activity.overdue_count" class="o_no_activity mr16">0 Late </span>
<button t-if="activity.today_count" type="button" class="btn btn-link o_activity_filter_button mr16" t-att-data-res_model="activity.model" t-att-data-model_name="activity.name" data-filter='today'> <t t-esc="activity.today_count"/> Today </button>
<span t-if="!activity.today_count" class="o_no_activity mr16">0 Today </span>
<button t-if="activity.planned_count" type="button" class="btn btn-link o_activity_filter_button float-end" t-att-data-res_model="activity.model" t-att-data-model_name="activity.name" data-filter='upcoming_all'> <t t-esc="activity.planned_count"/> Future </button>
<span t-if="!activity.planned_count" class="o_no_activity float-end">0 Future</span>
</div>
</div>
</div>
</t>
</t>
<t t-name="mail.systray.ActivityMenu">
<div class="o_mail_systray_item dropdown">
<a class="dropdown-toggle o-no-caret o-dropdown--narrow" data-bs-toggle="dropdown" data-bs-offset="0,0" aria-expanded="false" title="Activities" href="#" role="button">
<i class="fa fa-lg fa-clock-o" role="img" aria-label="Activities"/> <span class="o_notification_counter badge"/>
</a>
<div class="o_mail_systray_dropdown dropdown-menu dropdown-menu-end" role="menu">
<div class="o_mail_systray_dropdown_items"/>
</div>
</div>
</t>
</templates>
@@ -2193,6 +2193,7 @@ patch(MockServer.prototype, 'mail', {
const modelName = activity['res_model'];
if (!userActivitiesByModelName[modelName]) {
userActivitiesByModelName[modelName] = {
id: modelName, // for simplicity
model: modelName,
name: modelName,
overdue_count: 0,
@@ -49,6 +49,7 @@ class Users(models.Model):
icon = module and modules.module.get_module_icon(module)
res_ids = set()
user_activities[act['mailing_type']] = {
'id': self.env['ir.model']._get('mailing.mailing').id,
'name': name,
'model': 'mailing.mailing',
'type': 'activity',
+5 -2
View File
@@ -25,15 +25,18 @@
'installable': True,
'application': True,
'assets': {
'mail.assets_messaging': [
'note/static/src/models/*.js',
],
'web.assets_backend': [
'note/static/src/components/*/*.js',
'note/static/src/scss/note.scss',
'note/static/src/js/systray_activity_menu.js',
],
'web.qunit_suite_tests': [
'note/static/tests/**/*',
],
'web.assets_qweb': [
'note/static/src/xml/**/*',
'note/static/src/components/*/*.xml',
],
},
'license': 'LGPL-3',
+1
View File
@@ -61,6 +61,7 @@ GROUP BY id"""
activities[note_index]['name'] = note_label
else:
activities.append({
'id': self.env['ir.model']._get('note.note').id,
'type': 'activity',
'name': note_label,
'model': 'note.note',
@@ -0,0 +1,26 @@
/** @odoo-module **/
import { useRefToModel } from '@mail/component_hooks/use_ref_to_model';
import { useUpdateToModel } from '@mail/component_hooks/use_update_to_model';
import '@mail/components/activity_menu_view/activity_menu_view'; // ensure components are registered beforehand.
import { getMessagingComponent } from "@mail/utils/messaging_component";
import { DatePicker } from '@web/core/datepicker/datepicker';
import { patch } from 'web.utils';
const ActivityMenuView = getMessagingComponent('ActivityMenuView');
patch(ActivityMenuView.prototype, 'note', {
/**
* @override
*/
setup() {
this._super();
useRefToModel({ fieldName: 'noteInputRef', refName: 'noteInput', });
useUpdateToModel({ methodName: 'onComponentUpdate' });
},
});
Object.assign(ActivityMenuView.components, {
DatePicker,
});
@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-inherit="mail.ActivityMenuView" t-inherit-mode="extension">
<xpath expr="//*[@name='activityGroupLoop']" position="after">
<div t-if="!activityMenuView.isAddingNote" class="o_note_show d-grid" t-on-click="activityMenuView.onClickAddNote">
<a role="button" class="btn text-center">Add new note</a>
</div>
<div t-if="activityMenuView.isAddingNote" class="o_note o_ActivityMenuView_activityGroup">
<div class="o_ActivityMenuView_activityGroupIconContainer">
<img src="/note/static/description/icon.svg" alt="Channel"/>
</div>
<div class="o_ActivityMenuView_activityGroupInfo">
<div class="o_ActivityMenuView_activityGroupTitle">
<span class="o_ActivityMenuView_activityGroupName"><strong>Add a note</strong></span>
<DatePicker
date="activityMenuView.addingNoteDate"
onDateTimeChanged="activityMenuView.onDateTimeChanged"
placeholder="activityMenuView.addingNoteDatePlaceholder"
/>
</div>
<div class="o_note_input_box">
<p><input class="o_note_input" type="text" placeholder="Remember..." t-on-keydown="activityMenuView.onKeydownNoteInput" t-ref="noteInput"/></p>
<span class="ml8 mr4">
<a class="o_note_save" t-on-click="activityMenuView.onClickSaveNote">SAVE</a>
</span>
</div>
</div>
</div>
</xpath>
</t>
</templates>
@@ -1,133 +0,0 @@
/** @odoo-module **/
import ActivityMenu from '@mail/js/systray/systray_activity_menu';
import { _t } from 'web.core';
import datepicker from 'web.datepicker';
ActivityMenu.include({
events: _.extend({}, ActivityMenu.prototype.events, {
'click .o_note_show': '_onAddNoteClick',
'click .o_note_save': '_onNoteSaveClick',
'click .o_note_set_datetime': '_onNoteDateTimeSetClick',
'keydown input.o_note_input': '_onNoteInputKeyDown',
'click .o_note': '_onNewNoteClick',
}),
//--------------------------------------------------
// Private
//--------------------------------------------------
/**
* Moving notes at first place
* @override
*/
_getActivityData: function () {
var self = this;
return this._super.apply(this, arguments).then(function () {
var reminderIndex = _.findIndex(self.activities, function (val) {
return val.model === 'note.note';
});
if (reminderIndex > 0) {
self.activities.splice(0, 0, self.activities.splice(reminderIndex, 1)[0]);
}
});
},
/**
* Save the note to database using datepicker date and field as note
* By default, when no datetime is set, it uses the current datetime.
*
* @private
*/
_saveNote: function () {
var note = this.$('.o_note_input').val().trim();
if (! note) {
return;
}
var params = {'note': note};
var noteDateTime = this.noteDateTimeWidget.getValue();
if (noteDateTime) {
params = _.extend(params, {'date_deadline': noteDateTime});
} else {
params = _.extend(params, {'date_deadline': moment()});
}
this.$('.o_note_show').removeClass('d-none');
this.$('.o_note').addClass('d-none');
this._rpc({
route: '/note/new',
params: params,
}).then(this._updateActivityPreview.bind(this));
},
//-----------------------------------------
// Handlers
//-----------------------------------------
/**
* @override
*/
_onActivityFilterClick: function (ev) {
var $el = $(ev.currentTarget);
if (!$el.hasClass("o_note")) {
this._super.apply(this, arguments);
}
},
/**
* When add new note button clicked, toggling quick note create view inside
* Systray activity view
*
* @private
* @param {MouseEvent} ev
*/
_onAddNoteClick: function (ev) {
var self = this;
ev.stopPropagation();
if (!this.noteDateTimeWidget){
this.noteDateTimeWidget = new datepicker.DateWidget(this, {useCurrent: true});
}
this.noteDateTimeWidget.appendTo(this.$('.o_note_datetime')).then(function() {
self.noteDateTimeWidget.$input.attr('placeholder', _t("Today"));
self.noteDateTimeWidget.setValue(false);
self.$('.o_note_show, .o_note').toggleClass('d-none');
self.$('.o_note_input').val('').focus();
});
},
/**
* When focusing on input for new quick note systerm tray must be open.
* Preventing to close
*
* @private
* @param {MouseEvent} ev
*/
_onNewNoteClick: function (ev) {
ev.stopPropagation();
},
/**
* Opens datetime picker for note.
* Quick FIX due to no option for set custom icon instead of caret in datepicker.
*
* @private
* @param {MouseEvent} ev
*/
_onNoteDateTimeSetClick: function (ev) {
ev.preventDefault();
ev.stopPropagation();
this.noteDateTimeWidget.$input.click();
},
/**
* Saving note (quick create) and updating activity preview
*
* @private
* @param {MouseEvent} ev
*/
_onNoteSaveClick: function (ev) {
this._saveNote();
},
/**
* Handling Enter key for quick create note.
*
* @private
* @param {KeyboardEvent} ev
*/
_onNoteInputKeyDown: function (ev) {
if (ev.which === $.ui.keyCode.ENTER) {
this._saveNote();
}
},
});
@@ -0,0 +1,17 @@
/** @odoo-module **/
import { addFields, addRecordMethods } from '@mail/model/model_core';
import { attr } from '@mail/model/model_field';
import '@mail/models/activity_group'; // ensure the model definition is loaded before the patch
addRecordMethods('ActivityGroup', {
_computeIsNote() {
return this.irModel.model === 'note.note';
},
});
addFields('ActivityGroup', {
isNote: attr({
compute: '_computeIsNote',
}),
});
@@ -0,0 +1,112 @@
/** @odoo-module **/
import { addFields, addRecordMethods, patchRecordMethods } from '@mail/model/model_core';
import { attr } from '@mail/model/model_field';
import { clear } from '@mail/model/model_field_command';
import '@mail/models/activity_menu_view'; // ensure the model definition is loaded before the patch
const { DateTime } = luxon;
addRecordMethods('ActivityMenuView', {
/**
* @param {MouseEvent} ev
*/
onClickAddNote(ev) {
this.update({
addingNoteDoFocus: true,
isAddingNote: true,
});
},
/**
* @param {MouseEvent} ev
*/
onClickSaveNote(ev) {
this.saveNote();
},
onComponentUpdate() {
if (this.addingNoteDoFocus && this.noteInputRef.el) {
this.noteInputRef.el.focus();
this.update({ addingNoteDoFocus: clear() });
}
},
/**
* @param {DateTime|string} date
*/
onDateTimeChanged(date) {
this.update({ addingNoteDate: date ? date : clear() });
},
/**
* @param {KeyboardEvent} ev
*/
onKeydownNoteInput(ev) {
if (ev.key === 'Enter') {
this.saveNote();
}
},
async saveNote() {
const note = this.noteInputRef.el.value.trim();
if (!note) {
return;
}
this.update({ isAddingNote: false });
await this.messaging.rpc({
route: '/note/new',
params: {
'note': note,
'date_deadline': this.addingNoteDate ? this.addingNoteDate : new DateTime.local(),
},
});
this.fetchData();
},
/**
* @private
* @returns {string}
*/
_computeAddingNoteDatePlaceholder() {
return this.env._t("Today");
},
});
patchRecordMethods('ActivityMenuView', {
/**
* @override
*/
close() {
this.update({
addingNoteDoFocus: clear(),
isAddingNote: false,
});
this._super();
},
/**
* @override
*/
_onClickCaptureGlobal(ev) {
if (ev.target.closest('.bootstrap-datetimepicker-widget')) {
return;
}
this._super(ev);
},
/**
* @override
*/
_sortActivityGroups(ev) {
const res = this._super();
res.unshift(['truthy-first', 'isNote']);
return res;
},
});
addFields('ActivityMenuView', {
addingNoteDate: attr(),
addingNoteDoFocus: attr({
default: false,
}),
addingNoteDatePlaceholder: attr({
compute: '_computeAddingNoteDatePlaceholder',
}),
isAddingNote: attr({
default: false,
}),
noteInputRef: attr(),
});
+4 -20
View File
@@ -51,11 +51,11 @@
}
// Quick create notes from systray
.o_note.o_mail_preview {
.o_note.o_ActivityMenuView_activityGroup {
background-color: white;
.o_preview_info {
.o_preview_title {
.o_preview_name {
.o_ActivityMenuView_activityGroupInfo {
.o_ActivityMenuView_activityGroupTitle {
.o_ActivityMenuView_activityGroupName {
flex: 1 1 100%;
}
}
@@ -74,20 +74,4 @@
.o_note_input {
border: none;
}
.o_note_datetime {
.o_datepicker {
.o_datepicker_input {
float: right;
text-align: right;
border: none;
font-size: 11px;
}
.o_datepicker_button {
display: none;
}
}
}
.o_note_set_datetime {
color: $text-muted;
}
}
-32
View File
@@ -1,32 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates>
<t t-extend="mail.systray.ActivityMenu.Previews">
<t t-jquery="t[t-foreach*='activities'][t-as*='activity']" t-operation="after">
<div class="o_note_show d-grid">
<a role="button" class="btn text-center">Add new note</a>
</div>
<div class="o_note o_mail_preview d-none">
<div class="o_mail_preview_image o_mail_preview_app">
<img src="/note/static/description/icon.svg" alt="Channel"/>
</div>
<div class="o_preview_info">
<div class="o_preview_title">
<span class="o_preview_name"><strong>Add a note</strong></span>
<div class="o_note_datetime"/>
<span class="ml4">
<a class="o_note_set_datetime">
<span class="fa fa-calendar" role="img" aria-label="Set date and time" title="Set date and time"/>
</a>
</span>
</div>
<div class="o_note_input_box">
<p><input class="o_note_input" type="text" placeholder="Remember..." /></p>
<span class="ml8 mr4">
<a class="o_note_save">SAVE</a>
</span>
</div>
</div>
</div>
</t>
</t>
</templates>
@@ -61,6 +61,7 @@ patch(MockServer.prototype, 'note', {
activities[noteIndex]['name'] = 'Notes';
} else {
activities.push({
id: 'note.note', // for simplicity
type: 'activity',
name: 'Notes',
model: 'note.note',
@@ -10,53 +10,52 @@ QUnit.module("ActivityMenu");
QUnit.test('note activity menu widget: create note from activity menu', async function (assert) {
assert.expect(15);
await start();
assert.containsOnce(document.body, '.o_mail_systray_item',
const { click } = await start();
assert.containsOnce(document.body, '.o_ActivityMenuView',
'should contain an instance of widget');
await testUtils.nextTick();
assert.strictEqual(document.querySelector('.o_notification_counter').innerText, '0',
assert.containsNone(document.body, '.o_ActivityMenuView_counter',
"should not have any activity notification initially");
// toggle quick create for note
await testUtils.dom.click(document.querySelector('.dropdown-toggle[title="Activities"]'));
assert.containsOnce(document.body, '.o_no_activity',
await click('.dropdown-toggle[title="Activities"]');
assert.containsOnce(document.body, '.o_ActivityMenuView_noActivity',
"should not have any activity preview");
assert.doesNotHaveClass(document.querySelector('.o_note_show'), 'd-none',
assert.containsOnce(document.body, '.o_note_show',
'ActivityMenu should have Add new note CTA');
await testUtils.dom.click(document.querySelector('.o_note_show'));
assert.hasClass(document.querySelector('.o_note_show'), 'd-none',
await click('.o_note_show');
assert.containsNone(document.body, '.o_note_show',
'ActivityMenu should hide CTA when entering a new note');
assert.doesNotHaveClass(document.querySelector('.o_note'), 'd-none',
assert.containsOnce(document.body, '.o_note',
'ActivityMenu should display input for new note');
// creating quick note without date
await testUtils.fields.editInput(document.querySelector("input.o_note_input"), "New Note");
await testUtils.dom.click(document.querySelector(".o_note_save"));
assert.strictEqual(document.querySelector('.o_notification_counter').innerText, '1',
await click('.o_note_save');
assert.strictEqual(document.querySelector('.o_ActivityMenuView_counter').innerText, '1',
"should increment activity notification counter after creating a note");
assert.containsOnce(document.body, '.o_mail_preview[data-res_model="note.note"]',
assert.containsOnce(document.body, '.o_ActivityMenuView_activityGroup[data-res_model="note.note"]',
"should have an activity preview that is a note");
assert.strictEqual(document.querySelector('.o_activity_filter_button[data-filter="today"]').innerText.trim(),
assert.strictEqual(document.querySelector('.o_ActivityMenuView_activityGroupFilterButton[data-filter="today"]').innerText.trim(),
"1 Today",
"should display one note for today");
assert.doesNotHaveClass(document.querySelector('.o_note_show'), 'd-none',
'ActivityMenu add note button should be displayed');
assert.hasClass(document.querySelector('.o_note'), 'd-none',
assert.containsNone(document.body, '.o_note',
'ActivityMenu add note input should be hidden');
// creating quick note with date
await testUtils.dom.click(document.querySelector('.o_note_show'));
await click('.o_note_show');
document.querySelector('input.o_note_input').value = "New Note";
await testUtils.dom.click(document.querySelector(".o_note_save"));
assert.strictEqual(document.querySelector('.o_notification_counter').innerText, '2',
await click(".o_note_save");
assert.strictEqual(document.querySelector('.o_ActivityMenuView_counter').innerText, '2',
"should increment activity notification counter after creating a second note");
assert.strictEqual(document.querySelector('.o_activity_filter_button[data-filter="today"]').innerText.trim(),
assert.strictEqual(document.querySelector('.o_ActivityMenuView_activityGroupFilterButton[data-filter="today"]').innerText.trim(),
"2 Today",
"should display 2 notes for today");
assert.doesNotHaveClass(document.querySelector('.o_note_show'), 'd-none',
'ActivityMenu add note button should be displayed');
assert.hasClass(document.querySelector('.o_note'), 'd-none',
assert.containsNone(document.body, '.o_note',
'ActivityMenu add note input should be hidden');
});
});
+3
View File
@@ -9,11 +9,14 @@
'summary': 'Test of Discuss with all possible overrides installed.',
'description': """Test of Discuss with all possible overrides installed, including feature and performance tests.""",
'depends': [
'calendar',
'crm',
'crm_livechat',
'hr_holidays',
'im_livechat',
'mail',
'mail_bot',
'note',
'website_livechat',
],
'installable': True,
@@ -6,7 +6,6 @@ import {
} from '@mail/../tests/helpers/test_utils';
import session from 'web.session';
import testUtils from 'web.test_utils';
import { date_to_str } from 'web.time';
import { patchWithCleanup } from '@web/../tests/helpers/utils';
@@ -25,7 +24,7 @@ QUnit.module('systray_activity_menu_tests.js', {
{ res_id: resPartnerId1, res_model: 'res.partner' },
{ res_id: mailTestActivityIds[0], res_model: 'mail.test.activity' },
{ date_deadline: date_to_str(tomorrow), res_id: mailTestActivityIds[1], res_model: 'mail.test.activity' },
{ date_deadline: date_to_str(tomorrow), res_id: mailTestActivityIds[2], res_model: 'mail.test.activity' },
{ date_deadline: date_to_str(tomorrow), res_id: mailTestActivityIds[2], res_model: 'mail.test.activity' },
{ date_deadline: date_to_str(yesterday), res_id: mailTestActivityIds[3], res_model: 'mail.test.activity' },
]);
},
@@ -34,28 +33,27 @@ QUnit.module('systray_activity_menu_tests.js', {
QUnit.test('activity menu widget: menu with no records', async function (assert) {
assert.expect(1);
await start({
const { click } = await start({
mockRPC: function (route, args) {
if (args.method === 'systray_get_activities') {
return Promise.resolve([]);
}
},
});
await testUtils.nextTick();
assert.containsOnce(document.body, '.o_no_activity');
await click('.o_ActivityMenuView_dropdownToggle');
assert.containsOnce(document.body, '.o_ActivityMenuView_noActivity');
});
QUnit.test('activity menu widget: activity menu with 2 models', async function (assert) {
assert.expect(10);
const { env } = await start();
const { click, env } = await start();
await testUtils.nextTick();
assert.containsOnce(document.body, '.o_mail_systray_item', 'should contain an instance of widget');
// the assertion below has not been replace because there are includes of ActivityMenu that modify the length.
assert.ok(document.querySelectorAll('.o_mail_preview').length);
assert.containsOnce(document.body, '.o_notification_counter', "widget should have notification counter");
assert.strictEqual(parseInt(document.querySelector('.o_notification_counter').innerText), 5, "widget should have 5 notification counter");
await click('.o_ActivityMenuView_dropdownToggle');
assert.containsOnce(document.body, '.o_ActivityMenuView', 'should contain an instance of widget');
assert.ok(document.querySelectorAll('.o_ActivityMenuView_activityGroup').length);
assert.containsOnce(document.body, '.o_ActivityMenuView_counter', "widget should have notification counter");
assert.strictEqual(parseInt(document.querySelector('.o_ActivityMenuView_counter').innerText), 5, "widget should have 5 notification counter");
var context = {};
patchWithCleanup(env.services.action, {
@@ -69,45 +67,45 @@ QUnit.test('activity menu widget: activity menu with 2 models', async function (
force_search_count: 1,
search_default_activities_overdue: 1,
};
await testUtils.dom.click(document.querySelector('.dropdown-toggle[title="Activities"]'));
assert.containsOnce(document.body, '.o_mail_systray_dropdown.show', 'ActivityMenu should be open');
await testUtils.dom.click(document.querySelector(".o_activity_filter_button[data-model_name='mail.test.activity'][data-filter='overdue']"));
assert.containsOnce(document.body, '.o_ActivityMenuView_dropdownMenu.show', 'ActivityMenu should be open');
await click('.o_ActivityMenuView_activityGroupFilterButton[data-model_name="mail.test.activity"][data-filter="overdue"]');
assert.containsNone(document.body, '.show', 'ActivityMenu should be closed');
// case 2: click on "today"
context = {
force_search_count: 1,
search_default_activities_today: 1,
};
await testUtils.dom.click(document.querySelector('.dropdown-toggle[title="Activities"]'));
await testUtils.dom.click(document.querySelector(".o_activity_filter_button[data-model_name='mail.test.activity'][data-filter='today']"));
await click('.dropdown-toggle[title="Activities"]');
await click('.o_ActivityMenuView_activityGroupFilterButton[data-model_name="mail.test.activity"][data-filter="today"]');
// case 3: click on "future"
context = {
force_search_count: 1,
search_default_activities_upcoming_all: 1,
};
await testUtils.dom.click(document.querySelector('.dropdown-toggle[title="Activities"]'));
await testUtils.dom.click(document.querySelector(".o_activity_filter_button[data-model_name='mail.test.activity'][data-filter='upcoming_all']"));
await click('.dropdown-toggle[title="Activities"]');
await click('.o_ActivityMenuView_activityGroupFilterButton[data-model_name="mail.test.activity"][data-filter="upcoming_all"]');
// case 4: click anywere else
context = {
force_search_count: 1,
search_default_activities_overdue: 1,
search_default_activities_today: 1,
};
await testUtils.dom.click(document.querySelector('.dropdown-toggle[title="Activities"]'));
await testUtils.dom.click(document.querySelector(".o_mail_systray_dropdown_items > div[data-model_name='mail.test.activity']"));
await click('.dropdown-toggle[title="Activities"]');
await click('.o_ActivityMenuView_activityGroups > div[data-model_name="mail.test.activity"]');
});
QUnit.test('activity menu widget: activity view icon', async function (assert) {
assert.expect(14);
patchWithCleanup(session, { uid: 10 });
const { env } = await start();
await testUtils.nextTick();
assert.containsN(document.body, '.o_mail_activity_action', 2,
const { click, env } = await start();
await click('.o_ActivityMenuView_dropdownToggle');
assert.containsN(document.body, '.o_ActivityMenuView_activityGroupActionButton', 2,
"widget should have 2 activity view icons");
var first = document.querySelector('.o_mail_activity_action');
var second = document.querySelectorAll('.o_mail_activity_action')[1];
var first = document.querySelector('.o_ActivityMenuView_activityGroupActionButton');
var second = document.querySelectorAll('.o_ActivityMenuView_activityGroupActionButton')[1];
assert.strictEqual(first.getAttribute('data-model_name'), "res.partner",
"first activity action should link to 'res.partner'");
assert.hasClass(first, 'fa-clock-o', "should display the activity action icon");
@@ -129,18 +127,16 @@ QUnit.test('activity menu widget: activity view icon', async function (assert) {
},
});
// click on the "mail.test.activity" activity icon
await testUtils.dom.click(document.querySelector('.dropdown-toggle[title="Activities"]'));
assert.hasClass(document.querySelector('.dropdown-menu'), 'show',
assert.hasClass(document.querySelector('.o-dropdown-menu'), 'show',
"dropdown should be expanded");
await testUtils.dom.click(document.querySelector(".o_mail_activity_action[data-model_name='mail.test.activity']"));
assert.doesNotHaveClass(document.querySelector('.dropdown-menu'), 'show',
await click('.o_ActivityMenuView_activityGroupActionButton[data-model_name="mail.test.activity"]');
assert.containsNone(document.body, '.o-dropdown-menu',
"dropdown should be collapsed");
// click on the "res.partner" activity icon
await testUtils.dom.click(document.querySelector('.dropdown-toggle[title="Activities"]'));
await testUtils.dom.click(document.querySelector(".o_mail_activity_action[data-model_name='res.partner']"));
await click('.dropdown-toggle[title="Activities"]');
await click('.o_ActivityMenuView_activityGroupActionButton[data-model_name="res.partner"]');
assert.verifySteps([
'do_action:mail.test.activity',
@@ -152,19 +148,18 @@ QUnit.test('activity menu widget: close on messaging menu click', async function
assert.expect(2);
const { click } = await start();
await testUtils.nextTick();
await testUtils.dom.click(document.querySelector('.dropdown-toggle[title="Activities"]'));
await click('.dropdown-toggle[title="Activities"]');
assert.hasClass(
document.querySelector('.o_mail_systray_dropdown'),
document.querySelector('.o_ActivityMenuView_dropdownMenu'),
'show',
"activity menu should be shown after click on itself"
);
await click(`.o_MessagingMenu_toggler`);
assert.doesNotHaveClass(
document.querySelector('.o_mail_systray_dropdown'),
'show',
assert.containsNone(
document.body,
'.o_ActivityMenuView_dropdownMenu',
"activity menu should be hidden after click on messaging menu"
);
});