NestJS backend rewrite; migrate client to FluentUI V9

This commit is contained in:
2025-09-18 15:48:08 +09:00
parent 32ff36a12c
commit 34529cea68
62 changed files with 5642 additions and 3679 deletions

View File

@ -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>
)
}