[IMP] mail: move fileUploader methods in the model view

task-2579306

closes odoo/odoo#81848

Signed-off-by: Sébastien Theys (seb) <seb@odoo.com>
This commit is contained in:
Didier (did)
2022-01-07 17:35:03 +00:00
committed by Sébastien Theys
parent f50ad369ac
commit 5a958cb3ff
7 changed files with 150 additions and 163 deletions
@@ -39,7 +39,7 @@ export class AttachmentBox extends Component {
* @param {FileList} detail.files
*/
async _onDropZoneFilesDropped(detail) {
await this.attachmentBoxView.fileUploaderView.component.uploadFiles(detail.files);
await this.attachmentBoxView.fileUploaderView.uploadFiles(detail.files);
this.isDropZoneVisible.value = false;
}
@@ -122,7 +122,7 @@ export class Composer extends Component {
* @private
*/
_onClickAddAttachment() {
this.composerView.fileUploaderView.component.openBrowserFileUploader();
this.composerView.fileUploaderView.openBrowserFileUploader();
if (!this.messaging.device.isMobileDevice) {
this.composerView.update({ doFocus: true });
}
@@ -172,7 +172,7 @@ export class Composer extends Component {
* @param {FileList} detail.files
*/
async _onDropZoneFilesDropped(detail) {
await this.composerView.fileUploaderView.component.uploadFiles(detail.files);
await this.composerView.fileUploaderView.uploadFiles(detail.files);
this.isDropZoneVisible.value = false;
}
@@ -201,7 +201,7 @@ export class Composer extends Component {
if (!ev.clipboardData || !ev.clipboardData.files) {
return;
}
await this.composerView.fileUploaderView.component.uploadFiles(ev.clipboardData.files);
await this.composerView.fileUploaderView.uploadFiles(ev.clipboardData.files);
}
}
@@ -1,21 +1,9 @@
/** @odoo-module **/
import { useComponentToModel } from '@mail/component_hooks/use_component_to_model/use_component_to_model';
import { useRefToModel } from '@mail/component_hooks/use_ref_to_model/use_ref_to_model';
import { registerMessagingComponent } from '@mail/utils/messaging_component';
import { replace } from '@mail/model/model_field_command';
import core from 'web.core';
const { Component } = owl;
const { useRef } = owl.hooks;
const geAttachmentNextTemporaryId = (function () {
let tmpId = 0;
return () => {
tmpId -= 1;
return tmpId;
};
})();
export class FileUploader extends Component {
@@ -24,9 +12,7 @@ export class FileUploader extends Component {
*/
setup() {
super.setup();
useComponentToModel({ fieldName: 'component', modelName: 'FileUploaderView', propNameAsRecordLocalId: 'localId' });
this._fileInputRef = useRef('fileInput');
this._fileUploadId = _.uniqueId('o_FileUploader_fileupload');
useRefToModel({ fieldName: 'fileInputRef', modelName: 'FileUploaderView', propNameAsRecordLocalId: 'localId', refName: 'fileInput' });
}
//--------------------------------------------------------------------------
@@ -37,142 +23,6 @@ export class FileUploader extends Component {
return this.messaging && this.messaging.models['FileUploaderView'].get(this.props.localId);
}
/**
* @param {FileList|Array} files
* @returns {Promise}
*/
async uploadFiles(files) {
await this._performUpload({ files });
this._fileInputRef.el.value = '';
}
openBrowserFileUploader() {
this._fileInputRef.el.click();
}
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
/**
* @private
* @param {Object} param0
* @param {Composer} param0.composer
* @param {File} param0.file
* @param {Thread} param0.thread
* @returns {FormData}
*/
_createFormData({ composer, file, thread }) {
const formData = new window.FormData();
formData.append('csrf_token', core.csrf_token);
formData.append('is_pending', Boolean(composer));
formData.append('thread_id', thread && thread.id);
formData.append('thread_model', thread && thread.model);
formData.append('ufile', file, file.name);
return formData;
}
/**
* @private
* @param {Object} param0
* @param {FileList|Array} param0.files
* @returns {Promise}
*/
async _performUpload({ files }) {
const composer = this.fileUploaderView.composerView && this.fileUploaderView.composerView.composer; // save before async
const thread = this.fileUploaderView.thread; // save before async
const chatter = this.fileUploaderView.attachmentBoxView && this.fileUploaderView.attachmentBoxView.chatter; // save before async
const activity = this.fileUploaderView.activityView && this.fileUploaderView.activityView.activity; // save before async
const uploadingAttachments = new Map();
for (const file of files) {
uploadingAttachments.set(file, this.messaging.models['Attachment'].insert({
composer: composer && replace(composer),
filename: file.name,
id: geAttachmentNextTemporaryId(),
isUploading: true,
mimetype: file.type,
name: file.name,
originThread: (!composer && thread) ? replace(thread) : undefined,
}));
}
const attachments = [];
for (const file of files) {
const uploadingAttachment = uploadingAttachments.get(file);
if (!uploadingAttachment.exists()) {
// This happens when a pending attachment is being deleted by user before upload.
continue;
}
if ((composer && !composer.exists()) || (thread && !thread.exists())) {
return;
}
try {
const response = await this.env.browser.fetch('/mail/attachment/upload', {
method: 'POST',
body: this._createFormData({ composer, file, thread }),
signal: uploadingAttachment.uploadingAbortController.signal,
});
const attachmentData = await response.json();
if (uploadingAttachment.exists()) {
uploadingAttachment.delete();
}
if ((composer && !composer.exists()) || (thread && !thread.exists())) {
return;
}
const attachment = this._onAttachmentUploaded({ attachmentData, composer, thread });
attachments.push(attachment);
} catch (e) {
if (e.name !== 'AbortError') {
throw e;
}
}
}
if (chatter && chatter.exists() && chatter.hasParentReloadOnAttachmentsChanged) {
chatter.reloadParentView();
}
if (activity && activity.exists()) {
activity.markAsDone({ attachments });
}
}
//--------------------------------------------------------------------------
// Handlers
//--------------------------------------------------------------------------
/**
* @private
* @param {Object} param0
* @param {Object} param0.attachmentData
* @param {Composer} param0.composer
* @param {Thread} param0.thread
* @returns {Attachment}
*/
_onAttachmentUploaded({ attachmentData, composer, thread }) {
if (attachmentData.error || !attachmentData.id) {
this.env.services['notification'].notify({
type: 'danger',
message: attachmentData.error,
});
return;
}
return this.messaging.models['Attachment'].insert({
composer: composer && replace(composer),
originThread: (!composer && thread) ? replace(thread) : undefined,
...attachmentData,
});
}
/**
* Called when there are changes in the file input.
*
* @private
* @param {Event} ev
* @param {EventTarget} ev.target
* @param {FileList|Array} ev.target.files
*/
async _onChangeAttachment(ev) {
await this.uploadFiles(ev.target.files);
}
}
Object.assign(FileUploader, {
@@ -3,7 +3,9 @@
<t t-name="mail.FileUploader" owl="1">
<div class="o_FileUploader" t-attf-class="{{ className }}" t-ref="root">
<input class="o_FileUploader_input" t-on-change="_onChangeAttachment" multiple="true" type="file" t-ref="fileInput" t-key="'fileInput'"/>
<t t-if="fileUploaderView">
<input class="o_FileUploader_input" t-on-change="fileUploaderView.onChangeAttachment" multiple="true" type="file" t-ref="fileInput" t-key="'fileInput'"/>
</t>
</div>
</t>
@@ -61,7 +61,7 @@ registerModel({
* explorer for upload.
*/
onClickUploadDocument() {
this.fileUploaderView.component.openBrowserFileUploader();
this.fileUploaderView.openBrowserFileUploader();
},
/**
* @private
@@ -25,7 +25,7 @@ registerModel({
* Handles click on the "add attachment" button.
*/
onClickAddAttachment() {
this.fileUploaderView.component.openBrowserFileUploader();
this.fileUploaderView.openBrowserFileUploader();
},
},
fields: {
@@ -4,10 +4,48 @@ import { registerModel } from '@mail/model/model_core';
import { attr, many2one, one2one } from '@mail/model/model_field';
import { clear, replace } from '@mail/model/model_field_command';
import core from 'web.core';
const getAttachmentNextTemporaryId = (function () {
let tmpId = 0;
return () => {
tmpId -= 1;
return tmpId;
};
})();
registerModel({
name: 'FileUploaderView',
identifyingFields: [['activityView', 'attachmentBoxView', 'composerView']],
lifecycleHooks: {
_created() {
this.onChangeAttachment = this.onChangeAttachment.bind(this);
}
},
recordMethods: {
openBrowserFileUploader() {
this.fileInputRef.el.click();
},
/**
* Called when there are changes in the file input.
*
* @param {Event} ev
* @param {EventTarget} ev.target
* @param {FileList|Array} ev.target.files
*/
onChangeAttachment(ev) {
this.uploadFiles(ev.target.files);
},
/**
* @param {FileList|Array} files
* @returns {Promise}
*/
async uploadFiles(files) {
await this._performUpload({ files });
if (this.fileInputRef && this.fileInputRef.el) {
this.fileInputRef.el.value = '';
}
},
/**
* @private
* @returns {FieldCommand}
@@ -24,6 +62,106 @@ registerModel({
}
return clear();
},
/**
* @private
* @param {Object} param0
* @param {Composer} param0.composer
* @param {File} param0.file
* @param {Thread} param0.thread
* @returns {FormData}
*/
_createFormData({ composer, file, thread }) {
const formData = new window.FormData();
formData.append('csrf_token', core.csrf_token);
formData.append('is_pending', Boolean(composer));
formData.append('thread_id', thread && thread.id);
formData.append('thread_model', thread && thread.model);
formData.append('ufile', file, file.name);
return formData;
},
/**
* @private
* @param {Object} param0
* @param {Object} param0.attachmentData
* @param {Composer} param0.composer
* @param {Thread} param0.thread
* @returns {Attachment}
*/
_onAttachmentUploaded({ attachmentData, composer, thread }) {
if (attachmentData.error || !attachmentData.id) {
this.env.services['notification'].notify({
type: 'danger',
message: attachmentData.error,
});
return;
}
return this.messaging.models['Attachment'].insert({
composer: composer && replace(composer),
originThread: (!composer && thread) ? replace(thread) : undefined,
...attachmentData,
});
},
/**
* @private
* @param {Object} param0
* @param {FileList|Array} param0.files
* @returns {Promise}
*/
async _performUpload({ files }) {
const composer = this.composerView && this.composerView.composer; // save before async
const thread = this.thread; // save before async
const chatter = this.attachmentBoxView && this.attachmentBoxView.chatter; // save before async
const activity = this.activityView && this.activityView.activity; // save before async
const uploadingAttachments = new Map();
for (const file of files) {
uploadingAttachments.set(file, this.messaging.models['Attachment'].insert({
composer: composer && replace(composer),
filename: file.name,
id: getAttachmentNextTemporaryId(),
isUploading: true,
mimetype: file.type,
name: file.name,
originThread: (!composer && thread) ? replace(thread) : undefined,
}));
}
const attachments = [];
for (const file of files) {
const uploadingAttachment = uploadingAttachments.get(file);
if (!uploadingAttachment.exists()) {
// This happens when a pending attachment is being deleted by user before upload.
continue;
}
if ((composer && !composer.exists()) || (thread && !thread.exists())) {
return;
}
try {
const response = await this.env.browser.fetch('/mail/attachment/upload', {
method: 'POST',
body: this._createFormData({ composer, file, thread }),
signal: uploadingAttachment.uploadingAbortController.signal,
});
const attachmentData = await response.json();
if (uploadingAttachment.exists()) {
uploadingAttachment.delete();
}
if ((composer && !composer.exists()) || (thread && !thread.exists())) {
return;
}
const attachment = this._onAttachmentUploaded({ attachmentData, composer, thread });
attachments.push(attachment);
} catch (e) {
if (e.name !== 'AbortError') {
throw e;
}
}
}
if (chatter && chatter.exists() && chatter.hasParentReloadOnAttachmentsChanged) {
chatter.reloadParentView();
}
if (activity && activity.exists()) {
activity.markAsDone({ attachments });
}
},
},
fields: {
activityView: one2one('ActivityView', {
@@ -36,15 +174,12 @@ registerModel({
isCausal: true,
readonly: true,
}),
/**
* States the OWL component of this file uploader view.
*/
component: attr(),
composerView: one2one('ComposerView', {
inverse: 'fileUploaderView',
isCausal: true,
readonly: true,
}),
fileInputRef: attr(),
thread: many2one('Thread', {
compute: '_computeThread',
readonly: true,