summaryrefslogtreecommitdiff
path: root/addons/mail/static/src/components
diff options
context:
space:
mode:
authorXavier Dubuc <xdu@odoo.com>2020-10-09 15:11:41 +0000
committerSébastien Theys <seb@odoo.com>2020-10-09 16:45:35 +0000
commit336d3a428129857e39d98075ca8e95003c09b01e (patch)
treea4e7fb6c3ccba614251dde9f8540fa76ea3aec67 /addons/mail/static/src/components
parent76c2f412c09ef1169b5cf936f0770cd4200f29e2 (diff)
[FIX] mail: ask for confirmation before attachment deletion
also hide all messages with empty body and no attachments task-2282322 closes odoo/odoo#59672 X-original-commit: 60ae42dcda777cd66e4e421dbc02f9ef12a430ef Related: odoo/enterprise#13971 Signed-off-by: Alexandre Kühn (aku) <aku@odoo.com> Signed-off-by: Sébastien Theys (seb) <seb@odoo.com>
Diffstat (limited to 'addons/mail/static/src/components')
-rw-r--r--addons/mail/static/src/components/attachment/attachment.js24
-rw-r--r--addons/mail/static/src/components/attachment/attachment.xml6
-rw-r--r--addons/mail/static/src/components/attachment_box/attachment_box_tests.js48
-rw-r--r--addons/mail/static/src/components/attachment_delete_confirm_dialog/attachment_delete_confirm_dialog.js86
-rw-r--r--addons/mail/static/src/components/attachment_delete_confirm_dialog/attachment_delete_confirm_dialog.xml10
-rw-r--r--addons/mail/static/src/components/chat_window_manager/chat_window_manager_tests.js12
-rw-r--r--addons/mail/static/src/components/chatter/chatter_tests.js1
-rw-r--r--addons/mail/static/src/components/composer/composer_tests.js38
-rw-r--r--addons/mail/static/src/components/discuss/tests/discuss_inbox_tests.js10
-rw-r--r--addons/mail/static/src/components/discuss/tests/discuss_moderation_tests.js11
-rw-r--r--addons/mail/static/src/components/discuss/tests/discuss_tests.js29
-rw-r--r--addons/mail/static/src/components/message/message_tests.js16
-rw-r--r--addons/mail/static/src/components/message_list/message_list.js5
-rw-r--r--addons/mail/static/src/components/message_list/message_list.xml62
-rw-r--r--addons/mail/static/src/components/moderation_ban_dialog/moderation_ban_dialog.js6
-rw-r--r--addons/mail/static/src/components/moderation_discard_dialog/moderation_discard_dialog.js10
-rw-r--r--addons/mail/static/src/components/moderation_discard_dialog/moderation_discard_dialog.xml4
-rw-r--r--addons/mail/static/src/components/moderation_reject_dialog/moderation_reject_dialog.js6
-rw-r--r--addons/mail/static/src/components/thread_view/thread_view_tests.js186
19 files changed, 510 insertions, 60 deletions
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 @@
</t>
</div>
</t>
+ <t t-if="state.hasDeleteConfirmDialog">
+ <AttachmentDeleteConfirmDialog
+ attachmentLocalId="props.attachmentLocalId"
+ t-on-dialog-closed="_onDeleteConfirmDialogClosed"
+ />
+ </t>
</t>
</div>
</t>
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 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
- <!-- TODO FIXME file to be completed in task-2282322 -->
+ <t t-name="mail.AttachmentDeleteConfirmDialog" owl="1">
+ <Dialog contentClass="'o_AttachmentDeleteConfirmDialog'" title="getTitle()" size="'medium'" t-ref="dialog">
+ <p class="o_AttachmentDeleteConfirmDialog_mainText" t-esc="getBody()"/>
+ <t t-set-slot="buttons">
+ <button class="o_AttachmentDeleteConfirmDialog_confirmButton btn btn-primary" t-on-click="_onClickOk">Ok</button>
+ <button class="o_AttachmentDeleteConfirmDialog_cancelButton btn btn-secondary" t-on-click="_onClickCancel">Cancel</button>
+ </t>
+ </Dialog>
+ </t>
</templates>
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 <ThreadView/>
* component is patched. This is acceptable when <ThreadView/> 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 @@
</div>
</t>
<t t-foreach="orderedMessages" t-as="message" t-key="message.localId">
- <t t-set="message_day" t-value="getDateDay(message)"/>
- <t t-if="current_day !== message_day">
- <div class="o_MessageList_separator o_MessageList_separatorDate o_MessageList_item">
- <hr class="o_MessageList_separatorLine"/><span class="o_MessageList_separatorLabel o_MessageList_separatorLabelDate"><t t-esc="message_day"/></span><hr class="o_MessageList_separatorLine"/>
- <t t-set="current_day" t-value="message_day"/>
- <t t-set="isMessageSquashed" t-value="false"/>
- </div>
- </t>
- <t t-else="">
- <t t-set="isMessageSquashed" t-value="shouldMessageBeSquashed(prev_message, message)"/>
- </t>
- <Message
- class="o_MessageList_item o_MessageList_message"
- t-att-class="{
- 'o-has-message-selection': props.selectedMessageLocalId,
- }"
- hasMarkAsReadIcon="props.haveMessagesMarkAsReadIcon"
- hasCheckbox="props.hasMessageCheckbox"
- hasReplyIcon="props.haveMessagesReplyIcon"
- isSelected="props.selectedMessageLocalId === message.localId"
- isSquashed="isMessageSquashed"
- messageLocalId="message.localId"
- threadViewLocalId="threadView.localId"
- t-ref="{{ message.localId }}"
- />
- <t t-set="prev_message" t-value="message"/>
- <t t-if="threadView.thread.model === 'mail.channel' and threadView.thread.localMessageUnreadCounter > 0 and threadView.thread.lastSeenByCurrentPartnerMessageId === message.id">
- <div class="o_MessageList_separator o_MessageList_separatorNewMessages o_MessageList_item" t-att-class="{ 'o-disable-animation': env.disableAnimation }" t-transition="fade">
- <hr class="o_MessageList_separatorLine o_MessageList_separatorLineNewMessages"/><span class="o_MessageList_separatorLabel o_MessageList_separatorLabelNewMessages">New messages</span>
- </div>
+ <t t-if="!message.isEmpty">
+ <t t-set="message_day" t-value="getDateDay(message)"/>
+ <t t-if="current_day !== message_day">
+ <div class="o_MessageList_separator o_MessageList_separatorDate o_MessageList_item">
+ <hr class="o_MessageList_separatorLine"/><span class="o_MessageList_separatorLabel o_MessageList_separatorLabelDate"><t t-esc="message_day"/></span><hr class="o_MessageList_separatorLine"/>
+ <t t-set="current_day" t-value="message_day"/>
+ <t t-set="isMessageSquashed" t-value="false"/>
+ </div>
+ </t>
+ <t t-else="">
+ <t t-set="isMessageSquashed" t-value="shouldMessageBeSquashed(prev_message, message)"/>
+ </t>
+ <Message
+ class="o_MessageList_item o_MessageList_message"
+ t-att-class="{
+ 'o-has-message-selection': props.selectedMessageLocalId,
+ }"
+ hasMarkAsReadIcon="props.haveMessagesMarkAsReadIcon"
+ hasCheckbox="props.hasMessageCheckbox"
+ hasReplyIcon="props.haveMessagesReplyIcon"
+ isSelected="props.selectedMessageLocalId === message.localId"
+ isSquashed="isMessageSquashed"
+ messageLocalId="message.localId"
+ threadViewLocalId="threadView.localId"
+ t-ref="{{ message.localId }}"
+ />
+ <t t-set="prev_message" t-value="message"/>
+ <t t-if="threadView.thread.model === 'mail.channel' and threadView.thread.localMessageUnreadCounter > 0 and threadView.thread.lastSeenByCurrentPartnerMessageId === message.id">
+ <div class="o_MessageList_separator o_MessageList_separatorNewMessages o_MessageList_item" t-att-class="{ 'o-disable-animation': env.disableAnimation }" t-transition="fade">
+ <hr class="o_MessageList_separatorLine o_MessageList_separatorLineNewMessages"/><span class="o_MessageList_separatorLabel o_MessageList_separatorLabelNewMessages">New messages</span>
+ </div>
+ </t>
</t>
</t>
<!-- LOADING (if order desc)-->
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 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="mail.ModerationDiscardDialog" owl="1">
- <Dialog contentClass="'o_ModerationDiscardDialog'" title="CONFIRMATION" size="'medium'" t-ref="dialog">
- <p t-esc="getText()"/>
+ <Dialog contentClass="'o_ModerationDiscardDialog'" title="getTitle()" size="'medium'" t-ref="dialog">
+ <p t-esc="getBody()"/>
<p>Do you confirm the action?</p>
<t t-set-slot="buttons">
<button class="o-discard btn btn-primary" t-on-click="_onClickDiscard">Discard</button>
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)"
+ );
+});
+
});
});
});