[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:
committed by
Sébastien Theys
parent
f50ad369ac
commit
5a958cb3ff
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user