summaryrefslogtreecommitdiff
path: root/addons/mail/static/src/components/file_uploader
diff options
context:
space:
mode:
authorstephanchrst <stephanchrst@gmail.com>2022-05-10 21:51:50 +0700
committerstephanchrst <stephanchrst@gmail.com>2022-05-10 21:51:50 +0700
commit3751379f1e9a4c215fb6eb898b4ccc67659b9ace (patch)
treea44932296ef4a9b71d5f010906253d8c53727726 /addons/mail/static/src/components/file_uploader
parent0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (diff)
initial commit 2
Diffstat (limited to 'addons/mail/static/src/components/file_uploader')
-rw-r--r--addons/mail/static/src/components/file_uploader/file_uploader.js241
-rw-r--r--addons/mail/static/src/components/file_uploader/file_uploader.scss3
-rw-r--r--addons/mail/static/src/components/file_uploader/file_uploader.xml10
-rw-r--r--addons/mail/static/src/components/file_uploader/file_uploader_tests.js94
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'
+ );
+});
+
+});
+});
+});
+
+});