@@ -0,0 +1,166 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import classNames, { Argument } from 'classnames';
|
||||
|
||||
import { FileThumbnail, FileDetails, FileActions, FileProgress } from './parts';
|
||||
import {
|
||||
isPendingFileStatusCancel,
|
||||
isPendingFileStatusError,
|
||||
isPendingFileStatusSuccess,
|
||||
} from 'app/features/files/utils/pending-files';
|
||||
import { DataFileType } from './types';
|
||||
import DriveService from 'app/deprecated/Apps/Drive/Drive.js';
|
||||
import RouterService from 'app/features/router/services/router-service';
|
||||
|
||||
import './file.scss';
|
||||
import { PendingFileRecoilType } from 'app/features/files/types/file';
|
||||
import Api from 'app/features/global/framework/api-service';
|
||||
import FileUploadAPIClient from '../../features/files/api/file-upload-api-client';
|
||||
import LargePreview from './parts/large-preview';
|
||||
import { MessageFileType } from 'app/features/messages/types/message';
|
||||
import { useFileViewerModal } from 'app/features/viewer/hooks/use-viewer';
|
||||
|
||||
type PropsType = {
|
||||
source: 'internal' | 'drive' | string;
|
||||
externalId: string | any;
|
||||
file: DataFileType;
|
||||
messageFile: MessageFileType;
|
||||
context: 'input' | 'message' | 'drive';
|
||||
progress?: number;
|
||||
status?: PendingFileRecoilType['status'];
|
||||
onRemove?: () => void;
|
||||
className?: string;
|
||||
large?: boolean;
|
||||
xlarge?: boolean;
|
||||
};
|
||||
|
||||
export default ({
|
||||
source,
|
||||
externalId,
|
||||
file: _file,
|
||||
messageFile,
|
||||
className,
|
||||
context,
|
||||
progress,
|
||||
status,
|
||||
onRemove,
|
||||
large,
|
||||
xlarge,
|
||||
}: PropsType) => {
|
||||
const { companyId, workspaceId } = RouterService.getStateFromRoute();
|
||||
const [file, setFile] = useState<DataFileType>(_file);
|
||||
const classNameArguments: Argument[] = [
|
||||
'file-component',
|
||||
className,
|
||||
{ 'large-view': large },
|
||||
{
|
||||
'file-component-error':
|
||||
status && (isPendingFileStatusError(status) || isPendingFileStatusCancel(status)),
|
||||
'file-component-uploading': progress != undefined && progress < 1,
|
||||
},
|
||||
];
|
||||
|
||||
const { open: openViewer } = useFileViewerModal();
|
||||
|
||||
useEffect(() => {
|
||||
if (source === 'drive') {
|
||||
(async () => {
|
||||
if (typeof externalId === 'string') {
|
||||
externalId = { id: externalId, workspace_id: workspaceId };
|
||||
}
|
||||
|
||||
let driveFile = (await Api.post('/ajax/drive/v2/find', {
|
||||
options: {
|
||||
element_id: externalId?.id,
|
||||
workspace_id: externalId?.workspace_id,
|
||||
},
|
||||
})) as any;
|
||||
driveFile = driveFile?.data || {};
|
||||
|
||||
setFile({
|
||||
...file,
|
||||
thumbnail: driveFile.preview_link,
|
||||
name: driveFile.name,
|
||||
size: driveFile.size,
|
||||
type: FileUploadAPIClient.mimeToType(
|
||||
FileUploadAPIClient.extensionToMime(driveFile.extension),
|
||||
),
|
||||
});
|
||||
})();
|
||||
} else {
|
||||
setFile(_file);
|
||||
}
|
||||
}, [_file]);
|
||||
|
||||
const onClickFile = async () => {
|
||||
if (source === 'internal') {
|
||||
//Only if upload has ended
|
||||
if ((!status || isPendingFileStatusSuccess(status)) && file.id) openViewer(messageFile);
|
||||
}
|
||||
if (source === 'drive') {
|
||||
if (typeof externalId === 'string') {
|
||||
externalId = { id: externalId, workspace_id: workspaceId };
|
||||
}
|
||||
|
||||
const file = (await Api.post('/ajax/drive/v2/find', {
|
||||
options: {
|
||||
element_id: externalId?.id,
|
||||
workspace_id: externalId?.workspace_id,
|
||||
},
|
||||
})) as any;
|
||||
DriveService.viewDocument(file?.data, context === 'input');
|
||||
}
|
||||
};
|
||||
|
||||
let computedHeight = 200;
|
||||
let computedWidth = file.thumbnail_ratio * 200;
|
||||
const isMediaFile = ['image', 'video'].includes(file.type);
|
||||
|
||||
if (xlarge) {
|
||||
computedWidth = Math.max(
|
||||
160,
|
||||
Math.min(
|
||||
Math.min(messageFile.metadata?.thumbnails?.[0]?.width || 600, 600),
|
||||
file.thumbnail_ratio * document.body.clientHeight * 0.5,
|
||||
),
|
||||
);
|
||||
computedHeight = Math.max(
|
||||
200,
|
||||
Math.min(
|
||||
Math.min(
|
||||
messageFile.metadata?.thumbnails?.[0]?.height || 10000,
|
||||
document.body.clientHeight * 0.5,
|
||||
),
|
||||
computedWidth / file.thumbnail_ratio,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(classNameArguments)}
|
||||
style={large ? { width: computedWidth, height: computedHeight } : {}}
|
||||
onClick={() => companyId && onClickFile()}
|
||||
>
|
||||
{large && <LargePreview file={file} />}
|
||||
<div
|
||||
className={classNames('file-info-container', {
|
||||
'media-file-info-container': isMediaFile,
|
||||
})}
|
||||
>
|
||||
<FileThumbnail file={file} />
|
||||
<FileDetails file={file} source={source} />
|
||||
<FileActions
|
||||
deletable={context === 'input'}
|
||||
actionMenu={context === 'message' && source === 'internal'}
|
||||
status={status}
|
||||
file={file}
|
||||
messageFile={messageFile}
|
||||
onRemove={onRemove}
|
||||
source={source}
|
||||
/>
|
||||
</div>
|
||||
<FileProgress progress={progress} status={status} file={file} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,160 @@
|
||||
.file-component {
|
||||
border: 1px solid var(--grey-light);
|
||||
box-shadow: 0px 1px 2px rgba(0, 0, 0, 0.1);
|
||||
border-radius: 8px 8px 8px 8px;
|
||||
width: 218px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
overflow: hidden;
|
||||
min-height: 50px;
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&.large-view {
|
||||
min-height: 200px;
|
||||
max-height: 500px;
|
||||
max-width: 90%;
|
||||
min-width: 160px;
|
||||
position: relative;
|
||||
|
||||
.file-large-preview {
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
top: 0px;
|
||||
left: 0px;
|
||||
right: 0px;
|
||||
background-size: contain;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.file-info-container {
|
||||
opacity: 1;
|
||||
transition: opacity 0s;
|
||||
}
|
||||
}
|
||||
|
||||
.file-info-container {
|
||||
background-color: #fffffff0;
|
||||
position: absolute;
|
||||
border-top: 1px solid var(--grey-light);
|
||||
bottom: 0px;
|
||||
width: 100%;
|
||||
|
||||
.file-thumbnail-container {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.media-file-info-container {
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
|
||||
.file-large-preview-play-container {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border-radius: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
&.file-component-uploading {
|
||||
border-radius: 8px 8px 0px 0px;
|
||||
|
||||
& .file-info-container {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.file-info-container {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.file-progress-bar-container {
|
||||
padding-top: 3px;
|
||||
padding-bottom: 3px;
|
||||
height: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
&.file-component-error {
|
||||
background: var(--error-background);
|
||||
}
|
||||
|
||||
.file-info-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
padding: 8px 8px 6px 8px;
|
||||
|
||||
.file-thumbnail-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 32px;
|
||||
|
||||
.file-thumbnail-component {
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.file-component-details {
|
||||
overflow: hidden;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.ant-tag {
|
||||
height: 15px;
|
||||
line-height: 15px;
|
||||
padding: 0 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.file-menu {
|
||||
display: flex;
|
||||
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
.ant-btn {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
&:hover {
|
||||
color: var(--black);
|
||||
}
|
||||
color: var(--grey-dark);
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.file-progress-bar-container {
|
||||
display: flex;
|
||||
|
||||
.file-progress-bar {
|
||||
.ant-progress-outer {
|
||||
height: 3px;
|
||||
display: flex;
|
||||
|
||||
.ant-progress-inner {
|
||||
border-radius: 0px;
|
||||
|
||||
.ant-progress-bg {
|
||||
border-radius: 2px;
|
||||
height: 3px !important;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import React, { useRef } from 'react';
|
||||
import { Button } from 'antd';
|
||||
import { MoreHorizontal, RotateCw, X } from 'react-feather';
|
||||
|
||||
import {
|
||||
isPendingFileStatusPending,
|
||||
isPendingFileStatusPause,
|
||||
isPendingFileStatusSuccess,
|
||||
isPendingFileStatusError,
|
||||
isPendingFileStatusCancel,
|
||||
} from 'app/features/files/utils/pending-files';
|
||||
import Languages from 'app/features/global/services/languages-service';
|
||||
import { useUpload } from 'app/features/files/hooks/use-upload';
|
||||
import { DataFileType } from '../types';
|
||||
import MenuManager from 'app/components/menus/menus-manager';
|
||||
import { PendingFileRecoilType } from 'app/features/files/types/file';
|
||||
import { MessageFileType } from 'app/features/messages/types/message';
|
||||
import { useFileViewerModal } from 'app/features/viewer/hooks/use-viewer';
|
||||
import { useEditors } from 'app/views/client/viewer/other/editors-service';
|
||||
|
||||
type PropsType = {
|
||||
file: DataFileType;
|
||||
messageFile: MessageFileType;
|
||||
status?: PendingFileRecoilType['status'];
|
||||
deletable?: boolean;
|
||||
actionMenu?: boolean;
|
||||
onRemove?: () => void;
|
||||
source?: string;
|
||||
};
|
||||
|
||||
export const FileActions = ({
|
||||
file,
|
||||
messageFile,
|
||||
status,
|
||||
deletable,
|
||||
actionMenu,
|
||||
onRemove,
|
||||
}: PropsType): JSX.Element => {
|
||||
const { cancelUpload, deleteOneFile, downloadOneFile, retryUpload } = useUpload();
|
||||
const menuRef = useRef<HTMLElement>();
|
||||
const { open: openPreview } = useFileViewerModal();
|
||||
const { candidates } = useEditors(file.name.split('.').pop() || '');
|
||||
|
||||
const onClickDownload = async () => {
|
||||
file.company_id &&
|
||||
(await downloadOneFile({
|
||||
companyId: file.company_id,
|
||||
fileId: file.id,
|
||||
messageFile,
|
||||
}));
|
||||
};
|
||||
|
||||
const onClickOpen = async () => {
|
||||
openPreview(messageFile);
|
||||
};
|
||||
|
||||
const buildMenu = (e: React.MouseEvent<HTMLElement, MouseEvent>) => {
|
||||
e.stopPropagation();
|
||||
|
||||
const menu = [
|
||||
{
|
||||
type: 'menu',
|
||||
text: Languages.t('scenes.apps.drive.download_button'),
|
||||
onClick: onClickDownload,
|
||||
},
|
||||
];
|
||||
|
||||
if (candidates.length > 0) {
|
||||
const openerName = candidates[0].name || candidates[0].app?.identity.name;
|
||||
|
||||
menu.push({
|
||||
type: 'menu',
|
||||
text: Languages.t(
|
||||
'scenes.apps.drive.viewer.edit_with_button',
|
||||
[openerName],
|
||||
`Edit with ${openerName}`,
|
||||
),
|
||||
onClick: onClickOpen,
|
||||
});
|
||||
}
|
||||
|
||||
MenuManager.openMenu(menu, (window as any).getBoundingClientRect(menuRef.current), null, {
|
||||
margin: 0,
|
||||
});
|
||||
};
|
||||
|
||||
const onClickCancel = (e: React.MouseEvent<HTMLElement, MouseEvent>) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
if (status && isPendingFileStatusSuccess(status)) {
|
||||
if (file.id) deleteOneFile(file.id);
|
||||
} else {
|
||||
cancelUpload(file.id);
|
||||
}
|
||||
|
||||
if (onRemove) onRemove();
|
||||
};
|
||||
|
||||
const onClickRetry = () => {
|
||||
retryUpload(file.id);
|
||||
};
|
||||
|
||||
const setActions = () => {
|
||||
if (actionMenu) {
|
||||
return (
|
||||
<Button
|
||||
ref={node => node && (menuRef.current = node)}
|
||||
shape="circle"
|
||||
icon={<MoreHorizontal size={16} />}
|
||||
onClick={buildMenu}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (deletable && status) {
|
||||
if (isPendingFileStatusError(status)) {
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
shape="circle"
|
||||
icon={<RotateCw size={16} color="var(--error)" />}
|
||||
onClick={onClickRetry}
|
||||
/>
|
||||
<Button
|
||||
shape="circle"
|
||||
icon={<X size={16} color="var(--error)" />}
|
||||
onClick={onClickCancel}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
isPendingFileStatusPending(status) ||
|
||||
isPendingFileStatusPause(status) ||
|
||||
isPendingFileStatusSuccess(status)
|
||||
) {
|
||||
return <Button shape="circle" icon={<X size={16} />} onClick={onClickCancel} />;
|
||||
}
|
||||
|
||||
if (isPendingFileStatusCancel(status)) {
|
||||
return <></>;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return <div className="file-menu">{setActions()}</div>;
|
||||
};
|
||||
|
||||
export default FileActions;
|
||||
@@ -0,0 +1,37 @@
|
||||
import React from 'react';
|
||||
import { Tag, Typography } from 'antd';
|
||||
import { capitalize } from 'lodash';
|
||||
|
||||
import Numbers from 'app/features/global/utils/Numbers';
|
||||
import { DataFileType } from '../types';
|
||||
|
||||
type PropsType = {
|
||||
file: DataFileType;
|
||||
source: string;
|
||||
};
|
||||
|
||||
const setRealFileSize = (file: DataFileType): string => Numbers.humanFileSize(file.size, true);
|
||||
|
||||
const setFileType = (file: DataFileType): string => capitalize(file.type.split('/')[0]);
|
||||
|
||||
const { Text } = Typography;
|
||||
export const FileDetails = ({ file, source }: PropsType) => {
|
||||
let sourceTag: string | null = null;
|
||||
if (source === 'drive') {
|
||||
sourceTag = 'Drive';
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="file-component-details">
|
||||
<Text ellipsis style={{ verticalAlign: 'middle' }}>
|
||||
{file.name}
|
||||
</Text>
|
||||
<Text type="secondary" ellipsis style={{ verticalAlign: 'middle' }}>
|
||||
{!!sourceTag && <Tag color={'orange'}>{sourceTag}</Tag>}
|
||||
{setRealFileSize(file)} {setFileType(file)}
|
||||
</Text>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default FileDetails;
|
||||
@@ -0,0 +1,4 @@
|
||||
export * from './details';
|
||||
export * from './thumbnail';
|
||||
export * from './actions';
|
||||
export * from './progress';
|
||||
@@ -0,0 +1,33 @@
|
||||
import React from 'react';
|
||||
import { DataFileType } from '../types';
|
||||
import { Play } from 'react-feather';
|
||||
|
||||
type PropsType = {
|
||||
file: DataFileType;
|
||||
};
|
||||
|
||||
const LargePreview = ({ file: { thumbnail, type } }: PropsType): JSX.Element => {
|
||||
return (
|
||||
<div
|
||||
className="file-large-preview"
|
||||
style={{
|
||||
backgroundImage: `url(${thumbnail})`,
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
{type === 'video' && (
|
||||
<div className='file-large-preview-play-container'>
|
||||
<Play
|
||||
size={32}
|
||||
color={'white'}
|
||||
strokeWidth={3}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default LargePreview;
|
||||
@@ -0,0 +1,61 @@
|
||||
import React from 'react';
|
||||
import { Progress } from 'antd';
|
||||
|
||||
import { DataFileType } from '../types';
|
||||
import { PendingFileRecoilType } from 'app/features/files/types/file';
|
||||
import {
|
||||
isPendingFileStatusPending,
|
||||
isPendingFileStatusPause,
|
||||
isPendingFileStatusSuccess,
|
||||
isPendingFileStatusError,
|
||||
isPendingFileStatusCancel,
|
||||
} from 'app/features/files/utils/pending-files';
|
||||
|
||||
type PropsType = {
|
||||
file: DataFileType;
|
||||
status?: PendingFileRecoilType['status'];
|
||||
progress?: number;
|
||||
};
|
||||
|
||||
const setStatus = (status: PendingFileRecoilType['status']): 'normal' | 'exception' | 'active' => {
|
||||
switch (status) {
|
||||
case 'error':
|
||||
case 'pause':
|
||||
return 'exception';
|
||||
case 'pending':
|
||||
return 'active';
|
||||
default:
|
||||
return 'normal';
|
||||
}
|
||||
};
|
||||
|
||||
export const FileProgress = ({ status, progress }: PropsType): JSX.Element => {
|
||||
const setProgressStrokeColor = (): string => {
|
||||
if (!status) return '';
|
||||
|
||||
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)';
|
||||
};
|
||||
|
||||
return status && !isPendingFileStatusSuccess(status) && progress != undefined ? (
|
||||
<div className="file-progress-bar-container">
|
||||
<Progress
|
||||
type="line"
|
||||
className="file-progress-bar"
|
||||
percent={progress * 100}
|
||||
showInfo={false}
|
||||
status={setStatus(status)}
|
||||
strokeColor={setProgressStrokeColor()}
|
||||
trailColor="var(--progress-bar-background)"
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="file-progress-bar-container" />
|
||||
);
|
||||
};
|
||||
|
||||
export default FileProgress;
|
||||
@@ -0,0 +1,42 @@
|
||||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { FileText, Film, Headphones, Archive, Link, Image } from 'react-feather';
|
||||
import { DataFileType } from '../types';
|
||||
|
||||
type PropsType = {
|
||||
file: DataFileType;
|
||||
};
|
||||
|
||||
export const FileThumbnail = ({ file }: PropsType): JSX.Element => {
|
||||
const type = file.type;
|
||||
const canHavePreview = ['image', 'video', 'pdf', 'document', 'slides', 'spreadsheet'].includes(
|
||||
type,
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={classNames('file-thumbnail-container', 'small-right-margin')}>
|
||||
{canHavePreview && file.thumbnail && (
|
||||
<div
|
||||
className="ant-image file-thumbnail-component"
|
||||
style={{
|
||||
width: 32,
|
||||
height: 32,
|
||||
backgroundImage: `url(${file.thumbnail})`,
|
||||
backgroundSize: 'cover',
|
||||
backgroundPosition: 'center',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{type === 'image' && !file.thumbnail && <Image size={20} />}
|
||||
{type === 'video' && !file.thumbnail && <Film size={20} />}
|
||||
{['pdf', 'document', 'slides', 'spreadsheet', 'other'].includes(type) && !file.thumbnail && (
|
||||
<FileText size={20} />
|
||||
)}
|
||||
{type === 'audio' && <Headphones size={20} />}
|
||||
{type === 'archive' && <Archive size={20} />}
|
||||
{type === 'link' && <Link size={20} />}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default FileThumbnail;
|
||||
@@ -0,0 +1,9 @@
|
||||
export type DataFileType = {
|
||||
id: string;
|
||||
name: string;
|
||||
thumbnail?: string;
|
||||
thumbnail_ratio: number;
|
||||
company_id?: string;
|
||||
size: number;
|
||||
type: string;
|
||||
};
|
||||
Reference in New Issue
Block a user