diff --git a/addons/mail/static/src/components/attachment/attachment.js b/addons/mail/static/src/components/attachment/attachment.js index a17ed341b9a..9d64324d9b7 100644 --- a/addons/mail/static/src/components/attachment/attachment.js +++ b/addons/mail/static/src/components/attachment/attachment.js @@ -3,7 +3,11 @@ odoo.define('mail/static/src/components/attachment/attachment.js', function (req const useStore = require('mail/static/src/component_hooks/use_store/use_store.js'); -const { Component } = owl; +const components = { + AttachmentDeleteConfirmDialog: require('mail/static/src/components/attachment_delete_confirm_dialog/attachment_delete_confirm_dialog.js'), +}; + +const { Component, useState } = owl; class Attachment extends Component { @@ -18,6 +22,9 @@ class Attachment extends Component { attachment: attachment ? attachment.__state : undefined, }; }); + this.state = useState({ + hasDeleteConfirmDialog: false, + }); } //-------------------------------------------------------------------------- @@ -125,13 +132,24 @@ class Attachment extends Component { */ _onClickUnlink(ev) { ev.stopPropagation(); - this.attachment.remove(); - this.trigger('o-attachment-removed'); + if (this.attachment.isLinkedToComposer) { + this.attachment.remove(); + this.trigger('o-attachment-removed', { attachmentLocalId: this.props.attachmentLocalId }); + } else { + this.state.hasDeleteConfirmDialog = true; + } } + /** + * @private + */ + _onDeleteConfirmDialogClosed() { + this.state.hasDeleteConfirmDialog = false; + } } Object.assign(Attachment, { + components, defaultProps: { attachmentLocalIds: [], detailsMode: 'auto', diff --git a/addons/mail/static/src/components/attachment/attachment.xml b/addons/mail/static/src/components/attachment/attachment.xml index 7be59392838..96e96b407ee 100644 --- a/addons/mail/static/src/components/attachment/attachment.xml +++ b/addons/mail/static/src/components/attachment/attachment.xml @@ -102,6 +102,12 @@ + + + diff --git a/addons/mail/static/src/components/attachment_box/attachment_box_tests.js b/addons/mail/static/src/components/attachment_box/attachment_box_tests.js index 9330a8c955b..6f217c2b5d7 100644 --- a/addons/mail/static/src/components/attachment_box/attachment_box_tests.js +++ b/addons/mail/static/src/components/attachment_box/attachment_box_tests.js @@ -284,6 +284,54 @@ QUnit.test('view attachments', async function (assert) { ); }); +QUnit.test('remove attachment should ask for confirmation', async function (assert) { + assert.expect(5); + + await this.start(); + const thread = this.env.models['mail.thread'].create({ + attachments: [ + ['insert', { + id: 143, + mimetype: 'text/plain', + name: 'Blah.txt' + }], + ], + id: 100, + model: 'res.partner', + }); + await this.createAttachmentBoxComponent(thread); + assert.containsOnce( + document.body, + '.o_Attachment', + "should have an attachment", + ); + assert.containsOnce( + document.body, + '.o_Attachment_actionUnlink', + "attachment should have a delete button" + ); + + await afterNextRender(() => document.querySelector('.o_Attachment_actionUnlink').click()); + assert.containsOnce( + document.body, + '.o_AttachmentDeleteConfirmDialog', + "A confirmation dialog should have been opened" + ); + assert.strictEqual( + document.querySelector('.o_AttachmentDeleteConfirmDialog_mainText').textContent, + `Do you really want to delete "Blah.txt"?`, + "Confirmation dialog should contain the attachment delete confirmation text" + ); + + // Confirm the deletion + await afterNextRender(() => document.querySelector('.o_AttachmentDeleteConfirmDialog_confirmButton').click()); + assert.containsNone( + document.body, + '.o_Attachment', + "should no longer have an attachment", + ); +}); + }); }); }); diff --git a/addons/mail/static/src/components/attachment_delete_confirm_dialog/attachment_delete_confirm_dialog.js b/addons/mail/static/src/components/attachment_delete_confirm_dialog/attachment_delete_confirm_dialog.js index 3b4527b3677..46fa1c65260 100644 --- a/addons/mail/static/src/components/attachment_delete_confirm_dialog/attachment_delete_confirm_dialog.js +++ b/addons/mail/static/src/components/attachment_delete_confirm_dialog/attachment_delete_confirm_dialog.js @@ -1,6 +1,90 @@ odoo.define('mail/static/src/components/attachment_delete_confirm_dialog/attachment_delete_confirm_dialog.js', function (require) { 'use strict'; -// TODO FIXME file to be completed in task-2282322 +const useStore = require('mail/static/src/component_hooks/use_store/use_store.js'); + +const components = { + Dialog: require('web.OwlDialog'), +}; + +const { Component } = owl; +const { useRef } = owl.hooks; + +class AttachmentDeleteConfirmDialog extends Component { + + /** + * @override + */ + constructor(...args) { + super(...args); + useStore(props => { + const attachment = this.env.models['mail.attachment'].get(props.attachmentLocalId); + return { + attachment: attachment ? attachment.__state : undefined, + }; + }); + // to manually trigger the dialog close event + this._dialogRef = useRef('dialog'); + } + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + /** + * @returns {mail.attachment} + */ + get attachment() { + return this.env.models['mail.attachment'].get(this.props.attachmentLocalId); + } + + /** + * @returns {string} + */ + getBody() { + return _.str.sprintf( + this.env._t(`Do you really want to delete "%s"?`), + owl.utils.escape(this.attachment.displayName) + ); + } + + /** + * @returns {string} + */ + getTitle() { + return this.env._t("Confirmation"); + } + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * @private + */ + _onClickCancel() { + this._dialogRef.comp._close(); + } + + /** + * @private + */ + _onClickOk() { + this._dialogRef.comp._close(); + this.attachment.remove(); + this.trigger('o-attachment-removed', { attachmentLocalId: this.props.attachmentLocalId }); + } + +} + +Object.assign(AttachmentDeleteConfirmDialog, { + components, + props: { + attachmentLocalId: String, + }, + template: 'mail.AttachmentDeleteConfirmDialog', +}); + +return AttachmentDeleteConfirmDialog; }); diff --git a/addons/mail/static/src/components/attachment_delete_confirm_dialog/attachment_delete_confirm_dialog.xml b/addons/mail/static/src/components/attachment_delete_confirm_dialog/attachment_delete_confirm_dialog.xml index c98c47daf1b..6b466a9ba2d 100644 --- a/addons/mail/static/src/components/attachment_delete_confirm_dialog/attachment_delete_confirm_dialog.xml +++ b/addons/mail/static/src/components/attachment_delete_confirm_dialog/attachment_delete_confirm_dialog.xml @@ -1,4 +1,12 @@ - + + + + + Ok + Cancel + + + diff --git a/addons/mail/static/src/components/chat_window_manager/chat_window_manager_tests.js b/addons/mail/static/src/components/chat_window_manager/chat_window_manager_tests.js index 1a261265dc7..bf9db6c0276 100644 --- a/addons/mail/static/src/components/chat_window_manager/chat_window_manager_tests.js +++ b/addons/mail/static/src/components/chat_window_manager/chat_window_manager_tests.js @@ -802,9 +802,8 @@ QUnit.test('[technical] chat window: scroll conservation on toggle home menu', a this.data['mail.channel'].records.push({ id: 20 }); for (let i = 0; i < 10; i++) { this.data['mail.message'].records.push({ + body: "not empty", channel_ids: [20], - model: 'mail.channel', - res_id: 20, }); } await this.start(); @@ -1543,9 +1542,8 @@ QUnit.test('chat window with a thread: keep scroll position in message list on f this.data['mail.channel'].records.push({ id: 20 }); for (let i = 0; i < 10; i++) { this.data['mail.message'].records.push({ + body: "not empty", channel_ids: [20], - model: 'mail.channel', - res_id: 20, }); } await this.start(); @@ -1588,9 +1586,8 @@ QUnit.test('chat window should scroll to the newly posted message just after pos }); for (let i = 0; i < 10; i++) { this.data['mail.message'].records.push({ + body: "not empty", channel_ids: [20], - model: 'mail.channel', - res_id: 20, }); } await this.start(); @@ -1710,9 +1707,8 @@ QUnit.test('[technical] chat window with a thread: keep scroll position in messa this.data['mail.channel'].records.push({ id: 20 }); for (let i = 0; i < 10; i++) { this.data['mail.message'].records.push({ + body: "not empty", channel_ids: [20], - model: 'mail.channel', - res_id: 20, }); } await this.start(); diff --git a/addons/mail/static/src/components/chatter/chatter_tests.js b/addons/mail/static/src/components/chatter/chatter_tests.js index 95abab26f43..9057f0684ab 100644 --- a/addons/mail/static/src/components/chatter/chatter_tests.js +++ b/addons/mail/static/src/components/chatter/chatter_tests.js @@ -47,6 +47,7 @@ QUnit.test('base rendering when chatter has no attachment', async function (asse this.data['res.partner'].records.push({ id: 100 }); for (let i = 0; i < 60; i++) { this.data['mail.message'].records.push({ + body: "not empty", model: 'res.partner', res_id: 100, }); diff --git a/addons/mail/static/src/components/composer/composer_tests.js b/addons/mail/static/src/components/composer/composer_tests.js index d37162ffa8d..4910dfb7c43 100644 --- a/addons/mail/static/src/components/composer/composer_tests.js +++ b/addons/mail/static/src/components/composer/composer_tests.js @@ -1746,6 +1746,44 @@ QUnit.test('warning on send with shortcut when attempting to post message with s ); }); +QUnit.test('remove an attachment from composer does not need any confirmation', async function (assert) { + assert.expect(3); + + await this.start(); + const composer = this.env.models['mail.composer'].create(); + await this.createComposerComponent(composer); + const file = await createFile({ + content: 'hello, world', + contentType: 'text/plain', + name: 'text.txt', + }); + await afterNextRender(() => + inputFiles( + document.querySelector('.o_FileUploader_input'), + [file] + ) + ); + assert.containsOnce( + document.body, + '.o_Composer_attachmentList', + "should have an attachment list" + ); + assert.containsOnce( + document.body, + '.o_Composer .o_Attachment', + "should have only one attachment" + ); + + await afterNextRender(() => + document.querySelector('.o_Attachment_asideItemUnlink').click() + ); + assert.containsNone( + document.body, + '.o_Composer .o_Attachment', + "should not have any attachment left after unlinking the only one" + ); +}); + QUnit.test('remove an uploading attachment', async function (assert) { assert.expect(4); diff --git a/addons/mail/static/src/components/discuss/tests/discuss_inbox_tests.js b/addons/mail/static/src/components/discuss/tests/discuss_inbox_tests.js index 00cd5d2c310..d547065f44d 100644 --- a/addons/mail/static/src/components/discuss/tests/discuss_inbox_tests.js +++ b/addons/mail/static/src/components/discuss/tests/discuss_inbox_tests.js @@ -44,6 +44,7 @@ QUnit.test('reply: discard on pressing escape', async function (assert) { }); // message expected to be found in inbox this.data['mail.message'].records.push({ + body: "not empty", model: 'res.partner', needaction: true, needaction_partner_ids: [this.data.currentPartnerId], @@ -143,6 +144,7 @@ QUnit.test('reply: discard on discard button click', async function (assert) { assert.expect(4); this.data['mail.message'].records.push({ + body: "not empty", model: 'res.partner', needaction: true, needaction_partner_ids: [this.data.currentPartnerId], @@ -183,6 +185,7 @@ QUnit.test('reply: discard on reply button toggle', async function (assert) { assert.expect(3); this.data['mail.message'].records.push({ + body: "not empty", model: 'res.partner', needaction: true, needaction_partner_ids: [this.data.currentPartnerId], @@ -218,6 +221,7 @@ QUnit.test('reply: discard on click away', async function (assert) { assert.expect(7); this.data['mail.message'].records.push({ + body: "not empty", model: 'res.partner', needaction: true, needaction_partner_ids: [this.data.currentPartnerId], @@ -284,6 +288,7 @@ QUnit.test('"reply to" composer should log note if message replied to is a note' assert.expect(6); this.data['mail.message'].records.push({ + body: "not empty", is_discussion: false, model: 'res.partner', needaction: true, @@ -336,6 +341,7 @@ QUnit.test('"reply to" composer should send message if message replied to is not assert.expect(6); this.data['mail.message'].records.push({ + body: "not empty", is_discussion: true, model: 'res.partner', needaction: true, @@ -388,6 +394,7 @@ QUnit.test('error notifications should not be shown in Inbox', async function (a assert.expect(3); this.data['mail.message'].records.push({ + body: "not empty", id: 100, model: 'mail.channel', needaction: true, @@ -422,6 +429,7 @@ QUnit.test('show subject of message in Inbox', async function (assert) { assert.expect(3); this.data['mail.message'].records.push({ + body: "not empty", model: 'mail.channel', // random existing model needaction: true, // message_fetch domain needaction_partner_ids: [this.data.currentPartnerId], // not needed, for consistency @@ -449,6 +457,7 @@ QUnit.test('show subject of message in history', async function (assert) { assert.expect(3); this.data['mail.message'].records.push({ + body: "not empty", history_partner_ids: [3], // not needed, for consistency model: 'mail.channel', // random existing model subject: "Salutations, voyageur", // will be asserted in the test @@ -509,6 +518,7 @@ QUnit.test('click on (non-channel/non-partner) origin thread link should redirec }); this.data['some.model'] = { fields: {}, records: [{ id: 10 }] }; this.data['mail.message'].records.push({ + body: "not empty", model: 'some.model', needaction: true, needaction_partner_ids: [this.data.currentPartnerId], diff --git a/addons/mail/static/src/components/discuss/tests/discuss_moderation_tests.js b/addons/mail/static/src/components/discuss/tests/discuss_moderation_tests.js index 7f1df69628c..04ca8b08e18 100644 --- a/addons/mail/static/src/components/discuss/tests/discuss_moderation_tests.js +++ b/addons/mail/static/src/components/discuss/tests/discuss_moderation_tests.js @@ -747,6 +747,7 @@ QUnit.test('as author, sent message accepted in moderated channel', async functi name: "general", // random name, will be asserted in the test }); this.data['mail.message'].records.push({ + body: "not empty", id: 100, // random unique id, will be referenced in the test model: 'mail.channel', // expected value to link message to channel moderation_status: 'pending_moderation', // message is expected to be pending @@ -820,6 +821,7 @@ QUnit.test('as author, sent message rejected in moderated channel', async functi name: "general", // random name, will be asserted in the test }); this.data['mail.message'].records.push({ + body: "not empty", id: 100, // random unique id, will be referenced in the test model: 'mail.channel', // expected value to link message to channel moderation_status: 'pending_moderation', // message is expected to be pending @@ -884,6 +886,7 @@ QUnit.test('as moderator, pending moderation message accessibility', async funct moderation: true, // channel must be moderated to test the feature }); this.data['mail.message'].records.push({ + body: "not empty", id: 100, // random unique id, will be referenced in the test model: 'mail.channel', // expected value to link message to channel moderation_status: 'pending_moderation', // message is expected to be pending @@ -936,6 +939,7 @@ QUnit.test('as author, pending moderation message should appear in origin thread }); this.data['mail.message'].records.push({ author_id: this.data.currentPartnerId, // test as author of message + body: "not empty", id: 100, // random unique id, will be referenced in the test model: 'mail.channel', // expected value to link message to channel moderation_status: 'pending_moderation', // message is expected to be pending @@ -983,6 +987,7 @@ QUnit.test('as moderator, new pending moderation message posted by someone else' // simulate receiving the message const messageData = { author_id: 10, // random id, different than current partner + body: "not empty", channel_ids: [], // server do NOT return channel_id of the message if pending moderation id: 1, // random unique id model: 'mail.channel', // expected value to link message to channel @@ -1027,16 +1032,19 @@ QUnit.test('accept multiple moderation messages', async function (assert) { }); this.data['mail.message'].records.push( { + body: "not empty", model: 'mail.channel', moderation_status: 'pending_moderation', res_id: 20, }, { + body: "not empty", model: 'mail.channel', moderation_status: 'pending_moderation', res_id: 20, }, { + body: "not empty", model: 'mail.channel', moderation_status: 'pending_moderation', res_id: 20, @@ -1100,16 +1108,19 @@ QUnit.test('accept multiple moderation messages after having accepted other mess }); this.data['mail.message'].records.push( { + body: "not empty", model: 'mail.channel', moderation_status: 'pending_moderation', res_id: 20, }, { + body: "not empty", model: 'mail.channel', moderation_status: 'pending_moderation', res_id: 20, }, { + body: "not empty", model: 'mail.channel', moderation_status: 'pending_moderation', res_id: 20, diff --git a/addons/mail/static/src/components/discuss/tests/discuss_tests.js b/addons/mail/static/src/components/discuss/tests/discuss_tests.js index 67d52c33c88..989bb75235e 100644 --- a/addons/mail/static/src/components/discuss/tests/discuss_tests.js +++ b/addons/mail/static/src/components/discuss/tests/discuss_tests.js @@ -513,6 +513,7 @@ QUnit.test('sidebar: channel rendering with needaction counter', async function this.data['mail.channel'].records.push({ id: 20 }); // expected needaction message this.data['mail.message'].records.push({ + body: "not empty", channel_ids: [20], // link message to channel id: 100, // random unique id, useful to link notification }); @@ -1186,6 +1187,7 @@ QUnit.test('load single message from channel initially', async function (assert) // channel expected to be rendered, with a random unique id that will be referenced in the test this.data['mail.channel'].records.push({ id: 20 }); this.data['mail.message'].records.push({ + body: "not empty", channel_ids: [20], date: "2019-04-20 10:00:00", id: 100, @@ -1554,6 +1556,7 @@ QUnit.test('load all messages from channel initially, less than fetch limit (29 this.data['res.partner'].records.push({ id: 11 }); for (let i = 28; i >= 0; i--) { this.data['mail.message'].records.push({ + body: "not empty", channel_ids: [20], date: "2019-04-20 10:00:00", model: 'mail.channel', @@ -1613,6 +1616,7 @@ QUnit.test('load more messages from channel', async function (assert) { this.data['res.partner'].records.push({ id: 11 }); for (let i = 0; i < 40; i++) { this.data['mail.message'].records.push({ + body: "not empty", channel_ids: [20], date: "2019-04-20 10:00:00", model: 'mail.channel', @@ -1684,6 +1688,7 @@ QUnit.test('auto-scroll to bottom of thread', async function (assert) { this.data['mail.channel'].records.push({ id: 20 }); for (let i = 1; i <= 25; i++) { this.data['mail.message'].records.push({ + body: "not empty", channel_ids: [20], model: 'mail.channel', res_id: 20, @@ -1719,6 +1724,7 @@ QUnit.test('load more messages from channel (auto-load on scroll)', async functi this.data['mail.channel'].records.push({ id: 20 }); for (let i = 0; i < 40; i++) { this.data['mail.message'].records.push({ + body: "not empty", channel_ids: [20], model: 'mail.channel', res_id: 20, @@ -1784,6 +1790,7 @@ QUnit.test('new messages separator [REQUIRE FOCUS]', async function (assert) { }); for (let i = 1; i <= 25; i++) { this.data['mail.message'].records.push({ + body: "not empty", channel_ids: [20], id: 100 + i, // for setting proper value for seen_message_id model: 'mail.channel', @@ -1886,6 +1893,7 @@ QUnit.test('restore thread scroll position', async function (assert) { this.data['mail.channel'].records.push({ id: 11 }, { id: 12 }); for (let i = 1; i <= 25; i++) { this.data['mail.message'].records.push({ + body: "not empty", channel_ids: [11], model: 'mail.channel', res_id: 11, @@ -1893,6 +1901,7 @@ QUnit.test('restore thread scroll position', async function (assert) { } for (let i = 1; i <= 24; i++) { this.data['mail.message'].records.push({ + body: "not empty", channel_ids: [12], model: 'mail.channel', res_id: 12, @@ -2036,6 +2045,7 @@ QUnit.test('message origin redirect to channel', async function (assert) { this.data['mail.channel'].records.push({ id: 11 }, { id: 12 }); this.data['mail.message'].records.push( { + body: "not empty", channel_ids: [11, 12], id: 100, model: 'mail.channel', @@ -2043,6 +2053,7 @@ QUnit.test('message origin redirect to channel', async function (assert) { res_id: 11, }, { + body: "not empty", channel_ids: [11, 12], id: 101, model: 'mail.channel', @@ -2245,6 +2256,7 @@ QUnit.test('redirect to author (open chat)', async function (assert) { this.data['mail.message'].records.push( { author_id: 7, + body: "not empty", channel_ids: [1], id: 100, model: 'mail.channel', @@ -2483,6 +2495,7 @@ QUnit.test('inbox: mark all messages as read', async function (assert) { this.data['mail.message'].records.push( // first expected message { + body: "not empty", channel_ids: [20], // link message to channel id: 100, // random unique id, useful to link notification // needaction needs to be set here for message_fetch domain, because @@ -2491,6 +2504,7 @@ QUnit.test('inbox: mark all messages as read', async function (assert) { }, // second expected message { + body: "not empty", channel_ids: [20], // link message to channel id: 101, // random unique id, useful to link notification // needaction needs to be set here for message_fetch domain, because @@ -2586,8 +2600,8 @@ QUnit.test('starred: unstar all', async function (assert) { // messages expected to be starred this.data['mail.message'].records.push( - { starred_partner_ids: [this.data.currentPartnerId] }, - { starred_partner_ids: [this.data.currentPartnerId] } + { body: "not empty", starred_partner_ids: [this.data.currentPartnerId] }, + { body: "not empty", starred_partner_ids: [this.data.currentPartnerId] } ); await this.start({ discuss: { @@ -2647,6 +2661,7 @@ QUnit.test('toggle_star message', async function (assert) { // with a random unique id, will be referenced in the test this.data['mail.channel'].records.push({ id: 20 }); this.data['mail.message'].records.push({ + body: "not empty", channel_ids: [20], id: 100, model: 'mail.channel', @@ -3176,6 +3191,7 @@ QUnit.test('rendering of inbox message', async function (assert) { assert.expect(7); this.data['mail.message'].records.push({ + body: "not empty", model: 'res.partner', // random existing model needaction: true, // for message_fetch domain needaction_partner_ids: [this.data.currentPartnerId], // for consistency @@ -3228,6 +3244,7 @@ QUnit.test('mark channel as seen on last message visible [REQUIRE FOCUS]', async // and a random unique id that will be referenced in the test this.data['mail.channel'].records.push({ id: 10, message_unread_counter: 1 }); this.data['mail.message'].records.push({ + body: "not empty", channel_ids: [10], model: 'mail.channel', res_id: 10, @@ -3318,6 +3335,7 @@ QUnit.test('receive new needaction messages', async function (assert) { // simulate receiving a new needaction message await afterNextRender(() => { const data = { + body: "not empty", id: 100, needaction_partner_ids: [3], model: 'res.partner', @@ -3359,6 +3377,7 @@ QUnit.test('receive new needaction messages', async function (assert) { // simulate receiving another new needaction message await afterNextRender(() => { const data2 = { + body: "not empty", id: 101, needaction_partner_ids: [3], model: 'res.partner', @@ -3533,6 +3552,7 @@ QUnit.test('load recent messages from thread (already loaded some old messages)' this.data['mail.channel'].records.push({ id: 20 }); for (let i = 0; i < 50; i++) { this.data['mail.message'].records.push({ + body: "not empty", channel_ids: [20], // id of related channel id: 100 + i, // random unique id, will be referenced in the test model: 'mail.channel', // expected value to link message to channel @@ -3610,6 +3630,7 @@ QUnit.test('messages marked as read move to "History" mailbox', async function ( // expected messages this.data['mail.message'].records.push( { + body: "not empty", id: 100, // random unique id, useful to link notification model: 'mail.channel', // value to link message to channel // needaction needs to be set here for message_fetch domain, because @@ -3618,6 +3639,7 @@ QUnit.test('messages marked as read move to "History" mailbox', async function ( res_id: 20, // id of related channel }, { + body: "not empty", id: 101, // random unique id, useful to link notification model: 'mail.channel', // value to link message to channel // needaction needs to be set here for message_fetch domain, because @@ -3734,11 +3756,13 @@ QUnit.test('mark a single message as read should only move this message to "Hist this.data['mail.message'].records.push( { + body: "not empty", id: 1, needaction: true, needaction_partner_ids: [this.data.currentPartnerId], }, { + body: "not empty", id: 2, needaction: true, needaction_partner_ids: [this.data.currentPartnerId], @@ -3856,6 +3880,7 @@ QUnit.test('all messages in "Inbox" in "History" after marked all as read', asyn for (let id = messageOffset; id < messageOffset + 40; id++) { // message expected to be found in Inbox this.data['mail.message'].records.push({ + body: "not empty", id, // will be used to link notification to message // needaction needs to be set here for message_fetch domain, because // mocked models don't have computed fields diff --git a/addons/mail/static/src/components/message/message_tests.js b/addons/mail/static/src/components/message/message_tests.js index bf27d71fdb5..a880169967b 100644 --- a/addons/mail/static/src/components/message/message_tests.js +++ b/addons/mail/static/src/components/message/message_tests.js @@ -708,7 +708,7 @@ QUnit.test('only show messaging seen indicator if authored by me, after last see }); QUnit.test('allow attachment delete on authored message', async function (assert) { - assert.expect(3); + assert.expect(5); await this.start(); const message = this.env.models['mail.message'].create({ @@ -735,6 +735,20 @@ QUnit.test('allow attachment delete on authored message', async function (assert ); await afterNextRender(() => document.querySelector('.o_Attachment_asideItemUnlink').click()); + assert.containsOnce( + document.body, + '.o_AttachmentDeleteConfirmDialog', + "An attachment delete confirmation dialog should have been opened" + ); + assert.strictEqual( + document.querySelector('.o_AttachmentDeleteConfirmDialog_mainText').textContent, + `Do you really want to delete "BLAH"?`, + "Confirmation dialog should contain the attachment delete confirmation text" + ); + + await afterNextRender(() => + document.querySelector('.o_AttachmentDeleteConfirmDialog_confirmButton').click() + ); assert.containsNone( document.body, '.o_Attachment', diff --git a/addons/mail/static/src/components/message_list/message_list.js b/addons/mail/static/src/components/message_list/message_list.js index b7332514141..708af154cb8 100644 --- a/addons/mail/static/src/components/message_list/message_list.js +++ b/addons/mail/static/src/components/message_list/message_list.js @@ -107,7 +107,7 @@ class MessageList extends Component { * Because of this, the scroll position must be changed when whole UI * is rendered. To make this simpler, this is done when * component is patched. This is acceptable when has a - * fixed height, which is the case for the moment. + * fixed height, which is the case for the moment. task-2358066 */ async adjustFromComponentHints() { if (!this.threadView) { @@ -418,9 +418,6 @@ class MessageList extends Component { if (this.props.order === 'asc' && this.props.hasScrollAdjust) { this.el.scrollTop = this.el.scrollHeight - scrollHeight + scrollTop; } - this.env.messagingBus.trigger('o-component-message-list-more-messages-loaded', { - threadViewer: this.threadView.threadViewer, - }); this.threadView.markComponentHintProcessed(hint); } diff --git a/addons/mail/static/src/components/message_list/message_list.xml b/addons/mail/static/src/components/message_list/message_list.xml index bff87fba3d2..4f8670a64d1 100644 --- a/addons/mail/static/src/components/message_list/message_list.xml +++ b/addons/mail/static/src/components/message_list/message_list.xml @@ -49,36 +49,38 @@ - - - - - - - - - - - - - - - - New messages - + + + + + + + + + + + + + + + + + New messages + + diff --git a/addons/mail/static/src/components/moderation_ban_dialog/moderation_ban_dialog.js b/addons/mail/static/src/components/moderation_ban_dialog/moderation_ban_dialog.js index 2880c1747bb..c7a5ed71a65 100644 --- a/addons/mail/static/src/components/moderation_ban_dialog/moderation_ban_dialog.js +++ b/addons/mail/static/src/components/moderation_ban_dialog/moderation_ban_dialog.js @@ -3,7 +3,9 @@ odoo.define('mail/static/src/components/moderation_ban_dialog/moderation_ban_dia const useStore = require('mail/static/src/component_hooks/use_store/use_store.js'); -const Dialog = require('web.OwlDialog'); +const components = { + Dialog: require('web.OwlDialog'), +}; const { Component } = owl; const { useRef } = owl.hooks; @@ -71,7 +73,7 @@ class ModerationBanDialog extends Component { } Object.assign(ModerationBanDialog, { - components: { Dialog }, + components, props: { messageLocalIds: { type: Array, diff --git a/addons/mail/static/src/components/moderation_discard_dialog/moderation_discard_dialog.js b/addons/mail/static/src/components/moderation_discard_dialog/moderation_discard_dialog.js index 5f6a906df46..87e4ec84f94 100644 --- a/addons/mail/static/src/components/moderation_discard_dialog/moderation_discard_dialog.js +++ b/addons/mail/static/src/components/moderation_discard_dialog/moderation_discard_dialog.js @@ -3,7 +3,9 @@ odoo.define('mail/static/src/components/moderation_discard_dialog/moderation_dis const useStore = require('mail/static/src/component_hooks/use_store/use_store.js'); -const Dialog = require('web.OwlDialog'); +const components = { + Dialog: require('web.OwlDialog'), +}; const { Component } = owl; const { useRef } = owl.hooks; @@ -38,7 +40,7 @@ class ModerationDiscardDialog extends Component { /** * @returns {string} */ - getText() { + getBody() { if (this.messages.length === 1) { return this.env._t("You are going to discard 1 message."); } @@ -60,7 +62,7 @@ class ModerationDiscardDialog extends Component { /** * @returns {string} */ - get CONFIRMATION() { + getTitle() { return this.env._t("Confirmation"); } @@ -86,7 +88,7 @@ class ModerationDiscardDialog extends Component { } Object.assign(ModerationDiscardDialog, { - components: { Dialog }, + components, props: { messageLocalIds: { type: Array, diff --git a/addons/mail/static/src/components/moderation_discard_dialog/moderation_discard_dialog.xml b/addons/mail/static/src/components/moderation_discard_dialog/moderation_discard_dialog.xml index 1ff759bc53a..58dbc14dc5c 100644 --- a/addons/mail/static/src/components/moderation_discard_dialog/moderation_discard_dialog.xml +++ b/addons/mail/static/src/components/moderation_discard_dialog/moderation_discard_dialog.xml @@ -1,8 +1,8 @@ - - + + Do you confirm the action? Discard diff --git a/addons/mail/static/src/components/moderation_reject_dialog/moderation_reject_dialog.js b/addons/mail/static/src/components/moderation_reject_dialog/moderation_reject_dialog.js index 675e3e48d8e..e488aea4ea7 100644 --- a/addons/mail/static/src/components/moderation_reject_dialog/moderation_reject_dialog.js +++ b/addons/mail/static/src/components/moderation_reject_dialog/moderation_reject_dialog.js @@ -3,7 +3,9 @@ odoo.define('mail/static/src/components/moderation_reject_dialog/moderation_reje const useStore = require('mail/static/src/component_hooks/use_store/use_store.js'); -const Dialog = require('web.OwlDialog'); +const components = { + Dialog: require('web.OwlDialog'), +}; const { Component, useState } = owl; const { useRef } = owl.hooks; @@ -81,7 +83,7 @@ class ModerationRejectDialog extends Component { } Object.assign(ModerationRejectDialog, { - components: { Dialog }, + components, props: { messageLocalIds: { type: Array, diff --git a/addons/mail/static/src/components/thread_view/thread_view_tests.js b/addons/mail/static/src/components/thread_view/thread_view_tests.js index 2bf4c08a7a8..5230f252e03 100644 --- a/addons/mail/static/src/components/thread_view/thread_view_tests.js +++ b/addons/mail/static/src/components/thread_view/thread_view_tests.js @@ -101,6 +101,7 @@ QUnit.test('message list desc order', async function (assert) { for (let i = 0; i <= 60; i++) { this.data['mail.message'].records.push({ + body: "not empty", channel_ids: [100], model: 'mail.channel', res_id: 100, @@ -172,6 +173,7 @@ QUnit.test('message list asc order', async function (assert) { for (let i = 0; i <= 60; i++) { this.data['mail.message'].records.push({ + body: "not empty", channel_ids: [100], model: 'mail.channel', res_id: 100, @@ -389,6 +391,7 @@ QUnit.test('show message subject if thread is mailing channel', async function ( assert.expect(3); this.data['mail.message'].records.push({ + body: "not empty", channel_ids: [100], model: 'mail.channel', res_id: 100, @@ -717,6 +720,7 @@ QUnit.test('basic rendering of canceled notification', async function (assert) { this.data['mail.channel'].records.push({ id: 11 }); this.data['res.partner'].records.push({ id: 12, name: "Someone" }); this.data['mail.message'].records.push({ + body: "not empty", channel_ids: [11], id: 10, message_type: 'email', @@ -817,6 +821,7 @@ QUnit.test('should scroll to bottom on receiving new message if the list is init this.data['mail.channel'].records.push({ id: 20 }); for (let i = 0; i <= 10; i++) { this.data['mail.message'].records.push({ + body: "not empty", channel_ids: [20], }); } @@ -887,6 +892,7 @@ QUnit.test('should not scroll on receiving new message if the list is initially this.data['mail.channel'].records.push({ id: 20 }); for (let i = 0; i <= 10; i++) { this.data['mail.message'].records.push({ + body: "not empty", channel_ids: [20], }); } @@ -952,6 +958,186 @@ QUnit.test('should not scroll on receiving new message if the list is initially ); }); +QUnit.test("delete all attachments of message without content should no longer display the message", async function (assert) { + assert.expect(2); + + this.data['ir.attachment'].records.push({ + id: 143, + mimetype: 'text/plain', + name: "Blah.txt", + }); + this.data['mail.channel'].records.push({ id: 11 }); + this.data['mail.message'].records.push( + { + attachment_ids: [143], + channel_ids: [11], + id: 101, + } + ); + await this.start(); + const threadViewer = this.env.models['mail.thread_viewer'].create({ + hasThreadView: true, + thread: [['insert', { id: 11, model: 'mail.channel' }]], + }); + // wait for messages of the thread to be loaded + await this.afterEvent({ + eventName: 'o-thread-view-hint-processed', + func: () => { + this.createThreadViewComponent(threadViewer.threadView); + }, + message: "thread become loaded with messages", + predicate: ({ hint, threadViewer }) => { + return ( + hint.type === 'messages-loaded' && + threadViewer.thread.model === 'mail.channel' && + threadViewer.thread.id === 11 + ); + }, + }); + assert.containsOnce( + document.body, + '.o_Message', + "there should be 1 message displayed initially" + ); + + await afterNextRender(() => { + document.querySelector(`.o_Attachment[data-attachment-local-id="${ + this.env.models['mail.attachment'].findFromIdentifyingData({ id: 143 }).localId + }"] .o_Attachment_asideItemUnlink`).click(); + }); + await afterNextRender(() => + document.querySelector('.o_AttachmentDeleteConfirmDialog_confirmButton').click() + ); + assert.containsNone( + document.body, + '.o_Message', + "message should no longer be displayed after removing all its attachments (empty content)" + ); +}); + +QUnit.test('delete all attachments of a message with some text content should still keep it displayed', async function (assert) { + assert.expect(2); + + this.data['ir.attachment'].records.push({ + id: 143, + mimetype: 'text/plain', + name: "Blah.txt", + }); + this.data['mail.channel'].records.push({ id: 11 }); + this.data['mail.message'].records.push( + { + attachment_ids: [143], + body: "Some content", + channel_ids: [11], + id: 101, + }, + ); + await this.start(); + const threadViewer = this.env.models['mail.thread_viewer'].create({ + hasThreadView: true, + thread: [['insert', { id: 11, model: 'mail.channel' }]], + }); + // wait for messages of the thread to be loaded + await this.afterEvent({ + eventName: 'o-thread-view-hint-processed', + func: () => { + this.createThreadViewComponent(threadViewer.threadView); + }, + message: "thread become loaded with messages", + predicate: ({ hint, threadViewer }) => { + return ( + hint.type === 'messages-loaded' && + threadViewer.thread.model === 'mail.channel' && + threadViewer.thread.id === 11 + ); + }, + }); + assert.containsOnce( + document.body, + '.o_Message', + "there should be 1 message displayed initially" + ); + + await afterNextRender(() => { + document.querySelector(`.o_Attachment[data-attachment-local-id="${ + this.env.models['mail.attachment'].findFromIdentifyingData({ id: 143 }).localId + }"] .o_Attachment_asideItemUnlink`).click(); + }); + await afterNextRender(() => + document.querySelector('.o_AttachmentDeleteConfirmDialog_confirmButton').click() + ); + assert.containsOnce( + document.body, + '.o_Message', + "message should still be displayed after removing its attachments (non-empty content)" + ); +}); + +QUnit.test('delete all attachments of a message with tracking fields should still keep it displayed', async function (assert) { + assert.expect(2); + + this.data['ir.attachment'].records.push({ + id: 143, + mimetype: 'text/plain', + name: "Blah.txt", + }); + this.data['mail.channel'].records.push({ id: 11 }); + this.data['mail.message'].records.push( + { + attachment_ids: [143], + channel_ids: [11], + id: 101, + tracking_value_ids: [6] + }, + ); + this.data['mail.tracking.value'].records.push({ + changed_field: "Name", + field_type: "char", + id: 6, + new_value: "New name", + old_value: "Old name", + }); + await this.start(); + const threadViewer = this.env.models['mail.thread_viewer'].create({ + hasThreadView: true, + thread: [['insert', { id: 11, model: 'mail.channel' }]], + }); + // wait for messages of the thread to be loaded + await this.afterEvent({ + eventName: 'o-thread-view-hint-processed', + func: () => { + this.createThreadViewComponent(threadViewer.threadView); + }, + message: "thread become loaded with messages", + predicate: ({ hint, threadViewer }) => { + return ( + hint.type === 'messages-loaded' && + threadViewer.thread.model === 'mail.channel' && + threadViewer.thread.id === 11 + ); + }, + }); + assert.containsOnce( + document.body, + '.o_Message', + "there should be 1 message displayed initially" + ); + + await afterNextRender(() => { + document.querySelector(`.o_Attachment[data-attachment-local-id="${ + this.env.models['mail.attachment'].findFromIdentifyingData({ id: 143 }).localId + }"] .o_Attachment_asideItemUnlink`).click(); + }); + await afterNextRender(() => + document.querySelector('.o_AttachmentDeleteConfirmDialog_confirmButton').click() + ); + assert.containsOnce( + document.body, + '.o_Message', + "message should still be displayed after removing its attachments (non-empty content)" + ); +}); + }); }); }); diff --git a/addons/mail/static/src/models/message/message.js b/addons/mail/static/src/models/message/message.js index 953c3c29385..8b4029b4b48 100644 --- a/addons/mail/static/src/models/message/message.js +++ b/addons/mail/static/src/models/message/message.js @@ -401,6 +401,17 @@ function factory(dependencies) { return inlineBody.toLowerCase() === this.subtype_description.toLowerCase(); } + /** + * @private + */ + _computeIsEmpty() { + return ( + (!this.body || htmlToTextContentInline(this.body) === '') && + this.attachments.length === 0 && + this.tracking_value_ids.length === 0 + ); + } + /** * @private * @returns {boolean} @@ -551,6 +562,19 @@ function factory(dependencies) { 'subtype_description', ], }), + /** + * Determine whether the message has to be considered empty or not. + * + * An empty message has no text, no attachment and no tracking value. + */ + isEmpty: attr({ + compute: '_computeIsEmpty', + dependencies: [ + 'attachments', + 'body', + 'tracking_value_ids', + ], + }), isModeratedByCurrentPartner: attr({ compute: '_computeIsModeratedByCurrentPartner', default: false, diff --git a/addons/mail/static/src/widgets/form_renderer/form_renderer_tests.js b/addons/mail/static/src/widgets/form_renderer/form_renderer_tests.js index 8817664497c..e57317a7496 100644 --- a/addons/mail/static/src/widgets/form_renderer/form_renderer_tests.js +++ b/addons/mail/static/src/widgets/form_renderer/form_renderer_tests.js @@ -463,7 +463,7 @@ QUnit.test('basic chatter rendering without messages', async function (assert) { QUnit.test('chatter updating', async function (assert) { assert.expect(3); - this.data['mail.message'].records.push({ model: 'res.partner', res_id: 12 }); + this.data['mail.message'].records.push({ body: "not empty", model: 'res.partner', res_id: 12 }); this.data['res.partner'].records.push( { display_name: "first partner", id: 11 }, { display_name: "second partner", id: 12 } diff --git a/addons/mail/static/tests/helpers/mock_models.js b/addons/mail/static/tests/helpers/mock_models.js index 83ac74008a3..59bf06b72b6 100644 --- a/addons/mail/static/tests/helpers/mock_models.js +++ b/addons/mail/static/tests/helpers/mock_models.js @@ -136,6 +136,7 @@ class MockModels { res_model_name: { string: "Res Model Name", type: 'char' }, starred_partner_ids: { string: "Favorited By", type: 'many2many', relation: 'res.partner' }, subject: { string: "Subject", type: 'char' }, + tracking_value_ids: { relation: 'mail.tracking.value', string: "Tracking values", type: 'one2many' }, }, records: [], }, @@ -177,6 +178,15 @@ class MockModels { }, records: [], }, + 'mail.tracking.value': { + fields: { + changed_field: { string: 'Changed field', type: 'char' }, + field_type: { string: 'Field type', type: 'char' }, + new_value: { string: 'New value', type: 'char' }, + old_value: { string: 'Old value', type: 'char' }, + }, + records: [], + }, 'res.country': { fields: { code: { string: "Code", type: 'char' },
Do you confirm the action?