<template> <div class="control__elements"> <ul class="field-file__list"> <li v-for="(file, index) in files" :key="file.name + '-' + index" class="field-file__item" > <FileInProgress v-if="file.state === 'loading'" :file="file" :read-only="readOnly" :icon-class="getIconClass(file.mime_type)" @delete="forceRemoveFile(getFileInProgressIndex(file.id))" /> <FileFailed v-else-if="file.state === 'failed'" :file="file" :read-only="readOnly" :icon-class="getIconClass(file.mime_type)" @delete="forceRemoveFile(getFileInProgressIndex(file.id))" /> <FileUploaded v-else :ref="`file-uploaded-${index}`" :file="file" :read-only="readOnly" :icon-class="getIconClass(file.mime_type)" @delete="removeFile(value, index)" @rename="(name) => renameFile(value, index, name)" @click="$refs.fileModal.show(index)" /> </li> </ul> <UploadFileDropzone v-if="!readOnly" @input="filesAdded($event)" /> <ButtonText v-if="!readOnly" icon="iconoir-plus" @click.prevent="showModal()" > {{ $t('rowEditFieldFile.addFile') }} </ButtonText> <UserFilesModal v-if="!readOnly" ref="uploadModal" :upload-file="uploadFile" :user-file-upload-types="userFileUploadTypes" @uploaded="addFiles(value, $event)" ></UserFilesModal> <div v-show="touched && !valid" class="error"> {{ error }} </div> <FileFieldModal v-if="Boolean(value)" ref="fileModal" :files="value" :read-only="readOnly" @removed="removeFile(value, $event)" @renamed="renameFile(value, $event.index, $event.value)" ></FileFieldModal> </div> </template> <script> import { uuid } from '@baserow/modules/core/utils/string' import FileFieldModal from '@baserow/modules/database/components/field/FileFieldModal' import rowEditField from '@baserow/modules/database/mixins/rowEditField' import fileField from '@baserow/modules/database/mixins/fileField' import UploadFileDropzone from '@baserow/modules/core/components/files/UploadFileDropzone' import UserFileService from '@baserow/modules/core/services/userFile' import { UploadFileUserFileUploadType } from '@baserow/modules/core/userFileUploadTypes' import UserFilesModal from '@baserow/modules/core/components/files/UserFilesModal' import axios from 'axios' import FileInProgress from '@baserow/modules/core/components/files/FileInProgress' import FileFailed from '@baserow/modules/core/components/files/FileFailed' import FileUploaded from '@baserow/modules/core/components/files/FileUploaded' import { IMAGE_FILE_TYPES } from '@baserow/modules/core/enums' export default { components: { FileUploaded, FileFailed, FileInProgress, UploadFileDropzone, FileFieldModal, UserFilesModal, }, mixins: [rowEditField, fileField], props: { uploadFile: { type: Function, required: false, default: null, }, userFileUploadTypes: { type: Array, required: false, default: null, }, }, data() { return { filesInProgress: [], currentFileUploading: null, cancelToken: null, } }, computed: { files() { return this.value?.concat(this.filesInProgress) }, uploadFileFunction() { return this.uploadFile || UserFileService(this.$client).uploadFile }, }, methods: { showModal() { this.$refs.uploadModal.show(UploadFileUserFileUploadType.getType()) }, async filesAdded(event) { const files = event.target.files || event.dataTransfer?.files if (!files) { return } let successfulUploads = [] const filesWithData = Array.from(files).map((file) => ({ file, fileData: this.forceAddFile(file, { state: 'loading' }), })) for (const { file, fileData } of filesWithData) { // File has been removed in the meantime if (this.getFileInProgressIndex(fileData.id) === -1) { continue } const progress = (event) => { fileData.percentage = Math.round((event.loaded * 100) / event.total) } try { this.currentFileUploading = fileData this.cancelToken = axios.CancelToken.source() const { data } = await this.uploadFileFunction( file, progress, this.cancelToken.token ) successfulUploads.push({ id: fileData.id, order: this.getFileInProgressIndex(fileData.id), data, }) } catch (error) { const message = error.handler.getMessage('userFile') error.handler.handled() this.forceUpdateFile(fileData.id, { state: 'failed', error: message.message, }) } } // Make sure a file has not been removed after it has been uploaded successfulUploads = successfulUploads.filter( ({ id }) => this.getFileInProgressIndex(id) !== -1 ) // Make sure to re-establish the order in which the files have been submitted successfulUploads.sort((a, b) => a.order - b.order) successfulUploads.forEach(({ id }) => this.forceRemoveFile(this.getFileInProgressIndex(id)) ) this.addFiles( this.value, successfulUploads.map(({ data }) => data) ) }, forceUpdateFile(id, values) { const fileIndex = this.getFileInProgressIndex(id) this.$set(this.filesInProgress, fileIndex, { ...this.files[fileIndex], ...values, }) }, forceRemoveFile(index) { if (this.getFileInProgressIndex(this.currentFileUploading.id) === index) { this.cancelToken.cancel() } this.filesInProgress.splice(index, 1) }, forceAddFile(file, additionalData = {}) { const id = uuid() const fileData = { id, visible_name: file.name, isImage: IMAGE_FILE_TYPES.includes(file.type), percentage: 0, ...additionalData, } this.filesInProgress.push(fileData) return fileData }, removeFile(...args) { fileField.methods.removeFile.call(this, ...args) this.touch() }, addFiles(...args) { fileField.methods.addFiles.call(this, ...args) this.touch() }, renameFile(value, index, newName) { const success = fileField.methods.renameFile.call( this, value, index, newName ) if (!success) { this.$refs[`file-uploaded-${index}`][0].resetName() } this.touch() }, getFileInProgressIndex(id) { return this.filesInProgress.findIndex((file) => file.id === id) }, }, } </script>