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 @@