[FIX] mail: allow temporary attachment to be canceled

task-2324065

closes odoo/odoo#57623

X-original-commit: 9f774d4fb512017ab5f99981255afc8d2c35113c
Signed-off-by: Alexandre Kühn (aku) <aku@odoo.com>
This commit is contained in:
Xavier Dubuc
2020-09-14 10:08:50 +00:00
committed by Alexandre Kühn
parent 059faa7f1f
commit 06aef5e05f
4 changed files with 160 additions and 17 deletions
@@ -39,7 +39,7 @@
</div>
<div class="o_Attachment_actions">
<!-- Remove button -->
<t t-if="props.isEditable and !attachment.isTemporary" t-key="'unlink'">
<t t-if="props.isEditable" t-key="'unlink'">
<div class="o_Attachment_action o_Attachment_actionUnlink"
t-att-class="{
'o-pretty': attachment.isLinkedToComposer,
@@ -75,7 +75,7 @@
</t>
<!-- Attachment aside -->
<t t-if="detailsMode !== 'hover' and (props.isDownloadable or props.isEditable)">
<div class="o_Attachment_aside" t-att-class="{ 'o-has-multiple-action': !attachment.isTemporary and props.isDownloadable and props.isEditable }">
<div class="o_Attachment_aside" t-att-class="{ 'o-has-multiple-action': props.isDownloadable and props.isEditable }">
<!-- Uploading icon -->
<t t-if="attachment.isTemporary and attachment.isLinkedToComposer">
<div class="o_Attachment_asideItem o_Attachment_asideItemUploading" title="Uploading">
@@ -89,7 +89,7 @@
</div>
</t>
<!-- Remove button -->
<t t-if="props.isEditable and !attachment.isTemporary">
<t t-if="props.isEditable">
<div class="o_Attachment_asideItem o_Attachment_asideItemUnlink" t-att-class="{ 'o-pretty': attachment.isLinkedToComposer }" t-on-click="_onClickUnlink" title="Remove">
<i class="fa fa-times"/>
</div>
@@ -40,9 +40,10 @@ QUnit.module('composer_tests.js', {
};
this.start = async params => {
const { env, widget } = await start(Object.assign({}, params, {
const { afterEvent, env, widget } = await start(Object.assign({}, params, {
data: this.data,
}));
this.afterEvent = afterEvent;
this.env = env;
this.widget = widget;
};
@@ -1141,6 +1142,102 @@ QUnit.test('warning on send with shortcut when attempting to post message with s
);
});
QUnit.test('remove an uploading attachment', async function (assert) {
assert.expect(4);
await this.start({
async mockFetch(resource, init) {
const res = this._super(...arguments);
if (resource === '/web/binary/upload_attachment') {
// simulates uploading indefinitely
await new Promise(() => {});
}
return res;
}
});
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"
);
assert.containsOnce(
document.body,
'.o_Composer .o_Attachment.o-temporary',
"should have an uploading 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 temporary one"
);
});
QUnit.test('remove an uploading attachment aborts upload', async function (assert) {
assert.expect(1);
await this.start({
async mockFetch(resource, init) {
const res = this._super(...arguments);
if (resource === '/web/binary/upload_attachment') {
// simulates uploading indefinitely
await new Promise(() => {});
}
return res;
}
});
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_Attachment',
"should contain an attachment"
);
const attachmentLocalId = document.querySelector('.o_Attachment').dataset.attachmentLocalId;
await this.afterEvent({
eventName: 'o-attachment-upload-abort',
func: () => {
document.querySelector('.o_Attachment_asideItemUnlink').click();
},
message: "attachment upload request should have been aborted",
predicate: ({ attachment }) => {
return attachment.localId === attachmentLocalId;
},
});
});
});
});
});
@@ -97,14 +97,26 @@ class FileUploader extends Component {
*/
async _performUpload(files) {
for (const file of files) {
const response = await this.env.browser.fetch('/web/binary/upload_attachment', {
method: 'POST',
body: this._createFormData(file),
});
let html = await response.text();
const template = document.createElement('template');
template.innerHTML = html.trim();
window.eval(template.content.firstChild.textContent);
const uploadingAttachment = this.env.models['mail.attachment'].find(attachment =>
attachment.isTemporary &&
attachment.filename === file.name
);
try {
const response = await this.env.browser.fetch('/web/binary/upload_attachment', {
method: 'POST',
body: this._createFormData(file),
signal: uploadingAttachment.uploadingAbortController.signal,
});
let html = await response.text();
const template = document.createElement('template');
template.innerHTML = html.trim();
window.eval(template.content.firstChild.textContent);
} catch (e) {
if (e.name !== 'AbortError') {
throw e;
}
}
}
}
@@ -96,11 +96,15 @@ function factory(dependencies) {
* Remove this attachment globally.
*/
async remove() {
await this.async(() => this.env.services.rpc({
model: 'ir.attachment',
method: 'unlink',
args: [this.id],
}, { shadow: true }));
if (!this.isTemporary) {
await this.async(() => this.env.services.rpc({
model: 'ir.attachment',
method: 'unlink',
args: [this.id],
}, { shadow: true }));
} else if (this.uploadingAbortController) {
this.uploadingAbortController.abort();
}
this.delete();
}
@@ -246,6 +250,25 @@ function factory(dependencies) {
return this.mimetype && this.mimetype.split('/').shift();
}
/**
* @private
* @returns {AbortController|undefined}
*/
_computeUploadingAbortController() {
if (this.isTemporary) {
if (!this.uploadingAbortController) {
const abortController = new AbortController();
abortController.signal.onabort = () => {
this.env.messagingBus.trigger('o-attachment-upload-abort', {
attachment: this
});
};
return abortController;
}
return this.uploadingAbortController;
}
return undefined;
}
}
Attachment.fields = {
@@ -328,6 +351,17 @@ function factory(dependencies) {
inverse: 'attachments',
}),
type: attr(),
/**
* Abort Controller linked to the uploading process of this attachment.
* Useful in order to cancel the in-progress uploading of this attachment.
*/
uploadingAbortController: attr({
compute: '_computeUploadingAbortController',
dependencies: [
'isTemporary',
'uploadingAbortController',
],
}),
url: attr(),
};