mirror of
https://github.com/directus/directus.git
synced 2026-04-25 03:00:53 -04:00
@@ -52,12 +52,7 @@ import { registerDisplays } from './displays/register';
|
||||
import App from './app.vue';
|
||||
|
||||
async function init() {
|
||||
await Promise.all([
|
||||
registerInterfaces(),
|
||||
registerDisplays(),
|
||||
registerLayouts(),
|
||||
loadModules(),
|
||||
]);
|
||||
await Promise.all([registerInterfaces(), registerDisplays(), registerLayouts(), loadModules()]);
|
||||
|
||||
Vue.config.productionTip = false;
|
||||
|
||||
@@ -75,4 +70,8 @@ async function init() {
|
||||
console.groupEnd();
|
||||
}
|
||||
|
||||
// Prevent the browser from opening files that are dragged on the window
|
||||
window.addEventListener('dragover', (e) => e.preventDefault(), false);
|
||||
window.addEventListener('drop', (e) => e.preventDefault(), false);
|
||||
|
||||
init();
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<private-view :title="title">
|
||||
<private-view :title="title" :class="{ dragging }">
|
||||
<template #headline v-if="breadcrumb">
|
||||
<v-breadcrumb :items="breadcrumb" />
|
||||
</template>
|
||||
@@ -133,6 +133,13 @@
|
||||
<layout-drawer-detail @input="layout = $event" :value="layout" />
|
||||
<portal-target name="drawer" />
|
||||
</template>
|
||||
|
||||
<template v-if="showDropEffect">
|
||||
<div class="drop-border top" />
|
||||
<div class="drop-border right" />
|
||||
<div class="drop-border bottom" />
|
||||
<div class="drop-border left" />
|
||||
</template>
|
||||
</private-view>
|
||||
</template>
|
||||
|
||||
@@ -152,9 +159,11 @@ import FolderPicker from '../components/folder-picker.vue';
|
||||
import emitter, { Events } from '@/events';
|
||||
import router from '@/router';
|
||||
import Vue from 'vue';
|
||||
import { useUserStore } from '@/stores';
|
||||
import { useNotificationsStore, useUserStore } from '@/stores';
|
||||
import { subDays } from 'date-fns';
|
||||
import useFolders from '../composables/use-folders';
|
||||
import useEventListener from '@/composables/use-event-listener';
|
||||
import uploadFiles from '@/utils/upload-files';
|
||||
|
||||
type Item = {
|
||||
[field: string]: any;
|
||||
@@ -179,6 +188,7 @@ export default defineComponent({
|
||||
const selection = ref<Item[]>([]);
|
||||
|
||||
const userStore = useUserStore();
|
||||
const notificationsStore = useNotificationsStore();
|
||||
|
||||
const { layout, layoutOptions, layoutQuery, filters, searchQuery } = usePreset(ref('directus_files'));
|
||||
const { batchLink } = useLinks();
|
||||
@@ -242,6 +252,13 @@ export default defineComponent({
|
||||
onMounted(() => emitter.on(Events.upload, refresh));
|
||||
onUnmounted(() => emitter.off(Events.upload, refresh));
|
||||
|
||||
const { onDragEnter, onDragLeave, onDrop, onDragOver, showDropEffect, dragging } = useFileUpload();
|
||||
|
||||
useEventListener(window, 'dragenter', onDragEnter);
|
||||
useEventListener(window, 'dragover', onDragOver);
|
||||
useEventListener(window, 'dragleave', onDragLeave);
|
||||
useEventListener(window, 'drop', onDrop);
|
||||
|
||||
return {
|
||||
batchDelete,
|
||||
batchLink,
|
||||
@@ -264,6 +281,11 @@ export default defineComponent({
|
||||
selectedFolder,
|
||||
refresh,
|
||||
clearFilters,
|
||||
onDragEnter,
|
||||
onDragLeave,
|
||||
showDropEffect,
|
||||
onDrop,
|
||||
dragging,
|
||||
};
|
||||
|
||||
function useBatchDelete() {
|
||||
@@ -381,6 +403,133 @@ export default defineComponent({
|
||||
filters.value = [];
|
||||
searchQuery.value = null;
|
||||
}
|
||||
|
||||
function useFileUpload() {
|
||||
const showDropEffect = ref(false);
|
||||
|
||||
let dragNotificationID: string;
|
||||
let fileUploadNotificationID: string;
|
||||
|
||||
const dragCounter = ref(0);
|
||||
|
||||
const dragging = computed(() => dragCounter.value > 0);
|
||||
|
||||
return { onDragEnter, onDragLeave, onDrop, onDragOver, showDropEffect, dragging };
|
||||
|
||||
function enableDropEffect() {
|
||||
showDropEffect.value = true;
|
||||
|
||||
dragNotificationID = notificationsStore.add({
|
||||
title: i18n.t('drop_to_upload'),
|
||||
icon: 'cloud_upload',
|
||||
type: 'info',
|
||||
persist: true,
|
||||
closeable: false,
|
||||
});
|
||||
}
|
||||
|
||||
function disableDropEffect() {
|
||||
showDropEffect.value = false;
|
||||
|
||||
if (dragNotificationID) {
|
||||
notificationsStore.remove(dragNotificationID);
|
||||
}
|
||||
}
|
||||
|
||||
function onDragEnter(event: DragEvent) {
|
||||
if (!event.dataTransfer) return;
|
||||
if (event.dataTransfer?.types.indexOf('Files') === -1) return;
|
||||
|
||||
event.preventDefault();
|
||||
dragCounter.value++;
|
||||
|
||||
const isDropzone = event.target && (event.target as HTMLElement).getAttribute?.('data-dropzone') === '';
|
||||
|
||||
if (dragCounter.value === 1 && showDropEffect.value === false && isDropzone === false) {
|
||||
enableDropEffect();
|
||||
}
|
||||
|
||||
if (isDropzone) {
|
||||
disableDropEffect();
|
||||
dragCounter.value = 0;
|
||||
}
|
||||
}
|
||||
|
||||
function onDragOver(event: DragEvent) {
|
||||
if (!event.dataTransfer) return;
|
||||
if (event.dataTransfer?.types.indexOf('Files') === -1) return;
|
||||
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
function onDragLeave(event: DragEvent) {
|
||||
if (!event.dataTransfer) return;
|
||||
if (event.dataTransfer?.types.indexOf('Files') === -1) return;
|
||||
|
||||
event.preventDefault();
|
||||
dragCounter.value--;
|
||||
|
||||
if (dragCounter.value === 0) {
|
||||
disableDropEffect();
|
||||
}
|
||||
|
||||
if (event.target && (event.target as HTMLElement).getAttribute?.('data-dropzone') === '') {
|
||||
enableDropEffect();
|
||||
dragCounter.value = 1;
|
||||
}
|
||||
}
|
||||
|
||||
async function onDrop(event: DragEvent) {
|
||||
if (!event.dataTransfer) return;
|
||||
if (event.dataTransfer?.types.indexOf('Files') === -1) return;
|
||||
|
||||
event.preventDefault();
|
||||
showDropEffect.value = false;
|
||||
|
||||
dragCounter.value = 0;
|
||||
|
||||
if (dragNotificationID) {
|
||||
notificationsStore.remove(dragNotificationID);
|
||||
}
|
||||
|
||||
const files = [...event.dataTransfer.files];
|
||||
|
||||
fileUploadNotificationID = notificationsStore.add({
|
||||
title: i18n.tc('upload_file_indeterminate', files.length, {
|
||||
done: 0,
|
||||
total: files.length,
|
||||
}),
|
||||
type: 'info',
|
||||
persist: true,
|
||||
closeable: false,
|
||||
loading: true,
|
||||
});
|
||||
|
||||
await uploadFiles(files, {
|
||||
preset: {
|
||||
folder: props.queryFilters?.folder || null,
|
||||
},
|
||||
onProgressChange: (progress) => {
|
||||
const percentageDone = progress.reduce((val, cur) => (val += cur)) / progress.length;
|
||||
|
||||
const total = files.length;
|
||||
const done = progress.filter((p) => p === 100).length;
|
||||
|
||||
notificationsStore.update(fileUploadNotificationID, {
|
||||
title: i18n.tc('upload_file_indeterminate', files.length, {
|
||||
done,
|
||||
total,
|
||||
}),
|
||||
loading: false,
|
||||
progress: percentageDone,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
notificationsStore.remove(fileUploadNotificationID);
|
||||
emitter.emit(Events.upload);
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
@@ -414,4 +563,52 @@ export default defineComponent({
|
||||
.layout {
|
||||
--layout-offset-top: 64px;
|
||||
}
|
||||
|
||||
.drop-border {
|
||||
position: fixed;
|
||||
z-index: 500;
|
||||
background-color: var(--primary);
|
||||
|
||||
&.top,
|
||||
&.bottom {
|
||||
width: 100%;
|
||||
height: 4px;
|
||||
}
|
||||
|
||||
&.left,
|
||||
&.right {
|
||||
width: 4px;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
&.top {
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
&.right {
|
||||
top: 0;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
&.bottom {
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
&.left {
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.dragging {
|
||||
::v-deep * {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
::v-deep [data-dropzone] {
|
||||
pointer-events: all;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -10,7 +10,7 @@ export default async function uploadFile(
|
||||
options?: {
|
||||
onProgressChange?: (percentage: number) => void;
|
||||
notifications?: boolean;
|
||||
preset?: Record<string, string>;
|
||||
preset?: Record<string, any>;
|
||||
}
|
||||
) {
|
||||
const progressHandler = options?.onProgressChange || (() => undefined);
|
||||
|
||||
@@ -7,7 +7,7 @@ export default async function uploadFiles(
|
||||
options?: {
|
||||
onProgressChange?: (percentages: number[]) => void;
|
||||
notifications?: boolean;
|
||||
preset?: Record<string, string>;
|
||||
preset?: Record<string, any>;
|
||||
}
|
||||
) {
|
||||
const progressHandler = options?.onProgressChange || (() => undefined);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="private-view" :class="{ theme, dragging }">
|
||||
<div class="private-view" :class="{ theme }">
|
||||
<aside role="navigation" aria-label="Module Navigation" class="navigation" :class="{ 'is-open': navOpen }">
|
||||
<module-bar />
|
||||
<div class="module-nav alt-colors">
|
||||
@@ -43,13 +43,6 @@
|
||||
<v-overlay class="drawer-overlay" :active="drawerOpen" @click="drawerOpen = false" />
|
||||
|
||||
<notifications-group v-if="notificationsPreviewActive === false" :dense="drawerOpen === false" />
|
||||
|
||||
<template v-if="showDropEffect">
|
||||
<div class="drop-border top" />
|
||||
<div class="drop-border right" />
|
||||
<div class="drop-border bottom" />
|
||||
<div class="drop-border left" />
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -62,11 +55,8 @@ import ProjectInfo from './components/project-info';
|
||||
import DrawerButton from './components/drawer-button/';
|
||||
import NotificationsGroup from './components/notifications-group/';
|
||||
import NotificationsPreview from './components/notifications-preview/';
|
||||
import { useNotificationsStore, useUserStore, useAppStore } from '@/stores';
|
||||
import NotificationItem from './components/notification-item';
|
||||
import uploadFiles from '@/utils/upload-files';
|
||||
import { useUserStore, useAppStore } from '@/stores';
|
||||
import i18n from '@/lang';
|
||||
import useEventListener from '@/composables/use-event-listener';
|
||||
import emitter, { Events } from '@/events';
|
||||
|
||||
export default defineComponent({
|
||||
@@ -78,7 +68,6 @@ export default defineComponent({
|
||||
DrawerButton,
|
||||
NotificationsGroup,
|
||||
NotificationsPreview,
|
||||
NotificationItem,
|
||||
},
|
||||
props: {
|
||||
title: {
|
||||
@@ -90,7 +79,6 @@ export default defineComponent({
|
||||
const navOpen = ref(false);
|
||||
const contentEl = ref<Element>();
|
||||
const userStore = useUserStore();
|
||||
const notificationsStore = useNotificationsStore();
|
||||
const appStore = useAppStore();
|
||||
|
||||
const notificationsPreviewActive = ref(false);
|
||||
@@ -103,151 +91,15 @@ export default defineComponent({
|
||||
|
||||
provide('main-element', contentEl);
|
||||
|
||||
const { onDragEnter, onDragLeave, onDrop, onDragOver, showDropEffect, dragging } = useFileUpload();
|
||||
|
||||
useEventListener(window, 'dragenter', onDragEnter);
|
||||
useEventListener(window, 'dragover', onDragOver);
|
||||
useEventListener(window, 'dragleave', onDragLeave);
|
||||
useEventListener(window, 'drop', onDrop);
|
||||
|
||||
return {
|
||||
navOpen,
|
||||
contentEl,
|
||||
theme,
|
||||
onDragEnter,
|
||||
onDragLeave,
|
||||
showDropEffect,
|
||||
onDrop,
|
||||
dragging,
|
||||
drawerOpen,
|
||||
openDrawer,
|
||||
notificationsPreviewActive,
|
||||
};
|
||||
|
||||
function useFileUpload() {
|
||||
const showDropEffect = ref(false);
|
||||
|
||||
let dragNotificationID: string;
|
||||
let fileUploadNotificationID: string;
|
||||
|
||||
const dragCounter = ref(0);
|
||||
|
||||
const dragging = computed(() => dragCounter.value > 0);
|
||||
|
||||
return { onDragEnter, onDragLeave, onDrop, onDragOver, showDropEffect, dragging };
|
||||
|
||||
function enableDropEffect() {
|
||||
showDropEffect.value = true;
|
||||
|
||||
dragNotificationID = notificationsStore.add({
|
||||
title: i18n.t('drop_to_upload'),
|
||||
icon: 'cloud_upload',
|
||||
type: 'info',
|
||||
persist: true,
|
||||
closeable: false,
|
||||
});
|
||||
}
|
||||
|
||||
function disableDropEffect() {
|
||||
showDropEffect.value = false;
|
||||
|
||||
if (dragNotificationID) {
|
||||
notificationsStore.remove(dragNotificationID);
|
||||
}
|
||||
}
|
||||
|
||||
function onDragEnter(event: DragEvent) {
|
||||
if (!event.dataTransfer) return;
|
||||
if (event.dataTransfer?.types.indexOf('Files') === -1) return;
|
||||
|
||||
event.preventDefault();
|
||||
dragCounter.value++;
|
||||
|
||||
const isDropzone = event.target && (event.target as HTMLElement).getAttribute?.('data-dropzone') === '';
|
||||
|
||||
if (dragCounter.value === 1 && showDropEffect.value === false && isDropzone === false) {
|
||||
enableDropEffect();
|
||||
}
|
||||
|
||||
if (isDropzone) {
|
||||
disableDropEffect();
|
||||
dragCounter.value = 0;
|
||||
}
|
||||
}
|
||||
|
||||
function onDragOver(event: DragEvent) {
|
||||
if (!event.dataTransfer) return;
|
||||
if (event.dataTransfer?.types.indexOf('Files') === -1) return;
|
||||
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
function onDragLeave(event: DragEvent) {
|
||||
if (!event.dataTransfer) return;
|
||||
if (event.dataTransfer?.types.indexOf('Files') === -1) return;
|
||||
|
||||
event.preventDefault();
|
||||
dragCounter.value--;
|
||||
|
||||
if (dragCounter.value === 0) {
|
||||
disableDropEffect();
|
||||
}
|
||||
|
||||
if (event.target && (event.target as HTMLElement).getAttribute?.('data-dropzone') === '') {
|
||||
enableDropEffect();
|
||||
dragCounter.value = 1;
|
||||
}
|
||||
}
|
||||
|
||||
async function onDrop(event: DragEvent) {
|
||||
if (!event.dataTransfer) return;
|
||||
if (event.dataTransfer?.types.indexOf('Files') === -1) return;
|
||||
|
||||
event.preventDefault();
|
||||
showDropEffect.value = false;
|
||||
|
||||
dragCounter.value = 0;
|
||||
|
||||
if (dragNotificationID) {
|
||||
notificationsStore.remove(dragNotificationID);
|
||||
}
|
||||
|
||||
const files = [...event.dataTransfer.files];
|
||||
|
||||
fileUploadNotificationID = notificationsStore.add({
|
||||
title: i18n.tc('upload_file_indeterminate', files.length, {
|
||||
done: 0,
|
||||
total: files.length,
|
||||
}),
|
||||
type: 'info',
|
||||
persist: true,
|
||||
closeable: false,
|
||||
loading: true,
|
||||
});
|
||||
|
||||
await uploadFiles(files, {
|
||||
onProgressChange: (progress) => {
|
||||
const percentageDone = progress.reduce((val, cur) => (val += cur)) / progress.length;
|
||||
|
||||
const total = files.length;
|
||||
const done = progress.filter((p) => p === 100).length;
|
||||
|
||||
notificationsStore.update(fileUploadNotificationID, {
|
||||
title: i18n.tc('upload_file_indeterminate', files.length, {
|
||||
done,
|
||||
total,
|
||||
}),
|
||||
loading: false,
|
||||
progress: percentageDone,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
notificationsStore.remove(fileUploadNotificationID);
|
||||
emitter.emit(Events.upload);
|
||||
}
|
||||
}
|
||||
|
||||
function openDrawer(event: PointerEvent) {
|
||||
if (event.target && (event.target as HTMLElement).classList.contains('close') === false) {
|
||||
drawerOpen.value = true;
|
||||
@@ -391,52 +243,4 @@ export default defineComponent({
|
||||
--content-padding-bottom: 132px;
|
||||
}
|
||||
}
|
||||
|
||||
.dragging {
|
||||
::v-deep * {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
::v-deep [data-dropzone] {
|
||||
pointer-events: all;
|
||||
}
|
||||
}
|
||||
|
||||
.drop-border {
|
||||
position: fixed;
|
||||
z-index: 500;
|
||||
background-color: var(--primary);
|
||||
|
||||
&.top,
|
||||
&.bottom {
|
||||
width: 100%;
|
||||
height: 4px;
|
||||
}
|
||||
|
||||
&.left,
|
||||
&.right {
|
||||
width: 4px;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
&.top {
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
&.right {
|
||||
top: 0;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
&.bottom {
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
&.left {
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user