365 lines
15 KiB
TypeScript
365 lines
15 KiB
TypeScript
import { useDocuments, useDownload, useFolders } from '../hooks/swrHooks'
|
||
import { IDocument, IDocumentFolder } from '../interfaces/documents'
|
||
import React, { useEffect, useState } from 'react'
|
||
import DocumentService from '../services/DocumentService'
|
||
import { mutate } from 'swr'
|
||
import FileViewer from './modals/FileViewer'
|
||
import { IconCancel, IconDownload, IconFileUpload, IconX } from '@tabler/icons-react'
|
||
import { Breadcrumb, BreadcrumbButton, BreadcrumbDivider, BreadcrumbItem, Button, createTableColumn, DataGrid, DataGridBody, DataGridCell, DataGridHeader, DataGridHeaderCell, DataGridRow, Divider, Field, ProgressBar, Spinner, TableCellLayout } from '@fluentui/react-components'
|
||
import { DocumentAdd20Regular, DocumentColor, DocumentRegular, DocumentTextColor, FolderRegular, ImageColor, TableColor } from '@fluentui/react-icons'
|
||
|
||
interface DocumentProps {
|
||
doc: IDocument;
|
||
}
|
||
|
||
const handleSave = async (file: Blob, filename: string) => {
|
||
const link = document.createElement('a')
|
||
link.href = window.URL.createObjectURL(file)
|
||
link.download = filename
|
||
link.click()
|
||
link.remove()
|
||
window.URL.revokeObjectURL(link.href)
|
||
}
|
||
|
||
function getFileExtension(filename: string): string {
|
||
return filename.split('.').pop()?.toLowerCase() || '';
|
||
}
|
||
|
||
function handleDocFormatIcon(docName: string) {
|
||
const ext = getFileExtension(docName);
|
||
|
||
switch (ext) {
|
||
case 'docx':
|
||
return <DocumentTextColor />
|
||
case 'pdf':
|
||
return <DocumentTextColor color='red' />
|
||
case 'xlsx':
|
||
return <TableColor />
|
||
case 'jpg':
|
||
return <ImageColor />
|
||
default:
|
||
return <DocumentRegular />
|
||
}
|
||
}
|
||
|
||
function ItemDocument({ doc }: DocumentProps) {
|
||
const [shouldFetch, setShouldFetch] = useState(false)
|
||
|
||
const { file, isLoading } = useDownload(shouldFetch ? doc?.document_folder_id : null, shouldFetch ? doc?.id : null)
|
||
|
||
useEffect(() => {
|
||
if (shouldFetch) {
|
||
if (file) {
|
||
handleSave(file, doc.name)
|
||
setShouldFetch(false)
|
||
}
|
||
}
|
||
}, [shouldFetch, file, doc.name])
|
||
|
||
return (
|
||
<Button icon={isLoading ?
|
||
<Spinner size='tiny' />
|
||
:
|
||
<IconDownload />
|
||
} appearance='subtle' onClick={(e) => {
|
||
e.stopPropagation()
|
||
if (!isLoading) {
|
||
setShouldFetch(true)
|
||
}
|
||
}} />
|
||
)
|
||
}
|
||
|
||
export default function FolderViewer() {
|
||
const [currentFolder, setCurrentFolder] = useState<IDocumentFolder | null>(null)
|
||
const [breadcrumbs, setBreadcrumbs] = useState<IDocumentFolder[]>([])
|
||
const { folders, isLoading: foldersLoading } = useFolders()
|
||
const { documents, isLoading: documentsLoading } = useDocuments(currentFolder?.id)
|
||
const [uploadProgress, setUploadProgress] = useState(0)
|
||
const [isUploading, setIsUploading] = useState(false)
|
||
const [fileViewerModal, setFileViewerModal] = useState(false)
|
||
const [currentFileNo, setCurrentFileNo] = useState<number>(-1)
|
||
|
||
const [dragOver, setDragOver] = useState(false)
|
||
const [filesToUpload, setFilesToUpload] = useState<File[]>([])
|
||
|
||
const handleFolderClick = (folder: IDocumentFolder) => {
|
||
setCurrentFolder(folder)
|
||
setBreadcrumbs((prev) => [...prev, folder])
|
||
}
|
||
|
||
const handleDocumentClick = async (index: number) => {
|
||
setCurrentFileNo(index)
|
||
setFileViewerModal(true)
|
||
}
|
||
|
||
const handleBreadcrumbClick = (index: number) => {
|
||
const newBreadcrumbs = breadcrumbs.slice(0, index + 1);
|
||
setBreadcrumbs(newBreadcrumbs)
|
||
setCurrentFolder(newBreadcrumbs[newBreadcrumbs.length - 1])
|
||
}
|
||
|
||
const handleDragOver = (e: React.DragEvent) => {
|
||
e.preventDefault()
|
||
setDragOver(true)
|
||
}
|
||
|
||
const handleDragLeave = () => {
|
||
setDragOver(false)
|
||
}
|
||
|
||
const handleDrop = (e: React.DragEvent) => {
|
||
e.preventDefault()
|
||
setDragOver(false)
|
||
const files = Array.from(e.dataTransfer.files)
|
||
setFilesToUpload((prevFiles) => [...prevFiles, ...files])
|
||
}
|
||
|
||
const handleFileInput = (files: File[] | null) => {
|
||
if (files !== null) {
|
||
setFilesToUpload((prevFiles) => [...prevFiles, ...files])
|
||
}
|
||
}
|
||
|
||
const uploadFiles = async () => {
|
||
setIsUploading(true)
|
||
if (filesToUpload.length > 0 && currentFolder && currentFolder.id) {
|
||
const formData = new FormData()
|
||
for (const file of filesToUpload) {
|
||
formData.append('files', file)
|
||
}
|
||
try {
|
||
await DocumentService.uploadFiles(currentFolder.id, formData, setUploadProgress);
|
||
setIsUploading(false);
|
||
setFilesToUpload([]);
|
||
mutate(`/info/documents/${currentFolder.id}`);
|
||
} catch (error) {
|
||
console.error(error);
|
||
setIsUploading(false);
|
||
}
|
||
}
|
||
}
|
||
|
||
const fileInputRef = React.useRef<HTMLInputElement | null>(null);
|
||
|
||
const handleClick = () => {
|
||
fileInputRef.current?.click();
|
||
};
|
||
|
||
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||
if (e.target.files) {
|
||
// do what Mantine's onChange did
|
||
console.log("Selected files:", Array.from(e.target.files));
|
||
handleFileInput(Array.from(e.target.files))
|
||
}
|
||
};
|
||
|
||
if (foldersLoading || documentsLoading) {
|
||
return (
|
||
<Spinner />
|
||
)
|
||
}
|
||
|
||
return (
|
||
<div style={{
|
||
width: '100%',
|
||
height: '100%',
|
||
padding: '1rem',
|
||
}}>
|
||
{fileViewerModal &&
|
||
<FileViewer
|
||
open={fileViewerModal}
|
||
setOpen={setFileViewerModal}
|
||
currentFileNo={currentFileNo}
|
||
setCurrentFileNo={setCurrentFileNo}
|
||
docs={documents}
|
||
/>
|
||
}
|
||
|
||
<div style={{
|
||
display: 'flex',
|
||
flexDirection: 'column',
|
||
height: '100%',
|
||
width: '100%',
|
||
gap: '1rem'
|
||
}}>
|
||
<Breadcrumb>
|
||
<BreadcrumbItem>
|
||
<BreadcrumbButton onClick={() => {
|
||
setCurrentFolder(null)
|
||
setBreadcrumbs([])
|
||
}}>Главная</BreadcrumbButton>
|
||
</BreadcrumbItem>
|
||
|
||
{breadcrumbs.map((breadcrumb, index) => (
|
||
<>
|
||
<BreadcrumbDivider />
|
||
<BreadcrumbItem key={breadcrumb.id}>
|
||
<BreadcrumbButton icon={<FolderRegular />} onClick={() => {
|
||
handleBreadcrumbClick(index)
|
||
}}>{breadcrumb.name}</BreadcrumbButton>
|
||
</BreadcrumbItem>
|
||
</>
|
||
|
||
))}
|
||
</Breadcrumb>
|
||
|
||
{currentFolder &&
|
||
<div style={{
|
||
display: 'flex',
|
||
flexDirection: 'column',
|
||
gap: '1rem'
|
||
}}>
|
||
<div style={{
|
||
display: 'flex',
|
||
flexDirection: 'column',
|
||
gap: '1rem',
|
||
padding: '1rem',
|
||
border: filesToUpload.length > 0 ? '1px dashed gray' : 'none',
|
||
borderRadius: '8px',
|
||
}}>
|
||
<div style={{ display: 'flex', gap: '1rem' }}>
|
||
<input
|
||
type="file"
|
||
multiple
|
||
ref={fileInputRef}
|
||
style={{ display: "none" }}
|
||
onChange={handleFileChange}
|
||
/>
|
||
<Button appearance="primary" icon={<DocumentAdd20Regular />} onClick={handleClick}>
|
||
Добавить
|
||
</Button>
|
||
|
||
{filesToUpload.length > 0 &&
|
||
<>
|
||
<Button
|
||
appearance='primary'
|
||
icon={<IconFileUpload />}
|
||
onClick={uploadFiles}
|
||
disabled={isUploading}
|
||
>
|
||
Загрузить все
|
||
</Button>
|
||
|
||
<Button
|
||
appearance='outline'
|
||
icon={<IconCancel />}
|
||
onClick={() => {
|
||
setFilesToUpload([])
|
||
}}
|
||
>
|
||
Отмена
|
||
</Button>
|
||
</>
|
||
}
|
||
</div>
|
||
|
||
{isUploading &&
|
||
<Field validationMessage={"Загрузка файлов..."} validationState='none'>
|
||
<ProgressBar value={uploadProgress} />
|
||
</Field>
|
||
}
|
||
|
||
<Divider />
|
||
|
||
{filesToUpload.length > 0 &&
|
||
<div style={{
|
||
display: 'flex',
|
||
flexDirection: 'column'
|
||
}}>
|
||
{filesToUpload.map((file, index) => (
|
||
<div style={{
|
||
display: 'flex',
|
||
}} key={index}>
|
||
<Button appearance='transparent' icon={<DocumentColor />}>
|
||
{file.name}
|
||
</Button>
|
||
|
||
<Button style={{ marginLeft: 'auto' }} appearance='subtle' icon={<IconX />} onClick={() => {
|
||
setFilesToUpload(prev => {
|
||
return prev.filter((_, i) => i != index)
|
||
})
|
||
}} />
|
||
</div>
|
||
))}
|
||
</div>
|
||
}
|
||
</div>
|
||
</div>
|
||
}
|
||
|
||
<div style={{
|
||
width: '100%',
|
||
overflow: 'auto'
|
||
}}>
|
||
<DataGrid
|
||
onDragOver={handleDragOver}
|
||
onDragLeave={handleDragLeave}
|
||
onDrop={handleDrop}
|
||
style={{ backgroundColor: dragOver ? 'rgba(0, 0, 0, 0.1)' : 'inherit' }}
|
||
items={currentFolder
|
||
? (documents ?? []).map((doc: IDocument) => ({ kind: "doc", data: doc }))
|
||
: (folders ?? []).map((folder: IDocumentFolder) => ({ kind: "folder", data: folder }))}
|
||
columns={[
|
||
createTableColumn({
|
||
columnId: "name",
|
||
renderHeaderCell: () => "Название",
|
||
renderCell: (item) => (
|
||
<TableCellLayout truncate media={item.kind === "doc" ? handleDocFormatIcon(item.data.name) : <FolderRegular />}>
|
||
{item.data.name}
|
||
</TableCellLayout>
|
||
),
|
||
}),
|
||
createTableColumn({
|
||
columnId: "date",
|
||
renderHeaderCell: () => "Дата создания",
|
||
renderCell: (item) =>
|
||
new Date(item.data.create_date).toLocaleDateString(),
|
||
}),
|
||
createTableColumn({
|
||
columnId: "actions",
|
||
renderHeaderCell: () => "",
|
||
renderCell: (item) => {
|
||
if (item.kind === "doc") {
|
||
// replace with your <ItemDocument doc={doc} />
|
||
return <ItemDocument doc={item.data} />;
|
||
}
|
||
return null;
|
||
},
|
||
}),
|
||
]}
|
||
focusMode="cell"
|
||
resizableColumns
|
||
getRowId={(item) => item.data.id}
|
||
>
|
||
<DataGridHeader>
|
||
<DataGridRow>
|
||
{({ renderHeaderCell }) => (
|
||
<DataGridHeaderCell>{renderHeaderCell()}</DataGridHeaderCell>
|
||
)}
|
||
</DataGridRow>
|
||
</DataGridHeader>
|
||
<DataGridBody>
|
||
{({ item, rowId }: { item: { kind: string, data: any }, rowId: any }) => (
|
||
<DataGridRow
|
||
key={rowId}
|
||
style={{ cursor: "pointer" }}
|
||
onClick={() => {
|
||
if (item.kind === "doc") {
|
||
const index = documents?.findIndex((d: any) => d.id === item.data.id) ?? -1;
|
||
handleDocumentClick(index);
|
||
} else {
|
||
handleFolderClick(item.data as IDocumentFolder);
|
||
}
|
||
}}
|
||
>
|
||
{({ renderCell }) => <DataGridCell>{renderCell(item)}</DataGridCell>}
|
||
</DataGridRow>
|
||
)}
|
||
</DataGridBody>
|
||
</DataGrid>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)
|
||
} |