From afe1179bb9a89d3368c6a7863dda4a1dc3b4ffbd Mon Sep 17 00:00:00 2001 From: MontaGhanmy Date: Wed, 11 Dec 2024 10:03:26 +0100 Subject: [PATCH 01/13] staged --- .../atoms/icons-colored/assets/arrow-down.svg | 3 + .../atoms/icons-colored/assets/arrow-up.svg | 3 + .../icons-colored/assets/check-green.svg | 3 + .../app/atoms/icons-colored/assets/folder.svg | 3 + .../src/app/atoms/icons-colored/index.tsx | 12 +++ .../pending-root-list.tsx | 52 +++++++++ .../pending-root-list.tsx | 97 +++++++++++++++++ .../file-uploads/uploads-viewer.tsx | 14 +-- .../app/components/uploads/file-tree-utils.ts | 11 +- .../features/drive/hooks/use-drive-upload.tsx | 5 +- .../features/files/hooks/use-upload-zones.ts | 10 +- .../app/features/files/hooks/use-upload.ts | 6 +- .../files/services/file-upload-service.ts | 102 +++++++++++++----- .../state/atoms/root-pending-files-list.ts | 9 ++ .../files/state/selectors/current-task.ts | 3 + .../body/drive/modals/create/create-link.tsx | 2 +- 16 files changed, 289 insertions(+), 46 deletions(-) create mode 100644 tdrive/frontend/src/app/atoms/icons-colored/assets/arrow-down.svg create mode 100644 tdrive/frontend/src/app/atoms/icons-colored/assets/arrow-up.svg create mode 100644 tdrive/frontend/src/app/atoms/icons-colored/assets/check-green.svg create mode 100644 tdrive/frontend/src/app/atoms/icons-colored/assets/folder.svg create mode 100644 tdrive/frontend/src/app/components/file-uploads/pending-file-components/pending-root-list.tsx create mode 100644 tdrive/frontend/src/app/components/file-uploads/pending-root-components/pending-root-list.tsx create mode 100644 tdrive/frontend/src/app/features/files/state/atoms/root-pending-files-list.ts diff --git a/tdrive/frontend/src/app/atoms/icons-colored/assets/arrow-down.svg b/tdrive/frontend/src/app/atoms/icons-colored/assets/arrow-down.svg new file mode 100644 index 000000000..6d833db8c --- /dev/null +++ b/tdrive/frontend/src/app/atoms/icons-colored/assets/arrow-down.svg @@ -0,0 +1,3 @@ + + + diff --git a/tdrive/frontend/src/app/atoms/icons-colored/assets/arrow-up.svg b/tdrive/frontend/src/app/atoms/icons-colored/assets/arrow-up.svg new file mode 100644 index 000000000..29717e7c4 --- /dev/null +++ b/tdrive/frontend/src/app/atoms/icons-colored/assets/arrow-up.svg @@ -0,0 +1,3 @@ + + + diff --git a/tdrive/frontend/src/app/atoms/icons-colored/assets/check-green.svg b/tdrive/frontend/src/app/atoms/icons-colored/assets/check-green.svg new file mode 100644 index 000000000..fd3a232dd --- /dev/null +++ b/tdrive/frontend/src/app/atoms/icons-colored/assets/check-green.svg @@ -0,0 +1,3 @@ + + + diff --git a/tdrive/frontend/src/app/atoms/icons-colored/assets/folder.svg b/tdrive/frontend/src/app/atoms/icons-colored/assets/folder.svg new file mode 100644 index 000000000..89279d319 --- /dev/null +++ b/tdrive/frontend/src/app/atoms/icons-colored/assets/folder.svg @@ -0,0 +1,3 @@ + + + diff --git a/tdrive/frontend/src/app/atoms/icons-colored/index.tsx b/tdrive/frontend/src/app/atoms/icons-colored/index.tsx index bdd6178fd..74d4c1065 100644 --- a/tdrive/frontend/src/app/atoms/icons-colored/index.tsx +++ b/tdrive/frontend/src/app/atoms/icons-colored/index.tsx @@ -10,6 +10,10 @@ import { ReactComponent as FileTypeUnknownSvg } from './assets/file-type-unknown import { ReactComponent as FileTypeMediaSvg } from './assets/file-type-media.svg'; import { ReactComponent as FileTypeSlidesSvg } from './assets/file-type-slides.svg'; import { ReactComponent as FileTypeLinkSvg } from './assets/file-type-link.svg'; +import { ReactComponent as FolderSvg } from './assets/folder.svg'; +import { ReactComponent as ArrowDownSvg } from './assets/arrow-down.svg'; +import { ReactComponent as ArrowUpSvg } from './assets/arrow-up.svg'; +import { ReactComponent as CheckGreenSvg } from './assets/check-green.svg'; import { ReactComponent as RemoveSvg } from './assets/remove.svg'; import { ReactComponent as SentSvg } from './assets/sent.svg'; @@ -37,6 +41,14 @@ export const FileTypeSlidesIcon = (props: ComponentProps<'svg'>) => ( export const FileTypeLinkIcon = (props: ComponentProps<'svg'>) => ; +export const FolderIcon = (props: ComponentProps<'svg'>) => ; + +export const ArrowDownIcon = (props: ComponentProps<'svg'>) => ; + +export const ArrowUpIcon = (props: ComponentProps<'svg'>) => ; + +export const CheckGreenIcon = (props: ComponentProps<'svg'>) => ; + export const RemoveIcon = (props: ComponentProps<'svg'>) => ; export const SentIcon = (props: ComponentProps<'svg'>) => ; diff --git a/tdrive/frontend/src/app/components/file-uploads/pending-file-components/pending-root-list.tsx b/tdrive/frontend/src/app/components/file-uploads/pending-file-components/pending-root-list.tsx new file mode 100644 index 000000000..8334faa9c --- /dev/null +++ b/tdrive/frontend/src/app/components/file-uploads/pending-file-components/pending-root-list.tsx @@ -0,0 +1,52 @@ +import classNames from 'classnames'; +import { Layout, Row, Col, Typography } from 'antd'; +import PerfectScrollbar from 'react-perfect-scrollbar'; + +import Languages from '@features/global/services/languages-service'; +import { PendingFileRecoilType } from '@features/files/types/file'; +import { useUpload } from '@features/files/hooks/use-upload'; + +import './styles.scss'; + +type PropsType = { + rootPendingFilesState: { [key: string]: PendingFileRecoilType[] }; + visible: boolean; +}; + +const { Text } = Typography; +const { Header, Content } = Layout; +export default ({ rootPendingFilesState, visible }: PropsType) => { + const { getOnePendingFile, currentTask } = useUpload(); + + return Object.keys(rootPendingFilesState || {}).length > 0 ? ( + +
+ + + + {currentTask.total > 0 && `${currentTask.uploaded}/${currentTask.total} `} + {Languages.t('components.drive_dropzone.uploading')} + + + +
+ {Object.keys(rootPendingFilesState || {}) && ( + + + + + -- + + + + + )} +
+ ) : ( + <> + ); +}; diff --git a/tdrive/frontend/src/app/components/file-uploads/pending-root-components/pending-root-list.tsx b/tdrive/frontend/src/app/components/file-uploads/pending-root-components/pending-root-list.tsx new file mode 100644 index 000000000..7011aeae3 --- /dev/null +++ b/tdrive/frontend/src/app/components/file-uploads/pending-root-components/pending-root-list.tsx @@ -0,0 +1,97 @@ +import { useEffect, useState } from 'react'; +import { + FileTypePdfIcon, + FolderIcon, + ArrowDownIcon, + ArrowUpIcon, + CheckGreenIcon, +} from 'app/atoms/icons-colored'; +import { PendingFileRecoilType } from 'app/features/files/types/file'; + +const PendingRootList = ({ + roots, +}: { + roots: { [key: string]: PendingFileRecoilType[] }; +}): JSX.Element => { + const [modalExpanded, setModalExpanded] = useState(true); + const keys = Object.keys(roots || {}); + const rootsInProgress = Object.keys(roots || {}).filter(key => roots[key].length > 0); + console.log('rootsInProgress', rootsInProgress); + const uploadingNmber = `${rootsInProgress.length}/${Object.keys(roots).length}`; + const uploadingPercentage: number = + Math.floor((rootsInProgress.length / Object.keys(roots).length) * 100) || 100; + return ( + <> + {keys.length > 0 && ( +
+
+

+ Uploading {uploadingNmber} files... {`${uploadingPercentage}%`} +

+ +
+ {modalExpanded && ( +
+
+ {keys.map(key => { + const root = roots[key]; + const isFileRoot = key.includes('.'); + const progress = + (root.reduce((acc, file) => { + const progress = file.progress || 0; + return acc + progress; + }, 0) / + root.length) * + 100; + + return ( +
+
+
+ {isFileRoot ? : } +

{key}

+
+ +
+
+
+
+ +
+
+ +
+
+
+
+
+
+ ); + })} +
+
+
+ + +
+
+
+ )} +
+ )} + + ); +}; + +export default PendingRootList; diff --git a/tdrive/frontend/src/app/components/file-uploads/uploads-viewer.tsx b/tdrive/frontend/src/app/components/file-uploads/uploads-viewer.tsx index 552e4b45b..062793d17 100644 --- a/tdrive/frontend/src/app/components/file-uploads/uploads-viewer.tsx +++ b/tdrive/frontend/src/app/components/file-uploads/uploads-viewer.tsx @@ -1,16 +1,10 @@ -import React from 'react'; import { useUpload } from '@features/files/hooks/use-upload'; -import PendingFilesList from './pending-file-components/pending-files-list'; - +import PendingRootList from './pending-root-components/pending-root-list'; const ChatUploadsViewer = (): JSX.Element => { const { currentTask } = useUpload(); - - return ( - 0 && !currentTask.completed} - pendingFilesState={currentTask.files} - /> - ); + const roots = currentTask.roots || {}; + const keys = Object.keys(roots); + return <>{keys.length > 0 && }; }; export default ChatUploadsViewer; diff --git a/tdrive/frontend/src/app/components/uploads/file-tree-utils.ts b/tdrive/frontend/src/app/components/uploads/file-tree-utils.ts index d23fa5c03..f0598c301 100644 --- a/tdrive/frontend/src/app/components/uploads/file-tree-utils.ts +++ b/tdrive/frontend/src/app/components/uploads/file-tree-utils.ts @@ -1,5 +1,5 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -type TreeItem = { [key: string]: File | TreeItem }; +type TreeItem = { [key: string]: { root: string; file: File } | TreeItem }; export type FileTreeObject = { tree: TreeItem; @@ -163,7 +163,10 @@ export const getFilesTree = ( return; } if (dir_index === path.split('/').length - 1) { - dirs[dir] = real_file; + dirs[dir] = { + root: path.split('/')[0], + file: real_file, + }; } else { if (!dirs[dir]) { dirs[dir] = {}; @@ -172,8 +175,8 @@ export const getFilesTree = ( } }); }); - - fcb && fcb(tree, documents_number, total_size); + console.log("tree is:: ", tree); + // fcb && fcb(tree, documents_number, total_size); resolve({ tree, documentsCount: documents_number, totalSize: total_size }); }; diff --git a/tdrive/frontend/src/app/features/drive/hooks/use-drive-upload.tsx b/tdrive/frontend/src/app/features/drive/hooks/use-drive-upload.tsx index e29bfcf03..7b9af7e15 100644 --- a/tdrive/frontend/src/app/features/drive/hooks/use-drive-upload.tsx +++ b/tdrive/frontend/src/app/features/drive/hooks/use-drive-upload.tsx @@ -16,7 +16,7 @@ export const useDriveUpload = () => { const uploadVersion = async (file: File, context: { companyId: string; id: string }) => { return new Promise(r => { - FileUploadService.upload([file], { + FileUploadService.upload([{root: file.name, file}], { context: { companyId: context.companyId, id: context.id, @@ -62,6 +62,7 @@ export const useDriveUpload = () => { for (const parentId of Object.keys(filesPerParentId)) { logger.debug(`Upload files for directory ${parentId}`); expectedUploadsCount += filesPerParentId[parentId].length; + console.log("UP:: TREE IS:: ", filesPerParentId[parentId]); await FileUploadService.upload(filesPerParentId[parentId], { context: { companyId: context.companyId, @@ -114,7 +115,7 @@ export const useDriveUpload = () => { if (request.status != 200) throw new Error(`Unexpected response status code: ${request.status} from ${JSON.stringify(url)}`); const file = new File([request.response], name); - FileUploadService.upload([file], { + FileUploadService.upload([{root: file.name , file}], { context: { companyId: context.companyId, parentId: context.parentId, diff --git a/tdrive/frontend/src/app/features/files/hooks/use-upload-zones.ts b/tdrive/frontend/src/app/features/files/hooks/use-upload-zones.ts index 343e85a50..658b14275 100644 --- a/tdrive/frontend/src/app/features/files/hooks/use-upload-zones.ts +++ b/tdrive/frontend/src/app/features/files/hooks/use-upload-zones.ts @@ -27,7 +27,15 @@ export const useUploadZones = (zoneId: string) => { list: File[], options?: { uploader?: (file: File, context: any) => Promise; context?: any }, ) => { - const newFiles = await FileUploadService.upload(list, options); + const newFiles = await FileUploadService.upload( + list.map((file, index) => { + return { + root: file.name, + file, + }; + }), + options, + ); setFiles([ ...files, ...newFiles.map(f => { diff --git a/tdrive/frontend/src/app/features/files/hooks/use-upload.ts b/tdrive/frontend/src/app/features/files/hooks/use-upload.ts index a03d50a16..59f629b67 100644 --- a/tdrive/frontend/src/app/features/files/hooks/use-upload.ts +++ b/tdrive/frontend/src/app/features/files/hooks/use-upload.ts @@ -2,12 +2,16 @@ import FileUploadService from '@features/files/services/file-upload-service'; import RouterServices from '@features/router/services/router-service'; import { useRecoilState, useRecoilValue } from 'recoil'; import { PendingFilesListState } from '../state/atoms/pending-files-list'; +import { RootPendingFilesListState } from '../state/atoms/root-pending-files-list'; import { CurrentTaskSelector } from '../state/selectors/current-task'; export const useUpload = () => { const { companyId } = RouterServices.getStateFromRoute(); const [pendingFilesListState, setPendingFilesListState] = useRecoilState(PendingFilesListState); - FileUploadService.setRecoilHandler(setPendingFilesListState); + const [rootPendingFilesListState, setRootPendingFilesListState] = + useRecoilState(RootPendingFilesListState); + FileUploadService.setRecoilHandler(setPendingFilesListState, setRootPendingFilesListState); + // FileUploadService.setRecoilHandler(setPendingFilesListState); const currentTask = useRecoilValue(CurrentTaskSelector); diff --git a/tdrive/frontend/src/app/features/files/services/file-upload-service.ts b/tdrive/frontend/src/app/features/files/services/file-upload-service.ts index df93111ab..52ec036e7 100644 --- a/tdrive/frontend/src/app/features/files/services/file-upload-service.ts +++ b/tdrive/frontend/src/app/features/files/services/file-upload-service.ts @@ -10,8 +10,8 @@ import RouterServices from '@features/router/services/router-service'; import _ from 'lodash'; import FileUploadAPIClient from '../api/file-upload-api-client'; import { isPendingFileStatusPending } from '../utils/pending-files'; -import { FileTreeObject } from "components/uploads/file-tree-utils"; -import { DriveApiClient } from "features/drive/api-client/api-client"; +import { FileTreeObject } from 'components/uploads/file-tree-utils'; +import { DriveApiClient } from 'features/drive/api-client/api-client'; import { ToasterService } from 'app/features/global/services/toaster-service'; import Languages from 'app/features/global/services/languages-service'; @@ -22,12 +22,15 @@ export enum Events { const logger = Logger.getLogger('Services/FileUploadService'); class FileUploadService { private pendingFiles: PendingFileType[] = []; + private GroupedPendingFiles: { [key: string]: PendingFileType[] } = {}; public currentTaskId = ''; private recoilHandler: Function = () => undefined; + private rootRecoilHandler: Function = () => undefined; private logger: Logger.Logger = Logger.getLogger('FileUploadService'); - setRecoilHandler(handler: Function) { + setRecoilHandler(handler: Function, rootHandler: Function) { this.recoilHandler = handler; + this.rootRecoilHandler = rootHandler; } notify() { @@ -39,19 +42,53 @@ class FileUploadService { file: f.backendFile, }; }); + const updatedRootState = Object.keys(this.GroupedPendingFiles).reduce( + (acc: any, key: string) => { + const files = this.GroupedPendingFiles[key] + .map((f: PendingFileType) => { + // filter out the files that are not part of the current task + if ( + f.uploadTaskId !== this.currentTaskId || + f.status === 'error' || + f.status === 'success' + ) { + return null; + } + return { + id: f.id, + status: f.status, + progress: f.progress, + file: f.backendFile, + }; + }) + .filter(file => file !== null); // remove null entries from the array + + // Add to the accumulator object + acc[key] = [...files]; + return acc; + }, + {}, + ); this.recoilHandler(_.cloneDeep(updatedState)); + this.rootRecoilHandler(_.cloneDeep(updatedRootState)); } - public async createDirectories(root: FileTreeObject['tree'], context: { companyId: string; parentId: string }) { + public async createDirectories( + root: FileTreeObject['tree'], + context: { companyId: string; parentId: string }, + ) { // Create all directories - const filesPerParentId: { [key: string]: File[] } = {}; - filesPerParentId[context.parentId] = [] + const filesPerParentId: { [key: string]: { root: string; file: File }[] } = {}; + filesPerParentId[context.parentId] = []; const traverserTreeLevel = async (tree: FileTreeObject['tree'], parentId: string) => { for (const directory of Object.keys(tree)) { - if (tree[directory] instanceof File) { + if (tree[directory].file instanceof File) { logger.trace(`${directory} is a file, save it for future upload`); - filesPerParentId[parentId].push(tree[directory] as File); + filesPerParentId[parentId].push({ + root: tree[directory].root as string, + file: tree[directory].file as File, + }); } else { logger.debug(`Create directory ${directory}`); @@ -77,20 +114,20 @@ class FileUploadService { backendFile: null, resumable: null, label: directory, - type: "file", + type: 'file', pausable: false, }; - this.pendingFiles.push(pendingFile); - this.notify(); - try { - const driveItem = await DriveApiClient.create(context.companyId, { item: item, version: {}}); + const driveItem = await DriveApiClient.create(context.companyId, { + item: item, + version: {}, + }); this.logger.debug(`Directory ${directory} created`); pendingFile.status = 'success'; this.notify(); if (driveItem?.id) { - filesPerParentId[driveItem.id] = [] + filesPerParentId[driveItem.id] = []; await traverserTreeLevel(tree[directory] as FileTreeObject['tree'], driveItem.id); } } catch (e) { @@ -99,14 +136,14 @@ class FileUploadService { } } } - } + }; await traverserTreeLevel(root, context.parentId); return filesPerParentId; } public async upload( - fileList: File[], + fileList: { root: string; file: File }[], options?: { context?: any; callback?: (file: FileType | null, context: any) => void; @@ -125,7 +162,7 @@ class FileUploadService { } for (const file of fileList) { - if (!file) continue; + if (!file.file) continue; const pendingFile: PendingFileType = { id: uuid(), @@ -134,21 +171,26 @@ class FileUploadService { lastProgress: new Date().getTime(), speed: 0, uploadTaskId: this.currentTaskId, - originalFile: file, + originalFile: file.file, backendFile: null, resumable: null, - type: "file", + type: 'file', label: null, - pausable: true + pausable: true, }; - this.pendingFiles.push(pendingFile); + console.log('fs:: upload:: pendingFile', pendingFile); + this.pendingFiles.push(pendingFile); + if (!this.GroupedPendingFiles[file.root]) { + this.GroupedPendingFiles[file.root] = []; + } + this.GroupedPendingFiles[file.root].push(pendingFile); this.notify(); // First we create the file object const resource = ( - await FileUploadAPIClient.upload(file, { companyId, ...(options?.context || {}) }) + await FileUploadAPIClient.upload(file.file, { companyId, ...(options?.context || {}) }) )?.resource; if (!resource) { @@ -173,7 +215,7 @@ class FileUploadService { }, }); - pendingFile.resumable.addFile(file); + pendingFile.resumable.addFile(file.file); pendingFile.resumable.on('fileAdded', () => pendingFile.resumable.upload()); @@ -208,9 +250,16 @@ class FileUploadService { pendingFile.resumable.on('fileError', () => { pendingFile.status = 'error'; pendingFile.resumable.cancel(); - const intendedFilename = (pendingFile.originalFile || {}).name || (pendingFile.backendFile || { metadata: {}}).metadata.name; - ToasterService.error(Languages.t('services.file_upload_service.toaster.upload_file_error', [intendedFilename], - 'Error uploading file ' + intendedFilename)); + const intendedFilename = + (pendingFile.originalFile || {}).name || + (pendingFile.backendFile || { metadata: {} }).metadata.name; + ToasterService.error( + Languages.t( + 'services.file_upload_service.toaster.upload_file_error', + [intendedFilename], + 'Error uploading file ' + intendedFilename, + ), + ); options?.callback?.(null, options?.context || {}); this.notify(); }); @@ -335,7 +384,6 @@ class FileUploadService { }); } - public getDownloadRoute({ companyId, fileId }: { companyId: string; fileId: string }): string { return FileUploadAPIClient.getDownloadRoute({ companyId: companyId, diff --git a/tdrive/frontend/src/app/features/files/state/atoms/root-pending-files-list.ts b/tdrive/frontend/src/app/features/files/state/atoms/root-pending-files-list.ts new file mode 100644 index 000000000..d9fd7c570 --- /dev/null +++ b/tdrive/frontend/src/app/features/files/state/atoms/root-pending-files-list.ts @@ -0,0 +1,9 @@ +import { atom } from 'recoil'; +import { PendingFileRecoilType } from '@features/files/types/file'; + +export const RootPendingFilesListState = atom< + { [key: string]: PendingFileRecoilType[] } | undefined +>({ + key: 'RootPendingFilesListState', + default: {}, +}); diff --git a/tdrive/frontend/src/app/features/files/state/selectors/current-task.ts b/tdrive/frontend/src/app/features/files/state/selectors/current-task.ts index 0ff96e3b8..8c3aea396 100644 --- a/tdrive/frontend/src/app/features/files/state/selectors/current-task.ts +++ b/tdrive/frontend/src/app/features/files/state/selectors/current-task.ts @@ -1,11 +1,13 @@ import FileUploadService from '@features/files/services/file-upload-service'; import { selector } from 'recoil'; import { PendingFilesListState } from '../atoms/pending-files-list'; +import { RootPendingFilesListState } from '../atoms/root-pending-files-list'; export const CurrentTaskSelector = selector({ key: 'CurrentTaskFilesSelector', get: ({ get }) => { const list = get(PendingFilesListState); + const rootList = get(RootPendingFilesListState) || {}; const currentTaskFiles = list ? list.filter( @@ -16,6 +18,7 @@ export const CurrentTaskSelector = selector({ : []; return { + roots: rootList, files: currentTaskFiles, total: currentTaskFiles.length, uploaded: currentTaskFiles.filter(f => f.status === 'success').length, diff --git a/tdrive/frontend/src/app/views/client/body/drive/modals/create/create-link.tsx b/tdrive/frontend/src/app/views/client/body/drive/modals/create/create-link.tsx index e83507763..9d981201d 100644 --- a/tdrive/frontend/src/app/views/client/body/drive/modals/create/create-link.tsx +++ b/tdrive/frontend/src/app/views/client/body/drive/modals/create/create-link.tsx @@ -21,7 +21,7 @@ export const CreateLink = () => { type: 'text/uri-list', }); - await FileUploadService.upload([file], { + await FileUploadService.upload([{ root: file.name, file }], { context: { parentId: state.parent_id, }, From 21c1dd369dfee9167f1b09e100ec8bfda356e691 Mon Sep 17 00:00:00 2001 From: MontaGhanmy Date: Mon, 20 Jan 2025 19:37:37 +0100 Subject: [PATCH 02/13] feat: new upload modal/file upload service --- .../src/services/documents/services/index.ts | 7 + .../documents/web/controllers/documents.ts | 2 + .../services/user/services/users/service.ts | 2 + .../node/src/services/user/web/controller.ts | 1 + .../icons-colored/assets/icon-cancel.svg | 3 + .../atoms/icons-colored/assets/icon-pause.svg | 3 + .../icons-colored/assets/icon-resume.svg | 4 + .../icons-colored/assets/icon-show-folder.svg | 11 + .../src/app/atoms/icons-colored/index.tsx | 14 +- .../pending-file-row.tsx | 9 +- .../file-type-icon-map.tsx | 35 ++ .../pending-root-list.tsx | 177 +++--- .../pending-root-row.tsx | 117 ++++ .../app/components/uploads/file-tree-utils.ts | 66 ++- .../features/drive/api-client/api-client.ts | 6 +- .../features/drive/hooks/use-drive-upload.tsx | 124 +++-- .../features/files/hooks/use-exp-upload.ts | 66 +++ .../app/features/files/hooks/use-upload.ts | 19 +- .../files/services/file-upload-service-new.ts | 503 ++++++++++++++++++ .../files/services/file-upload-service.ts | 312 +++++++++-- .../state/atoms/root-pending-files-list.ts | 10 +- .../src/app/features/files/types/file.ts | 17 +- .../src/app/features/files/utils/resumable.js | 2 +- .../app/views/client/body/drive/browser.tsx | 17 +- .../app/views/client/common/disk-usage.tsx | 2 +- 25 files changed, 1320 insertions(+), 209 deletions(-) create mode 100644 tdrive/frontend/src/app/atoms/icons-colored/assets/icon-cancel.svg create mode 100644 tdrive/frontend/src/app/atoms/icons-colored/assets/icon-pause.svg create mode 100644 tdrive/frontend/src/app/atoms/icons-colored/assets/icon-resume.svg create mode 100644 tdrive/frontend/src/app/atoms/icons-colored/assets/icon-show-folder.svg create mode 100644 tdrive/frontend/src/app/components/file-uploads/pending-root-components/file-type-icon-map.tsx create mode 100644 tdrive/frontend/src/app/components/file-uploads/pending-root-components/pending-root-row.tsx create mode 100644 tdrive/frontend/src/app/features/files/hooks/use-exp-upload.ts create mode 100644 tdrive/frontend/src/app/features/files/services/file-upload-service-new.ts diff --git a/tdrive/backend/node/src/services/documents/services/index.ts b/tdrive/backend/node/src/services/documents/services/index.ts index 294d4f8c3..f5aaaae03 100644 --- a/tdrive/backend/node/src/services/documents/services/index.ts +++ b/tdrive/backend/node/src/services/documents/services/index.ts @@ -368,6 +368,7 @@ export class DocumentsService { content: Partial, version: Partial, context: DriveExecutionContext, + tmp = false, ): Promise => { try { const driveItem = getDefaultDriveItem(content, context); @@ -468,6 +469,8 @@ export class DocumentsService { logger.error(error, "πŸš€πŸš€ error:"); } + if (tmp) driveItem.is_in_trash = true; + await this.repository.save(driveItem); driveItemVersion.drive_item_id = driveItem.id; @@ -476,8 +479,12 @@ export class DocumentsService { await this.repository.save(driveItem); + console.log("πŸš€πŸš€ DRIVE ITEM SAVED:: ", driveItem); + //TODO[ASH] update item size only for files, there is not need to do during direcotry creation await updateItemSize(driveItem.parent_id, this.repository, context); + + console.log("πŸš€πŸš€ DRIVE ITEM SIZE UPDATED:: ", driveItem); // If AV feature is enabled, scan the file if (!driveItem.is_directory && globalResolver.services.av?.avEnabled && version) { diff --git a/tdrive/backend/node/src/services/documents/web/controllers/documents.ts b/tdrive/backend/node/src/services/documents/web/controllers/documents.ts index 145706aa8..9aa557cba 100644 --- a/tdrive/backend/node/src/services/documents/web/controllers/documents.ts +++ b/tdrive/backend/node/src/services/documents/web/controllers/documents.ts @@ -47,6 +47,7 @@ export class DocumentsController { Body: { item: Partial; version: Partial; + tmp?: boolean; }; }>, ): Promise => { @@ -78,6 +79,7 @@ export class DocumentsController { item, version, context, + request.body.tmp ); } catch (error) { logger.error({ error: `${error}` }, "Failed to create Drive item"); diff --git a/tdrive/backend/node/src/services/user/services/users/service.ts b/tdrive/backend/node/src/services/user/services/users/service.ts index d72091664..408efc115 100644 --- a/tdrive/backend/node/src/services/user/services/users/service.ts +++ b/tdrive/backend/node/src/services/user/services/users/service.ts @@ -173,6 +173,8 @@ export class UserServiceImpl { findOptions.$in = [["id", options.userIds]]; } + console.log("πŸš€πŸš€ Finding user:: ", findFilter, findOptions, context); + return this.repository.find(findFilter, findOptions, context); } diff --git a/tdrive/backend/node/src/services/user/web/controller.ts b/tdrive/backend/node/src/services/user/web/controller.ts index d13283aa2..87d8ec465 100644 --- a/tdrive/backend/node/src/services/user/web/controller.ts +++ b/tdrive/backend/node/src/services/user/web/controller.ts @@ -130,6 +130,7 @@ export class UsersCrudController const context = getExecutionContext(request); const userIds = request.query.user_ids ? request.query.user_ids.split(",") : []; + console.log("πŸš€πŸš€ userIds:: ", userIds); let users: ListResult; if (request.query.search) { diff --git a/tdrive/frontend/src/app/atoms/icons-colored/assets/icon-cancel.svg b/tdrive/frontend/src/app/atoms/icons-colored/assets/icon-cancel.svg new file mode 100644 index 000000000..62f810e63 --- /dev/null +++ b/tdrive/frontend/src/app/atoms/icons-colored/assets/icon-cancel.svg @@ -0,0 +1,3 @@ + + + diff --git a/tdrive/frontend/src/app/atoms/icons-colored/assets/icon-pause.svg b/tdrive/frontend/src/app/atoms/icons-colored/assets/icon-pause.svg new file mode 100644 index 000000000..22d9c8dcf --- /dev/null +++ b/tdrive/frontend/src/app/atoms/icons-colored/assets/icon-pause.svg @@ -0,0 +1,3 @@ + + + diff --git a/tdrive/frontend/src/app/atoms/icons-colored/assets/icon-resume.svg b/tdrive/frontend/src/app/atoms/icons-colored/assets/icon-resume.svg new file mode 100644 index 000000000..f88c327db --- /dev/null +++ b/tdrive/frontend/src/app/atoms/icons-colored/assets/icon-resume.svg @@ -0,0 +1,4 @@ + + + + diff --git a/tdrive/frontend/src/app/atoms/icons-colored/assets/icon-show-folder.svg b/tdrive/frontend/src/app/atoms/icons-colored/assets/icon-show-folder.svg new file mode 100644 index 000000000..81c03f8ed --- /dev/null +++ b/tdrive/frontend/src/app/atoms/icons-colored/assets/icon-show-folder.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/tdrive/frontend/src/app/atoms/icons-colored/index.tsx b/tdrive/frontend/src/app/atoms/icons-colored/index.tsx index 74d4c1065..67deb4be1 100644 --- a/tdrive/frontend/src/app/atoms/icons-colored/index.tsx +++ b/tdrive/frontend/src/app/atoms/icons-colored/index.tsx @@ -14,6 +14,10 @@ import { ReactComponent as FolderSvg } from './assets/folder.svg'; import { ReactComponent as ArrowDownSvg } from './assets/arrow-down.svg'; import { ReactComponent as ArrowUpSvg } from './assets/arrow-up.svg'; import { ReactComponent as CheckGreenSvg } from './assets/check-green.svg'; +import { ReactComponent as ShowFolderSvg } from './assets/icon-show-folder.svg'; +import { ReactComponent as ResumeSvg } from './assets/icon-resume.svg'; +import { ReactComponent as PauseSvg } from './assets/icon-pause.svg'; +import { ReactComponent as CancelSvg } from './assets/icon-cancel.svg'; import { ReactComponent as RemoveSvg } from './assets/remove.svg'; import { ReactComponent as SentSvg } from './assets/sent.svg'; @@ -47,7 +51,15 @@ export const ArrowDownIcon = (props: ComponentProps<'svg'>) => ) => ; -export const CheckGreenIcon = (props: ComponentProps<'svg'>) => ; +export const CheckGreenIcon = (props: ComponentProps<'svg'>) => ; + +export const ShowFolderIcon = (props: ComponentProps<'svg'>) => ; + +export const ResumeIcon = (props: ComponentProps<'svg'>) => ; + +export const PauseIcon = (props: ComponentProps<'svg'>) => ; + +export const CancelIcon = (props: ComponentProps<'svg'>) => ; export const RemoveIcon = (props: ComponentProps<'svg'>) => ; diff --git a/tdrive/frontend/src/app/components/file-uploads/pending-file-components/pending-file-row.tsx b/tdrive/frontend/src/app/components/file-uploads/pending-file-components/pending-file-row.tsx index c3b78e7dd..4cf1bea1d 100644 --- a/tdrive/frontend/src/app/components/file-uploads/pending-file-components/pending-file-row.tsx +++ b/tdrive/frontend/src/app/components/file-uploads/pending-file-components/pending-file-row.tsx @@ -79,7 +79,11 @@ export default ({ pendingFileState, pendingFile }: PropsType) => { {capitalize(pendingFile?.originalFile.name)} {isPendingFileStatusPause(pendingFile.status) && ( - + ({Languages.t('general.paused')}) )} @@ -150,7 +154,6 @@ export default ({ pendingFileState, pendingFile }: PropsType) => { ) } - onClick={() => pauseOrResumeUpload(pendingFileState.id)} style={{ display: 'flex', alignItems: 'center', @@ -189,7 +192,7 @@ export default ({ pendingFileState, pendingFile }: PropsType) => { type="link" shape="circle" icon={} - onClick={() => cancelUpload(pendingFileState.id)} + // onClick={() => cancelUpload(pendingFileState.id)} style={{ display: 'flex', alignItems: 'center', justifyContent: 'center' }} /> diff --git a/tdrive/frontend/src/app/components/file-uploads/pending-root-components/file-type-icon-map.tsx b/tdrive/frontend/src/app/components/file-uploads/pending-root-components/file-type-icon-map.tsx new file mode 100644 index 000000000..8df50d0e6 --- /dev/null +++ b/tdrive/frontend/src/app/components/file-uploads/pending-root-components/file-type-icon-map.tsx @@ -0,0 +1,35 @@ +import { + FileTypeArchiveIcon, + FileTypeDocumentIcon, + FileTypeSpreadsheetIcon, + FileTypeMediaIcon, + FileTypeSlidesIcon, + FileTypePdfIcon, +} from 'app/atoms/icons-colored'; + +// Map mime types to their respective JSX icon elements +export const fileTypeIconsMap = { + 'application/pdf': , + 'application/vnd.openxmlformats-officedocument.wordprocessingml.document': ( + + ), + 'application/msword': , + 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': , + 'application/vnd.ms-excel': , + 'application/vnd.ms-powerpoint': , + 'application/vnd.openxmlformats-officedocument.presentationml.presentation': ( + + ), + 'application/zip': , + 'application/x-rar-compressed': , + 'application/x-tar': , + 'application/x-7z-compressed': , + 'application/x-bzip': , + 'application/x-bzip2': , + 'application/x-gzip': , + 'video/mp4': , + 'video/mpeg': , + 'video/ogg': , + 'video/webm': , + 'video/quicktime': , +}; diff --git a/tdrive/frontend/src/app/components/file-uploads/pending-root-components/pending-root-list.tsx b/tdrive/frontend/src/app/components/file-uploads/pending-root-components/pending-root-list.tsx index 7011aeae3..d6dde59de 100644 --- a/tdrive/frontend/src/app/components/file-uploads/pending-root-components/pending-root-list.tsx +++ b/tdrive/frontend/src/app/components/file-uploads/pending-root-components/pending-root-list.tsx @@ -1,91 +1,106 @@ -import { useEffect, useState } from 'react'; -import { - FileTypePdfIcon, - FolderIcon, - ArrowDownIcon, - ArrowUpIcon, - CheckGreenIcon, -} from 'app/atoms/icons-colored'; -import { PendingFileRecoilType } from 'app/features/files/types/file'; +import { useState, useMemo, useCallback } from 'react'; +import { useUpload } from '@features/files/hooks/use-upload'; +import { ArrowDownIcon, ArrowUpIcon } from 'app/atoms/icons-colored'; +import { UploadRootListType } from 'app/features/files/types/file'; +import PendingRootRow from './pending-root-row'; -const PendingRootList = ({ - roots, -}: { - roots: { [key: string]: PendingFileRecoilType[] }; -}): JSX.Element => { +const getFilteredRoots = (keys: string[], roots: UploadRootListType) => { + const inProgress = keys.filter(key => roots[key].status === 'uploading'); + const completed = keys.filter(key => roots[key].status === 'completed'); + return { inProgress, completed }; +}; + +interface ModalHeaderProps { + uploadingCount: number; + totalRoots: number; + uploadingPercentage: number; + toggleModal: () => void; + modalExpanded: boolean; +} + +const ModalHeader: React.FC = ({ + uploadingCount, + totalRoots, + uploadingPercentage, + toggleModal, + modalExpanded, +}) => ( +
+

