odoo.define('mail.Followers', function (require) { "use strict"; var AbstractField = require('web.AbstractField'); var concurrency = require('web.concurrency'); var core = require('web.core'); var Dialog = require('web.Dialog'); var field_registry = require('web.field_registry'); var _t = core._t; var QWeb = core.qweb; // ----------------------------------------------------------------------------- // Followers ('mail_followers') widget // ----------------------------------------------------------------------------- var Followers = AbstractField.extend({ template: 'mail.Followers', events: { // click on '(Un)Follow' button, that toggles the follow for uid 'click .o_followers_follow_button': '_onFollowButtonClicked', // click on a subtype, that (un)subscribes for this subtype 'click .o_subtypes_list .custom-checkbox': '_onSubtypeClicked', // click on 'invite' button, that opens the invite wizard 'click .o_add_follower': '_onAddFollower', 'click .o_add_follower_channel': '_onAddChannel', // click on 'edit_subtype' (pencil) button to edit subscription 'click .o_edit_subtype': '_onEditSubtype', 'click .o_remove_follower': '_onRemoveFollower', 'click .o_mail_redirect': '_onRedirect', }, // this widget only supports one2many but is not generic enough to claim // that it supports all one2many fields // supportedFieldTypes: ['one2many'], // inherited init: function (parent, name, record, options) { this._super.apply(this, arguments); this.image = this.attrs.image || 'image_small'; this.comment = this.attrs.help || false; this.followers = []; this.subtypes = []; this.data_subtype = {}; this._isFollower = undefined; var session = this.getSession(); this.partnerID = session.partner_id; this.dp = new concurrency.DropPrevious(); options = options || {}; this.isEditable = options.isEditable; }, _render: function () { // note: the rendering of this widget is asynchronous as it needs to // fetch the details of the followers, but it performs a first rendering // synchronously (_displayGeneric), and updates its rendering once it // has fetched the required data, so this function doesn't return a promise // as we don't want to wait to the data to be loaded to display the widget var self = this; var fetch_def = this.dp.add(this._readFollowers()); concurrency.rejectAfter(concurrency.delay(0), fetch_def).then(this._displayGeneric.bind(this)); return fetch_def.then(function () { self._displayButtons(); self._displayFollowers(self.followers); if (self.subtypes) { // current user is follower self._displaySubtypes(self.subtypes); } }); }, isSet: function () { return true; }, _reset: function (record) { this._super.apply(this, arguments); // the mail widgets being persistent, one need to update the res_id on reset this.res_id = record.res_id; }, // public getFollowers: function () { return this.followers; }, // private _displayButtons: function () { if (this._isFollower) { this.$('button.o_followers_follow_button').removeClass('o_followers_notfollow').addClass('o_followers_following'); this.$('.o_subtypes_list > .dropdown-toggle').attr('disabled', false); this.$('.o_followers_actions .dropdown-toggle').addClass('o_followers_following'); } else { this.$('button.o_followers_follow_button').removeClass('o_followers_following').addClass('o_followers_notfollow'); this.$('.o_subtypes_list > .dropdown-toggle').attr('disabled', true); this.$('.o_followers_actions .dropdown-toggle').removeClass('o_followers_following'); } this.$('button.o_followers_follow_button').attr("aria-pressed", this.is_follower); }, _displayGeneric: function () { // only display the number of followers (e.g. if read failed) this.$('.o_followers_actions').hide(); this.$('.o_followers_title_box > button').prop('disabled', true); this.$('.o_followers_count') .html(this.value.res_ids.length) .parent().attr('title', this._formatFollowers(this.value.res_ids.length)); }, _displayFollowers: function () { var self = this; // render the dropdown content var $followers_list = this.$('.o_followers_list').empty(); $(QWeb.render('mail.Followers.add_more', {widget: this})).appendTo($followers_list); var $follower_li; _.each(this.followers, function (record) { if(!record.active) { record.title = _.str.sprintf(_t('%s \n(inactive)'), record.name); } else { record.title = record.name; } $follower_li = $(QWeb.render('mail.Followers.partner', { 'record': _.extend(record, {'avatar_url': '/web/image/' + record.res_model + '/' + record.res_id + '/image_small'}), 'widget': self}) ); $follower_li.appendTo($followers_list); // On mouse-enter it will show the edit_subtype pencil. if (record.is_editable) { $follower_li.on('mouseenter mouseleave', function (e) { $(e.currentTarget).find('.o_edit_subtype').toggleClass('d-none', e.type === 'mouseleave'); }); } }); // clean and display title this.$('.o_followers_actions').show(); this.$('.o_followers_title_box > button').prop('disabled', !$followers_list.children().length); this.$('.o_followers_count') .html(this.value.res_ids.length) .parent().attr('title', this._formatFollowers(this.value.res_ids.length)); }, _displaySubtypes:function (data, dialog, display_warning) { var old_parent_model; var $list; if (dialog) { $list = $('
').appendTo(this.dialog.$el); } else { $list = this.$('.o_subtypes_list .dropdown-menu'); } $list.empty(); this.data_subtype = data; _.each(data, function (record) { if (old_parent_model !== record.parent_model && old_parent_model !== undefined) { $list.append($('
', {class: 'dropdown-divider'})); } old_parent_model = record.parent_model; record.followed = record.followed || undefined; $list.append(QWeb.render('mail.Followers.subtype', { 'record': record, 'dialog': dialog, 'display_warning': display_warning && record.internal, })); }); if (display_warning) { $(QWeb.render('mail.Followers.subtypes.warning')).appendTo(this.dialog.$el); } }, _inviteFollower: function (channel_only) { var action = { type: 'ir.actions.act_window', res_model: 'mail.wizard.invite', view_mode: 'form', view_type: 'form', views: [[false, 'form']], name: _t("Invite Follower"), target: 'new', context: { 'default_res_model': this.model, 'default_res_id': this.res_id, 'mail_invite_follower_channel_only': channel_only, }, }; this.do_action(action, { on_close: this._reload.bind(this), }); }, _formatFollowers: function (count){ var str = ''; if (count <= 0) { str = _t("No follower"); } else if (count === 1){ str = _t("One follower"); } else { str = ''+count+' '+_t("followers"); } return str; }, _readFollowers: function () { var self = this; var missing_ids = _.difference(this.value.res_ids, _.pluck(this.followers, 'id')); var def; if (missing_ids.length) { def = this._rpc({ route: '/mail/read_followers', params: { follower_ids: missing_ids, res_model: this.model } }); } return Promise.resolve(def).then(function (results) { if (results) { self.followers = _.uniq(results.followers.concat(self.followers), 'id'); if (results.subtypes) { //read_followers will return False if current user is not in the list self.subtypes = results.subtypes; } } // filter out previously fetched followers that are no longer following self.followers = _.filter(self.followers, function (follower) { return _.contains(self.value.res_ids, follower.id); }); var user_follower = _.filter(self.followers, function (rec) { return rec.is_uid; }); self._isFollower = user_follower.length >= 1; }); }, _reload: function () { this.trigger_up('reload', {fieldNames: [this.name]}); }, _follow: function () { var kwargs = { partner_ids: [this.partnerID], context: {}, // FIXME }; this._rpc({ model: this.model, method: 'message_subscribe', args: [[this.res_id]], kwargs: kwargs, }) .then(this._reload.bind(this)); }, /** * Remove partners or channels from the followers * @param {Array} [ids.partner_ids] the partner ids * @param {Array} [ids.channel_ids] the channel ids */ _unfollow: function (ids) { var self = this; return new Promise(function (resolve, reject) { var follower = _.find(self.followers, { res_id: ids.partner_ids ? ids.partner_ids[0] : ids.channel_ids[0] }); var text = _.str.sprintf(_t("If you remove a follower, he won't be notified of any email or discussion on this document. Do you really want to remove %s?"), follower.name); Dialog.confirm(this, text, { title: _t("Warning"), confirm_callback: function () { var args = [ [self.res_id], ids.partner_ids, ids.channel_ids, {}, // FIXME ]; self._rpc({ model: self.model, method: 'message_unsubscribe', args: args }) .then(self._reload.bind(self)); resolve(); }, cancel_callback: reject, }); }); }, _updateSubscription: function (event, followerID, isChannel) { var ids = {}; var subtypes; if (followerID !== undefined) { // Subtypes edited from the modal subtypes = this.dialog.$('input[type="checkbox"]'); if (isChannel) { ids.channel_ids = [followerID]; } else { ids.partner_ids = [followerID]; } } else { subtypes = this.$('.o_followers_actions input[type="checkbox"]'); ids.partner_ids = [this.partnerID]; } // Get the subtype ids var checklist = []; _.each(subtypes, function (record) { if ($(record).is(':checked')) { checklist.push(parseInt($(record).data('id'))); } }); // If no more subtype followed, unsubscribe the follower if (!checklist.length) { this._unfollow(ids).guardedCatch(function () { $(event.currentTarget).find('input').addBack('input').prop('checked', true); }); } else { var kwargs = _.extend({}, ids); if (followerID === undefined || followerID === this.partnerID) { //this.subtypes will only be updated if the current user //just added himself to the followers. We need to update //the subtypes manually when editing subtypes //for current user _.each(this.subtypes, function (subtype) { subtype.followed = checklist.indexOf(subtype.id) > -1; }); } kwargs.subtype_ids = checklist; kwargs.context = {}; // FIXME this._rpc({ model: this.model, method: 'message_subscribe', args: [[this.res_id]], kwargs: kwargs, }) .then(this._reload.bind(this)); } }, //-------------------------------------------------------------------------- // Handlers //-------------------------------------------------------------------------- /** * @private * @param {MouseEvent} ev */ _onAddFollower: function (ev) { ev.preventDefault(); this._inviteFollower(false); }, /** * @private * @param {MouseEvent} ev */ _onAddChannel: function (ev) { ev.preventDefault(); this._inviteFollower(true); }, /** * @private * @param {MouseEvent} ev */ _onEditSubtype: function (ev) { var self = this; var $currentTarget = $(ev.currentTarget); var follower_id = $currentTarget.data('follower-id'); // id of model mail_follower this._rpc({ route: '/mail/read_subscription_data', params: {res_model: this.model, follower_id: follower_id}, }) .then(function (data) { var res_id = $currentTarget.data('oe-id'); // id of model res_partner or mail_channel var is_channel = $currentTarget.data('oe-model') === 'mail.channel'; self.dialog = new Dialog(this, { size: 'medium', title: _t("Edit Subscription of ") + $currentTarget.siblings('a').text(), buttons: [ { text: _t("Apply"), classes: 'btn-primary', click: function () { self._updateSubscription(ev, res_id, is_channel); }, close: true }, { text: _t("Cancel"), close: true, }, ], }); self.dialog.opened().then(function () { self._displaySubtypes(data, true, is_channel); }); self.dialog.open(); }); }, /** * @private */ _onFollowButtonClicked: function () { if (!this._isFollower) { this._follow(); } else { this._unfollow({partner_ids: [this.partnerID]}); } }, /** * @private * @param {MouseEvent} ev */ _onRedirect: function (ev) { ev.preventDefault(); var $target = $(ev.target); this.do_action({ type: 'ir.actions.act_window', view_type: 'form', view_mode: 'form', res_model: $target.data('oe-model'), views: [[false, 'form']], res_id: $target.data('oe-id'), }); }, /** * @private * @param {MouseEvent} ev */ _onRemoveFollower: function (ev) { var resModel = $(ev.target).parent().find('a').data('oe-model'); var resID = $(ev.target).parent().find('a').data('oe-id'); if (resModel === 'res.partner') { return this._unfollow({partner_ids: [resID]}); } else { return this._unfollow({channel_ids: [resID]}); } }, /** * @private * @param {MouseEvent} ev */ _onSubtypeClicked: function (ev) { ev.stopPropagation(); this._updateSubscription(ev); var $list = this.$('.o_subtypes_list'); if (!$list.hasClass('show')) { $list.addClass('show'); } if (this.$('.o_subtypes_list .dropdown-menu')[0].children.length < 1) { $list.removeClass('show'); } }, }); field_registry.add('mail_followers', Followers); return Followers; });