[IMP] mail: new attachment button in the chatter

- Added a new button to the chatter that displays the amount of
 attachments on the current record and when clicked, all the attachments
 attached to the current record

      - added mail.FormView (includes), used by the chatter attachment
       button to fetch the field value of attachment_count.

      - added a computed field "attachment_count" to mail.thread that
       contains the amount of attachment attached to the record.

      - changed the followers div, it is now sharing a "right side"
       area with the attachment button.

 task: 1860031
This commit is contained in:
ThanhDodeurOdoo
2018-09-13 11:50:26 +02:00
parent 6a2b4827d4
commit 7f97c9fc05
10 changed files with 356 additions and 20 deletions
+7
View File
@@ -110,6 +110,7 @@ class MailThread(models.AbstractModel):
message_has_error_counter = fields.Integer(
'Number of error', compute='_compute_message_has_error',
help="Number of messages with delivery error")
related_attachment_count = fields.Integer('Attachment Count', compute='_compute_related_attachment_count')
@api.one
@api.depends('message_follower_ids')
@@ -145,6 +146,12 @@ class MailThread(models.AbstractModel):
# using read() below is much faster than followers.mapped('res_id')
return [('id', 'in', [res['res_id'] for res in followers.read(['res_id'])])]
@api.multi
def _compute_related_attachment_count(self):
for record in self:
domain = [('res_id', '=', record.id), ('res_model', '=', self._name)]
record.related_attachment_count = self.env['ir.attachment'].search_count(domain)
@api.multi
@api.depends('message_follower_ids')
def _compute_is_follower(self):
@@ -0,0 +1,99 @@
odoo.define('mail.AttachmentBox', function (require) {
"use strict";
var core = require('web.core');
var Widget = require('web.Widget');
var DocumentViewer = require('mail.DocumentViewer');
var QWeb = core.qweb;
var AttachmentBox = Widget.extend({
template: 'mail.chatter.AttachmentBox',
events: {
"click .o_attachment_download": "_onAttachmentDownload",
"click .o_attachment_view": "_onAttachmentView",
},
/**
* @override
* @param {string} record.model
* @param {Number} record.res_id
*/
init: function (parent, record) {
this._super.apply(this, arguments);
this.currentResID = record.res_id;
this.currentResModel = record.model;
this.attachmentIDs = {};
this.imageList = {};
this.otherList = {};
},
/**
* @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];
}));
},
//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
/**
* @param {Object} record
*/
update: function (record) {
this.currentResID = record.res_id;
this.currentResModel = record.model;
},
//--------------------------------------------------------------------------
// Handlers
//--------------------------------------------------------------------------
/**
* @private
* @param {MouseEvent} ev
* used to prevent the click from opening the document viewer.
*/
_onAttachmentDownload: function (ev) {
ev.stopPropagation();
},
/**
* @private
* @param {MouseEvent} ev
*/
_onAttachmentView: function (ev) {
ev.stopPropagation();
ev.preventDefault();
var activeAttachmentID = $(ev.currentTarget).data('id');
if (activeAttachmentID) {
var attachmentViewer = new DocumentViewer(this, this.attachmentIDs, activeAttachmentID);
attachmentViewer.appendTo($('body'));
}
},
});
return AttachmentBox;
});
+58 -4
View File
@@ -2,6 +2,7 @@ odoo.define('mail.Chatter', function (require) {
"use strict";
var Activity = require('mail.Activity');
var AttachmentBox = require('mail.AttachmentBox');
var ChatterComposer = require('mail.composer.Chatter');
var Followers = require('mail.Followers');
var ThreadField = require('mail.ThreadField');
@@ -30,6 +31,7 @@ var Chatter = Widget.extend({
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_schedule_activity': '_onScheduleActivity',
},
supportedFieldTypes: ['one2many'],
@@ -69,21 +71,23 @@ 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.related_attachment_count || 0,
}));
// render and append the buttons
this._$topbar.append(QWeb.render('mail.chatter.Buttons', {
this._$topbar.prepend(QWeb.render('mail.chatter.Buttons', {
newMessageButton: !!this.fields.thread,
logNoteButton: this.hasLogButton,
scheduleActivityButton: !!this.fields.activity,
isMobile: config.device.isMobile,
}));
// start and append the widgets
var fieldDefs = _.invoke(this.fields, 'appendTo', $('<div>'));
var def = this._dp.add($.when.apply($, fieldDefs));
@@ -107,6 +111,7 @@ var Chatter = Widget.extend({
// close the composer if we switch to another record as it is record dependent
if (this.record.res_id !== record.res_id) {
this._closeComposer(true);
this._closeAttachments();
}
// update the state
@@ -138,12 +143,24 @@ var Chatter = Widget.extend({
self.$el.height('auto');
self._updateMentionSuggestions();
});
this._updateAttachmentCounter();
},
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
/**
* @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;
}
},
/**
* @private
* @param {boolean} force
@@ -199,6 +216,23 @@ var Chatter = Widget.extend({
_enableChatter: function () {
this.$('.btn').prop('disabled', false); // enable buttons
},
/**
* @private
*/
_openAttachments: function () {
var self = this;
this.fields.attachments = new AttachmentBox(this, this.record);
var $anchor = this.$('.o_chatter_topbar');
if (this._composer) {
$anchor = this.$('.o_thread_composer');
}
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;
},
/**
* @private
* @param {Object} options
@@ -235,6 +269,8 @@ var Chatter = Widget.extend({
self._closeComposer(true);
if (self._reloadAfterPost(messageData)) {
self.trigger_up('reload');
} else if (messageData.attachment_ids.length) {
self.trigger_up('reload', {fieldNames: ['related_attachment_count']});
}
});
});
@@ -290,7 +326,7 @@ var Chatter = Widget.extend({
self.fields.activity.$el.appendTo(self.$el);
}
if (self.fields.followers) {
self.fields.followers.$el.appendTo(self._$topbar);
self.fields.followers.$el.insertBefore(self.$('.o_chatter_button_attachment'));
}
if (self.fields.thread) {
self.fields.thread.$el.appendTo(self.$el);
@@ -324,6 +360,14 @@ var Chatter = Widget.extend({
this.record = record;
this.recordName = record.data.display_name;
},
/**
* @private
*/
_updateAttachmentCounter: function () {
var count = this.record.data.related_attachment_count || 0;
this.$('.o_chatter_attachment_button_count').html(' ('+ count +')');
this.$('.o_chatter_button_attachment').toggleClass('o_hidden', !count);
},
/**
* @private
*/
@@ -363,6 +407,16 @@ var Chatter = Widget.extend({
// Handlers
//--------------------------------------------------------------------------
/**
* @private
*/
_onOpenAttachments: function () {
if (this.attachmentBoxOpened) {
this._closeAttachments();
} else {
this._openAttachments();
}
},
/**
* Discard changes on the record.
* This is notified by the composer, when opening the full-composer.
+2 -1
View File
@@ -29,7 +29,8 @@ FormRenderer.include({
*/
confirmChange: function (state, id, fields) {
if (this.chatter) {
var updatedMailFields = _.intersection(fields, _.values(this.mailFields));
var chatterFields = ['related_attachment_count'].concat(_.values(this.mailFields));
var updatedMailFields = _.intersection(fields, chatterFields);
if (updatedMailFields.length) {
this.chatter.update(state, updatedMailFields);
}
+25
View File
@@ -0,0 +1,25 @@
odoo.define('mail.FormView', function (require) {
"use strict";
var FormView = require('web.FormView');
/**
* This file is used to add "related_attachment_count" to fieldsInfo so we can fetch its value for the
* chatter's attachment button without having it explicitly declared in the form view template.
*
*/
FormView.include({
/**
* @override
*/
init: function () {
this._super.apply(this, arguments);
if ('message_ids' in this.fieldsInfo[this.viewType]) {
this.fieldsInfo[this.viewType].related_attachment_count = {};
}
},
});
});
@@ -0,0 +1,26 @@
.oe_chatter {
.btn.o_active_attach {
color: #875A7B;
height: 300px;
}
.o_chatter_attachment {
padding: 3px;
.o_border_dashed {
position: relative;
margin-top: 15px;
margin-bottom: 30px;
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;
}
}
}
}
+16 -6
View File
@@ -16,20 +16,30 @@
padding: 0;
display: flex;
> .o_topbar_right_area {
border-bottom: 1px solid transparent;
position: relative;
float: right;
flex: 1 1 auto;
.o_chatter_button_attachment {
height: 2.3em;
float: right;
}
.o_followers {
height: $o-statusbar-height + 1;
border-bottom: 1px solid transparent;
flex: 0 1 auto;
}
}
> .btn {
height: $o-statusbar-height + 1;
border: 1px solid transparent;
}
> .o_followers {
height: $o-statusbar-height + 1;
border-bottom: 1px solid transparent;
flex: 1 1 auto;
}
}
&.o_chatter_composer_active .o_chatter_topbar {
> .btn, > .o_followers{
> .btn, > .o_followers, > .o_topbar_right_area{
border-radius: $border-radius-sm $border-radius-sm 0 0;
border-width: 0 0 1px;
border-bottom-color: gray('300');
+38 -1
View File
@@ -1,11 +1,34 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates>
<!--
Chatter attachment tab
-->
<div t-name="mail.chatter.AttachmentBox" class="o_mail_chatter_attachments">
<div class="o_chatter_attachment">
<div class="o_border_dashed">
<span class="o_attach_title">Attachments</span>
</div>
<div t-if="widget.imageList.length > 0" class="o_attachments_previews">
<t t-foreach="widget.imageList" t-as="attachment">
<t t-call="mail.AttachmentPreview"/>
</t>
</div>
<div t-if="widget.otherList.length > 0" class="o_attachments_list">
<t t-foreach="widget.otherList" t-as="attachment">
<t t-call="mail.Attachment"/>
</t>
</div>
</div>
</div>
<!--
Chatter composer
@param {mail.composer.Chatter} widget
-->
<t t-name="mail.chatter.Composer" t-extend="mail.Composer">
<!-- Insert information before the composer -->
<t t-jquery=".o_composer_container" t-operation="before">
@@ -69,6 +92,18 @@
<button t-if="scheduleActivityButton" class="btn btn-link o_chatter_button_schedule_activity" title="Log or schedule an activity" disabled="disabled">
<i class="fa fa-clock-o" role="img" aria-label="Dates" title="Dates"/> <t t-if="isMobile">Activity</t><t t-else="">Schedule activity</t>
</button>
</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">
<span>
<i class="fa fa-paperclip"/>
<span class="o_chatter_attachment_button_count">
<t t-esc="' ('+count+')'"/>
</span>
</span>
</button>
</t>
<!--
@@ -76,7 +111,9 @@
-->
<t t-name="mail.Chatter">
<aside class="o_chatter">
<div class="o_chatter_topbar"/>
<div class="o_chatter_topbar">
<div class="o_topbar_right_area"/>
</div>
</aside>
</t>
+76 -3
View File
@@ -1,6 +1,7 @@
odoo.define('mail.chatter_tests', function (require) {
"use strict";
var AttachmentBox = require('mail.AttachmentBox');
var mailTestUtils = require('mail.testUtils');
var concurrency = require('web.concurrency');
@@ -34,7 +35,7 @@ QUnit.module('Chatter', {
string: "Followers",
type: "one2many",
relation: 'mail.followers',
relation_field: "res_id"
relation_field: "res_id",
},
message_ids: {
string: "messages",
@@ -53,9 +54,14 @@ QUnit.module('Chatter', {
type: 'selection',
selection: [['overdue', 'Overdue'], ['today', 'Today'], ['planned', 'Planned']],
},
related_attachment_count: {
string: 'Attachment count',
type: 'integer',
},
},
records: [{
id: 2,
related_attachment_count: 3,
display_name: "first partner",
foo: "HELLO",
message_follower_ids: [],
@@ -134,6 +140,25 @@ QUnit.module('Chatter', {
},
records: [],
},
'ir.attachment': {
fields:{
name:{type:'char', string:"attachment name", required:true},
res_model:{type:'char', string:"res model"},
res_id:{type:'integer', string:"res id"},
url:{type:'char', string:'url'},
type:{ type:'selection', selection:[['url',"URL"],['binary',"BINARY"]]},
mimetype:{type:'char', string:"mimetype"},
datas_fname:{type:'char', string:"filename"},
},
records:[
{id:1, name:"name1", type:'url', mimetype:'image/png', datas_fname:'filename.jpg',
res_id: 7, res_model: 'partner'},
{id:2, name:"name2", type:'binary', mimetype:"application/x-msdos-program",
datas_fname:"file2.txt", res_id: 7, res_model: 'partner'},
{id:3, name:"name2", type:'binary', mimetype:"application/x-msdos-program",
datas_fname:"file2.txt", res_id: 5, res_model: 'partner'},
],
},
};
},
afterEach: function () {
@@ -144,7 +169,7 @@ QUnit.module('Chatter', {
});
QUnit.test('basic rendering', function (assert) {
assert.expect(8);
assert.expect(9);
var count = 0;
var unwanted_read_count = 0;
@@ -187,6 +212,7 @@ QUnit.test('basic rendering', function (assert) {
"there should be a followers widget, moved inside the chatter's topbar");
assert.ok(form.$('.o_chatter').length, "there should be a chatter widget");
assert.ok(form.$('.o_mail_thread').length, "there should be a mail thread");
assert.strictEqual(form.$('.o_chatter_button_attachment').length, 1, "should have one attachment button");
assert.ok(!form.$('.o_chatter_topbar .o_chatter_button_log_note').length,
"log note button should not be available");
@@ -196,6 +222,53 @@ QUnit.test('basic rendering', function (assert) {
form.destroy();
});
QUnit.test('attachmentBox basic rendering', function (assert) {
assert.expect(11);
this.data.partner.records.push({
id: 7,
display_name: "attachment_test",
});
var form = createView({
View: FormView,
model: 'partner',
data: this.data,
services: this.services,
arch: '<form string="Partners">' +
'<sheet>' +
'<field name="foo"/>' +
'</sheet>' +
'<div class="oe_chatter">' +
'</div>' +
'</form>',
res_id: 7,
});
var $button = form.$('.o_chatter_button_attachment');
assert.strictEqual($button.length, 1, "should have one attachment button");
$button.click();
assert.strictEqual(form.$('.o_mail_chatter_attachments').length, 1,
"attachment widget should exist after a first click on the button");
assert.strictEqual(form.$('.o_attachment_image').length, 1, "there should be an image preview");
assert.strictEqual(form.$('.o_attachments_previews').length, 1, "there should be a list of previews");
assert.strictEqual(form.$('.o_attachments_list').length, 1, "there should be a list of non previewable attachments");
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
// asserts both at the same time.
assert.strictEqual(form.$('a[title = "Download name2"]').text().trim(), 'name2txt',
"the attachment name should be correct");
assert.ok(form.$('.o_attachment_image').css('background-image').indexOf('/web/image/1/160x160/?crop=true') >= 0,
"the attachment image URL should be correct");
assert.strictEqual(form.$('.o_attachment_download').eq(0).attr('href'), '/web/content/1?download=true',
"the download URL of name1 must be correct");
assert.strictEqual(form.$('.o_attachment_download').eq(1).attr('href'), '/web/content/2?download=true',
"the download URL of name2 must be correct");
$button.click();
assert.strictEqual(form.$('.o_mail_chatter_attachments').length, 0,
"attachment widget should de destroyed after we reclick the button");
form.destroy();
});
QUnit.test('chatter in create mode', function (assert) {
assert.expect(9);
@@ -1496,7 +1569,7 @@ QUnit.test('form activity widget: mark as done and remove', function (assert) {
} else if (route === '/web/dataset/call_kw/partner/read') {
nbReads++;
if (nbReads === 1) { // first read
assert.strictEqual(args.args[1].length, 4, 'should read all fiels the first time');
assert.strictEqual(args.args[1].length, 5, 'should read all fiels the first time');
} else if (nbReads === 2) { // second read: after the unlink
assert.ok(_.isEqual(args.args[1], ['activity_ids', 'display_name']),
'should only read the activities (+ display_name) after an unlink');
+9 -5
View File
@@ -55,23 +55,26 @@
<script type="text/javascript" src="/mail/static/src/js/chatter.js"></script>
<script type="text/javascript" src="/mail/static/src/js/followers.js"></script>
<script type="text/javascript" src="/mail/static/src/js/form_controller.js"></script>
<script type="text/javascript" src="/mail/static/src/js/form_view.js"></script>
<script type="text/javascript" src="/mail/static/src/js/form_renderer.js"></script>
<script type="text/javascript" src="/mail/static/src/js/basic_model.js"></script>
<script type="text/javascript" src="/mail/static/src/js/basic_view.js"></script>
<script type="text/javascript" src="/mail/static/src/js/thread_field.js"></script>
<script type="text/javascript" src="/mail/static/src/js/thread_widget.js"></script>
<!-- systray -->
<script type="text/javascript" src="/mail/static/src/js/systray/systray_activity_menu.js"></script>
<script type="text/javascript" src="/mail/static/src/js/systray/systray_messaging_menu.js"></script>
<!-- systray -->
<script type="text/javascript" src="/mail/static/src/js/systray/systray_activity_menu.js"></script>
<script type="text/javascript" src="/mail/static/src/js/systray/systray_messaging_menu.js"></script>
<script type="text/javascript" src="/mail/static/src/js/tour.js"></script>
<!-- utils -->
<script type="text/javascript" src="/mail/static/src/js/utils.js"></script>
<!-- utils -->
<script type="text/javascript" src="/mail/static/src/js/utils.js"></script>
<script type="text/javascript" src="/mail/static/src/js/activity.js"></script>
<!-- Activity view type -->
<script type="text/javascript" src="/mail/static/src/js/views/activity/activity_view.js"></script>
<script type="text/javascript" src="/mail/static/src/js/views/activity/activity_model.js"></script>
<script type="text/javascript" src="/mail/static/src/js/views/activity/activity_controller.js"></script>
<script type="text/javascript" src="/mail/static/src/js/views/activity/activity_renderer.js"></script>
<!-- Attachment box -->
<script type="text/javascript" src="/mail/static/src/js/attachment_box.js"></script>
<!-- Others -->
<script type="text/javascript" src="/mail/static/src/js/emojis.js"></script>
@@ -87,6 +90,7 @@
<link rel="stylesheet" type="text/scss" href="/mail/static/src/scss/mail_activity.scss"/>
<link rel="stylesheet" type="text/scss" href="/mail/static/src/scss/activity_view.scss"/>
<link rel="stylesheet" type="text/scss" href="/mail/static/src/scss/kanban_view.scss"/>
<link rel="stylesheet" type="text/scss" href="/mail/static/src/scss/attachment_box.scss"/>
</xpath>
</template>