Files
site/nova/resources/js/components/Form/FileField.vue
2025-04-21 16:03:20 +02:00

340 lines
8.3 KiB
Vue

<template>
<default-field
:field="field"
:errors="errors"
:full-width-content="true"
:show-help-text="!isReadonly && showHelpText"
>
<template slot="field">
<div v-if="hasValue" :class="{ 'mb-6': !isReadonly }">
<template v-if="shouldShowLoader">
<ImageLoader
:src="imageUrl"
:maxWidth="maxWidth"
:rounded="field.rounded"
@missing="value => (missing = value)"
/>
</template>
<template v-if="field.value && !imageUrl">
<card
class="flex item-center relative border border-lg border-50 overflow-hidden p-4"
>
<span class="truncate mr-3"> {{ field.value }} </span>
<DeleteButton
:dusk="field.attribute + '-internal-delete-link'"
class="ml-auto"
v-if="shouldShowRemoveButton"
@click="confirmRemoval"
/>
</card>
</template>
<p
v-if="imageUrl && !isReadonly"
class="mt-3 flex items-center text-sm"
>
<DeleteButton
:dusk="field.attribute + '-delete-link'"
v-if="shouldShowRemoveButton"
@click="confirmRemoval"
>
<span class="class ml-2 mt-1"> {{ __('Delete') }} </span>
</DeleteButton>
</p>
<portal to="modals">
<confirm-upload-removal-modal
v-if="removeModalOpen"
@confirm="removeFile"
@close="closeRemoveModal"
/>
</portal>
</div>
<p v-if="!hasValue && isReadonly" class="pt-2 text-sm text-90">
{{ __('This file field is read-only.') }}
</p>
<span
v-if="shouldShowField"
class="form-file mr-4"
:class="{ 'opacity-75': isReadonly }"
>
<input
ref="fileField"
:dusk="field.attribute"
class="form-file-input select-none"
type="file"
:id="idAttr"
name="name"
@change="fileChange"
:disabled="isReadonly || uploading"
:accept="field.acceptedTypes"
/>
<label
:for="labelFor"
class="form-file-btn btn btn-default btn-primary select-none"
>
<span v-if="uploading"
>{{ __('Uploading') }} ({{ uploadProgress }}%)</span
>
<span v-else>{{ __('Choose File') }}</span>
</label>
</span>
<span v-if="shouldShowField" class="text-90 text-sm select-none">
{{ currentLabel }}
</span>
<p v-if="hasError" class="text-xs mt-2 text-danger">{{ firstError }}</p>
</template>
</default-field>
</template>
<script>
import ImageLoader from '@/components/ImageLoader'
import DeleteButton from '@/components/DeleteButton'
import { FormField, HandlesValidationErrors, Errors } from 'laravel-nova'
import Vapor from 'laravel-vapor'
export default {
props: [
'resourceId',
'relatedResourceName',
'relatedResourceId',
'viaRelationship',
],
mixins: [HandlesValidationErrors, FormField],
components: { DeleteButton, ImageLoader },
data: () => ({
file: null,
fileName: '',
removeModalOpen: false,
missing: false,
deleted: false,
uploadErrors: new Errors(),
vaporFile: {
key: '',
uuid: '',
filename: '',
extension: '',
},
uploading: false,
uploadProgress: 0,
}),
mounted() {
this.field.fill = formData => {
let attribute = this.field.attribute
if (this.file && !this.isVaporField) {
formData.append(attribute, this.file, this.fileName)
}
if (this.file && this.isVaporField) {
formData.append(attribute, this.fileName)
formData.append('vaporFile[' + attribute + '][key]', this.vaporFile.key)
formData.append(
'vaporFile[' + attribute + '][uuid]',
this.vaporFile.uuid
)
formData.append(
'vaporFile[' + attribute + '][filename]',
this.vaporFile.filename
)
formData.append(
'vaporFile[' + attribute + '][extension]',
this.vaporFile.extension
)
}
}
},
methods: {
/**
* Respond to the file change
*/
fileChange(event) {
let path = event.target.value
let fileName = path.match(/[^\\/]*$/)[0]
this.fileName = fileName
let extension = fileName.split('.').pop()
this.file = this.$refs.fileField.files[0]
if (this.isVaporField) {
this.uploading = true
this.$emit('file-upload-started')
Vapor.store(this.$refs.fileField.files[0], {
progress: progress => {
this.uploadProgress = Math.round(progress * 100)
},
}).then(response => {
this.vaporFile.key = response.key
this.vaporFile.uuid = response.uuid
this.vaporFile.filename = fileName
this.vaporFile.extension = extension
this.uploading = false
this.uploadProgress = 0
this.$emit('file-upload-finished')
})
}
},
/**
* Confirm removal of the linked file
*/
confirmRemoval() {
this.removeModalOpen = true
},
/**
* Close the upload removal modal
*/
closeRemoveModal() {
this.removeModalOpen = false
},
/**
* Remove the linked file from storage
*/
async removeFile() {
this.uploadErrors = new Errors()
const {
resourceName,
resourceId,
relatedResourceName,
relatedResourceId,
viaRelationship,
} = this
const attribute = this.field.attribute
const uri =
this.viaRelationship &&
this.relatedResourceName &&
this.relatedResourceId
? `/nova-api/${resourceName}/${resourceId}/${relatedResourceName}/${relatedResourceId}/field/${attribute}?viaRelationship=${viaRelationship}`
: `/nova-api/${resourceName}/${resourceId}/field/${attribute}`
try {
await Nova.request().delete(uri)
this.closeRemoveModal()
this.deleted = true
this.$emit('file-deleted')
Nova.success(this.__('The file was deleted!'))
} catch (error) {
this.closeRemoveModal()
if (error.response.status == 422) {
this.uploadErrors = new Errors(error.response.data.errors)
}
}
},
},
computed: {
/**
* 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 current label of the file field.
*/
currentLabel() {
return this.fileName || this.__('no file selected')
},
/**
* 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.field.attribute}`
},
/**
* 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.isReadonly)
},
/**
* Determine whether the field should show the remove button.
*/
shouldShowRemoveButton() {
return Boolean(this.field.deletable && !this.isReadonly)
},
/**
* Return the preview or thumbnail URL for the field.
*/
imageUrl() {
return this.field.previewUrl || this.field.thumbnailUrl
},
/**
* Determine the maximum width of the field.
*/
maxWidth() {
return this.field.maxWidth || 320
},
/**
* Determing if the field is a Vapor field.
*/
isVaporField() {
return this.field.component == 'vapor-file-field'
},
},
}
</script>