mirror of
https://github.com/directus/directus.git
synced 2026-02-11 11:35:53 -05:00
408 lines
8.8 KiB
Vue
408 lines
8.8 KiB
Vue
<template>
|
|
<div
|
|
data-dropzone
|
|
class="v-upload"
|
|
:class="{ dragging, uploading }"
|
|
@dragenter.prevent="onDragEnter"
|
|
@dragover.prevent
|
|
@dragleave.prevent="onDragLeave"
|
|
@drop.stop.prevent="onDrop"
|
|
>
|
|
<template v-if="dragging">
|
|
<v-icon class="upload-icon" x-large name="file_upload" />
|
|
<p class="type-label">{{ t('drop_to_upload') }}</p>
|
|
</template>
|
|
|
|
<template v-else-if="uploading">
|
|
<p class="type-label">{{ progress }}%</p>
|
|
<p class="type-text">
|
|
{{
|
|
multiple && numberOfFiles > 1
|
|
? t('upload_files_indeterminate', { done: done, total: numberOfFiles })
|
|
: t('upload_file_indeterminate')
|
|
}}
|
|
</p>
|
|
<v-progress-linear :value="progress" rounded />
|
|
</template>
|
|
|
|
<template v-else>
|
|
<div class="actions">
|
|
<v-button v-tooltip="t('click_to_browse')" icon rounded secondary @click="openFileBrowser">
|
|
<input ref="input" class="browse" type="file" :multiple="multiple" @input="onBrowseSelect" />
|
|
<v-icon name="file_upload" />
|
|
</v-button>
|
|
<v-button
|
|
v-if="fromLibrary"
|
|
v-tooltip="t('choose_from_library')"
|
|
icon
|
|
rounded
|
|
secondary
|
|
@click="activeDialog = 'choose'"
|
|
>
|
|
<v-icon name="folder_open" />
|
|
</v-button>
|
|
<v-button v-if="fromUrl" v-tooltip="t('import_from_url')" icon rounded secondary @click="activeDialog = 'url'">
|
|
<v-icon name="link" />
|
|
</v-button>
|
|
</div>
|
|
|
|
<p class="type-label">{{ t('drag_file_here') }}</p>
|
|
|
|
<template v-if="fromUrl !== false || fromLibrary !== false">
|
|
<drawer-collection
|
|
collection="directus_files"
|
|
:active="activeDialog === 'choose'"
|
|
:multiple="multiple"
|
|
:filter="filterByFolder"
|
|
@update:active="activeDialog = null"
|
|
@input="setSelection"
|
|
/>
|
|
|
|
<v-dialog
|
|
:model-value="activeDialog === 'url'"
|
|
:persistent="urlLoading"
|
|
@esc="activeDialog = null"
|
|
@update:model-value="activeDialog = null"
|
|
>
|
|
<v-card>
|
|
<v-card-title>{{ t('import_from_url') }}</v-card-title>
|
|
<v-card-text>
|
|
<v-input v-model="url" autofocus :placeholder="t('url')" :nullable="false" :disabled="urlLoading" />
|
|
</v-card-text>
|
|
<v-card-actions>
|
|
<v-button :disabled="urlLoading" secondary @click="activeDialog = null">
|
|
{{ t('cancel') }}
|
|
</v-button>
|
|
<v-button :loading="urlLoading" :disabled="isValidURL === false" @click="importFromURL">
|
|
{{ t('import_label') }}
|
|
</v-button>
|
|
</v-card-actions>
|
|
</v-card>
|
|
</v-dialog>
|
|
</template>
|
|
</template>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { useI18n } from 'vue-i18n';
|
|
import { ref, computed } from 'vue';
|
|
import { uploadFiles } from '@/utils/upload-files';
|
|
import { uploadFile } from '@/utils/upload-file';
|
|
import DrawerCollection from '@/views/private/components/drawer-collection.vue';
|
|
import api from '@/api';
|
|
import emitter, { Events } from '@/events';
|
|
import { unexpectedError } from '@/utils/unexpected-error';
|
|
import { Filter } from '@directus/types';
|
|
|
|
interface Props {
|
|
multiple?: boolean;
|
|
preset?: Record<string, any>;
|
|
fileId?: string;
|
|
fromUrl?: boolean;
|
|
fromLibrary?: boolean;
|
|
folder?: string;
|
|
}
|
|
|
|
const props = withDefaults(defineProps<Props>(), {
|
|
multiple: false,
|
|
preset: () => ({}),
|
|
fileId: undefined,
|
|
fromUrl: false,
|
|
fromLibrary: false,
|
|
folder: undefined,
|
|
});
|
|
|
|
const emit = defineEmits(['input']);
|
|
|
|
const { t } = useI18n();
|
|
|
|
const { uploading, progress, upload, onBrowseSelect, done, numberOfFiles } = useUpload();
|
|
const { onDragEnter, onDragLeave, onDrop, dragging } = useDragging();
|
|
const { url, isValidURL, loading: urlLoading, importFromURL } = useURLImport();
|
|
const { setSelection } = useSelection();
|
|
const activeDialog = ref<'choose' | 'url' | null>(null);
|
|
const input = ref<HTMLInputElement>();
|
|
|
|
const filterByFolder = computed(() => {
|
|
if (!props.folder) return undefined;
|
|
return { folder: { id: { _eq: props.folder } } } as Filter;
|
|
});
|
|
|
|
function validFiles(files: FileList) {
|
|
if (files.length === 0) return false;
|
|
|
|
for (const file of files) {
|
|
if (file.size === 0) return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
function useUpload() {
|
|
const uploading = ref(false);
|
|
const progress = ref(0);
|
|
const numberOfFiles = ref(0);
|
|
const done = ref(0);
|
|
|
|
return { uploading, progress, upload, onBrowseSelect, numberOfFiles, done };
|
|
|
|
async function upload(files: FileList) {
|
|
uploading.value = true;
|
|
progress.value = 0;
|
|
|
|
const folderPreset: { folder?: string } = {};
|
|
|
|
if (props.folder) {
|
|
folderPreset.folder = props.folder;
|
|
}
|
|
|
|
try {
|
|
if (!validFiles(files)) {
|
|
throw new Error('An error has occurred while uploading the files.');
|
|
}
|
|
|
|
numberOfFiles.value = files.length;
|
|
|
|
if (props.multiple === true) {
|
|
const uploadedFiles = await uploadFiles(Array.from(files), {
|
|
onProgressChange: (percentage) => {
|
|
progress.value = Math.round(percentage.reduce((acc, cur) => (acc += cur)) / files.length);
|
|
done.value = percentage.filter((p) => p === 100).length;
|
|
},
|
|
preset: {
|
|
...props.preset,
|
|
...folderPreset,
|
|
},
|
|
});
|
|
|
|
uploadedFiles && emit('input', uploadedFiles);
|
|
} else {
|
|
const uploadedFile = await uploadFile(Array.from(files)[0], {
|
|
onProgressChange: (percentage) => {
|
|
progress.value = percentage;
|
|
done.value = percentage === 100 ? 1 : 0;
|
|
},
|
|
fileId: props.fileId,
|
|
preset: {
|
|
...props.preset,
|
|
...folderPreset,
|
|
},
|
|
});
|
|
|
|
uploadedFile && emit('input', uploadedFile);
|
|
}
|
|
} catch (err: any) {
|
|
unexpectedError(err);
|
|
} finally {
|
|
uploading.value = false;
|
|
done.value = 0;
|
|
numberOfFiles.value = 0;
|
|
}
|
|
}
|
|
|
|
function onBrowseSelect(event: Event) {
|
|
const files = (event.target as HTMLInputElement)?.files;
|
|
|
|
if (files) {
|
|
upload(files);
|
|
}
|
|
}
|
|
}
|
|
|
|
function useDragging() {
|
|
const dragging = ref(false);
|
|
|
|
let dragCounter = 0;
|
|
|
|
return { onDragEnter, onDragLeave, onDrop, dragging };
|
|
|
|
function onDragEnter() {
|
|
dragCounter++;
|
|
|
|
if (dragCounter === 1) {
|
|
dragging.value = true;
|
|
}
|
|
}
|
|
|
|
function onDragLeave() {
|
|
dragCounter--;
|
|
|
|
if (dragCounter === 0) {
|
|
dragging.value = false;
|
|
}
|
|
}
|
|
|
|
function onDrop(event: DragEvent) {
|
|
dragCounter = 0;
|
|
dragging.value = false;
|
|
|
|
const files = event.dataTransfer?.files;
|
|
|
|
if (files) {
|
|
upload(files);
|
|
}
|
|
}
|
|
}
|
|
|
|
function useSelection() {
|
|
return { setSelection };
|
|
|
|
async function setSelection(selection: string[]) {
|
|
if (props.multiple) {
|
|
const filesResponse = await api.get(`/files`, {
|
|
params: {
|
|
filter: {
|
|
id: {
|
|
_in: selection,
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
emit('input', filesResponse.data.data);
|
|
} else {
|
|
if (selection[0]) {
|
|
const id = selection[0];
|
|
const fileResponse = await api.get(`/files/${id}`);
|
|
emit('input', fileResponse.data.data);
|
|
} else {
|
|
emit('input', null);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function useURLImport() {
|
|
const url = ref('');
|
|
const loading = ref(false);
|
|
|
|
const isValidURL = computed(() => {
|
|
try {
|
|
new URL(url.value);
|
|
return true;
|
|
} catch {
|
|
return false;
|
|
}
|
|
});
|
|
|
|
return { url, loading, isValidURL, importFromURL };
|
|
|
|
async function importFromURL() {
|
|
loading.value = true;
|
|
|
|
try {
|
|
const response = await api.post(`/files/import`, {
|
|
url: url.value,
|
|
data: {
|
|
folder: props.folder,
|
|
},
|
|
});
|
|
|
|
emitter.emit(Events.upload);
|
|
|
|
if (props.multiple) {
|
|
emit('input', [response.data.data]);
|
|
} else {
|
|
emit('input', response.data.data);
|
|
}
|
|
|
|
activeDialog.value = null;
|
|
url.value = '';
|
|
} catch (err: any) {
|
|
unexpectedError(err);
|
|
} finally {
|
|
loading.value = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
function openFileBrowser() {
|
|
input.value?.click();
|
|
}
|
|
</script>
|
|
|
|
<style lang="scss" scoped>
|
|
.v-upload {
|
|
position: relative;
|
|
display: flex;
|
|
flex-direction: column;
|
|
justify-content: center;
|
|
min-height: var(--input-height-tall);
|
|
padding: 32px;
|
|
color: var(--foreground-subdued);
|
|
text-align: center;
|
|
border: 2px dashed var(--border-normal);
|
|
border-radius: var(--border-radius);
|
|
transition: var(--fast) var(--transition);
|
|
transition-property: color, border-color, background-color;
|
|
|
|
p {
|
|
color: inherit;
|
|
}
|
|
|
|
&:not(.uploading):hover {
|
|
border-color: var(--border-normal-alt);
|
|
}
|
|
}
|
|
|
|
.actions {
|
|
display: flex;
|
|
justify-content: center;
|
|
margin-bottom: 18px;
|
|
|
|
.v-button {
|
|
margin-right: 12px;
|
|
|
|
&:last-child {
|
|
margin-right: 0;
|
|
}
|
|
}
|
|
}
|
|
|
|
.browse {
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
display: block;
|
|
width: 100%;
|
|
height: 100%;
|
|
cursor: pointer;
|
|
opacity: 0;
|
|
appearance: none;
|
|
}
|
|
|
|
.dragging {
|
|
color: var(--primary);
|
|
background-color: var(--primary-alt);
|
|
border-color: var(--primary);
|
|
|
|
* {
|
|
pointer-events: none;
|
|
}
|
|
|
|
.upload-icon {
|
|
margin: 0 auto;
|
|
margin-bottom: 12px;
|
|
}
|
|
}
|
|
|
|
.uploading {
|
|
--v-progress-linear-color: var(--white);
|
|
--v-progress-linear-background-color: rgb(255 255 255 / 0.25);
|
|
--v-progress-linear-height: 8px;
|
|
|
|
color: var(--white);
|
|
background-color: var(--primary);
|
|
border-color: var(--primary);
|
|
border-style: solid;
|
|
|
|
.v-progress-linear {
|
|
position: absolute;
|
|
bottom: 30px;
|
|
left: 32px;
|
|
width: calc(100% - 64px);
|
|
}
|
|
}
|
|
</style>
|