NestJS backend rewrite; migrate client to FluentUI V9
This commit is contained in:
@ -4,23 +4,14 @@ import React, { useEffect, useState } from 'react'
|
||||
import DocumentService from '../services/DocumentService'
|
||||
import { mutate } from 'swr'
|
||||
import FileViewer from './modals/FileViewer'
|
||||
import { ActionIcon, Anchor, Breadcrumbs, Button, Divider, FileButton, Flex, Loader, MantineStyleProp, RingProgress, ScrollAreaAutosize, Stack, Table, Text } from '@mantine/core'
|
||||
import { IconCancel, IconDownload, IconFile, IconFileFilled, IconFilePlus, IconFileUpload, IconFolderFilled, IconX } from '@tabler/icons-react'
|
||||
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 FileItemStyle: MantineStyleProp = {
|
||||
cursor: 'pointer',
|
||||
display: 'flex',
|
||||
width: '100%',
|
||||
flexDirection: 'row',
|
||||
gap: '8px',
|
||||
alignItems: 'center',
|
||||
padding: '8px'
|
||||
}
|
||||
|
||||
const handleSave = async (file: Blob, filename: string) => {
|
||||
const link = document.createElement('a')
|
||||
link.href = window.URL.createObjectURL(file)
|
||||
@ -30,6 +21,27 @@ const handleSave = async (file: Blob, filename: string) => {
|
||||
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)
|
||||
|
||||
@ -45,22 +57,16 @@ function ItemDocument({ doc }: DocumentProps) {
|
||||
}, [shouldFetch, file, doc.name])
|
||||
|
||||
return (
|
||||
<Flex>
|
||||
<ActionIcon
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
if (!isLoading) {
|
||||
setShouldFetch(true)
|
||||
}
|
||||
}}
|
||||
variant='subtle'>
|
||||
{isLoading ?
|
||||
<Loader size='sm' />
|
||||
:
|
||||
<IconDownload />
|
||||
}
|
||||
</ActionIcon>
|
||||
</Flex>
|
||||
<Button icon={isLoading ?
|
||||
<Spinner size='tiny' />
|
||||
:
|
||||
<IconDownload />
|
||||
} appearance='subtle' onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
if (!isLoading) {
|
||||
setShouldFetch(true)
|
||||
}
|
||||
}} />
|
||||
)
|
||||
}
|
||||
|
||||
@ -134,14 +140,32 @@ export default function FolderViewer() {
|
||||
}
|
||||
}
|
||||
|
||||
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 (
|
||||
<Loader />
|
||||
<Spinner />
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<ScrollAreaAutosize w={'100%'} h={'100%'} p={'sm'}>
|
||||
<div style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
padding: '1rem',
|
||||
}}>
|
||||
{fileViewerModal &&
|
||||
<FileViewer
|
||||
open={fileViewerModal}
|
||||
@ -152,51 +176,74 @@ export default function FolderViewer() {
|
||||
/>
|
||||
}
|
||||
|
||||
|
||||
<Stack>
|
||||
<Breadcrumbs>
|
||||
<Anchor
|
||||
onClick={() => {
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
gap: '1rem'
|
||||
}}>
|
||||
<Breadcrumb>
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbButton onClick={() => {
|
||||
setCurrentFolder(null)
|
||||
setBreadcrumbs([])
|
||||
}}
|
||||
>
|
||||
Главная
|
||||
</Anchor>
|
||||
}}>Главная</BreadcrumbButton>
|
||||
</BreadcrumbItem>
|
||||
|
||||
{breadcrumbs.map((breadcrumb, index) => (
|
||||
<Anchor
|
||||
key={breadcrumb.id}
|
||||
onClick={() => handleBreadcrumbClick(index)}
|
||||
>
|
||||
{breadcrumb.name}
|
||||
</Anchor>
|
||||
<>
|
||||
<BreadcrumbDivider />
|
||||
<BreadcrumbItem key={breadcrumb.id}>
|
||||
<BreadcrumbButton icon={<FolderRegular />} onClick={() => {
|
||||
handleBreadcrumbClick(index)
|
||||
}}>{breadcrumb.name}</BreadcrumbButton>
|
||||
</BreadcrumbItem>
|
||||
</>
|
||||
|
||||
))}
|
||||
</Breadcrumbs>
|
||||
</Breadcrumb>
|
||||
|
||||
{currentFolder &&
|
||||
<Flex direction='column' gap='sm'>
|
||||
<Flex direction='column' gap='sm' p='sm' style={{
|
||||
<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',
|
||||
}}>
|
||||
<Flex gap='sm'>
|
||||
<FileButton multiple onChange={handleFileInput}>
|
||||
{(props) => <Button variant='filled' leftSection={isUploading ? <Loader /> : <IconFilePlus />} {...props}>Добавить</Button>}
|
||||
</FileButton>
|
||||
<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
|
||||
variant='filled'
|
||||
leftSection={isUploading ? <RingProgress sections={[{ value: uploadProgress, color: 'blue' }]} /> : <IconFileUpload />}
|
||||
appearance='primary'
|
||||
icon={<IconFileUpload />}
|
||||
onClick={uploadFiles}
|
||||
disabled={isUploading}
|
||||
>
|
||||
Загрузить все
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant='outline'
|
||||
leftSection={<IconCancel />}
|
||||
appearance='outline'
|
||||
icon={<IconCancel />}
|
||||
onClick={() => {
|
||||
setFilesToUpload([])
|
||||
}}
|
||||
@ -205,88 +252,114 @@ export default function FolderViewer() {
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
</Flex>
|
||||
</div>
|
||||
|
||||
{isUploading &&
|
||||
<Field validationMessage={"Загрузка файлов..."} validationState='none'>
|
||||
<ProgressBar value={uploadProgress} />
|
||||
</Field>
|
||||
}
|
||||
|
||||
<Divider />
|
||||
|
||||
{filesToUpload.length > 0 &&
|
||||
<Flex direction='column'>
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column'
|
||||
}}>
|
||||
{filesToUpload.map((file, index) => (
|
||||
<Flex key={index} p='8px'>
|
||||
<Flex gap='sm' direction='row' align='center'>
|
||||
<IconFile />
|
||||
<Text>{file.name}</Text>
|
||||
</Flex>
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
}} key={index}>
|
||||
<Button appearance='transparent' icon={<DocumentColor />}>
|
||||
{file.name}
|
||||
</Button>
|
||||
|
||||
<ActionIcon onClick={() => {
|
||||
<Button style={{ marginLeft: 'auto' }} appearance='subtle' icon={<IconX />} onClick={() => {
|
||||
setFilesToUpload(prev => {
|
||||
return prev.filter((_, i) => i != index)
|
||||
})
|
||||
}} ml='auto' variant='subtle'>
|
||||
<IconX />
|
||||
</ActionIcon>
|
||||
</Flex>
|
||||
}} />
|
||||
</div>
|
||||
))}
|
||||
</Flex>
|
||||
</div>
|
||||
}
|
||||
</Flex>
|
||||
</Flex>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<Table
|
||||
onDragOver={handleDragOver}
|
||||
onDragLeave={handleDragLeave}
|
||||
onDrop={handleDrop}
|
||||
bg={dragOver ? 'rgba(0, 0, 0, 0.1)' : 'inherit'}
|
||||
highlightOnHover>
|
||||
<Table.Thead>
|
||||
<Table.Tr>
|
||||
<Table.Th>Название</Table.Th>
|
||||
<Table.Th p={0}>Дата создания</Table.Th>
|
||||
<Table.Th p={0}></Table.Th>
|
||||
</Table.Tr>
|
||||
</Table.Thead>
|
||||
|
||||
<Table.Tbody>
|
||||
{currentFolder ? (
|
||||
documents?.map((doc: IDocument, index: number) => (
|
||||
<Table.Tr key={doc.id} onClick={() => handleDocumentClick(index)} style={{ cursor: 'pointer' }}>
|
||||
<Table.Td p={0}>
|
||||
<Flex style={FileItemStyle}>
|
||||
<IconFileFilled />
|
||||
{doc.name}
|
||||
</Flex>
|
||||
</Table.Td>
|
||||
<Table.Td p={0}>
|
||||
{new Date(doc.create_date).toLocaleDateString()}
|
||||
</Table.Td>
|
||||
<Table.Td p={0}>
|
||||
<ItemDocument
|
||||
doc={doc}
|
||||
/>
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
))
|
||||
) : (
|
||||
folders?.map((folder: IDocumentFolder) => (
|
||||
<Table.Tr key={folder.id} onClick={() => handleFolderClick(folder)} style={{ cursor: 'pointer' }}>
|
||||
<Table.Td p={0}>
|
||||
<Flex style={FileItemStyle}>
|
||||
<IconFolderFilled />
|
||||
{folder.name}
|
||||
</Flex>
|
||||
</Table.Td>
|
||||
<Table.Td p={0} align='left'>
|
||||
{new Date(folder.create_date).toLocaleDateString()}
|
||||
</Table.Td>
|
||||
<Table.Td p={0}>
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
))
|
||||
)}
|
||||
</Table.Tbody>
|
||||
</Table>
|
||||
</Stack>
|
||||
</ScrollAreaAutosize>
|
||||
<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>
|
||||
)
|
||||
}
|
Reference in New Issue
Block a user