[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:
committed by
Alexandre Kühn
parent
059faa7f1f
commit
06aef5e05f
@@ -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(),
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user