diff --git a/addons/mail/static/src/js/announcement.js b/addons/mail/static/src/js/announcement.js index d85e8c9b5af..100bd1ea0b1 100644 --- a/addons/mail/static/src/js/announcement.js +++ b/addons/mail/static/src/js/announcement.js @@ -1,58 +1,68 @@ -openerp_announcement = function(instance) { - instance.web.WebClient.include({ - show_application: function() { - return $.when(this._super.apply(this, arguments)).then(this.proxy('show_annoucement_bar')); - }, - _ab_location: function(dbuuid) { - return _.str.sprintf('https://services.openerp.com/openerp-enterprise/ab/css/%s.css', dbuuid); - }, - show_annoucement_bar: function() { - if (this.session.get_cookie('ab') === 'c') { +odoo.define(['web.core', 'web.Model', 'web.session', 'web.WebClient'], function (require) { +"use strict"; + +var core = require('web.core'); +var Model = require('web.Model'); +var session = require('web.session'); +var WebClient = require('web.WebClient'); + +var QWeb = core.qweb; + +WebClient.include({ + show_application: function() { + return $.when(this._super.apply(this, arguments)).then(this.proxy('show_announcement_bar')); + }, + _ab_location: function(dbuuid) { + return _.str.sprintf('https://services.openerp.com/openerp-enterprise/ab/css/%s.css', dbuuid); + }, + show_announcement_bar: function() { + if (session.get_cookie('ab') === 'c') { + return; + } + var self = this; + var config_parameter = new Model('ir.config_parameter'); + $(QWeb.render('WebClient.announcement_bar')).prependTo($('body')); + var $bar = $('#announcement_bar_table'); + + return config_parameter.call('get_param', ['database.uuid', false]).then(function(dbuuid) { + if (!dbuuid) { return; } - var self = this; - var config_parameter = new instance.web.Model('ir.config_parameter'); - $(openerp.qweb.render('WebClient.announcement_bar')).prependTo($('body')); - var $bar = $('#announcement_bar_table'); - - return config_parameter.call('get_param', ['database.uuid', false]).then(function(dbuuid) { - if (!dbuuid) { - return; - } - var $link = $bar.find('.url a'); - $link.attr('href', _.str.sprintf('%s/%s', $link.attr('href'), dbuuid)); - var $css = $('').attr({ - rel : 'stylesheet', - type: 'text/css', - media: 'screen', - href: self._ab_location(dbuuid) - }); - $css.on('load', function() { - var close = function() { - var ttl = 7*24*60*60; - self.session.set_cookie('ab', 'c', ttl); - $bar.slideUp(); - setTimeout(function () { - $('.openerp_webclient_container').css('height', 'calc(100% - 34px)'); - }, 400); - }; - /* - We need a timeout of at least 350ms because the announcement bar appears - with an animation of 350ms and the computed height might be wrong if we don't wait - */ - setTimeout(function(){ - var height = $('#announcement_bar_table').outerHeight() - + $('#oe_main_menu_navbar').outerHeight(); - $('.openerp_webclient_container').css('height', 'calc(100% - ' + height + 'px)'); - $bar.find('.close').on('click', close); - self.trigger('ab_loaded', $bar); - }, 400) - }); - - $('head').append($css); - }).fail(function(result, ev){ - ev.preventDefault(); + var $link = $bar.find('.url a'); + $link.attr('href', _.str.sprintf('%s/%s', $link.attr('href'), dbuuid)); + var $css = $('').attr({ + rel : 'stylesheet', + type: 'text/css', + media: 'screen', + href: self._ab_location(dbuuid) }); - } - }); -}; + $css.on('load', function() { + var close = function() { + var ttl = 7*24*60*60; + session.set_cookie('ab', 'c', ttl); + $bar.slideUp(); + setTimeout(function () { + $('.openerp_webclient_container').css('height', 'calc(100% - 34px)'); + }, 400); + }; + /* + We need a timeout of at least 350ms because the announcement bar appears + with an animation of 350ms and the computed height might be wrong if we don't wait + */ + setTimeout(function(){ + var height = $('#announcement_bar_table').outerHeight() + + $('#oe_main_menu_navbar').outerHeight(); + $('.openerp_webclient_container').css('height', 'calc(100% - ' + height + 'px)'); + $bar.find('.close').on('click', close); + self.trigger('ab_loaded', $bar); + }, 400); + }); + + $('head').append($css); + }).fail(function(result, ev){ + ev.preventDefault(); + }); + } +}); + +}); diff --git a/addons/mail/static/src/js/mail.js b/addons/mail/static/src/js/mail.js index 86fd68b7d49..0abc9f189bf 100644 --- a/addons/mail/static/src/js/mail.js +++ b/addons/mail/static/src/js/mail.js @@ -1,133 +1,32 @@ -openerp.mail = function (session) { - var _t = session.web._t, - _lt = session.web._lt; +odoo.define('mail.mail', ['mail.utils', 'web.core', 'web.data', 'web.form_common', 'web.pyeval', 'web.SearchView', 'web.session', 'web.SystrayMenu', 'web.time', 'web.utils', 'web.web_client', 'web.Widget'], function (require) { +"use strict"; - var mail = session.mail; +var mail_utils = require('mail.utils'); +var core = require('web.core'); +var data = require('web.data'); +var form_common = require('web.form_common'); +var pyeval = require('web.pyeval'); +var SearchView = require('web.SearchView'); +var real_session = require('web.session'); +var SystrayMenu = require('web.SystrayMenu'); +var time = require('web.time'); +var utils = require('web.utils'); +var web_client = require('web.web_client'); +var Widget = require('web.Widget'); - openerp_mail_followers(session, mail); // import mail_followers.js - openerp_FieldMany2ManyTagsEmail(session); // import manyy2many_tags_email.js - openerp_announcement(session); +var _t = core._t; +var qweb = core.qweb; - /** - * ------------------------------------------------------------ - * ChatterUtils - * ------------------------------------------------------------ - * - * This class holds a few tools method for Chatter. - * Some regular expressions not used anymore, kept because I want to - * - (^|\s)@((\w|@|\.)*): @login@log.log - * - (^|\s)\[(\w+).(\w+),(\d)\|*((\w|[@ .,])*)\]: [ir.attachment,3|My Label], - * for internal links - */ - - mail.ChatterUtils = { - - /** parse text to find email: Tagada
-> [Tagada, address@mail.fr] or False */ - parse_email: function (text) { - var result = text.match(/(.*)<(.*@.*)>/); - if (result) { - return [_.str.trim(result[1]), _.str.trim(result[2])]; - } - result = text.match(/(.*@.*)/); - if (result) { - return [_.str.trim(result[1]), _.str.trim(result[1])]; - } - return [text, false]; - }, - - /* Get an image in /web/binary/image?... */ - get_image: function (session, model, field, id, resize) { - var r = resize ? encodeURIComponent(resize) : ''; - id = id || ''; - return session.url('/web/binary/image', {model: model, field: field, id: id, resize: r}); - }, - - /* Get the url of an attachment {'id': id} */ - get_attachment_url: function (session, message_id, attachment_id) { - return session.url('/mail/download_attachment', { - 'model': 'mail.message', - 'id': message_id, - 'method': 'download_attachment', - 'attachment_id': attachment_id - }); - }, - - /** - * Replaces some expressions - * - :name - shortcut to an image - */ - do_replace_expressions: function (string) { - var icon_list = ['al', 'pinky'] - /* special shortcut: :name, try to find an icon if in list */ - var regex_login = new RegExp(/(^|\s):((\w)*)/g); - var regex_res = regex_login.exec(string); - while (regex_res != null) { - var icon_name = regex_res[2]; - if (_.include(icon_list, icon_name)) - string = string.replace(regex_res[0], regex_res[1] + ', )
- * TDE note : should be done server-side, in Python -> use mail.compose.message ?
- */
- get_text2html: function (text) {
- return text
- .replace(/((?:https?|ftp):\/\/[\S]+)/g,'$1 ')
- .replace(/[\n\r]/g,' , )
+ * TDE note : should be done server-side, in Python -> use mail.compose.message ?
+ */
+function get_text2html(text) {
+ return text
+ .replace(/((?:https?|ftp):\/\/[\S]+)/g,'$1 ')
+ .replace(/[\n\r]/g,'
')
- },
-
- /* Returns the complete domain with "&"
- * TDE note: please add some comments to explain how/why
- */
- expand_domain: function (domain) {
- var new_domain = [];
- var nb_and = -1;
- // TDE note: smarted code maybe ?
- for ( var k = domain.length-1; k >= 0 ; k-- ) {
- if ( typeof domain[k] != 'array' && typeof domain[k] != 'object' ) {
- nb_and -= 2;
- continue;
- }
- nb_and += 1;
- }
-
- for (var k = 0; k < nb_and ; k++) {
- domain.unshift('&');
- }
-
- return domain;
- },
-
- // inserts zero width space between each letter of a string so that
- // the word will correctly wrap in html boxes smaller than the text
- breakword: function(str){
- var out = '';
- if (!str) {
- return str;
- }
- for(var i = 0, len = str.length; i < len; i++){
- out += _.str.escapeHTML(str[i]) + '';
- }
- return out;
- },
- };
-
-
- /**
- * ------------------------------------------------------------
- * MessageCommon
- * ------------------------------------------------------------
- *
- * Common base for expandables, chatter messages and composer. It manages
- * the various variables common to those models.
- */
-
- mail.MessageCommon = session.web.Widget.extend({
+/**
+ * ------------------------------------------------------------
+ * MessageCommon
+ * ------------------------------------------------------------
+ *
+ * Common base for expandables, chatter messages and composer. It manages
+ * the various variables common to those models.
+ */
+var MessageCommon = Widget.extend({
/**
* ------------------------------------------------------------
* FIXME: this comment was moved as is from the ThreadMessage Init as
@@ -146,1935 +45,390 @@ openerp.mail = function (session) {
* - - - - sub sub message (parent id = sub thread)
* - - sub message (parent_id = root message)
* - - - sub thread
- */
-
- init: function (parent, datasets, options) {
- this._super(parent, options);
-
- // record options
- this.options = datasets.options || options || {};
- // record domain and context
- this.domain = datasets.domain || options.domain || [];
- this.context = _.extend({
- default_model: false,
- default_res_id: 0,
- default_parent_id: false }, options.context || {});
-
- // data of this message
- this.id = datasets.id || false,
- this.last_id = this.id,
- this.model = datasets.model || this.context.default_model || false,
- this.res_id = datasets.res_id || this.context.default_res_id || false,
- this.parent_id = datasets.parent_id || false,
- this.type = datasets.type || false,
- this.subtype = datasets.subtype || false,
- this.is_author = datasets.is_author || false,
- this.author_avatar = datasets.author_avatar || false,
- this.is_private = datasets.is_private || false,
- this.subject = datasets.subject || false,
- this.name = datasets.name || false,
- this.record_name = datasets.record_name || false,
- this.body = datasets.body || '',
- this.body_short = datasets.body_short || '',
- this.vote_nb = datasets.vote_nb || 0,
- this.has_voted = datasets.has_voted || false,
- this.is_favorite = datasets.is_favorite || false,
- this.thread_level = datasets.thread_level || 0,
- this.to_read = datasets.to_read || false,
- this.author_id = datasets.author_id || false,
- this.attachment_ids = datasets.attachment_ids || [],
- this.partner_ids = datasets.partner_ids || [];
- this.date = datasets.date;
- this.user_pid = datasets.user_pid || false;
- this.format_data();
-
- // update record_name: Partner profile
- if (this.model == 'res.partner') {
- this.record_name = 'Partner Profile of ' + this.record_name;
- }
- else if (this.model == 'hr.employee') {
- this.record_name = 'News from ' + this.record_name;
- }
- // record options and data
- this.show_record_name = this.options.show_record_name && this.record_name && !this.thread_level;
- this.options.show_read = false;
- this.options.show_unread = false;
- if (this.options.show_read_unread_button) {
- if (this.options.read_action == 'read') this.options.show_read = true;
- else if (this.options.read_action == 'unread') this.options.show_unread = true;
- else {
- this.options.show_read = this.to_read;
- this.options.show_unread = !this.to_read;
- }
- this.options.rerender = true;
- this.options.toggle_read = true;
- }
- this.parent_thread = typeof parent.on_message_detroy == 'function' ? parent : this.options.root_thread;
- this.thread = false;
- },
-
- /* Convert date, timerelative and avatar in displayable data. */
- format_data: function () {
-
- //formating and add some fields for render
- this.date = this.date ? session.web.str_to_datetime(this.date) : false;
- this.display_date = moment(new Date(this.date)).format('ddd MMM DD YYYY LT');
- if (this.date && new Date().getTime()-this.date.getTime() < 7*24*60*60*1000) {
- this.timerelative = $.timeago(this.date);
- }
- if (this.author_avatar) {
- this.avatar = "data:image/png;base64," + this.author_avatar;
- } else if (this.type == 'email' && (!this.author_id || !this.author_id[0])) {
- this.avatar = ('/mail/static/src/img/email_icon.png');
- } else if (this.author_id && this.template != 'mail.compose_message') {
- this.avatar = mail.ChatterUtils.get_image(this.session, 'res.partner', 'image_small', this.author_id[0]);
- } else {
- this.avatar = mail.ChatterUtils.get_image(this.session, 'res.users', 'image_small', this.session.uid);
- }
- if (this.author_id && this.author_id[1]) {
- var parsed_email = mail.ChatterUtils.parse_email(this.author_id[1]);
- this.author_id.push(parsed_email[0], parsed_email[1]);
- }
- if (this.partner_ids && this.partner_ids.length > 3) {
- this.extra_partners_nbr = this.partner_ids.length - 3;
- this.extra_partners_str = ''
- var extra_partners = this.partner_ids.slice(3);
- for (var key in extra_partners) {
- this.extra_partners_str += extra_partners[key][1];
- }
- }
- },
-
-
- /* upload the file on the server, add in the attachments list and reload display
- */
- display_attachments: function () {
- for (var l in this.attachment_ids) {
- var attach = this.attachment_ids[l];
- if (!attach.formating) {
- attach.url = mail.ChatterUtils.get_attachment_url(this.session, this.id, attach.id);
- attach.name = mail.ChatterUtils.breakword(attach.name || attach.filename);
- attach.formating = true;
- }
- }
- this.$(".oe_msg_attachment_list").html( session.web.qweb.render('mail.thread.message.attachments', {'widget': this}) );
- },
-
- /* return the link to resized image
- */
- attachments_resize_image: function (id, resize) {
- return mail.ChatterUtils.get_image(this.session, 'ir.attachment', 'datas', id, resize);
- },
-
- /* get all child message id linked.
- * @return array of id
- */
- get_child_ids: function () {
- return _.map(this.get_childs(), function (val) { return val.id; });
- },
-
- /* get all child message linked.
- * @return array of message object
- */
- get_childs: function (nb_thread_level) {
- var res=[];
- if (arguments[1] && this.id) res.push(this);
- if ((isNaN(nb_thread_level) || nb_thread_level>0) && this.thread) {
- _(this.thread.messages).each(function (val, key) {
- res = res.concat( val.get_childs((isNaN(nb_thread_level) ? undefined : nb_thread_level-1), true) );
- });
- }
- return res;
- },
-
- /**
- * search a message in all thread and child thread.
- * This method return an object message.
- * @param {object}{int} option.id
- * @param {object}{string} option.model
- * @param {object}{boolean} option._go_thread_wall
- * private for check the top thread
- * @return thread object
- */
- browse_message: function (options) {
- // goto the wall thread for launch browse
- if (!options._go_thread_wall) {
- options._go_thread_wall = true;
- for (var i in this.options.root_thread.messages) {
- var res=this.options.root_thread.messages[i].browse_message(options);
- if (res) return res;
- }
- }
-
- if (this.id==options.id)
- return this;
-
- for (var i in this.thread.messages) {
- if (this.thread.messages[i].thread) {
- var res=this.thread.messages[i].browse_message(options);
- if (res) return res;
- }
- }
-
- return false;
- },
-
- /**
- * call on_message_delete on his parent thread
- */
- destroy: function () {
-
- this._super();
- this.parent_thread.on_message_detroy(this);
-
- }
-
- });
-
- /**
- * ------------------------------------------------------------
- * ComposeMessage widget
- * ------------------------------------------------------------
- *
- * This widget handles the display of a form to compose a new message.
- * This form is a mail.compose.message form_view.
- * On first time : display a compact textarea that is not the compose form.
- * When the user focuses the textarea, the compose message is instantiated.
*/
- mail.ThreadComposeMessage = mail.MessageCommon.extend({
- template: 'mail.compose_message',
+ init: function (parent, datasets, options) {
+ this._super(parent, options);
- /**
- * @param {Object} parent parent
- * @param {Object} [options]
- * @param {Object} [context] context passed to the
- * mail.compose.message DataSetSearch. Please refer to this model
- * for more details about fields and default values.
- * @param {Object} recipients = [
- {
- 'email_address': [str],
- 'partner_id': False/[int],
- 'name': [str],
- 'full_name': name
";
- }
- });
- $target.data('liker-list', voter);
- self.bindTooltipTo($target, voter);
- $target.tooltip('hide').tooltip('show');
- $(".tooltip").on("mouseleave", function () {
- $(this).remove();
- });
- });
- }
- return true;
- },
- bindTooltipTo: function($el, value) {
- $el.tooltip({
- 'title': value,
- 'placement': 'top',
- 'container': this.el,
- 'html': true,
- 'trigger': 'manual',
- 'animation': false
- }).on("mouseleave", function () {
- setTimeout(function () {
- if (!$(".tooltip:hover").length) {
- $el.tooltip("hide");
- }
- },100);
- });
- },
- on_record_clicked: function (event) {
- event.preventDefault();
- var self = this;
- var state = {
- 'model': this.model,
- 'id': this.res_id,
- 'title': this.record_name
- };
- session.webclient.action_manager.do_push_state(state);
- this.context.params = {
- model: this.model,
- res_id: this.res_id,
- };
- this.thread.ds_thread.call("message_redirect_action", {context: this.context}).then(function(action){
- self.do_action(action);
- });
- },
-
- on_record_author_clicked: function (event) {
- event.preventDefault();
- var partner_id = $(event.target).data('partner');
- var state = {
- 'model': 'res.partner',
- 'id': partner_id,
- 'title': this.record_name
- };
- session.webclient.action_manager.do_push_state(state);
- var action = {
- type:'ir.actions.act_window',
- view_type: 'form',
- view_mode: 'form',
- res_model: 'res.partner',
- views: [[false, 'form']],
- res_id: partner_id,
- }
- this.do_action(action);
- },
-
- /* Call the on_compose_message on the thread of this message. */
- on_message_reply:function (event) {
- event.stopPropagation();
- this.create_thread();
- this.thread.on_compose_message(event);
- return false;
- },
-
- on_expand: function (event) {
- event.stopPropagation();
- this.$('.oe_msg_body:first > .oe_msg_body_short:first').toggle();
- this.$('.oe_msg_body:first > .oe_msg_body_long:first').toggle();
- return false;
- },
-
- /**
- * Instantiate the thread object of this message.
- * Each message have only one thread.
- */
- create_thread: function () {
- if (this.thread) {
- return false;
- }
- /*create thread*/
- this.thread = new mail.Thread(this, this, {
- 'domain': this.domain,
- 'context':{
- 'default_model': this.model,
- 'default_res_id': this.res_id,
- 'default_parent_id': this.id
- },
- 'options': this.options
- }
- );
- /*insert thread in parent message*/
- this.thread.insertAfter(this.$el);
- },
-
- /**
- * Fade out the message and his child thread.
- * Then this object is destroyed.
- */
- animated_destroy: function (fadeTime) {
- var self=this;
- this.$el.fadeOut(fadeTime, function () {
- self.parent_thread.message_to_expandable(self);
- });
- if (this.thread) {
- this.thread.$el.fadeOut(fadeTime);
- }
- },
-
- /**
- * Wait a confirmation for delete the message on the DB.
- * Make an animate destroy
- */
- on_message_delete: function (event) {
- event.stopPropagation();
- if (! confirm(_t("Do you really want to delete this message?"))) { return false; }
-
- this.animated_destroy(150);
- // delete this message and his childs
- var ids = [this.id].concat( this.get_child_ids() );
- this.ds_message.unlink(ids);
- return false;
- },
-
- /* Check if the message must be destroy and detroy it or check for re render widget
- * @param {callback} apply function
- */
- check_for_rerender: function () {
- var self = this;
-
- var messages = [this].concat(this.get_childs());
- var message_ids = _.map(messages, function (msg) { return msg.id;});
- var domain = mail.ChatterUtils.expand_domain( this.options.root_thread.domain )
- .concat([["id", "in", message_ids ]]);
-
- return this.parent_thread.ds_message.call('message_read', [undefined, domain, [], !!this.parent_thread.options.display_indented_thread, this.context, this.parent_thread.id])
- .then( function (records) {
- // remove message not loaded
- _.map(messages, function (msg) {
- if(!_.find(records, function (record) { return record.id == msg.id; })) {
- msg.animated_destroy(150);
- } else {
- msg.renderElement();
- msg.start();
- }
- self.options.root_thread.MailWidget.do_reload_menu_emails();
- });
-
- });
- },
-
- on_message_read: function (event) {
- event.stopPropagation();
- this.on_message_read_unread(true);
- return false;
- },
-
- on_message_unread: function (event) {
- event.stopPropagation();
- this.on_message_read_unread(false);
- return false;
- },
-
- /* Set the selected thread and all childs as read or unread, based on
- * read parameter.
- * @param {boolean} read_value
- */
- on_message_read_unread: function (read_value) {
- var self = this;
- var messages = [this].concat(this.get_childs());
-
- // inside the inbox, when the user mark a message as read/done, don't apply this value
- // for the stared/favorite message
- if (this.options.view_inbox && read_value) {
- var messages = _.filter(messages, function (val) { return !val.is_favorite && val.id; });
- if (!messages.length) {
- this.check_for_rerender();
- return false;
- }
- }
- var message_ids = _.map(messages, function (val) { return val.id; });
-
- this.ds_message.call('set_message_read', [message_ids, read_value, true, this.context])
- .then(function () {
- // apply modification
- _.each(messages, function (msg) {
- msg.to_read = !read_value;
- if (msg.options.toggle_read) {
- msg.options.show_read = msg.to_read;
- msg.options.show_unread = !msg.to_read;
- }
- });
- // check if the message must be display, destroy or rerender
- self.check_for_rerender();
- });
- return false;
- },
-
- /**
- * add or remove a vote for a message and display the result
- */
- on_vote: function (event) {
- event.stopPropagation();
- this.ds_message.call('vote_toggle', [[this.id]])
- .then(
- _.bind(function (vote) {
- this.has_voted = vote;
- this.vote_nb += this.has_voted ? 1 : -1;
- this.display_vote();
- }, this));
- return false;
- },
-
- /**
- * Display the render of this message's vote
- */
- display_vote: function () {
- var vote_element = session.web.qweb.render('mail.thread.message.vote', {'widget': this});
- this.$(".oe_msg_footer:first .oe_mail_vote_count").remove();
- this.$(".oe_msg_footer:first .oe_msg_vote").replaceWith(vote_element);
- this.$('.oe_msg_vote').on('click', this.on_vote);
- this.$('.oe_mail_vote_count').on('mouseenter', this.on_hover);
- },
-
- /**
- * add or remove a favorite (or starred) for a message and change class on the DOM
- */
- on_star: function (event) {
- event.stopPropagation();
- var self=this;
- var button = self.$('.oe_star:first');
-
- this.ds_message.call('set_message_starred', [[self.id], !self.is_favorite, true])
- .then(function (star) {
- self.is_favorite=star;
- if (self.is_favorite) {
- button.addClass('oe_starred');
- } else {
- button.removeClass('oe_starred');
- }
-
- if (self.options.view_inbox && self.is_favorite) {
- self.on_message_read_unread(true);
- } else {
- self.check_for_rerender();
- }
- });
- return false;
- },
-
- });
-
- /**
- * ------------------------------------------------------------
- * Thread Widget
- * ------------------------------------------------------------
- *
- * This widget handles the display of a thread of messages. The
- * thread view:
- * - root thread
- * - - sub message (parent_id = root message)
- * - - - sub thread
- * - - - - sub sub message (parent id = sub thread)
- * - - sub message (parent_id = root message)
- * - - - sub thread
- */
- mail.Thread = session.web.Widget.extend({
- template: 'mail.thread',
-
- /**
- * @param {Object} parent parent
- * @param {Array} [domain]
- * @param {Object} [context] context of the thread. It should
- contain at least default_model, default_res_id. Please refer to
- the ComposeMessage widget for more information about it.
- * @param {Object} [options]
- * @param {Object} [message] read about mail.ThreadMessage object
- * @param {Object} [thread]
- * @param {int} [display_indented_thread] number thread level to indented threads.
- * other are on flat mode
- * @param {Array} [parents] liked with the parents thread
- * use with browse, fetch... [O]= top parent
- */
- init: function (parent, datasets, options) {
- var self = this;
- this._super(parent, options);
- this.MailWidget = parent instanceof mail.Widget ? parent : false;
- this.domain = options.domain || [];
- this.context = _.extend(options.context || {});
-
- this.options = options.options;
- this.options.root_thread = (options.options.root_thread != undefined ? options.options.root_thread : this);
- this.options.show_compose_message = this.options.show_compose_message && !this.thread_level;
-
- // record options and data
- this.parent_message= parent.thread!= undefined ? parent : false ;
-
- // data of this thread
- this.id = datasets.id || false;
- this.last_id = datasets.last_id || false;
- this.parent_id = datasets.parent_id || false;
- this.is_private = datasets.is_private || false;
- this.author_id = datasets.author_id || false;
- this.thread_level = (datasets.thread_level+1) || 0;
- datasets.partner_ids = datasets.partner_ids || [];
- if (datasets.author_id && !_.contains(_.flatten(datasets.partner_ids),datasets.author_id[0]) && datasets.author_id[0]) {
- datasets.partner_ids.push(datasets.author_id);
- }
- this.user_pid = datasets.user_pid || false;
- this.partner_ids = datasets.partner_ids;
- this.messages = [];
- this.options.flat_mode = (this.options.display_indented_thread - this.thread_level > 0);
-
- // object compose message
- this.compose_message = false;
-
- this.ds_thread = new session.web.DataSetSearch(this, this.context.default_model || 'mail.thread');
- this.ds_message = new session.web.DataSetSearch(this, 'mail.message');
- this.render_mutex = new $.Mutex();
- },
-
- start: function () {
- this._super.apply(this, arguments);
- this.bind_events();
- return $.when();
- },
-
- /* instantiate the compose message object and insert this on the DOM.
- * The compose message is display in compact form.
- */
- instantiate_compose_message: function () {
- // add message composition form view
- if (!this.compose_message) {
- this.compose_message = new mail.ThreadComposeMessage(this, this, {
- 'context': this.options.compose_as_todo && !this.thread_level ? _.extend(this.context, { 'default_starred': true }) : this.context,
- 'options': this.options,
- });
- if (!this.thread_level || this.thread_level > this.options.display_indented_thread) {
- this.compose_message.insertBefore(this.$el);
- } else {
- this.compose_message.prependTo(this.$el);
- }
- }
- },
-
- /* When the expandable object is visible on screen (with scrolling)
- * then the on_expandable function is launch
- */
- on_scroll: function () {
- var expandables =
- _.each( _.filter(this.messages, function (val) {return val.max_limit && !val.parent_id;}), function (val) {
- var pos = val.$el.position();
- if (pos.top) {
- /* bottom of the screen */
- var bottom = $(window).scrollTop()+$(window).height()+200;
- if (bottom > pos.top) {
- val.on_expandable();
- }
- }
- });
- },
-
- /**
- * Bind events in the widget. Each event is slightly described
- * in the function. */
- bind_events: function () {
- var self = this;
- self.$('.oe_mail_list_recipients .oe_more').on('click', self.on_show_recipients);
- self.$('.oe_mail_compose_textarea .oe_more_hidden').on('click', self.on_hide_recipients);
- },
-
- /**
- *show all the partner list of this parent message
- */
- on_show_recipients: function () {
- var p=$(this).parent();
- p.find('.oe_more_hidden, .oe_hidden').show();
- p.find('.oe_more').hide();
- return false;
- },
-
- /**
- *hide a part of the partner list of this parent message
- */
- on_hide_recipients: function () {
- var p=$(this).parent();
- p.find('.oe_more_hidden, .oe_hidden').hide();
- p.find('.oe_more').show();
- return false;
- },
-
- /* get all child message/thread id linked.
- * @return array of id
- */
- get_child_ids: function () {
- return _.map(this.get_childs(), function (val) { return val.id; });
- },
-
- /* get all child message/thread linked.
- * @param {int} nb_thread_level, number of traversed thread level for this search
- * @return array of thread object
- */
- get_childs: function (nb_thread_level) {
- var res=[];
- if (arguments[1]) res.push(this);
- if (isNaN(nb_thread_level) || nb_thread_level>0) {
- _(this.messages).each(function (val, key) {
- if (val.thread) {
- res = res.concat( val.thread.get_childs((isNaN(nb_thread_level) ? undefined : nb_thread_level-1), true) );
- }
- });
- }
- return res;
- },
-
- /**
- *search a thread in all thread and child thread.
- * This method return an object thread.
- * @param {object}{int} option.id
- * @param {object}{string} option.model
- * @param {object}{boolean} option._go_thread_wall
- * private for check the top thread
- * @param {object}{boolean} option.default_return_top_thread
- * return the top thread (wall) if no thread found
- * @return thread object
- */
- browse_thread: function (options) {
- // goto the wall thread for launch browse
- if (!options._go_thread_wall) {
- options._go_thread_wall = true;
- return this.options.root_thread.browse_thread(options);
- }
-
- if (this.id == options.id) {
- return this;
- }
-
- if (options.id) {
- for (var i in this.messages) {
- if (this.messages[i].thread) {
- var res = this.messages[i].thread.browse_thread({'id':options.id, '_go_thread_wall':true});
- if (res) return res;
- }
- }
- }
-
- //if option default_return_top_thread, return the top if no found thread
- if (options.default_return_top_thread) {
- return this;
- }
-
- return false;
- },
-
- /**
- *search a message in all thread and child thread.
- * This method return an object message.
- * @param {object}{int} option.id
- * @param {object}{string} option.model
- * @param {object}{boolean} option._go_thread_wall
- * private for check the top thread
- * @return message object
- */
- browse_message: function (options) {
- if (this.options.root_thread.messages[0])
- return this.options.root_thread.messages[0].browse_message(options);
- },
-
- /**
- *If compose_message doesn't exist, instantiate the compose message.
- * Call the on_toggle_quick_composer method to allow the user to write his message.
- * (Is call when a user click on "Reply" button)
- */
- on_compose_message: function (event) {
- this.instantiate_compose_message();
- this.compose_message.on_toggle_quick_composer(event);
- return false;
- },
-
- /**
- *display the message "there are no message" on the thread
- */
- no_message: function () {
- var no_message = $(session.web.qweb.render('mail.wall_no_message', {}));
- if (this.options.help) {
- no_message.html(this.options.help);
- }
- if (!this.$el.find(".oe_view_nocontent").length)
- {
- no_message.appendTo(this.$el);
- }
- },
-
- /**
- *make a request to read the message (calling RPC to "message_read").
- * The result of this method is send to the switch message for sending ach message to
- * his parented object thread.
- * @param {Array} replace_domain: added to this.domain
- * @param {Object} replace_context: added to this.context
- * @param {Array} ids read (if the are some ids, the method don't use the domain)
- */
- message_fetch: function (replace_domain, replace_context, ids, callback) {
- return this.ds_message.call('message_read', [
- // ids force to read
- ids === false ? undefined : ids && ids.slice(0, this.options.fetch_limit),
- // domain + additional
- (replace_domain ? replace_domain : this.domain),
- // ids allready loaded
- (this.id ? [this.id].concat( this.get_child_ids() ) : this.get_child_ids()),
- // option for sending in flat mode by server
- this.options.flat_mode,
- // context + additional
- (replace_context ? replace_context : this.context),
- // parent_id
- this.context.default_parent_id || undefined,
- this.options.fetch_limit,
- ]).done(callback ? _.bind(callback, this, arguments) : this.proxy('switch_new_message')
- ).done(this.proxy('message_fetch_set_read'));
- },
-
- message_fetch_set_read: function (message_list) {
- if (! this.context.mail_read_set_read) return;
- var self = this;
- this.render_mutex.exec(function() {
- msg_ids = _.pluck(message_list, 'id');
- return self.ds_message.call('set_message_read', [msg_ids, true, false, self.context])
- .then(function (nb_read) {
- if (nb_read) {
- self.options.root_thread.MailWidget.do_reload_menu_emails();
- }
- });
- });
- },
-
- /**
- *create the message object and attached on this thread.
- * When the message object is create, this method call insert_message for,
- * displaying this message on the DOM.
- * @param : {object} data from calling RPC to "message_read"
- */
- create_message_object: function (data) {
- var self = this;
-
- data.thread_level = self.thread_level || 0;
- data.options = _.extend(data.options || {}, self.options);
-
- if (data.type=='expandable') {
- var message = new mail.ThreadExpandable(self, data, {'context':{
- 'default_model': data.model || self.context.default_model,
- 'default_res_id': data.res_id || self.context.default_res_id,
- 'default_parent_id': self.id,
- }});
- } else {
- data.record_name= (data.record_name != '' && data.record_name) || (self.parent_message && self.parent_message.record_name);
- var message = new mail.ThreadMessage(self, data, {'context':{
- 'default_model': data.model,
- 'default_res_id': data.res_id,
- 'default_parent_id': data.id,
- }});
- }
-
- // check if the message is already create
- for (var i in self.messages) {
- if (message.id && self.messages[i] && self.messages[i].id == message.id) {
- self.messages[i].destroy();
- }
- }
- self.messages.push( message );
-
- return message;
- },
-
- /**
- *insert the message on the DOM.
- * All message (and expandable message) are sorted. The method get the
- * older and newer message to insert the message (before, after).
- * If there are no older or newer, the message is prepend or append to
- * the thread (parent object or on root thread for flat view).
- * The sort is define by the thread_level (O for newer on top).
- * @param : {object} ThreadMessage object
- */
- insert_message: function (message, dom_insert_after, prepend) {
- var self=this;
- if (this.options.show_compact_message > this.thread_level) {
- this.instantiate_compose_message();
- this.compose_message.do_show_compact();
- }
-
- this.$('.oe_view_nocontent').remove();
- if (dom_insert_after && dom_insert_after.parent()[0] == self.$el[0]) {
- message.insertAfter(dom_insert_after);
- } else if (prepend) {
- message.prependTo(self.$el);
- } else {
- message.appendTo(self.$el);
- }
- message.$el.hide().fadeIn(500);
-
- return message
- },
-
- /**
- *get the parent thread of the messages.
- * Each message is send to his parent object (or parent thread flat mode) for creating the object message.
- * @param : {Array} datas from calling RPC to "message_read"
- */
- switch_new_message: function (records, dom_insert_after) {
- var self=this;
- var dom_insert_after = typeof dom_insert_after == 'object' ? dom_insert_after : false;
- _(records).each(function (record) {
- var thread = self.browse_thread({
- 'id': record.parent_id,
- 'default_return_top_thread':true
- });
- // create object and attach to the thread object
- var message = thread.create_message_object( record );
- // insert the message on dom
- thread.insert_message( message, dom_insert_after);
- });
- if (!records.length && this.options.root_thread == this) {
- this.no_message();
- }
- },
-
- /**
- * this method is call when the widget of a message or an expandable message is destroy
- * in this thread. The this.messages array is filter to remove this message
- */
- on_message_detroy: function (message) {
-
- this.messages = _.filter(this.messages, function (val) { return !val.isDestroyed(); });
- if (this.options.root_thread == this && !this.messages.length) {
- this.no_message();
- }
- return false;
-
- },
-
- /**
- * Convert a destroyed message into a expandable message
- */
- message_to_expandable: function (message) {
-
- if (!this.thread_level || message.isDestroyed()) {
- message.destroy();
- return false;
- }
-
- var messages = _.sortBy( this.messages, function (val) { return val.id; });
- var it = _.indexOf( messages, message );
-
- var msg_up = message.$el.prev('.oe_msg');
- var msg_down = message.$el.next('.oe_msg');
- var msg_up = msg_up.hasClass('oe_msg_expandable') ? _.find( this.messages, function (val) { return val.$el[0] == msg_up[0]; }) : false;
- var msg_down = msg_down.hasClass('oe_msg_expandable') ? _.find( this.messages, function (val) { return val.$el[0] == msg_down[0]; }) : false;
-
- var message_dom = [ ["id", "=", message.id] ];
-
- if ( msg_up && msg_up.type == "expandable" && msg_down && msg_down.type == "expandable") {
- // concat two expandable message and add this message to this dom
- msg_up.domain = mail.ChatterUtils.expand_domain( msg_up.domain );
- msg_down.domain = mail.ChatterUtils.expand_domain( msg_down.domain );
-
- msg_down.domain = ['|','|'].concat( msg_up.domain ).concat( message_dom ).concat( msg_down.domain );
-
- if ( !msg_down.max_limit ) {
- msg_down.nb_messages += 1 + msg_up.nb_messages;
- }
-
- msg_up.$el.remove();
- msg_up.destroy();
-
- msg_down.reinit();
-
- } else if ( msg_up && msg_up.type == "expandable") {
- // concat preview expandable message and this message to this dom
- msg_up.domain = mail.ChatterUtils.expand_domain( msg_up.domain );
- msg_up.domain = ['|'].concat( msg_up.domain ).concat( message_dom );
-
- msg_up.nb_messages++;
-
- msg_up.reinit();
-
- } else if ( msg_down && msg_down.type == "expandable") {
- // concat next expandable message and this message to this dom
- msg_down.domain = mail.ChatterUtils.expand_domain( msg_down.domain );
- msg_down.domain = ['|'].concat( msg_down.domain ).concat( message_dom );
-
- // it's maybe a message expandable for the max limit read message
- if ( !msg_down.max_limit ) {
- msg_down.nb_messages++;
- }
-
- msg_down.reinit();
-
- } else {
- // create a expandable message
- var expandable = new mail.ThreadExpandable(this, {
- 'model': message.model,
- 'parent_id': message.parent_id,
- 'nb_messages': 1,
- 'thread_level': message.thread_level,
- 'parent_id': message.parent_id,
- 'domain': message_dom,
- 'options': message.options,
- }, {
- 'context':{
- 'default_model': message.model || this.context.default_model,
- 'default_res_id': message.res_id || this.context.default_res_id,
- 'default_parent_id': this.id,
- }
- });
-
- // add object on array and DOM
- this.messages.push(expandable);
- expandable.insertAfter(message.$el);
- }
-
- // destroy message
- message.destroy();
-
- return true;
- },
- });
-
- /**
- * ------------------------------------------------------------
- * mail : root Widget
- * ------------------------------------------------------------
- *
- * This widget handles the display of messages with thread options. Its main
- * use is to receive a context and a domain, and to delegate the message
- * fetching and displaying to the Thread widget.
- */
- session.web.client_actions.add('mail.Widget', 'session.mail.Widget');
- mail.Widget = session.web.Widget.extend({
- template: 'mail.Root',
-
- /**
- * @param {Object} parent parent
- * @param {Array} [domain]
- * @param {Object} [context] context of the thread. It should
- * contain at least default_model, default_res_id. Please refer to
- * the compose_message widget for more information about it.
- * @param {Object} [options]
- *... @param {Number} [truncate_limit=250] number of character to
- * display before having a "show more" link; note that the text
- * will not be truncated if it does not have 110% of the parameter
- *... @param {Boolean} [show_record_name] display the name and link for do action
- *... @param {boolean} [show_reply_button] display the reply button
- *... @param {boolean} [show_read_unread_button] display the read/unread button
- *... @param {int} [display_indented_thread] number thread level to indented threads.
- * other are on flat mode
- *... @param {Boolean} [show_compose_message] allow to display the composer
- *... @param {Boolean} [show_compact_message] display the compact message on the thread
- * when the user clic on this compact mode, the composer is open
- *... @param {Array} [message_ids] List of ids to fetch by the root thread.
- * When you use this option, the domain is not used for the fetch root.
- * @param {String} [no_message] Message to display when there are no message
- * @param {Boolean} [show_link] Display partner (authors, followers...) on link or not
- * @param {Boolean} [compose_as_todo] The root composer mark automatically the message as todo
- * @param {Boolean} [readonly] Read only mode, hide all action buttons and composer
- */
- init: function (parent, action) {
- this._super(parent, action);
- var self = this;
- this.action = _.clone(action);
- this.domain = this.action.domain || this.action.params.domain || [];
- this.context = this.action.context || this.action.params.context || {};
-
- this.action.params = _.extend({
- 'display_indented_thread' : -1,
- 'show_reply_button' : false,
- 'show_read_unread_button' : false,
- 'truncate_limit' : 250,
- 'show_record_name' : false,
- 'show_compose_message' : false,
- 'show_compact_message' : false,
- 'compose_placeholder': false,
- 'show_link': true,
- 'view_inbox': false,
- 'message_ids': undefined,
- 'compose_as_todo' : false,
- 'readonly' : false,
- 'emails_from_on_composer': true,
- 'fetch_limit': 30 // limit of chatter messages
- }, this.action.params);
-
- this.action.params.help = this.action.help || false;
- },
-
- start: function (options) {
- this._super.apply(this, arguments);
- this.message_render();
- },
-
- /**
- * create an object "related_menu"
- * contains the menu widget and the sub menu related of this wall
- */
- do_reload_menu_emails: function () {
- var menu = session.webclient.menu;
- if (!menu || !menu.current_menu) {
- return $.when();
- }
- return menu.rpc("/web/menu/load_needaction", {'menu_ids': [menu.current_menu]}).done(function(r) {
- menu.on_needaction_loaded(r);
- }).then(function () {
- menu.trigger("need_action_reloaded");
- });
- },
-
- /**
- *Create the root thread and display this object in the DOM.
- * Call the no_message method then c all the message_fetch method
- * of this root thread to display the messages.
- */
- message_render: function (search) {
-
- this.thread = new mail.Thread(this, {}, {
- 'domain' : this.domain,
- 'context' : this.context,
- 'options': this.action.params,
- });
-
- this.thread.appendTo( this.$el );
-
- if (this.action.params.show_compose_message) {
- this.thread.instantiate_compose_message();
- this.thread.compose_message.do_show_compact();
- }
-
- this.thread.message_fetch(null, null, this.action.params.message_ids);
-
- },
-
- });
-
-
- /**
- * ------------------------------------------------------------
- * mail_thread Widget
- * ------------------------------------------------------------
- *
- * This widget handles the display of messages on a document. Its main
- * use is to receive a context and a domain, and to delegate the message
- * fetching and displaying to the Thread widget.
- * Use Help on the field to display a custom "no message loaded"
- */
- session.web.form.widgets.add('mail_thread', 'openerp.mail.RecordThread');
- mail.RecordThread = session.web.form.AbstractField.extend({
- template: 'mail.record_thread',
-
- init: function (parent, node) {
- this._super.apply(this, arguments);
- this.ParentViewManager = parent;
- this.node = _.clone(node);
- this.node.params = _.extend({
- 'display_indented_thread': -1,
- 'show_reply_button': false,
- 'show_read_unread_button': true,
- 'read_action': 'unread',
- 'show_record_name': false,
- 'show_compact_message': 1,
- 'display_log_button' : true,
- }, this.node.params);
- if (this.node.attrs.placeholder) {
- this.node.params.compose_placeholder = this.node.attrs.placeholder;
- }
- if (this.node.attrs.readonly) {
- this.node.params.readonly = this.node.attrs.readonly;
- }
- if ('display_log_button' in this.options) {
- this.node.params.display_log_button = this.options.display_log_button;
- }
- this.domain = (this.node.params && this.node.params.domain) || (this.field && this.field.domain) || [];
-
- if (!this.ParentViewManager.is_action_enabled('edit')) {
- this.node.params.show_link = false;
- }
- },
-
- start: function () {
- this._super.apply(this, arguments);
- // NB: check the actual_mode property on view to know if the view is in create mode anymore
- this.view.on("change:actual_mode", this, this._check_visibility);
- this._check_visibility();
- },
-
- _check_visibility: function () {
- this.$el.toggle(this.view.get("actual_mode") !== "create");
- },
-
- render_value: function () {
- var self = this;
-
- if (! this.view.datarecord.id || session.web.BufferedDataSet.virtual_id_regex.test(this.view.datarecord.id)) {
- this.$('oe_mail_thread').hide();
- return;
- }
-
- this.node.params = _.extend(this.node.params, {
- 'message_ids': this.get_value(),
- 'show_compose_message': this.view.is_action_enabled('edit'),
- });
- this.node.context = {
- 'mail_read_set_read': true, // set messages as read in Chatter
- 'default_res_id': this.view.datarecord.id || false,
- 'default_model': this.view.model || false,
- };
-
- if (this.root) {
- $('').insertAfter(this.root.$el);
- this.root.destroy();
- }
- // create and render Thread widget
- this.root = new mail.Widget(this, _.extend(this.node, {
- 'domain' : (this.domain || []).concat([['model', '=', this.view.model], ['res_id', '=', this.view.datarecord.id]]),
- }));
-
- return this.root.replace(this.$('.oe_mail-placeholder'));
- },
- });
-
-
- /**
- * ------------------------------------------------------------
- * Aside Widget
- * ------------------------------------------------------------
- *
- * This widget handles the display of a sidebar on the Wall. Its main use
- * is to display group and employees suggestion (if hr is installed).
- */
- mail.WallSidebar = session.web.Widget.extend({
- template: 'mail.wall.sidebar',
- });
-
-
- /**
- * ------------------------------------------------------------
- * Wall Widget
- * ------------------------------------------------------------
- *
- * This widget handles the display of messages on a Wall. Its main
- * use is to receive a context and a domain, and to delegate the message
- * fetching and displaying to the Thread widget.
- */
-
- session.web.client_actions.add('mail.wall', 'session.mail.Wall');
- mail.Wall = session.web.Widget.extend({
- template: 'mail.wall',
-
- /**
- * @param {Object} parent parent
- * @param {Object} [options]
- * @param {Array} [options.domain] domain on the Wall
- * @param {Object} [options.context] context, is an object. It should
- * contain default_model, default_res_id, to give it to the threads.
- */
- init: function (parent, action) {
- this._super(parent, action);
- this.ActionManager = parent;
-
- this.action = _.clone(action);
- this.domain = this.action.params.domain || this.action.domain || [];
- this.context = _.extend(this.action.params.context || {}, this.action.context || {});
-
- // filter some parameters that we will propagate as search_default
- this.defaults = {};
- for (var key in this.action.context.params) {
- if (_.indexOf(['model', 'res_id'], key) == -1) {
- continue;
- }
- this.context['search_default_' + key] = this.action.context.params[key];
- }
- for (var key in this.context) {
- if (key.match(/^search_default_/)) {
- this.defaults[key.replace(/^search_default_/, '')] = this.context[key];
- }
- }
- this.action.params = _.extend({
- 'display_indented_thread': 1,
- 'show_reply_button': true,
- 'show_read_unread_button': true,
- 'show_compose_message': true,
- 'show_record_name': true,
- 'show_compact_message': this.action.params.view_mailbox ? false : 1,
- 'view_inbox': false,
- 'emails_from_on_composer': false,
- 'fetch_limit': 30 // allow inbox to load all children messages
- }, this.action.params);
- },
-
- start: function () {
- this._super.apply(this);
- this.bind_events();
- var searchview_loaded = this.load_searchview(this.defaults);
- if (! this.searchview.has_defaults) {
- this.message_render();
- }
- // render sidebar
- var wall_sidebar = new mail.WallSidebar(this);
- wall_sidebar.appendTo(this.$el.find('.oe_mail_wall_aside'));
- },
-
- /**
- * Load the mail.message search view
- * @param {Object} defaults ??
- */
- load_searchview: function (defaults) {
- var self = this,
- ds_msg = new session.web.DataSetSearch(this, 'mail.message'),
- options = { $buttons: this.$('.oe-search-options') };
- this.searchview = new session.web.SearchView(this, ds_msg, false, defaults || {}, options);
- this.searchview.on('search_data', this, this.do_searchview_search);
- this.searchview.appendTo(this.$('.oe-view-manager-search-view')).then(function () {
- self.searchview.toggle_visibility(true);
- });
- if (this.searchview.has_defaults) {
- this.searchview.ready.then(this.searchview.do_search);
- }
- return this.searchview
- },
-
- /**
- * Get the domains, contexts and groupbys in parameter from search
- * view, then render the filtered threads.
- * @param {Array} domains
- * @param {Array} contexts
- * @param {Array} groupbys
- */
- do_searchview_search: function (domains, contexts, groupbys) {
- var self = this;
- session.web.pyeval.eval_domains_and_contexts({
- domains: domains || [],
- contexts: contexts || [],
- group_by_seq: groupbys || []
- }).then(function (results) {
- if(self.root) {
- $('').insertAfter(self.root.$el);
- self.root.destroy();
- }
- return self.message_render(results);
- });
- },
-
- /**
- * Create the root thread widget and display this object in the DOM
- */
- message_render: function (search) {
- var domain = this.domain.concat(search && search['domain'] ? search['domain'] : []);
- var context = _.extend(this.context, search && search['context'] ? search['context'] : {});
-
- this.root = new mail.Widget(this, _.extend(this.action, {
- 'domain' : domain,
- 'context' : context,
- }));
- return this.root.replace(this.$('.oe_mail-placeholder'));
- },
-
- bind_events: function () {
- var self=this;
- this.$(".oe_write_full").click(function (event) {
- event.stopPropagation();
- var action = {
- name: _t('Compose Email'),
- type: 'ir.actions.act_window',
- res_model: 'mail.compose.message',
- view_mode: 'form',
- view_type: 'form',
- action_from: 'mail.ThreadComposeMessage',
- views: [[false, 'form']],
- target: 'new',
- context: {
- },
- };
- session.client.action_manager.do_action(action);
- });
- this.$(".oe_write_onwall").click(function (event) { self.root.thread.on_compose_message(event); });
}
- });
+ },
- /**
- * ------------------------------------------------------------
- * Sub-widgets loading
- * ------------------------------------------------------------
- *
- * Load here widgets that could depend on widgets defined in mail.js
- */
+ bind_events: function () {
+ var self = this;
+ this.$('.oe_compact_inbox').on('click', self.on_toggle_quick_composer);
+ this.$('.oe_compose_post').on('click', self.on_toggle_quick_composer);
+ this.$('.oe_compose_log').on('click', self.on_toggle_quick_composer);
+ this.$('input.oe_form_binary_file').on('change', _.bind( this.on_attachment_change, this));
+ this.$('.oe_cancel').on('click', _.bind( this.on_cancel, this));
+ this.$('.oe_post').on('click', self.on_message_post);
+ this.$('.oe_full').on('click', _.bind( this.on_compose_fullmail, this, this.id ? 'reply' : 'comment'));
+ /* stack for don't close the compose form if the user click on a button */
+ this.$('.oe_msg_left, .oe_msg_center').on('mousedown', _.bind( function () { this.stay_open = true; }, this));
+ this.$('.oe_msg_left, .oe_msg_content').on('mouseup', _.bind( function () { this.$('textarea').focus(); }, this));
+ var ev_stay = {};
+ ev_stay.mouseup = ev_stay.keydown = ev_stay.focus = function () { self.stay_open = false; };
+ this.$('textarea').on(ev_stay);
+ this.$('textarea').autosize();
- openerp.mail.suggestions(session, mail); // import suggestion.js (suggestion widget)
+ // auto close
+ this.$('textarea').on('blur', self.on_toggle_quick_composer);
+
+ // event: delete child attachments off the oe_msg_attachment_list box
+ this.$(".oe_msg_attachment_list").on('click', '.oe_delete', this.on_attachment_delete);
+
+ this.$(".oe_recipients").on('change', 'input', this.on_checked_recipient);
+ },
+
+ on_compose_fullmail: function (default_composition_mode) {
+ var self = this;
+ if(!this.do_check_attachment_upload()) {
+ return false;
+ }
+ var recipient_done = $.Deferred();
+ if (this.is_log) {
+ recipient_done.resolve([]);
+ }
+ else {
+ recipient_done = this.check_recipient_partners();
+ }
+ $.when(recipient_done).done(function (partner_ids) {
+ var context = {
+ 'default_parent_id': self.id,
+ 'default_body': mail_utils.get_text2html(self.$el ? (self.$el.find('textarea:not(.oe_compact)').val() || '') : ''),
+ 'default_attachment_ids': _.map(self.attachment_ids, function (file) {return file.id;}),
+ 'default_partner_ids': partner_ids,
+ 'default_is_log': self.is_log,
+ 'mail_post_autofollow': true,
+ 'mail_post_autofollow_partner_ids': partner_ids,
+ 'is_private': self.is_private
+ };
+ if (default_composition_mode != 'reply' && self.context.default_model && self.context.default_res_id) {
+ context.default_model = self.context.default_model;
+ context.default_res_id = self.context.default_res_id;
+ }
- /**
- * ------------------------------------------------------------
- * UserMenu
- * ------------------------------------------------------------
- *
- * Add a link on the top user bar for write a full mail
- */
- session.web.ComposeMessageTopButton = session.web.Widget.extend({
- template:'mail.compose_message_top_button',
- events: {
- "click": "on_compose_message",
- },
- on_compose_message: function (ev) {
- ev.preventDefault();
var action = {
type: 'ir.actions.act_window',
res_model: 'mail.compose.message',
@@ -2082,12 +436,1558 @@ openerp.mail = function (session) {
view_type: 'form',
views: [[false, 'form']],
target: 'new',
- context: {},
+ context: context,
};
- session.client.action_manager.do_action(action);
- },
- });
- // Put the ComposeMessageTopButton widget in the systray menu
- session.web.SystrayItems.push(session.web.ComposeMessageTopButton);
+ self.do_action(action);
+ self.on_cancel();
+ });
+
+ },
+
+ reinit: function() {
+ var $render = $( qweb.render('mail.compose_message', {'widget': this}) );
+
+ $render.insertAfter(this.$el.last());
+ this.$el.remove();
+ this.$el = $render;
+
+ this.display_attachments();
+ this.bind_events();
+ },
+
+ on_cancel: function (event) {
+ if (event) event.stopPropagation();
+ this.attachment_ids=[];
+ this.stay_open = false;
+ this.show_composer = false;
+ this.reinit();
+ },
+
+ /* return true if all file are complete else return false and make an alert */
+ do_check_attachment_upload: function () {
+ if (_.find(this.attachment_ids, function (file) {return file.upload;})) {
+ this.do_warn(qweb.render('mail.error_upload'), qweb.render('mail.error_upload_please_wait'));
+ return false;
+ } else {
+ return true;
+ }
+ },
+
+ check_recipient_partners: function () {
+ var self = this;
+ var check_done = $.Deferred();
+ var recipients = _.filter(this.recipients, function (recipient) { return recipient.checked; });
+ var recipients_to_find = _.filter(recipients, function (recipient) { return (! recipient.partner_id); });
+ var names_to_find = _.pluck(recipients_to_find, 'full_name');
+ var recipients_to_check = _.filter(recipients, function (recipient) { return (recipient.partner_id && ! recipient.email_address); });
+ var recipient_ids = _.pluck(_.filter(recipients, function (recipient) { return recipient.partner_id && recipient.email_address; }), 'partner_id');
+ var names_to_remove = [];
+ var recipient_ids_to_remove = [];
+
+ // have unknown names -> call message_get_partner_info_from_emails to try to find partner_id
+ var find_done = $.Deferred();
+ if (names_to_find.length > 0) {
+ find_done = self.parent_thread.ds_thread._model.call('message_partner_info_from_emails', [this.context.default_res_id, names_to_find]);
+ }
+ else {
+ find_done.resolve([]);
+ }
+
+ // for unknown names + incomplete partners -> open popup - cancel = remove from recipients
+ $.when(find_done).pipe(function (result) {
+ var emails_deferred = [];
+ var recipient_popups = result.concat(recipients_to_check);
+
+ _.each(recipient_popups, function (partner_info) {
+ var deferred = $.Deferred();
+ emails_deferred.push(deferred);
+
+ var partner_name = partner_info.full_name;
+ var partner_id = partner_info.partner_id;
+ var parsed_email = mail_utils.parse_email(partner_name);
+
+ var pop = new form_common.FormOpenPopup(this);
+ pop.show_element(
+ 'res.partner',
+ partner_id,
+ { 'force_email': true,
+ 'ref': "compound_context",
+ 'default_name': parsed_email[0],
+ 'default_email': parsed_email[1],
+ }, {
+ title: _t("Please complete partner's informations"),
+ }
+ );
+ pop.on('closed', self, function () {
+ deferred.resolve();
+ });
+ pop.view_form.on('on_button_cancel', self, function () {
+ names_to_remove.push(partner_name);
+ if (partner_id) {
+ recipient_ids_to_remove.push(partner_id);
+ }
+ });
+ });
+
+ $.when.apply($, emails_deferred).then(function () {
+ var new_names_to_find = _.difference(names_to_find, names_to_remove);
+ find_done = $.Deferred();
+ if (new_names_to_find.length > 0) {
+ find_done = self.parent_thread.ds_thread._model.call('message_partner_info_from_emails', [self.context.default_res_id, new_names_to_find, true]);
+ }
+ else {
+ find_done.resolve([]);
+ }
+ $.when(find_done).pipe(function (result) {
+ var recipient_popups = result.concat(recipients_to_check);
+ _.each(recipient_popups, function (partner_info) {
+ if (partner_info.partner_id && _.indexOf(partner_info.partner_id, recipient_ids_to_remove) == -1) {
+ recipient_ids.push(partner_info.partner_id);
+ }
+ });
+ }).pipe(function () {
+ check_done.resolve(recipient_ids);
+ });
+ });
+ });
+
+ return check_done;
+ },
+
+ on_message_post: function () {
+ var self = this;
+ if (self.flag_post) {
+ return;
+ }
+ if (this.do_check_attachment_upload() && (this.attachment_ids.length || this.$('textarea').val().match(/\S+/))) {
+ self.flag_post = true;
+ if (this.is_log) {
+ this.do_send_message_post([], this.is_log);
+ }
+ else {
+ this.check_recipient_partners().done(function (partner_ids) {
+ self.do_send_message_post(partner_ids, self.is_log);
+ });
+ }
+ }
+ },
+
+ /* do post a message and fetch the message */
+ do_send_message_post: function (partner_ids, log) {
+ var self = this;
+ var values = {
+ 'body': this.$('textarea').val(),
+ 'subject': false,
+ 'parent_id': this.context.default_parent_id,
+ 'attachment_ids': _.map(this.attachment_ids, function (file) {return file.id;}),
+ 'partner_ids': partner_ids,
+ 'context': _.extend(this.parent_thread.context, {
+ 'mail_post_autofollow': true,
+ 'mail_post_autofollow_partner_ids': partner_ids,
+ }),
+ 'type': 'comment',
+ 'content_subtype': 'plaintext',
+ };
+ if (log) {
+ values.subtype = false;
+ }
+ else {
+ values.subtype = 'mail.mt_comment';
+ }
+ this.parent_thread.ds_thread._model.call('message_post', [this.context.default_res_id], values).done(function (message_id) {
+ var thread = self.parent_thread;
+ var root = thread == self.options.root_thread;
+ if (self.options.display_indented_thread < self.thread_level && thread.parent_message) {
+ thread = thread.parent_message.parent_thread;
+ }
+ // create object and attach to the thread object
+ thread.message_fetch([["id", "=", message_id]], false, [message_id], function (arg, data) {
+ var message = thread.create_message_object( data.slice(-1)[0] );
+ // insert the message on dom
+ thread.insert_message( message, root ? undefined : self.$el, root );
+ });
+ self.on_cancel();
+ self.flag_post = false;
+ });
+ },
+
+ /* Quick composer: toggle minimal / expanded mode
+ * - toggle minimal (one-liner) / expanded (textarea, buttons) mode
+ * - when going into expanded mode:
+ * - call `message_get_suggested_recipients` to have a list of partners to add
+ * - compute email_from list (list of unknown email_from to propose to create partners)
+ */
+ on_toggle_quick_composer: function (event) {
+ var self = this;
+ var $input = $(event.target);
+ this.compute_emails_from();
+ var email_addresses = _.pluck(this.recipients, 'email_address');
+ var suggested_partners = $.Deferred();
+
+ // if clicked: call for suggested recipients
+ if (event.type == 'click') {
+ this.is_log = $input.hasClass('oe_compose_log');
+ suggested_partners = this.parent_thread.ds_thread.call('message_get_suggested_recipients', [[this.context.default_res_id], this.context]).done(function (additional_recipients) {
+ var thread_recipients = additional_recipients[self.context.default_res_id];
+ _.each(thread_recipients, function (recipient) {
+ var parsed_email = mail_utils.parse_email(recipient[1]);
+ if (_.indexOf(email_addresses, parsed_email[1]) == -1) {
+ self.recipients.push({
+ 'checked': true,
+ 'partner_id': recipient[0],
+ 'full_name': recipient[1],
+ 'name': parsed_email[0],
+ 'email_address': parsed_email[1],
+ 'reason': recipient[2],
+ });
+ }
+ });
+ });
+ }
+ else {
+ suggested_partners.resolve({});
+ }
+
+ // when call for suggested partners finished: re-render the widget
+ $.when(suggested_partners).pipe(function () {
+ if ((!self.stay_open || (event && event.type == 'click')) && (!self.show_composer || !self.$('textarea:not(.oe_compact)').val().match(/\S+/) && !self.attachment_ids.length)) {
+ self.show_composer = !self.show_composer || self.stay_open;
+ self.reinit();
+ }
+ if (!self.stay_open && self.show_composer && (!event || event.type != 'blur')) {
+ self.$('textarea:not(.oe_compact):first').focus();
+ }
+ });
+
+ return suggested_partners;
+
+ },
+
+ do_hide_compact: function () {
+ this.show_compact_message = false;
+ if (!this.show_composer) {
+ this.reinit();
+ }
+ },
+
+ do_show_compact: function () {
+ this.show_compact_message = true;
+ if (!this.show_composer) {
+ this.reinit();
+ }
+ },
+
+ /** Compute the list of unknown email_from the the given thread
+ * TDE FIXME: seems odd to delegate to the composer
+ * TDE TODO: please de-obfuscate and comment your code */
+ compute_emails_from: function () {
+ var self = this;
+ var messages = [];
+
+ if (this.parent_thread.parent_message) {
+ // go to the parented message
+ var message = this.parent_thread.parent_message;
+ var parent_message = message.parent_id ? message.parent_thread.parent_message : message;
+ if(parent_message){
+ messages = [parent_message].concat(parent_message.get_childs());
+ }
+ } else if (this.options.emails_from_on_composer) {
+ // get all wall messages if is not a mail.Wall
+ _.each(this.options.root_thread.messages, function (msg) {messages.push(msg); messages.concat(msg.get_childs());});
+ }
+
+ _.each(messages, function (thread) {
+ if (thread.author_id && !thread.author_id[0] &&
+ !_.find(self.recipients, function (recipient) {return recipient.email_address == thread.author_id[3];}) &&
+ _.some([thread.author_id[1], thread.author_id[2], thread.author_id[3]])) {
+ self.recipients.push({ 'full_name': thread.author_id[1],
+ 'name': thread.author_id[2],
+ 'email_address': thread.author_id[3],
+ 'partner_id': false,
+ 'checked': true,
+ 'reason': 'Incoming email author'
+ });
+ }
+ });
+ return self.recipients;
+ },
+
+ on_checked_recipient: function (event) {
+ var $input = $(event.target);
+ var full_name = $input.attr("data");
+ _.each(this.recipients, function (recipient) {
+ if (recipient.full_name == full_name) {
+ recipient.checked = $input.is(":checked");
+ }
+ });
+ },
+});
+
+/**
+ * ------------------------------------------------------------
+ * Thread Message Expandable Widget
+ * ------------------------------------------------------------
+ *
+ * This widget handles the display the expandable message in a thread.
+ * - thread
+ * - - visible message
+ * - - expandable
+ * - - visible message
+ * - - visible message
+ * - - expandable
+ */
+var ThreadExpandable = MessageCommon.extend({
+ template: 'mail.thread.expandable',
+
+ init: function (parent, datasets, options) {
+ this._super(parent, datasets, options);
+ this.type = 'expandable';
+ this.max_limit = datasets.max_limit;
+ this.nb_messages = datasets.nb_messages;
+ this.flag_used = false;
+ },
+
+ start: function () {
+ this._super.apply(this, arguments);
+ this.bind_events();
+ },
+
+ reinit: function () {
+ var $render = $(qweb.render('mail.thread.expandable', {'widget': this}));
+ this.$el.replaceWith( $render );
+ this.$el = $render;
+ this.bind_events();
+ },
+
+ /**
+ * Bind events in the widget. Each event is slightly described
+ * in the function. */
+ bind_events: function () {
+ this.$('.oe_msg_more_message').on('click', this.on_expandable);
+ },
+
+ animated_destroy: function (fadeTime) {
+ var self=this;
+ this.$el.fadeOut(fadeTime, function () {
+ self.destroy();
+ });
+ },
+
+ /*The selected thread and all childs (messages/thread) became read
+ * @param {object} mouse envent
+ */
+ on_expandable: function (event) {
+ var self = this;
+ if (event)event.stopPropagation();
+ if (this.flag_used) {
+ return false;
+ }
+ this.flag_used = true;
+
+ // read messages
+ self.parent_thread.message_fetch(this.domain, this.context, false, function (arg, data) {
+ self.id = false;
+ // insert the message on dom after this message
+ var element = self.$el;
+ if (self.thread_level === 0){
+ element = element.parent();
+ }
+ self.parent_thread.switch_new_message(data, element);
+ self.animated_destroy(200);
+ });
+
+ return false;
+ },
+
+});
+
+var ThreadMessage = MessageCommon.extend({
+ template: 'mail.thread.message',
+
+ start: function () {
+ this._super.apply(this, arguments);
+ this.bind_events();
+ if(this.thread_level < this.options.display_indented_thread) {
+ this.create_thread();
+ }
+ this.display_attachments();
+
+ this.ds_notification = new data.DataSetSearch(this, 'mail.notification');
+ this.ds_message = new data.DataSetSearch(this, 'mail.message');
+ },
+
+ /**
+ * Bind events in the widget. Each event is slightly described
+ * in the function. */
+ bind_events: function () {
+ // header icons bindings
+ this.$('.oe_read').on('click', this.on_message_read);
+ this.$('.oe_unread').on('click', this.on_message_unread);
+ this.$('.oe_msg_delete').on('click', this.on_message_delete);
+ this.$('.oe_reply').on('click', this.on_message_reply);
+ this.$('.oe_star').on('click', this.on_star);
+ this.$('.oe_msg_vote').on('click', this.on_vote);
+ this.$('.oe_mail_vote_count').on('mouseenter', this.on_hover);
+ this.$('.oe_mail_expand').on('click', this.on_expand);
+ this.$('.oe_mail_reduce').on('click', this.on_expand);
+ this.$('.oe_mail_action_model').on('click', this.on_record_clicked);
+ this.$('.oe_mail_action_author').on('click', this.on_record_author_clicked);
+ },
+ on_hover : function(event){
+ var self = this;
+ var voter = "";
+ var limit = 10;
+ event.stopPropagation();
+ var $target = $(event.target).hasClass("fa-thumbs-o-up") ? $(event.target).parent() : $(event.target);
+ //Note: We can set data-content attr on target element once we fetch data so that next time when one moves mouse on element it saves call
+ //But if there is new like comes then we'll not have new likes in popover in that case
+ if ($target.data('liker-list'))
+ {
+ voter = $target.data('liker-list');
+ self.bindTooltipTo($target, voter);
+ $target.tooltip('hide').tooltip('show');
+ $(".tooltip").on("mouseleave", function () {
+ $(this).remove();
+ });
+ }else{
+ this.ds_message.call('get_likers_list', [this.id, limit])
+ .done(function (data) {
+ _.each(data, function(people, index) {
+ voter = voter + people.substring(0,1).toUpperCase() + people.substring(1);
+ if(index != data.length-1) {
+ voter = voter + "
";
+ }
+ });
+ $target.data('liker-list', voter);
+ self.bindTooltipTo($target, voter);
+ $target.tooltip('hide').tooltip('show');
+ $(".tooltip").on("mouseleave", function () {
+ $(this).remove();
+ });
+ });
+ }
+ return true;
+ },
+ bindTooltipTo: function($el, value) {
+ $el.tooltip({
+ 'title': value,
+ 'placement': 'top',
+ 'container': this.el,
+ 'html': true,
+ 'trigger': 'manual',
+ 'animation': false
+ }).on("mouseleave", function () {
+ setTimeout(function () {
+ if (!$(".tooltip:hover").length) {
+ $el.tooltip("hide");
+ }
+ },100);
+ });
+ },
+ on_record_clicked: function (event) {
+ event.preventDefault();
+ var self = this;
+ var state = {
+ 'model': this.model,
+ 'id': this.res_id,
+ 'title': this.record_name
+ };
+ web_client.action_manager.do_push_state(state);
+ this.context.params = {
+ model: this.model,
+ res_id: this.res_id,
+ };
+ this.thread.ds_thread.call("message_redirect_action", {context: this.context}).then(function(action){
+ self.do_action(action);
+ });
+ },
+
+ on_record_author_clicked: function (event) {
+ event.preventDefault();
+ var partner_id = $(event.target).data('partner');
+ var state = {
+ 'model': 'res.partner',
+ 'id': partner_id,
+ 'title': this.record_name
+ };
+ web_client.action_manager.do_push_state(state);
+ var action = {
+ type:'ir.actions.act_window',
+ view_type: 'form',
+ view_mode: 'form',
+ res_model: 'res.partner',
+ views: [[false, 'form']],
+ res_id: partner_id,
+ };
+ this.do_action(action);
+ },
+
+ /* Call the on_compose_message on the thread of this message. */
+ on_message_reply:function (event) {
+ event.stopPropagation();
+ this.create_thread();
+ this.thread.on_compose_message(event);
+ return false;
+ },
+
+ on_expand: function (event) {
+ event.stopPropagation();
+ this.$('.oe_msg_body:first > .oe_msg_body_short:first').toggle();
+ this.$('.oe_msg_body:first > .oe_msg_body_long:first').toggle();
+ return false;
+ },
+
+ /**
+ * Instantiate the thread object of this message.
+ * Each message have only one thread.
+ */
+ create_thread: function () {
+ if (this.thread) {
+ return false;
+ }
+ /*create thread*/
+ this.thread = new Thread(this, this, {
+ 'domain': this.domain,
+ 'context':{
+ 'default_model': this.model,
+ 'default_res_id': this.res_id,
+ 'default_parent_id': this.id
+ },
+ 'options': this.options
+ }
+ );
+ /*insert thread in parent message*/
+ this.thread.insertAfter(this.$el);
+ },
+
+ /**
+ * Fade out the message and his child thread.
+ * Then this object is destroyed.
+ */
+ animated_destroy: function (fadeTime) {
+ var self=this;
+ this.$el.fadeOut(fadeTime, function () {
+ self.parent_thread.message_to_expandable(self);
+ });
+ if (this.thread) {
+ this.thread.$el.fadeOut(fadeTime);
+ }
+ },
+
+ /**
+ * Wait a confirmation for delete the message on the DB.
+ * Make an animate destroy
+ */
+ on_message_delete: function (event) {
+ event.stopPropagation();
+ if (! confirm(_t("Do you really want to delete this message?"))) { return false; }
+
+ this.animated_destroy(150);
+ // delete this message and his childs
+ var ids = [this.id].concat( this.get_child_ids() );
+ this.ds_message.unlink(ids);
+ return false;
+ },
+
+ /* Check if the message must be destroy and detroy it or check for re render widget
+ * @param {callback} apply function
+ */
+ check_for_rerender: function () {
+ var self = this;
+
+ var messages = [this].concat(this.get_childs());
+ var message_ids = _.map(messages, function (msg) { return msg.id;});
+ var domain = mail_utils.expand_domain( this.options.root_thread.domain )
+ .concat([["id", "in", message_ids ]]);
+
+ return this.parent_thread.ds_message.call('message_read', [undefined, domain, [], !!this.parent_thread.options.display_indented_thread, this.context, this.parent_thread.id])
+ .then( function (records) {
+ // remove message not loaded
+ _.map(messages, function (msg) {
+ if(!_.find(records, function (record) { return record.id == msg.id; })) {
+ msg.animated_destroy(150);
+ } else {
+ msg.renderElement();
+ msg.start();
+ }
+ self.options.root_thread.MailWidget.do_reload_menu_emails();
+ });
+
+ });
+ },
+
+ on_message_read: function (event) {
+ event.stopPropagation();
+ this.on_message_read_unread(true);
+ return false;
+ },
+
+ on_message_unread: function (event) {
+ event.stopPropagation();
+ this.on_message_read_unread(false);
+ return false;
+ },
+
+ /* Set the selected thread and all childs as read or unread, based on
+ * read parameter.
+ * @param {boolean} read_value
+ */
+ on_message_read_unread: function (read_value) {
+ var self = this;
+ var messages = [this].concat(this.get_childs());
+
+ // inside the inbox, when the user mark a message as read/done, don't apply this value
+ // for the stared/favorite message
+ if (this.options.view_inbox && read_value) {
+ messages = _.filter(messages, function (val) { return !val.is_favorite && val.id; });
+ if (!messages.length) {
+ this.check_for_rerender();
+ return false;
+ }
+ }
+ var message_ids = _.map(messages, function (val) { return val.id; });
+
+ this.ds_message.call('set_message_read', [message_ids, read_value, true, this.context])
+ .then(function () {
+ // apply modification
+ _.each(messages, function (msg) {
+ msg.to_read = !read_value;
+ if (msg.options.toggle_read) {
+ msg.options.show_read = msg.to_read;
+ msg.options.show_unread = !msg.to_read;
+ }
+ });
+ // check if the message must be display, destroy or rerender
+ self.check_for_rerender();
+ });
+ return false;
+ },
+
+ /**
+ * add or remove a vote for a message and display the result
+ */
+ on_vote: function (event) {
+ event.stopPropagation();
+ this.ds_message.call('vote_toggle', [[this.id]])
+ .then(
+ _.bind(function (vote) {
+ this.has_voted = vote;
+ this.vote_nb += this.has_voted ? 1 : -1;
+ this.display_vote();
+ }, this));
+ return false;
+ },
+
+ /**
+ * Display the render of this message's vote
+ */
+ display_vote: function () {
+ var vote_element = qweb.render('mail.thread.message.vote', {'widget': this});
+ this.$(".oe_msg_footer:first .oe_mail_vote_count").remove();
+ this.$(".oe_msg_footer:first .oe_msg_vote").replaceWith(vote_element);
+ this.$('.oe_msg_vote').on('click', this.on_vote);
+ this.$('.oe_mail_vote_count').on('mouseenter', this.on_hover);
+ },
+
+ /**
+ * add or remove a favorite (or starred) for a message and change class on the DOM
+ */
+ on_star: function (event) {
+ event.stopPropagation();
+ var self=this;
+ var button = self.$('.oe_star:first');
+
+ this.ds_message.call('set_message_starred', [[self.id], !self.is_favorite, true])
+ .then(function (star) {
+ self.is_favorite=star;
+ if (self.is_favorite) {
+ button.addClass('oe_starred');
+ } else {
+ button.removeClass('oe_starred');
+ }
+
+ if (self.options.view_inbox && self.is_favorite) {
+ self.on_message_read_unread(true);
+ } else {
+ self.check_for_rerender();
+ }
+ });
+ return false;
+ },
+
+});
+
+/**
+ * ------------------------------------------------------------
+ * Thread Widget
+ * ------------------------------------------------------------
+ *
+ * This widget handles the display of a thread of messages. The
+ * thread view:
+ * - root thread
+ * - - sub message (parent_id = root message)
+ * - - - sub thread
+ * - - - - sub sub message (parent id = sub thread)
+ * - - sub message (parent_id = root message)
+ * - - - sub thread
+ */
+var Thread = Widget.extend({
+ template: 'mail.thread',
+
+ /**
+ * @param {Object} parent parent
+ * @param {Array} [domain]
+ * @param {Object} [context] context of the thread. It should
+ contain at least default_model, default_res_id. Please refer to
+ the ComposeMessage widget for more information about it.
+ * @param {Object} [options]
+ * @param {Object} [message] read about mail.ThreadMessage object
+ * @param {Object} [thread]
+ * @param {int} [display_indented_thread] number thread level to indented threads.
+ * other are on flat mode
+ * @param {Array} [parents] liked with the parents thread
+ * use with browse, fetch... [O]= top parent
+ */
+ init: function (parent, datasets, options) {
+ this._super(parent, options);
+ this.MailWidget = parent instanceof MailWidget ? parent : false;
+ this.domain = options.domain || [];
+ this.context = _.extend(options.context || {});
+
+ this.options = options.options;
+ this.options.root_thread = (options.options.root_thread !== undefined ? options.options.root_thread : this);
+ this.options.show_compose_message = this.options.show_compose_message && !this.thread_level;
+
+ // record options and data
+ this.parent_message= parent.thread !== undefined ? parent : false ;
+
+ // data of this thread
+ this.id = datasets.id || false;
+ this.last_id = datasets.last_id || false;
+ this.parent_id = datasets.parent_id || false;
+ this.is_private = datasets.is_private || false;
+ this.author_id = datasets.author_id || false;
+ this.thread_level = (datasets.thread_level+1) || 0;
+ datasets.partner_ids = datasets.partner_ids || [];
+ if (datasets.author_id && !_.contains(_.flatten(datasets.partner_ids),datasets.author_id[0]) && datasets.author_id[0]) {
+ datasets.partner_ids.push(datasets.author_id);
+ }
+ this.user_pid = datasets.user_pid || false;
+ this.partner_ids = datasets.partner_ids;
+ this.messages = [];
+ this.options.flat_mode = (this.options.display_indented_thread - this.thread_level > 0);
+
+ // object compose message
+ this.compose_message = false;
+
+ this.ds_thread = new data.DataSetSearch(this, this.context.default_model || 'mail.thread');
+ this.ds_message = new data.DataSetSearch(this, 'mail.message');
+ this.render_mutex = new utils.Mutex();
+ },
+
+ start: function () {
+ this._super.apply(this, arguments);
+ this.bind_events();
+ return $.when();
+ },
+
+ /* instantiate the compose message object and insert this on the DOM.
+ * The compose message is display in compact form.
+ */
+ instantiate_compose_message: function () {
+ // add message composition form view
+ if (!this.compose_message) {
+ this.compose_message = new ThreadComposeMessage(this, this, {
+ 'context': this.options.compose_as_todo && !this.thread_level ? _.extend(this.context, { 'default_starred': true }) : this.context,
+ 'options': this.options,
+ });
+ if (!this.thread_level || this.thread_level > this.options.display_indented_thread) {
+ this.compose_message.insertBefore(this.$el);
+ } else {
+ this.compose_message.prependTo(this.$el);
+ }
+ }
+ },
+
+ /* When the expandable object is visible on screen (with scrolling)
+ * then the on_expandable function is launch
+ */
+ on_scroll: function () {
+ _.each( _.filter(this.messages, function (val) {return val.max_limit && !val.parent_id;}), function (val) {
+ var pos = val.$el.position();
+ if (pos.top) {
+ /* bottom of the screen */
+ var bottom = $(window).scrollTop()+$(window).height()+200;
+ if (bottom > pos.top) {
+ val.on_expandable();
+ }
+ }
+ });
+ },
+
+ /**
+ * Bind events in the widget. Each event is slightly described
+ * in the function. */
+ bind_events: function () {
+ var self = this;
+ self.$('.oe_mail_list_recipients .oe_more').on('click', self.on_show_recipients);
+ self.$('.oe_mail_compose_textarea .oe_more_hidden').on('click', self.on_hide_recipients);
+ },
+
+ /**
+ *show all the partner list of this parent message
+ */
+ on_show_recipients: function () {
+ var p=$(this).parent();
+ p.find('.oe_more_hidden, .oe_hidden').show();
+ p.find('.oe_more').hide();
+ return false;
+ },
+
+ /**
+ *hide a part of the partner list of this parent message
+ */
+ on_hide_recipients: function () {
+ var p=$(this).parent();
+ p.find('.oe_more_hidden, .oe_hidden').hide();
+ p.find('.oe_more').show();
+ return false;
+ },
+
+ /* get all child message/thread id linked.
+ * @return array of id
+ */
+ get_child_ids: function () {
+ return _.map(this.get_childs(), function (val) { return val.id; });
+ },
+
+ /* get all child message/thread linked.
+ * @param {int} nb_thread_level, number of traversed thread level for this search
+ * @return array of thread object
+ */
+ get_childs: function (nb_thread_level) {
+ var res=[];
+ if (arguments[1]) res.push(this);
+ if (isNaN(nb_thread_level) || nb_thread_level>0) {
+ _(this.messages).each(function (val) {
+ if (val.thread) {
+ res = res.concat( val.thread.get_childs((isNaN(nb_thread_level) ? undefined : nb_thread_level-1), true) );
+ }
+ });
+ }
+ return res;
+ },
+
+ /**
+ *search a thread in all thread and child thread.
+ * This method return an object thread.
+ * @param {object}{int} option.id
+ * @param {object}{string} option.model
+ * @param {object}{boolean} option._go_thread_wall
+ * private for check the top thread
+ * @param {object}{boolean} option.default_return_top_thread
+ * return the top thread (wall) if no thread found
+ * @return thread object
+ */
+ browse_thread: function (options) {
+ // goto the wall thread for launch browse
+ if (!options._go_thread_wall) {
+ options._go_thread_wall = true;
+ return this.options.root_thread.browse_thread(options);
+ }
+
+ if (this.id == options.id) {
+ return this;
+ }
+
+ if (options.id) {
+ for (var i in this.messages) {
+ if (this.messages[i].thread) {
+ var res = this.messages[i].thread.browse_thread({'id':options.id, '_go_thread_wall':true});
+ if (res) return res;
+ }
+ }
+ }
+
+ //if option default_return_top_thread, return the top if no found thread
+ if (options.default_return_top_thread) {
+ return this;
+ }
+
+ return false;
+ },
+
+ /**
+ *search a message in all thread and child thread.
+ * This method return an object message.
+ * @param {object}{int} option.id
+ * @param {object}{string} option.model
+ * @param {object}{boolean} option._go_thread_wall
+ * private for check the top thread
+ * @return message object
+ */
+ browse_message: function (options) {
+ if (this.options.root_thread.messages[0])
+ return this.options.root_thread.messages[0].browse_message(options);
+ },
+
+ /**
+ *If compose_message doesn't exist, instantiate the compose message.
+ * Call the on_toggle_quick_composer method to allow the user to write his message.
+ * (Is call when a user click on "Reply" button)
+ */
+ on_compose_message: function (event) {
+ this.instantiate_compose_message();
+ this.compose_message.on_toggle_quick_composer(event);
+ return false;
+ },
+
+ /**
+ *display the message "there are no message" on the thread
+ */
+ no_message: function () {
+ var no_message = $(qweb.render('mail.wall_no_message', {}));
+ if (this.options.help) {
+ no_message.html(this.options.help);
+ }
+ if (!this.$el.find(".oe_view_nocontent").length)
+ {
+ no_message.appendTo(this.$el);
+ }
+ },
+
+ /**
+ *make a request to read the message (calling RPC to "message_read").
+ * The result of this method is send to the switch message for sending ach message to
+ * his parented object thread.
+ * @param {Array} replace_domain: added to this.domain
+ * @param {Object} replace_context: added to this.context
+ * @param {Array} ids read (if the are some ids, the method don't use the domain)
+ */
+ message_fetch: function (replace_domain, replace_context, ids, callback) {
+ return this.ds_message.call('message_read', [
+ // ids force to read
+ ids === false ? undefined : ids && ids.slice(0, this.options.fetch_limit),
+ // domain + additional
+ (replace_domain ? replace_domain : this.domain),
+ // ids allready loaded
+ (this.id ? [this.id].concat( this.get_child_ids() ) : this.get_child_ids()),
+ // option for sending in flat mode by server
+ this.options.flat_mode,
+ // context + additional
+ (replace_context ? replace_context : this.context),
+ // parent_id
+ this.context.default_parent_id || undefined,
+ this.options.fetch_limit,
+ ]).done(callback ? _.bind(callback, this, arguments) : this.proxy('switch_new_message')
+ ).done(this.proxy('message_fetch_set_read'));
+ },
+
+ message_fetch_set_read: function (message_list) {
+ if (! this.context.mail_read_set_read) return;
+ var self = this;
+ this.render_mutex.exec(function() {
+ var msg_ids = _.pluck(message_list, 'id');
+ return self.ds_message.call('set_message_read', [msg_ids, true, false, self.context])
+ .then(function (nb_read) {
+ if (nb_read) {
+ self.options.root_thread.MailWidget.do_reload_menu_emails();
+ }
+ });
+ });
+ },
+
+ /**
+ *create the message object and attached on this thread.
+ * When the message object is create, this method call insert_message for,
+ * displaying this message on the DOM.
+ * @param : {object} data from calling RPC to "message_read"
+ */
+ create_message_object: function (data) {
+ var self = this;
+ var message;
+
+ data.thread_level = self.thread_level || 0;
+ data.options = _.extend(data.options || {}, self.options);
+
+ if (data.type=='expandable') {
+ message = new ThreadExpandable(self, data, {'context':{
+ 'default_model': data.model || self.context.default_model,
+ 'default_res_id': data.res_id || self.context.default_res_id,
+ 'default_parent_id': self.id,
+ }});
+ } else {
+ data.record_name= (data.record_name !== '' && data.record_name) || (self.parent_message && self.parent_message.record_name);
+ message = new ThreadMessage(self, data, {'context':{
+ 'default_model': data.model,
+ 'default_res_id': data.res_id,
+ 'default_parent_id': data.id,
+ }});
+ }
+
+ // check if the message is already create
+ for (var i in self.messages) {
+ if (message.id && self.messages[i] && self.messages[i].id === message.id) {
+ self.messages[i].destroy();
+ }
+ }
+ self.messages.push( message );
+
+ return message;
+ },
+
+ /**
+ *insert the message on the DOM.
+ * All message (and expandable message) are sorted. The method get the
+ * older and newer message to insert the message (before, after).
+ * If there are no older or newer, the message is prepend or append to
+ * the thread (parent object or on root thread for flat view).
+ * The sort is define by the thread_level (O for newer on top).
+ * @param : {object} ThreadMessage object
+ */
+ insert_message: function (message, dom_insert_after, prepend) {
+ var self=this;
+ if (this.options.show_compact_message > this.thread_level) {
+ this.instantiate_compose_message();
+ this.compose_message.do_show_compact();
+ }
+
+ this.$('.oe_view_nocontent').remove();
+ if (dom_insert_after && dom_insert_after.parent()[0] == self.$el[0]) {
+ message.insertAfter(dom_insert_after);
+ } else if (prepend) {
+ message.prependTo(self.$el);
+ } else {
+ message.appendTo(self.$el);
+ }
+ message.$el.hide().fadeIn(500);
+
+ return message;
+ },
+
+ /**
+ *get the parent thread of the messages.
+ * Each message is send to his parent object (or parent thread flat mode) for creating the object message.
+ * @param : {Array} datas from calling RPC to "message_read"
+ */
+ switch_new_message: function (records, dom_insert_after) {
+ var self = this;
+ dom_insert_after = typeof dom_insert_after == 'object' ? dom_insert_after : false;
+ _(records).each(function (record) {
+ var thread = self.browse_thread({
+ 'id': record.parent_id,
+ 'default_return_top_thread':true
+ });
+ // create object and attach to the thread object
+ var message = thread.create_message_object( record );
+ // insert the message on dom
+ thread.insert_message( message, dom_insert_after);
+ });
+ if (!records.length && this.options.root_thread == this) {
+ this.no_message();
+ }
+ },
+
+ /**
+ * this method is call when the widget of a message or an expandable message is destroy
+ * in this thread. The this.messages array is filter to remove this message
+ */
+ on_message_detroy: function () {
+
+ this.messages = _.filter(this.messages, function (val) { return !val.isDestroyed(); });
+ if (this.options.root_thread == this && !this.messages.length) {
+ this.no_message();
+ }
+ return false;
+
+ },
+
+ /**
+ * Convert a destroyed message into a expandable message
+ */
+ message_to_expandable: function (message) {
+
+ if (!this.thread_level || message.isDestroyed()) {
+ message.destroy();
+ return false;
+ }
+
+ var messages = _.sortBy( this.messages, function (val) { return val.id; });
+
+ var msg_up = message.$el.prev('.oe_msg');
+ var msg_down = message.$el.next('.oe_msg');
+ msg_up = msg_up.hasClass('oe_msg_expandable') ? _.find( this.messages, function (val) { return val.$el[0] == msg_up[0]; }) : false;
+ msg_down = msg_down.hasClass('oe_msg_expandable') ? _.find( this.messages, function (val) { return val.$el[0] == msg_down[0]; }) : false;
+
+ var message_dom = [ ["id", "=", message.id] ];
+
+ if ( msg_up && msg_up.type == "expandable" && msg_down && msg_down.type == "expandable") {
+ // concat two expandable message and add this message to this dom
+ msg_up.domain = mail_utils.expand_domain( msg_up.domain );
+ msg_down.domain = mail_utils.expand_domain( msg_down.domain );
+
+ msg_down.domain = ['|','|'].concat( msg_up.domain ).concat( message_dom ).concat( msg_down.domain );
+
+ if ( !msg_down.max_limit ) {
+ msg_down.nb_messages += 1 + msg_up.nb_messages;
+ }
+
+ msg_up.$el.remove();
+ msg_up.destroy();
+
+ msg_down.reinit();
+
+ } else if ( msg_up && msg_up.type == "expandable") {
+ // concat preview expandable message and this message to this dom
+ msg_up.domain = mail_utils.expand_domain( msg_up.domain );
+ msg_up.domain = ['|'].concat( msg_up.domain ).concat( message_dom );
+
+ msg_up.nb_messages++;
+
+ msg_up.reinit();
+
+ } else if ( msg_down && msg_down.type == "expandable") {
+ // concat next expandable message and this message to this dom
+ msg_down.domain = mail_utils.expand_domain( msg_down.domain );
+ msg_down.domain = ['|'].concat( msg_down.domain ).concat( message_dom );
+
+ // it's maybe a message expandable for the max limit read message
+ if ( !msg_down.max_limit ) {
+ msg_down.nb_messages++;
+ }
+
+ msg_down.reinit();
+
+ } else {
+ // create a expandable message
+ var expandable = new ThreadExpandable(this, {
+ 'model': message.model,
+ 'parent_id': message.parent_id,
+ 'nb_messages': 1,
+ 'thread_level': message.thread_level,
+ 'domain': message_dom,
+ 'options': message.options,
+ }, {
+ 'context':{
+ 'default_model': message.model || this.context.default_model,
+ 'default_res_id': message.res_id || this.context.default_res_id,
+ 'default_parent_id': this.id,
+ }
+ });
+
+ // add object on array and DOM
+ this.messages.push(expandable);
+ expandable.insertAfter(message.$el);
+ }
+
+ // destroy message
+ message.destroy();
+
+ return true;
+ },
+});
+/**
+ * ------------------------------------------------------------
+ * mail : root Widget
+ * ------------------------------------------------------------
+ *
+ * This widget handles the display of messages with thread options. Its main
+ * use is to receive a context and a domain, and to delegate the message
+ * fetching and displaying to the Thread widget.
+ */
+var MailWidget = Widget.extend({
+ template: 'mail.Root',
+
+ /**
+ * @param {Object} parent parent
+ * @param {Array} [domain]
+ * @param {Object} [context] context of the thread. It should
+ * contain at least default_model, default_res_id. Please refer to
+ * the compose_message widget for more information about it.
+ * @param {Object} [options]
+ *... @param {Number} [truncate_limit=250] number of character to
+ * display before having a "show more" link; note that the text
+ * will not be truncated if it does not have 110% of the parameter
+ *... @param {Boolean} [show_record_name] display the name and link for do action
+ *... @param {boolean} [show_reply_button] display the reply button
+ *... @param {boolean} [show_read_unread_button] display the read/unread button
+ *... @param {int} [display_indented_thread] number thread level to indented threads.
+ * other are on flat mode
+ *... @param {Boolean} [show_compose_message] allow to display the composer
+ *... @param {Boolean} [show_compact_message] display the compact message on the thread
+ * when the user clic on this compact mode, the composer is open
+ *... @param {Array} [message_ids] List of ids to fetch by the root thread.
+ * When you use this option, the domain is not used for the fetch root.
+ * @param {String} [no_message] Message to display when there are no message
+ * @param {Boolean} [show_link] Display partner (authors, followers...) on link or not
+ * @param {Boolean} [compose_as_todo] The root composer mark automatically the message as todo
+ * @param {Boolean} [readonly] Read only mode, hide all action buttons and composer
+ */
+ init: function (parent, action) {
+ this._super(parent, action);
+ this.action = _.clone(action);
+ this.domain = this.action.domain || this.action.params.domain || [];
+ this.context = this.action.context || this.action.params.context || {};
+
+ this.action.params = _.extend({
+ 'display_indented_thread' : -1,
+ 'show_reply_button' : false,
+ 'show_read_unread_button' : false,
+ 'truncate_limit' : 250,
+ 'show_record_name' : false,
+ 'show_compose_message' : false,
+ 'show_compact_message' : false,
+ 'compose_placeholder': false,
+ 'show_link': true,
+ 'view_inbox': false,
+ 'message_ids': undefined,
+ 'compose_as_todo' : false,
+ 'readonly' : false,
+ 'emails_from_on_composer': true,
+ 'fetch_limit': 30 // limit of chatter messages
+ }, this.action.params);
+
+ this.action.params.help = this.action.help || false;
+ },
+
+ start: function () {
+ this._super.apply(this, arguments);
+ this.message_render();
+ },
+
+ /**
+ * create an object "related_menu"
+ * contains the menu widget and the sub menu related of this wall
+ */
+ do_reload_menu_emails: function () {
+ var menu = web_client.menu;
+ if (!menu || !menu.current_menu) {
+ return $.when();
+ }
+ return menu.rpc("/web/menu/load_needaction", {'menu_ids': [menu.current_menu]}).done(function(r) {
+ menu.on_needaction_loaded(r);
+ }).then(function () {
+ menu.trigger("need_action_reloaded");
+ });
+ },
+
+ /**
+ *Create the root thread and display this object in the DOM.
+ * Call the no_message method then c all the message_fetch method
+ * of this root thread to display the messages.
+ */
+ message_render: function () {
+
+ this.thread = new Thread(this, {}, {
+ 'domain' : this.domain,
+ 'context' : this.context,
+ 'options': this.action.params,
+ });
+
+ this.thread.appendTo( this.$el );
+
+ if (this.action.params.show_compose_message) {
+ this.thread.instantiate_compose_message();
+ this.thread.compose_message.do_show_compact();
+ }
+
+ this.thread.message_fetch(null, null, this.action.params.message_ids);
+
+ },
+
+});
+core.action_registry.add('mail.Widget', MailWidget);
+
+
+/**
+ * ------------------------------------------------------------
+ * mail_thread Widget
+ * ------------------------------------------------------------
+ *
+ * This widget handles the display of messages on a document. Its main
+ * use is to receive a context and a domain, and to delegate the message
+ * fetching and displaying to the Thread widget.
+ * Use Help on the field to display a custom "no message loaded"
+ */
+var RecordThread = form_common.AbstractField.extend({
+ template: 'mail.record_thread',
+
+ init: function (parent, node) {
+ this._super.apply(this, arguments);
+ this.ParentViewManager = parent;
+ this.node = _.clone(node);
+ this.node.params = _.extend({
+ 'display_indented_thread': -1,
+ 'show_reply_button': false,
+ 'show_read_unread_button': true,
+ 'read_action': 'unread',
+ 'show_record_name': false,
+ 'show_compact_message': 1,
+ 'display_log_button' : true,
+ }, this.node.params);
+ if (this.node.attrs.placeholder) {
+ this.node.params.compose_placeholder = this.node.attrs.placeholder;
+ }
+ if (this.node.attrs.readonly) {
+ this.node.params.readonly = this.node.attrs.readonly;
+ }
+ if ('display_log_button' in this.options) {
+ this.node.params.display_log_button = this.options.display_log_button;
+ }
+ this.domain = (this.node.params && this.node.params.domain) || (this.field && this.field.domain) || [];
+
+ if (!this.ParentViewManager.is_action_enabled('edit')) {
+ this.node.params.show_link = false;
+ }
+ },
+
+ start: function () {
+ this._super.apply(this, arguments);
+ // NB: check the actual_mode property on view to know if the view is in create mode anymore
+ this.view.on("change:actual_mode", this, this._check_visibility);
+ this._check_visibility();
+ },
+
+ _check_visibility: function () {
+ this.$el.toggle(this.view.get("actual_mode") !== "create");
+ },
+
+ render_value: function () {
+ if (! this.view.datarecord.id || data.BufferedDataSet.virtual_id_regex.test(this.view.datarecord.id)) {
+ this.$('oe_mail_thread').hide();
+ return;
+ }
+
+ this.node.params = _.extend(this.node.params, {
+ 'message_ids': this.get_value(),
+ 'show_compose_message': this.view.is_action_enabled('edit'),
+ });
+ this.node.context = {
+ 'mail_read_set_read': true, // set messages as read in Chatter
+ 'default_res_id': this.view.datarecord.id || false,
+ 'default_model': this.view.model || false,
+ };
+
+ if (this.root) {
+ $('').insertAfter(this.root.$el);
+ this.root.destroy();
+ }
+ // create and render Thread widget
+ this.root = new MailWidget(this, _.extend(this.node, {
+ 'domain' : (this.domain || []).concat([['model', '=', this.view.model], ['res_id', '=', this.view.datarecord.id]]),
+ }));
+
+ return this.root.replace(this.$('.oe_mail-placeholder'));
+ },
+});
+core.form_widget_registry.add('mail_thread', RecordThread);
+
+/**
+ * ------------------------------------------------------------
+ * Aside Widget
+ * ------------------------------------------------------------
+ *
+ * This widget handles the display of a sidebar on the Wall. Its main use
+ * is to display group and employees suggestion (if hr is installed).
+ */
+var WallSidebar = Widget.extend({
+ template: 'mail.wall.sidebar',
+});
+
+
+
+/**
+ * ------------------------------------------------------------
+ * Wall Widget
+ * ------------------------------------------------------------
+ *
+ * This widget handles the display of messages on a Wall. Its main
+ * use is to receive a context and a domain, and to delegate the message
+ * fetching and displaying to the Thread widget.
+ */
+
+
+var MailWall = Widget.extend({
+ template: 'mail.wall',
+
+ /**
+ * @param {Object} parent parent
+ * @param {Object} [options]
+ * @param {Array} [options.domain] domain on the Wall
+ * @param {Object} [options.context] context, is an object. It should
+ * contain default_model, default_res_id, to give it to the threads.
+ */
+ init: function (parent, action) {
+ var key;
+ this._super(parent, action);
+ this.ActionManager = parent;
+
+ this.action = _.clone(action);
+ this.domain = this.action.params.domain || this.action.domain || [];
+ this.context = _.extend(this.action.params.context || {}, this.action.context || {});
+
+ // filter some parameters that we will propagate as search_default
+ this.defaults = {};
+ for (key in this.action.context.params) {
+ if (_.indexOf(['model', 'res_id'], key) == -1) {
+ continue;
+ }
+ this.context['search_default_' + key] = this.action.context.params[key];
+ }
+ for (key in this.context) {
+ if (key.match(/^search_default_/)) {
+ this.defaults[key.replace(/^search_default_/, '')] = this.context[key];
+ }
+ }
+ this.action.params = _.extend({
+ 'display_indented_thread': 1,
+ 'show_reply_button': true,
+ 'show_read_unread_button': true,
+ 'show_compose_message': true,
+ 'show_record_name': true,
+ 'show_compact_message': this.action.params.view_mailbox ? false : 1,
+ 'view_inbox': false,
+ 'emails_from_on_composer': false,
+ 'fetch_limit': 30 // allow inbox to load all children messages
+ }, this.action.params);
+ },
+
+ start: function () {
+ this._super.apply(this);
+ this.bind_events();
+ this.load_searchview(this.defaults);
+ if (! this.searchview.has_defaults) {
+ this.message_render();
+ }
+ // render sidebar
+ var wall_sidebar = new WallSidebar(this);
+ wall_sidebar.appendTo(this.$el.find('.oe_mail_wall_aside'));
+ },
+
+ /**
+ * Load the mail.message search view
+ * @param {Object} defaults ??
+ */
+ load_searchview: function (defaults) {
+ var self = this,
+ ds_msg = new data.DataSetSearch(this, 'mail.message'),
+ options = { $buttons: this.$('.oe-search-options') };
+ this.searchview = new SearchView(this, ds_msg, false, defaults || {}, options);
+ this.searchview.on('search_data', this, this.do_searchview_search);
+ this.searchview.appendTo(this.$('.oe-view-manager-search-view')).then(function () {
+ self.searchview.toggle_visibility(true);
+ });
+ if (this.searchview.has_defaults) {
+ this.searchview.ready.then(this.searchview.do_search);
+ }
+ return this.searchview;
+ },
+
+ /**
+ * Get the domains, contexts and groupbys in parameter from search
+ * view, then render the filtered threads.
+ * @param {Array} domains
+ * @param {Array} contexts
+ * @param {Array} groupbys
+ */
+ do_searchview_search: function (domains, contexts, groupbys) {
+ var self = this;
+ pyeval.eval_domains_and_contexts({
+ domains: domains || [],
+ contexts: contexts || [],
+ group_by_seq: groupbys || []
+ }).then(function (results) {
+ if(self.root) {
+ $('').insertAfter(self.root.$el);
+ self.root.destroy();
+ }
+ return self.message_render(results);
+ });
+ },
+
+ /**
+ * Create the root thread widget and display this object in the DOM
+ */
+ message_render: function (search) {
+ var domain = this.domain.concat(search && search.domain ? search.domain : []);
+ var context = _.extend(this.context, search && search.context ? search.context : {});
+
+ this.root = new MailWidget(this, _.extend(this.action, {
+ 'domain' : domain,
+ 'context' : context,
+ }));
+ return this.root.replace(this.$('.oe_mail-placeholder'));
+ },
+
+ bind_events: function () {
+ var self=this;
+ this.$(".oe_write_full").click(function (event) {
+ event.stopPropagation();
+ var action = {
+ name: _t('Compose Email'),
+ type: 'ir.actions.act_window',
+ res_model: 'mail.compose.message',
+ view_mode: 'form',
+ view_type: 'form',
+ action_from: 'mail.ThreadComposeMessage',
+ views: [[false, 'form']],
+ target: 'new',
+ context: {
+ },
+ };
+ web_client.action_manager.do_action(action);
+ });
+ this.$(".oe_write_onwall").click(function (event) { self.root.thread.on_compose_message(event); });
+ }
+});
+core.action_registry.add('mail.wall', MailWall);
+
+/**
+ * ------------------------------------------------------------
+ * Sub-widgets loading
+ * ------------------------------------------------------------
+ *
+ * Load here widgets that could depend on widgets defined in mail.js
+ */
+
+// openerp.mail.suggestions(session, mail); // import suggestion.js (suggestion widget)
+
+/**
+ * ------------------------------------------------------------
+ * UserMenu
+ * ------------------------------------------------------------
+ *
+ * Add a link on the top user bar for write a full mail
+ */
+var ComposeMessageTopButton = Widget.extend({
+ template:'mail.compose_message_top_button',
+ events: {
+ "click": "on_compose_message",
+ },
+ on_compose_message: function (ev) {
+ ev.preventDefault();
+ var action = {
+ type: 'ir.actions.act_window',
+ res_model: 'mail.compose.message',
+ view_mode: 'form',
+ view_type: 'form',
+ views: [[false, 'form']],
+ target: 'new',
+ context: {},
+ };
+ web_client.action_manager.do_action(action);
+ },
+});
+
+// Put the ComposeMessageTopButton widget in the systray menu
+SystrayMenu.Items.push(ComposeMessageTopButton);
+
+return {
+ Thread: Thread,
+ RecordThread: RecordThread,
+ WallSidebar: WallSidebar,
+ Wall: MailWall,
};
+
+});
diff --git a/addons/mail/static/src/js/mail_followers.js b/addons/mail/static/src/js/mail_followers.js
index 78b80269fc3..3d6d6d1ed16 100644
--- a/addons/mail/static/src/js/mail_followers.js
+++ b/addons/mail/static/src/js/mail_followers.js
@@ -1,380 +1,388 @@
-openerp_mail_followers = function(session, mail) {
- var _t = session.web._t,
- _lt = session.web._lt;
+odoo.define('mail.mail_followers', ['mail.utils', 'web.core', 'web.data', 'web.Dialog', 'web.form_common', 'web.session', 'web.web_client'], function (require) {
+"use strict";
- var mail_followers = session.mail_followers = {};
+var mail_utils = require('mail.utils');
+var core = require('web.core');
+var data = require('web.data');
+var Dialog = require('web.Dialog');
+var form_common = require('web.form_common');
+var session = require('web.session');
+var web_client = require('web.web_client');
- /**
- * ------------------------------------------------------------
- * mail_followers Widget
- * ------------------------------------------------------------
- *
- * This widget handles the display of a list of records as a vertical
- * list, with an image on the left. The widget itself is a floatting
- * right-sided box.
- * This widget is mainly used to display the followers of records
- * in OpenChatter.
- */
+var _t = core._t;
+var qweb = core.qweb;
- /* Add the widget to registry */
- session.web.form.widgets.add('mail_followers', 'openerp.mail_followers.Followers');
+/**
+ * ------------------------------------------------------------
+ * mail_followers Widget
+ * ------------------------------------------------------------
+ *
+ * This widget handles the display of a list of records as a vertical
+ * list, with an image on the left. The widget itself is a floatting
+ * right-sided box.
+ * This widget is mainly used to display the followers of records
+ * in OpenChatter.
+ */
- mail_followers.Followers = session.web.form.AbstractField.extend({
- template: 'mail.followers',
- init: function() {
- this._super.apply(this, arguments);
- this.image = this.node.attrs.image || 'image_small';
- this.comment = this.node.attrs.help || false;
- this.displayed_limit = this.node.attrs.displayed_nb || 10;
- this.displayed_nb = this.displayed_limit;
- this.ds_model = new session.web.DataSetSearch(this, this.view.model);
- this.ds_users = new session.web.DataSetSearch(this, 'res.users');
+var Followers = form_common.AbstractField.extend({
+ template: 'mail.followers',
- this.value = [];
- this.followers = [];
-
- this.view_is_editable = this.__parentedParent.is_action_enabled('edit');
- },
+ init: function() {
+ this._super.apply(this, arguments);
+ this.image = this.node.attrs.image || 'image_small';
+ this.comment = this.node.attrs.help || false;
+ this.displayed_limit = this.node.attrs.displayed_nb || 10;
+ this.displayed_nb = this.displayed_limit;
+ this.ds_model = new data.DataSetSearch(this, this.view.model);
+ this.ds_users = new data.DataSetSearch(this, 'res.users');
- start: function() {
- // use actual_mode property on view to know if the view is in create mode anymore
- this.view.on("change:actual_mode", this, this.on_check_visibility_mode);
- this.on_check_visibility_mode();
- this.reinit();
- this.bind_events();
- this._super();
- },
-
- on_check_visibility_mode: function () {
- this.set({"force_invisible": this.view.get("actual_mode") == "create"});
- },
-
- set_value: function(_value) {
- this.value = _value;
- this._super(_value);
- },
-
- reinit: function() {
- this.message_is_follower == undefined;
- this.display_buttons();
- },
-
- bind_events: function() {
- var self = this;
- // event: click on '(Un)Follow' button, that toggles the follow for uid
- this.$('.oe_follower').on('click', function (event) {
- if($(this).hasClass('oe_notfollow'))
- self.do_follow();
- else
- self.do_unfollow();
- });
- // event: click on a subtype, that (un)subscribe for this subtype
- this.$el.on('click', '.oe_subtype_list input', function(event) {
- self.do_update_subscription(event);
- var $list = self.$('.oe_subtype_list');
- if(!$list.hasClass('open')) {
- $list.addClass('open');
- }
- if(self.$('.oe_subtype_list ul')[0].children.length < 1) {
- $list.removeClass('open');
- }
- event.stopPropagation();
- });
- // event: click on 'invite' button, that opens the invite wizard
- this.$('.oe_invite').on('click', self.on_invite_follower);
- // event: click on 'edit_subtype(pencil)' button to edit subscription
- this.$el.on('click', '.oe_edit_subtype', self.on_edit_subtype);
- this.$el.on('click', '.oe_remove_follower', self.on_remove_follower);
- this.$el.on('click', '.oe_show_more', self.on_show_more_followers);
- this.$el.on('click', 'a[data-partner]', self.on_follower_clicked);
- },
-
- on_edit_subtype: function(event) {
- var self = this;
- var $currentTarget = $(event.currentTarget);
- var user_pid = $currentTarget.data('id');
- $('div.oe_edit_actions').remove();
- self.$dialog = new session.web.Dialog(this, {
- size: 'small',
- title: _t('Edit Subscription of ') + $currentTarget.siblings('a').text(),
- buttons: [
- { text: _t("Apply"), click: function() {
- self.do_update_subscription(event, user_pid);
- this.parents('.modal').modal('hide');
- }},
- { text: _t("Cancel"), click: function() { this.parents('.modal').modal('hide'); }}
- ],
- }, "
');
+ regex_res = regex_login.exec(string);
+ }
+ return string;
+}
+
+/**
+ * Replaces textarea text into html text (add
');
+}
+
+/* Returns the complete domain with "&"
+ * TDE note: please add some comments to explain how/why
+ */
+function expand_domain(domain) {
+ var nb_and = -1;
+ var k;
+ // TDE note: smarted code maybe ?
+ for (k = domain.length-1; k >= 0 ; k-- ) {
+ if (typeof domain[k] != 'object' ) {
+ nb_and -= 2;
+ continue;
+ }
+ nb_and += 1;
+ }
+
+ for (k = 0; k < nb_and ; k++) {
+ domain.unshift('&');
+ }
+
+ return domain;
+}
+
+// inserts zero width space between each letter of a string so that
+// the word will correctly wrap in html boxes smaller than the text
+function breakword(str){
+ var out = '';
+ if (!str) {
+ return str;
+ }
+ for(var i = 0, len = str.length; i < len; i++){
+ out += _.str.escapeHTML(str[i]) + '';
+ }
+ return out;
+}
+
+return {
+ parse_email: parse_email,
+ get_image: get_image,
+ get_attachment_url: get_attachment_url,
+ do_replace_expressions: do_replace_expressions,
+ get_text2html: get_text2html,
+ expand_domain: expand_domain,
+ breakword: breakword,
+};
+
+});
diff --git a/addons/mail/static/src/js/many2many_tags_email.js b/addons/mail/static/src/js/many2many_tags_email.js
index 0b2b33f4872..9a96e2ef78e 100644
--- a/addons/mail/static/src/js/many2many_tags_email.js
+++ b/addons/mail/static/src/js/many2many_tags_email.js
@@ -1,12 +1,19 @@
-openerp_FieldMany2ManyTagsEmail = function(instance) {
-var _t = instance.web._t;
+odoo.define('mail.many2manytags', ['web.core', 'web.form_common', 'web.form_relational', 'web.Model'], function (require) {
+"use strict";
+
+var core = require('web.core');
+var form_common = require('web.form_common');
+var form_relational = require('web.form_relational');
+var Model = require('web.Model');
+
+var _t = core._t;
/**
* Extend of FieldMany2ManyTags widget method.
* When the user add a partner and the partner don't have an email, open a popup to purpose to add an email.
* The user can choose to add an email or cancel and close the popup.
*/
-instance.web.form.FieldMany2ManyTagsEmail = instance.web.form.FieldMany2ManyTags.extend({
+var FieldMany2ManyTagsEmail = form_relational.FieldMany2ManyTags.extend({
start: function() {
this.values = [];
@@ -41,7 +48,7 @@ instance.web.form.FieldMany2ManyTagsEmail = instance.web.form.FieldMany2ManyTags
_check_email_popup: function (ids) {
var self = this;
- new instance.web.Model('res.partner').call("search", [[
+ new Model('res.partner').call("search", [[
["id", "in", ids],
["email", "=", false],
["notify_email", "=", 'always'] ]],
@@ -54,7 +61,7 @@ instance.web.form.FieldMany2ManyTagsEmail = instance.web.form.FieldMany2ManyTags
// unvalid partner
_.each(record_ids, function (id) {
- var pop = new instance.web.form.FormOpenPopup(self);
+ var pop = new form_common.FormOpenPopup(self);
pop.show_element(
'res.partner',
id,
@@ -81,6 +88,6 @@ instance.web.form.FieldMany2ManyTagsEmail = instance.web.form.FieldMany2ManyTags
/**
* Registry of form fields
*/
-instance.web.form.widgets.add('many2many_tags_email', 'instance.web.form.FieldMany2ManyTagsEmail');
+core.form_widget_registry.add('many2many_tags_email', FieldMany2ManyTagsEmail);
-};
+});
diff --git a/addons/mail/static/src/js/suggestions.js b/addons/mail/static/src/js/suggestions.js
index 47a5bcf625c..b0ba8027fdb 100644
--- a/addons/mail/static/src/js/suggestions.js
+++ b/addons/mail/static/src/js/suggestions.js
@@ -1,78 +1,83 @@
-openerp.mail.suggestions = function(session, mail) {
- var _t = session.web._t;
- var QWeb = session.web.qweb;
+odoo.define(['mail.mail', 'web.core', 'web.data', 'web.session', 'web.Widget'], function (require) {
- var suggestions = session.suggestions = {};
- var removed_suggested_group = session.suggestions.removed_suggested_group = [];
+var mail = require('mail.mail');
+var core = require('web.core');
+var data = require('web.data');
+var real_session = require('web.session');
+var Widget = require('web.Widget');
- suggestions.Groups = session.web.Widget.extend({
- events: {
- 'click .oe_suggestion_remove.oe_suggestion_group': 'stop_group_suggestion',
- 'click .oe_suggestion_remove_item.oe_suggestion_group': 'remove_group_suggestion',
- 'click .oe_suggestion_join': 'join_group',
- },
+var QWeb = core.qweb;
- init: function () {
- this._super.apply(this, arguments);
- this.mail_group = new session.web.DataSetSearch(this, 'mail.group');
- this.res_users = new session.web.DataSetSearch(this, 'res.users');
- this.groups = [];
- },
+var removed_suggested_group = [];
- start: function () {
- this._super.apply(this, arguments);
- return this.fetch_suggested_groups();
- },
+var Groups = Widget.extend({
+ events: {
+ 'click .oe_suggestion_remove.oe_suggestion_group': 'stop_group_suggestion',
+ 'click .oe_suggestion_remove_item.oe_suggestion_group': 'remove_group_suggestion',
+ 'click .oe_suggestion_join': 'join_group',
+ },
- fetch_suggested_groups: function () {
- var self = this;
- var group = self.mail_group.call('get_suggested_thread', {'removed_suggested_threads': removed_suggested_group}).then(function (res) {
- _(res).each(function (result) {
- result['image']=self.session.url('/web/binary/image', {model: 'mail.group', field: 'image_small', id: result.id});
- });
- self.groups = res;
+ init: function () {
+ this._super.apply(this, arguments);
+ this.mail_group = new data.DataSetSearch(this, 'mail.group');
+ this.res_users = new data.DataSetSearch(this, 'res.users');
+ this.groups = [];
+ },
+
+ start: function () {
+ this._super.apply(this, arguments);
+ return this.fetch_suggested_groups();
+ },
+
+ fetch_suggested_groups: function () {
+ var self = this;
+ var group = self.mail_group.call('get_suggested_thread', {'removed_suggested_threads': removed_suggested_group}).then(function (res) {
+ _(res).each(function (result) {
+ result.image = real_session.url('/web/binary/image', {model: 'mail.group', field: 'image_small', id: result.id});
});
- return $.when(group).then(this.proxy('display_suggested_groups'));
- },
+ self.groups = res;
+ });
+ return $.when(group).then(this.proxy('display_suggested_groups'));
+ },
- display_suggested_groups: function () {
- var suggested_groups = this.$('.oe_sidebar_suggestion.oe_suggestion_group');
- if (suggested_groups) {
- suggested_groups.empty();
- }
- if (this.groups.length === 0) {
- return this.$el.empty();
- }
- return this.$el.empty().html(QWeb.render('mail.suggestions.groups', {'widget': this}));
- },
-
- join_group: function (event) {
- var self = this;
- var group_id = parseInt($(event.currentTarget).attr('id'), 10);
- return this.mail_group.call('message_subscribe_users', [[group_id], [this.session.uid]]).then(function(res) {
- self.fetch_suggested_groups();
- });
- },
-
- remove_group_suggestion: function (event) {
- removed_suggested_group.push($(event.currentTarget).attr('id'));
- return this.fetch_suggested_groups();
- },
-
- stop_group_suggestion: function (event) {
- var self = this;
- return this.res_users.call('stop_showing_groups_suggestions', [this.session.uid]).then(function(res) {
- self.$(".oe_sidebar_suggestion.oe_suggestion_group").hide();
- });
+ display_suggested_groups: function () {
+ var suggested_groups = this.$('.oe_sidebar_suggestion.oe_suggestion_group');
+ if (suggested_groups) {
+ suggested_groups.empty();
}
- });
+ if (this.groups.length === 0) {
+ return this.$el.empty();
+ }
+ return this.$el.empty().html(QWeb.render('mail.suggestions.groups', {'widget': this}));
+ },
- session.mail.WallSidebar.include({
- start: function () {
- this._super.apply(this, arguments);
- var sug_groups = new suggestions.Groups(this);
- return sug_groups.appendTo(this.$('.oe_suggestions_groups'));
- },
- });
+ join_group: function (event) {
+ var self = this;
+ var group_id = parseInt($(event.currentTarget).attr('id'), 10);
+ return this.mail_group.call('message_subscribe_users', [[group_id], [this.session.uid]]).then(function(res) {
+ self.fetch_suggested_groups();
+ });
+ },
-};
+ remove_group_suggestion: function (event) {
+ removed_suggested_group.push($(event.currentTarget).attr('id'));
+ return this.fetch_suggested_groups();
+ },
+
+ stop_group_suggestion: function (event) {
+ var self = this;
+ return this.res_users.call('stop_showing_groups_suggestions', [this.session.uid]).then(function(res) {
+ self.$(".oe_sidebar_suggestion.oe_suggestion_group").hide();
+ });
+ }
+});
+
+mail.WallSidebar.include({
+ start: function () {
+ this._super.apply(this, arguments);
+ var sug_groups = new Groups(this);
+ return sug_groups.appendTo(this.$('.oe_suggestions_groups'));
+ },
+});
+
+});
diff --git a/addons/mail/views/mail.xml b/addons/mail/views/mail.xml
index c2ba922ab2d..a8ddba4c93a 100644
--- a/addons/mail/views/mail.xml
+++ b/addons/mail/views/mail.xml
@@ -7,6 +7,7 @@
+