diff --git a/addons/mail/static/src/components/attachment/attachment.xml b/addons/mail/static/src/components/attachment/attachment.xml
index 8af00288b8b..7be59392838 100644
--- a/addons/mail/static/src/components/attachment/attachment.xml
+++ b/addons/mail/static/src/components/attachment/attachment.xml
@@ -39,7 +39,7 @@
-
+
-
+
@@ -89,7 +89,7 @@
-
+
diff --git a/addons/mail/static/src/components/composer/composer_tests.js b/addons/mail/static/src/components/composer/composer_tests.js
index 5d337e0ed0c..459e0d2c799 100644
--- a/addons/mail/static/src/components/composer/composer_tests.js
+++ b/addons/mail/static/src/components/composer/composer_tests.js
@@ -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;
+ },
+ });
+});
+
});
});
});
diff --git a/addons/mail/static/src/components/file_uploader/file_uploader.js b/addons/mail/static/src/components/file_uploader/file_uploader.js
index a8f2fb9cf68..efa0026842d 100644
--- a/addons/mail/static/src/components/file_uploader/file_uploader.js
+++ b/addons/mail/static/src/components/file_uploader/file_uploader.js
@@ -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;
+ }
+ }
}
}
diff --git a/addons/mail/static/src/models/attachment/attachment.js b/addons/mail/static/src/models/attachment/attachment.js
index a9adce2579f..77ebf667db0 100644
--- a/addons/mail/static/src/models/attachment/attachment.js
+++ b/addons/mail/static/src/models/attachment/attachment.js
@@ -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(),
};