From 7f97c9fc056a0949c61c8444ee20f4c95bb3e9a9 Mon Sep 17 00:00:00 2001 From: ThanhDodeurOdoo Date: Thu, 31 May 2018 16:04:06 +0200 Subject: [PATCH] [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 --- addons/mail/models/mail_thread.py | 7 ++ addons/mail/static/src/js/attachment_box.js | 99 +++++++++++++++++++ addons/mail/static/src/js/chatter.js | 62 +++++++++++- addons/mail/static/src/js/form_renderer.js | 3 +- addons/mail/static/src/js/form_view.js | 25 +++++ .../mail/static/src/scss/attachment_box.scss | 26 +++++ addons/mail/static/src/scss/chatter.scss | 22 +++-- addons/mail/static/src/xml/chatter.xml | 39 +++++++- addons/mail/static/tests/chatter_tests.js | 79 ++++++++++++++- addons/mail/views/mail_templates.xml | 14 ++- 10 files changed, 356 insertions(+), 20 deletions(-) create mode 100644 addons/mail/static/src/js/attachment_box.js create mode 100644 addons/mail/static/src/js/form_view.js create mode 100644 addons/mail/static/src/scss/attachment_box.scss diff --git a/addons/mail/models/mail_thread.py b/addons/mail/models/mail_thread.py index d730d039050..a2c7c543f54 100644 --- a/addons/mail/models/mail_thread.py +++ b/addons/mail/models/mail_thread.py @@ -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): diff --git a/addons/mail/static/src/js/attachment_box.js b/addons/mail/static/src/js/attachment_box.js new file mode 100644 index 00000000000..b42e8aeca95 --- /dev/null +++ b/addons/mail/static/src/js/attachment_box.js @@ -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; + +}); diff --git a/addons/mail/static/src/js/chatter.js b/addons/mail/static/src/js/chatter.js index ea0f3dd8e4b..0814723caf0 100644 --- a/addons/mail/static/src/js/chatter.js +++ b/addons/mail/static/src/js/chatter.js @@ -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', $('
')); 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. diff --git a/addons/mail/static/src/js/form_renderer.js b/addons/mail/static/src/js/form_renderer.js index 47a2b6b795e..e01bdf18633 100644 --- a/addons/mail/static/src/js/form_renderer.js +++ b/addons/mail/static/src/js/form_renderer.js @@ -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); } diff --git a/addons/mail/static/src/js/form_view.js b/addons/mail/static/src/js/form_view.js new file mode 100644 index 00000000000..2eaed79e8c0 --- /dev/null +++ b/addons/mail/static/src/js/form_view.js @@ -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 = {}; + } + }, +}); + +}); diff --git a/addons/mail/static/src/scss/attachment_box.scss b/addons/mail/static/src/scss/attachment_box.scss new file mode 100644 index 00000000000..ab412bf52fe --- /dev/null +++ b/addons/mail/static/src/scss/attachment_box.scss @@ -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; + } + } + } +} diff --git a/addons/mail/static/src/scss/chatter.scss b/addons/mail/static/src/scss/chatter.scss index 01a781a29d4..261552c2b23 100644 --- a/addons/mail/static/src/scss/chatter.scss +++ b/addons/mail/static/src/scss/chatter.scss @@ -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'); diff --git a/addons/mail/static/src/xml/chatter.xml b/addons/mail/static/src/xml/chatter.xml index f78f60791e0..7881f3fd49c 100644 --- a/addons/mail/static/src/xml/chatter.xml +++ b/addons/mail/static/src/xml/chatter.xml @@ -1,11 +1,34 @@ + + +
+
+
+ Attachments +
+
+ + + +
+
+ + + +
+
+
+ + @@ -69,6 +92,18 @@ + + + + + diff --git a/addons/mail/static/tests/chatter_tests.js b/addons/mail/static/tests/chatter_tests.js index 37fb75572c3..4e1c782a715 100644 --- a/addons/mail/static/tests/chatter_tests.js +++ b/addons/mail/static/tests/chatter_tests.js @@ -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: '
' + + '' + + '' + + '' + + '
' + + '
' + + '
', + 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'); diff --git a/addons/mail/views/mail_templates.xml b/addons/mail/views/mail_templates.xml index 7e116200f43..6a44fb77f4f 100644 --- a/addons/mail/views/mail_templates.xml +++ b/addons/mail/views/mail_templates.xml @@ -55,23 +55,26 @@ + - - - + + + - - + + + + @@ -87,6 +90,7 @@ +