Big folder upload (#780)

This commit is contained in:
Montassar Ghanmy
2025-02-04 15:56:07 +01:00
committed by GitHub
parent 51910fea58
commit 6002974306
39 changed files with 1190 additions and 543 deletions
+5
View File
@@ -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",
@@ -184,6 +185,10 @@
"general.resume": "Resume",
"general.send": "Send message",
"general.update": "Update",
"general.uploading": "Uploading",
"general.uploaded": "Uploaded",
"general.files": "file(s)",
"general.upload_failed": "Upload failed",
"general.user.anonymous": "Anonymous",
"general.user.deactivated": "User is no longer in this company",
"general.user.deleted": "Deleted Account",
+5
View File
@@ -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",
@@ -177,6 +178,10 @@
"general.resume": "Reprendre",
"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.upload_failed": "Échec de l'envoi",
"general.user.anonymous": "Compte anonyme",
"general.user.deactivated": "Cet utilisateur n'est plus dans cette l'entreprise",
"general.user.deleted": "Compte supprimé",
+5
View File
@@ -184,6 +184,11 @@
"general.resume": "Продолжить",
"general.send": "Отправить сообщение",
"general.update": "Обновить",
"general.uploading": "Загрузка",
"general.uploaded": "Загружено",
"general.close": "Закрыть",
"general.files": "файлы",
"general.upload_failed": "Загрузка не удалась",
"general.user.anonymous": "Анонимный",
"general.user.deactivated": "Пользователь больше не состоит в этой компании",
"general.user.deleted": "Удаленная учетная запись",
+5
View File
@@ -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",
@@ -166,6 +167,10 @@
"general.resume": "Tiếp tục",
"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.upload_failed": "Tải lên thất bại",
"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",
@@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M19.5 8.25L12 15.75L4.5 8.25" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 225 B

@@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M4.5 15.75L12 8.25L19.5 15.75" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 226 B

@@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M2.25 12C2.25 6.615 6.615 2.25 12 2.25C17.385 2.25 21.75 6.615 21.75 12C21.75 17.385 17.385 21.75 12 21.75C6.615 21.75 2.25 17.385 2.25 12ZM15.61 10.186C15.67 10.1061 15.7134 10.0149 15.7377 9.91795C15.762 9.82098 15.7666 9.72014 15.7514 9.62135C15.7361 9.52257 15.7012 9.42782 15.6489 9.3427C15.5965 9.25757 15.5276 9.18378 15.4463 9.12565C15.3649 9.06753 15.2728 9.02624 15.1753 9.00423C15.0778 8.98221 14.9769 8.97991 14.8785 8.99746C14.7801 9.01501 14.6862 9.05205 14.6023 9.10641C14.5184 9.16077 14.4462 9.23135 14.39 9.314L11.154 13.844L9.53 12.22C9.38783 12.0875 9.19978 12.0154 9.00548 12.0188C8.81118 12.0223 8.62579 12.101 8.48838 12.2384C8.35097 12.3758 8.27225 12.5612 8.26882 12.7555C8.2654 12.9498 8.33752 13.1378 8.47 13.28L10.72 15.53C10.797 15.6069 10.8898 15.6662 10.992 15.7036C11.0942 15.7411 11.2033 15.7559 11.3118 15.7469C11.4202 15.738 11.5255 15.7055 11.6201 15.6519C11.7148 15.5982 11.7967 15.5245 11.86 15.436L15.61 10.186Z" fill="#00A029"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

@@ -0,0 +1,3 @@
<svg width="28" height="28" viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M14 12.9231V19.0769V12.9231ZM17.0769 16H10.9231H17.0769ZM15.0872 8.62564L12.9128 6.45128C12.77 6.30826 12.6003 6.19479 12.4136 6.11735C12.2269 6.03992 12.0268 6.00004 11.8246 6H6.30769C5.69565 6 5.10868 6.24313 4.67591 6.67591C4.24313 7.10868 4 7.69565 4 8.30769V20.6154C4 21.2274 4.24313 21.8144 4.67591 22.2472C5.10868 22.6799 5.69565 22.9231 6.30769 22.9231H21.6923C22.3043 22.9231 22.8913 22.6799 23.3241 22.2472C23.7569 21.8144 24 21.2274 24 20.6154V11.3846C24 10.7726 23.7569 10.1856 23.3241 9.75283C22.8913 9.32005 22.3043 9.07692 21.6923 9.07692H16.1754C15.7675 9.07656 15.3754 8.91424 15.0872 8.62564Z" fill="#A4A4A4"/>
</svg>

After

Width:  |  Height:  |  Size: 741 B

@@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9.75 9.75L14.25 14.25M14.25 9.75L9.75 14.25M21 12C21 13.1819 20.7672 14.3522 20.3149 15.4442C19.8626 16.5361 19.1997 17.5282 18.364 18.364C17.5282 19.1997 16.5361 19.8626 15.4442 20.3149C14.3522 20.7672 13.1819 21 12 21C10.8181 21 9.64778 20.7672 8.55585 20.3149C7.46392 19.8626 6.47177 19.1997 5.63604 18.364C4.80031 17.5282 4.13738 16.5361 3.68508 15.4442C3.23279 14.3522 3 13.1819 3 12C3 9.61305 3.94821 7.32387 5.63604 5.63604C7.32387 3.94821 9.61305 3 12 3C14.3869 3 16.6761 3.94821 18.364 5.63604C20.0518 7.32387 21 9.61305 21 12Z" stroke="#A4A4A4" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 736 B

@@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M14.25 9V15M9.75 15V9M21 12C21 13.1819 20.7672 14.3522 20.3149 15.4442C19.8626 16.5361 19.1997 17.5282 18.364 18.364C17.5282 19.1997 16.5361 19.8626 15.4442 20.3149C14.3522 20.7672 13.1819 21 12 21C10.8181 21 9.64778 20.7672 8.55585 20.3149C7.46392 19.8626 6.47177 19.1997 5.63604 18.364C4.80031 17.5282 4.13738 16.5361 3.68508 15.4442C3.23279 14.3522 3 13.1819 3 12C3 9.61305 3.94821 7.32387 5.63604 5.63604C7.32387 3.94821 9.61305 3 12 3C14.3869 3 16.6761 3.94821 18.364 5.63604C20.0518 7.32387 21 9.61305 21 12Z" stroke="#A4A4A4" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 713 B

@@ -0,0 +1,4 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M21 12C21 13.1819 20.7672 14.3522 20.3149 15.4442C19.8626 16.5361 19.1997 17.5282 18.364 18.364C17.5282 19.1997 16.5361 19.8626 15.4442 20.3149C14.3522 20.7672 13.1819 21 12 21C10.8181 21 9.64778 20.7672 8.55585 20.3149C7.46392 19.8626 6.47177 19.1997 5.63604 18.364C4.80031 17.5282 4.13738 16.5361 3.68508 15.4442C3.23279 14.3522 3 13.1819 3 12C3 9.61305 3.94821 7.32387 5.63604 5.63604C7.32387 3.94821 9.61305 3 12 3C14.3869 3 16.6761 3.94821 18.364 5.63604C20.0518 7.32387 21 9.61305 21 12Z" stroke="#45454A" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M15.91 11.672C15.9686 11.7044 16.0174 11.752 16.0513 11.8097C16.0853 11.8673 16.1032 11.9331 16.1032 12C16.1032 12.0669 16.0853 12.1327 16.0513 12.1903C16.0174 12.248 15.9686 12.2955 15.91 12.328L10.307 15.441C10.2499 15.4727 10.1855 15.4889 10.1202 15.4881C10.0549 15.4873 9.99099 15.4694 9.93472 15.4363C9.87845 15.4031 9.8318 15.3559 9.79941 15.2992C9.76701 15.2425 9.74998 15.1783 9.75 15.113V8.887C9.75 8.601 10.057 8.421 10.307 8.56L15.91 11.672Z" stroke="#45454A" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

@@ -0,0 +1,11 @@
<svg width="28" height="28" viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_2347_259)">
<rect width="28" height="28" rx="14" fill="#F0F2F3"/>
<path d="M7 14.5455V14C7 13.566 7.17019 13.1498 7.47314 12.8429C7.77608 12.536 8.18696 12.3636 8.61538 12.3636H19.3846C19.813 12.3636 20.2239 12.536 20.5269 12.8429C20.8298 13.1498 21 13.566 21 14V14.5455M14.761 9.86182L13.239 8.32C13.139 8.21858 13.0202 8.13812 12.8895 8.08322C12.7588 8.02831 12.6187 8.00003 12.4772 8H8.61538C8.18696 8 7.77608 8.1724 7.47314 8.47928C7.17019 8.78616 7 9.20237 7 9.63636V18.3636C7 18.7976 7.17019 19.2138 7.47314 19.5207C7.77608 19.8276 8.18696 20 8.61538 20H19.3846C19.813 20 20.2239 19.8276 20.5269 19.5207C20.8298 19.2138 21 18.7976 21 18.3636V11.8182C21 11.3842 20.8298 10.968 20.5269 10.6611C20.2239 10.3542 19.813 10.1818 19.3846 10.1818H15.5228C15.2372 10.1816 14.9628 10.0665 14.761 9.86182Z" stroke="#424244" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</g>
<defs>
<clipPath id="clip0_2347_259">
<rect width="28" height="28" rx="14" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

