[IMP] mail: ease activities management by adding a shortcut-counter in top menu
This commit improves the use of activities through various applications by adding a counter of activities displaying next activities grouped by application. For each application activities are divided into late, today and future activities in order to achieve a better management. This commit also merges Inbox and Messaging icons in topbar to keep only one displaying All / Chat / Channels messages.
This commit is contained in:
committed by
Thibault Delavallée
parent
f5c8bd30a3
commit
07f1e9025d
@@ -132,16 +132,35 @@ class MailActivity(models.Model):
|
|||||||
def create(self, values):
|
def create(self, values):
|
||||||
activity = super(MailActivity, self).create(values)
|
activity = super(MailActivity, self).create(values)
|
||||||
self.env[activity.res_model].browse(activity.res_id).message_subscribe(partner_ids=[activity.user_id.partner_id.id])
|
self.env[activity.res_model].browse(activity.res_id).message_subscribe(partner_ids=[activity.user_id.partner_id.id])
|
||||||
|
self.env['bus.bus'].sendone(
|
||||||
|
(self._cr.dbname, 'res.partner', activity.user_id.partner_id.id),
|
||||||
|
{'type': 'activity_updated', 'activity_created': True})
|
||||||
return activity
|
return activity
|
||||||
|
|
||||||
@api.multi
|
@api.multi
|
||||||
def write(self, values):
|
def write(self, values):
|
||||||
res = super(MailActivity, self).write(values)
|
res = super(MailActivity, self).write(values)
|
||||||
if values.get('user_id'):
|
if values.get('user_id'):
|
||||||
|
pre_responsibles = self.mapped('user_id.partner_id')
|
||||||
for activity in self:
|
for activity in self:
|
||||||
self.env[activity.res_model].browse(activity.res_id).message_subscribe(partner_ids=[activity.user_id.partner_id.id])
|
self.env[activity.res_model].browse(activity.res_id).message_subscribe(partner_ids=[activity.user_id.partner_id.id])
|
||||||
|
self.env['bus.bus'].sendone(
|
||||||
|
(self._cr.dbname, 'res.partner', activity.user_id.partner_id.id),
|
||||||
|
{'type': 'activity_updated', 'activity_created': True})
|
||||||
|
for partner in pre_responsibles:
|
||||||
|
self.env['bus.bus'].sendone(
|
||||||
|
(self._cr.dbname, 'res.partner', partner.id),
|
||||||
|
{'type': 'activity_updated', 'activity_deleted': True})
|
||||||
return res
|
return res
|
||||||
|
|
||||||
|
@api.multi
|
||||||
|
def unlink(self):
|
||||||
|
for activity in self:
|
||||||
|
self.env['bus.bus'].sendone(
|
||||||
|
(self._cr.dbname, 'res.partner', activity.user_id.partner_id.id),
|
||||||
|
{'type': 'activity_updated', 'activity_deleted': True})
|
||||||
|
return super(MailActivity, self).unlink()
|
||||||
|
|
||||||
@api.multi
|
@api.multi
|
||||||
def action_done(self, feedback=False):
|
def action_done(self, feedback=False):
|
||||||
message = self.env['mail.message']
|
message = self.env['mail.message']
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import logging
|
|||||||
|
|
||||||
from email.utils import formataddr
|
from email.utils import formataddr
|
||||||
|
|
||||||
from odoo import _, api, fields, models, SUPERUSER_ID, tools
|
from odoo import _, api, fields, models, modules, SUPERUSER_ID, tools
|
||||||
from odoo.exceptions import UserError, AccessError
|
from odoo.exceptions import UserError, AccessError
|
||||||
from odoo.osv import expression
|
from odoo.osv import expression
|
||||||
from odoo.tools import pycompat
|
from odoo.tools import pycompat
|
||||||
@@ -420,6 +420,8 @@ class Message(models.Model):
|
|||||||
for message in message_values:
|
for message in message_values:
|
||||||
message['is_note'] = message['subtype_id'] and subtypes_dict[message['subtype_id'][0]]['internal']
|
message['is_note'] = message['subtype_id'] and subtypes_dict[message['subtype_id'][0]]['internal']
|
||||||
message['subtype_description'] = message['subtype_id'] and subtypes_dict[message['subtype_id'][0]]['description']
|
message['subtype_description'] = message['subtype_id'] and subtypes_dict[message['subtype_id'][0]]['description']
|
||||||
|
if message['model']:
|
||||||
|
message['module_icon'] = modules.module.get_module_icon(self.env[message['model']]._original_module)
|
||||||
return message_values
|
return message_values
|
||||||
|
|
||||||
#------------------------------------------------------
|
#------------------------------------------------------
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||||
|
|
||||||
from odoo import _, api, exceptions, fields, models
|
from odoo import _, api, exceptions, fields, models, modules
|
||||||
from odoo.tools import pycompat
|
from odoo.tools import pycompat
|
||||||
|
|
||||||
|
|
||||||
@@ -119,6 +119,36 @@ class Users(models.Model):
|
|||||||
def message_get_suggested_recipients(self):
|
def message_get_suggested_recipients(self):
|
||||||
return dict((res_id, list()) for res_id in self._ids)
|
return dict((res_id, list()) for res_id in self._ids)
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def activity_user_count(self):
|
||||||
|
query = """SELECT m.name, count(*), act.res_model as model,
|
||||||
|
CASE
|
||||||
|
WHEN now()::date - act.date_deadline::date = 0 Then 'today'
|
||||||
|
WHEN now()::date - act.date_deadline::date > 0 Then 'overdue'
|
||||||
|
WHEN now()::date - act.date_deadline::date < 0 Then 'planned'
|
||||||
|
END AS states
|
||||||
|
FROM mail_activity AS act
|
||||||
|
JOIN ir_model AS m ON act.res_model_id = m.id
|
||||||
|
WHERE user_id = %s
|
||||||
|
GROUP BY m.name, states, act.res_model;
|
||||||
|
"""
|
||||||
|
self.env.cr.execute(query, [self.env.uid])
|
||||||
|
activity_data = self.env.cr.dictfetchall()
|
||||||
|
|
||||||
|
user_activities = {}
|
||||||
|
for activity in activity_data:
|
||||||
|
if not user_activities.get(activity['model']):
|
||||||
|
user_activities[activity['model']] = {
|
||||||
|
'name': activity['name'],
|
||||||
|
'model': activity['model'],
|
||||||
|
'icon': modules.module.get_module_icon(self.env[activity['model']]._original_module),
|
||||||
|
'total_count': 0, 'today_count': 0, 'overdue_count': 0, 'planned_count': 0,
|
||||||
|
}
|
||||||
|
user_activities[activity['model']]['%s_count' % activity['states']] += activity['count']
|
||||||
|
user_activities[activity['model']]['total_count'] += activity['count']
|
||||||
|
|
||||||
|
return user_activities.values()
|
||||||
|
|
||||||
|
|
||||||
class res_groups_mail_channel(models.Model):
|
class res_groups_mail_channel(models.Model):
|
||||||
""" Update of res.groups class
|
""" Update of res.groups class
|
||||||
|
|||||||
@@ -137,6 +137,7 @@ function make_message (data) {
|
|||||||
model: data.model,
|
model: data.model,
|
||||||
res_id: data.res_id,
|
res_id: data.res_id,
|
||||||
url: session.url("/mail/view?message_id=" + data.id),
|
url: session.url("/mail/view?message_id=" + data.id),
|
||||||
|
module_icon:data.module_icon,
|
||||||
};
|
};
|
||||||
|
|
||||||
_.each(_.keys(emoji_substitutions), function (key) {
|
_.each(_.keys(emoji_substitutions), function (key) {
|
||||||
@@ -476,6 +477,8 @@ function on_partner_notification (data) {
|
|||||||
on_channel_seen_notification(data);
|
on_channel_seen_notification(data);
|
||||||
} else if (data.info === 'transient_message') {
|
} else if (data.info === 'transient_message') {
|
||||||
on_transient_message_notification(data);
|
on_transient_message_notification(data);
|
||||||
|
} else if (data.type === 'activity_updated') {
|
||||||
|
onActivityUpdateNodification(data);
|
||||||
} else {
|
} else {
|
||||||
on_chat_session_notification(data);
|
on_chat_session_notification(data);
|
||||||
}
|
}
|
||||||
@@ -599,6 +602,9 @@ function on_transient_message_notification (data) {
|
|||||||
add_message(data);
|
add_message(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onActivityUpdateNodification (data) {
|
||||||
|
chat_manager.bus.trigger('activity_updated', data);
|
||||||
|
}
|
||||||
// Public interface
|
// Public interface
|
||||||
//----------------------------------------------------------------------------------
|
//----------------------------------------------------------------------------------
|
||||||
var ChatManager = Class.extend(Mixins.EventDispatcherMixin, ServicesMixin, {
|
var ChatManager = Class.extend(Mixins.EventDispatcherMixin, ServicesMixin, {
|
||||||
@@ -1128,6 +1134,20 @@ var ChatManager = Class.extend(Mixins.EventDispatcherMixin, ServicesMixin, {
|
|||||||
|
|
||||||
get_channels_preview: function (channels) {
|
get_channels_preview: function (channels) {
|
||||||
var channels_preview = _.map(channels, function (channel) {
|
var channels_preview = _.map(channels, function (channel) {
|
||||||
|
if (channel.channel_ids && _.contains(channel.channel_ids,"channel_inbox")) {
|
||||||
|
// map inbox(mail_message) data with existing channel/chat template
|
||||||
|
var info = _.pick(channel, 'id', 'body', 'avatar_src', 'res_id', 'model', 'module_icon', 'subject','date', 'record_name', 'status', 'displayed_author', 'email_from', 'unread_counter');
|
||||||
|
info.last_message = {
|
||||||
|
body: info.body,
|
||||||
|
date: info.date,
|
||||||
|
displayed_author: info.displayed_author || info.email_from,
|
||||||
|
};
|
||||||
|
info.name = info.record_name || info.subject || info.displayed_author;
|
||||||
|
info.image_src = info.module_icon || info.avatar_src;
|
||||||
|
info.message_id = info.id;
|
||||||
|
info.id = 'channel_inbox';
|
||||||
|
return info;
|
||||||
|
}
|
||||||
var info = _.pick(channel, 'id', 'is_chat', 'name', 'status', 'unread_counter');
|
var info = _.pick(channel, 'id', 'is_chat', 'name', 'status', 'unread_counter');
|
||||||
info.last_message = _.last(channel.cache['[]'].messages);
|
info.last_message = _.last(channel.cache['[]'].messages);
|
||||||
if (!info.is_chat) {
|
if (!info.is_chat) {
|
||||||
|
|||||||
@@ -268,6 +268,7 @@ var ChatAction = Widget.extend(ControlPanelMixin, {
|
|||||||
chat_manager.bus.on('update_starred', self, self.throttled_renderSidebar);
|
chat_manager.bus.on('update_starred', self, self.throttled_renderSidebar);
|
||||||
chat_manager.bus.on('update_channel_unread_counter', self, self.throttled_renderSidebar);
|
chat_manager.bus.on('update_channel_unread_counter', self, self.throttled_renderSidebar);
|
||||||
chat_manager.bus.on('update_dm_presence', self, self.throttled_renderSidebar);
|
chat_manager.bus.on('update_dm_presence', self, self.throttled_renderSidebar);
|
||||||
|
chat_manager.bus.on('activity_updated', self, self.throttled_renderSidebar);
|
||||||
self.thread.$el.on("scroll", null, _.debounce(function () {
|
self.thread.$el.on("scroll", null, _.debounce(function () {
|
||||||
if (self.thread.is_at_bottom()) {
|
if (self.thread.is_at_bottom()) {
|
||||||
chat_manager.mark_channel_as_seen(self.channel);
|
chat_manager.mark_channel_as_seen(self.channel);
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ odoo.define('mail.systray', function (require) {
|
|||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
var core = require('web.core');
|
var core = require('web.core');
|
||||||
|
var framework = require('web.framework');
|
||||||
|
var session = require('web.session');
|
||||||
var SystrayMenu = require('web.SystrayMenu');
|
var SystrayMenu = require('web.SystrayMenu');
|
||||||
var Widget = require('web.Widget');
|
var Widget = require('web.Widget');
|
||||||
|
|
||||||
@@ -9,39 +11,6 @@ var chat_manager = require('mail.chat_manager');
|
|||||||
|
|
||||||
var QWeb = core.qweb;
|
var QWeb = core.qweb;
|
||||||
|
|
||||||
/**
|
|
||||||
* Menu item appended in the systray part of the navbar, redirects to the Inbox in Discuss
|
|
||||||
* Also displays the needaction counter (= Inbox counter)
|
|
||||||
*/
|
|
||||||
var InboxItem = Widget.extend({
|
|
||||||
template:'mail.chat.InboxItem',
|
|
||||||
events: {
|
|
||||||
"click": "on_click",
|
|
||||||
},
|
|
||||||
start: function () {
|
|
||||||
this.$needaction_counter = this.$('.o_notification_counter');
|
|
||||||
chat_manager.bus.on("update_needaction", this, this.update_counter);
|
|
||||||
chat_manager.is_ready.then(this.update_counter.bind(this));
|
|
||||||
return this._super();
|
|
||||||
},
|
|
||||||
update_counter: function () {
|
|
||||||
var counter = chat_manager.get_needaction_counter();
|
|
||||||
this.$needaction_counter.text(counter);
|
|
||||||
this.$el.toggleClass('o_no_notification', !counter);
|
|
||||||
},
|
|
||||||
on_click: function (event) {
|
|
||||||
event.preventDefault();
|
|
||||||
chat_manager.is_ready.then(this.discuss_redirect.bind(this));
|
|
||||||
},
|
|
||||||
discuss_redirect: _.debounce(function () {
|
|
||||||
var self = this;
|
|
||||||
this.do_action('mail.mail_channel_action_client_chat', {clear_breadcrumbs: true}).then(function () {
|
|
||||||
self.trigger_up('hide_app_switcher');
|
|
||||||
core.bus.trigger('change_menu_section', chat_manager.get_discuss_menu_id());
|
|
||||||
});
|
|
||||||
}, 1000, true),
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Menu item appended in the systray part of the navbar
|
* Menu item appended in the systray part of the navbar
|
||||||
*
|
*
|
||||||
@@ -56,12 +25,13 @@ var MessagingMenu = Widget.extend({
|
|||||||
"click": "on_click",
|
"click": "on_click",
|
||||||
"click .o_filter_button": "on_click_filter_button",
|
"click .o_filter_button": "on_click_filter_button",
|
||||||
"click .o_new_message": "on_click_new_message",
|
"click .o_new_message": "on_click_new_message",
|
||||||
"click .o_mail_channel_preview": "on_click_channel",
|
"click .o_mail_channel_preview": "_onClickChannel",
|
||||||
},
|
},
|
||||||
start: function () {
|
start: function () {
|
||||||
this.$filter_buttons = this.$('.o_filter_button');
|
this.$filter_buttons = this.$('.o_filter_button');
|
||||||
this.$channels_preview = this.$('.o_mail_navbar_dropdown_channels');
|
this.$channels_preview = this.$('.o_mail_navbar_dropdown_channels');
|
||||||
this.filter = false;
|
this.filter = false;
|
||||||
|
chat_manager.bus.on("update_needaction", this, this.update_counter);
|
||||||
chat_manager.bus.on("update_channel_unread_counter", this, this.update_counter);
|
chat_manager.bus.on("update_channel_unread_counter", this, this.update_counter);
|
||||||
chat_manager.is_ready.then(this.update_counter.bind(this));
|
chat_manager.is_ready.then(this.update_counter.bind(this));
|
||||||
return this._super();
|
return this._super();
|
||||||
@@ -70,10 +40,9 @@ var MessagingMenu = Widget.extend({
|
|||||||
return this.$el.hasClass('open');
|
return this.$el.hasClass('open');
|
||||||
},
|
},
|
||||||
update_counter: function () {
|
update_counter: function () {
|
||||||
var counter = chat_manager.get_unread_conversation_counter();
|
var counter = chat_manager.get_needaction_counter() + chat_manager.get_unread_conversation_counter();
|
||||||
this.$('.o_notification_counter').text(counter);
|
this.$('.o_notification_counter').text(counter);
|
||||||
this.$el.toggleClass('o_no_notification', !counter);
|
this.$el.toggleClass('o_no_notification', !counter);
|
||||||
this.$el.toggleClass('o_unread_chat', !!chat_manager.get_chat_unread_counter());
|
|
||||||
if (this.is_open()) {
|
if (this.is_open()) {
|
||||||
this.update_channels_preview();
|
this.update_channels_preview();
|
||||||
}
|
}
|
||||||
@@ -94,8 +63,23 @@ var MessagingMenu = Widget.extend({
|
|||||||
return channel.type !== 'static';
|
return channel.type !== 'static';
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
chat_manager.get_messages({channel_id: 'channel_inbox'}).then(function(result) {
|
||||||
chat_manager.get_channels_preview(channels).then(self._render_channels_preview.bind(self));
|
var res = [];
|
||||||
|
_.each(result, function (message) {
|
||||||
|
message.unread_counter = 1;
|
||||||
|
var duplicatedMessage = _.findWhere(res, {model: message.model, 'res_id': message.res_id});
|
||||||
|
if (message.model && message.res_id && duplicatedMessage) {
|
||||||
|
message.unread_counter = duplicatedMessage.unread_counter + 1;
|
||||||
|
res[_.findIndex(res, duplicatedMessage)] = message;
|
||||||
|
} else {
|
||||||
|
res.push(message);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (self.filter === 'channel_inbox' || !self.filter) {
|
||||||
|
channels = _.union(channels, res);
|
||||||
|
}
|
||||||
|
chat_manager.get_channels_preview(channels).then(self._render_channels_preview.bind(self));
|
||||||
|
});
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
_render_channels_preview: function (channels_preview) {
|
_render_channels_preview: function (channels_preview) {
|
||||||
@@ -136,16 +120,172 @@ var MessagingMenu = Widget.extend({
|
|||||||
on_click_new_message: function () {
|
on_click_new_message: function () {
|
||||||
chat_manager.bus.trigger('open_chat');
|
chat_manager.bus.trigger('open_chat');
|
||||||
},
|
},
|
||||||
on_click_channel: function (event) {
|
|
||||||
var channel_id = $(event.currentTarget).data('channel_id');
|
// Handlers
|
||||||
var channel = chat_manager.get_channel(channel_id);
|
|
||||||
if (channel) {
|
/**
|
||||||
chat_manager.open_channel(channel);
|
* When a channel is clicked on, we want to open chat/channel window
|
||||||
|
* If channel is inbox then redirect to that record view
|
||||||
|
* If record not linked redirect to Inbox
|
||||||
|
* @private
|
||||||
|
* @param {MouseEvent} event
|
||||||
|
*/
|
||||||
|
_onClickChannel: function (event) {
|
||||||
|
var channelID = $(event.currentTarget).data('channel_id');
|
||||||
|
if (channelID == 'channel_inbox') {
|
||||||
|
var resID = $(event.currentTarget).data('res_id');
|
||||||
|
var resModel = $(event.currentTarget).data('res_model');
|
||||||
|
if (resModel && resID) {
|
||||||
|
this.do_action({
|
||||||
|
type: 'ir.actions.act_window',
|
||||||
|
res_model: resModel,
|
||||||
|
views: [[false, 'form'], [false, 'kanban']],
|
||||||
|
res_id: resID
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// if no model linked redirect to inbox
|
||||||
|
framework.redirect('mail/view?message_id=channel_inbox');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
var channel = chat_manager.get_channel(channelID);
|
||||||
|
if (channel) {
|
||||||
|
chat_manager.open_channel(channel);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
SystrayMenu.Items.push(MessagingMenu);
|
/**
|
||||||
SystrayMenu.Items.push(InboxItem);
|
* Menu item appended in the systray part of the navbar, redirects to the next activities of all app
|
||||||
|
*/
|
||||||
|
var ActivityMenu = Widget.extend({
|
||||||
|
template:'mail.chat.ActivityMenu',
|
||||||
|
events: {
|
||||||
|
"click": "_onActivityMenuClick",
|
||||||
|
"click .o_activity_filter_button, .o_mail_channel_preview": "_onActivityFilterClick",
|
||||||
|
},
|
||||||
|
start: function () {
|
||||||
|
this.$activities_preview = this.$('.o_mail_navbar_dropdown_channels');
|
||||||
|
chat_manager.bus.on("activity_updated", this, this._updateCounter);
|
||||||
|
chat_manager.is_ready.then(this._updateCounter.bind(this));
|
||||||
|
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: 'activity_user_count',
|
||||||
|
}).then(function (data) {
|
||||||
|
self.isCounterUpdated = false;
|
||||||
|
self.activities = data;
|
||||||
|
self.activityCounter = _.reduce(data, function(total_count, p_data){ return total_count + p_data.total_count; }, 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'
|
||||||
|
});
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Check wether activity systray dropdown is open or not
|
||||||
|
* @private
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
_isOpen: function () {
|
||||||
|
return this.$el.hasClass('open');
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Update(render) activity system tray view on activity updation.
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
_updateActivityPreview: function () {
|
||||||
|
var self = this;
|
||||||
|
self._getActivityData().then(function (){
|
||||||
|
self.$activities_preview.html(QWeb.render('mail.chat.ActivityMenuPreview', {
|
||||||
|
activities : self.activities
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* 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.isCounterUpdated = true;
|
||||||
|
this.$('.o_notification_counter').text(this.activityCounter);
|
||||||
|
this.$el.toggleClass('o_no_notification', !this.activityCounter);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
// Handlers
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Redirect to particular model view
|
||||||
|
* @private
|
||||||
|
* @param {MouseEvent} event
|
||||||
|
*/
|
||||||
|
_onActivityFilterClick: function (event) {
|
||||||
|
event.stopPropagation();
|
||||||
|
var $target = $(event.currentTarget);
|
||||||
|
var context = {};
|
||||||
|
context['search_default_activities_' + $target.data('filter')] = 1;
|
||||||
|
this.do_action({
|
||||||
|
type: 'ir.actions.act_window',
|
||||||
|
name: $target.data('model_name'),
|
||||||
|
res_model: $target.data('res_model'),
|
||||||
|
views: [[false, 'kanban'], [false, 'form']],
|
||||||
|
search_view_id: [false],
|
||||||
|
domain: [['activity_user_id', '=', session.uid]],
|
||||||
|
context:context,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* When menu clicked update activity preview if counter updated
|
||||||
|
* @private
|
||||||
|
* @param {MouseEvent} event
|
||||||
|
*/
|
||||||
|
_onActivityMenuClick: function () {
|
||||||
|
if (!this._isOpen() && this.isCounterUpdated) {
|
||||||
|
this._updateActivityPreview();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
SystrayMenu.Items.push(MessagingMenu);
|
||||||
|
SystrayMenu.Items.push(ActivityMenu);
|
||||||
|
|
||||||
|
// to test activity menu in qunit test cases we need it
|
||||||
|
return {
|
||||||
|
ActivityMenu: ActivityMenu,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|||||||
@@ -12,14 +12,19 @@
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
&.o_unread_chat > a > i {
|
|
||||||
color: @odoo-brand-optional;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.open .o_mail_navbar_dropdown {
|
&.open .o_mail_navbar_dropdown {
|
||||||
.o-flex-display();
|
.o-flex-display();
|
||||||
.o-flex-flow(column, nowrap);
|
.o-flex-flow(column, nowrap);
|
||||||
}
|
}
|
||||||
|
.o_notification_counter {
|
||||||
|
background: @odoo-brand-optional;
|
||||||
|
color: white;
|
||||||
|
padding: 0em 0.3em;
|
||||||
|
font-size: 0.7em;
|
||||||
|
top: 7%;
|
||||||
|
position: absolute;
|
||||||
|
margin: 0px 0px 0px 8px;
|
||||||
|
}
|
||||||
.o_mail_navbar_dropdown {
|
.o_mail_navbar_dropdown {
|
||||||
width: 350px;
|
width: 350px;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
@@ -107,12 +112,24 @@
|
|||||||
}
|
}
|
||||||
.o_last_message_preview {
|
.o_last_message_preview {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
max-height: 20px;
|
||||||
color: @odoo-main-color-muted;
|
color: @odoo-main-color-muted;
|
||||||
.o-text-overflow();
|
.o-text-overflow();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.o_activity_filter_button {
|
||||||
|
.btn-link;
|
||||||
|
padding: 2px;
|
||||||
|
}
|
||||||
|
.o_no_activity {
|
||||||
|
cursor: initial;
|
||||||
|
.o-align-items(center);
|
||||||
|
color: grey;
|
||||||
|
opacity: 0.5;
|
||||||
|
padding: 3px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,18 +1,10 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<templates>
|
<templates>
|
||||||
|
|
||||||
<t t-name="mail.chat.InboxItem">
|
|
||||||
<li class="o_mail_navbar_item">
|
|
||||||
<a href="#" title="Inbox">
|
|
||||||
<i class="fa fa-at"/> <span class="o_notification_counter"/>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
</t>
|
|
||||||
|
|
||||||
<t t-name="mail.chat.MessagingMenu">
|
<t t-name="mail.chat.MessagingMenu">
|
||||||
<li class="o_mail_navbar_item">
|
<li class="o_mail_navbar_item">
|
||||||
<a class="dropdown-toggle" data-toggle="dropdown" aria-expanded="false" title="Conversations" href="#">
|
<a class="dropdown-toggle" data-toggle="dropdown" aria-expanded="false" title="Conversations" href="#">
|
||||||
<i class="fa fa-comments"/> <span class="o_notification_counter"/>
|
<i class="fa fa-comments"/> <span class="o_notification_counter badge"/>
|
||||||
</a>
|
</a>
|
||||||
<ul class="o_mail_navbar_dropdown dropdown-menu" role="menu">
|
<ul class="o_mail_navbar_dropdown dropdown-menu" role="menu">
|
||||||
<li class="o_mail_navbar_dropdown_top">
|
<li class="o_mail_navbar_dropdown_top">
|
||||||
@@ -29,9 +21,14 @@
|
|||||||
</t>
|
</t>
|
||||||
|
|
||||||
<t t-name="mail.chat.ChannelsPreview">
|
<t t-name="mail.chat.ChannelsPreview">
|
||||||
|
<t t-if="_.isEmpty(channels)">
|
||||||
|
<li class="text-center o_no_activity mt16">
|
||||||
|
<span>No discussion yet...</span>
|
||||||
|
</li>
|
||||||
|
</t>
|
||||||
<t t-foreach="channels" t-as="channel">
|
<t t-foreach="channels" t-as="channel">
|
||||||
<div t-attf-class="o_mail_channel_preview #{channel.unread_counter ? 'o_channel_unread' : ''}"
|
<div t-attf-class="o_mail_channel_preview #{channel.unread_counter ? 'o_channel_unread' : ''}"
|
||||||
t-att-data-channel_id="channel.id">
|
t-att-data-channel_id="channel.id" t-att-data-res_id="channel.res_id" t-att-data-res_model="channel.model">
|
||||||
<img class="o_channel_image" t-att-src="channel.image_src"/>
|
<img class="o_channel_image" t-att-src="channel.image_src"/>
|
||||||
<div class="o_channel_info">
|
<div class="o_channel_info">
|
||||||
<div class="o_channel_title">
|
<div class="o_channel_title">
|
||||||
@@ -53,5 +50,43 @@
|
|||||||
</t>
|
</t>
|
||||||
</t>
|
</t>
|
||||||
|
|
||||||
|
<t t-name="mail.chat.ActivityMenuPreview">
|
||||||
|
<t t-if="_.isEmpty(activities)">
|
||||||
|
<li class="text-center o_no_activity">
|
||||||
|
<span>No activities planned.</span>
|
||||||
|
</li>
|
||||||
|
</t>
|
||||||
|
<t t-foreach="activities" t-as="activity">
|
||||||
|
<div class="o_mail_channel_preview" t-att-data-res_model="activity.model" t-att-data-model_name="activity.name" data-filter='my'>
|
||||||
|
<img class="o_channel_image" t-att-src="activity.icon"/>
|
||||||
|
<div class="o_channel_info">
|
||||||
|
<div class="o_channel_title">
|
||||||
|
<span class="o_channel_name">
|
||||||
|
<t t-esc="activity.name"/>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<button t-if="activity.overdue_count" type="button" class="btn btn-link o_activity_filter_button mr16" t-att-data-res_model="activity.model" t-att-data-model_name="activity.name" data-filter='overdue'><t t-esc="activity.overdue_count"/> Late </button>
|
||||||
|
<span t-if="!activity.overdue_count" class="o_no_activity mr16">0 Late </span>
|
||||||
|
<button t-if="activity.today_count" type="button" class="btn btn-link o_activity_filter_button mr16" t-att-data-res_model="activity.model" t-att-data-model_name="activity.name" data-filter='today'> <t t-esc="activity.today_count"/> Today </button>
|
||||||
|
<span t-if="!activity.today_count" class="o_no_activity mr16">0 Today </span>
|
||||||
|
<button t-if="activity.planned_count" type="button" class="btn btn-link o_activity_filter_button pull-right mr16" t-att-data-res_model="activity.model" t-att-data-model_name="activity.name" data-filter='upcoming_all'> <t t-esc="activity.planned_count"/> Future </button>
|
||||||
|
<span t-if="!activity.planned_count" class="o_no_activity pull-right mr16">0 Future</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</t>
|
||||||
|
</t>
|
||||||
|
|
||||||
|
<t t-name="mail.chat.ActivityMenu">
|
||||||
|
<li class="o_mail_navbar_item">
|
||||||
|
<a class="dropdown-toggle" data-toggle="dropdown" aria-expanded="false" title="Activities" href="#">
|
||||||
|
<i class="fa fa-check-square-o"/> <span class="o_notification_counter badge"/>
|
||||||
|
</a>
|
||||||
|
<ul class="o_mail_navbar_dropdown dropdown-menu" role="menu">
|
||||||
|
<li class="o_mail_navbar_dropdown_channels"/>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
</t>
|
||||||
</templates>
|
</templates>
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,90 @@
|
|||||||
|
odoo.define('mail.systray_tests', function (require) {
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
var systray = require('mail.systray');
|
||||||
|
var testUtils = require('web.test_utils');
|
||||||
|
|
||||||
|
|
||||||
|
QUnit.module('mail', {}, function () {
|
||||||
|
|
||||||
|
QUnit.module('ActivityMenu', {
|
||||||
|
beforeEach: function () {
|
||||||
|
|
||||||
|
this.data = {
|
||||||
|
'mail.activity.menu': {
|
||||||
|
fields: {
|
||||||
|
name: { type: "char" },
|
||||||
|
res_model: { type: "char" },
|
||||||
|
planned_count: { type: "integer"},
|
||||||
|
today_count: { type: "integer"},
|
||||||
|
overdue_count: { type: "integer"},
|
||||||
|
total_count: { type: "integer"},
|
||||||
|
},
|
||||||
|
records: [{
|
||||||
|
name: "Contact",
|
||||||
|
res_model: "res.partner",
|
||||||
|
planned_count: 0,
|
||||||
|
today_count: 1,
|
||||||
|
overdue_count: 0,
|
||||||
|
total_count: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Task",
|
||||||
|
res_model: "project.task",
|
||||||
|
planned_count: 1,
|
||||||
|
today_count: 0,
|
||||||
|
overdue_count: 0,
|
||||||
|
total_count: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Issue",
|
||||||
|
res_model: "project.issue",
|
||||||
|
planned_count: 1,
|
||||||
|
today_count: 1,
|
||||||
|
overdue_count: 0,
|
||||||
|
total_count: 2,
|
||||||
|
}],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
QUnit.test('activity menu widget: menu with no records', function (assert) {
|
||||||
|
assert.expect(1);
|
||||||
|
|
||||||
|
var activityMenu = new systray.ActivityMenu();
|
||||||
|
testUtils.addMockEnvironment(activityMenu, {
|
||||||
|
mockRPC: function (route, args) {
|
||||||
|
if (args.method === 'activity_user_count') {
|
||||||
|
return $.when([]);
|
||||||
|
}
|
||||||
|
return this._super(route, args);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
activityMenu.appendTo($('#qunit-fixture'));
|
||||||
|
assert.ok(activityMenu.$('.o_no_activity').hasClass('o_no_activity'), "should not have instance of widget");
|
||||||
|
activityMenu.destroy();
|
||||||
|
});
|
||||||
|
|
||||||
|
QUnit.test('activity menu widget: activity menu with 3 records', function (assert) {
|
||||||
|
assert.expect(4);
|
||||||
|
var self = this;
|
||||||
|
var activityMenu = new systray.ActivityMenu();
|
||||||
|
|
||||||
|
testUtils.addMockEnvironment(activityMenu, {
|
||||||
|
mockRPC: function (route, args) {
|
||||||
|
if (args.method === 'activity_user_count') {
|
||||||
|
return $.when(self.data['mail.activity.menu']['records']);
|
||||||
|
}
|
||||||
|
return this._super(route, args);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
activityMenu.appendTo($('#qunit-fixture'));
|
||||||
|
assert.ok(activityMenu.$el.hasClass('o_mail_navbar_item'), 'should be the instance of widget');
|
||||||
|
assert.ok(activityMenu.$('.o_mail_channel_preview').hasClass('o_mail_channel_preview'), "should instance of widget");
|
||||||
|
assert.ok(activityMenu.$('.o_notification_counter').hasClass('o_notification_counter'), "widget should have notification counter");
|
||||||
|
assert.strictEqual(parseInt(activityMenu.el.innerText), 4, "widget should have 4 notification counter");
|
||||||
|
activityMenu.destroy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -40,6 +40,7 @@
|
|||||||
<template id="qunit_suite" name="mail_tests" inherit_id="web.qunit_suite">
|
<template id="qunit_suite" name="mail_tests" inherit_id="web.qunit_suite">
|
||||||
<xpath expr="//t[@t-set='head']" position="inside">
|
<xpath expr="//t[@t-set='head']" position="inside">
|
||||||
<script type="text/javascript" src="/mail/static/tests/chatter_tests.js"></script>
|
<script type="text/javascript" src="/mail/static/tests/chatter_tests.js"></script>
|
||||||
|
<script type="text/javascript" src="/mail/static/tests/systray_tests.js"></script>
|
||||||
<script type="text/javascript" src="/mail/static/tests/chat_window_test.js"></script>
|
<script type="text/javascript" src="/mail/static/tests/chat_window_test.js"></script>
|
||||||
</xpath>
|
</xpath>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
Reference in New Issue
Block a user