diff --git a/addons/calendar/static/tests/helpers/mock_server.js b/addons/calendar/static/tests/helpers/mock_server.js
index b2650358fb2..d85833a862f 100644
--- a/addons/calendar/static/tests/helpers/mock_server.js
+++ b/addons/calendar/static/tests/helpers/mock_server.js
@@ -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',
diff --git a/addons/calendar/static/tests/systray_activity_menu_tests.js b/addons/calendar/static/tests/systray_activity_menu_tests.js
index b1a9ee256d2..fc4fba7daac 100644
--- a/addons/calendar/static/tests/systray_activity_menu_tests.js
+++ b/addons/calendar/static/tests/systray_activity_menu_tests.js
@@ -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');
});
});
diff --git a/addons/crm/__manifest__.py b/addons/crm/__manifest__.py
index 7b435da7bff..5d0ed383a24 100644
--- a/addons/crm/__manifest__.py
+++ b/addons/crm/__manifest__.py
@@ -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',
diff --git a/addons/crm/static/src/js/systray_activity_menu.js b/addons/crm/static/src/js/systray_activity_menu.js
deleted file mode 100644
index 8893ab7d8af..00000000000
--- a/addons/crm/static/src/js/systray_activity_menu.js
+++ /dev/null
@@ -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);
- }
- },
-});
diff --git a/addons/crm/static/src/models/activity_group_view.js b/addons/crm/static/src/models/activity_group_view.js
new file mode 100644
index 00000000000..8f9d0de4be6
--- /dev/null
+++ b/addons/crm/static/src/models/activity_group_view.js
@@ -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);
+ }
+ },
+});
diff --git a/addons/crm/static/src/models/ir_model.js b/addons/crm/static/src/models/ir_model.js
new file mode 100644
index 00000000000..29b92ba1f0c
--- /dev/null
+++ b/addons/crm/static/src/models/ir_model.js
@@ -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();
+ },
+});
diff --git a/addons/im_livechat/static/src/legacy/public_livechat.scss b/addons/im_livechat/static/src/legacy/public_livechat.scss
index 7368528801b..1b826d272f9 100644
--- a/addons/im_livechat/static/src/legacy/public_livechat.scss
+++ b/addons/im_livechat/static/src/legacy/public_livechat.scss
@@ -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
//-------------------------------------
diff --git a/addons/mail/models/res_users.py b/addons/mail/models/res_users.py
index 5bc103465a9..4e0129eac20 100644
--- a/addons/mail/models/res_users.py
+++ b/addons/mail/models/res_users.py
@@ -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',
diff --git a/addons/mail/static/src/components/activity_menu_container/activity_menu_container.js b/addons/mail/static/src/components/activity_menu_container/activity_menu_container.js
index 51c4c695eec..0980f769bd0 100644
--- a/addons/mail/static/src/components/activity_menu_container/activity_menu_container.js
+++ b/addons/mail/static/src/components/activity_menu_container/activity_menu_container.js
@@ -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',
});
diff --git a/addons/mail/static/src/components/activity_menu_container/activity_menu_container.xml b/addons/mail/static/src/components/activity_menu_container/activity_menu_container.xml
index 525f7efe3ca..5c014da12e7 100644
--- a/addons/mail/static/src/components/activity_menu_container/activity_menu_container.xml
+++ b/addons/mail/static/src/components/activity_menu_container/activity_menu_container.xml
@@ -1,6 +1,8 @@
-
+
+
+
diff --git a/addons/mail/static/src/components/activity_menu_view/activity_menu_view.js b/addons/mail/static/src/components/activity_menu_view/activity_menu_view.js
new file mode 100644
index 00000000000..8ec205caa00
--- /dev/null
+++ b/addons/mail/static/src/components/activity_menu_view/activity_menu_view.js
@@ -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);
diff --git a/addons/mail/static/src/components/activity_menu_view/activity_menu_view.scss b/addons/mail/static/src/components/activity_menu_view/activity_menu_view.scss
new file mode 100644
index 00000000000..0e1edcad442
--- /dev/null
+++ b/addons/mail/static/src/components/activity_menu_view/activity_menu_view.scss
@@ -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;
+}
diff --git a/addons/mail/static/src/components/activity_menu_view/activity_menu_view.xml b/addons/mail/static/src/components/activity_menu_view/activity_menu_view.xml
new file mode 100644
index 00000000000..86718f8706e
--- /dev/null
+++ b/addons/mail/static/src/components/activity_menu_view/activity_menu_view.xml
@@ -0,0 +1,57 @@
+
+
+
+
+
+
+
+
diff --git a/addons/mail/static/src/js/systray/systray_activity_menu.js b/addons/mail/static/src/js/systray/systray_activity_menu.js
deleted file mode 100644
index 2da1c4bb5f0..00000000000
--- a/addons/mail/static/src/js/systray/systray_activity_menu.js
+++ /dev/null
@@ -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;
diff --git a/addons/mail/static/src/model/model_manager.js b/addons/mail/static/src/model/model_manager.js
index e9af17e36a3..6c5355f0afa 100644
--- a/addons/mail/static/src/model/model_manager.js
+++ b/addons/mail/static/src/model/model_manager.js
@@ -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
+ }
}
}
}
diff --git a/addons/mail/static/src/models/activity.js b/addons/mail/static/src/models/activity.js
index e0795776973..c87d59634b5 100644
--- a/addons/mail/static/src/models/activity.js
+++ b/addons/mail/static/src/models/activity.js
@@ -265,7 +265,9 @@ registerModel({
inverse: 'activity',
isCausal: true,
}),
- assignee: one('User'),
+ assignee: one('User', {
+ inverse: 'activitiesAsAssignee',
+ }),
attachments: many('Attachment', {
inverse: 'activities',
}),
diff --git a/addons/mail/static/src/models/activity_group.js b/addons/mail/static/src/models/activity_group.js
new file mode 100644
index 00000000000..d604152cc10
--- /dev/null
+++ b/addons/mail/static/src/models/activity_group.js
@@ -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',
+ }),
+ ],
+});
diff --git a/addons/mail/static/src/models/activity_group_view.js b/addons/mail/static/src/models/activity_group_view.js
new file mode 100644
index 00000000000..a77948b43bb
--- /dev/null
+++ b/addons/mail/static/src/models/activity_group_view.js
@@ -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,
+ }),
+ },
+});
diff --git a/addons/mail/static/src/models/activity_menu_view.js b/addons/mail/static/src/models/activity_menu_view.js
new file mode 100644
index 00000000000..7df52d85ee4
--- /dev/null
+++ b/addons/mail/static/src/models/activity_menu_view.js
@@ -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,
+ }),
+ },
+});
diff --git a/addons/mail/static/src/models/ir_model.js b/addons/mail/static/src/models/ir_model.js
new file mode 100644
index 00000000000..8efcb543023
--- /dev/null
+++ b/addons/mail/static/src/models/ir_model.js
@@ -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(),
+ },
+});
diff --git a/addons/mail/static/src/models/messaging_notification_handler.js b/addons/mail/static/src/models/messaging_notification_handler.js
index 499bc1439de..8d343925017 100644
--- a/addons/mail/static/src/models/messaging_notification_handler.js
+++ b/addons/mail/static/src/models/messaging_notification_handler.js
@@ -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':
diff --git a/addons/mail/static/src/models/user.js b/addons/mail/static/src/models/user.js
index 41819439a07..ed78e421ab5 100644
--- a/addons/mail/static/src/models/user.js
+++ b/addons/mail/static/src/models/user.js
@@ -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,
diff --git a/addons/mail/static/src/scss/composer.scss b/addons/mail/static/src/scss/composer.scss
index aca5a63d672..33a61c521ba 100644
--- a/addons/mail/static/src/scss/composer.scss
+++ b/addons/mail/static/src/scss/composer.scss
@@ -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);
diff --git a/addons/mail/static/src/scss/discuss.scss b/addons/mail/static/src/scss/discuss.scss
deleted file mode 100644
index 9b334b34511..00000000000
--- a/addons/mail/static/src/scss/discuss.scss
+++ /dev/null
@@ -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;
- }
- }
- }
-}
diff --git a/addons/mail/static/src/scss/systray.scss b/addons/mail/static/src/scss/systray.scss
deleted file mode 100644
index fc481b823d2..00000000000
--- a/addons/mail/static/src/scss/systray.scss
+++ /dev/null
@@ -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;
- }
- }
- }
-}
diff --git a/addons/mail/static/src/scss/thread.scss b/addons/mail/static/src/scss/thread.scss
index 6973d93c275..4c2b3b071e4 100644
--- a/addons/mail/static/src/scss/thread.scss
+++ b/addons/mail/static/src/scss/thread.scss
@@ -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) {
diff --git a/addons/mail/static/src/widgets/common.xml b/addons/mail/static/src/widgets/common.xml
deleted file mode 100644
index ad554bf5721..00000000000
--- a/addons/mail/static/src/widgets/common.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/addons/mail/static/src/xml/systray.xml b/addons/mail/static/src/xml/systray.xml
deleted file mode 100644
index c1b8188095d..00000000000
--- a/addons/mail/static/src/xml/systray.xml
+++ /dev/null
@@ -1,61 +0,0 @@
-
-
-
-
-
-
-
-
- Congratulations, you're done with your activities.
-
-
-
-
-
-
![Activity]()
-
-
-
-
-
- 0 Late
-
- 0 Today
-
- 0 Future
-
-
-
-
-
-
-
-
-
-
diff --git a/addons/mail/static/tests/helpers/mock_server.js b/addons/mail/static/tests/helpers/mock_server.js
index 8b9fcd9be48..8febe7422e6 100644
--- a/addons/mail/static/tests/helpers/mock_server.js
+++ b/addons/mail/static/tests/helpers/mock_server.js
@@ -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,
diff --git a/addons/mass_mailing_sms/models/res_users.py b/addons/mass_mailing_sms/models/res_users.py
index 8c23887c537..8e5a4eb730a 100644
--- a/addons/mass_mailing_sms/models/res_users.py
+++ b/addons/mass_mailing_sms/models/res_users.py
@@ -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',
diff --git a/addons/note/__manifest__.py b/addons/note/__manifest__.py
index a6e0dcca805..ed4a5fd6d65 100644
--- a/addons/note/__manifest__.py
+++ b/addons/note/__manifest__.py
@@ -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',
diff --git a/addons/note/models/res_users.py b/addons/note/models/res_users.py
index 0a56ba84005..5e1e9a772fc 100644
--- a/addons/note/models/res_users.py
+++ b/addons/note/models/res_users.py
@@ -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',
diff --git a/addons/note/static/src/components/activity_menu_view/activity_menu_view.js b/addons/note/static/src/components/activity_menu_view/activity_menu_view.js
new file mode 100644
index 00000000000..760ed692abf
--- /dev/null
+++ b/addons/note/static/src/components/activity_menu_view/activity_menu_view.js
@@ -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,
+});
diff --git a/addons/note/static/src/components/activity_menu_view/activity_menu_view.xml b/addons/note/static/src/components/activity_menu_view/activity_menu_view.xml
new file mode 100644
index 00000000000..a9f0567abee
--- /dev/null
+++ b/addons/note/static/src/components/activity_menu_view/activity_menu_view.xml
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+
+
+
diff --git a/addons/note/static/src/js/systray_activity_menu.js b/addons/note/static/src/js/systray_activity_menu.js
deleted file mode 100644
index 654de17a71d..00000000000
--- a/addons/note/static/src/js/systray_activity_menu.js
+++ /dev/null
@@ -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();
- }
- },
-});
diff --git a/addons/note/static/src/models/activity_group.js b/addons/note/static/src/models/activity_group.js
new file mode 100644
index 00000000000..32e6ed46063
--- /dev/null
+++ b/addons/note/static/src/models/activity_group.js
@@ -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',
+ }),
+});
diff --git a/addons/note/static/src/models/activity_menu_view.js b/addons/note/static/src/models/activity_menu_view.js
new file mode 100644
index 00000000000..202cb8ccfae
--- /dev/null
+++ b/addons/note/static/src/models/activity_menu_view.js
@@ -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(),
+});
diff --git a/addons/note/static/src/scss/note.scss b/addons/note/static/src/scss/note.scss
index a30808dd143..90316aa14b7 100644
--- a/addons/note/static/src/scss/note.scss
+++ b/addons/note/static/src/scss/note.scss
@@ -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;
- }
}
diff --git a/addons/note/static/src/xml/systray.xml b/addons/note/static/src/xml/systray.xml
deleted file mode 100644
index a4cb6beecac..00000000000
--- a/addons/note/static/src/xml/systray.xml
+++ /dev/null
@@ -1,32 +0,0 @@
-
-
-
-
-
-
-
-

