diff --git a/addons/mail/static/src/js/chatter.js b/addons/mail/static/src/js/chatter.js index c8496d62ef4ca..8c5c5d8ba836c 100644 --- a/addons/mail/static/src/js/chatter.js +++ b/addons/mail/static/src/js/chatter.js @@ -34,7 +34,15 @@ var Chatter = Widget.extend(chat_mixin, { }, supportedFieldTypes: ['one2many'], - // inherited + /** + * @override + * @param {widget} parent + * @param {Object} record + * @param {Object} mailFields + * @param {boolean} [mailFields.mail_activity=false] + * @param {boolean} [mailFields.mail_followers=false] + * @param {boolean} [mailFields.mail_thread=false] + */ init: function (parent, record, mailFields, options) { this._super.apply(this, arguments); this._setState(record); @@ -61,6 +69,9 @@ var Chatter = Widget.extend(chat_mixin, { this.postRefresh = nodeOptions.post_refresh || 'never'; } }, + /** + * @override + */ start: function () { this.$topbar = this.$('.o_chatter_topbar'); @@ -80,7 +91,15 @@ var Chatter = Widget.extend(chat_mixin, { return this._super.apply(this, arguments); }, - // public + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + /** + * @param {Object} record + * @param {integer} [record.res_id=undefined] + * @param {Object[]} [fieldNames=undefined] + */ update: function (record, fieldNames) { var self = this; @@ -120,7 +139,14 @@ var Chatter = Widget.extend(chat_mixin, { }); }, - // private + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * @private + * @param {boolean} force + */ _closeComposer: function (force) { if (this.composer && (this.composer.is_empty() || force)) { this.$el.removeClass('o_chatter_composer_active'); @@ -129,6 +155,24 @@ var Chatter = Widget.extend(chat_mixin, { this.composer.clear_composer(); } }, + /** + * @private + */ + _disableChatter: function () { + this.$('.btn').prop('disabled', true); // disable buttons + }, + /** + * @private + */ + _enableChatter: function () { + this.$('.btn').prop('disabled', false); // enable buttons + }, + /** + * @private + * @param {Object} options + * @param {Object[]} [options.suggested_partners=[]] + * @param {boolean} [options.is_log=false] + */ _openComposer: function (options) { var self = this; var old_composer = this.composer; @@ -172,6 +216,11 @@ var Chatter = Widget.extend(chat_mixin, { self.$('.o_chatter_button_log_note').toggleClass('o_active', self.composer.options.is_log); }); }, + /** + * @private + * @param {Deferred} def + * @returns {Deferred} + */ _render: function (def) { // the rendering of the chatter is aynchronous: relational data of its fields needs to be // fetched (in some case, it might be synchronous as they hold an internal cache). @@ -196,9 +245,24 @@ var Chatter = Widget.extend(chat_mixin, { if (self.fields.thread) { self.fields.thread.$el.appendTo(self.$el); } - }).always($spinner.remove.bind($spinner)); + }).always(function () { + // disable widgets in creation mode, otherwise enable + self._switchEnableChatter(!self.creationMode); + $spinner.remove; + }); }, + /** + * @private + * @param {Object} record + * @param {integer} [record.res_id=false] + * @param {string} [record.model=false] + * @param {string} record.data.display_name + */ _setState: function (record) { + + this.wasCreationMode = !!this.creationMode; + this.creationMode = !record.res_id; + if (!this.record || this.record.res_id !== record.res_id) { this.context = { default_res_id: record.res_id || false, @@ -211,6 +275,20 @@ var Chatter = Widget.extend(chat_mixin, { this.record = record; this.record_name = record.data.display_name; }, + /** + * @private + * @param {boolean} [enable] use true to enable the buttons or false to disable them + */ + _switchEnableChatter: function (enable) { + if (_.isBoolean(enable)) { + enable ? this._enableChatter() : this._disableChatter(); + } else { + this.$('.btn').prop('disabled', !this.$('.btn').first().prop('disabled')); // toggle based on 1st btn + } + }, + /** + * @private + */ _updateMentionSuggestions: function () { if (!this.fields.followers) { return; @@ -243,7 +321,13 @@ var Chatter = Widget.extend(chat_mixin, { }); }, - // handlers + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * @private + */ _onOpenComposerMessage: function () { var self = this; if (!this.suggested_partners_def) { @@ -275,9 +359,21 @@ var Chatter = Widget.extend(chat_mixin, { self._openComposer({ is_log: false, suggested_partners: suggested_partners }); }); }, + /** + * @private + */ _onOpenComposerNote: function () { this._openComposer({is_log: true}); }, + /** + * @private + * @param {OdooEvent} event + * @param {string} event.name='reload_mail_fields' + * @param {Object} event.data + * @param {boolean} [event.data.activity=false] + * @param {boolean} [event.data.followers=false] + * @param {boolean} [event.data.thread=false] + */ _onReloadMailFields: function (event) { var fieldNames = []; if (this.fields.activity && event.data.activity) { @@ -294,6 +390,9 @@ var Chatter = Widget.extend(chat_mixin, { keepChanges: true, }); }, + /** + * @private + */ _onScheduleActivity: function () { this.fields.activity.scheduleActivity(false); }, diff --git a/addons/mail/static/src/js/form_renderer.js b/addons/mail/static/src/js/form_renderer.js index 574075db9d64d..47a2b6b795ecc 100644 --- a/addons/mail/static/src/js/form_renderer.js +++ b/addons/mail/static/src/js/form_renderer.js @@ -44,39 +44,22 @@ FormRenderer.include({ /** * Overrides the function that renders the nodes to return the chatter's $el * for the 'oe_chatter' div node. - * Returns an empty div instead of the chatter's $el in create mode. * * @override * @private */ _renderNode: function (node) { if (node.tag === 'div' && node.attrs.class === 'oe_chatter') { - if (this.chatter) { - // Detach the chatter before updating the $el. - // This is important because if the view is now in create mode - // (edit mode with no res_id), the chatter will be removed from - // the DOM, and its handlers will be unbound. By detaching it - // beforehand, we ensure to keep its handlers alive so that if - // it is re-appended later, everything will still work properly - this.chatter.$el.detach(); - } - if (this.mode === 'edit' && !this.state.data.id) { - // there is no chatter in create mode - var $div = $('
'); - this._handleAttributes($div, node); - return $div; + if (!this.chatter) { + this.chatter = new Chatter(this, this.state, this.mailFields, { + isEditable: this.activeActions.edit, + }); + this.chatter.appendTo($('
')); + this._handleAttributes(this.chatter.$el, node); } else { - if (!this.chatter) { - this.chatter = new Chatter(this, this.state, this.mailFields, { - isEditable: this.activeActions.edit, - }); - this.chatter.appendTo($('
')); - this._handleAttributes(this.chatter.$el, node); - } else { - this.chatter.update(this.state); - } - return this.chatter.$el; + this.chatter.update(this.state); } + return this.chatter.$el; } else { return this._super.apply(this, arguments); } diff --git a/addons/mail/static/src/js/thread.js b/addons/mail/static/src/js/thread.js index 68a01df2345b2..1a655a52e08e2 100644 --- a/addons/mail/static/src/js/thread.js +++ b/addons/mail/static/src/js/thread.js @@ -67,9 +67,14 @@ var Thread = Widget.extend({ }, }, + /** + * @override + * @param {widget} parent + * @param {Object} options + */ init: function (parent, options) { this._super.apply(this, arguments); - this.options = _.defaults(options || {}, { + this.enabledOptions = _.defaults(options || {}, { display_order: ORDER.ASC, display_needactions: true, display_stars: true, @@ -79,13 +84,35 @@ var Thread = Widget.extend({ display_email_icon: true, display_reply_icon: false, }); + this.disabledOptions = { + display_order: this.enabledOptions.display_order, + display_needactions: false, + display_stars: false, + display_document_link: false, + display_avatar: this.enabledOptions, + squash_close_messages: false, + display_email_icon: false, + display_reply_icon: false, + }; + this.options = this.enabledOptions; this.expanded_msg_ids = []; this.selected_id = null; }, + /** + * @param {Object[]} messages + * @param {Object} [options] + * @param {integer} [options.display_order=ORDER.ASC] + * @param {boolean} [options.display_load_more=false] + * @param {boolean} [options.isCreationMode=false] + * @param {boolean} [options.squash_close_messages=false] + */ render: function (messages, options) { var self = this; - var msgs = _.map(messages, this._preprocess_message.bind(this)); + + this.options = options.isCreationMode ? this.disabledOptions : this.enabledOptions; + + var msgs = _.map(messages, this._preprocessMessage.bind(this)); if (this.options.display_order === ORDER.DESC) { msgs.reverse(); } @@ -219,42 +246,10 @@ var Thread = Widget.extend({ } }, - _redirect: _.debounce(function (options) { - if ('channel_id' in options) { - this.trigger('redirect_to_channel', options.channel_id); - } else { - this.trigger('redirect', options.model, options.id); - } - }, 200, true), - on_click_show_more: function () { this.trigger('load_more_messages'); }, - _preprocess_message: function (message) { - var msg = _.extend({}, message); - - msg.date = moment.min(msg.date, moment()); - msg.hour = time_from_now(msg.date); - - var date = msg.date.format('YYYY-MM-DD'); - if (date === moment().format('YYYY-MM-DD')) { - msg.day = _t("Today"); - } else if (date === moment().subtract(1, 'days').format('YYYY-MM-DD')) { - msg.day = _t("Yesterday"); - } else { - msg.day = msg.date.format('LL'); - } - - if (_.contains(this.expanded_msg_ids, message.id)) { - msg.expanded = true; - } - - msg.display_subject = message.subject && message.message_type !== 'notification' && !(message.model && (message.model !== 'mail.channel')); - msg.is_selected = msg.id === this.selected_id; - return msg; - }, - /** * Removes a message and re-renders the thread * @param {int} [message_id] the id of the removed message @@ -308,6 +303,56 @@ var Thread = Widget.extend({ clearInterval(this.update_timestamps_interval); }, + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * @private + * @param {Object} message + * @param {integer} [message.id] + * @param {string} [message.model] + * @param {string} [message.subject] + * @param {string} [message.message_type] + */ + _preprocessMessage: function (message) { + var msg = _.extend({}, message); + + msg.date = moment.min(msg.date, moment()); + msg.hour = time_from_now(msg.date); + + var date = msg.date.format('YYYY-MM-DD'); + if (date === moment().format('YYYY-MM-DD')) { + msg.day = _t("Today"); + } else if (date === moment().subtract(1, 'days').format('YYYY-MM-DD')) { + msg.day = _t("Yesterday"); + } else { + msg.day = msg.date.format('LL'); + } + + if (_.contains(this.expanded_msg_ids, message.id)) { + msg.expanded = true; + } + + msg.display_subject = message.subject && message.message_type !== 'notification' && !(message.model && (message.model !== 'mail.channel')); + msg.is_selected = msg.id === this.selected_id; + return msg; + }, + /** + * @private + * @param {Object} options + * @param {integer} [options.channel_id] + * @param {string} options.model + * @param {integer} options.id + */ + _redirect: _.debounce(function (options) { + if ('channel_id' in options) { + this.trigger('redirect_to_channel', options.channel_id); + } else { + this.trigger('redirect', options.model, options.id); + } + }, 200, true), + //-------------------------------------------------------------------------- // Handlers //-------------------------------------------------------------------------- diff --git a/addons/mail/static/src/js/thread_field.js b/addons/mail/static/src/js/thread_field.js index 75e379e6ea112..6f0ed7b2db53f 100644 --- a/addons/mail/static/src/js/thread_field.js +++ b/addons/mail/static/src/js/thread_field.js @@ -8,6 +8,7 @@ var AbstractField = require('web.AbstractField'); var core = require('web.core'); var field_registry = require('web.field_registry'); var concurrency = require('web.concurrency'); +var session = require('web.session'); var _t = core._t; @@ -69,7 +70,15 @@ var ThreadField = AbstractField.extend(chat_mixin, { this.res_id = record.res_id; }, - // public + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + /** + * @param {Object} message + * @param {integer[]} message.partner_ids + * @return {Deferred} + */ postMessage: function (message) { var self = this; var options = {model: this.model, res_id: this.res_id}; @@ -84,18 +93,54 @@ var ThreadField = AbstractField.extend(chat_mixin, { }); }, - // private + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * @private + * @returns {Object[]} an array containing a single message 'Creating a new record...' + */ + _forgeCreationModeMessages: function () { + return [{ + id: 0, + body: "

Creating a new record...

", + date: moment(), + author_id: [session.partner_id, session.partner_display_name], + displayed_author: session.partner_display_name, + avatar_src: "/web/image/res.partner/" + session.partner_id + "/image_small", + attachment_ids: [], + customer_email_data: [], + tracking_value_ids: [], + }]; + }, + /** + * @private + * @param {integer[]} ids + * @param {Object} [options] + */ _fetchAndRenderThread: function (ids, options) { var self = this; options = options || {}; options.ids = ids; var fetch_def = this.dp.add(this._getMessages(options)); return fetch_def.then(function (raw_messages) { - self.thread.render(raw_messages, {display_load_more: raw_messages.length < ids.length}); + var isCreationMode = false; + if (!self.res_id) { + raw_messages = self._forgeCreationModeMessages(); + isCreationMode = true; + } + self.thread.render(raw_messages, { + display_load_more: raw_messages.length < ids.length, + isCreationMode: isCreationMode, + }); }); }, - // handlers + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + /** * When a new message arrives, fetch its data to render it * @param {Number} message_id : the identifier of the new message diff --git a/addons/mail/static/src/xml/chatter.xml b/addons/mail/static/src/xml/chatter.xml index d45bd3312037e..e36bff6e6f1d4 100644 --- a/addons/mail/static/src/xml/chatter.xml +++ b/addons/mail/static/src/xml/chatter.xml @@ -50,14 +50,14 @@ Chatter (mail_thread widget) buttons --> - - - diff --git a/addons/mail/static/tests/chatter_tests.js b/addons/mail/static/tests/chatter_tests.js index d4b4c37704dee..a19e9485fb325 100644 --- a/addons/mail/static/tests/chatter_tests.js +++ b/addons/mail/static/tests/chatter_tests.js @@ -160,8 +160,8 @@ QUnit.test('basic rendering', function (assert) { form.destroy(); }); -QUnit.test('chatter is not rendered in mode === create', function (assert) { - assert.expect(4); +QUnit.test('chatter in create mode', function (assert) { + assert.expect(8); var form = createView({ View: FormView, @@ -197,16 +197,30 @@ QUnit.test('chatter is not rendered in mode === create', function (assert) { assert.strictEqual(form.$('.o_chatter').length, 1, "chatter should be displayed"); + // entering create mode form.$buttons.find('.o_form_button_create').click(); - - assert.strictEqual(form.$('.o_chatter').length, 0, - "chatter should not be displayed"); - + assert.strictEqual(form.$('.o_chatter').length, 1, + "chatter should still be displayed in create mode"); + + // topbar buttons disabled in create mode (e.g. 'send message') + assert.strictEqual(form.$('.o_chatter_topbar button:not(:disabled)').length, 0, + "button should be disabled in create mode") + + // chatter containing a single message with 'Creating a record...' + assert.strictEqual(form.$('.o_mail_thread').length, 1, + "there should be a mail thread"); + assert.strictEqual(form.$('.o_thread_message').length, 1, + "there should be a single thread message"); + assert.strictEqual(form.$('.o_thread_message_content').text().trim(), + "Creating a new record...", + "the content of the message should be 'Creating a new record...'"); + + // getting out of creation mode by saving form.$('.o_field_char').val('coucou').trigger('input'); form.$buttons.find('.o_form_button_save').click(); assert.strictEqual(form.$('.o_chatter').length, 1, - "chatter should be displayed"); + "chatter should still be displayed after saving from creation mode"); // check if chatter buttons still work form.$('.o_chatter_button_new_message').click(); @@ -259,8 +273,8 @@ QUnit.test('chatter rendering inside the sheet', function (assert) { form.$buttons.find('.o_form_button_create').click(); - assert.strictEqual(form.$('.o_chatter').length, 0, - "chatter should not be displayed"); + assert.strictEqual(form.$('.o_chatter').length, 1, + "chatter should be displayed"); // creating a new record... form.$('.o_field_char').val('coucou').trigger('input'); form.$buttons.find('.o_form_button_save').click(); @@ -861,7 +875,7 @@ QUnit.test('form activity widget: schedule activity does not discard changes', f }); QUnit.test('form activity widget: mark as done and remove', function (assert) { - assert.expect(14); + assert.expect(15); var self = this; @@ -969,6 +983,8 @@ QUnit.test('form activity widget: mark as done and remove', function (assert) { "there should be no more activity"); assert.strictEqual(form.$('.o_mail_thread .o_thread_message').length, 1, "a chatter message should have been generated"); + assert.strictEqual(form.$('.o_thread_message:contains(The activity has been done)').length, 1, + "the message's body should be correct"); form.destroy(); }); diff --git a/addons/web/models/ir_http.py b/addons/web/models/ir_http.py index 23a87f4cd027b..0cb7c5a6daa3b 100644 --- a/addons/web/models/ir_http.py +++ b/addons/web/models/ir_http.py @@ -33,6 +33,7 @@ def session_info(self): "server_version_info": version_info.get('server_version_info'), "name": user.name, "username": user.login, + "partner_display_name": user.partner_id.display_name, "company_id": request.env.user.company_id.id if request.session.uid else None, "partner_id": request.env.user.partner_id.id if request.session.uid and request.env.user.partner_id else None, "user_companies": {'current_company': (user.company_id.id, user.company_id.name), 'allowed_companies': [(comp.id, comp.name) for comp in user.company_ids]} if display_switch_company_menu else False,