+ Uploading {uploadingCount}/{totalRoots} files... {uploadingPercentage}% +

+ +
+); + +interface ModalFooterProps { + pauseOrResumeUpload: () => void; + cancelUpload: () => void; + isPaused: () => boolean; +} + +const ModalFooter: React.FC = ({ + pauseOrResumeUpload, + cancelUpload, + isPaused, +}) => ( +
+
+ + +
+
+); + +const PendingRootList = ({ roots }: { roots: UploadRootListType }): JSX.Element => { const [modalExpanded, setModalExpanded] = useState(true); - const keys = Object.keys(roots || {}); - const rootsInProgress = Object.keys(roots || {}).filter(key => roots[key].length > 0); - console.log('rootsInProgress', rootsInProgress); - const uploadingNmber = `${rootsInProgress.length}/${Object.keys(roots).length}`; - const uploadingPercentage: number = - Math.floor((rootsInProgress.length / Object.keys(roots).length) * 100) || 100; + const { pauseOrResumeUpload, isPaused, cancelUpload } = useUpload(); + const keys = useMemo(() => Object.keys(roots || {}), [roots]); + + const { inProgress: rootsInProgress } = useMemo( + () => getFilteredRoots(keys, roots), + [keys, roots], + ); + + const totalRoots = keys.length; + const uploadingCount = rootsInProgress.length; + const uploadingPercentage = Math.floor((uploadingCount / totalRoots) * 100) || 100; + + const toggleModal = useCallback(() => setModalExpanded(prev => !prev), []); + return ( <> - {keys.length > 0 && ( -
-
-

- Uploading {uploadingNmber} files... {`${uploadingPercentage}%`} -

- -
+ {totalRoots > 0 && ( +
+ + {modalExpanded && (
-
- {keys.map(key => { - const root = roots[key]; - const isFileRoot = key.includes('.'); - const progress = - (root.reduce((acc, file) => { - const progress = file.progress || 0; - return acc + progress; - }, 0) / - root.length) * - 100; - - return ( -
-
-
- {isFileRoot ? : } -

{key}

-
- -
-
-
-
- -
-
- -
-
-
-
-
-
- ); - })} -
-
-
- - -
+
+ {keys.map(key => ( + + ))}
+
)}
diff --git a/tdrive/frontend/src/app/components/file-uploads/pending-root-components/pending-root-row.tsx b/tdrive/frontend/src/app/components/file-uploads/pending-root-components/pending-root-row.tsx new file mode 100644 index 000000000..922b9e8f9 --- /dev/null +++ b/tdrive/frontend/src/app/components/file-uploads/pending-root-components/pending-root-row.tsx @@ -0,0 +1,117 @@ +import { useEffect, useState, useCallback } from 'react'; +import { useUpload } from '@features/files/hooks/use-upload'; +import RouterService from '@features/router/services/router-service'; +import { UploadRootType } from 'app/features/files/types/file'; +import { + FileTypeUnknownIcon, + FolderIcon, + CheckGreenIcon, + PauseIcon, + CancelIcon, + ResumeIcon, + ShowFolderIcon, +} from 'app/atoms/icons-colored'; +import { fileTypeIconsMap } from './file-type-icon-map'; + +const PendingRootRow = ({ + rootKey, + root, +}: { + rootKey: string; + root: UploadRootType; +}): JSX.Element => { + const { pauseOrResumeRootUpload, cancelRootUpload, clearRoots } = useUpload(); + const [showFolder, setShowFolder] = useState(false); + + const firstPendingFile = root.items[0]; + const uploadedFilesSize = root.uploadedSize; + const uploadProgress = Math.floor((uploadedFilesSize / root.size) * 100); + const isUploadCompleted = root.status === 'completed'; + const isFileRoot = rootKey.includes('.'); + + // Callback function to open the folder after the upload is completed + const handleShowFolder = useCallback(() => { + if (!showFolder || isFileRoot) return; + const parentId = firstPendingFile.parentId; + RouterService.push(RouterService.generateRouteFromState({ dirId: parentId || '' })); + clearRoots(); + }, [showFolder, isFileRoot, root, clearRoots]); + + // Function to determine the icon for the root + // If the root is a file, it will show the file icon based on the content type + // If the root is a folder, it will show the folder icon + const itemTypeIcon = useCallback( + (type: string) => + isFileRoot ? ( + fileTypeIconsMap[type as keyof typeof fileTypeIconsMap] || + ) : ( + + ), + [isFileRoot], + ); + + // A timeout to show the folder icon after the upload is completed + // This is to give a visual feedback to the user and will be shown shortly + // after the green check icon appears + useEffect(() => { + if (isUploadCompleted) { + const timeout = setTimeout(() => setShowFolder(true), 1500); + return () => clearTimeout(timeout); + } + }, [isUploadCompleted]); + + return ( +
+
+
+ {itemTypeIcon(firstPendingFile?.type)} +

{rootKey}

+ +
+ {isUploadCompleted ? ( + + ) : ( + firstPendingFile?.status !== 'cancel' && + firstPendingFile?.status !== 'error' && ( + <> + + + + ) + )} +
+
+
+ +
+ {!showFolder && ( +
+
+
+ )} +
+
+ ); +}; + +export default PendingRootRow; diff --git a/tdrive/frontend/src/app/components/uploads/file-tree-utils.ts b/tdrive/frontend/src/app/components/uploads/file-tree-utils.ts index f0598c301..f334d57d1 100644 --- a/tdrive/frontend/src/app/components/uploads/file-tree-utils.ts +++ b/tdrive/frontend/src/app/components/uploads/file-tree-utils.ts @@ -119,6 +119,7 @@ export const getFilesTree = ( }); } + let timeBegin = Date.now(); [].slice.call(items).forEach(function (entry: any) { entry = entry.webkitGetAsEntry(); if (entry) { @@ -134,21 +135,29 @@ export const getFilesTree = ( resolve(true); }, resolve.bind(null, true)); } else if (entry.isDirectory) { + console.log('GetFilesTree:: entriesApi: readDirectory'); + const timeToRead = Date.now(); readDirectory(entry, null, resolve); + console.log('GetFilesTree:: entriesApi: readDirectory: ', Date.now() - timeToRead); } }), ); } }); + console.log('GetFilesTree:: entriesApi: slice.call: ', Date.now() - timeBegin); + console.log('GetFilesTree:: entriesApi: rootPromises: ', rootPromises.length, rootPromises); if (files.length > 1000000) { return false; } + timeBegin = Date.now(); Promise.all(rootPromises).then(cb.bind(null, fd, files)); + console.log('GetFilesTree:: entriesApi: solvePromises: ', Date.now() - timeBegin); } const cb = function (event: Event, files: File[], paths?: string[]) { + const begin = Date.now(); const documents_number = paths ? paths.length : 0; let total_size = 0; const tree: any = {}; @@ -175,31 +184,72 @@ export const getFilesTree = ( } }); }); - console.log("tree is:: ", tree); + console.log('GetFilesTree:: cb: ', (Date.now() - begin) / 1000); + console.log('tree is:: ', tree); // fcb && fcb(tree, documents_number, total_size); resolve({ tree, documentsCount: documents_number, totalSize: total_size }); }; + // Handle file input based on the event type, starting with `dataTransfer` for drag-and-drop events if (event.dataTransfer) { + console.log('GetFilesTree:: event.dataTransfer'); const dt = event.dataTransfer; + + // When dragging files into the browser, `dataTransfer.items` contains a list of the dragged items. + // `webkitGetAsEntry` allows access to a directory-like API, letting us explore folders and subfolders. + // This means we can recursively scan for files in folders without relying on manual user input. if (dt.items && dt.items.length && 'webkitGetAsEntry' in dt.items[0]) { + console.log('GetFilesTree:: webkitGetAsEntry'); + // Use `entriesApi` to iterate through items, handling directories and files. + // This is ideal for cases where users drag entire folder structures into the app. entriesApi(dt.items, (files, paths) => cb(event, files || [], paths)); - } else if ('getFilesAndDirectories' in dt) { + } + // If `getFilesAndDirectories` is available on `dataTransfer`, it indicates a newer API is supported. + // This API directly provides both files and directories, making it easier to process structured uploads. + else if ('getFilesAndDirectories' in dt) { + console.log('GetFilesTree:: getFilesAndDirectories'); + // Use `newDirectoryApi` to process files and directories in a standardized way. newDirectoryApi(dt, (files, paths) => cb(event, files || [], paths)); - } else if (dt.files) { + } + // If neither of the advanced APIs (`webkitGetAsEntry` or `getFilesAndDirectories`) is available, + // fall back to using the basic `dataTransfer.files` property. + // This works only for files, meaning directories won’t be detected or handled. + else if (dt.files) { + // Use `arrayApi` to process the flat list of files. arrayApi(dt, (files, paths) => cb(event, files || [], paths)); - } else cb(event, [], []); - } else if (event.target) { + } + // If no files or directories can be detected (e.g., if the user drops something invalid), + // return an empty response to ensure the application doesn’t break. + else cb(event, [], []); + } + // If the event comes from a file input field rather than drag-and-drop (`event.target` exists): + else if (event.target) { const t = event.target as any; + + // When a file input element (``) is used, it stores the selected files in `target.files`. + // This is the standard way for users to upload files through a file picker dialog. if (t.files && t.files.length) { + // Process the selected files as a flat array using `arrayApi`. arrayApi(t, (files, paths) => cb(event, files || [], paths)); - } else if ('getFilesAndDirectories' in t) { + } + // If the input element supports `getFilesAndDirectories`, handle structured uploads. + // This could occur in custom or enhanced file inputs that allow folder selection. + else if ('getFilesAndDirectories' in t) { newDirectoryApi(t, (files, paths) => cb(event, files || [], paths)); - } else { + } + // If no valid files or directories can be detected, return an empty response. + else { cb(event, [], []); } - } else { + } + // Fallback for cases where neither `dataTransfer` nor `target` is available: + // This typically occurs in unusual scenarios, such as handling a manually triggered upload. + else { + // If a callback (`fcb`) is provided, call it with the first file found (if any). + // This is a last-resort assumption that `event.target.files` has at least one valid file. fcb && fcb([(event.target as any).files[0]], 1, (event.target as any).files[0].size); + + // Resolve the promise with a default response, treating the single file as the entire tree. resolve({ tree: (event.target as any).files[0], documentsCount: 1, diff --git a/tdrive/frontend/src/app/features/drive/api-client/api-client.ts b/tdrive/frontend/src/app/features/drive/api-client/api-client.ts index e0f897573..3c5bcdecd 100644 --- a/tdrive/frontend/src/app/features/drive/api-client/api-client.ts +++ b/tdrive/frontend/src/app/features/drive/api-client/api-client.ts @@ -106,17 +106,17 @@ export class DriveApiClient { static async create( companyId: string, - data: { item: Partial; version?: Partial }, + data: { item: Partial; version?: Partial; tmp?: boolean }, ) { if (!data.version) data.version = {} as Partial; return new Promise((resolve, reject) => { Api.post< - { item: Partial; version: Partial }, + { item: Partial; version: Partial; tmp?: boolean }, DriveItem >( `/internal/services/documents/v1/companies/${companyId}/item${appendTdriveToken()}`, - data as { item: Partial; version: Partial }, + data as { item: Partial; version: Partial; tmp?: boolean }, (res) => { if ((res as any)?.statusCode || (res as any)?.error) { reject(res); diff --git a/tdrive/frontend/src/app/features/drive/hooks/use-drive-upload.tsx b/tdrive/frontend/src/app/features/drive/hooks/use-drive-upload.tsx index 7b9af7e15..604312478 100644 --- a/tdrive/frontend/src/app/features/drive/hooks/use-drive-upload.tsx +++ b/tdrive/frontend/src/app/features/drive/hooks/use-drive-upload.tsx @@ -10,13 +10,13 @@ import Logger from '@features/global/framework/logger-service'; * @returns */ export const useDriveUpload = () => { - const { create, refresh } = useDriveActions(); + const { create, refresh, restore } = useDriveActions(); - const logger = Logger.getLogger('useDriveUpload') + const logger = Logger.getLogger('useDriveUpload'); const uploadVersion = async (file: File, context: { companyId: string; id: string }) => { return new Promise(r => { - FileUploadService.upload([{root: file.name, file}], { + FileUploadService.upload([{ root: file.name, file }], { context: { companyId: context.companyId, id: context.id, @@ -48,58 +48,66 @@ export const useDriveUpload = () => { context: { companyId: string; parentId: string }, ) => { // Create all directories - logger.debug("Start creating directories ..."); - const filesPerParentId = await FileUploadService.createDirectories(tree.tree, context); - await refresh(context.parentId, true); - logger.debug("All directories created"); - - // Upload files into directories - logger.debug("Start file uploading") - //create counter to calculate number of uploaded files, and refresh browsing window only when all the files were uploaded - let expectedUploadsCount = 0; - const parentFolder = context.parentId; - let uploadedFilesCount = 0; - for (const parentId of Object.keys(filesPerParentId)) { - logger.debug(`Upload files for directory ${parentId}`); - expectedUploadsCount += filesPerParentId[parentId].length; - console.log("UP:: TREE IS:: ", filesPerParentId[parentId]); - await FileUploadService.upload(filesPerParentId[parentId], { - context: { - companyId: context.companyId, - parentId: parentId, - }, - callback: (file, context) => { - logger.debug('created file: ', file); - uploadedFilesCount++; - if (file) { - create( - { - company_id: context.companyId, - workspace_id: 'drive', //We don't set workspace ID for now - parent_id: context.parentId, - name: file.metadata?.name, - size: file.upload_data?.size, - }, - { - provider: 'internal', - application_id: '', - file_metadata: { - name: file.metadata?.name, - size: file.upload_data?.size, - mime: file.metadata?.mime, - thumbnails: file?.thumbnails, - source: 'internal', - external_id: file.id, - }, - }, - ); - } - if (uploadedFilesCount == expectedUploadsCount) { - refresh(parentFolder, true); - } - }, - }); - } + logger.debug('Start creating directories ...'); + const { filesPerParentId, idsToBeRestored } = await FileUploadService.createDirectories( + tree.tree, + context, + ); + idsToBeRestored.map(async (id: string) => { + await restore(id, context.parentId); + }); + refresh(context.parentId, true); + // await refresh(context.parentId, true); + // logger.debug("All directories created"); + // // Upload files into directories + // logger.debug("Start file uploading") + // //create counter to calculate number of uploaded files, and refresh browsing window only when all the files were uploaded + // let expectedUploadsCount = 0; + // const parentFolder = context.parentId; + // let uploadedFilesCount = 0; + // for (const parentId of Object.keys(filesPerParentId)) { + // logger.debug(`Upload files for directory ${parentId}`); + // expectedUploadsCount += filesPerParentId[parentId].length; + // await FileUploadService.upload(filesPerParentId[parentId], { + // context: { + // companyId: context.companyId, + // parentId: parentId, + // }, + // callback: (file, context) => { + // logger.debug('created file: ', file); + // uploadedFilesCount++; + // if (file) { + // create( + // { + // company_id: context.companyId, + // workspace_id: 'drive', //We don't set workspace ID for now + // parent_id: context.parentId, + // name: file.metadata?.name, + // size: file.upload_data?.size, + // }, + // { + // provider: 'internal', + // application_id: '', + // file_metadata: { + // name: file.metadata?.name, + // size: file.upload_data?.size, + // mime: file.metadata?.mime, + // thumbnails: file?.thumbnails, + // source: 'internal', + // external_id: file.id, + // }, + // }, + // ); + // } + // if (uploadedFilesCount == expectedUploadsCount) { + // idsToBeRestored.map( async (id: string) => { + // await restore(id, parentFolder); + // }); + // refresh(parentFolder, true); + // } + // }, + // }); + // } }; const uploadFromUrl = ( @@ -113,9 +121,11 @@ export const useDriveUpload = () => { request.onload = function () { try { if (request.status != 200) - throw new Error(`Unexpected response status code: ${request.status} from ${JSON.stringify(url)}`); + throw new Error( + `Unexpected response status code: ${request.status} from ${JSON.stringify(url)}`, + ); const file = new File([request.response], name); - FileUploadService.upload([{root: file.name , file}], { + FileUploadService.upload([{ root: file.name, file }], { context: { companyId: context.companyId, parentId: context.parentId, diff --git a/tdrive/frontend/src/app/features/files/hooks/use-exp-upload.ts b/tdrive/frontend/src/app/features/files/hooks/use-exp-upload.ts new file mode 100644 index 000000000..6b3b7249c --- /dev/null +++ b/tdrive/frontend/src/app/features/files/hooks/use-exp-upload.ts @@ -0,0 +1,66 @@ +import RouterServices from '@features/router/services/router-service'; +import Resumable from '@features/files/utils/resumable'; + +/* eslint-disable @typescript-eslint/no-explicit-any */ +type TreeItem = { [key: string]: { root: string; file: File } | TreeItem }; + +export type FileTreeObject = { + tree: TreeItem; + documentsCount: number; + totalSize: number; +}; + +export const useUploadExp = () => { + const { companyId } = RouterServices.getStateFromRoute(); + + // Initialize a single Resumable instance + const resumable = new Resumable({ + target: `/upload/${companyId}`, // Example API endpoint, adjust as needed + chunkSize: 1 * 1024 * 1024, // 1 MB chunk size + simultaneousUploads: 3, + testChunks: false, + }); + + // Add files from the tree to the Resumable instance + const addFilesToResumable = (tree: TreeItem) => { + const traverseTree = (item: TreeItem) => { + Object.values(item).forEach(value => { + if ('file' in value && value.file instanceof File) { + resumable.addFile(value.file); // Add file to Resumable + } else { + traverseTree(value as TreeItem); // Recursively traverse nested items + } + }); + }; + + traverseTree(tree); + }; + + // Start upload and log each file being uploaded + const startUpload = () => { + resumable.on('fileAdded', (file: any) => { + console.log(`Uploading file: ${file.fileName}`); + }); + + resumable.on('fileSuccess', (file: any, message: any) => { + console.log(`File uploaded successfully: ${file.fileName}`, message); + }); + + resumable.on('fileError', (file: any, error: any) => { + console.error(`Error uploading file: ${file.fileName}`, error); + }); + + resumable.upload(); // Start the upload + }; + + // Main uploadTree function + const uploadTree = (tree: FileTreeObject) => { + console.log('Uploading tree:: ', tree, ' for company: ', companyId); + addFilesToResumable(tree.tree); // Add files from the tree + startUpload(); // Start uploading + }; + + return { + uploadTree, + }; +}; diff --git a/tdrive/frontend/src/app/features/files/hooks/use-upload.ts b/tdrive/frontend/src/app/features/files/hooks/use-upload.ts index 59f629b67..dc2fbbdff 100644 --- a/tdrive/frontend/src/app/features/files/hooks/use-upload.ts +++ b/tdrive/frontend/src/app/features/files/hooks/use-upload.ts @@ -8,16 +8,19 @@ import { CurrentTaskSelector } from '../state/selectors/current-task'; export const useUpload = () => { const { companyId } = RouterServices.getStateFromRoute(); const [pendingFilesListState, setPendingFilesListState] = useRecoilState(PendingFilesListState); - const [rootPendingFilesListState, setRootPendingFilesListState] = + const [_rootPendingFilesListState, setRootPendingFilesListState] = useRecoilState(RootPendingFilesListState); FileUploadService.setRecoilHandler(setPendingFilesListState, setRootPendingFilesListState); - // FileUploadService.setRecoilHandler(setPendingFilesListState); const currentTask = useRecoilValue(CurrentTaskSelector); - const pauseOrResumeUpload = (id: string) => FileUploadService.pauseOrResume(id); + const pauseOrResumeUpload = () => FileUploadService.pauseOrResume(); - const cancelUpload = (id: string) => FileUploadService.cancel(id); + const pauseOrResumeRootUpload = (id: string) => FileUploadService.pauseOrResumeRoot(id); + + const cancelUpload = () => FileUploadService.cancelUpload(); + + const cancelRootUpload = (id: string) => FileUploadService.cancelRoot(id); const getOnePendingFile = (id: string) => FileUploadService.getPendingFile(id); @@ -27,13 +30,21 @@ export const useUpload = () => { const retryUpload = (id: string) => FileUploadService.retry(id); + const clearRoots = () => FileUploadService.clearRoots(); + + const isPaused = () => FileUploadService.getPauseStatus(); + return { pendingFilesListState, pauseOrResumeUpload, + pauseOrResumeRootUpload, cancelUpload, + cancelRootUpload, getOnePendingFile, currentTask, deleteOneFile, retryUpload, + clearRoots, + isPaused, }; }; diff --git a/tdrive/frontend/src/app/features/files/services/file-upload-service-new.ts b/tdrive/frontend/src/app/features/files/services/file-upload-service-new.ts new file mode 100644 index 000000000..c9354e730 --- /dev/null +++ b/tdrive/frontend/src/app/features/files/services/file-upload-service-new.ts @@ -0,0 +1,503 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +/* eslint-disable @typescript-eslint/ban-types */ +import { v1 as uuid } from 'uuid'; + +import JWTStorage from '@features/auth/jwt-storage-service'; +import { FileType, PendingFileType } from '@features/files/types/file'; +import Resumable from '@features/files/utils/resumable'; +import Logger from '@features/global/framework/logger-service'; +import RouterServices from '@features/router/services/router-service'; +import _ from 'lodash'; +import FileUploadAPIClient from '../api/file-upload-api-client'; +import { isPendingFileStatusPending } from '../utils/pending-files'; +import { FileTreeObject } from 'components/uploads/file-tree-utils'; +import { DriveApiClient } from 'features/drive/api-client/api-client'; +import { ToasterService } from 'app/features/global/services/toaster-service'; +import Languages from 'app/features/global/services/languages-service'; +import { DriveItem, DriveItemVersion } from 'app/features/drive/types'; + +export enum Events { + ON_CHANGE = 'notify', +} + +const logger = Logger.getLogger('Services/FileUploadService'); +class FileUploadService { + private isPaused = false; + private isCancelled = false; + private pendingFiles: PendingFileType[] = []; + private GroupedPendingFiles: { [key: string]: PendingFileType[] } = {}; + private RootSizes: { [key: string]: number } = {}; + private GroupIds: { [key: string]: string } = {}; + public currentTaskId = ''; + private recoilHandler: Function = () => undefined; + private rootRecoilHandler: Function = () => undefined; + private logger: Logger.Logger = Logger.getLogger('FileUploadService'); + + setRecoilHandler(handler: Function, rootHandler: Function) { + this.recoilHandler = handler; + this.rootRecoilHandler = rootHandler; + } + + /** + * Helper method to pause execution when `isPaused` is true. + * @private + */ + async _waitWhilePaused() { + while (this.isPaused) { + if (this.isCancelled) return; + await new Promise(resolve => setTimeout(resolve, 100)); // Check every 100ms + } + } + + /** + * Helper method to cancel execution when `isCancelled` is true. + * @private + */ + private async checkCancellation() { + if (this.isCancelled) { + logger.warn('Operation cancelled.'); + throw new Error('Upload process cancelled.'); + } + } + + notify() { + const updatedState = this.pendingFiles.map((f: PendingFileType) => { + return { + id: f.id, + status: f.status, + progress: f.progress, + file: f.originalFile?.type, + }; + }); + const updatedRootState = Object.keys(this.GroupedPendingFiles).reduce( + (acc: any, key: string) => { + // uploaded size + const uploadedSize = this.GroupedPendingFiles[key] + .map((f: PendingFileType) => { + // if the file is successful and originalFile exists, add the size to the accumulator + if (f.status === 'success' && f.originalFile?.size) { + return f.originalFile.size; + } + return 0; + }) + .reduce((acc: number, size: number) => acc + size, 0); + // Add to the accumulator object + acc[key] = { + size: this.RootSizes[key], + status: this.isPaused, + items: [], + uploadedSize, + }; + return acc; + }, + {}, + ); + this.recoilHandler(_.cloneDeep(updatedState)); + this.rootRecoilHandler(_.cloneDeep(updatedRootState)); + } + + public async createDirectories( + root: FileTreeObject['tree'], + context: { companyId: string; parentId: string }, + ) { + // loop through the tree and determine root sizes and set it to the rootSizes object + const traverser = (tree: FileTreeObject['tree']) => { + for (const directory of Object.keys(tree)) { + const root = tree[directory].root as string; + if (tree[directory].file instanceof File) { + // if root is not in the rootSizes object, add it + if (!this.RootSizes[root] && !this.isCancelled) { + this.RootSizes[root] = 0; + } + this.RootSizes[root] += (tree[directory].file as File).size; + } else { + traverser(tree[directory] as FileTreeObject['tree']); + } + } + }; + traverser(root); + // Create all directories + const filesPerParentId: { [key: string]: { root: string; file: File }[] } = {}; + filesPerParentId[context.parentId] = []; + const idsToBeRestored: string[] = []; + + const traverserTreeLevel = async ( + tree: FileTreeObject['tree'], + parentId: string, + tmp = false, + ) => { + // cancel upload + if (this.isCancelled) return; + + // check if upload is paused + await this._waitWhilePaused(); + + // start descending the tree + for (const directory of Object.keys(tree)) { + await this.checkCancellation(); + await this._waitWhilePaused(); + if (tree[directory].file instanceof File && !this.isCancelled) { + logger.trace(`${directory} is a file, save it for future upload`); + filesPerParentId[parentId].push({ + root: tree[directory].root as string, + file: tree[directory].file as File, + }); + } else { + logger.debug(`Create directory ${directory}`); + + const item = { + company_id: context.companyId, + parent_id: parentId, + name: directory, + is_directory: true, + }; + + if (!this.pendingFiles.some(f => isPendingFileStatusPending(f.status))) { + //New upload task when all previous task is finished + this.currentTaskId = uuid(); + } + const pendingFile: PendingFileType = { + id: uuid(), + status: 'pending', + progress: 0, + lastProgress: new Date().getTime(), + speed: 0, + uploadTaskId: this.currentTaskId, + originalFile: null, + backendFile: null, + resumable: null, + label: directory, + type: 'file', + pausable: false, + }; + + try { + const driveItem = await DriveApiClient.create(context.companyId, { + item: item, + version: {}, + tmp, + }); + this.GroupIds[directory] = driveItem.id; + this.logger.debug(`Directory ${directory} created`); + pendingFile.status = 'success'; + this.notify(); + if (driveItem?.id) { + filesPerParentId[driveItem.id] = []; + if (tmp && idsToBeRestored.includes(driveItem.id)) idsToBeRestored.push(driveItem.id); + await traverserTreeLevel(tree[directory] as FileTreeObject['tree'], driveItem.id); + } + } catch (e) { + this.logger.error(e); + throw new Error('Could not create directory'); + } + } + } + // uploading the files goes here + await this.upload(filesPerParentId[parentId], { + context: { + companyId: context.companyId, + parentId: parentId, + }, + callback: async (file, context) => { + if (file) { + const item = { + company_id: context.companyId, + workspace_id: 'drive', //We don't set workspace ID for now + parent_id: context.parentId, + name: file.metadata?.name, + size: file.upload_data?.size, + } as Partial; + const version = { + provider: 'internal', + application_id: '', + file_metadata: { + name: file.metadata?.name, + size: file.upload_data?.size, + mime: file.metadata?.mime, + thumbnails: file?.thumbnails, + source: 'internal', + external_id: file.id, + }, + } as Partial; + const driveFile = await DriveApiClient.create(context.companyId, { item, version }); + } + }, + }); + }; + + // split the tree per root + const rootKeys = Object.keys(root); + const rootTrees = rootKeys.map(key => { + return { [key]: root[key] }; + }); + + // tree promises + const treePromises = rootTrees.map(tree => { + return traverserTreeLevel(tree, context.parentId, true); + }); + + try { + await Promise.all(treePromises); + } catch (error) { + if (!this.isCancelled) { + console.error('An error occurred while processing treePromises:', error); + // Optionally, handle the error or rethrow it + throw error; // Re-throw the error if necessary + } else { + console.warn('Operation was cancelled. Error ignored.'); + } + } + + // await traverserTreeLevel(root, context.parentId, true); + + return { filesPerParentId, idsToBeRestored }; + } + + public async upload( + fileList: { root: string; file: File }[], + options?: { + context?: any; + callback?: (file: FileType | null, context: any) => void; + }, + ): Promise { + const { companyId } = RouterServices.getStateFromRoute(); + + if (!fileList || !companyId) { + this.logger.log('FileList or companyId is undefined', [fileList, companyId]); + return []; + } + + if (!this.pendingFiles.some(f => isPendingFileStatusPending(f.status))) { + //New upload task when all previous task is finished + this.currentTaskId = uuid(); + } + + for (const file of fileList) { + if (!file.file) continue; + + const pendingFile: PendingFileType = { + id: uuid(), + status: 'pending', + progress: 0, + lastProgress: new Date().getTime(), + speed: 0, + uploadTaskId: this.currentTaskId, + originalFile: file.file, + backendFile: null, + resumable: null, + type: 'file', + label: null, + pausable: true, + }; + + this.pendingFiles.push(pendingFile); + if (!this.GroupedPendingFiles[file.root]) { + this.GroupedPendingFiles[file.root] = []; + } + this.GroupedPendingFiles[file.root].push(pendingFile); + this.notify(); + + // First we create the file object + const resource = ( + await FileUploadAPIClient.upload(file.file, { companyId, ...(options?.context || {}) }) + )?.resource; + + if (!resource) { + throw new Error('A server error occured'); + } + + pendingFile.backendFile = resource; + this.notify(); + + // Then we overwrite the file object with resumable + pendingFile.resumable = this.getResumableInstance({ + target: FileUploadAPIClient.getRoute({ + companyId, + fileId: pendingFile.backendFile.id, + fullApiRouteUrl: true, + }), + query: { + thumbnail_sync: 1, + }, + headers: { + Authorization: JWTStorage.getAutorizationHeader(), + }, + }); + + pendingFile.resumable.addFile(file.file); + + pendingFile.resumable.on('fileAdded', () => pendingFile.resumable.upload()); + + pendingFile.resumable.on('fileProgress', (f: any) => { + const bytesDelta = + (f.progress() - pendingFile.progress) * (pendingFile?.originalFile?.size || 0); + const timeDelta = new Date().getTime() - pendingFile.lastProgress; + + // To avoid jumping time ? + if (timeDelta > 1000) { + pendingFile.speed = bytesDelta / timeDelta; + } + + pendingFile.backendFile = f; + pendingFile.lastProgress = new Date().getTime(); + pendingFile.progress = f.progress(); + this.notify(); + }); + + pendingFile.resumable.on('fileSuccess', (_f: any, message: string) => { + try { + pendingFile.backendFile = JSON.parse(message).resource; + pendingFile.status = 'success'; + options?.callback?.(pendingFile.backendFile, options?.context || {}); + this.notify(); + } catch (e) { + logger.error(`Error on fileSuccess Event`, e); + } + }); + + pendingFile.resumable.on('fileError', () => { + pendingFile.status = 'error'; + pendingFile.resumable.cancel(); + const intendedFilename = + (pendingFile.originalFile || {}).name || + (pendingFile.backendFile || { metadata: {} }).metadata.name; + ToasterService.error( + Languages.t( + 'services.file_upload_service.toaster.upload_file_error', + [intendedFilename], + 'Error uploading file ' + intendedFilename, + ), + ); + options?.callback?.(null, options?.context || {}); + this.notify(); + }); + } + + return this.pendingFiles.filter(f => f.uploadTaskId === this.currentTaskId); + } + + public cancelUpload() { + this.isCancelled = true; + + // pause or resume the resumable tasks + const fileToCancel = this.pendingFiles; + + if (!fileToCancel) { + console.error(`No files found for id`); + return; + } + + for (const file of fileToCancel) { + if (file.status === 'success') continue; + + try { + if (file.resumable) { + file.resumable.cancel(); + if (file.backendFile) + this.deleteOneFile({ + companyId: file.backendFile.company_id, + fileId: file.backendFile.id, + }); + } else { + console.warn('Resumable object is not available for file', file); + } + } catch (error) { + console.error('Error while pausing or resuming file', file, error); + } + } + + // clean everything + this.pendingFiles = []; + this.GroupedPendingFiles = {}; + this.RootSizes = {}; + this.GroupIds = {}; + + this.notify(); + } + + public pauseOrResume() { + // pause or resume the curent upload task + this.isPaused = !this.isPaused; + + // pause or resume the resumable tasks + const fileToCancel = this.pendingFiles; + + if (!fileToCancel) { + console.error(`No files found for id`); + return; + } + + for (const file of fileToCancel) { + if (file.status === 'success') continue; + + try { + if (file.resumable) { + if (file.status !== 'pause') { + file.status = 'pause'; + file.resumable.pause(); + } else { + file.status = 'pending'; + file.resumable.upload(); + } + } else { + console.warn('Resumable object is not available for file', file); + } + } catch (error) { + console.error('Error while pausing or resuming file', file, error); + } + } + + this.notify(); + } + + private getResumableInstance({ + target, + headers, + chunkSize, + testChunks, + simultaneousUploads, + maxChunkRetries, + query, + }: { + target: string; + headers: { Authorization: string }; + chunkSize?: number; + testChunks?: number; + simultaneousUploads?: number; + maxChunkRetries?: number; + query?: { [key: string]: any }; + }) { + return new Resumable({ + target, + headers, + chunkSize: chunkSize || 5000000, + testChunks: testChunks || false, + simultaneousUploads: simultaneousUploads || 5, + maxChunkRetries: maxChunkRetries || 2, + query, + }); + } + + public async deleteOneFile({ + companyId, + fileId, + }: { + companyId: string; + fileId: string; + }): Promise { + const response = await FileUploadAPIClient.delete({ companyId, fileId }); + + if (response.status === 'success') { + this.pendingFiles = this.pendingFiles.filter(f => f.backendFile?.id !== fileId); + this.notify(); + } else { + logger.error(`Error while processing delete for file`, fileId); + } + } + + public getPauseStatus() { + return this.isPaused; + } +} + +export default new FileUploadService(); diff --git a/tdrive/frontend/src/app/features/files/services/file-upload-service.ts b/tdrive/frontend/src/app/features/files/services/file-upload-service.ts index 52ec036e7..1a0121bc0 100644 --- a/tdrive/frontend/src/app/features/files/services/file-upload-service.ts +++ b/tdrive/frontend/src/app/features/files/services/file-upload-service.ts @@ -14,6 +14,7 @@ import { FileTreeObject } from 'components/uploads/file-tree-utils'; import { DriveApiClient } from 'features/drive/api-client/api-client'; import { ToasterService } from 'app/features/global/services/toaster-service'; import Languages from 'app/features/global/services/languages-service'; +import { DriveItem, DriveItemVersion } from 'app/features/drive/types'; export enum Events { ON_CHANGE = 'notify', @@ -21,8 +22,13 @@ export enum Events { const logger = Logger.getLogger('Services/FileUploadService'); class FileUploadService { + private isPaused = false; + private isCancelled = false; + private pausedRoots: { [key: string]: boolean } = {}; private pendingFiles: PendingFileType[] = []; private GroupedPendingFiles: { [key: string]: PendingFileType[] } = {}; + private RootSizes: { [key: string]: number } = {}; + private GroupIds: { [key: string]: string } = {}; public currentTaskId = ''; private recoilHandler: Function = () => undefined; private rootRecoilHandler: Function = () => undefined; @@ -33,38 +39,63 @@ class FileUploadService { this.rootRecoilHandler = rootHandler; } + /** + * Helper method to pause execution when `isPaused` is true. + * @private + */ + async _waitWhilePaused(id?: string) { + while (this.isPaused || (id && this.pausedRoots[id])) { + if (this.isCancelled) return; + await new Promise(resolve => setTimeout(resolve, 100)); // Check every 100ms + console.log('waiting while paused:: ', id); + } + } + + /** + * Helper method to cancel execution when `isCancelled` is true. + * @private + */ + private async checkCancellation() { + if (this.isCancelled) { + logger.warn('Operation cancelled.'); + throw new Error('Upload process cancelled.'); + } + } + notify() { const updatedState = this.pendingFiles.map((f: PendingFileType) => { return { id: f.id, status: f.status, progress: f.progress, - file: f.backendFile, + file: f.originalFile?.type, }; }); const updatedRootState = Object.keys(this.GroupedPendingFiles).reduce( (acc: any, key: string) => { - const files = this.GroupedPendingFiles[key] + // uploaded size + const uploadedSize = this.GroupedPendingFiles[key] .map((f: PendingFileType) => { - // filter out the files that are not part of the current task - if ( - f.uploadTaskId !== this.currentTaskId || - f.status === 'error' || - f.status === 'success' - ) { - return null; + // if the file is successful and originalFile exists, add the size to the accumulator + if (f.status === 'success' && f.originalFile?.size) { + return f.originalFile.size; } - return { - id: f.id, - status: f.status, - progress: f.progress, - file: f.backendFile, - }; + return 0; }) - .filter(file => file !== null); // remove null entries from the array - + .reduce((acc: number, size: number) => acc + size, 0); + // status can be "uploading", "completed", "paused" based on the size, uploadedSize and pausedRoots + const status = this.pausedRoots[key] + ? 'paused' + : uploadedSize === this.RootSizes[key] + ? 'completed' + : 'uploading'; // Add to the accumulator object - acc[key] = [...files]; + acc[key] = { + items: [], + size: this.RootSizes[key], + uploadedSize, + status, + }; return acc; }, {}, @@ -77,13 +108,44 @@ class FileUploadService { root: FileTreeObject['tree'], context: { companyId: string; parentId: string }, ) { + // loop through the tree and determine root sizes and set it to the rootSizes object + const traverser = (tree: FileTreeObject['tree']) => { + for (const directory of Object.keys(tree)) { + const root = tree[directory].root as string; + if (tree[directory].file instanceof File) { + // if root is not in the rootSizes object, add it + if (!this.RootSizes[root] && !this.isCancelled) { + this.RootSizes[root] = 0; + } + this.RootSizes[root] += (tree[directory].file as File).size; + } else { + traverser(tree[directory] as FileTreeObject['tree']); + } + } + }; + traverser(root); // Create all directories const filesPerParentId: { [key: string]: { root: string; file: File }[] } = {}; filesPerParentId[context.parentId] = []; + const idsToBeRestored: string[] = []; + + const traverserTreeLevel = async ( + tree: FileTreeObject['tree'], + parentId: string, + tmp = false, + ) => { + // cancel upload + if (this.isCancelled) return; - const traverserTreeLevel = async (tree: FileTreeObject['tree'], parentId: string) => { + // check if upload is paused + await this._waitWhilePaused(); + + // start descending the tree for (const directory of Object.keys(tree)) { - if (tree[directory].file instanceof File) { + const root = tree[directory].root as string; + await this.checkCancellation(); + await this._waitWhilePaused(root); + if (tree[directory].file instanceof File && !this.isCancelled) { logger.trace(`${directory} is a file, save it for future upload`); filesPerParentId[parentId].push({ root: tree[directory].root as string, @@ -122,12 +184,15 @@ class FileUploadService { const driveItem = await DriveApiClient.create(context.companyId, { item: item, version: {}, + tmp, }); + this.GroupIds[directory] = driveItem.id; this.logger.debug(`Directory ${directory} created`); pendingFile.status = 'success'; this.notify(); if (driveItem?.id) { filesPerParentId[driveItem.id] = []; + if (tmp && idsToBeRestored.includes(driveItem.id)) idsToBeRestored.push(driveItem.id); await traverserTreeLevel(tree[directory] as FileTreeObject['tree'], driveItem.id); } } catch (e) { @@ -136,10 +201,67 @@ class FileUploadService { } } } + // uploading the files goes here + await this.upload(filesPerParentId[parentId], { + context: { + companyId: context.companyId, + parentId: parentId, + }, + callback: async (file, context) => { + if (file) { + const item = { + company_id: context.companyId, + workspace_id: 'drive', //We don't set workspace ID for now + parent_id: context.parentId, + name: file.metadata?.name, + size: file.upload_data?.size, + } as Partial; + const version = { + provider: 'internal', + application_id: '', + file_metadata: { + name: file.metadata?.name, + size: file.upload_data?.size, + mime: file.metadata?.mime, + thumbnails: file?.thumbnails, + source: 'internal', + external_id: file.id, + }, + } as Partial; + + // create the document + await DriveApiClient.create(context.companyId, { item, version }); + } + }, + }); }; - await traverserTreeLevel(root, context.parentId); - return filesPerParentId; + // split the tree per root + const rootKeys = Object.keys(root); + const rootTrees = rootKeys.map(key => { + return { [key]: root[key] }; + }); + + // tree promises + const treePromises = rootTrees.map(tree => { + return traverserTreeLevel(tree, context.parentId, true); + }); + + try { + await Promise.all(treePromises); + } catch (error) { + if (!this.isCancelled) { + console.error('An error occurred while processing treePromises:', error); + // Optionally, handle the error or rethrow it + throw error; // Re-throw the error if necessary + } else { + console.warn('Operation was cancelled. Error ignored.'); + } + } + + // await traverserTreeLevel(root, context.parentId, true); + + return { filesPerParentId, idsToBeRestored }; } public async upload( @@ -179,8 +301,6 @@ class FileUploadService { pausable: true, }; - console.log('fs:: upload:: pendingFile', pendingFile); - this.pendingFiles.push(pendingFile); if (!this.GroupedPendingFiles[file.root]) { this.GroupedPendingFiles[file.root] = []; @@ -239,7 +359,6 @@ class FileUploadService { try { pendingFile.backendFile = JSON.parse(message).resource; pendingFile.status = 'success'; - console.log('fileSuccess', options?.callback); options?.callback?.(pendingFile.backendFile, options?.context || {}); this.notify(); } catch (e) { @@ -286,20 +405,19 @@ class FileUploadService { return this.pendingFiles.filter(f => f.backendFile?.id && f.backendFile.id === id)[0]; } - public cancel(id: string, timeout = 1000) { - const fileToCancel = this.pendingFiles.filter(f => f.id === id)[0]; - - fileToCancel.status = 'cancel'; - - if (fileToCancel.resumable) { - fileToCancel.resumable.cancel(); - this.notify(); - - if (fileToCancel.backendFile) - this.deleteOneFile({ - companyId: fileToCancel.backendFile.company_id, - fileId: fileToCancel.backendFile.id, - }); + public cancelRoot(id: string, timeout = 1000) { + const filesToCancel = this.GroupedPendingFiles[id]; + + for (const file of filesToCancel) { + file.status = 'cancel'; + if (file.resumable) { + file.resumable.cancel(); + if (file.backendFile) + this.deleteOneFile({ + companyId: file.backendFile.company_id, + fileId: file.backendFile.id, + }); + } } setTimeout(() => { @@ -308,6 +426,45 @@ class FileUploadService { }, timeout); } + public cancelUpload() { + this.isCancelled = true; + + // pause or resume the resumable tasks + const fileToCancel = this.pendingFiles; + + if (!fileToCancel) { + console.error(`No files found for id`); + return; + } + + for (const file of fileToCancel) { + if (file.status === 'success') continue; + + try { + if (file.resumable) { + file.resumable.cancel(); + if (file.backendFile) + this.deleteOneFile({ + companyId: file.backendFile.company_id, + fileId: file.backendFile.id, + }); + } else { + console.warn('Resumable object is not available for file', file); + } + } catch (error) { + console.error('Error while pausing or resuming file', file, error); + } + } + + // clean everything + this.pendingFiles = []; + this.GroupedPendingFiles = {}; + this.RootSizes = {}; + this.GroupIds = {}; + + this.notify(); + } + public retry(id: string) { const fileToRetry = this.pendingFiles.filter(f => f.id === id)[0]; @@ -319,15 +476,66 @@ class FileUploadService { } } - public pauseOrResume(id: string) { - const fileToCancel = this.pendingFiles.filter(f => f.id === id)[0]; + private pauseOrResumeFile(file: PendingFileType) { + try { + if (file.resumable) { + if (file.status !== 'pause') { + file.status = 'pause'; + file.resumable.pause(); + } else { + file.status = 'pending'; + file.resumable.upload(); + } + } else { + console.warn('Resumable object is not available for file', file); + } + } catch (error) { + console.error('Error while pausing or resuming file', file, error); + } + } + + public pauseOrResume() { + // pause or resume the curent upload task + this.isPaused = !this.isPaused; - fileToCancel.status !== 'pause' - ? (fileToCancel.status = 'pause') - : (fileToCancel.status = 'pending'); - fileToCancel.status === 'pause' - ? fileToCancel.resumable.pause() - : fileToCancel.resumable.upload(); + // pause or resume the resumable tasks + const filesToProcess = this.pendingFiles; + + if (!filesToProcess || filesToProcess.length === 0) { + console.error(`No files found for id`); + return; + } + + for (const file of filesToProcess) { + if (file.status === 'success') continue; + this.pauseOrResumeFile(file); + } + + this.notify(); + } + + public pauseOrResumeRoot(id: string) { + // set the pause status for the root + if (Object.keys(this.pausedRoots).includes(id)) { + this.pausedRoots[id] = !this.pausedRoots[id]; + } else { + this.pausedRoots[id] = true; + } + + // pause or resume the resumable tasks + const filesToProcess = this.GroupedPendingFiles[id]; + + if (!filesToProcess || filesToProcess.length === 0) { + console.error(`No files found for id: ${id}`); + return; + } + + for (const file of filesToProcess) { + if (file.status === 'success') continue; + + // pause or resume the file + this.pauseOrResumeFile(file); + } this.notify(); } @@ -390,6 +598,16 @@ class FileUploadService { fileId: fileId, }); } + + public clearRoots() { + this.GroupedPendingFiles = {}; + this.GroupIds = {}; + this.notify(); + } + + public getPauseStatus() { + return this.isPaused; + } } export default new FileUploadService(); diff --git a/tdrive/frontend/src/app/features/files/state/atoms/root-pending-files-list.ts b/tdrive/frontend/src/app/features/files/state/atoms/root-pending-files-list.ts index d9fd7c570..c886c886d 100644 --- a/tdrive/frontend/src/app/features/files/state/atoms/root-pending-files-list.ts +++ b/tdrive/frontend/src/app/features/files/state/atoms/root-pending-files-list.ts @@ -2,7 +2,15 @@ import { atom } from 'recoil'; import { PendingFileRecoilType } from '@features/files/types/file'; export const RootPendingFilesListState = atom< - { [key: string]: PendingFileRecoilType[] } | undefined + | { + [key: string]: { + size: number; + uploadedSize: number; + status: string; + items: PendingFileRecoilType[]; + }; + } + | undefined >({ key: 'RootPendingFilesListState', default: {}, diff --git a/tdrive/frontend/src/app/features/files/types/file.ts b/tdrive/frontend/src/app/features/files/types/file.ts index db4ddca05..5c221f079 100644 --- a/tdrive/frontend/src/app/features/files/types/file.ts +++ b/tdrive/frontend/src/app/features/files/types/file.ts @@ -57,6 +57,7 @@ export type FileType = { upload_data: FileUploadDataObjectType; user_id: string; user?: UserType; + parent_id?: string; }; export type PendingFileRecoilType = { @@ -64,13 +65,16 @@ export type PendingFileRecoilType = { status: 'pending' | 'error' | 'success' | 'pause' | 'cancel'; progress: number; //Between 0 and 1 file: FileType | null; + type: string; + parentId: string | null; + size: number; }; /** * It could be not only a file, but also a task with creating folders */ export type PendingFileType = { - type: "file" | "folder" + type: 'file' | 'folder'; resumable: typeof Resumable | null; //Contain the resumable instance in charge of this file uploadTaskId: string; id: string; @@ -83,3 +87,14 @@ export type PendingFileType = { label: string | null; pausable: boolean; }; + +export type UploadRootType = { + size: number; + uploadedSize: number; + status: string; + items: PendingFileRecoilType[]; +}; + +export type UploadRootListType = { + [key: string]: UploadRootType; +}; diff --git a/tdrive/frontend/src/app/features/files/utils/resumable.js b/tdrive/frontend/src/app/features/files/utils/resumable.js index ac456e535..cb3c4633c 100755 --- a/tdrive/frontend/src/app/features/files/utils/resumable.js +++ b/tdrive/frontend/src/app/features/files/utils/resumable.js @@ -97,7 +97,7 @@ window.define = }, minFileSize: 1, minFileSizeErrorCallback: function (file, errorCount) { - alert( + console.log( file.fileName || file.name + ' is too small, please upload files larger than ' + diff --git a/tdrive/frontend/src/app/views/client/body/drive/browser.tsx b/tdrive/frontend/src/app/views/client/body/drive/browser.tsx index fc34fa1b8..77a95079e 100644 --- a/tdrive/frontend/src/app/views/client/body/drive/browser.tsx +++ b/tdrive/frontend/src/app/views/client/body/drive/browser.tsx @@ -49,6 +49,7 @@ import { useCurrentUser } from 'app/features/users/hooks/use-current-user'; import { ConfirmModal } from './modals/confirm-move'; import { useHistory } from 'react-router-dom'; import { SortIcon } from 'app/atoms/icons-agnostic'; +import { useUploadExp } from 'app/features/files/hooks/use-exp-upload'; export const DriveCurrentFolderAtom = atomFamily< string, @@ -109,6 +110,7 @@ export default memo( paginateItem, } = useDriveItem(parentId); const { uploadTree } = useDriveUpload(); + const { uploadTree: _uploadTree } = useUploadExp(); const loading = loadingParent || loadingParentChange; @@ -309,6 +311,8 @@ export default memo( } }, [paginateItem, loading, parentId, itemsPerPage]); + const [isPreparingUpload, setIsPreparingUpload] = useState(false); + return ( <> {viewId == 'shared-with-me' ? ( @@ -329,7 +333,16 @@ export default memo( ref={uploadZoneRef} driveCollectionKey={uploadZone} onAddFiles={async (_, event) => { + console.log('STARTING UPLOAD...'); + setIsPreparingUpload(true); + const timeBeing = new Date().getTime(); const tree = await getFilesTree(event); + // await new Promise(resolve => setTimeout(resolve, 3000)); + const timeEnd = new Date().getTime(); + console.log('TIME TO UPLOAD: ', (timeEnd - timeBeing) / 1000); + setIsPreparingUpload(false); + // _uploadTree(tree); + // // return; setCreationModalState({ parent_id: '', open: false }); uploadTree(tree, { companyId, @@ -483,8 +496,10 @@ export default memo(
diff --git a/tdrive/frontend/src/app/views/client/common/disk-usage.tsx b/tdrive/frontend/src/app/views/client/common/disk-usage.tsx index 65829ce26..dc5578e3c 100644 --- a/tdrive/frontend/src/app/views/client/common/disk-usage.tsx +++ b/tdrive/frontend/src/app/views/client/common/disk-usage.tsx @@ -10,7 +10,7 @@ import { useDriveItem } from "features/drive/hooks/use-drive-item"; const DiskUsage = () => { const { viewId } = RouterServices.getStateFromRoute(); - console.log("VIEW-iD::" + viewId); + // console.log("VIEW-iD::" + viewId); const [used, setUsed] = useState(0); const [usedBytes, setUsedBytes] = useState(0); From 8f1a0228f2309320b919b48f6dceb2ad0c943c37 Mon Sep 17 00:00:00 2001 From: MontaGhanmy Date: Tue, 21 Jan 2025 10:21:01 +0100 Subject: [PATCH 03/13] feat: updated upload modal/file upload service --- tdrive/frontend/public/locales/en.json | 2 + tdrive/frontend/public/locales/fr.json | 2 + tdrive/frontend/public/locales/ru.json | 2 + tdrive/frontend/public/locales/vi.json | 2 + .../pending-root-list.tsx | 2 +- .../pending-root-list.tsx | 21 +- .../file-uploads/uploads-viewer.tsx | 19 +- .../features/drive/hooks/use-drive-upload.tsx | 67 +-- .../app/features/files/hooks/use-upload.ts | 9 +- .../files/services/file-upload-service-new.ts | 503 ------------------ .../files/services/file-upload-service.ts | 111 ++-- .../files/state/selectors/current-task.ts | 1 + 12 files changed, 104 insertions(+), 637 deletions(-) delete mode 100644 tdrive/frontend/src/app/features/files/services/file-upload-service-new.ts diff --git a/tdrive/frontend/public/locales/en.json b/tdrive/frontend/public/locales/en.json index 6b24cb83e..97b1514eb 100644 --- a/tdrive/frontend/public/locales/en.json +++ b/tdrive/frontend/public/locales/en.json @@ -183,6 +183,8 @@ "general.resume": "Resume", "general.send": "Send message", "general.update": "Update", + "general.uploading": "Uploading", + "general.files": "files", "general.user.anonymous": "Anonymous", "general.user.deactivated": "User is no longer in this company", "general.user.deleted": "Deleted Account", diff --git a/tdrive/frontend/public/locales/fr.json b/tdrive/frontend/public/locales/fr.json index a10a2ecc1..5a7e16520 100644 --- a/tdrive/frontend/public/locales/fr.json +++ b/tdrive/frontend/public/locales/fr.json @@ -176,6 +176,8 @@ "general.resume": "Reprendre", "general.send": "Envoyer un message", "general.update": "Mettre Γ  jour", + "general.uploading": "TΓ©lΓ©chargement en cours", + "general.files": "fichiers", "general.user.anonymous": "Compte anonyme", "general.user.deactivated": "Cet utilisateur n'est plus dans cette l'entreprise", "general.user.deleted": "Compte supprimΓ©", diff --git a/tdrive/frontend/public/locales/ru.json b/tdrive/frontend/public/locales/ru.json index fd84e29bd..7c884a0e6 100644 --- a/tdrive/frontend/public/locales/ru.json +++ b/tdrive/frontend/public/locales/ru.json @@ -183,6 +183,8 @@ "general.resume": "ΠŸΡ€ΠΎΠ΄ΠΎΠ»ΠΆΠΈΡ‚ΡŒ", "general.send": "ΠžΡ‚ΠΏΡ€Π°Π²ΠΈΡ‚ΡŒ сообщСниС", "general.update": "ΠžΠ±Π½ΠΎΠ²ΠΈΡ‚ΡŒ", + "general.uploading": "Π—Π°Π³Ρ€ΡƒΠ·ΠΊΠ°", + "general.files": "Ρ„Π°ΠΉΠ»Ρ‹", "general.user.anonymous": "Анонимный", "general.user.deactivated": "ΠŸΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»ΡŒ большС Π½Π΅ состоит Π² этой ΠΊΠΎΠΌΠΏΠ°Π½ΠΈΠΈ", "general.user.deleted": "УдалСнная учСтная запись", diff --git a/tdrive/frontend/public/locales/vi.json b/tdrive/frontend/public/locales/vi.json index 4cbf011a5..0ad94a686 100644 --- a/tdrive/frontend/public/locales/vi.json +++ b/tdrive/frontend/public/locales/vi.json @@ -165,6 +165,8 @@ "general.resume": "TiαΊΏp tα»₯c", "general.send": "Gα»­i", "general.update": "CαΊ­p nhαΊ­t", + "general.uploading": "Đang tαΊ£i lΓͺn", + "general.files": "tệp", "general.user.anonymous": "VΓ΄ danh", "general.user.deactivated": "Người dΓΉng khΓ΄ng cΓ²n thuα»™c cΓ΄ng ty nΓ y nα»―a", "general.user.deleted": "TΓ i khoαΊ£n Δ‘Γ£ bα»‹ xΓ³a", diff --git a/tdrive/frontend/src/app/components/file-uploads/pending-file-components/pending-root-list.tsx b/tdrive/frontend/src/app/components/file-uploads/pending-file-components/pending-root-list.tsx index 8334faa9c..8825e3b4d 100644 --- a/tdrive/frontend/src/app/components/file-uploads/pending-file-components/pending-root-list.tsx +++ b/tdrive/frontend/src/app/components/file-uploads/pending-file-components/pending-root-list.tsx @@ -16,7 +16,7 @@ type PropsType = { const { Text } = Typography; const { Header, Content } = Layout; export default ({ rootPendingFilesState, visible }: PropsType) => { - const { getOnePendingFile, currentTask } = useUpload(); + const { currentTask } = useUpload(); return Object.keys(rootPendingFilesState || {}).length > 0 ? ( diff --git a/tdrive/frontend/src/app/components/file-uploads/pending-root-components/pending-root-list.tsx b/tdrive/frontend/src/app/components/file-uploads/pending-root-components/pending-root-list.tsx index d6dde59de..8a8e6a1bf 100644 --- a/tdrive/frontend/src/app/components/file-uploads/pending-root-components/pending-root-list.tsx +++ b/tdrive/frontend/src/app/components/file-uploads/pending-root-components/pending-root-list.tsx @@ -2,7 +2,9 @@ import { useState, useMemo, useCallback } from 'react'; import { useUpload } from '@features/files/hooks/use-upload'; import { ArrowDownIcon, ArrowUpIcon } from 'app/atoms/icons-colored'; import { UploadRootListType } from 'app/features/files/types/file'; +import Languages from '@features/global/services/languages-service'; import PendingRootRow from './pending-root-row'; +import { UploadStateEnum } from 'app/features/files/services/file-upload-service'; const getFilteredRoots = (keys: string[], roots: UploadRootListType) => { const inProgress = keys.filter(key => roots[key].status === 'uploading'); @@ -27,7 +29,8 @@ const ModalHeader: React.FC = ({ }) => (

- Uploading {uploadingCount}/{totalRoots} files... {uploadingPercentage}% + {Languages.t('general.uploading')} {uploadingCount}/{totalRoots}{' '} + {Languages.t('general.files')}... {uploadingPercentage}%

); -const PendingRootList = ({ roots }: { roots: UploadRootListType }): JSX.Element => { +const PendingRootList = ({ + roots, + status, +}: { + roots: UploadRootListType; + status: UploadStateEnum; +}): JSX.Element => { const [modalExpanded, setModalExpanded] = useState(true); - const { pauseOrResumeUpload, isPaused, cancelUpload } = useUpload(); + const { pauseOrResumeUpload, cancelUpload } = useUpload(); const keys = useMemo(() => Object.keys(roots || {}), [roots]); const { inProgress: rootsInProgress } = useMemo( @@ -71,6 +80,8 @@ const PendingRootList = ({ roots }: { roots: UploadRootListType }): JSX.Element [keys, roots], ); + const isPaused = useCallback(() => status === UploadStateEnum.Paused, [status]); + const totalRoots = keys.length; const uploadingCount = rootsInProgress.length; const uploadingPercentage = Math.floor((uploadingCount / totalRoots) * 100) || 100; diff --git a/tdrive/frontend/src/app/components/file-uploads/uploads-viewer.tsx b/tdrive/frontend/src/app/components/file-uploads/uploads-viewer.tsx index 062793d17..2727ee053 100644 --- a/tdrive/frontend/src/app/components/file-uploads/uploads-viewer.tsx +++ b/tdrive/frontend/src/app/components/file-uploads/uploads-viewer.tsx @@ -1,10 +1,19 @@ import { useUpload } from '@features/files/hooks/use-upload'; import PendingRootList from './pending-root-components/pending-root-list'; -const ChatUploadsViewer = (): JSX.Element => { + +const UploadsViewer = (): JSX.Element => { const { currentTask } = useUpload(); - const roots = currentTask.roots || {}; - const keys = Object.keys(roots); - return <>{keys.length > 0 && }; + + // Destructure and provide default values for safety + const { roots = {}, status } = currentTask || {}; + const rootKeys = Object.keys(roots); + + // Early return for clarity + if (rootKeys.length === 0) { + return <>; + } + + return ; }; -export default ChatUploadsViewer; +export default UploadsViewer; diff --git a/tdrive/frontend/src/app/features/drive/hooks/use-drive-upload.tsx b/tdrive/frontend/src/app/features/drive/hooks/use-drive-upload.tsx index 604312478..5bb153a70 100644 --- a/tdrive/frontend/src/app/features/drive/hooks/use-drive-upload.tsx +++ b/tdrive/frontend/src/app/features/drive/hooks/use-drive-upload.tsx @@ -47,67 +47,12 @@ export const useDriveUpload = () => { tree: FileTreeObject, context: { companyId: string; parentId: string }, ) => { - // Create all directories - logger.debug('Start creating directories ...'); - const { filesPerParentId, idsToBeRestored } = await FileUploadService.createDirectories( - tree.tree, - context, - ); - idsToBeRestored.map(async (id: string) => { - await restore(id, context.parentId); - }); - refresh(context.parentId, true); - // await refresh(context.parentId, true); - // logger.debug("All directories created"); - // // Upload files into directories - // logger.debug("Start file uploading") - // //create counter to calculate number of uploaded files, and refresh browsing window only when all the files were uploaded - // let expectedUploadsCount = 0; - // const parentFolder = context.parentId; - // let uploadedFilesCount = 0; - // for (const parentId of Object.keys(filesPerParentId)) { - // logger.debug(`Upload files for directory ${parentId}`); - // expectedUploadsCount += filesPerParentId[parentId].length; - // await FileUploadService.upload(filesPerParentId[parentId], { - // context: { - // companyId: context.companyId, - // parentId: parentId, - // }, - // callback: (file, context) => { - // logger.debug('created file: ', file); - // uploadedFilesCount++; - // if (file) { - // create( - // { - // company_id: context.companyId, - // workspace_id: 'drive', //We don't set workspace ID for now - // parent_id: context.parentId, - // name: file.metadata?.name, - // size: file.upload_data?.size, - // }, - // { - // provider: 'internal', - // application_id: '', - // file_metadata: { - // name: file.metadata?.name, - // size: file.upload_data?.size, - // mime: file.metadata?.mime, - // thumbnails: file?.thumbnails, - // source: 'internal', - // external_id: file.id, - // }, - // }, - // ); - // } - // if (uploadedFilesCount == expectedUploadsCount) { - // idsToBeRestored.map( async (id: string) => { - // await restore(id, parentFolder); - // }); - // refresh(parentFolder, true); - // } - // }, - // }); - // } + logger.debug('Start creating directories and file upload ...'); + const { idsToBeRestored } = await FileUploadService.createDirectories(tree.tree, context); + + await Promise.all(idsToBeRestored.map(id => restore(id, context.parentId))); + + await refresh(context.parentId, true); }; const uploadFromUrl = ( diff --git a/tdrive/frontend/src/app/features/files/hooks/use-upload.ts b/tdrive/frontend/src/app/features/files/hooks/use-upload.ts index dc2fbbdff..6b17cc744 100644 --- a/tdrive/frontend/src/app/features/files/hooks/use-upload.ts +++ b/tdrive/frontend/src/app/features/files/hooks/use-upload.ts @@ -7,10 +7,10 @@ import { CurrentTaskSelector } from '../state/selectors/current-task'; export const useUpload = () => { const { companyId } = RouterServices.getStateFromRoute(); - const [pendingFilesListState, setPendingFilesListState] = useRecoilState(PendingFilesListState); + const [pendingFilesListState, _setPendingFilesListState] = useRecoilState(PendingFilesListState); const [_rootPendingFilesListState, setRootPendingFilesListState] = useRecoilState(RootPendingFilesListState); - FileUploadService.setRecoilHandler(setPendingFilesListState, setRootPendingFilesListState); + FileUploadService.setRecoilHandler(setRootPendingFilesListState); const currentTask = useRecoilValue(CurrentTaskSelector); @@ -32,8 +32,6 @@ export const useUpload = () => { const clearRoots = () => FileUploadService.clearRoots(); - const isPaused = () => FileUploadService.getPauseStatus(); - return { pendingFilesListState, pauseOrResumeUpload, @@ -44,7 +42,6 @@ export const useUpload = () => { currentTask, deleteOneFile, retryUpload, - clearRoots, - isPaused, + clearRoots }; }; diff --git a/tdrive/frontend/src/app/features/files/services/file-upload-service-new.ts b/tdrive/frontend/src/app/features/files/services/file-upload-service-new.ts deleted file mode 100644 index c9354e730..000000000 --- a/tdrive/frontend/src/app/features/files/services/file-upload-service-new.ts +++ /dev/null @@ -1,503 +0,0 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ -/* eslint-disable @typescript-eslint/ban-types */ -import { v1 as uuid } from 'uuid'; - -import JWTStorage from '@features/auth/jwt-storage-service'; -import { FileType, PendingFileType } from '@features/files/types/file'; -import Resumable from '@features/files/utils/resumable'; -import Logger from '@features/global/framework/logger-service'; -import RouterServices from '@features/router/services/router-service'; -import _ from 'lodash'; -import FileUploadAPIClient from '../api/file-upload-api-client'; -import { isPendingFileStatusPending } from '../utils/pending-files'; -import { FileTreeObject } from 'components/uploads/file-tree-utils'; -import { DriveApiClient } from 'features/drive/api-client/api-client'; -import { ToasterService } from 'app/features/global/services/toaster-service'; -import Languages from 'app/features/global/services/languages-service'; -import { DriveItem, DriveItemVersion } from 'app/features/drive/types'; - -export enum Events { - ON_CHANGE = 'notify', -} - -const logger = Logger.getLogger('Services/FileUploadService'); -class FileUploadService { - private isPaused = false; - private isCancelled = false; - private pendingFiles: PendingFileType[] = []; - private GroupedPendingFiles: { [key: string]: PendingFileType[] } = {}; - private RootSizes: { [key: string]: number } = {}; - private GroupIds: { [key: string]: string } = {}; - public currentTaskId = ''; - private recoilHandler: Function = () => undefined; - private rootRecoilHandler: Function = () => undefined; - private logger: Logger.Logger = Logger.getLogger('FileUploadService'); - - setRecoilHandler(handler: Function, rootHandler: Function) { - this.recoilHandler = handler; - this.rootRecoilHandler = rootHandler; - } - - /** - * Helper method to pause execution when `isPaused` is true. - * @private - */ - async _waitWhilePaused() { - while (this.isPaused) { - if (this.isCancelled) return; - await new Promise(resolve => setTimeout(resolve, 100)); // Check every 100ms - } - } - - /** - * Helper method to cancel execution when `isCancelled` is true. - * @private - */ - private async checkCancellation() { - if (this.isCancelled) { - logger.warn('Operation cancelled.'); - throw new Error('Upload process cancelled.'); - } - } - - notify() { - const updatedState = this.pendingFiles.map((f: PendingFileType) => { - return { - id: f.id, - status: f.status, - progress: f.progress, - file: f.originalFile?.type, - }; - }); - const updatedRootState = Object.keys(this.GroupedPendingFiles).reduce( - (acc: any, key: string) => { - // uploaded size - const uploadedSize = this.GroupedPendingFiles[key] - .map((f: PendingFileType) => { - // if the file is successful and originalFile exists, add the size to the accumulator - if (f.status === 'success' && f.originalFile?.size) { - return f.originalFile.size; - } - return 0; - }) - .reduce((acc: number, size: number) => acc + size, 0); - // Add to the accumulator object - acc[key] = { - size: this.RootSizes[key], - status: this.isPaused, - items: [], - uploadedSize, - }; - return acc; - }, - {}, - ); - this.recoilHandler(_.cloneDeep(updatedState)); - this.rootRecoilHandler(_.cloneDeep(updatedRootState)); - } - - public async createDirectories( - root: FileTreeObject['tree'], - context: { companyId: string; parentId: string }, - ) { - // loop through the tree and determine root sizes and set it to the rootSizes object - const traverser = (tree: FileTreeObject['tree']) => { - for (const directory of Object.keys(tree)) { - const root = tree[directory].root as string; - if (tree[directory].file instanceof File) { - // if root is not in the rootSizes object, add it - if (!this.RootSizes[root] && !this.isCancelled) { - this.RootSizes[root] = 0; - } - this.RootSizes[root] += (tree[directory].file as File).size; - } else { - traverser(tree[directory] as FileTreeObject['tree']); - } - } - }; - traverser(root); - // Create all directories - const filesPerParentId: { [key: string]: { root: string; file: File }[] } = {}; - filesPerParentId[context.parentId] = []; - const idsToBeRestored: string[] = []; - - const traverserTreeLevel = async ( - tree: FileTreeObject['tree'], - parentId: string, - tmp = false, - ) => { - // cancel upload - if (this.isCancelled) return; - - // check if upload is paused - await this._waitWhilePaused(); - - // start descending the tree - for (const directory of Object.keys(tree)) { - await this.checkCancellation(); - await this._waitWhilePaused(); - if (tree[directory].file instanceof File && !this.isCancelled) { - logger.trace(`${directory} is a file, save it for future upload`); - filesPerParentId[parentId].push({ - root: tree[directory].root as string, - file: tree[directory].file as File, - }); - } else { - logger.debug(`Create directory ${directory}`); - - const item = { - company_id: context.companyId, - parent_id: parentId, - name: directory, - is_directory: true, - }; - - if (!this.pendingFiles.some(f => isPendingFileStatusPending(f.status))) { - //New upload task when all previous task is finished - this.currentTaskId = uuid(); - } - const pendingFile: PendingFileType = { - id: uuid(), - status: 'pending', - progress: 0, - lastProgress: new Date().getTime(), - speed: 0, - uploadTaskId: this.currentTaskId, - originalFile: null, - backendFile: null, - resumable: null, - label: directory, - type: 'file', - pausable: false, - }; - - try { - const driveItem = await DriveApiClient.create(context.companyId, { - item: item, - version: {}, - tmp, - }); - this.GroupIds[directory] = driveItem.id; - this.logger.debug(`Directory ${directory} created`); - pendingFile.status = 'success'; - this.notify(); - if (driveItem?.id) { - filesPerParentId[driveItem.id] = []; - if (tmp && idsToBeRestored.includes(driveItem.id)) idsToBeRestored.push(driveItem.id); - await traverserTreeLevel(tree[directory] as FileTreeObject['tree'], driveItem.id); - } - } catch (e) { - this.logger.error(e); - throw new Error('Could not create directory'); - } - } - } - // uploading the files goes here - await this.upload(filesPerParentId[parentId], { - context: { - companyId: context.companyId, - parentId: parentId, - }, - callback: async (file, context) => { - if (file) { - const item = { - company_id: context.companyId, - workspace_id: 'drive', //We don't set workspace ID for now - parent_id: context.parentId, - name: file.metadata?.name, - size: file.upload_data?.size, - } as Partial; - const version = { - provider: 'internal', - application_id: '', - file_metadata: { - name: file.metadata?.name, - size: file.upload_data?.size, - mime: file.metadata?.mime, - thumbnails: file?.thumbnails, - source: 'internal', - external_id: file.id, - }, - } as Partial; - const driveFile = await DriveApiClient.create(context.companyId, { item, version }); - } - }, - }); - }; - - // split the tree per root - const rootKeys = Object.keys(root); - const rootTrees = rootKeys.map(key => { - return { [key]: root[key] }; - }); - - // tree promises - const treePromises = rootTrees.map(tree => { - return traverserTreeLevel(tree, context.parentId, true); - }); - - try { - await Promise.all(treePromises); - } catch (error) { - if (!this.isCancelled) { - console.error('An error occurred while processing treePromises:', error); - // Optionally, handle the error or rethrow it - throw error; // Re-throw the error if necessary - } else { - console.warn('Operation was cancelled. Error ignored.'); - } - } - - // await traverserTreeLevel(root, context.parentId, true); - - return { filesPerParentId, idsToBeRestored }; - } - - public async upload( - fileList: { root: string; file: File }[], - options?: { - context?: any; - callback?: (file: FileType | null, context: any) => void; - }, - ): Promise { - const { companyId } = RouterServices.getStateFromRoute(); - - if (!fileList || !companyId) { - this.logger.log('FileList or companyId is undefined', [fileList, companyId]); - return []; - } - - if (!this.pendingFiles.some(f => isPendingFileStatusPending(f.status))) { - //New upload task when all previous task is finished - this.currentTaskId = uuid(); - } - - for (const file of fileList) { - if (!file.file) continue; - - const pendingFile: PendingFileType = { - id: uuid(), - status: 'pending', - progress: 0, - lastProgress: new Date().getTime(), - speed: 0, - uploadTaskId: this.currentTaskId, - originalFile: file.file, - backendFile: null, - resumable: null, - type: 'file', - label: null, - pausable: true, - }; - - this.pendingFiles.push(pendingFile); - if (!this.GroupedPendingFiles[file.root]) { - this.GroupedPendingFiles[file.root] = []; - } - this.GroupedPendingFiles[file.root].push(pendingFile); - this.notify(); - - // First we create the file object - const resource = ( - await FileUploadAPIClient.upload(file.file, { companyId, ...(options?.context || {}) }) - )?.resource; - - if (!resource) { - throw new Error('A server error occured'); - } - - pendingFile.backendFile = resource; - this.notify(); - - // Then we overwrite the file object with resumable - pendingFile.resumable = this.getResumableInstance({ - target: FileUploadAPIClient.getRoute({ - companyId, - fileId: pendingFile.backendFile.id, - fullApiRouteUrl: true, - }), - query: { - thumbnail_sync: 1, - }, - headers: { - Authorization: JWTStorage.getAutorizationHeader(), - }, - }); - - pendingFile.resumable.addFile(file.file); - - pendingFile.resumable.on('fileAdded', () => pendingFile.resumable.upload()); - - pendingFile.resumable.on('fileProgress', (f: any) => { - const bytesDelta = - (f.progress() - pendingFile.progress) * (pendingFile?.originalFile?.size || 0); - const timeDelta = new Date().getTime() - pendingFile.lastProgress; - - // To avoid jumping time ? - if (timeDelta > 1000) { - pendingFile.speed = bytesDelta / timeDelta; - } - - pendingFile.backendFile = f; - pendingFile.lastProgress = new Date().getTime(); - pendingFile.progress = f.progress(); - this.notify(); - }); - - pendingFile.resumable.on('fileSuccess', (_f: any, message: string) => { - try { - pendingFile.backendFile = JSON.parse(message).resource; - pendingFile.status = 'success'; - options?.callback?.(pendingFile.backendFile, options?.context || {}); - this.notify(); - } catch (e) { - logger.error(`Error on fileSuccess Event`, e); - } - }); - - pendingFile.resumable.on('fileError', () => { - pendingFile.status = 'error'; - pendingFile.resumable.cancel(); - const intendedFilename = - (pendingFile.originalFile || {}).name || - (pendingFile.backendFile || { metadata: {} }).metadata.name; - ToasterService.error( - Languages.t( - 'services.file_upload_service.toaster.upload_file_error', - [intendedFilename], - 'Error uploading file ' + intendedFilename, - ), - ); - options?.callback?.(null, options?.context || {}); - this.notify(); - }); - } - - return this.pendingFiles.filter(f => f.uploadTaskId === this.currentTaskId); - } - - public cancelUpload() { - this.isCancelled = true; - - // pause or resume the resumable tasks - const fileToCancel = this.pendingFiles; - - if (!fileToCancel) { - console.error(`No files found for id`); - return; - } - - for (const file of fileToCancel) { - if (file.status === 'success') continue; - - try { - if (file.resumable) { - file.resumable.cancel(); - if (file.backendFile) - this.deleteOneFile({ - companyId: file.backendFile.company_id, - fileId: file.backendFile.id, - }); - } else { - console.warn('Resumable object is not available for file', file); - } - } catch (error) { - console.error('Error while pausing or resuming file', file, error); - } - } - - // clean everything - this.pendingFiles = []; - this.GroupedPendingFiles = {}; - this.RootSizes = {}; - this.GroupIds = {}; - - this.notify(); - } - - public pauseOrResume() { - // pause or resume the curent upload task - this.isPaused = !this.isPaused; - - // pause or resume the resumable tasks - const fileToCancel = this.pendingFiles; - - if (!fileToCancel) { - console.error(`No files found for id`); - return; - } - - for (const file of fileToCancel) { - if (file.status === 'success') continue; - - try { - if (file.resumable) { - if (file.status !== 'pause') { - file.status = 'pause'; - file.resumable.pause(); - } else { - file.status = 'pending'; - file.resumable.upload(); - } - } else { - console.warn('Resumable object is not available for file', file); - } - } catch (error) { - console.error('Error while pausing or resuming file', file, error); - } - } - - this.notify(); - } - - private getResumableInstance({ - target, - headers, - chunkSize, - testChunks, - simultaneousUploads, - maxChunkRetries, - query, - }: { - target: string; - headers: { Authorization: string }; - chunkSize?: number; - testChunks?: number; - simultaneousUploads?: number; - maxChunkRetries?: number; - query?: { [key: string]: any }; - }) { - return new Resumable({ - target, - headers, - chunkSize: chunkSize || 5000000, - testChunks: testChunks || false, - simultaneousUploads: simultaneousUploads || 5, - maxChunkRetries: maxChunkRetries || 2, - query, - }); - } - - public async deleteOneFile({ - companyId, - fileId, - }: { - companyId: string; - fileId: string; - }): Promise { - const response = await FileUploadAPIClient.delete({ companyId, fileId }); - - if (response.status === 'success') { - this.pendingFiles = this.pendingFiles.filter(f => f.backendFile?.id !== fileId); - this.notify(); - } else { - logger.error(`Error while processing delete for file`, fileId); - } - } - - public getPauseStatus() { - return this.isPaused; - } -} - -export default new FileUploadService(); diff --git a/tdrive/frontend/src/app/features/files/services/file-upload-service.ts b/tdrive/frontend/src/app/features/files/services/file-upload-service.ts index 1a0121bc0..bb29bf9a0 100644 --- a/tdrive/frontend/src/app/features/files/services/file-upload-service.ts +++ b/tdrive/frontend/src/app/features/files/services/file-upload-service.ts @@ -20,23 +20,27 @@ export enum Events { ON_CHANGE = 'notify', } +export enum UploadStateEnum { + Progress = 'progress', + Completed = 'completed', + Paused = 'paused', + Cancelled = 'cancelled', +} + const logger = Logger.getLogger('Services/FileUploadService'); class FileUploadService { - private isPaused = false; - private isCancelled = false; - private pausedRoots: { [key: string]: boolean } = {}; private pendingFiles: PendingFileType[] = []; private GroupedPendingFiles: { [key: string]: PendingFileType[] } = {}; private RootSizes: { [key: string]: number } = {}; private GroupIds: { [key: string]: string } = {}; + private pausedRoots: { [key: string]: boolean } = {}; public currentTaskId = ''; + public uploadStatus = UploadStateEnum.Progress; private recoilHandler: Function = () => undefined; - private rootRecoilHandler: Function = () => undefined; private logger: Logger.Logger = Logger.getLogger('FileUploadService'); - setRecoilHandler(handler: Function, rootHandler: Function) { + setRecoilHandler(handler: Function) { this.recoilHandler = handler; - this.rootRecoilHandler = rootHandler; } /** @@ -44,8 +48,8 @@ class FileUploadService { * @private */ async _waitWhilePaused(id?: string) { - while (this.isPaused || (id && this.pausedRoots[id])) { - if (this.isCancelled) return; + while (this.uploadStatus === UploadStateEnum.Paused || (id && this.pausedRoots[id])) { + if (this.uploadStatus === UploadStateEnum.Cancelled) return; await new Promise(resolve => setTimeout(resolve, 100)); // Check every 100ms console.log('waiting while paused:: ', id); } @@ -56,52 +60,40 @@ class FileUploadService { * @private */ private async checkCancellation() { - if (this.isCancelled) { + if (this.uploadStatus === UploadStateEnum.Cancelled) { logger.warn('Operation cancelled.'); throw new Error('Upload process cancelled.'); } } notify() { - const updatedState = this.pendingFiles.map((f: PendingFileType) => { - return { - id: f.id, - status: f.status, - progress: f.progress, - file: f.originalFile?.type, + const updatedState = Object.keys(this.GroupedPendingFiles).reduce((acc: any, key: string) => { + // uploaded size + const uploadedSize = this.GroupedPendingFiles[key] + .map((f: PendingFileType) => { + // if the file is successful and originalFile exists, add the size to the accumulator + if (f.status === 'success' && f.originalFile?.size) { + return f.originalFile.size; + } + return 0; + }) + .reduce((acc: number, size: number) => acc + size, 0); + // status can be "uploading", "completed", "paused" based on the size, uploadedSize and pausedRoots + const status = this.pausedRoots[key] + ? 'paused' + : uploadedSize === this.RootSizes[key] + ? 'completed' + : 'uploading'; + // Add to the accumulator object + acc[key] = { + items: [], + size: this.RootSizes[key], + uploadedSize, + status, }; - }); - const updatedRootState = Object.keys(this.GroupedPendingFiles).reduce( - (acc: any, key: string) => { - // uploaded size - const uploadedSize = this.GroupedPendingFiles[key] - .map((f: PendingFileType) => { - // if the file is successful and originalFile exists, add the size to the accumulator - if (f.status === 'success' && f.originalFile?.size) { - return f.originalFile.size; - } - return 0; - }) - .reduce((acc: number, size: number) => acc + size, 0); - // status can be "uploading", "completed", "paused" based on the size, uploadedSize and pausedRoots - const status = this.pausedRoots[key] - ? 'paused' - : uploadedSize === this.RootSizes[key] - ? 'completed' - : 'uploading'; - // Add to the accumulator object - acc[key] = { - items: [], - size: this.RootSizes[key], - uploadedSize, - status, - }; - return acc; - }, - {}, - ); + return acc; + }, {}); this.recoilHandler(_.cloneDeep(updatedState)); - this.rootRecoilHandler(_.cloneDeep(updatedRootState)); } public async createDirectories( @@ -114,7 +106,7 @@ class FileUploadService { const root = tree[directory].root as string; if (tree[directory].file instanceof File) { // if root is not in the rootSizes object, add it - if (!this.RootSizes[root] && !this.isCancelled) { + if (!this.RootSizes[root] && this.uploadStatus !== UploadStateEnum.Cancelled) { this.RootSizes[root] = 0; } this.RootSizes[root] += (tree[directory].file as File).size; @@ -135,7 +127,7 @@ class FileUploadService { tmp = false, ) => { // cancel upload - if (this.isCancelled) return; + if (this.uploadStatus === UploadStateEnum.Cancelled) return; // check if upload is paused await this._waitWhilePaused(); @@ -145,7 +137,7 @@ class FileUploadService { const root = tree[directory].root as string; await this.checkCancellation(); await this._waitWhilePaused(root); - if (tree[directory].file instanceof File && !this.isCancelled) { + if (tree[directory].file instanceof File) { logger.trace(`${directory} is a file, save it for future upload`); filesPerParentId[parentId].push({ root: tree[directory].root as string, @@ -250,7 +242,7 @@ class FileUploadService { try { await Promise.all(treePromises); } catch (error) { - if (!this.isCancelled) { + if (this.uploadStatus !== UploadStateEnum.Cancelled) { console.error('An error occurred while processing treePromises:', error); // Optionally, handle the error or rethrow it throw error; // Re-throw the error if necessary @@ -427,7 +419,7 @@ class FileUploadService { } public cancelUpload() { - this.isCancelled = true; + this.uploadStatus === UploadStateEnum.Cancelled; // pause or resume the resumable tasks const fileToCancel = this.pendingFiles; @@ -496,7 +488,18 @@ class FileUploadService { public pauseOrResume() { // pause or resume the curent upload task - this.isPaused = !this.isPaused; + switch (this.uploadStatus) { + case UploadStateEnum.Progress: + this.uploadStatus = UploadStateEnum.Paused; + break; + case UploadStateEnum.Paused: + this.uploadStatus = UploadStateEnum.Progress; + break; + case UploadStateEnum.Cancelled: + throw new Error('Cannot toggle upload status: Upload is cancelled.'); + default: + throw new Error(`Unexpected upload status: ${this.uploadStatus}`); + } // pause or resume the resumable tasks const filesToProcess = this.pendingFiles; @@ -604,10 +607,6 @@ class FileUploadService { this.GroupIds = {}; this.notify(); } - - public getPauseStatus() { - return this.isPaused; - } } export default new FileUploadService(); diff --git a/tdrive/frontend/src/app/features/files/state/selectors/current-task.ts b/tdrive/frontend/src/app/features/files/state/selectors/current-task.ts index 8c3aea396..f92b277b3 100644 --- a/tdrive/frontend/src/app/features/files/state/selectors/current-task.ts +++ b/tdrive/frontend/src/app/features/files/state/selectors/current-task.ts @@ -21,6 +21,7 @@ export const CurrentTaskSelector = selector({ roots: rootList, files: currentTaskFiles, total: currentTaskFiles.length, + status: FileUploadService.uploadStatus, uploaded: currentTaskFiles.filter(f => f.status === 'success').length, completed: currentTaskFiles.every(f => f.status === 'success'), }; From a73064e4dabc580681da6917b0898e4c3928eb87 Mon Sep 17 00:00:00 2001 From: MontaGhanmy Date: Tue, 21 Jan 2025 10:46:20 +0100 Subject: [PATCH 04/13] ref: unnecessary logs --- .../src/services/documents/services/index.ts | 4 -- .../documents/web/controllers/documents.ts | 2 +- .../services/user/services/users/service.ts | 2 - .../node/src/services/user/web/controller.ts | 1 - .../app/components/uploads/file-tree-utils.ts | 9 ---- .../features/files/hooks/use-exp-upload.ts | 1 - .../app/views/client/body/drive/browser.tsx | 46 +++++++++++-------- 7 files changed, 28 insertions(+), 37 deletions(-) diff --git a/tdrive/backend/node/src/services/documents/services/index.ts b/tdrive/backend/node/src/services/documents/services/index.ts index 6f8527369..07baa7436 100644 --- a/tdrive/backend/node/src/services/documents/services/index.ts +++ b/tdrive/backend/node/src/services/documents/services/index.ts @@ -482,12 +482,8 @@ export class DocumentsService { await this.repository.save(driveItem); - console.log("πŸš€πŸš€ DRIVE ITEM SAVED:: ", driveItem); - //TODO[ASH] update item size only for files, there is not need to do during direcotry creation await updateItemSize(driveItem.parent_id, this.repository, context); - - console.log("πŸš€πŸš€ DRIVE ITEM SIZE UPDATED:: ", driveItem); // If AV feature is enabled, scan the file if (!driveItem.is_directory && globalResolver.services.av?.avEnabled && version) { diff --git a/tdrive/backend/node/src/services/documents/web/controllers/documents.ts b/tdrive/backend/node/src/services/documents/web/controllers/documents.ts index b50e741d1..57123dcfd 100644 --- a/tdrive/backend/node/src/services/documents/web/controllers/documents.ts +++ b/tdrive/backend/node/src/services/documents/web/controllers/documents.ts @@ -79,7 +79,7 @@ export class DocumentsController { item, version, context, - request.body.tmp + request.body.tmp, ); } catch (error) { logger.error({ error: `${error}` }, "Failed to create Drive item"); diff --git a/tdrive/backend/node/src/services/user/services/users/service.ts b/tdrive/backend/node/src/services/user/services/users/service.ts index 7cd61a9b1..ac361266b 100644 --- a/tdrive/backend/node/src/services/user/services/users/service.ts +++ b/tdrive/backend/node/src/services/user/services/users/service.ts @@ -217,8 +217,6 @@ export class UserServiceImpl { findOptions.$in = [["id", options.userIds]]; } - console.log("πŸš€πŸš€ Finding user:: ", findFilter, findOptions, context); - return this.repository.find(findFilter, findOptions, context); } diff --git a/tdrive/backend/node/src/services/user/web/controller.ts b/tdrive/backend/node/src/services/user/web/controller.ts index 4fa8edcc5..52c07789e 100644 --- a/tdrive/backend/node/src/services/user/web/controller.ts +++ b/tdrive/backend/node/src/services/user/web/controller.ts @@ -131,7 +131,6 @@ export class UsersCrudController const context = getExecutionContext(request); const userIds = request.query.user_ids ? request.query.user_ids.split(",") : []; - console.log("πŸš€πŸš€ userIds:: ", userIds); let users: ListResult; if (request.query.search) { diff --git a/tdrive/frontend/src/app/components/uploads/file-tree-utils.ts b/tdrive/frontend/src/app/components/uploads/file-tree-utils.ts index f334d57d1..f4e2fec1d 100644 --- a/tdrive/frontend/src/app/components/uploads/file-tree-utils.ts +++ b/tdrive/frontend/src/app/components/uploads/file-tree-utils.ts @@ -135,17 +135,13 @@ export const getFilesTree = ( resolve(true); }, resolve.bind(null, true)); } else if (entry.isDirectory) { - console.log('GetFilesTree:: entriesApi: readDirectory'); const timeToRead = Date.now(); readDirectory(entry, null, resolve); - console.log('GetFilesTree:: entriesApi: readDirectory: ', Date.now() - timeToRead); } }), ); } }); - console.log('GetFilesTree:: entriesApi: slice.call: ', Date.now() - timeBegin); - console.log('GetFilesTree:: entriesApi: rootPromises: ', rootPromises.length, rootPromises); if (files.length > 1000000) { return false; @@ -153,7 +149,6 @@ export const getFilesTree = ( timeBegin = Date.now(); Promise.all(rootPromises).then(cb.bind(null, fd, files)); - console.log('GetFilesTree:: entriesApi: solvePromises: ', Date.now() - timeBegin); } const cb = function (event: Event, files: File[], paths?: string[]) { @@ -184,7 +179,6 @@ export const getFilesTree = ( } }); }); - console.log('GetFilesTree:: cb: ', (Date.now() - begin) / 1000); console.log('tree is:: ', tree); // fcb && fcb(tree, documents_number, total_size); resolve({ tree, documentsCount: documents_number, totalSize: total_size }); @@ -192,14 +186,12 @@ export const getFilesTree = ( // Handle file input based on the event type, starting with `dataTransfer` for drag-and-drop events if (event.dataTransfer) { - console.log('GetFilesTree:: event.dataTransfer'); const dt = event.dataTransfer; // When dragging files into the browser, `dataTransfer.items` contains a list of the dragged items. // `webkitGetAsEntry` allows access to a directory-like API, letting us explore folders and subfolders. // This means we can recursively scan for files in folders without relying on manual user input. if (dt.items && dt.items.length && 'webkitGetAsEntry' in dt.items[0]) { - console.log('GetFilesTree:: webkitGetAsEntry'); // Use `entriesApi` to iterate through items, handling directories and files. // This is ideal for cases where users drag entire folder structures into the app. entriesApi(dt.items, (files, paths) => cb(event, files || [], paths)); @@ -207,7 +199,6 @@ export const getFilesTree = ( // If `getFilesAndDirectories` is available on `dataTransfer`, it indicates a newer API is supported. // This API directly provides both files and directories, making it easier to process structured uploads. else if ('getFilesAndDirectories' in dt) { - console.log('GetFilesTree:: getFilesAndDirectories'); // Use `newDirectoryApi` to process files and directories in a standardized way. newDirectoryApi(dt, (files, paths) => cb(event, files || [], paths)); } diff --git a/tdrive/frontend/src/app/features/files/hooks/use-exp-upload.ts b/tdrive/frontend/src/app/features/files/hooks/use-exp-upload.ts index 6b3b7249c..4151580d1 100644 --- a/tdrive/frontend/src/app/features/files/hooks/use-exp-upload.ts +++ b/tdrive/frontend/src/app/features/files/hooks/use-exp-upload.ts @@ -55,7 +55,6 @@ export const useUploadExp = () => { // Main uploadTree function const uploadTree = (tree: FileTreeObject) => { - console.log('Uploading tree:: ', tree, ' for company: ', companyId); addFilesToResumable(tree.tree); // Add files from the tree startUpload(); // Start uploading }; diff --git a/tdrive/frontend/src/app/views/client/body/drive/browser.tsx b/tdrive/frontend/src/app/views/client/body/drive/browser.tsx index f1602d879..d37c26e37 100644 --- a/tdrive/frontend/src/app/views/client/body/drive/browser.tsx +++ b/tdrive/frontend/src/app/views/client/body/drive/browser.tsx @@ -21,14 +21,14 @@ import { useOnBuildDateContextMenu, useOnBuildSortContextMenu, } from './context-menu'; -import {DocumentRow, DocumentRowOverlay} from './documents/document-row'; +import { DocumentRow, DocumentRowOverlay } from './documents/document-row'; import { FolderRow } from './documents/folder-row'; import { FolderRowSkeleton } from './documents/folder-row-skeleton'; import HeaderPath from './header-path'; import { ConfirmDeleteModal } from './modals/confirm-delete'; import { ConfirmTrashModal } from './modals/confirm-trash'; import { CreateModalAtom } from './modals/create'; -import { UploadModelAtom } from './modals/upload' +import { UploadModelAtom } from './modals/upload'; import { PropertiesModal } from './modals/properties'; import { AccessModal } from './modals/update-access'; import { PublicLinkModal } from './modals/public-link'; @@ -52,8 +52,8 @@ import { SortIcon } from 'app/atoms/icons-agnostic'; import { useUploadExp } from 'app/features/files/hooks/use-exp-upload'; export const DriveCurrentFolderAtom = atomFamily< - string, - { context?: string; initialFolderId: string } + string, + { context?: string; initialFolderId: string } >({ key: 'DriveCurrentFolderAtom', default: options => options.initialFolderId || 'root', @@ -333,16 +333,8 @@ export default memo( ref={uploadZoneRef} driveCollectionKey={uploadZone} onAddFiles={async (_, event) => { - console.log('STARTING UPLOAD...'); - setIsPreparingUpload(true); - const timeBeing = new Date().getTime(); const tree = await getFilesTree(event); - // await new Promise(resolve => setTimeout(resolve, 3000)); - const timeEnd = new Date().getTime(); - console.log('TIME TO UPLOAD: ', (timeEnd - timeBeing) / 1000); setIsPreparingUpload(false); - // _uploadTree(tree); - // // return; setCreationModalState({ parent_id: '', open: false }); uploadTree(tree, { companyId, @@ -371,7 +363,11 @@ export default memo( (loading && (!items?.length || loadingParentChange) ? 'opacity-50 ' : '') } > -
+
{sharedWithMe ? (
@@ -389,7 +385,7 @@ export default memo( { x: evt.clientX, y: evt.clientY }, 'center', undefined, - "browser-share-with-me-menu-file-type" + 'browser-share-with-me-menu-file-type', ); }} testClassId="button-open-menu-file-type" @@ -412,7 +408,7 @@ export default memo( { x: evt.clientX, y: evt.clientY }, 'center', undefined, - "browser-share-with-me-menu-people" + 'browser-share-with-me-menu-people', ); }} testClassId="button-open-menu-people" @@ -432,7 +428,7 @@ export default memo( { x: evt.clientX, y: evt.clientY }, 'center', undefined, - "browser-share-with-me-menu-last-modified" + 'browser-share-with-me-menu-last-modified', ); }} testClassId="button-open-menu-last-modified" @@ -463,9 +459,17 @@ export default memo( </BaseSmall> )} - <Menu menu={() => onBuildSortContextMenu()} sortData={sortLabel} testClassId="browser-menu-sorting"> + <Menu + menu={() => onBuildSortContextMenu()} + sortData={sortLabel} + testClassId="browser-menu-sorting" + > {' '} - <Button theme="outline" className="ml-4 flex flex-row items-center border-0 md:border !text-gray-500 md:!text-blue-500 px-0 md:px-4" testClassId="button-sorting"> + <Button + theme="outline" + className="ml-4 flex flex-row items-center border-0 md:border !text-gray-500 md:!text-blue-500 px-0 md:px-4" + testClassId="button-sorting" + > <SortIcon className={`h-4 w-4 mr-2 -ml-1 ${ sortLabel.order === 'asc' ? 'transform rotate-180' : '' @@ -480,7 +484,11 @@ export default memo( {viewId !== 'shared_with_me' && ( <Menu menu={() => onBuildContextMenu(details)} testClassId="browser-menu-more"> {' '} - <Button theme="secondary" className="ml-4 flex flex-row items-center bg-transparent md:bg-blue-500 md:bg-opacity-25 !text-gray-500 md:!text-blue-500 px-0 md:px-4" testClassId="button-more"> + <Button + theme="secondary" + className="ml-4 flex flex-row items-center bg-transparent md:bg-blue-500 md:bg-opacity-25 !text-gray-500 md:!text-blue-500 px-0 md:px-4" + testClassId="button-more" + > <span> {selectedCount > 1 ? `${selectedCount} items` From 6ced85d0969055890c46faa9790e82688cd334cb Mon Sep 17 00:00:00 2001 From: MontaGhanmy <monta.ghanmy@gmail.com> Date: Tue, 21 Jan 2025 17:34:35 +0100 Subject: [PATCH 05/13] ref: size per root --- .../documents/web/controllers/documents.ts | 2 +- .../pending-root-list.tsx | 4 ++- .../pending-root-row.tsx | 9 ++++++- .../file-uploads/uploads-viewer.tsx | 4 +-- .../app/components/uploads/file-tree-utils.ts | 24 ++++++++++++++---- .../features/drive/hooks/use-drive-upload.tsx | 4 +-- .../files/services/file-upload-service.ts | 25 ++++++------------- .../state/atoms/root-pending-files-list.ts | 1 + .../files/state/selectors/current-task.ts | 1 + .../src/app/features/files/types/file.ts | 1 + 10 files changed, 44 insertions(+), 31 deletions(-) diff --git a/tdrive/backend/node/src/services/documents/web/controllers/documents.ts b/tdrive/backend/node/src/services/documents/web/controllers/documents.ts index 57123dcfd..0fe7df111 100644 --- a/tdrive/backend/node/src/services/documents/web/controllers/documents.ts +++ b/tdrive/backend/node/src/services/documents/web/controllers/documents.ts @@ -129,7 +129,7 @@ export class DocumentsController { reply.status(200).send(); } catch (error) { logger.error({ error: `${error}` }, "Failed to restore drive item"); - throw new CrudException("Failed to restore drive item", 500); + throw new CrudException(`Failed to restore drive item: ${error}`, 500); } }; diff --git a/tdrive/frontend/src/app/components/file-uploads/pending-root-components/pending-root-list.tsx b/tdrive/frontend/src/app/components/file-uploads/pending-root-components/pending-root-list.tsx index 8a8e6a1bf..b5e836a1b 100644 --- a/tdrive/frontend/src/app/components/file-uploads/pending-root-components/pending-root-list.tsx +++ b/tdrive/frontend/src/app/components/file-uploads/pending-root-components/pending-root-list.tsx @@ -67,9 +67,11 @@ const ModalFooter: React.FC<ModalFooterProps> = ({ const PendingRootList = ({ roots, status, + parentId, }: { roots: UploadRootListType; status: UploadStateEnum; + parentId: string; }): JSX.Element => { const [modalExpanded, setModalExpanded] = useState(true); const { pauseOrResumeUpload, cancelUpload } = useUpload(); @@ -104,7 +106,7 @@ const PendingRootList = ({ <div className="modal-body"> <div className="bg-white px-4 py-2"> {keys.map(key => ( - <PendingRootRow key={key} rootKey={key} root={roots[key]} /> + <PendingRootRow key={key} rootKey={key} root={roots[key]} parentId={parentId} /> ))} </div> <ModalFooter diff --git a/tdrive/frontend/src/app/components/file-uploads/pending-root-components/pending-root-row.tsx b/tdrive/frontend/src/app/components/file-uploads/pending-root-components/pending-root-row.tsx index 922b9e8f9..ae0ca9865 100644 --- a/tdrive/frontend/src/app/components/file-uploads/pending-root-components/pending-root-row.tsx +++ b/tdrive/frontend/src/app/components/file-uploads/pending-root-components/pending-root-row.tsx @@ -12,16 +12,20 @@ import { ShowFolderIcon, } from 'app/atoms/icons-colored'; import { fileTypeIconsMap } from './file-type-icon-map'; +import { useDriveActions } from 'app/features/drive/hooks/use-drive-actions'; const PendingRootRow = ({ rootKey, root, + parentId, }: { rootKey: string; root: UploadRootType; + parentId: string; }): JSX.Element => { const { pauseOrResumeRootUpload, cancelRootUpload, clearRoots } = useUpload(); const [showFolder, setShowFolder] = useState(false); + const { restore } = useDriveActions(); const firstPendingFile = root.items[0]; const uploadedFilesSize = root.uploadedSize; @@ -55,7 +59,10 @@ const PendingRootRow = ({ // after the green check icon appears useEffect(() => { if (isUploadCompleted) { - const timeout = setTimeout(() => setShowFolder(true), 1500); + const timeout = setTimeout(async () => { + setShowFolder(true); + await restore(root.id, parentId); + }, 1500); return () => clearTimeout(timeout); } }, [isUploadCompleted]); diff --git a/tdrive/frontend/src/app/components/file-uploads/uploads-viewer.tsx b/tdrive/frontend/src/app/components/file-uploads/uploads-viewer.tsx index 2727ee053..9b2e89148 100644 --- a/tdrive/frontend/src/app/components/file-uploads/uploads-viewer.tsx +++ b/tdrive/frontend/src/app/components/file-uploads/uploads-viewer.tsx @@ -5,7 +5,7 @@ const UploadsViewer = (): JSX.Element => { const { currentTask } = useUpload(); // Destructure and provide default values for safety - const { roots = {}, status } = currentTask || {}; + const { roots = {}, status, parentId } = currentTask || {}; const rootKeys = Object.keys(roots); // Early return for clarity @@ -13,7 +13,7 @@ const UploadsViewer = (): JSX.Element => { return <></>; } - return <PendingRootList roots={roots} status={status} />; + return <PendingRootList roots={roots} status={status} parentId={parentId} />; }; export default UploadsViewer; diff --git a/tdrive/frontend/src/app/components/uploads/file-tree-utils.ts b/tdrive/frontend/src/app/components/uploads/file-tree-utils.ts index f4e2fec1d..5d2903a6b 100644 --- a/tdrive/frontend/src/app/components/uploads/file-tree-utils.ts +++ b/tdrive/frontend/src/app/components/uploads/file-tree-utils.ts @@ -1,3 +1,5 @@ +import { number } from 'prop-types'; + /* eslint-disable @typescript-eslint/no-explicit-any */ type TreeItem = { [key: string]: { root: string; file: File } | TreeItem }; @@ -5,6 +7,7 @@ export type FileTreeObject = { tree: TreeItem; documentsCount: number; totalSize: number; + sizePerRoot: { [key: string]: number }; }; export const getFilesTree = ( @@ -152,10 +155,10 @@ export const getFilesTree = ( } const cb = function (event: Event, files: File[], paths?: string[]) { - const begin = Date.now(); const documents_number = paths ? paths.length : 0; let total_size = 0; const tree: any = {}; + const size_per_root: { [key: string]: number } = {}; (paths || []).forEach(function (path, file_index) { let dirs = tree; const real_file = files[file_index]; @@ -167,10 +170,17 @@ export const getFilesTree = ( return; } if (dir_index === path.split('/').length - 1) { + const root = path.split('/')[0]; dirs[dir] = { - root: path.split('/')[0], file: real_file, + root, }; + // Calculate the total size of each root + if (!size_per_root[root]) { + size_per_root[root] = real_file.size; + } else { + size_per_root[root] += real_file.size; + } } else { if (!dirs[dir]) { dirs[dir] = {}; @@ -179,9 +189,12 @@ export const getFilesTree = ( } }); }); - console.log('tree is:: ', tree); - // fcb && fcb(tree, documents_number, total_size); - resolve({ tree, documentsCount: documents_number, totalSize: total_size }); + resolve({ + tree, + documentsCount: documents_number, + totalSize: total_size, + sizePerRoot: size_per_root, + }); }; // Handle file input based on the event type, starting with `dataTransfer` for drag-and-drop events @@ -245,6 +258,7 @@ export const getFilesTree = ( tree: (event.target as any).files[0], documentsCount: 1, totalSize: (event.target as any).files[0].size, + sizePerRoot: { [(event.target as any).files[0].name]: (event.target as any).files[0].size }, }); } }); diff --git a/tdrive/frontend/src/app/features/drive/hooks/use-drive-upload.tsx b/tdrive/frontend/src/app/features/drive/hooks/use-drive-upload.tsx index 5bb153a70..4abce7a8e 100644 --- a/tdrive/frontend/src/app/features/drive/hooks/use-drive-upload.tsx +++ b/tdrive/frontend/src/app/features/drive/hooks/use-drive-upload.tsx @@ -48,9 +48,7 @@ export const useDriveUpload = () => { context: { companyId: string; parentId: string }, ) => { logger.debug('Start creating directories and file upload ...'); - const { idsToBeRestored } = await FileUploadService.createDirectories(tree.tree, context); - - await Promise.all(idsToBeRestored.map(id => restore(id, context.parentId))); + await FileUploadService.createDirectories(tree, context); await refresh(context.parentId, true); }; diff --git a/tdrive/frontend/src/app/features/files/services/file-upload-service.ts b/tdrive/frontend/src/app/features/files/services/file-upload-service.ts index bb29bf9a0..bfde8455e 100644 --- a/tdrive/frontend/src/app/features/files/services/file-upload-service.ts +++ b/tdrive/frontend/src/app/features/files/services/file-upload-service.ts @@ -35,6 +35,7 @@ class FileUploadService { private GroupIds: { [key: string]: string } = {}; private pausedRoots: { [key: string]: boolean } = {}; public currentTaskId = ''; + public parentId = ''; public uploadStatus = UploadStateEnum.Progress; private recoilHandler: Function = () => undefined; private logger: Logger.Logger = Logger.getLogger('FileUploadService'); @@ -86,6 +87,7 @@ class FileUploadService { : 'uploading'; // Add to the accumulator object acc[key] = { + id: this.GroupIds[key], items: [], size: this.RootSizes[key], uploadedSize, @@ -97,25 +99,11 @@ class FileUploadService { } public async createDirectories( - root: FileTreeObject['tree'], + tree: FileTreeObject, context: { companyId: string; parentId: string }, ) { - // loop through the tree and determine root sizes and set it to the rootSizes object - const traverser = (tree: FileTreeObject['tree']) => { - for (const directory of Object.keys(tree)) { - const root = tree[directory].root as string; - if (tree[directory].file instanceof File) { - // if root is not in the rootSizes object, add it - if (!this.RootSizes[root] && this.uploadStatus !== UploadStateEnum.Cancelled) { - this.RootSizes[root] = 0; - } - this.RootSizes[root] += (tree[directory].file as File).size; - } else { - traverser(tree[directory] as FileTreeObject['tree']); - } - } - }; - traverser(root); + const root = tree.tree; + this.RootSizes = tree.sizePerRoot || {}; // Create all directories const filesPerParentId: { [key: string]: { root: string; file: File }[] } = {}; filesPerParentId[context.parentId] = []; @@ -184,7 +172,8 @@ class FileUploadService { this.notify(); if (driveItem?.id) { filesPerParentId[driveItem.id] = []; - if (tmp && idsToBeRestored.includes(driveItem.id)) idsToBeRestored.push(driveItem.id); + if (tmp && !idsToBeRestored.includes(driveItem.id)) + idsToBeRestored.push(driveItem.id); await traverserTreeLevel(tree[directory] as FileTreeObject['tree'], driveItem.id); } } catch (e) { diff --git a/tdrive/frontend/src/app/features/files/state/atoms/root-pending-files-list.ts b/tdrive/frontend/src/app/features/files/state/atoms/root-pending-files-list.ts index c886c886d..e0a2c2c0e 100644 --- a/tdrive/frontend/src/app/features/files/state/atoms/root-pending-files-list.ts +++ b/tdrive/frontend/src/app/features/files/state/atoms/root-pending-files-list.ts @@ -4,6 +4,7 @@ import { PendingFileRecoilType } from '@features/files/types/file'; export const RootPendingFilesListState = atom< | { [key: string]: { + id: string; size: number; uploadedSize: number; status: string; diff --git a/tdrive/frontend/src/app/features/files/state/selectors/current-task.ts b/tdrive/frontend/src/app/features/files/state/selectors/current-task.ts index f92b277b3..df4b41f66 100644 --- a/tdrive/frontend/src/app/features/files/state/selectors/current-task.ts +++ b/tdrive/frontend/src/app/features/files/state/selectors/current-task.ts @@ -18,6 +18,7 @@ export const CurrentTaskSelector = selector({ : []; return { + parentId: FileUploadService.parentId, roots: rootList, files: currentTaskFiles, total: currentTaskFiles.length, diff --git a/tdrive/frontend/src/app/features/files/types/file.ts b/tdrive/frontend/src/app/features/files/types/file.ts index 5c221f079..a2dfeaead 100644 --- a/tdrive/frontend/src/app/features/files/types/file.ts +++ b/tdrive/frontend/src/app/features/files/types/file.ts @@ -89,6 +89,7 @@ export type PendingFileType = { }; export type UploadRootType = { + id: string; size: number; uploadedSize: number; status: string; From cda801896d54fb5878ac3cfb80e40c316c6c2576 Mon Sep 17 00:00:00 2001 From: MontaGhanmy <monta.ghanmy@gmail.com> Date: Wed, 22 Jan 2025 17:53:55 +0100 Subject: [PATCH 06/13] ref: feedback refreshing/upload modal title --- tdrive/frontend/public/locales/en.json | 2 + tdrive/frontend/public/locales/fr.json | 2 + tdrive/frontend/public/locales/ru.json | 2 + tdrive/frontend/public/locales/vi.json | 2 + .../pending-root-list.tsx | 43 +++++++++++++------ .../pending-root-row.tsx | 13 +++++- .../features/drive/hooks/use-drive-upload.tsx | 1 - .../app/features/files/hooks/use-upload.ts | 2 +- 8 files changed, 51 insertions(+), 16 deletions(-) diff --git a/tdrive/frontend/public/locales/en.json b/tdrive/frontend/public/locales/en.json index 40a761855..4790beb19 100644 --- a/tdrive/frontend/public/locales/en.json +++ b/tdrive/frontend/public/locales/en.json @@ -173,6 +173,7 @@ "general.add": "Add", "general.back": "Back", "general.cancel": "Cancel", + "general.close": "Close", "general.connexion_status.connected": "You are online", "general.connexion_status.connecting": "Reconnecting…", "general.connexion_status.disconnected": "You are offline", @@ -185,6 +186,7 @@ "general.send": "Send message", "general.update": "Update", "general.uploading": "Uploading", + "general.uploaded": "Uploaded", "general.files": "files", "general.user.anonymous": "Anonymous", "general.user.deactivated": "User is no longer in this company", diff --git a/tdrive/frontend/public/locales/fr.json b/tdrive/frontend/public/locales/fr.json index 3da86f5c0..c41e89fc0 100644 --- a/tdrive/frontend/public/locales/fr.json +++ b/tdrive/frontend/public/locales/fr.json @@ -166,6 +166,7 @@ "general.add": "Ajouter", "general.back": "Retour", "general.cancel": "Annuler", + "general.close": "Fermer", "general.connexion_status.connected": "Vous Γͺtes connectΓ©", "general.connexion_status.connecting": "Reconnexion en cours…", "general.connexion_status.disconnected": "Vous Γͺtes hors ligne", @@ -178,6 +179,7 @@ "general.send": "Envoyer un message", "general.update": "Mettre Γ  jour", "general.uploading": "TΓ©lΓ©chargement en cours", + "general.uploaded": "TΓ©lΓ©versΓ©", "general.files": "fichiers", "general.user.anonymous": "Compte anonyme", "general.user.deactivated": "Cet utilisateur n'est plus dans cette l'entreprise", diff --git a/tdrive/frontend/public/locales/ru.json b/tdrive/frontend/public/locales/ru.json index e248da68e..8fc03ec2b 100644 --- a/tdrive/frontend/public/locales/ru.json +++ b/tdrive/frontend/public/locales/ru.json @@ -185,6 +185,8 @@ "general.send": "ΠžΡ‚ΠΏΡ€Π°Π²ΠΈΡ‚ΡŒ сообщСниС", "general.update": "ΠžΠ±Π½ΠΎΠ²ΠΈΡ‚ΡŒ", "general.uploading": "Π—Π°Π³Ρ€ΡƒΠ·ΠΊΠ°", + "general.uploaded": "Π—Π°Π³Ρ€ΡƒΠΆΠ΅Π½ΠΎ", + "general.close": "Π—Π°ΠΊΡ€Ρ‹Ρ‚ΡŒ", "general.files": "Ρ„Π°ΠΉΠ»Ρ‹", "general.user.anonymous": "Анонимный", "general.user.deactivated": "ΠŸΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»ΡŒ большС Π½Π΅ состоит Π² этой ΠΊΠΎΠΌΠΏΠ°Π½ΠΈΠΈ", diff --git a/tdrive/frontend/public/locales/vi.json b/tdrive/frontend/public/locales/vi.json index 58c197a1a..c256398b5 100644 --- a/tdrive/frontend/public/locales/vi.json +++ b/tdrive/frontend/public/locales/vi.json @@ -155,6 +155,7 @@ "general.add": "ThΓͺm", "general.back": "Quay lαΊ‘i", "general.cancel": "Hủy", + "general.close": "Đóng", "general.connexion_status.connected": "BαΊ‘n Δ‘ang trα»±c tuyαΊΏn", "general.connexion_status.connecting": "Đang kαΊΏt nα»‘i...", "general.connexion_status.disconnected": "BαΊ‘n Δ‘ang ngoαΊ‘i tuyαΊΏn", @@ -167,6 +168,7 @@ "general.send": "Gα»­i", "general.update": "CαΊ­p nhαΊ­t", "general.uploading": "Đang tαΊ£i lΓͺn", + "general.uploaded": "Đã tαΊ£i lΓͺn", "general.files": "tệp", "general.user.anonymous": "VΓ΄ danh", "general.user.deactivated": "Người dΓΉng khΓ΄ng cΓ²n thuα»™c cΓ΄ng ty nΓ y nα»―a", diff --git a/tdrive/frontend/src/app/components/file-uploads/pending-root-components/pending-root-list.tsx b/tdrive/frontend/src/app/components/file-uploads/pending-root-components/pending-root-list.tsx index b5e836a1b..9e2af1342 100644 --- a/tdrive/frontend/src/app/components/file-uploads/pending-root-components/pending-root-list.tsx +++ b/tdrive/frontend/src/app/components/file-uploads/pending-root-components/pending-root-list.tsx @@ -1,5 +1,6 @@ import { useState, useMemo, useCallback } from 'react'; import { useUpload } from '@features/files/hooks/use-upload'; +import PerfectScrollbar from 'react-perfect-scrollbar'; import { ArrowDownIcon, ArrowUpIcon } from 'app/atoms/icons-colored'; import { UploadRootListType } from 'app/features/files/types/file'; import Languages from '@features/global/services/languages-service'; @@ -14,6 +15,7 @@ const getFilteredRoots = (keys: string[], roots: UploadRootListType) => { interface ModalHeaderProps { uploadingCount: number; + completedCount: number; totalRoots: number; uploadingPercentage: number; toggleModal: () => void; @@ -22,6 +24,7 @@ interface ModalHeaderProps { const ModalHeader: React.FC<ModalHeaderProps> = ({ uploadingCount, + completedCount, totalRoots, uploadingPercentage, toggleModal, @@ -29,8 +32,9 @@ const ModalHeader: React.FC<ModalHeaderProps> = ({ }) => ( <div className="w-full flex bg-[#45454A] text-white p-4 items-center justify-between"> <p> - {Languages.t('general.uploading')} {uploadingCount}/{totalRoots}{' '} - {Languages.t('general.files')}... {uploadingPercentage}% + {uploadingCount ? Languages.t('general.uploading') : Languages.t('general.uploaded')}{' '} + {uploadingCount + completedCount}/{totalRoots} {Languages.t('general.files')}...{' '} + {uploadingPercentage}% </p> <button className="ml-auto flex items-center" onClick={toggleModal}> {modalExpanded ? <ArrowDownIcon /> : <ArrowUpIcon />} @@ -42,23 +46,27 @@ interface ModalFooterProps { pauseOrResumeUpload: () => void; cancelUpload: () => void; isPaused: () => boolean; + uploadingCount: number; } const ModalFooter: React.FC<ModalFooterProps> = ({ pauseOrResumeUpload, cancelUpload, isPaused, + uploadingCount, }) => ( <div className="w-full flex bg-[#F0F2F3] text-black p-4 items-center justify-between"> <div className="flex space-x-4 ml-auto"> - <button - className="text-blue-500 px-4 py-2 rounded hover:bg-blue-600" - onClick={pauseOrResumeUpload} - > - {isPaused() ? Languages.t('general.resume') : Languages.t('general.pause')} - </button> + {uploadingCount > 0 && ( + <button + className="text-blue-500 px-4 py-2 rounded hover:bg-blue-600" + onClick={pauseOrResumeUpload} + > + {isPaused() ? Languages.t('general.resume') : Languages.t('general.pause')} + </button> + )} <button className="text-blue-500 px-4 py-2 rounded hover:bg-blue-600" onClick={cancelUpload}> - {Languages.t('general.cancel')} + {uploadingCount ? Languages.t('general.cancel') : Languages.t('general.close')} </button> </div> </div> @@ -77,7 +85,7 @@ const PendingRootList = ({ const { pauseOrResumeUpload, cancelUpload } = useUpload(); const keys = useMemo(() => Object.keys(roots || {}), [roots]); - const { inProgress: rootsInProgress } = useMemo( + const { inProgress: rootsInProgress, completed: rootsCompleted } = useMemo( () => getFilteredRoots(keys, roots), [keys, roots], ); @@ -86,6 +94,7 @@ const PendingRootList = ({ const totalRoots = keys.length; const uploadingCount = rootsInProgress.length; + const completedCount = rootsCompleted.length; const uploadingPercentage = Math.floor((uploadingCount / totalRoots) * 100) || 100; const toggleModal = useCallback(() => setModalExpanded(prev => !prev), []); @@ -96,6 +105,7 @@ const PendingRootList = ({ <div className="fixed bottom-4 right-4 w-1/3 shadow-lg rounded-sm overflow-hidden"> <ModalHeader uploadingCount={uploadingCount} + completedCount={completedCount} totalRoots={totalRoots} uploadingPercentage={uploadingPercentage} toggleModal={toggleModal} @@ -105,14 +115,21 @@ const PendingRootList = ({ {modalExpanded && ( <div className="modal-body"> <div className="bg-white px-4 py-2"> - {keys.map(key => ( - <PendingRootRow key={key} rootKey={key} root={roots[key]} parentId={parentId} /> - ))} + <PerfectScrollbar + options={{ suppressScrollX: true, suppressScrollY: false }} + component="div" + style={{ width: '100%', maxHeight: 300 }} + > + {keys.map(key => ( + <PendingRootRow key={key} rootKey={key} root={roots[key]} parentId={parentId} /> + ))} + </PerfectScrollbar> </div> <ModalFooter pauseOrResumeUpload={pauseOrResumeUpload} cancelUpload={cancelUpload} isPaused={isPaused} + uploadingCount={uploadingCount} /> </div> )} diff --git a/tdrive/frontend/src/app/components/file-uploads/pending-root-components/pending-root-row.tsx b/tdrive/frontend/src/app/components/file-uploads/pending-root-components/pending-root-row.tsx index ae0ca9865..2064a7881 100644 --- a/tdrive/frontend/src/app/components/file-uploads/pending-root-components/pending-root-row.tsx +++ b/tdrive/frontend/src/app/components/file-uploads/pending-root-components/pending-root-row.tsx @@ -13,6 +13,7 @@ import { } from 'app/atoms/icons-colored'; import { fileTypeIconsMap } from './file-type-icon-map'; import { useDriveActions } from 'app/features/drive/hooks/use-drive-actions'; +import { useDriveItem } from 'app/features/drive/hooks/use-drive-item'; const PendingRootRow = ({ rootKey, @@ -25,7 +26,9 @@ const PendingRootRow = ({ }): JSX.Element => { const { pauseOrResumeRootUpload, cancelRootUpload, clearRoots } = useUpload(); const [showFolder, setShowFolder] = useState(false); + const [restoredFolder, setRestoredFolder] = useState(false); const { restore } = useDriveActions(); + const { refresh } = useDriveItem(parentId || ''); const firstPendingFile = root.items[0]; const uploadedFilesSize = root.uploadedSize; @@ -61,12 +64,20 @@ const PendingRootRow = ({ if (isUploadCompleted) { const timeout = setTimeout(async () => { setShowFolder(true); - await restore(root.id, parentId); }, 1500); return () => clearTimeout(timeout); } }, [isUploadCompleted]); + useEffect(() => { + if (isUploadCompleted && !restoredFolder) { + setRestoredFolder(true); + console.log('Restoring folder', root.id); + restore(root.id, parentId); + refresh(parentId, true); + } + }, [isUploadCompleted]); + return ( <div className="root-row"> <div className="root-details mt-2"> diff --git a/tdrive/frontend/src/app/features/drive/hooks/use-drive-upload.tsx b/tdrive/frontend/src/app/features/drive/hooks/use-drive-upload.tsx index 4abce7a8e..7156ec4f2 100644 --- a/tdrive/frontend/src/app/features/drive/hooks/use-drive-upload.tsx +++ b/tdrive/frontend/src/app/features/drive/hooks/use-drive-upload.tsx @@ -49,7 +49,6 @@ export const useDriveUpload = () => { ) => { logger.debug('Start creating directories and file upload ...'); await FileUploadService.createDirectories(tree, context); - await refresh(context.parentId, true); }; diff --git a/tdrive/frontend/src/app/features/files/hooks/use-upload.ts b/tdrive/frontend/src/app/features/files/hooks/use-upload.ts index 6b17cc744..30776c12b 100644 --- a/tdrive/frontend/src/app/features/files/hooks/use-upload.ts +++ b/tdrive/frontend/src/app/features/files/hooks/use-upload.ts @@ -42,6 +42,6 @@ export const useUpload = () => { currentTask, deleteOneFile, retryUpload, - clearRoots + clearRoots, }; }; From 6d27567420fa894052741e0e8d5e8bca34f56a64 Mon Sep 17 00:00:00 2001 From: MontaGhanmy <monta.ghanmy@gmail.com> Date: Mon, 27 Jan 2025 00:00:56 +0100 Subject: [PATCH 07/13] ref: feedback with tmp handling and multiple uploads --- .../src/services/documents/services/index.ts | 3 --- .../documents/web/controllers/documents.ts | 2 -- .../pending-root-list.tsx | 3 +-- .../pending-root-row.tsx | 12 +++++++--- .../features/drive/api-client/api-client.ts | 21 +++++++++------- .../files/services/file-upload-service.ts | 24 +++++++++++++++---- 6 files changed, 42 insertions(+), 23 deletions(-) diff --git a/tdrive/backend/node/src/services/documents/services/index.ts b/tdrive/backend/node/src/services/documents/services/index.ts index 07baa7436..bf64ec717 100644 --- a/tdrive/backend/node/src/services/documents/services/index.ts +++ b/tdrive/backend/node/src/services/documents/services/index.ts @@ -371,7 +371,6 @@ export class DocumentsService { content: Partial<DriveFile>, version: Partial<FileVersion>, context: DriveExecutionContext, - tmp = false, ): Promise<DriveFile> => { try { const driveItem = getDefaultDriveItem(content, context); @@ -472,8 +471,6 @@ export class DocumentsService { logger.error(error, "πŸš€πŸš€ error:"); } - if (tmp) driveItem.is_in_trash = true; - await this.repository.save(driveItem); driveItemVersion.drive_item_id = driveItem.id; diff --git a/tdrive/backend/node/src/services/documents/web/controllers/documents.ts b/tdrive/backend/node/src/services/documents/web/controllers/documents.ts index 0fe7df111..fa719f309 100644 --- a/tdrive/backend/node/src/services/documents/web/controllers/documents.ts +++ b/tdrive/backend/node/src/services/documents/web/controllers/documents.ts @@ -47,7 +47,6 @@ export class DocumentsController { Body: { item: Partial<DriveFile>; version: Partial<FileVersion>; - tmp?: boolean; }; }>, ): Promise<DriveFile | any> => { @@ -79,7 +78,6 @@ export class DocumentsController { item, version, context, - request.body.tmp, ); } catch (error) { logger.error({ error: `${error}` }, "Failed to create Drive item"); diff --git a/tdrive/frontend/src/app/components/file-uploads/pending-root-components/pending-root-list.tsx b/tdrive/frontend/src/app/components/file-uploads/pending-root-components/pending-root-list.tsx index 9e2af1342..e5cbd51b1 100644 --- a/tdrive/frontend/src/app/components/file-uploads/pending-root-components/pending-root-list.tsx +++ b/tdrive/frontend/src/app/components/file-uploads/pending-root-components/pending-root-list.tsx @@ -33,8 +33,7 @@ const ModalHeader: React.FC<ModalHeaderProps> = ({ <div className="w-full flex bg-[#45454A] text-white p-4 items-center justify-between"> <p> {uploadingCount ? Languages.t('general.uploading') : Languages.t('general.uploaded')}{' '} - {uploadingCount + completedCount}/{totalRoots} {Languages.t('general.files')}...{' '} - {uploadingPercentage}% + {uploadingCount + completedCount}/{totalRoots} {Languages.t('general.files')} </p> <button className="ml-auto flex items-center" onClick={toggleModal}> {modalExpanded ? <ArrowDownIcon /> : <ArrowUpIcon />} diff --git a/tdrive/frontend/src/app/components/file-uploads/pending-root-components/pending-root-row.tsx b/tdrive/frontend/src/app/components/file-uploads/pending-root-components/pending-root-row.tsx index 2064a7881..8b6de2d33 100644 --- a/tdrive/frontend/src/app/components/file-uploads/pending-root-components/pending-root-row.tsx +++ b/tdrive/frontend/src/app/components/file-uploads/pending-root-components/pending-root-row.tsx @@ -70,11 +70,17 @@ const PendingRootRow = ({ }, [isUploadCompleted]); useEffect(() => { + const postProcess = async () => { + if (isUploadCompleted && !restoredFolder) { + await new Promise(resolve => setTimeout(resolve, 1000)); + await restore(root.id, parentId); + await new Promise(resolve => setTimeout(resolve, 1000)); + await refresh(parentId); + } + }; if (isUploadCompleted && !restoredFolder) { setRestoredFolder(true); - console.log('Restoring folder', root.id); - restore(root.id, parentId); - refresh(parentId, true); + postProcess(); } }, [isUploadCompleted]); diff --git a/tdrive/frontend/src/app/features/drive/api-client/api-client.ts b/tdrive/frontend/src/app/features/drive/api-client/api-client.ts index e4f3d7c11..fe4e562e9 100644 --- a/tdrive/frontend/src/app/features/drive/api-client/api-client.ts +++ b/tdrive/frontend/src/app/features/drive/api-client/api-client.ts @@ -1,5 +1,13 @@ import Api from '../../global/framework/api-service'; -import { BrowseFilter, BrowsePaginate, BrowseQuery, BrowseSort, DriveItem, DriveItemDetails, DriveItemVersion } from '../types'; +import { + BrowseFilter, + BrowsePaginate, + BrowseQuery, + BrowseSort, + DriveItem, + DriveItemDetails, + DriveItemVersion, +} from '../types'; import Workspace from '@deprecated/workspaces/workspaces'; import Logger from 'features/global/framework/logger-service'; import { JWTDataType } from 'app/features/auth/jwt-storage-service'; @@ -115,18 +123,15 @@ export class DriveApiClient { static async create( companyId: string, - data: { item: Partial<DriveItem>; version?: Partial<DriveItemVersion>; tmp?: boolean }, + data: { item: Partial<DriveItem>; version?: Partial<DriveItemVersion> }, ) { if (!data.version) data.version = {} as Partial<DriveItemVersion>; return new Promise<DriveItem>((resolve, reject) => { - Api.post< - { item: Partial<DriveItem>; version: Partial<DriveItemVersion>; tmp?: boolean }, - DriveItem - >( + Api.post<{ item: Partial<DriveItem>; version: Partial<DriveItemVersion> }, DriveItem>( `/internal/services/documents/v1/companies/${companyId}/item${appendTdriveToken()}`, - data as { item: Partial<DriveItem>; version: Partial<DriveItemVersion>; tmp?: boolean }, - (res) => { + data as { item: Partial<DriveItem>; version: Partial<DriveItemVersion> }, + res => { if ((res as any)?.statusCode || (res as any)?.error) { reject(res); } diff --git a/tdrive/frontend/src/app/features/files/services/file-upload-service.ts b/tdrive/frontend/src/app/features/files/services/file-upload-service.ts index bfde8455e..6aff3170c 100644 --- a/tdrive/frontend/src/app/features/files/services/file-upload-service.ts +++ b/tdrive/frontend/src/app/features/files/services/file-upload-service.ts @@ -102,8 +102,18 @@ class FileUploadService { tree: FileTreeObject, context: { companyId: string; parentId: string }, ) { + // init everything + // this.currentTaskId = ''; + // this.pendingFiles = []; + // this.GroupedPendingFiles = {}; + // this.GroupIds = {}; + // this.pausedRoots = {}; + // this.notify(); const root = tree.tree; - this.RootSizes = tree.sizePerRoot || {}; + this.RootSizes = this.RootSizes = { + ...this.RootSizes, + ...(tree.sizePerRoot || {}), + }; // Create all directories const filesPerParentId: { [key: string]: { root: string; file: File }[] } = {}; filesPerParentId[context.parentId] = []; @@ -126,10 +136,11 @@ class FileUploadService { await this.checkCancellation(); await this._waitWhilePaused(root); if (tree[directory].file instanceof File) { - logger.trace(`${directory} is a file, save it for future upload`); + const file = tree[directory].file as File; + console.log(`Adding file: ${file.name} under parentId: ${parentId}`); filesPerParentId[parentId].push({ root: tree[directory].root as string, - file: tree[directory].file as File, + file, }); } else { logger.debug(`Create directory ${directory}`); @@ -139,6 +150,7 @@ class FileUploadService { parent_id: parentId, name: directory, is_directory: true, + is_in_trash: tmp, }; if (!this.pendingFiles.some(f => isPendingFileStatusPending(f.status))) { @@ -164,7 +176,6 @@ class FileUploadService { const driveItem = await DriveApiClient.create(context.companyId, { item: item, version: {}, - tmp, }); this.GroupIds[directory] = driveItem.id; this.logger.debug(`Directory ${directory} created`); @@ -183,7 +194,10 @@ class FileUploadService { } } // uploading the files goes here - await this.upload(filesPerParentId[parentId], { + const files = _.cloneDeep(filesPerParentId[parentId]); + // reset the filesPerParentId + filesPerParentId[parentId] = []; + await this.upload(files, { context: { companyId: context.companyId, parentId: parentId, From 1539dfa10692cccd12eacde748760acbc06f0646 Mon Sep 17 00:00:00 2001 From: MontaGhanmy <monta.ghanmy@gmail.com> Date: Mon, 27 Jan 2025 10:37:18 +0100 Subject: [PATCH 08/13] ref: feedback with icon an progress header --- .../pending-root-list.tsx | 6 ++-- .../pending-root-row.tsx | 7 ++++- .../search-popup/parts/drive-item-result.tsx | 29 +++++++++++++++---- .../app/views/client/body/drive/browser.tsx | 3 +- .../body/drive/documents/document-icon.tsx | 2 +- .../body/drive/documents/folder-row.tsx | 2 +- .../body/drive/modals/selector/index.tsx | 3 +- 7 files changed, 39 insertions(+), 13 deletions(-) diff --git a/tdrive/frontend/src/app/components/file-uploads/pending-root-components/pending-root-list.tsx b/tdrive/frontend/src/app/components/file-uploads/pending-root-components/pending-root-list.tsx index e5cbd51b1..7c0110c94 100644 --- a/tdrive/frontend/src/app/components/file-uploads/pending-root-components/pending-root-list.tsx +++ b/tdrive/frontend/src/app/components/file-uploads/pending-root-components/pending-root-list.tsx @@ -32,8 +32,10 @@ const ModalHeader: React.FC<ModalHeaderProps> = ({ }) => ( <div className="w-full flex bg-[#45454A] text-white p-4 items-center justify-between"> <p> - {uploadingCount ? Languages.t('general.uploading') : Languages.t('general.uploaded')}{' '} - {uploadingCount + completedCount}/{totalRoots} {Languages.t('general.files')} + {uploadingCount > 0 + ? `${Languages.t('general.uploading')} ${uploadingCount}/${totalRoots}` + : `${Languages.t('general.uploaded')} ${completedCount}`}{' '} + {Languages.t('general.files')} </p> <button className="ml-auto flex items-center" onClick={toggleModal}> {modalExpanded ? <ArrowDownIcon /> : <ArrowUpIcon />} diff --git a/tdrive/frontend/src/app/components/file-uploads/pending-root-components/pending-root-row.tsx b/tdrive/frontend/src/app/components/file-uploads/pending-root-components/pending-root-row.tsx index 8b6de2d33..0c1b55517 100644 --- a/tdrive/frontend/src/app/components/file-uploads/pending-root-components/pending-root-row.tsx +++ b/tdrive/frontend/src/app/components/file-uploads/pending-root-components/pending-root-row.tsx @@ -72,6 +72,7 @@ const PendingRootRow = ({ useEffect(() => { const postProcess = async () => { if (isUploadCompleted && !restoredFolder) { + console.log("THE UPLOAD FINISHED WILL REFRESH"); await new Promise(resolve => setTimeout(resolve, 1000)); await restore(root.id, parentId); await new Promise(resolve => setTimeout(resolve, 1000)); @@ -88,7 +89,11 @@ const PendingRootRow = ({ <div className="root-row"> <div className="root-details mt-2"> <div className="flex items-center"> - {itemTypeIcon(firstPendingFile?.type)} + <div className="w-10 h-10 flex items-center justify-center bg-[#f3f3f7] rounded-md"> + <div className="w-full h-full flex items-center justify-center"> + {itemTypeIcon(firstPendingFile?.type)} + </div> + </div> <p className="ml-4">{rootKey}</p> <div className="progress-check flex items-center justify-center ml-auto"> diff --git a/tdrive/frontend/src/app/components/search-popup/parts/drive-item-result.tsx b/tdrive/frontend/src/app/components/search-popup/parts/drive-item-result.tsx index ca30c09f6..28a8e06ff 100644 --- a/tdrive/frontend/src/app/components/search-popup/parts/drive-item-result.tsx +++ b/tdrive/frontend/src/app/components/search-popup/parts/drive-item-result.tsx @@ -1,4 +1,4 @@ -import { FolderIcon } from '@heroicons/react/solid'; +import { FolderIcon } from 'app/atoms/icons-colored'; import Highlighter from 'react-highlight-words'; import { useRecoilValue } from 'recoil'; import { onDriveItemDownloadClick } from '../common'; @@ -20,7 +20,7 @@ import RouterServices from '@features/router/services/router-service'; import useRouterCompany from 'app/features/router/hooks/use-router-company'; import { DocumentIcon } from '@views/client/body/drive/documents/document-icon'; -export default (props: { driveItem: DriveItem & { user?: UserType }}) => { +export default (props: { driveItem: DriveItem & { user?: UserType } }) => { const history = useHistory(); const input = useRecoilValue(SearchInputState); const file = props.driveItem; @@ -31,7 +31,7 @@ export default (props: { driveItem: DriveItem & { user?: UserType }}) => { const { open } = useDrivePreview(); const company = useRouterCompany(); - function openDoc(file: DriveItem){ + function openDoc(file: DriveItem) { open(file); if (file.is_directory) setOpen(false); } @@ -39,7 +39,12 @@ export default (props: { driveItem: DriveItem & { user?: UserType }}) => { return ( <div className="flex items-center p-2 hover:bg-zinc-50 dark:hover:bg-zinc-800 rounded-md cursor-pointer testid:drive-item-result" - onClick={() => {history.push(RouterServices.generateRouteFromState({companyId: company, itemId: file.id})); openDoc(file)}} + onClick={() => { + history.push( + RouterServices.generateRouteFromState({ companyId: company, itemId: file.id }), + ); + openDoc(file); + }} > <FileResultMedia file={file} className="w-16 h-16 mr-3" /> <div className="grow mr-3 overflow-hidden"> @@ -96,14 +101,26 @@ export const FileResultMedia = (props: { if (file.is_directory) { return ( - <div className={'relative flex bg-blue-100 rounded-md ' + (props.className || '') + ' testid:folder-result-media'}> + <div + className={ + 'relative flex bg-blue-100 rounded-md ' + + (props.className || '') + + ' testid:folder-result-media' + } + > <FolderIcon className="w-10 h-10 m-auto text-blue-500 testid:folder-icon" /> </div> ); } return ( - <div className={'relative flex bg-zinc-200 rounded-md ' + (props.className || '') + ' testid:file-result-media'}> + <div + className={ + 'relative flex bg-zinc-200 rounded-md ' + + (props.className || '') + + ' testid:file-result-media' + } + > <Media size={props.size || 'md'} url={url} diff --git a/tdrive/frontend/src/app/views/client/body/drive/browser.tsx b/tdrive/frontend/src/app/views/client/body/drive/browser.tsx index d37c26e37..972df462e 100644 --- a/tdrive/frontend/src/app/views/client/body/drive/browser.tsx +++ b/tdrive/frontend/src/app/views/client/body/drive/browser.tsx @@ -333,10 +333,11 @@ export default memo( ref={uploadZoneRef} driveCollectionKey={uploadZone} onAddFiles={async (_, event) => { + setIsPreparingUpload(true); const tree = await getFilesTree(event); setIsPreparingUpload(false); setCreationModalState({ parent_id: '', open: false }); - uploadTree(tree, { + await uploadTree(tree, { companyId, parentId, }); diff --git a/tdrive/frontend/src/app/views/client/body/drive/documents/document-icon.tsx b/tdrive/frontend/src/app/views/client/body/drive/documents/document-icon.tsx index 094a0ccf0..bd945000f 100644 --- a/tdrive/frontend/src/app/views/client/body/drive/documents/document-icon.tsx +++ b/tdrive/frontend/src/app/views/client/body/drive/documents/document-icon.tsx @@ -9,7 +9,7 @@ import { FileTypeSpreadsheetIcon, FileTypeUnknownIcon, } from '@atoms/icons-colored'; -import { FolderIcon } from '@heroicons/react/solid'; +import { FolderIcon } from 'app/atoms/icons-colored'; import fileUploadApiClient from '@features/files/api/file-upload-api-client'; import type { DriveItem, FileMetadata } from 'app/features/drive/types'; import { ComponentProps } from 'react'; diff --git a/tdrive/frontend/src/app/views/client/body/drive/documents/folder-row.tsx b/tdrive/frontend/src/app/views/client/body/drive/documents/folder-row.tsx index ce25e0b72..7a35d119f 100644 --- a/tdrive/frontend/src/app/views/client/body/drive/documents/folder-row.tsx +++ b/tdrive/frontend/src/app/views/client/body/drive/documents/folder-row.tsx @@ -1,5 +1,5 @@ import { DotsHorizontalIcon } from '@heroicons/react/outline'; -import { FolderIcon } from '@heroicons/react/solid'; +import { FolderIcon } from 'app/atoms/icons-colored'; import { Button } from '@atoms/button/button'; import { Base, BaseSmall } from '@atoms/text'; import Menu from '@components/menus/menu'; diff --git a/tdrive/frontend/src/app/views/client/body/drive/modals/selector/index.tsx b/tdrive/frontend/src/app/views/client/body/drive/modals/selector/index.tsx index d006ae2fa..86aaafb84 100644 --- a/tdrive/frontend/src/app/views/client/body/drive/modals/selector/index.tsx +++ b/tdrive/frontend/src/app/views/client/body/drive/modals/selector/index.tsx @@ -1,4 +1,5 @@ -import { DocumentIcon, FolderIcon } from '@heroicons/react/solid'; +import { DocumentIcon } from '@heroicons/react/solid'; +import { FolderIcon } from 'app/atoms/icons-colored'; import { Button } from '@atoms/button/button'; import { Checkbox } from '@atoms/input/input-checkbox'; import { Modal, ModalContent } from '@atoms/modal'; From 0b86d8959896f17362056a29a9e3783070df3077 Mon Sep 17 00:00:00 2001 From: MontaGhanmy <monta.ghanmy@gmail.com> Date: Mon, 27 Jan 2025 15:45:24 +0100 Subject: [PATCH 09/13] ref: feedback --- .../pending-root-list.tsx | 17 ++++++++++------- .../pending-root-row.tsx | 4 +--- .../files/services/file-upload-service.ts | 18 ++++++++++-------- .../app/views/client/body/drive/browser.tsx | 2 ++ 4 files changed, 23 insertions(+), 18 deletions(-) diff --git a/tdrive/frontend/src/app/components/file-uploads/pending-root-components/pending-root-list.tsx b/tdrive/frontend/src/app/components/file-uploads/pending-root-components/pending-root-list.tsx index 7c0110c94..53cd53c75 100644 --- a/tdrive/frontend/src/app/components/file-uploads/pending-root-components/pending-root-list.tsx +++ b/tdrive/frontend/src/app/components/file-uploads/pending-root-components/pending-root-list.tsx @@ -10,7 +10,8 @@ import { UploadStateEnum } from 'app/features/files/services/file-upload-service const getFilteredRoots = (keys: string[], roots: UploadRootListType) => { const inProgress = keys.filter(key => roots[key].status === 'uploading'); const completed = keys.filter(key => roots[key].status === 'completed'); - return { inProgress, completed }; + const paused = keys.filter(key => roots[key].status === 'paused'); + return { inProgress, completed, paused }; }; interface ModalHeaderProps { @@ -86,16 +87,18 @@ const PendingRootList = ({ const { pauseOrResumeUpload, cancelUpload } = useUpload(); const keys = useMemo(() => Object.keys(roots || {}), [roots]); - const { inProgress: rootsInProgress, completed: rootsCompleted } = useMemo( - () => getFilteredRoots(keys, roots), - [keys, roots], - ); + const { + inProgress: rootsInProgress, + completed: rootsCompleted, + paused: rootsPaused, + } = useMemo(() => getFilteredRoots(keys, roots), [keys, roots]); const isPaused = useCallback(() => status === UploadStateEnum.Paused, [status]); const totalRoots = keys.length; const uploadingCount = rootsInProgress.length; const completedCount = rootsCompleted.length; + const pausedCount = rootsPaused.length; const uploadingPercentage = Math.floor((uploadingCount / totalRoots) * 100) || 100; const toggleModal = useCallback(() => setModalExpanded(prev => !prev), []); @@ -105,7 +108,7 @@ const PendingRootList = ({ {totalRoots > 0 && ( <div className="fixed bottom-4 right-4 w-1/3 shadow-lg rounded-sm overflow-hidden"> <ModalHeader - uploadingCount={uploadingCount} + uploadingCount={uploadingCount + pausedCount} completedCount={completedCount} totalRoots={totalRoots} uploadingPercentage={uploadingPercentage} @@ -130,7 +133,7 @@ const PendingRootList = ({ pauseOrResumeUpload={pauseOrResumeUpload} cancelUpload={cancelUpload} isPaused={isPaused} - uploadingCount={uploadingCount} + uploadingCount={uploadingCount + pausedCount} /> </div> )} diff --git a/tdrive/frontend/src/app/components/file-uploads/pending-root-components/pending-root-row.tsx b/tdrive/frontend/src/app/components/file-uploads/pending-root-components/pending-root-row.tsx index 0c1b55517..6e1e9b7e7 100644 --- a/tdrive/frontend/src/app/components/file-uploads/pending-root-components/pending-root-row.tsx +++ b/tdrive/frontend/src/app/components/file-uploads/pending-root-components/pending-root-row.tsx @@ -39,8 +39,7 @@ const PendingRootRow = ({ // Callback function to open the folder after the upload is completed const handleShowFolder = useCallback(() => { if (!showFolder || isFileRoot) return; - const parentId = firstPendingFile.parentId; - RouterService.push(RouterService.generateRouteFromState({ dirId: parentId || '' })); + RouterService.push(RouterService.generateRouteFromState({ dirId: root.id || '' })); clearRoots(); }, [showFolder, isFileRoot, root, clearRoots]); @@ -72,7 +71,6 @@ const PendingRootRow = ({ useEffect(() => { const postProcess = async () => { if (isUploadCompleted && !restoredFolder) { - console.log("THE UPLOAD FINISHED WILL REFRESH"); await new Promise(resolve => setTimeout(resolve, 1000)); await restore(root.id, parentId); await new Promise(resolve => setTimeout(resolve, 1000)); diff --git a/tdrive/frontend/src/app/features/files/services/file-upload-service.ts b/tdrive/frontend/src/app/features/files/services/file-upload-service.ts index 6aff3170c..e021ca54a 100644 --- a/tdrive/frontend/src/app/features/files/services/file-upload-service.ts +++ b/tdrive/frontend/src/app/features/files/services/file-upload-service.ts @@ -52,7 +52,6 @@ class FileUploadService { while (this.uploadStatus === UploadStateEnum.Paused || (id && this.pausedRoots[id])) { if (this.uploadStatus === UploadStateEnum.Cancelled) return; await new Promise(resolve => setTimeout(resolve, 100)); // Check every 100ms - console.log('waiting while paused:: ', id); } } @@ -102,13 +101,6 @@ class FileUploadService { tree: FileTreeObject, context: { companyId: string; parentId: string }, ) { - // init everything - // this.currentTaskId = ''; - // this.pendingFiles = []; - // this.GroupedPendingFiles = {}; - // this.GroupIds = {}; - // this.pausedRoots = {}; - // this.notify(); const root = tree.tree; this.RootSizes = this.RootSizes = { ...this.RootSizes, @@ -517,6 +509,16 @@ class FileUploadService { this.pauseOrResumeFile(file); } + // update the status of the roots + const roots = Object.keys(this.GroupedPendingFiles); + for (const root of roots) { + if (this.uploadStatus === UploadStateEnum.Paused) { + this.pausedRoots[root] = true; + } else { + this.pausedRoots[root] = false; + } + } + this.notify(); } diff --git a/tdrive/frontend/src/app/views/client/body/drive/browser.tsx b/tdrive/frontend/src/app/views/client/body/drive/browser.tsx index 972df462e..0d6582333 100644 --- a/tdrive/frontend/src/app/views/client/body/drive/browser.tsx +++ b/tdrive/frontend/src/app/views/client/body/drive/browser.tsx @@ -341,6 +341,8 @@ export default memo( companyId, parentId, }); + await new Promise (resolve => setTimeout(resolve, 1000)); + refresh(parentId); }} onDragOver={handleDragOver} onDrop={handleDrop} From c7935ba45d8b8557f1ad40a4ff2a55ee2298f8f2 Mon Sep 17 00:00:00 2001 From: MontaGhanmy <monta.ghanmy@gmail.com> Date: Wed, 29 Jan 2025 12:22:15 +0100 Subject: [PATCH 10/13] ref: hover icons/ navigate to file/ refactoring file upload service --- tdrive/frontend/public/locales/en.json | 2 +- .../pending-root-list.tsx | 18 +- .../pending-root-row.tsx | 86 ++++++-- .../features/drive/hooks/use-drive-upload.tsx | 6 +- .../app/features/files/hooks/use-upload.ts | 2 +- .../files/services/file-upload-service.ts | 189 +++++++++++------- .../body/drive/modals/create/create-link.tsx | 9 +- 7 files changed, 213 insertions(+), 99 deletions(-) diff --git a/tdrive/frontend/public/locales/en.json b/tdrive/frontend/public/locales/en.json index 4790beb19..8c55bb274 100644 --- a/tdrive/frontend/public/locales/en.json +++ b/tdrive/frontend/public/locales/en.json @@ -187,7 +187,7 @@ "general.update": "Update", "general.uploading": "Uploading", "general.uploaded": "Uploaded", - "general.files": "files", + "general.files": "file(s)", "general.user.anonymous": "Anonymous", "general.user.deactivated": "User is no longer in this company", "general.user.deleted": "Deleted Account", diff --git a/tdrive/frontend/src/app/components/file-uploads/pending-root-components/pending-root-list.tsx b/tdrive/frontend/src/app/components/file-uploads/pending-root-components/pending-root-list.tsx index 53cd53c75..2b05d71f5 100644 --- a/tdrive/frontend/src/app/components/file-uploads/pending-root-components/pending-root-list.tsx +++ b/tdrive/frontend/src/app/components/file-uploads/pending-root-components/pending-root-list.tsx @@ -32,13 +32,16 @@ const ModalHeader: React.FC<ModalHeaderProps> = ({ modalExpanded, }) => ( <div className="w-full flex bg-[#45454A] text-white p-4 items-center justify-between"> - <p> + <p className="testid:upload-modal-head-status"> {uploadingCount > 0 - ? `${Languages.t('general.uploading')} ${uploadingCount}/${totalRoots}` + ? `${Languages.t('general.uploading')} ${uploadingCount}` : `${Languages.t('general.uploaded')} ${completedCount}`}{' '} {Languages.t('general.files')} </p> - <button className="ml-auto flex items-center" onClick={toggleModal}> + <button + className="ml-auto flex items-center testid:upload-modal-toggle-arrow" + onClick={toggleModal} + > {modalExpanded ? <ArrowDownIcon /> : <ArrowUpIcon />} </button> </div> @@ -61,13 +64,16 @@ const ModalFooter: React.FC<ModalFooterProps> = ({ <div className="flex space-x-4 ml-auto"> {uploadingCount > 0 && ( <button - className="text-blue-500 px-4 py-2 rounded hover:bg-blue-600" + className="text-blue-500 px-4 py-2 rounded hover:bg-blue-600 hover:text-white testid:upload-modal-pause-resume" onClick={pauseOrResumeUpload} > {isPaused() ? Languages.t('general.resume') : Languages.t('general.pause')} </button> )} - <button className="text-blue-500 px-4 py-2 rounded hover:bg-blue-600" onClick={cancelUpload}> + <button + className="text-blue-500 px-4 py-2 rounded hover:bg-blue-600 hover:text-white testid:upload-modal-cancel-close" + onClick={cancelUpload} + > {uploadingCount ? Languages.t('general.cancel') : Languages.t('general.close')} </button> </div> @@ -106,7 +112,7 @@ const PendingRootList = ({ return ( <> {totalRoots > 0 && ( - <div className="fixed bottom-4 right-4 w-1/3 shadow-lg rounded-sm overflow-hidden"> + <div className="fixed bottom-4 right-4 w-1/3 shadow-lg rounded-sm overflow-hidden testid:upload-modal"> <ModalHeader uploadingCount={uploadingCount + pausedCount} completedCount={completedCount} diff --git a/tdrive/frontend/src/app/components/file-uploads/pending-root-components/pending-root-row.tsx b/tdrive/frontend/src/app/components/file-uploads/pending-root-components/pending-root-row.tsx index 6e1e9b7e7..ee386fbc0 100644 --- a/tdrive/frontend/src/app/components/file-uploads/pending-root-components/pending-root-row.tsx +++ b/tdrive/frontend/src/app/components/file-uploads/pending-root-components/pending-root-row.tsx @@ -38,10 +38,15 @@ const PendingRootRow = ({ // Callback function to open the folder after the upload is completed const handleShowFolder = useCallback(() => { - if (!showFolder || isFileRoot) return; - RouterService.push(RouterService.generateRouteFromState({ dirId: root.id || '' })); - clearRoots(); - }, [showFolder, isFileRoot, root, clearRoots]); + if (!showFolder || isFileRoot) { + const redirectionURL = RouterService.generateRouteFromState({ + itemId: root.id, + }); + window.open(redirectionURL, '_blank'); + } else { + RouterService.push(RouterService.generateRouteFromState({ dirId: root.id || '' })); + } + }, [showFolder, root, isFileRoot, clearRoots]); // Function to determine the icon for the root // If the root is a file, it will show the file icon based on the content type @@ -83,45 +88,87 @@ const PendingRootRow = ({ } }, [isUploadCompleted]); + // Helper to convert size to the closest unit + const formatFileSize = (sizeInBytes: number): string => { + if (sizeInBytes) { + if (sizeInBytes < 1024) return `${sizeInBytes} Bytes`; + if (sizeInBytes < 1024 ** 2) return `${(sizeInBytes / 1024).toFixed(2)} KB`; + if (sizeInBytes < 1024 ** 3) return `${(sizeInBytes / 1024 ** 2).toFixed(2)} MB`; + return `${(sizeInBytes / 1024 ** 3).toFixed(2)} GB`; + } else { + return '0 Bytes'; + } + }; + + // Helper to truncate the root name / key if it is too long + const truncateRootName = (rootName: string): string => { + if (rootName.length > 30) { + return `${rootName.substring(0, 20)}...`; + } + return rootName; + }; + return ( <div className="root-row"> <div className="root-details mt-2"> <div className="flex items-center"> <div className="w-10 h-10 flex items-center justify-center bg-[#f3f3f7] rounded-md"> - <div className="w-full h-full flex items-center justify-center"> + <div className="w-full h-full flex items-center justify-center testid:upload-modal-row-type"> {itemTypeIcon(firstPendingFile?.type)} </div> </div> - <p className="ml-4">{rootKey}</p> + <p className="ml-4"> + <span className="font-bold">{truncateRootName(rootKey)} </span> + {root.uploadedSize > 0 && ( + <span className="ml-4 text-sm"> + ({formatFileSize(root.uploadedSize)} / {formatFileSize(root.size)}) + </span> + )} + </p> <div className="progress-check flex items-center justify-center ml-auto"> {isUploadCompleted ? ( - <button onClick={handleShowFolder}> + <button + onClick={handleShowFolder} + className="hover:bg-gray-100 p-2 rounded-md transition-all duration-200 testid:upload-modal-row-show-folder" + > {!isFileRoot && ( <> <CheckGreenIcon className={`transition-opacity ${ - showFolder ? 'opacity-0 w-0 h-0' : 'opacity-1' + showFolder ? 'opacity-0 w-0 h-0' : 'opacity-1 hover:scale-110' }`} /> <ShowFolderIcon className={`transition-opacity duration-300 ${ - showFolder ? 'opacity-1' : 'opacity-0 w-0 h-0' + showFolder ? 'opacity-1 hover:scale-110' : 'opacity-0 w-0 h-0' }`} /> </> )} - {isFileRoot && <CheckGreenIcon className="opacity-1" />} + {isFileRoot && ( + <CheckGreenIcon className="opacity-1 hover:scale-110 transition-transform duration-200" /> + )} </button> ) : ( - firstPendingFile?.status !== 'cancel' && + root.status !== 'cancelled' && firstPendingFile?.status !== 'error' && ( <> - <button onClick={() => pauseOrResumeRootUpload(rootKey)}> - {root.status === 'paused' ? <ResumeIcon /> : <PauseIcon />} + <button + onClick={() => pauseOrResumeRootUpload(rootKey)} + className="hover:bg-blue-100 p-2 rounded-md transition-all duration-200 testid:upload-modal-row-pause-resume" + > + {root.status === 'paused' ? ( + <ResumeIcon className="hover:scale-110 transition-transform duration-200" /> + ) : ( + <PauseIcon className="hover:scale-110 transition-transform duration-200" /> + )} </button> - <button className="ml-2" onClick={() => cancelRootUpload(rootKey)}> - <CancelIcon /> + <button + className="ml-2 hover:bg-red-100 p-2 rounded-md transition-all duration-200 testid:upload-modal-row-cancel" + onClick={() => cancelRootUpload(rootKey)} + > + <CancelIcon className="hover:scale-110 transition-transform duration-200" /> </button> </> ) @@ -133,7 +180,14 @@ const PendingRootRow = ({ <div className="root-progress h-[3px] mt-4"> {!showFolder && ( <div className="w-full h-[3px] bg-[#F0F2F3]"> - <div className="h-full bg-[#00A029]" style={{ width: `${uploadProgress}%` }}></div> + <div + className={`testid:upload-modal-row-progress h-full ${ + root.status === 'cancelled' ? 'bg-[#FF0000]' : 'bg-[#00A029]' + }`} + style={{ + width: `${root.status === 'cancelled' ? 100 : uploadProgress}%`, + }} + ></div> </div> )} </div> diff --git a/tdrive/frontend/src/app/features/drive/hooks/use-drive-upload.tsx b/tdrive/frontend/src/app/features/drive/hooks/use-drive-upload.tsx index 7156ec4f2..6f2e300e2 100644 --- a/tdrive/frontend/src/app/features/drive/hooks/use-drive-upload.tsx +++ b/tdrive/frontend/src/app/features/drive/hooks/use-drive-upload.tsx @@ -21,7 +21,8 @@ export const useDriveUpload = () => { companyId: context.companyId, id: context.id, }, - callback: async (file, context) => { + callback: async (filePayload, context) => { + const file = filePayload.file; if (file) { const version = { drive_item_id: context.id, @@ -72,7 +73,8 @@ export const useDriveUpload = () => { companyId: context.companyId, parentId: context.parentId, }, - callback: (file, context) => { + callback: (filePayload, context) => { + const file = filePayload.file; if (file) { create( { diff --git a/tdrive/frontend/src/app/features/files/hooks/use-upload.ts b/tdrive/frontend/src/app/features/files/hooks/use-upload.ts index 30776c12b..3102c5a39 100644 --- a/tdrive/frontend/src/app/features/files/hooks/use-upload.ts +++ b/tdrive/frontend/src/app/features/files/hooks/use-upload.ts @@ -20,7 +20,7 @@ export const useUpload = () => { const cancelUpload = () => FileUploadService.cancelUpload(); - const cancelRootUpload = (id: string) => FileUploadService.cancelRoot(id); + const cancelRootUpload = (id: string) => FileUploadService.cancelRootUpload(id); const getOnePendingFile = (id: string) => FileUploadService.getPendingFile(id); diff --git a/tdrive/frontend/src/app/features/files/services/file-upload-service.ts b/tdrive/frontend/src/app/features/files/services/file-upload-service.ts index e021ca54a..ed75bc89d 100644 --- a/tdrive/frontend/src/app/features/files/services/file-upload-service.ts +++ b/tdrive/frontend/src/app/features/files/services/file-upload-service.ts @@ -27,13 +27,23 @@ export enum UploadStateEnum { Cancelled = 'cancelled', } +type RootState = { [key: string]: boolean }; + const logger = Logger.getLogger('Services/FileUploadService'); class FileUploadService { private pendingFiles: PendingFileType[] = []; - private GroupedPendingFiles: { [key: string]: PendingFileType[] } = {}; - private RootSizes: { [key: string]: number } = {}; - private GroupIds: { [key: string]: string } = {}; - private pausedRoots: { [key: string]: boolean } = {}; + private groupedPendingFiles: { [key: string]: PendingFileType[] } = {}; + private rootSizes: { [key: string]: number } = {}; + private groupIds: { [key: string]: string } = {}; + private rootStates: { + paused: RootState; + cancelled: RootState; + completed: RootState; + } = { + paused: {}, + cancelled: {}, + completed: {}, + }; public currentTaskId = ''; public parentId = ''; public uploadStatus = UploadStateEnum.Progress; @@ -49,8 +59,9 @@ class FileUploadService { * @private */ async _waitWhilePaused(id?: string) { - while (this.uploadStatus === UploadStateEnum.Paused || (id && this.pausedRoots[id])) { - if (this.uploadStatus === UploadStateEnum.Cancelled) return; + while (this.uploadStatus === UploadStateEnum.Paused || (id && this.rootStates.paused[id])) { + if (this.uploadStatus === UploadStateEnum.Cancelled || (id && this.rootStates.cancelled[id])) + return; await new Promise(resolve => setTimeout(resolve, 100)); // Check every 100ms } } @@ -59,17 +70,17 @@ class FileUploadService { * Helper method to cancel execution when `isCancelled` is true. * @private */ - private async checkCancellation() { - if (this.uploadStatus === UploadStateEnum.Cancelled) { + private async checkCancellation(id?: string) { + if (this.uploadStatus === UploadStateEnum.Cancelled || (id && this.rootStates.cancelled[id])) { logger.warn('Operation cancelled.'); throw new Error('Upload process cancelled.'); } } notify() { - const updatedState = Object.keys(this.GroupedPendingFiles).reduce((acc: any, key: string) => { + const updatedState = Object.keys(this.groupedPendingFiles).reduce((acc: any, key: string) => { // uploaded size - const uploadedSize = this.GroupedPendingFiles[key] + const uploadedSize = this.groupedPendingFiles[key] .map((f: PendingFileType) => { // if the file is successful and originalFile exists, add the size to the accumulator if (f.status === 'success' && f.originalFile?.size) { @@ -78,17 +89,22 @@ class FileUploadService { return 0; }) .reduce((acc: number, size: number) => acc + size, 0); - // status can be "uploading", "completed", "paused" based on the size, uploadedSize and pausedRoots - const status = this.pausedRoots[key] + // Determine the upload status based on paused, completed, or uploading states + const status = this.rootStates.cancelled[key] + ? 'cancelled' + : this.rootStates.paused[key] ? 'paused' - : uploadedSize === this.RootSizes[key] + : uploadedSize === this.rootSizes[key] ? 'completed' : 'uploading'; + if (status === 'completed') { + this.rootStates.completed[key] = true; + } // Add to the accumulator object acc[key] = { - id: this.GroupIds[key], + id: this.groupIds[key], items: [], - size: this.RootSizes[key], + size: this.rootSizes[key], uploadedSize, status, }; @@ -101,9 +117,12 @@ class FileUploadService { tree: FileTreeObject, context: { companyId: string; parentId: string }, ) { + // reset the upload status + this.uploadStatus = UploadStateEnum.Progress; + const root = tree.tree; - this.RootSizes = this.RootSizes = { - ...this.RootSizes, + this.rootSizes = this.rootSizes = { + ...this.rootSizes, ...(tree.sizePerRoot || {}), }; // Create all directories @@ -125,7 +144,7 @@ class FileUploadService { // start descending the tree for (const directory of Object.keys(tree)) { const root = tree[directory].root as string; - await this.checkCancellation(); + await this.checkCancellation(root); await this._waitWhilePaused(root); if (tree[directory].file instanceof File) { const file = tree[directory].file as File; @@ -169,7 +188,7 @@ class FileUploadService { item: item, version: {}, }); - this.GroupIds[directory] = driveItem.id; + this.groupIds[directory] = driveItem.id; this.logger.debug(`Directory ${directory} created`); pendingFile.status = 'success'; this.notify(); @@ -181,7 +200,6 @@ class FileUploadService { } } catch (e) { this.logger.error(e); - throw new Error('Could not create directory'); } } } @@ -194,7 +212,10 @@ class FileUploadService { companyId: context.companyId, parentId: parentId, }, - callback: async (file, context) => { + callback: async (filePayload, context) => { + const isFileRoot = filePayload.root.includes('.'); + const root = filePayload.root; + const file = filePayload.file; if (file) { const item = { company_id: context.companyId, @@ -217,7 +238,9 @@ class FileUploadService { } as Partial<DriveItemVersion>; // create the document - await DriveApiClient.create(context.companyId, { item, version }); + const documentId = await DriveApiClient.create(context.companyId, { item, version }); + // assign the group id with the document id + if (isFileRoot) this.groupIds[root] = documentId.id; } }, }); @@ -237,13 +260,7 @@ class FileUploadService { try { await Promise.all(treePromises); } catch (error) { - if (this.uploadStatus !== UploadStateEnum.Cancelled) { - console.error('An error occurred while processing treePromises:', error); - // Optionally, handle the error or rethrow it - throw error; // Re-throw the error if necessary - } else { - console.warn('Operation was cancelled. Error ignored.'); - } + logger.error('Error while processing tree', error); } // await traverserTreeLevel(root, context.parentId, true); @@ -255,7 +272,7 @@ class FileUploadService { fileList: { root: string; file: File }[], options?: { context?: any; - callback?: (file: FileType | null, context: any) => void; + callback?: (file: { root: string; file: FileType | null }, context: any) => void; }, ): Promise<PendingFileType[]> { const { companyId } = RouterServices.getStateFromRoute(); @@ -271,6 +288,10 @@ class FileUploadService { } for (const file of fileList) { + // cancel upload + await this.checkCancellation(file.root); + // wait here if the upload is paused + await this._waitWhilePaused(file.root); if (!file.file) continue; const pendingFile: PendingFileType = { @@ -289,10 +310,10 @@ class FileUploadService { }; this.pendingFiles.push(pendingFile); - if (!this.GroupedPendingFiles[file.root]) { - this.GroupedPendingFiles[file.root] = []; + if (!this.groupedPendingFiles[file.root]) { + this.groupedPendingFiles[file.root] = []; } - this.GroupedPendingFiles[file.root].push(pendingFile); + this.groupedPendingFiles[file.root].push(pendingFile); this.notify(); // First we create the file object @@ -346,7 +367,10 @@ class FileUploadService { try { pendingFile.backendFile = JSON.parse(message).resource; pendingFile.status = 'success'; - options?.callback?.(pendingFile.backendFile, options?.context || {}); + options?.callback?.( + { root: file.root, file: pendingFile.backendFile }, + options?.context || {}, + ); this.notify(); } catch (e) { logger.error(`Error on fileSuccess Event`, e); @@ -366,7 +390,7 @@ class FileUploadService { 'Error uploading file ' + intendedFilename, ), ); - options?.callback?.(null, options?.context || {}); + options?.callback?.({ root: file.root, file: null }, options?.context || {}); this.notify(); }); } @@ -392,29 +416,8 @@ class FileUploadService { return this.pendingFiles.filter(f => f.backendFile?.id && f.backendFile.id === id)[0]; } - public cancelRoot(id: string, timeout = 1000) { - const filesToCancel = this.GroupedPendingFiles[id]; - - for (const file of filesToCancel) { - file.status = 'cancel'; - if (file.resumable) { - file.resumable.cancel(); - if (file.backendFile) - this.deleteOneFile({ - companyId: file.backendFile.company_id, - fileId: file.backendFile.id, - }); - } - } - - setTimeout(() => { - this.pendingFiles = this.pendingFiles.filter(f => f.id !== id); - this.notify(); - }, timeout); - } - public cancelUpload() { - this.uploadStatus === UploadStateEnum.Cancelled; + this.uploadStatus = UploadStateEnum.Cancelled; // pause or resume the resumable tasks const fileToCancel = this.pendingFiles; @@ -445,13 +448,59 @@ class FileUploadService { // clean everything this.pendingFiles = []; - this.GroupedPendingFiles = {}; - this.RootSizes = {}; - this.GroupIds = {}; + this.groupedPendingFiles = {}; + this.rootSizes = {}; + this.groupIds = {}; this.notify(); } + public cancelRootUpload(id: string) { + this.rootStates.cancelled[id] = true; + // if it's 1 root, cancel the upload + if (Object.keys(this.groupedPendingFiles).length === 1) { + this.cancelUpload(); + return; + } else { + // pause or resume the resumable tasks + const filesToProcess = this.groupedPendingFiles[id]; + + if (!filesToProcess || filesToProcess.length === 0) { + console.error(`No files found for id: ${id}`); + return; + } + + for (const file of filesToProcess) { + if (file.status === 'success') continue; + + try { + if (file.resumable) { + file.resumable.cancel(); + if (file.backendFile) + this.deleteOneFile({ + companyId: file.backendFile.company_id, + fileId: file.backendFile.id, + }); + } else { + console.warn('Resumable object is not available for file', file); + } + } catch (error) { + console.error('Error while pausing or resuming file', file, error); + } + } + + // clean everything + this.pendingFiles = this.pendingFiles.filter(f => f.uploadTaskId !== id); + // remove the root key + this.groupedPendingFiles[id] = []; + // remove the root size + delete this.rootSizes[id]; + // remove the root id + delete this.groupIds[id]; + this.notify(); + } + } + public retry(id: string) { const fileToRetry = this.pendingFiles.filter(f => f.id === id)[0]; @@ -510,12 +559,14 @@ class FileUploadService { } // update the status of the roots - const roots = Object.keys(this.GroupedPendingFiles); + const roots = Object.keys(this.groupedPendingFiles); for (const root of roots) { + // check if the root has completed skip it + if (this.rootStates.completed[root]) continue; if (this.uploadStatus === UploadStateEnum.Paused) { - this.pausedRoots[root] = true; + this.rootStates.paused[root] = true; } else { - this.pausedRoots[root] = false; + this.rootStates.paused[root] = false; } } @@ -524,14 +575,14 @@ class FileUploadService { public pauseOrResumeRoot(id: string) { // set the pause status for the root - if (Object.keys(this.pausedRoots).includes(id)) { - this.pausedRoots[id] = !this.pausedRoots[id]; + if (Object.keys(this.rootStates.paused).includes(id)) { + this.rootStates.paused[id] = !this.rootStates.paused[id]; } else { - this.pausedRoots[id] = true; + this.rootStates.paused[id] = true; } // pause or resume the resumable tasks - const filesToProcess = this.GroupedPendingFiles[id]; + const filesToProcess = this.groupedPendingFiles[id]; if (!filesToProcess || filesToProcess.length === 0) { console.error(`No files found for id: ${id}`); @@ -608,8 +659,8 @@ class FileUploadService { } public clearRoots() { - this.GroupedPendingFiles = {}; - this.GroupIds = {}; + this.groupedPendingFiles = {}; + this.groupIds = {}; this.notify(); } } diff --git a/tdrive/frontend/src/app/views/client/body/drive/modals/create/create-link.tsx b/tdrive/frontend/src/app/views/client/body/drive/modals/create/create-link.tsx index f1396a4ae..f2168527d 100644 --- a/tdrive/frontend/src/app/views/client/body/drive/modals/create/create-link.tsx +++ b/tdrive/frontend/src/app/views/client/body/drive/modals/create/create-link.tsx @@ -5,7 +5,7 @@ import { useState } from 'react'; import { useRecoilState } from 'recoil'; import { CreateModalAtom } from '.'; import FileUploadService from 'features/files/services/file-upload-service'; -import Languages from "features/global/services/languages-service"; +import Languages from 'features/global/services/languages-service'; export const CreateLink = () => { const [name, setName] = useState<string>(''); @@ -25,7 +25,8 @@ export const CreateLink = () => { context: { parentId: state.parent_id, }, - callback: (file, context) => { + callback: (filePayload, context) => { + const file = filePayload?.file; if (file) create( { name, parent_id: context.parentId, size: file.upload_data?.size }, @@ -50,7 +51,7 @@ export const CreateLink = () => { <> <Input disabled={loading} - placeholder={ Languages.t('components.create_link_modal.hint')} + placeholder={Languages.t('components.create_link_modal.hint')} className="w-full mt-4" onChange={e => setName(e.target.value)} testClassId="create-link-name-input" @@ -74,7 +75,7 @@ export const CreateLink = () => { }} testClassId="create-link-button" > - { Languages.t('components.create_link_modal.button')} + {Languages.t('components.create_link_modal.button')} </Button> </> ); From d73b7a9ca2c53fe285d8a12aa77863295413d090 Mon Sep 17 00:00:00 2001 From: MontaGhanmy <monta.ghanmy@gmail.com> Date: Wed, 29 Jan 2025 18:07:30 +0100 Subject: [PATCH 11/13] feat: pause/resume when 1 only root remain in upload --- .../features/files/services/file-upload-service.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tdrive/frontend/src/app/features/files/services/file-upload-service.ts b/tdrive/frontend/src/app/features/files/services/file-upload-service.ts index ed75bc89d..b090a858e 100644 --- a/tdrive/frontend/src/app/features/files/services/file-upload-service.ts +++ b/tdrive/frontend/src/app/features/files/services/file-upload-service.ts @@ -574,6 +574,18 @@ class FileUploadService { } public pauseOrResumeRoot(id: string) { + const completedRoots = Object.keys(this.rootStates.completed); + const roots = Object.keys(this.rootSizes); + const isOnlyRootInProgress = roots.length - completedRoots.length === 1; + + // Check if this is the only root in progress + if (!this.rootStates.completed[id] && isOnlyRootInProgress) { + this.uploadStatus = + this.uploadStatus === UploadStateEnum.Progress + ? UploadStateEnum.Paused + : UploadStateEnum.Progress; + } + // set the pause status for the root if (Object.keys(this.rootStates.paused).includes(id)) { this.rootStates.paused[id] = !this.rootStates.paused[id]; From dfddbe86f4c623b27efb1fe2f52c2ddec4b6a902 Mon Sep 17 00:00:00 2001 From: MontaGhanmy <monta.ghanmy@gmail.com> Date: Wed, 29 Jan 2025 18:10:36 +0100 Subject: [PATCH 12/13] ref: removed old upload components --- .../pending-file-row.tsx | 224 ------------------ .../pending-files-list.tsx | 99 -------- .../pending-root-list.tsx | 52 ---- .../pending-file-components/styles.scss | 41 ---- 4 files changed, 416 deletions(-) delete mode 100644 tdrive/frontend/src/app/components/file-uploads/pending-file-components/pending-file-row.tsx delete mode 100644 tdrive/frontend/src/app/components/file-uploads/pending-file-components/pending-files-list.tsx delete mode 100644 tdrive/frontend/src/app/components/file-uploads/pending-file-components/pending-root-list.tsx delete mode 100644 tdrive/frontend/src/app/components/file-uploads/pending-file-components/styles.scss diff --git a/tdrive/frontend/src/app/components/file-uploads/pending-file-components/pending-file-row.tsx b/tdrive/frontend/src/app/components/file-uploads/pending-file-components/pending-file-row.tsx deleted file mode 100644 index f204ce24f..000000000 --- a/tdrive/frontend/src/app/components/file-uploads/pending-file-components/pending-file-row.tsx +++ /dev/null @@ -1,224 +0,0 @@ -import React from 'react'; -import { PauseCircle, PlayCircle, Trash2 } from 'react-feather'; -import { Row, Col, Typography, Divider, Progress, Button, Tooltip } from 'antd'; -import { capitalize } from 'lodash'; - -import { - isPendingFileStatusCancel, - isPendingFileStatusError, - isPendingFileStatusPause, - isPendingFileStatusPending, - isPendingFileStatusSuccess, -} from '../../../features/files/utils/pending-files'; -import Languages from '@features/global/services/languages-service'; -import { useUpload } from '@features/files/hooks/use-upload'; -import { PendingFileRecoilType, PendingFileType } from '@features/files/types/file'; - -type PropsType = { - pendingFileState: PendingFileRecoilType; - pendingFile: PendingFileType; -}; - -const { Text } = Typography; -export default ({ pendingFileState, pendingFile }: PropsType) => { - const { pauseOrResumeUpload, cancelUpload } = useUpload(); - - const getProgressStrokeColor = (status: PendingFileRecoilType['status']) => { - if (isPendingFileStatusCancel(status)) return 'var(--error)'; - if (isPendingFileStatusError(status)) return 'var(--error)'; - if (isPendingFileStatusPause(status)) return 'var(--warning)'; - if (isPendingFileStatusPending(status)) return 'var(--progress-bar-color)'; - - return 'var(--success)'; - }; - - const setStatus = () => { - switch (pendingFileState.status) { - case 'error': - case 'pause': - case 'cancel': - return 'exception'; - case 'pending': - return 'active'; - case 'success': - return 'success'; - default: - return 'normal'; - } - }; - - return ( - <div - style={{ - backgroundColor: - isPendingFileStatusCancel(pendingFileState.status) || - isPendingFileStatusError(pendingFileState.status) - ? 'var(--error-background)' - : undefined, - }} - > - <Row - className="testid:pending-files-row" - justify="space-between" - align="middle" - wrap={false} - style={{ - height: 39, - width: '100%', - }} - > - <Col className="small-left-margin" flex="auto" style={{ lineHeight: '16px' }}> - {pendingFile?.originalFile?.name ? ( - <Row justify="start" align="middle" wrap={false}> - <Text - ellipsis - style={{ - maxWidth: isPendingFileStatusPause(pendingFile.status) ? 130 : 160, - verticalAlign: 'middle', - }} - className="testid:file-name" - > - {capitalize(pendingFile?.originalFile.name)} - </Text> - {isPendingFileStatusPause(pendingFile.status) && ( - <Text - type="secondary" - className="ant-typography-single-line" - style={{ verticalAlign: 'middle', marginLeft: 4 }} - > - ({Languages.t('general.paused')}) - </Text> - )} - </Row> - ) : ( - <div - style={{ - marginTop: 8, - height: 8, - maxWidth: 160, - borderRadius: 8, - backgroundColor: 'var(--grey-background)', - }} - /> - )} - {pendingFile?.label ? ( - <Row justify="start" align="middle" wrap={false}> - <Text - className="testid:file-label" - ellipsis - style={{ - maxWidth: isPendingFileStatusPause(pendingFile.status) ? 130 : 160, - verticalAlign: 'middle', - }} - > - {pendingFile?.label} - </Text> - </Row> - ) : ( - <div - style={{ - marginTop: 8, - height: 8, - maxWidth: 160, - borderRadius: 8, - backgroundColor: 'var(--grey-background)', - }} - /> - )} - </Col> - - <Col - style={{ - display: 'flex', - alignItems: 'center', - justifyContent: 'center', - lineHeight: '16px', - }} - > - {pendingFileState.id ? ( - !isPendingFileStatusSuccess(pendingFileState.status) && - !isPendingFileStatusError(pendingFileState.status) ? ( - <Tooltip - placement="top" - title={ - isPendingFileStatusPause(pendingFileState.status) - ? Languages.t('general.resume') - : Languages.t('general.pause') - } - className="pending-file-row-tooltip-file-status" - > - <Button - type="link" - shape="circle" - disabled={isPendingFileStatusError(pendingFileState.status)} - icon={ - isPendingFileStatusPause(pendingFileState.status) ? ( - <PlayCircle size={16} color="var(--black)" /> - ) : ( - <PauseCircle size={16} color="var(--black)" /> - ) - } - style={{ - display: 'flex', - alignItems: 'center', - justifyContent: 'center', - }} - className="testid:button-toggle-tooltip-status" - /> - </Tooltip> - ) : ( - <div style={{ width: 32 }} /> - ) - ) : ( - <div - style={{ - marginTop: 8, - height: 8, - maxWidth: 32, - borderRadius: 8, - backgroundColor: 'var(--grey-background)', - }} - /> - )} - </Col> - - <Col - style={{ - display: 'flex', - alignItems: 'center', - justifyContent: 'center', - lineHeight: '16px', - }} - > - {!isPendingFileStatusSuccess(pendingFileState.status) && - !isPendingFileStatusError(pendingFileState.status) ? ( - <Tooltip title={Languages.t('general.cancel')} placement="top" className="testid:pending-file-row-tooltip-cancel"> - <Button - type="link" - shape="circle" - icon={<Trash2 size={16} color={'var(--black)'} />} - // onClick={() => cancelUpload(pendingFileState.id)} - style={{ display: 'flex', alignItems: 'center', justifyContent: 'center' }} - className="testid:button-toggle-tooltip-cancel" - /> - </Tooltip> - ) : ( - <div style={{ width: 32 }} /> - )} - </Col> - </Row> - <div className="file-progress-bar-container testid:progress-bar"> - <Progress - type="line" - className="file-progress-bar" - percent={pendingFileState.progress * 100} - showInfo={false} - trailColor="var(--white)" - status={setStatus()} - strokeColor={getProgressStrokeColor(pendingFileState.status)} - /> - </div> - <Divider style={{ margin: 0 }} /> - </div> - ); -}; diff --git a/tdrive/frontend/src/app/components/file-uploads/pending-file-components/pending-files-list.tsx b/tdrive/frontend/src/app/components/file-uploads/pending-file-components/pending-files-list.tsx deleted file mode 100644 index 3d3d3dd84..000000000 --- a/tdrive/frontend/src/app/components/file-uploads/pending-file-components/pending-files-list.tsx +++ /dev/null @@ -1,99 +0,0 @@ -import React, { useCallback, useState } from 'react'; -import classNames from 'classnames'; -import { Minus, Plus } from 'react-feather'; -import { Layout, Row, Col, Typography } from 'antd'; -import PerfectScrollbar from 'react-perfect-scrollbar'; -import moment from 'moment'; - -import PendingFileRow from './pending-file-row'; -import Languages from '@features/global/services/languages-service'; -import { PendingFileRecoilType } from '@features/files/types/file'; -import { useUpload } from '@features/files/hooks/use-upload'; - -import './styles.scss'; - -type PropsType = { - pendingFilesState: PendingFileRecoilType[]; - visible: boolean; -}; - -const { Text } = Typography; -const { Header, Content } = Layout; -export default ({ pendingFilesState, visible }: PropsType) => { - const { getOnePendingFile, currentTask } = useUpload(); - const [hiddenPendingFiles, setHiddenPendingFiles] = useState<boolean>(false); - - const handleTimeChange = useCallback(() => { - const pendingFiles = pendingFilesState.map(state => getOnePendingFile(state.id)); - const uploadingFiles = pendingFiles.filter(f => f?.resumable && f.resumable.isUploading()); - - const remainingSizeTotal = uploadingFiles - .map(f => (1 - f.progress) * (f?.originalFile?.size || 0)) - .reduce((accumulator: number, nextValue: number) => accumulator + nextValue, 0); - - const speed = - uploadingFiles - .map(f => f.speed) - .reduce((accumulator: number, nextValue: number) => accumulator + nextValue, 0) / - uploadingFiles.map(f => f.speed).length; - - const timeRemainingInMs = remainingSizeTotal / speed; - - const momentTimeRemaining = moment(new Date().getTime() + timeRemainingInMs).fromNow(); - - if (momentTimeRemaining !== 'Invalid date') { - return Languages.t('components.pending_file_list.estimation.end') + ` ${momentTimeRemaining}...`; - } else { - return Languages.t('components.pending_file_list.estimation.approximations'); - } - }, [getOnePendingFile, pendingFilesState]); - - return pendingFilesState.length > 0 ? ( - <Layout className={'pending-files-list-layout ' + (visible ? 'visible' : '') + ' testid:pending-file-list'}> - <Header - className={classNames('pending-files-list-header')} - onClick={() => setHiddenPendingFiles(!hiddenPendingFiles)} - > - <Row justify="space-between" align="middle"> - <Col> - <Text style={{ color: 'var(--white)' }}> - {currentTask.total > 0 && `${currentTask.uploaded}/${currentTask.total} `} - {Languages.t('components.drive_dropzone.uploading')} - </Text> - </Col> - <Col style={{ display: 'flex', alignItems: 'center' }}> - {hiddenPendingFiles ? <Plus size={18} /> : <Minus size={18} />} - </Col> - </Row> - </Header> - {!hiddenPendingFiles && ( - <Content className="pending-files-list-content"> - <PerfectScrollbar - options={{ suppressScrollX: true, suppressScrollY: false }} - component="div" - style={{ width: '100%', height: 114 }} - > - <Row justify="start" align="middle" style={{ background: '#DFE7FE' }}> - <Col className="small-left-margin"> - <Text style={{ color: '#6C6C6D' }}>{handleTimeChange()}</Text> - </Col> - </Row> - - <> - {pendingFilesState.length > 0 && - pendingFilesState.map((pendingFileState, index) => ( - <PendingFileRow - key={`${pendingFileState.file?.id}-${index}`} - pendingFileState={pendingFileState} - pendingFile={getOnePendingFile(pendingFileState.id)} - /> - ))} - </> - </PerfectScrollbar> - </Content> - )} - </Layout> - ) : ( - <></> - ); -}; diff --git a/tdrive/frontend/src/app/components/file-uploads/pending-file-components/pending-root-list.tsx b/tdrive/frontend/src/app/components/file-uploads/pending-file-components/pending-root-list.tsx deleted file mode 100644 index 8825e3b4d..000000000 --- a/tdrive/frontend/src/app/components/file-uploads/pending-file-components/pending-root-list.tsx +++ /dev/null @@ -1,52 +0,0 @@ -import classNames from 'classnames'; -import { Layout, Row, Col, Typography } from 'antd'; -import PerfectScrollbar from 'react-perfect-scrollbar'; - -import Languages from '@features/global/services/languages-service'; -import { PendingFileRecoilType } from '@features/files/types/file'; -import { useUpload } from '@features/files/hooks/use-upload'; - -import './styles.scss'; - -type PropsType = { - rootPendingFilesState: { [key: string]: PendingFileRecoilType[] }; - visible: boolean; -}; - -const { Text } = Typography; -const { Header, Content } = Layout; -export default ({ rootPendingFilesState, visible }: PropsType) => { - const { currentTask } = useUpload(); - - return Object.keys(rootPendingFilesState || {}).length > 0 ? ( - <Layout className={'pending-files-list-layout ' + (visible ? 'visible' : '')}> - <Header className={classNames('pending-files-list-header')}> - <Row justify="space-between" align="middle"> - <Col> - <Text style={{ color: 'var(--white)' }}> - {currentTask.total > 0 && `${currentTask.uploaded}/${currentTask.total} `} - {Languages.t('components.drive_dropzone.uploading')} - </Text> - </Col> - </Row> - </Header> - {Object.keys(rootPendingFilesState || {}) && ( - <Content className="pending-files-list-content"> - <PerfectScrollbar - options={{ suppressScrollX: true, suppressScrollY: false }} - component="div" - style={{ width: '100%', height: 114 }} - > - <Row justify="start" align="middle" style={{ background: '#DFE7FE' }}> - <Col className="small-left-margin"> - <Text style={{ color: '#6C6C6D' }}> -- </Text> - </Col> - </Row> - </PerfectScrollbar> - </Content> - )} - </Layout> - ) : ( - <></> - ); -}; diff --git a/tdrive/frontend/src/app/components/file-uploads/pending-file-components/styles.scss b/tdrive/frontend/src/app/components/file-uploads/pending-file-components/styles.scss deleted file mode 100644 index 01b654a61..000000000 --- a/tdrive/frontend/src/app/components/file-uploads/pending-file-components/styles.scss +++ /dev/null @@ -1,41 +0,0 @@ -.pending-files-list-layout { - border: 1px solid var(--black-alpha-70); - box-shadow: var(--box-shadow-base); - width: 256px; - - border-radius: var(--border-radius-base); - position: absolute; - z-index: 100; - bottom: 8px; - right: 24px; - opacity: 0; - pointer-events: none; - transition: opacity 0.2s; - transition-delay: 1s; - - &.visible { - opacity: 1; - transition-delay: 0s; - pointer-events: all; - } - - .pending-files-list-header { - color: var(--white); - padding: 0 8px; - background-color: var(--secondary); - height: 32px; - line-height: 32px; - border-radius: 8px 8px 0 0; - cursor: pointer; - - &.hidden { - border-radius: 8px; - } - } - - .pending-files-list-content { - min-height: 32px; - max-height: 176px; - overflow-y: none; - } -} From bc8a38ff21016ea369fd6e689e8315ed2f9a0689 Mon Sep 17 00:00:00 2001 From: MontaGhanmy <monta.ghanmy@gmail.com> Date: Wed, 29 Jan 2025 18:24:29 +0100 Subject: [PATCH 13/13] ref: preview file when it's the only root upload --- .../src/app/features/files/services/file-upload-service.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tdrive/frontend/src/app/features/files/services/file-upload-service.ts b/tdrive/frontend/src/app/features/files/services/file-upload-service.ts index b090a858e..a59718dfc 100644 --- a/tdrive/frontend/src/app/features/files/services/file-upload-service.ts +++ b/tdrive/frontend/src/app/features/files/services/file-upload-service.ts @@ -240,7 +240,11 @@ class FileUploadService { // create the document const documentId = await DriveApiClient.create(context.companyId, { item, version }); // assign the group id with the document id - if (isFileRoot) this.groupIds[root] = documentId.id; + if (isFileRoot) { + this.groupIds[root] = documentId.id; + // set the id for the root + this.notify(); + } } }, });