[IMP] mail: adds delete and upload to chatter

This commit adds the ability to upload and delete
attachments from the chatter.

Task: #1911277

closes odoo/odoo#29702
This commit is contained in:
Thanh Dodeur
2018-12-21 15:25:27 +00:00
parent 5545787e2c
commit 73d9b9588f
12 changed files with 279 additions and 68 deletions
+64 -27
View File
@@ -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;
+114 -27
View File
@@ -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
@@ -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.
@@ -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
*
@@ -70,6 +70,12 @@ var ThreadField = AbstractField.extend({
// Public
//--------------------------------------------------------------------------
/**
* @param {integer[]} attachmentIDs
*/
removeAttachments: function (attachmentIDs) {
this._documentThread.removeAttachmentsFromMessages(attachmentIDs);
},
/**
* @override
* @return {boolean}
@@ -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
+21 -10
View File
@@ -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;
}
}
}
}
+13 -1
View File
@@ -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;
@@ -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);
}
+12 -2
View File
@@ -20,6 +20,16 @@
<t t-call="mail.Attachment"/>
</t>
</div>
<div class="o_chatter_attachment_form">
<t t-call="HiddenInputFile">
<t t-set="fileupload_id" t-value="widget.fileuploadId"/>
<t t-set="fileupload_action" t-translation="off">/web/binary/upload_attachment</t>
<t t-set="multi_upload" t-value="true"/>
<input type="hidden" name="model" t-att-value="widget.currentResModel"/>
<input type="hidden" name="id" t-att-value="widget.currentResID"/>
<center><span class="btn btn-link o_upload_attachments_button"><span class="fa fa-plus-square"/> Add Attachments</span></center>
</t>
</div>
</div>
</div>
@@ -96,11 +106,11 @@
</t>
<t t-name="mail.chatter.Attachment.Button">
<button t-attf-class="btn btn-sm btn-link o_chatter_button_attachment #{!count and 'o_hidden' or ''}" title="View all the attachments of the current record">
<button class="btn btn-link o_chatter_button_attachment" title="View all the attachments of the current record">
<span>
<i class="fa fa-paperclip"/>
<span class="o_chatter_attachment_button_count">
<t t-esc="' ('+count+')'"/>
<t t-esc="count"/>
</span>
</span>
</button>
+2
View File
@@ -523,6 +523,7 @@
<div class="o_image_box">
<div class="o_attachment_image" t-attf-style="background-image:url('/web/image/#{attachment.id}/160x160/?crop=true')"/>
<div t-attf-class="o_image_overlay o_attachment_view" t-att-data-id="attachment.id">
<span class="fa fa-times o_attachment_delete_cross" t-att-title="'Delete ' + attachment.name" t-att-data-id="attachment.id" t-att-data-name="attachment.name"/>
<span class="o_attachment_title text-white"><t t-esc="attachment.name"/></span>
<a class="o_attachment_download" t-att-href='attachment.url'>
<i t-attf-class="fa fa-download text-white" t-att-title="'Download ' + attachment.name" role="img" aria-label="Download"></i>
@@ -546,6 +547,7 @@
<t t-set="type" t-value="attachment.mimetype and attachment.mimetype.split('/').shift()"/>
<div t-attf-class="o_attachment #{ editable ? 'o_attachment_editable' : '' } #{attachment.upload ? 'o_attachment_uploading' : ''}" t-att-title="attachment.name">
<div class="o_attachment_wrap">
<span t-if="!editable" class="fa fa-times o_attachment_delete_cross" t-att-title="'Delete ' + attachment.name" t-att-data-id="attachment.id" t-att-data-name="attachment.name"/>
<t t-set="has_preview" t-value="type == 'image' or type == 'video' or attachment.mimetype == 'application/pdf'"/>
<t t-set="ext" t-value="attachment.filename.split('.').pop()"/>
+14 -1
View File
@@ -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