@@ -10,6 +10,14 @@ 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 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';
@@ -37,6 +45,22 @@ export const FileTypeSlidesIcon = (props: ComponentProps<'svg'>) => (
export const FileTypeLinkIcon = (props: ComponentProps<'svg'>) => <FileTypeLinkSvg {...props} />;
export const FolderIcon = (props: ComponentProps<'svg'>) => <FolderSvg {...props} />;
export const ArrowDownIcon = (props: ComponentProps<'svg'>) => <ArrowDownSvg {...props} />;
export const ArrowUpIcon = (props: ComponentProps<'svg'>) => <ArrowUpSvg {...props} />;
export const CheckGreenIcon = (props: ComponentProps<'svg'>) => <CheckGreenSvg {...props} />;
export const ShowFolderIcon = (props: ComponentProps<'svg'>) => <ShowFolderSvg {...props} />;
export const ResumeIcon = (props: ComponentProps<'svg'>) => <ResumeSvg {...props} />;
export const PauseIcon = (props: ComponentProps<'svg'>) => <PauseSvg {...props} />;
export const CancelIcon = (props: ComponentProps<'svg'>) => <CancelSvg {...props} />;
export const RemoveIcon = (props: ComponentProps<'svg'>) => <RemoveSvg {...props} />;
export const SentIcon = (props: ComponentProps<'svg'>) => <SentSvg {...props} />;
@@ -1,221 +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)" />
)
}
onClick={() => pauseOrResumeUpload(pendingFileState.id)}
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>
);
};
@@ -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>
) : (
<></>
);
};
@@ -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;
}
}
@@ -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': <FileTypePdfIcon />,
'application/vnd.openxmlformats-officedocument.wordprocessingml.document': (
<FileTypeDocumentIcon />
),
'application/msword': <FileTypeDocumentIcon />,
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': <FileTypeSpreadsheetIcon />,
'application/vnd.ms-excel': <FileTypeSpreadsheetIcon />,
'application/vnd.ms-powerpoint': <FileTypeSlidesIcon />,
'application/vnd.openxmlformats-officedocument.presentationml.presentation': (
<FileTypeSlidesIcon />
),
'application/zip': <FileTypeArchiveIcon />,
'application/x-rar-compressed': <FileTypeArchiveIcon />,
'application/x-tar': <FileTypeArchiveIcon />,
'application/x-7z-compressed': <FileTypeArchiveIcon />,
'application/x-bzip': <FileTypeArchiveIcon />,
'application/x-bzip2': <FileTypeArchiveIcon />,
'application/x-gzip': <FileTypeArchiveIcon />,
'video/mp4': <FileTypeMediaIcon />,
'video/mpeg': <FileTypeMediaIcon />,
'video/ogg': <FileTypeMediaIcon />,
'video/webm': <FileTypeMediaIcon />,
'video/quicktime': <FileTypeMediaIcon />,
};
@@ -0,0 +1,152 @@
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';
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');
const completed = keys.filter(key => roots[key].status === 'completed');
const paused = keys.filter(key => roots[key].status === 'paused');
return { inProgress, completed, paused };
};
interface ModalHeaderProps {
uploadingCount: number;
completedCount: number;
totalRoots: number;
uploadingPercentage: number;
toggleModal: () => void;
modalExpanded: boolean;
}
const ModalHeader: React.FC<ModalHeaderProps> = ({
uploadingCount,
completedCount,
totalRoots,
uploadingPercentage,
toggleModal,
modalExpanded,
}) => (
<div className="w-full flex bg-[#45454A] text-white p-4 items-center justify-between">
<p className="testid:upload-modal-head-status">
{uploadingCount > 0
? `${Languages.t('general.uploading')} ${uploadingCount}`
: `${Languages.t('general.uploaded')} ${completedCount}`}{' '}
{Languages.t('general.files')}
</p>
<button
className="ml-auto flex items-center testid:upload-modal-toggle-arrow"
onClick={toggleModal}
>
{modalExpanded ? <ArrowDownIcon /> : <ArrowUpIcon />}
</button>
</div>
);
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">
{uploadingCount > 0 && (
<button
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 hover:text-white testid:upload-modal-cancel-close"
onClick={cancelUpload}
>
{uploadingCount ? Languages.t('general.cancel') : Languages.t('general.close')}
</button>
</div>
</div>
);
const PendingRootList = ({
roots,
status,
parentId,
}: {
roots: UploadRootListType;
status: UploadStateEnum;
parentId: string;
}): JSX.Element => {
const [modalExpanded, setModalExpanded] = useState(true);
const { pauseOrResumeUpload, cancelUpload } = useUpload();
const keys = useMemo(() => Object.keys(roots || {}), [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), []);
return (
<>
{totalRoots > 0 && (
<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}
totalRoots={totalRoots}
uploadingPercentage={uploadingPercentage}
toggleModal={toggleModal}
modalExpanded={modalExpanded}
/>
{modalExpanded && (
<div className="modal-body">
<div className="bg-white px-4 py-2">
<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 + pausedCount}
/>
</div>
)}
</div>
)}
</>
);
};
export default PendingRootList;
@@ -0,0 +1,208 @@
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';
import { useDriveActions } from 'app/features/drive/hooks/use-drive-actions';
import { useDriveItem } from 'app/features/drive/hooks/use-drive-item';
import Languages from 'app/features/global/services/languages-service';
const PendingRootRow = ({
rootKey,
root,
parentId,
}: {
rootKey: string;
root: UploadRootType;
parentId: string;
}): 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;
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) {
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
// If the root is a folder, it will show the folder icon
const itemTypeIcon = useCallback(
(type: string) =>
isFileRoot ? (
fileTypeIconsMap[type as keyof typeof fileTypeIconsMap] || <FileTypeUnknownIcon />
) : (
<FolderIcon />
),
[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(async () => {
setShowFolder(true);
}, 1500);
return () => clearTimeout(timeout);
}
}, [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);
postProcess();
}
}, [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 testid:upload-modal-row-type">
{itemTypeIcon(firstPendingFile?.type)}
</div>
</div>
<p className="ml-4">
<span className="font-bold">{truncateRootName(rootKey)} </span>
{root.status !== 'failed' && root.uploadedSize > 0 && (
<span className="ml-4 text-sm">
({formatFileSize(root.uploadedSize)} / {formatFileSize(root.size)})
</span>
)}
{root.status === 'failed' && (
<span className="ml-4 text-red-500">{Languages.t('general.upload_failed')}</span>
)}
</p>
<div className="progress-check flex items-center justify-center ml-auto">
{isUploadCompleted ? (
<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 hover:scale-110'
}`}
/>
<ShowFolderIcon
className={`transition-opacity duration-300 ${
showFolder ? 'opacity-1 hover:scale-110' : 'opacity-0 w-0 h-0'
}`}
/>
</>
)}
{isFileRoot && (
<CheckGreenIcon className="opacity-1 hover:scale-110 transition-transform duration-200" />
)}
</button>
) : (
!['cancelled', 'failed'].includes(root.status) &&
firstPendingFile?.status !== 'error' && (
<>
<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 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>
</>
)
)}
</div>
</div>
</div>
<div className="root-progress h-[3px] mt-4">
{!showFolder && (
<div className="w-full h-[3px] bg-[#F0F2F3]">
<div
className={`testid:upload-modal-row-progress h-full ${
root.status === 'failed'
? 'bg-[#FF0000]' // Red color for failed uploads
: root.status === 'cancelled'
? 'bg-[#FFA500]' // Orange for cancelled uploads
: 'bg-[#00A029]' // Green for successful uploads
}`}
style={{
width: `${
root.status === 'failed' || root.status === 'cancelled' ? 100 : uploadProgress
}%`,
}}
></div>
</div>
)}
</div>
</div>
);
};
export default PendingRootRow;
@@ -1,16 +1,19 @@
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 UploadsViewer = (): JSX.Element => {
const { currentTask } = useUpload();
return (
<PendingFilesList
visible={!!currentTask && currentTask.files.length > 0 && !currentTask.completed}
pendingFilesState={currentTask.files}
/>
);
// Destructure and provide default values for safety
const { roots = {}, status, parentId } = currentTask || {};
const rootKeys = Object.keys(roots);
// Early return for clarity
if (rootKeys.length === 0) {
return <></>;
}
return <PendingRootList roots={roots} status={status} parentId={parentId} />;
};
export default ChatUploadsViewer;
export default UploadsViewer;
@@ -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}
@@ -1,10 +1,13 @@
import { number } from 'prop-types';
/* 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;
documentsCount: number;
totalSize: number;
sizePerRoot: { [key: string]: number };
};
export const getFilesTree = (
@@ -119,6 +122,7 @@ export const getFilesTree = (
});
}
let timeBegin = Date.now();
[].slice.call(items).forEach(function (entry: any) {
entry = entry.webkitGetAsEntry();
if (entry) {
@@ -134,6 +138,7 @@ export const getFilesTree = (
resolve(true);
}, resolve.bind(null, true));
} else if (entry.isDirectory) {
const timeToRead = Date.now();
readDirectory(entry, null, resolve);
}
}),
@@ -145,6 +150,7 @@ export const getFilesTree = (
return false;
}
timeBegin = Date.now();
Promise.all(rootPromises).then(cb.bind(null, fd, files));
}
@@ -152,6 +158,7 @@ export const getFilesTree = (
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];
@@ -163,7 +170,17 @@ export const getFilesTree = (
return;
}
if (dir_index === path.split('/').length - 1) {
dirs[dir] = real_file;
const root = path.split('/')[0];
dirs[dir] = {
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] = {};
@@ -172,35 +189,76 @@ export const getFilesTree = (
}
});
});
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
if (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]) {
// 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) {
// 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 wont 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 doesnt 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 (`<input type="file">`) 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,
totalSize: (event.target as any).files[0].size,
sizePerRoot: { [(event.target as any).files[0].name]: (event.target as any).files[0].size },
});
}
});
@@ -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';
@@ -10,18 +10,19 @@ 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([file], {
FileUploadService.upload([{ root: file.name, file }], {
context: {
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,
@@ -47,58 +48,9 @@ export const useDriveUpload = () => {
tree: FileTreeObject,
context: { companyId: string; parentId: string },
) => {
// Create all directories
logger.debug("Start creating directories ...");
const filesPerParentId = await FileUploadService.createDirectories(tree.tree, context);
logger.debug('Start creating directories and file upload ...');
await FileUploadService.createDirectories(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;
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);
}
},
});
}
};
const uploadFromUrl = (
@@ -112,14 +64,17 @@ 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([file], {
FileUploadService.upload([{ root: file.name, file }], {
context: {
companyId: context.companyId,
parentId: context.parentId,
},
callback: (file, context) => {
callback: (filePayload, context) => {
const file = filePayload.file;
if (file) {
create(
{
@@ -0,0 +1,65 @@
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) => {
addFilesToResumable(tree.tree); // Add files from the tree
startUpload(); // Start uploading
};
return {
uploadTree,
};
};
@@ -27,7 +27,15 @@ export const useUploadZones = (zoneId: string) => {
list: File[],
options?: { uploader?: (file: File, context: any) => Promise<FileType>; 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 => {
@@ -2,18 +2,25 @@ 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 [pendingFilesListState, _setPendingFilesListState] = useRecoilState(PendingFilesListState);
const [_rootPendingFilesListState, setRootPendingFilesListState] =
useRecoilState(RootPendingFilesListState);
FileUploadService.setRecoilHandler(setRootPendingFilesListState);
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.cancelRootUpload(id);
const getOnePendingFile = (id: string) => FileUploadService.getPendingFile(id);
@@ -23,13 +30,18 @@ export const useUpload = () => {
const retryUpload = (id: string) => FileUploadService.retry(id);
const clearRoots = () => FileUploadService.clearRoots();
return {
pendingFilesListState,
pauseOrResumeUpload,
pauseOrResumeRootUpload,
cancelUpload,
cancelRootUpload,
getOnePendingFile,
currentTask,
deleteOneFile,
retryUpload,
clearRoots,
};
};
@@ -10,19 +10,46 @@ 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';
import { DriveItem, DriveItemVersion } from 'app/features/drive/types';
export enum Events {
ON_CHANGE = 'notify',
}
export enum UploadStateEnum {
Progress = 'progress',
Completed = 'completed',
Paused = 'paused',
Cancelled = 'cancelled',
Failed = 'failed',
}
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 rootStates: {
paused: RootState;
cancelled: RootState;
completed: RootState;
failed: RootState;
} = {
paused: {},
cancelled: {},
completed: {},
failed: {},
};
public currentTaskId = '';
public parentId = '';
public uploadStatus = UploadStateEnum.Progress;
private recoilHandler: Function = () => undefined;
private logger: Logger.Logger = Logger.getLogger('FileUploadService');
@@ -30,28 +57,114 @@ class FileUploadService {
this.recoilHandler = handler;
}
/**
* Helper method to pause execution when `isPaused` is true.
* @private
*/
async _waitWhilePaused(id?: string) {
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
}
}
/**
* Helper method to cancel execution when `isCancelled` is true.
* @private
*/
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 = this.pendingFiles.map((f: PendingFileType) => {
return {
id: f.id,
status: f.status,
progress: f.progress,
file: f.backendFile,
const updatedState = Object.keys(this.groupedPendingFiles).reduce((acc: any, key: string) => {
// Calculate the uploaded size
const uploadedSize = this.groupedPendingFiles[key]
.map((f: PendingFileType) =>
f.status === 'success' && f.originalFile?.size ? f.originalFile.size : 0,
)
.reduce((acc: number, size: number) => acc + size, 0);
// Check for failed files
const failedFiles = this.groupedPendingFiles[key].filter(f => f.status === 'error');
if (failedFiles.length > 0) {
this.rootStates.failed[key] = true;
}
// Determine the upload status based on failed, cancelled, paused, completed, or uploading states
const status = this.rootStates.failed[key]
? 'failed'
: this.rootStates.cancelled[key]
? 'cancelled'
: this.rootStates.paused[key]
? 'paused'
: 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],
items: [],
size: this.rootSizes[key],
uploadedSize,
status,
};
});
return acc;
}, {});
this.recoilHandler(_.cloneDeep(updatedState));
}
public async createDirectories(root: FileTreeObject['tree'], context: { companyId: string; parentId: string }) {
// Create all directories
const filesPerParentId: { [key: string]: File[] } = {};
filesPerParentId[context.parentId] = []
public async createDirectories(
tree: FileTreeObject,
context: { companyId: string; parentId: string },
) {
// reset the upload status
this.uploadStatus = UploadStateEnum.Progress;
const traverserTreeLevel = async (tree: FileTreeObject['tree'], parentId: string) => {
const root = tree.tree;
this.rootSizes = this.rootSizes = {
...this.rootSizes,
...(tree.sizePerRoot || {}),
};
// 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.uploadStatus === UploadStateEnum.Cancelled) return;
// check if upload is paused
await this._waitWhilePaused();
// start descending the tree
for (const directory of Object.keys(tree)) {
if (tree[directory] instanceof File) {
logger.trace(`${directory} is a file, save it for future upload`);
filesPerParentId[parentId].push(tree[directory] as File);
const root = tree[directory].root as string;
await this.checkCancellation(root);
await this._waitWhilePaused(root);
if (tree[directory].file instanceof File) {
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,
});
} else {
logger.debug(`Create directory ${directory}`);
@@ -60,6 +173,7 @@ class FileUploadService {
parent_id: parentId,
name: directory,
is_directory: true,
is_in_trash: tmp,
};
if (!this.pendingFiles.some(f => isPendingFileStatusPending(f.status))) {
@@ -77,39 +191,104 @@ 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.groupIds[directory] = driveItem.id;
this.logger.debug(`Directory ${directory} created`);
pendingFile.status = 'success';
this.notify();
if (driveItem?.id) {
filesPerParentId[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
const files = _.cloneDeep(filesPerParentId[parentId]);
// reset the filesPerParentId
filesPerParentId[parentId] = [];
await this.upload(files, {
context: {
companyId: context.companyId,
parentId: parentId,
},
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,
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<DriveItem>;
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<DriveItemVersion>;
// 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;
// set the id for the root
this.notify();
}
}
},
});
};
// 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) {
logger.error('Error while processing tree', error);
}
await traverserTreeLevel(root, context.parentId);
return filesPerParentId;
// await traverserTreeLevel(root, context.parentId, true);
return { filesPerParentId, idsToBeRestored };
}
public async upload(
fileList: File[],
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();
@@ -125,7 +304,11 @@ class FileUploadService {
}
for (const file of fileList) {
if (!file) continue;
// 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 = {
id: uuid(),
@@ -134,21 +317,24 @@ 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);
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 +359,7 @@ class FileUploadService {
},
});
pendingFile.resumable.addFile(file);
pendingFile.resumable.addFile(file.file);
pendingFile.resumable.on('fileAdded', () => pendingFile.resumable.upload());
@@ -197,8 +383,10 @@ class FileUploadService {
try {
pendingFile.backendFile = JSON.parse(message).resource;
pendingFile.status = 'success';
console.log('fileSuccess', options?.callback);
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);
@@ -208,10 +396,10 @@ 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));
options?.callback?.(null, options?.context || {});
const intendedFilename =
(pendingFile.originalFile || {}).name ||
(pendingFile.backendFile || { metadata: {} }).metadata.name;
options?.callback?.({ root: file.root, file: null }, options?.context || {});
this.notify();
});
}
@@ -237,26 +425,89 @@ 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];
public cancelUpload() {
this.uploadStatus = UploadStateEnum.Cancelled;
fileToCancel.status = 'cancel';
// pause or resume the resumable tasks
const fileToCancel = this.pendingFiles;
if (fileToCancel.resumable) {
fileToCancel.resumable.cancel();
this.notify();
if (fileToCancel.backendFile)
this.deleteOneFile({
companyId: fileToCancel.backendFile.company_id,
fileId: fileToCancel.backendFile.id,
});
if (!fileToCancel) {
console.error(`No files found for id`);
return;
}
setTimeout(() => {
this.pendingFiles = this.pendingFiles.filter(f => f.id !== id);
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 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();
}, timeout);
}
}
public retry(id: string) {
@@ -270,15 +521,101 @@ 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);
}
}
fileToCancel.status !== 'pause'
? (fileToCancel.status = 'pause')
: (fileToCancel.status = 'pending');
fileToCancel.status === 'pause'
? fileToCancel.resumable.pause()
: fileToCancel.resumable.upload();
public pauseOrResume() {
// pause or resume the curent upload task
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;
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);
}
// update the status of the roots
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.rootStates.paused[root] = true;
} else {
this.rootStates.paused[root] = false;
}
}
this.notify();
}
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];
} else {
this.rootStates.paused[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();
}
@@ -307,6 +644,7 @@ class FileUploadService {
testChunks: testChunks || false,
simultaneousUploads: simultaneousUploads || 5,
maxChunkRetries: maxChunkRetries || 2,
xhrTimeout: 60000,
query,
});
}
@@ -335,13 +673,18 @@ class FileUploadService {
});
}
public getDownloadRoute({ companyId, fileId }: { companyId: string; fileId: string }): string {
return FileUploadAPIClient.getDownloadRoute({
companyId: companyId,
fileId: fileId,
});
}
public clearRoots() {
this.groupedPendingFiles = {};
this.groupIds = {};
this.notify();
}
}
export default new FileUploadService();
@@ -0,0 +1,18 @@
import { atom } from 'recoil';
import { PendingFileRecoilType } from '@features/files/types/file';
export const RootPendingFilesListState = atom<
| {
[key: string]: {
id: string;
size: number;
uploadedSize: number;
status: string;
items: PendingFileRecoilType[];
};
}
| undefined
>({
key: 'RootPendingFilesListState',
default: {},
});
@@ -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,8 +18,11 @@ export const CurrentTaskSelector = selector({
: [];
return {
parentId: FileUploadService.parentId,
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'),
};
@@ -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,15 @@ export type PendingFileType = {
label: string | null;
pausable: boolean;
};
export type UploadRootType = {
id: string;
size: number;
uploadedSize: number;
status: string;
items: PendingFileRecoilType[];
};
export type UploadRootListType = {
[key: string]: UploadRootType;
};
@@ -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 ' +
@@ -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';
@@ -49,10 +49,11 @@ 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,
{ context?: string; initialFolderId: string }
string,
{ context?: string; initialFolderId: string }
>({
key: 'DriveCurrentFolderAtom',
default: options => options.initialFolderId || 'root',
@@ -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,12 +333,16 @@ 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,
});
await new Promise (resolve => setTimeout(resolve, 1000));
refresh(parentId);
}}
onDragOver={handleDragOver}
onDrop={handleDrop}
@@ -358,7 +366,11 @@ export default memo(
(loading && (!items?.length || loadingParentChange) ? 'opacity-50 ' : '')
}
>
<div className={`flex flex-row shrink-0 items-center mb-4 ${!sharedWithMe ? 'flex-wrap' : ''} border-b md:border-b-0 px-4 py-2 md:px-0 md:py-0`}>
<div
className={`flex flex-row shrink-0 items-center mb-4 ${
!sharedWithMe ? 'flex-wrap' : ''
} border-b md:border-b-0 px-4 py-2 md:px-0 md:py-0`}
>
{sharedWithMe ? (
<div>
<Title className="mb-4 block">
@@ -376,7 +388,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"
@@ -399,7 +411,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"
@@ -419,7 +431,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"
@@ -450,9 +462,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' : ''
@@ -467,7 +487,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`
@@ -493,8 +517,10 @@ export default memo(
<br />
<Button
onClick={() => uploadItemModal()}
theme="primary"
theme={isPreparingUpload ? 'outline' : 'primary'}
className="mt-4"
loading={isPreparingUpload}
disabled={isPreparingUpload}
testClassId="button-add-doc"
>
{Languages.t('scenes.app.drive.add_doc')}
@@ -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';
@@ -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';
@@ -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>('');
@@ -21,11 +21,12 @@ export const CreateLink = () => {
type: 'text/uri-list',
});
await FileUploadService.upload([file], {
await FileUploadService.upload([{ root: file.name, file }], {
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>
</>
);
@@ -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';
@@ -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);