diff --git a/addons/mail/static/src/js/attachment_box.js b/addons/mail/static/src/js/attachment_box.js index b42e8aeca95..f58c64f26e7 100644 --- a/addons/mail/static/src/js/attachment_box.js +++ b/addons/mail/static/src/js/attachment_box.js @@ -13,46 +13,43 @@ var AttachmentBox = Widget.extend({ events: { "click .o_attachment_download": "_onAttachmentDownload", "click .o_attachment_view": "_onAttachmentView", + "click .o_attachment_delete_cross": "_onDeleteAttachment", + "click .o_upload_attachments_button": "_onUploadAttachments", + "change .o_chatter_attachment_form .o_form_binary_form": "_onAddAttachment", }, /** * @override * @param {string} record.model * @param {Number} record.res_id + * @param {Object[]} attachments */ - init: function (parent, record) { + init: function (parent, record, attachments) { this._super.apply(this, arguments); + this.fileuploadId = _.uniqueId('oe_fileupload'); + $(window).on(this.fileuploadId, this._onUploaded.bind(this)); this.currentResID = record.res_id; this.currentResModel = record.model; - this.attachmentIDs = {}; + this.attachmentIDs = attachments; this.imageList = {}; this.otherList = {}; + + _.each(attachments, function (attachment) { + // required for compatibility with the chatter templates. + attachment.url = '/web/content/' + attachment.id + '?download=true'; + attachment.filename = attachment.datas_fname || 'unnamed'; + }); + var sortedAttachments = _.partition(attachments, function (att) { + return att.mimetype && att.mimetype.split('/')[0] === 'image'; + }); + this.imageList = sortedAttachments[0]; + this.otherList = sortedAttachments[1]; }, /** * @override */ - willStart: function () { - var self = this; - var domain = [ - ['res_id', '=', this.currentResID], - ['res_model', '=', this.currentResModel], - ]; - return $.when(this._super.apply(this, arguments), this._rpc({ - model: 'ir.attachment', - method: 'search_read', - domain: domain, - }).then(function (result) { - self.attachmentIDs = result; - _.each(result, function (attachment) { - attachment.url = '/web/content/' + attachment.id + '?download=true'; - // required for compatibility with the chatter templates. - attachment.filename = attachment.datas_fname || 'unnamed'; - }); - var sortedAttachments = _.partition(result, function (att) { - return att.mimetype && att.mimetype.split('/')[0] === 'image'; - }); - self.imageList = sortedAttachments[0]; - self.otherList = sortedAttachments[1]; - })); + destroy: function () { + $(window).off(this.fileuploadId); + this._super.apply(this, arguments); }, //-------------------------------------------------------------------------- @@ -60,8 +57,8 @@ var AttachmentBox = Widget.extend({ //-------------------------------------------------------------------------- /** - * @param {Object} record - */ + * @param {Object} record + */ update: function (record) { this.currentResID = record.res_id; this.currentResModel = record.model; @@ -71,6 +68,19 @@ var AttachmentBox = Widget.extend({ // Handlers //-------------------------------------------------------------------------- + /** + * Method triggered when user click on 'add attachment' and select a file + * + * @private + * @param {MouseEvent} ev + */ + _onAddAttachment: function (ev) { + var $input = $(ev.currentTarget).find('input.o_input_file'); + if ($input.val() !== '') { + var $binaryForm = this.$('form.o_form_binary_form'); + $binaryForm.submit(); + } + }, /** * @private * @param {MouseEvent} ev @@ -92,6 +102,33 @@ var AttachmentBox = Widget.extend({ attachmentViewer.appendTo($('body')); } }, + /** + * Opens File Explorer dialog if all fields are valid and record is saved + * + * @private + * @param {MouseEvent} ev + */ + _onUploadAttachments: function (ev) { + this.$('input.o_input_file').click(); + }, + /** + * @private + * @param {MouseEvent} ev + */ + _onDeleteAttachment: function (ev) { + ev.stopPropagation(); + var $target = $(ev.currentTarget); + this.trigger_up('delete_attachment', { + attachmentId: $target.data('id'), + attachmentName: $target.data('name') + }); + }, + /** + * @private + */ + _onUploaded: function() { + this.trigger_up('reload_attachment_box'); + }, }); return AttachmentBox; diff --git a/addons/mail/static/src/js/chatter.js b/addons/mail/static/src/js/chatter.js index 000353d1219..da23521f65f 100644 --- a/addons/mail/static/src/js/chatter.js +++ b/addons/mail/static/src/js/chatter.js @@ -4,6 +4,7 @@ odoo.define('mail.Chatter', function (require) { var Activity = require('mail.Activity'); var AttachmentBox = require('mail.AttachmentBox'); var ChatterComposer = require('mail.composer.Chatter'); +var Dialog = require('web.Dialog'); var Followers = require('mail.Followers'); var ThreadField = require('mail.ThreadField'); var mailUtils = require('mail.utils'); @@ -13,25 +14,28 @@ var config = require('web.config'); var core = require('web.core'); var Widget = require('web.Widget'); +var _t = core._t; var QWeb = core.qweb; // The purpose of this widget is to display the chatter area below the form view // -// It instanciates the optional mail_thread, mail_activity and mail_followers widgets. +// It instantiates the optional mail_thread, mail_activity and mail_followers widgets. // It Ensures that those widgets are appended at the right place, and allows them to communicate // with each other. -// It synchronizes the rendering of those widgets (as they may be asynchronous), to limitate +// It synchronizes the rendering of those widgets (as they may be asynchronous), to mitigate // the flickering when switching between records var Chatter = Widget.extend({ template: 'mail.Chatter', custom_events: { + delete_attachment: '_onDeleteAttachment', discard_record_changes: '_onDiscardRecordChanges', + reload_attachment_box: '_onReloadAttachmentBox', reload_mail_fields: '_onReloadMailFields', }, events: { 'click .o_chatter_button_new_message': '_onOpenComposerMessage', 'click .o_chatter_button_log_note': '_onOpenComposerNote', - 'click .o_chatter_button_attachment': '_onOpenAttachments', + 'click .o_chatter_button_attachment': '_onClickAttachmentButton', 'click .o_chatter_button_schedule_activity': '_onScheduleActivity', }, supportedFieldTypes: ['one2many'], @@ -52,15 +56,19 @@ var Chatter = Widget.extend({ this._super.apply(this, arguments); this._setState(record); - this._dp = new concurrency.DropPrevious(); + this.attachments = {}; + this.fields = {}; + this._areAttachmentsLoaded = false; + this._disableAttachmentBox = !!options.disable_attachment_box; + this._dp = new concurrency.DropPrevious(); + this._isAttachmentBoxOpen = false; // mention: get the prefetched partners and use them as mention suggestions // if there is a follower widget, the followers will be added to the // suggestions as well once fetched this._mentionPartnerSuggestions = this.call('mail_service', 'getMentionPartnerSuggestions'); this._mentionSuggestions = this._mentionPartnerSuggestions; - this.fields = {}; if (mailFields.mail_activity) { this.fields.activity = new Activity(this, mailFields.mail_activity, record, options); } @@ -74,16 +82,17 @@ var Chatter = Widget.extend({ this.hasLogButton = options.display_log_button || nodeOptions.display_log_button; this.postRefresh = nodeOptions.post_refresh || 'never'; } - this.attachmentBoxOpened = false; }, /** * @override */ start: function () { this._$topbar = this.$('.o_chatter_topbar'); - this.$('.o_topbar_right_area').append(QWeb.render('mail.chatter.Attachment.Button', { - count: this.record.data.message_attachment_count || 0, - })); + if(!this._disableAttachmentBox) { + this.$('.o_topbar_right_area').append(QWeb.render('mail.chatter.Attachment.Button', { + count: this.record.data.message_attachment_count || 0, + })); + } // render and append the buttons this._$topbar.prepend(QWeb.render('mail.chatter.Buttons', { newMessageButton: !!this.fields.thread, @@ -115,6 +124,8 @@ var Chatter = Widget.extend({ if (this.record.res_id !== record.res_id) { this._closeComposer(true); this._closeAttachments(); + this._areAttachmentsLoaded = false; + this.attachments = {}; } // update the state @@ -155,13 +166,12 @@ var Chatter = Widget.extend({ /** * @private - * @param {boolean} force */ _closeAttachments: function () { if (this.fields.attachments) { this.$('.o_chatter_button_attachment').removeClass('o_active_attach'); this.fields.attachments.destroy(); - this.attachmentBoxOpened = false; + this._isAttachmentBoxOpen = false; } }, /** @@ -231,22 +241,46 @@ var Chatter = Widget.extend({ _enableComposer: function () { this.$(".o_composer_button_send").prop('disabled', false); }, + /* + * @private + * @return {Deferred} + */ + _fetchAttachments: function () { + var self = this; + var domain = [ + ['res_id', '=', this.record.res_id], + ['res_model', '=', this.record.model], + ]; + return this._rpc({ + model: 'ir.attachment', + method: 'search_read', + domain: domain, + fields: ['id', 'name', 'datas_fname', 'mimetype'], + }).then(function (result) { + self._areAttachmentsLoaded = true; + self.attachments = result; + }); + + }, /** * @private */ - _openAttachments: function () { - var self = this; - this.fields.attachments = new AttachmentBox(this, this.record); - + _openAttachmentBox: function () { + if (this.fields.attachments) { + this._closeAttachments(); + } + this.fields.attachments = new AttachmentBox(this, this.record, this.attachments); var $anchor = this.$('.o_chatter_topbar'); if (this._composer) { - $anchor = this.$('.o_thread_composer'); + var $anchor = this.$('.o_thread_composer'); + } else { + var $anchor = this.$('.o_chatter_topbar'); } - this.fields.attachments.insertAfter($anchor).then(function () { - self.$el.addClass('o_chatter_composer_active'); - self.$('.o_chatter_button_attachment').addClass('o_active_attach'); - }); - this.attachmentBoxOpened = true; + this.fields.attachments.insertAfter($anchor); + this.$el.addClass('o_chatter_composer_active'); + this.$('.o_chatter_button_attachment').addClass('o_active_attach'); + + this._isAttachmentBoxOpen = true; }, /** * @private @@ -286,6 +320,7 @@ var Chatter = Widget.extend({ if (self._reloadAfterPost(messageData)) { self.trigger_up('reload'); } else if (messageData.attachment_ids.length) { + self._reloadAttachmentBox(); self.trigger_up('reload', {fieldNames: ['message_attachment_count']}); } }).fail(function () { @@ -320,6 +355,16 @@ var Chatter = Widget.extend({ messageData.partner_ids.length ); }, + /** + * @private + */ + _reloadAttachmentBox: function () { + this._areAttachmentsLoaded = false; + if (this._isAttachmentBoxOpen) { + this._fetchAttachments().then(this._openAttachmentBox.bind(this)); + } + this.trigger_up('reload', { fieldNames: ['message_attachment_count'] }); + }, /** * @private * @param {Deferred} def @@ -344,7 +389,11 @@ var Chatter = Widget.extend({ self.fields.activity.$el.appendTo(self.$el); } if (self.fields.followers) { - self.fields.followers.$el.insertBefore(self.$('.o_chatter_button_attachment')); + if (self._disableAttachmentBox) { + self.fields.followers.$el.appendTo(self.$('.o_topbar_right_area')); + } else { + self.fields.followers.$el.insertBefore(self.$('.o_chatter_button_attachment')); + } } if (self.fields.thread) { self.fields.thread.$el.appendTo(self.$el); @@ -383,8 +432,7 @@ var Chatter = Widget.extend({ */ _updateAttachmentCounter: function () { var count = this.record.data.message_attachment_count || 0; - this.$('.o_chatter_attachment_button_count').html(' ('+ count +')'); - this.$('.o_chatter_button_attachment').toggleClass('o_hidden', !count); + this.$('.o_chatter_attachment_button_count').html(count); }, /** * @private @@ -427,12 +475,45 @@ var Chatter = Widget.extend({ /** * @private + * @param {OdooEvent} ev + * @param {integer} ev.data.attachmentId + * @param {String} ev.data.attachmentName */ - _onOpenAttachments: function () { - if (this.attachmentBoxOpened) { + _onDeleteAttachment: function (ev) { + ev.stopPropagation(); + var self = this; + var options = { + confirm_callback: function () { + self._rpc({ + model: 'ir.attachment', + method: 'unlink', + args: [parseInt(ev.data.attachmentId, 10)], + }) + .then(function () { + self._reloadAttachmentBox(); + self.fields.thread.removeAttachments([ev.data.attachmentId]); + self.trigger_up('reload'); + }); + } + }; + var promptText = _.str.sprintf(_t("Do you really want to delete %s?"), _.escape(ev.data.attachmentName)); + Dialog.confirm(this, promptText, options); + }, + /** + * @private + */ + _onClickAttachmentButton: function () { + if(this._disableAttachmentBox) { + return; + } + if (this._isAttachmentBoxOpen) { this._closeAttachments(); } else { - this._openAttachments(); + var def; + if (!this._areAttachmentsLoaded) { + def = this._fetchAttachments(); + } + $.when(def).then(this._openAttachmentBox.bind(this)); } }, /** @@ -483,6 +564,12 @@ var Chatter = Widget.extend({ _onOpenComposerNote: function () { this._openComposer({ isLog: true }); }, + /** + * @private + */ + _onReloadAttachmentBox: function () { + this._reloadAttachmentBox(); + }, /** * @private * @param {OdooEvent} event diff --git a/addons/mail/static/src/js/models/messages/abstract_message.js b/addons/mail/static/src/js/models/messages/abstract_message.js index f5cf55f0ef1..c1ae22b9708 100644 --- a/addons/mail/static/src/js/models/messages/abstract_message.js +++ b/addons/mail/static/src/js/models/messages/abstract_message.js @@ -335,6 +335,14 @@ var AbstractMessage = Class.extend({ needsModeration: function () { return false; }, + /** + * @params {integer[]} attachmentIDs + */ + removeAttachments: function (attachmentIDs) { + this._attachmentIDs = _.reject(this._attachmentIDs, function (attachment) { + return _.contains(attachmentIDs, attachment.id); + }); + }, /** * State whether this message should redirect to the author * when clicking on the author of this message. diff --git a/addons/mail/static/src/js/models/threads/document_thread.js b/addons/mail/static/src/js/models/threads/document_thread.js index d902d69765c..6c15b39d25e 100644 --- a/addons/mail/static/src/js/models/threads/document_thread.js +++ b/addons/mail/static/src/js/models/threads/document_thread.js @@ -156,6 +156,14 @@ var DocumentThread = Thread.extend({ isLinkedToDocument: function () { return true; }, + /** + * @param {integer[]} attachmentIDs + */ + removeAttachmentsFromMessages: function (attachmentIDs) { + _.each(this.getMessages(), function (message) { + message.removeAttachments(attachmentIDs); + }); + }, /** * Set list of message IDs of this document thread * diff --git a/addons/mail/static/src/js/thread_field.js b/addons/mail/static/src/js/thread_field.js index 5c85a2adb37..a3807802ad7 100644 --- a/addons/mail/static/src/js/thread_field.js +++ b/addons/mail/static/src/js/thread_field.js @@ -70,6 +70,12 @@ var ThreadField = AbstractField.extend({ // Public //-------------------------------------------------------------------------- + /** + * @param {integer[]} attachmentIDs + */ + removeAttachments: function (attachmentIDs) { + this._documentThread.removeAttachmentsFromMessages(attachmentIDs); + }, /** * @override * @return {boolean} diff --git a/addons/mail/static/src/js/thread_widget.js b/addons/mail/static/src/js/thread_widget.js index d812d403007..2e2b7e222c6 100644 --- a/addons/mail/static/src/js/thread_widget.js +++ b/addons/mail/static/src/js/thread_widget.js @@ -34,6 +34,7 @@ var ThreadWidget = Widget.extend({ 'click .o_thread_show_more': '_onClickShowMore', 'click .o_attachment_download': '_onAttachmentDownload', 'click .o_attachment_view': '_onAttachmentView', + 'click .o_attachment_delete_cross': '_onDeleteAttachment', 'click .o_thread_message_needaction': '_onClickMessageNeedaction', 'click .o_thread_message_star': '_onClickMessageStar', 'click .o_thread_message_reply': '_onClickMessageReply', @@ -410,6 +411,18 @@ var ThreadWidget = Widget.extend({ }); }); }, + /** + * @private + * @param {MouseEvent} ev + */ + _onDeleteAttachment: function (ev) { + ev.stopPropagation(); + var $target = $(ev.currentTarget); + this.trigger_up('delete_attachment', { + attachmentId: $target.data('id'), + attachmentName: $target.data('name') + }); + }, /** * @private * @param {Object} options diff --git a/addons/mail/static/src/scss/attachment_box.scss b/addons/mail/static/src/scss/attachment_box.scss index ab412bf52fe..a9aa6aec260 100644 --- a/addons/mail/static/src/scss/attachment_box.scss +++ b/addons/mail/static/src/scss/attachment_box.scss @@ -1,25 +1,36 @@ .oe_chatter { + .btn.o_active_attach { - color: #875A7B; height: 300px; } - .o_chatter_attachment { - padding: 3px; + .o_mail_chatter_attachments { + min-height: 50px; - .o_border_dashed { - position: relative; - margin-top: 15px; - margin-bottom: 30px; - border-bottom: 1px dashed gray('300'); - text-align: center; + .o_chatter_attachment { + padding: 3px; - .o_attach_title { + input.o_input_file { + position: absolute; + top: 1px; + opacity: 0; + pointer-events: none; + } + + .o_border_dashed { + position: relative; + margin-top: 5px; + margin-bottom: 5px; + border-bottom: 1px dashed gray('300'); + text-align: center; + + .o_attach_title { position: relative; top: 10px; margin: 0 auto; padding: 0 10px; font-weight: bold; background: white; + } } } } diff --git a/addons/mail/static/src/scss/chatter.scss b/addons/mail/static/src/scss/chatter.scss index 261552c2b23..599802382a1 100644 --- a/addons/mail/static/src/scss/chatter.scss +++ b/addons/mail/static/src/scss/chatter.scss @@ -4,6 +4,11 @@ width: 100%; padding-top: 0!important; + .o_attachment_delete_cross { + @include o-position-absolute($right: 6px, $top:7px); + cursor: pointer; + } + .o_spinner { text-align: center; font-size: large; @@ -21,9 +26,16 @@ position: relative; float: right; flex: 1 1 auto; + .o_chatter_button_attachment { - height: 2.3em; + height: $o-statusbar-height - 2px; float: right; + padding: 0px; + margin-right: 25px; + + .fa-paperclip, .o_chatter_attachment_button_count { + font-weight: bold; + } } .o_followers { height: $o-statusbar-height + 1; diff --git a/addons/mail/static/src/scss/composer.scss b/addons/mail/static/src/scss/composer.scss index aa5131b0de5..096c527aa16 100644 --- a/addons/mail/static/src/scss/composer.scss +++ b/addons/mail/static/src/scss/composer.scss @@ -434,6 +434,10 @@ $o-mail-attachment-margin: 5px; font-size: $font-size-sm; } + .o_attachment_delete_cross { + color: white; + } + .o_attachment_download { @include o-position-absolute($right: 7px, $bottom:7px); } diff --git a/addons/mail/static/src/xml/chatter.xml b/addons/mail/static/src/xml/chatter.xml index 7461f4b9055..7f8629f1bae 100644 --- a/addons/mail/static/src/xml/chatter.xml +++ b/addons/mail/static/src/xml/chatter.xml @@ -20,6 +20,16 @@ +
+ + + /web/binary/upload_attachment + + + +
Add Attachments
+
+
@@ -96,11 +106,11 @@ - diff --git a/addons/mail/static/src/xml/thread.xml b/addons/mail/static/src/xml/thread.xml index 914168306b8..be73917a1b5 100644 --- a/addons/mail/static/src/xml/thread.xml +++ b/addons/mail/static/src/xml/thread.xml @@ -523,6 +523,7 @@
+ @@ -546,6 +547,7 @@
+ diff --git a/addons/mail/static/tests/chatter_tests.js b/addons/mail/static/tests/chatter_tests.js index 30225bd041a..6b1d318d43b 100644 --- a/addons/mail/static/tests/chatter_tests.js +++ b/addons/mail/static/tests/chatter_tests.js @@ -292,7 +292,7 @@ QUnit.test('Activity Done keep feedback on blur', function (assert) { }); QUnit.test('attachmentBox basic rendering', function (assert) { - assert.expect(11); + assert.expect(19); this.data.partner.records.push({ id: 7, display_name: "attachment_test", @@ -334,6 +334,19 @@ QUnit.test('attachmentBox basic rendering', function (assert) { assert.containsOnce(form, '.o_attachment_image', "there should be an image preview"); assert.containsOnce(form, '.o_attachments_previews', "there should be a list of previews"); assert.containsOnce(form, '.o_attachments_list', "there should be a list of non previewable attachments"); + assert.containsOnce(form, '.o_upload_attachments_button', "there should be an 'Add Attachments' button"); + assert.containsOnce(form, '.o_form_binary_form', "there should be a binary form"); + + assert.containsOnce(form, 'input[name="model"]', "there should be an model input"); + var $modelInput = form.$('input[name="model"]'); + assert.hasAttrValue($modelInput, 'value', 'partner'); + assert.hasAttrValue($modelInput, 'type', 'hidden'); + + assert.containsOnce(form, 'input[name="model"]', "there should be an id input"); + var $resIdInput = form.$('input[name="id"]'); + assert.hasAttrValue($resIdInput, 'value', '7'); + assert.hasAttrValue($resIdInput, 'type', 'hidden'); + assert.strictEqual(form.$('.o_attachment_title').text(), 'name1', "the image name should be correct"); // since there are two elements "Download name2"; one "name" and the other "txt" as text content, the following test