diff options
| author | stephanchrst <stephanchrst@gmail.com> | 2022-05-10 21:51:50 +0700 |
|---|---|---|
| committer | stephanchrst <stephanchrst@gmail.com> | 2022-05-10 21:51:50 +0700 |
| commit | 3751379f1e9a4c215fb6eb898b4ccc67659b9ace (patch) | |
| tree | a44932296ef4a9b71d5f010906253d8c53727726 /addons/mail/static/src/components/file_uploader | |
| parent | 0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (diff) | |
initial commit 2
Diffstat (limited to 'addons/mail/static/src/components/file_uploader')
4 files changed, 348 insertions, 0 deletions
diff --git a/addons/mail/static/src/components/file_uploader/file_uploader.js b/addons/mail/static/src/components/file_uploader/file_uploader.js new file mode 100644 index 00000000..4e57eadd --- /dev/null +++ b/addons/mail/static/src/components/file_uploader/file_uploader.js @@ -0,0 +1,241 @@ +odoo.define('mail/static/src/components/file_uploader/file_uploader.js', function (require) { +'use strict'; + +const useShouldUpdateBasedOnProps = require('mail/static/src/component_hooks/use_should_update_based_on_props/use_should_update_based_on_props.js'); + +const core = require('web.core'); + +const { Component } = owl; +const { useRef } = owl.hooks; + +class FileUploader extends Component { + + /** + * @override + */ + constructor(...args) { + super(...args); + this._fileInputRef = useRef('fileInput'); + this._fileUploadId = _.uniqueId('o_FileUploader_fileupload'); + this._onAttachmentUploaded = this._onAttachmentUploaded.bind(this); + useShouldUpdateBasedOnProps({ + compareDepth: { + attachmentLocalIds: 1, + newAttachmentExtraData: 3, + }, + }); + } + + mounted() { + $(window).on(this._fileUploadId, this._onAttachmentUploaded); + } + + willUnmount() { + $(window).off(this._fileUploadId); + } + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + /** + * @param {FileList|Array} files + * @returns {Promise} + */ + async uploadFiles(files) { + await this._unlinkExistingAttachments(files); + this._createTemporaryAttachments(files); + await this._performUpload(files); + this._fileInputRef.el.value = ''; + } + + openBrowserFileUploader() { + this._fileInputRef.el.click(); + } + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * @deprecated + * @private + * @param {Object} fileData + * @returns {mail.attachment} + */ + _createAttachment(fileData) { + return this.env.models['mail.attachment'].create(Object.assign( + {}, + fileData, + this.props.newAttachmentExtraData + )); + } + + /** + * @private + * @param {File} file + * @returns {FormData} + */ + _createFormData(file) { + let formData = new window.FormData(); + formData.append('callback', this._fileUploadId); + formData.append('csrf_token', core.csrf_token); + formData.append('id', this.props.uploadId); + formData.append('model', this.props.uploadModel); + formData.append('ufile', file, file.name); + return formData; + } + + /** + * @private + * @param {FileList|Array} files + */ + _createTemporaryAttachments(files) { + for (const file of files) { + this.env.models['mail.attachment'].create( + Object.assign( + { + filename: file.name, + isTemporary: true, + name: file.name + }, + this.props.newAttachmentExtraData + ), + ); + } + } + /** + * @private + * @param {FileList|Array} files + * @returns {Promise} + */ + async _performUpload(files) { + for (const file of files) { + const uploadingAttachment = this.env.models['mail.attachment'].find(attachment => + attachment.isTemporary && + attachment.filename === file.name + ); + if (!uploadingAttachment) { + // Uploading attachment no longer exists. + // This happens when an uploading attachment is being deleted by user. + continue; + } + 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; + } + } + } + } + + /** + * @private + * @param {FileList|Array} files + * @returns {Promise} + */ + async _unlinkExistingAttachments(files) { + for (const file of files) { + const attachment = this.props.attachmentLocalIds + .map(attachmentLocalId => this.env.models['mail.attachment'].get(attachmentLocalId)) + .find(attachment => attachment.name === file.name && attachment.size === file.size); + // if the files already exits, delete the file before upload + if (attachment) { + attachment.remove(); + } + } + } + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * @private + * @param {jQuery.Event} ev + * @param {...Object} filesData + */ + async _onAttachmentUploaded(ev, ...filesData) { + for (const fileData of filesData) { + const { error, filename, id, mimetype, name, size } = fileData; + if (error || !id) { + this.env.services['notification'].notify({ + type: 'danger', + message: owl.utils.escape(error), + }); + const relatedTemporaryAttachments = this.env.models['mail.attachment'] + .find(attachment => + attachment.filename === filename && + attachment.isTemporary + ); + for (const attachment of relatedTemporaryAttachments) { + attachment.delete(); + } + return; + } + // FIXME : needed to avoid problems on uploading + // Without this the useStore selector of component could be not called + // E.g. in attachment_box_tests.js + await new Promise(resolve => setTimeout(resolve)); + const attachment = this.env.models['mail.attachment'].insert( + Object.assign( + { + filename, + id, + mimetype, + name, + size, + }, + this.props.newAttachmentExtraData + ), + ); + this.trigger('o-attachment-created', { attachment }); + } + } + + /** + * 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, { + defaultProps: { + uploadId: 0, + uploadModel: 'mail.compose.message' + }, + props: { + attachmentLocalIds: { + type: Array, + element: String, + }, + newAttachmentExtraData: { + type: Object, + optional: true, + }, + uploadId: Number, + uploadModel: String, + }, + template: 'mail.FileUploader', +}); + +return FileUploader; + +}); diff --git a/addons/mail/static/src/components/file_uploader/file_uploader.scss b/addons/mail/static/src/components/file_uploader/file_uploader.scss new file mode 100644 index 00000000..32792313 --- /dev/null +++ b/addons/mail/static/src/components/file_uploader/file_uploader.scss @@ -0,0 +1,3 @@ +.o_FileUploader_input { + display: none !important; +} diff --git a/addons/mail/static/src/components/file_uploader/file_uploader.xml b/addons/mail/static/src/components/file_uploader/file_uploader.xml new file mode 100644 index 00000000..bf144037 --- /dev/null +++ b/addons/mail/static/src/components/file_uploader/file_uploader.xml @@ -0,0 +1,10 @@ +<?xml version="1.0" encoding="UTF-8"?> +<templates xml:space="preserve"> + + <t t-name="mail.FileUploader" owl="1"> + <div class="o_FileUploader"> + <input class="o_FileUploader_input" t-on-change="_onChangeAttachment" multiple="true" type="file" t-ref="fileInput" t-key="'fileInput'"/> + </div> + </t> + +</templates> diff --git a/addons/mail/static/src/components/file_uploader/file_uploader_tests.js b/addons/mail/static/src/components/file_uploader/file_uploader_tests.js new file mode 100644 index 00000000..4bf528f1 --- /dev/null +++ b/addons/mail/static/src/components/file_uploader/file_uploader_tests.js @@ -0,0 +1,94 @@ +odoo.define('mail/static/src/components/file_uploader/file_uploader_tests.js', function (require) { +"use strict"; + +const components = { + FileUploader: require('mail/static/src/components/file_uploader/file_uploader.js'), +}; +const { + afterEach, + beforeEach, + createRootComponent, + nextAnimationFrame, + start, +} = require('mail/static/src/utils/test_utils.js'); + +const { + file: { + createFile, + inputFiles, + }, +} = require('web.test_utils'); + +QUnit.module('mail', {}, function () { +QUnit.module('components', {}, function () { +QUnit.module('file_uploader', {}, function () { +QUnit.module('file_uploader_tests.js', { + beforeEach() { + beforeEach(this); + this.components = []; + + this.createFileUploaderComponent = async otherProps => { + const props = Object.assign({ attachmentLocalIds: [] }, otherProps); + return createRootComponent(this, components.FileUploader, { + props, + target: this.widget.el, + }); + }; + + this.start = async params => { + const { env, widget } = await start(Object.assign({}, params, { + data: this.data, + })); + this.env = env; + this.widget = widget; + }; + }, + afterEach() { + afterEach(this); + }, +}); + +QUnit.test('no conflicts between file uploaders', async function (assert) { + assert.expect(2); + + await this.start(); + const fileUploader1 = await this.createFileUploaderComponent(); + const fileUploader2 = await this.createFileUploaderComponent(); + const file1 = await createFile({ + name: 'text1.txt', + content: 'hello, world', + contentType: 'text/plain', + }); + inputFiles( + fileUploader1.el.querySelector('.o_FileUploader_input'), + [file1] + ); + await nextAnimationFrame(); // we can't use afterNextRender as fileInput are display:none + assert.strictEqual( + this.env.models['mail.attachment'].all().length, + 1, + 'Uploaded file should be the only attachment created' + ); + + const file2 = await createFile({ + name: 'text2.txt', + content: 'hello, world', + contentType: 'text/plain', + }); + inputFiles( + fileUploader2.el.querySelector('.o_FileUploader_input'), + [file2] + ); + await nextAnimationFrame(); + assert.strictEqual( + this.env.models['mail.attachment'].all().length, + 2, + 'Uploaded file should be the only attachment added' + ); +}); + +}); +}); +}); + +}); |