-
-
-
-
Add a note
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/addons/note/static/tests/helpers/mock_server.js b/addons/note/static/tests/helpers/mock_server.js
index 9b41cf2ffa1..27b57a30f24 100644
--- a/addons/note/static/tests/helpers/mock_server.js
+++ b/addons/note/static/tests/helpers/mock_server.js
@@ -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',
diff --git a/addons/note/static/tests/systray_activity_menu_tests.js b/addons/note/static/tests/systray_activity_menu_tests.js
index 4ffe1db627d..bd8f0f70cde 100644
--- a/addons/note/static/tests/systray_activity_menu_tests.js
+++ b/addons/note/static/tests/systray_activity_menu_tests.js
@@ -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');
});
});
diff --git a/addons/test_discuss_full/__manifest__.py b/addons/test_discuss_full/__manifest__.py
index 8667173d3d7..7e9acd93eeb 100644
--- a/addons/test_discuss_full/__manifest__.py
+++ b/addons/test_discuss_full/__manifest__.py
@@ -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,
diff --git a/addons/test_mail/static/tests/systray_activity_menu_tests.js b/addons/test_mail/static/tests/systray_activity_menu_tests.js
index 42031209304..ba6686c4c01 100644
--- a/addons/test_mail/static/tests/systray_activity_menu_tests.js
+++ b/addons/test_mail/static/tests/systray_activity_menu_tests.js
@@ -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"
);
});