From adc3461de54401fedf662b1eb901ffa340c260ec Mon Sep 17 00:00:00 2001 From: Rijk van Zanten Date: Thu, 16 Apr 2020 15:58:00 -0400 Subject: [PATCH] Global file upload (#422) * Add hover style for file drop in private view * Add uploadFile / uploadFiles utils * Show drag notification on file dragover * Finish global file uploads * Fix warning in drawer detail test * Use correct terminology * Ignore drop for non file drops --- src/lang/en-US/index.json | 5 + src/stores/notifications/notifications.ts | 14 +++ src/stores/notifications/types.ts | 3 + src/utils/upload-file/index.ts | 4 + src/utils/upload-file/upload-file.ts | 37 ++++++ src/utils/upload-files/index.ts | 4 + src/utils/upload-files/upload-files.ts | 36 ++++++ .../drawer-detail/drawer-detail.test.ts | 2 + .../notification-item/notification-item.vue | 19 ++- .../notifications-group.vue | 6 +- src/views/private/private-view.vue | 109 +++++++++++++++++- 11 files changed, 234 insertions(+), 5 deletions(-) create mode 100644 src/utils/upload-file/index.ts create mode 100644 src/utils/upload-file/upload-file.ts create mode 100644 src/utils/upload-files/index.ts create mode 100644 src/utils/upload-files/upload-files.ts diff --git a/src/lang/en-US/index.json b/src/lang/en-US/index.json index e82bc1a9ee..43f5e9bcc7 100644 --- a/src/lang/en-US/index.json +++ b/src/lang/en-US/index.json @@ -218,6 +218,11 @@ "has": "Contains some of these keys" }, + "drop_to_upload": "Drop to Upload", + "upload_file_indeterminate": "Uploading File | Uploading files {done}/{total}", + "upload_file_success": "File Uploaded | {count} Files Uploaded", + "upload_file_failed": "Couldn't Upload File | Couldn't Upload Files", + "about_directus": "About Directus", "activity_log": "Activity Log", "add_field_filter": "Add a field filter", diff --git a/src/stores/notifications/notifications.ts b/src/stores/notifications/notifications.ts index 5944fa0e2f..6a0f10eec5 100644 --- a/src/stores/notifications/notifications.ts +++ b/src/stores/notifications/notifications.ts @@ -39,6 +39,20 @@ export const useNotificationsStore = createStore({ this.state.queue = this.state.queue.filter((n) => n.id !== id); this.state.previous = [...this.state.previous, toBeRemoved]; }, + update(id: string, updates: Partial) { + this.state.queue = this.state.queue.map(updateIfNeeded); + this.state.previous = this.state.queue.map(updateIfNeeded); + + function updateIfNeeded(notification: Notification) { + if (notification.id === id) { + return { + ...notification, + ...updates, + }; + } + return notification; + } + } }, getters: { lastFour(state) { diff --git a/src/stores/notifications/types.ts b/src/stores/notifications/types.ts index f3ebe97bf6..553a57129b 100644 --- a/src/stores/notifications/types.ts +++ b/src/stores/notifications/types.ts @@ -7,6 +7,9 @@ export interface NotificationRaw { text?: string | VueI18n.TranslateResult; type?: 'info' | 'success' | 'warning' | 'error'; icon?: string | null; + closeable?: boolean; + progress?: number; + loading?: boolean; } export interface Notification extends NotificationRaw { diff --git a/src/utils/upload-file/index.ts b/src/utils/upload-file/index.ts new file mode 100644 index 0000000000..7bdd51039c --- /dev/null +++ b/src/utils/upload-file/index.ts @@ -0,0 +1,4 @@ +import uploadFile from './upload-file'; + +export { uploadFile }; +export default uploadFile; diff --git a/src/utils/upload-file/upload-file.ts b/src/utils/upload-file/upload-file.ts new file mode 100644 index 0000000000..0d7d4e4941 --- /dev/null +++ b/src/utils/upload-file/upload-file.ts @@ -0,0 +1,37 @@ +import api from '@/api'; +import useProjectsStore from '@/stores/projects'; +import notify from '@/utils/notify'; +import i18n from '@/lang'; + +export default async function uploadFile( + file: File, + onProgressChange?: (percentage: number) => void, + showNotifications = true +) { + const progressHandler = onProgressChange || (() => undefined); + const currentProjectKey = useProjectsStore().state.currentProjectKey; + const formData = new FormData(); + formData.append('file', file); + + try { + await api.post(`/${currentProjectKey}/files`, formData, { onUploadProgress }); + + if (showNotifications) { + notify({ + title: i18n.tc('upload_file_success', 0), + type: 'success', + }); + } + } catch (error) { + if (showNotifications) { + notify({ + title: i18n.tc('upload_file_failed', 0), + }); + } + } + + function onUploadProgress(progressEvent: { loaded: number; total: number }) { + const percentCompleted = Math.floor((progressEvent.loaded * 100) / progressEvent.total); + progressHandler(percentCompleted); + } +} diff --git a/src/utils/upload-files/index.ts b/src/utils/upload-files/index.ts new file mode 100644 index 0000000000..38b7b75f4f --- /dev/null +++ b/src/utils/upload-files/index.ts @@ -0,0 +1,4 @@ +import uploadFiles from './upload-files'; + +export { uploadFiles }; +export default uploadFiles; diff --git a/src/utils/upload-files/upload-files.ts b/src/utils/upload-files/upload-files.ts new file mode 100644 index 0000000000..3074a848d0 --- /dev/null +++ b/src/utils/upload-files/upload-files.ts @@ -0,0 +1,36 @@ +import uploadFile from '@/utils/upload-file'; +import notify from '@/utils/notify'; +import i18n from '@/lang'; + +export default async function uploadFiles( + files: File[], + onProgressChange?: (percentages: number[]) => void +) { + const progressHandler = onProgressChange || (() => undefined); + + const progressForFiles = files.map(() => 0); + + try { + await Promise.all( + files.map((file, index) => + uploadFile( + file, + (percentage: number) => { + progressForFiles[index] = percentage; + progressHandler(progressForFiles); + }, + false + ) + ) + ); + notify({ + title: i18n.tc('upload_file_success', files.length, { count: files.length }), + type: 'success', + }); + } catch (error) { + notify({ + title: i18n.tc('upload_file_failed', files.length, { count: files.length }), + type: 'error', + }); + } +} diff --git a/src/views/private/components/drawer-detail/drawer-detail.test.ts b/src/views/private/components/drawer-detail/drawer-detail.test.ts index a5ccd498e2..bd880cfd49 100644 --- a/src/views/private/components/drawer-detail/drawer-detail.test.ts +++ b/src/views/private/components/drawer-detail/drawer-detail.test.ts @@ -5,12 +5,14 @@ import * as GroupableComposition from '@/compositions/groupable/groupable'; import VIcon from '@/components/v-icon'; import VDivider from '@/components/v-divider'; import TransitionExpand from '@/components/transition/expand'; +import VBadge from '@/components/v-badge/'; const localVue = createLocalVue(); localVue.use(VueCompositionAPI); localVue.component('v-icon', VIcon); localVue.component('transition-expand', TransitionExpand); localVue.component('v-divider', VDivider); +localVue.component('v-badge', VBadge); describe('Drawer Detail', () => { it('Uses the useGroupable composition', () => { diff --git a/src/views/private/components/notification-item/notification-item.vue b/src/views/private/components/notification-item/notification-item.vue index df855dfa52..6d0851cfea 100644 --- a/src/views/private/components/notification-item/notification-item.vue +++ b/src/views/private/components/notification-item/notification-item.vue @@ -1,7 +1,9 @@