diff --git a/addons/calendar/__manifest__.py b/addons/calendar/__manifest__.py index 9ec631594e6..c735665be15 100644 --- a/addons/calendar/__manifest__.py +++ b/addons/calendar/__manifest__.py @@ -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': [ diff --git a/addons/calendar/models/res_users.py b/addons/calendar/models/res_users.py index bdb25f7567a..019aaede6dc 100644 --- a/addons/calendar/models/res_users.py +++ b/addons/calendar/models/res_users.py @@ -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) diff --git a/addons/calendar/static/src/components/activity_menu_view/activity_menu_view.xml b/addons/calendar/static/src/components/activity_menu_view/activity_menu_view.xml new file mode 100644 index 00000000000..975f8317385 --- /dev/null +++ b/addons/calendar/static/src/components/activity_menu_view/activity_menu_view.xml @@ -0,0 +1,24 @@ + + + + +
+ + +
+ + + + + All Day + + + + + +
+
+
+
+
+
diff --git a/addons/calendar/static/src/js/systray_activity_menu.js b/addons/calendar/static/src/js/systray_activity_menu.js deleted file mode 100644 index 28d1986fbb9..00000000000 --- a/addons/calendar/static/src/js/systray_activity_menu.js +++ /dev/null @@ -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); - } - }, -}); diff --git a/addons/calendar/static/src/models/activity_group.js b/addons/calendar/static/src/models/activity_group.js new file mode 100644 index 00000000000..57efcebbb22 --- /dev/null +++ b/addons/calendar/static/src/models/activity_group.js @@ -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', + }), +]); diff --git a/addons/calendar/static/src/models/activity_group_view.js b/addons/calendar/static/src/models/activity_group_view.js new file mode 100644 index 00000000000..fe22622b451 --- /dev/null +++ b/addons/calendar/static/src/models/activity_group_view.js @@ -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); + } + }, +}); diff --git a/addons/calendar/static/src/models/calendar_event.js b/addons/calendar/static/src/models/calendar_event.js new file mode 100644 index 00000000000..1277a23f388 --- /dev/null +++ b/addons/calendar/static/src/models/calendar_event.js @@ -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(), + }, +}); diff --git a/addons/calendar/static/src/scss/calendar.scss b/addons/calendar/static/src/scss/calendar.scss index dfa3b9752e3..ea5ff414863 100644 --- a/addons/calendar/static/src/scss/calendar.scss +++ b/addons/calendar/static/src/scss/calendar.scss @@ -57,7 +57,7 @@ } .o_meeting_filter { @include o-text-overflow(); - width: 64%; + min-width: 0; color: grey; vertical-align: top; &.o_meeting_bold { diff --git a/addons/calendar/static/src/xml/base_calendar.xml b/addons/calendar/static/src/xml/base_calendar.xml index eb0dc0b464f..8919bb7fc70 100644 --- a/addons/calendar/static/src/xml/base_calendar.xml +++ b/addons/calendar/static/src/xml/base_calendar.xml @@ -25,28 +25,6 @@ - - -
- - -
- - - - - All Day - - - - - -
-
-
-
-
-
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 @@ - - - - - - - - - - -
-
- Activity -
-
-
- - - -
- - - -
-
-
- - 0 Late - - 0 Today - - 0 Future -
-
-
-
-
- - -