✨ Big folder upload (#780)
@@ -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",
|
||||
|
||||
@@ -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é",
|
||||
|
||||
@@ -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": "Удаленная учетная запись",
|
||||
|
||||
@@ -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 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 (`<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);
|
||||
|
||||