Upload multiple files #6085
-
I'd like to discuss the possibility of adding a feature to the File field in Nova that allows for multiple file uploads. Current State:As of now, the File field in Nova does not support the uploading of multiple files simultaneously. Feature Request:I believe it would be a valuable addition to Nova to introduce an option for handling multiple file uploads directly within the native File field. This would enhance the user experience and streamline the process of managing file uploads. Why Native Support Matters:While there are workarounds like using Trix or external packages, I strongly believe that having native support for multiple file uploads in the Nova File field is crucial. It aligns with the principle of providing a seamless and integrated experience within the Nova ecosystem. Request for Discussion:I'd love to hear the community's thoughts on this matter. Do you think adding native support for multiple file uploads to the File field is beneficial? Are there any potential challenges or considerations that we should keep in mind? Note:Please refrain from suggesting workarounds such as Trix or external packages in this discussion. The focus is on exploring the feasibility and desirability of integrating this feature directly into the native Nova File field. |
Beta Was this translation helpful? Give feedback.
Replies: 6 comments 4 replies
-
Yes, please! |
Beta Was this translation helpful? Give feedback.
-
I also created a custom Field that implements multiple file upload using Vapor. Please see attached code. There are things that are not working like remove a file through UI, so please update your code if you have any updates. <template>
<DefaultField
:field="currentField"
:label-for="labelFor"
:errors="errors"
:show-help-text="!isReadonly && showHelpText"
:full-width-content="fullWidthContent"
>
<template #field>
<div class="space-y-4">
<div
v-if="hasValue && previewFile && files.length === 0"
class="grid grid-cols-4 gap-x-6 gap-y-2"
>
<FilePreviewBlock
v-if="previewFile"
:file="previewFile"
:removable="shouldShowRemoveButton"
@removed="confirmRemoval"
:rounded="field.rounded"
:dusk="`${field.attribute}-delete-link`"
/>
</div>
<ConfirmUploadRemovalModal
:show="removeModalOpen"
@confirm="removeUploadedFile"
@close="closeRemoveModal"
/>
<DropZone
multiple
v-if="shouldShowField"
:files="files"
@file-changed="handleFileChange"
@file-removed="file = null"
:rounded="field.rounded"
:accepted-types="field.acceptedTypes"
:disabled="file?.processing"
:dusk="`${field.attribute}-delete-link`"
:input-dusk="field.attribute"
/>
</div>
</template>
</DefaultField>
</template>
<script>
import {DependentFormField, Errors, HandlesValidationErrors} from 'laravel-nova'
import InlineFormData from '../../../../../vendor/laravel/nova/resources/js/fields/Form/InlineFormData.js'
import Vapor from 'laravel-vapor'
function createFile(file) {
return {
name: file.name,
extension: file.name.split('.').pop(),
type: file.type,
originalFile: file,
vapor: true,
processing: false,
progress: 0,
}
}
export default {
emits: ['file-upload-started', 'file-upload-finished', 'file-deleted'],
mixins: [HandlesValidationErrors, DependentFormField],
inject: ['removeFile'],
expose: ['beforeRemove'],
data: () => ({
previewFile: null,
files: [],
removeModalOpen: false,
missing: false,
deleted: false,
uploadErrors: new Errors(),
vaporFiles: [],
uploadProgress: 0,
startedDrag: false,
uploadModalShown: false,
}),
async mounted() {
this.preparePreviewImage()
this.field.fill = formData => {
this.files.forEach((file, index) => {
let attribute = `${this.fieldAttribute}[${index}]`;
this.fillVaporFilePayload(formData, attribute)
});
}
},
methods: {
preparePreviewImage() {
if (this.hasValue && this.imageUrl) {
this.fetchPreviewImage()
}
if (this.hasValue && !this.imageUrl) {
this.previewFile = createFile({
name: this.currentField.value,
type: this.currentField.value.split('.').pop(),
})
}
},
async fetchPreviewImage() {
let response = await fetch(this.imageUrl)
let data = await response.blob()
this.previewFile = createFile(
new File([data], this.currentField.value, {type: data.type})
)
},
handleFileChange(newFiles) {
this.files = Array.from(newFiles).map(file => createFile(file));
if (this.isVaporField) {
this.uploadVaporFiles();
}
},
uploadVaporFiles() {
this.files.forEach((file, index) => {
file.processing = true;
this.$emit('file-upload-started');
Vapor.store(file.originalFile, {
progress: progress => {
file.progress = Math.round(progress * 100);
},
})
.then(response => {
this.vaporFiles[index] = this.vaporFiles[index] || {};
this.vaporFiles[index].key = response.key
this.vaporFiles[index].uuid = response.uuid
this.vaporFiles[index].filename = file.name
this.vaporFiles[index].extension = file.extension
file.processing = false
file.progress = 100
this.$emit('file-upload-finished')
})
});
},
confirmRemoval() {
this.removeModalOpen = true
},
closeRemoveModal() {
this.removeModalOpen = false
},
beforeRemove() {
this.removeUploadedFile()
},
async removeUploadedFile() {
try {
await Promise.all(this.files.map(file => this.removeFile(file.fieldAttribute))); // Remove all files
this.$emit('file-deleted');
this.deleted = true;
this.files = [];
Nova.success(this.__('The files were deleted!'));
} catch (error) {
if (error.response?.status === 422) {
this.uploadErrors = new Errors(error.response.data.errors)
}
} finally {
this.closeRemoveModal();
}
},
fillVaporFilePayload(formData, attribute) {
const vaporFormData = formData instanceof InlineFormData ? formData.formData : formData;
vaporFormData.append('vapor', true)
this.vaporFiles.forEach((vaporFile, index) => {
const vaporAttribute = formData instanceof InlineFormData ? formData.slug(attribute) : attribute;
vaporFormData.append(`${vaporAttribute}[${index}][key]`, vaporFile.key);
vaporFormData.append(`${vaporAttribute}[${index}][uuid]`, vaporFile.uuid);
vaporFormData.append(`${vaporAttribute}[${index}][filename]`, vaporFile.filename);
vaporFormData.append(`${vaporAttribute}[${index}][extension]`, vaporFile.extension);
});
},
},
computed: {
files() {
return this.files;
},
/**
* Determine if the field has an upload error.
*/
hasError() {
return this.uploadErrors.has(this.fieldAttribute)
},
/**
* Return the first error for the field.
*/
firstError() {
if (this.hasError) {
return this.uploadErrors.first(this.fieldAttribute)
}
},
/**
* The ID attribute to use for the file field.
*/
idAttr() {
return this.labelFor
},
/**
* The label attribute to use for the file field.
*/
labelFor() {
let name = this.resourceName
if (this.relatedResourceName) {
name += '-' + this.relatedResourceName
}
return `file-${name}-${this.fieldAttribute}`
},
/**
* Determine whether the field has a value.
*/
hasValue() {
return (
Boolean(this.field.value || this.imageUrl) &&
!Boolean(this.deleted) &&
!Boolean(this.missing)
)
},
/**
* Determine whether the field should show the loader component.
*/
shouldShowLoader() {
return !Boolean(this.deleted) && Boolean(this.imageUrl)
},
/**
* Determine whether the file field input should be shown.
*/
shouldShowField() {
return Boolean(!this.currentlyIsReadonly)
},
/**
* Determine whether the field should show the remove button.
*/
shouldShowRemoveButton() {
return Boolean(this.currentField.deletable && !this.currentlyIsReadonly)
},
/**
* Return the preview or thumbnail URL for the field.
*/
imageUrl() {
return this.currentField.previewUrl || this.currentField.thumbnailUrl
},
/**
* Determining if the field is a Vapor field.
*/
isVaporField() {
return true
},
},
}
</script> |
Beta Was this translation helpful? Give feedback.
-
I completely agree that this should be a first party offering in Nova. It feels confounding to me that this is currently only possible using third party packages. I just ran into a major issue today when I realized that https://github.com/ebess/advanced-nova-media-library doesn't work fully with Nova Actions and I now need to pivot my entire approach to building/handling action forms. If the configuring was coming from a first-party field type in Nova itself, I imagine roadblocks like this could be avoided. |
Beta Was this translation helpful? Give feedback.
-
A year later and this very important feature is for some reason not implemented into nova!! |
Beta Was this translation helpful? Give feedback.
-
Agree this should be natively supported. I used https://novapackages.com/packages/ArdentHQ/nova-image-gallery-field until this is possible. |
Beta Was this translation helpful? Give feedback.
-
@davidhemphill any update on this? |
Beta Was this translation helpful? Give feedback.
I also created a custom Field that implements multiple file upload using Vapor. Please see attached code. There are things that are not working like remove a file through UI, so please update your code if you have any updates.