diff --git a/addons/mail/models/mail_activity.py b/addons/mail/models/mail_activity.py index e1be4f3f250..bc17d710f79 100644 --- a/addons/mail/models/mail_activity.py +++ b/addons/mail/models/mail_activity.py @@ -590,6 +590,35 @@ class MailActivityMixin(models.AbstractModel): related='activity_ids.summary', readonly=False, search='_search_activity_summary', groups="base.group_user",) + activity_exception_decoration = fields.Selection([ + ('warning', 'Alert'), + ('danger', 'Error')], + compute='_compute_activity_exception_type', + search='_search_activity_exception_decoration', + help="Type of the exception activity on record.") + activity_exception_icon = fields.Char('Icon', help="Icon to indicate an exception activity.", + compute='_compute_activity_exception_type') + + @api.depends('activity_ids.activity_type_id.decoration_type', 'activity_ids.activity_type_id.icon') + def _compute_activity_exception_type(self): + # prefetch all activity types for all activities, this will avoid any query in loops + self.mapped('activity_ids.activity_type_id.decoration_type') + + for record in self: + activity_type_ids = record.activity_ids.mapped('activity_type_id') + exception_activity_type_id = False + for activity_type_id in activity_type_ids: + if activity_type_id.decoration_type == 'danger': + exception_activity_type_id = activity_type_id + break + if activity_type_id.decoration_type == 'warning': + exception_activity_type_id = activity_type_id + if exception_activity_type_id: + record.activity_exception_decoration = exception_activity_type_id.decoration_type + record.activity_exception_icon = exception_activity_type_id.icon + + def _search_activity_exception_decoration(self, operator, operand): + return [('activity_ids.activity_type_id.decoration_type', operator, operand)] @api.depends('activity_ids.state') def _compute_activity_state(self): diff --git a/addons/mail/static/src/js/activity.js b/addons/mail/static/src/js/activity.js index 79bb48819d5..5596e407c09 100644 --- a/addons/mail/static/src/js/activity.js +++ b/addons/mail/static/src/js/activity.js @@ -650,6 +650,10 @@ var KanbanActivity = BasicActivity.extend({ events:_.extend({}, BasicActivity.prototype.events, { 'show.bs.dropdown': '_onDropdownShow', }), + fieldDependencies: _.extend({}, BasicActivity.prototype.fieldDependencies, { + activity_exception_decoration: {type: 'selection'}, + activity_exception_icon: {type: 'char'} + }), /** * @override @@ -685,13 +689,18 @@ var KanbanActivity = BasicActivity.extend({ * @private */ _render: function () { - var $span = this.$('.o_activity_btn > span'); - $span.removeClass(function (index, classNames) { - return classNames.split(/\s+/).filter(function (className) { - return _.str.startsWith(className, 'o_activity_color_'); - }).join(' '); - }); - $span.addClass('o_activity_color_' + (this.activityState || 'default')); + // span classes need to be updated manually because the template cannot + // be re-rendered eaasily (because of the dropdown state) + const spanClasses = ['fa', 'fa-lg', 'fa-fw']; + spanClasses.push('o_activity_color_' + (this.activityState || 'default')); + if (this.recordData.activity_exception_decoration) { + spanClasses.push('text-' + this.recordData.activity_exception_decoration); + spanClasses.push(this.recordData.activity_exception_icon); + } else { + spanClasses.push('fa-clock-o'); + } + this.$('.o_activity_btn > span').removeClass().addClass(spanClasses.join(' ')); + if (this.$el.hasClass('show')) { // note: this part of the rendering might be asynchronous this._renderDropdown(); @@ -744,9 +753,51 @@ var KanbanActivity = BasicActivity.extend({ }, }); +// ----------------------------------------------------------------------------- +// Activity Exception Widget to display Exception icon ('activity_exception' widget) +// ----------------------------------------------------------------------------- + +var ActivityException = AbstractField.extend({ + noLabel: true, + fieldDependencies: _.extend({}, AbstractField.prototype.fieldDependencies, { + activity_exception_icon: {type: 'char'} + }), + + //------------------------------------------------------------ + // Private + //------------------------------------------------------------ + + /** + * There is no edit mode for this widget, the icon is always readonly. + * + * @override + * @private + */ + _renderEdit: function () { + return this._renderReadonly(); + }, + + /** + * Displays the exception icon if there is one. + * + * @override + * @private + */ + _renderReadonly: function () { + this.$el.empty(); + if (this.value) { + this.$el.attr({ + 'title': _t('This record has an exception activity.'), + 'class': "pull-right mt-1 text-" + this.value + " fa " + this.recordData.activity_exception_icon + }); + } + } +}); + field_registry .add('mail_activity', Activity) - .add('kanban_activity', KanbanActivity); + .add('kanban_activity', KanbanActivity) + .add('activity_exception', ActivityException); return Activity; diff --git a/addons/mail/static/src/xml/web_kanban_activity.xml b/addons/mail/static/src/xml/web_kanban_activity.xml index 4d6c290f11f..2c5574c826c 100644 --- a/addons/mail/static/src/xml/web_kanban_activity.xml +++ b/addons/mail/static/src/xml/web_kanban_activity.xml @@ -4,8 +4,9 @@