Skip to content

Bai 1641 sort files tab by different fields #2122

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 14 commits into from
Apr 11, 2025
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
200 changes: 174 additions & 26 deletions frontend/src/entry/model/Files.tsx
Original file line number Diff line number Diff line change
@@ -1,42 +1,164 @@
import { CalendarMonth, Check, ExpandLess, ExpandMore, Sort, SortByAlpha } from '@mui/icons-material'
import { LoadingButton } from '@mui/lab'
import { Box, Button, Card, Container, LinearProgress, Stack, Typography } from '@mui/material'
import {
Box,
Button,
Card,
Container,
Divider,
LinearProgress,
ListItemIcon,
ListItemText,
Menu,
MenuItem,
Stack,
Typography,
} from '@mui/material'
import Grid2 from '@mui/material/Grid2'
import { styled } from '@mui/material/styles'
import { postFileForModelId } from 'actions/file'
import { useGetModelFiles } from 'actions/model'
import { AxiosProgressEvent } from 'axios'
import { ChangeEvent, useCallback, useMemo, useState } from 'react'
import { ChangeEvent, MouseEvent, useCallback, useMemo, useState } from 'react'
import EmptyBlob from 'src/common/EmptyBlob'
import FileUploadProgressDisplay, { FailedFileUpload, FileUploadProgress } from 'src/common/FileUploadProgressDisplay'
import Loading from 'src/common/Loading'
import Restricted from 'src/common/Restricted'
import FileDownload from 'src/entry/model/releases/FileDownload'
import MessageAlert from 'src/MessageAlert'
import { EntryInterface } from 'types/types'
import { EntryInterface, FileInterface } from 'types/types'
import { sortByCreatedAtDescending } from 'utils/arrayUtils'

type FilesProps = {
model: EntryInterface
}

export const SortingDirection = {
ASC: 'Ascending',
DESC: 'Descending',
} as const

export type SortingDirectionKeys = (typeof SortingDirection)[keyof typeof SortingDirection]

export const menuItemDetails = {}

const Input = styled('input')({
display: 'none',
})

export default function Files({ model }: FilesProps) {
const { entryFiles, isEntryFilesLoading, isEntryFilesError, mutateEntryFiles } = useGetModelFiles(model.id)

const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null)
const menuOpen = Boolean(anchorEl)
const [orderByValue, setOrderByValue] = useState('createdAt')
const [orderByButtonTitle, setOrderByButtonTitle] = useState('Order by')
const [ascOrDesc, setAscOrDesc] = useState<SortingDirectionKeys>(SortingDirection.DESC)
const [currentFileUploadProgress, setCurrentFileUploadProgress] = useState<FileUploadProgress | undefined>(undefined)
const [uploadedFiles, setUploadedFiles] = useState<string[]>([])
const [totalFilesToUpload, setTotalFilesToUpload] = useState(0)
const [isFilesUploading, setIsFilesUploading] = useState(false)
const [failedFileUploads, setFailedFileUploads] = useState<FailedFileUpload[]>([])

const sortFilesByValue = useMemo(
() => (a: FileInterface, b: FileInterface) => {
if (ascOrDesc === SortingDirection.DESC) {
return a[orderByValue] < b[orderByValue] ? -1 : 1
}
return a[orderByValue] > b[orderByValue] ? -1 : 1
},
[ascOrDesc, orderByValue],
)

const checkMenuOption = useCallback(
(menuOption: string) => {
if (menuOption === orderByValue) {
return true
} else {
return false
}
},
[orderByValue],
)

function handleMenuButtonClick(event: MouseEvent<HTMLButtonElement>) {
setAnchorEl(event.currentTarget)
}

const handleMenuButtonClose = () => {
setAnchorEl(null)
}

const orderByMenuListItems = (value, title) => (
<MenuItem
onClick={() => {
setOrderByValue(value)
setOrderByButtonTitle(title)
}}
sx={{ px: 2.5 }}
selected={checkMenuOption(value)}
>
<Grid2 container sx={{ minWidth: '200px' }}>
<Grid2 size={2}>
{checkMenuOption(value) ? (
<Check sx={{ width: '100%' }} color='primary' />
) : (
<Check sx={{ width: '100%' }} color='primary' opacity={0} />
)}
</Grid2>
<Grid2 size={2}>
<ListItemIcon>
{value === 'name' ? <SortByAlpha color='primary' /> : <CalendarMonth color='primary' />}
</ListItemIcon>
</Grid2>
<Grid2 size={8}>
<ListItemText>{title}</ListItemText>
</Grid2>
</Grid2>
</MenuItem>
)

const ascOrDescMenuListItems = (direction) => (
<MenuItem
onClick={() => {
setAscOrDesc(direction)
}}
sx={{ px: 2.5 }}
selected={checkAscOrDesc(direction)}
>
<Grid2 container sx={{ minWidth: '200px' }}>
<Grid2 size={2}>
{checkAscOrDesc(direction) ? (
<Check sx={{ width: '100%' }} color='primary' />
) : (
<Check sx={{ width: '100%' }} color='primary' opacity={0} />
)}
</Grid2>
<Grid2 size={2}>
<ListItemIcon>
{direction === SortingDirection.ASC ? (
<Sort color='primary' />
) : (
<Sort sx={{ transform: 'scaleY(-1)' }} color='primary' />
)}
</ListItemIcon>
</Grid2>
<Grid2 size={8}>
<ListItemText>{direction}</ListItemText>
</Grid2>
</Grid2>
</MenuItem>
)

const checkAscOrDesc = (value: string) => {
return value === ascOrDesc
}

const sortedEntryFiles = useMemo(() => [...entryFiles].sort(sortByCreatedAtDescending), [entryFiles])

const entryFilesList = useMemo(
() =>
entryFiles.length ? (
sortedEntryFiles.map((file) => (
sortedEntryFiles.sort(sortFilesByValue).map((file) => (
<Card key={file._id} sx={{ width: '100%' }}>
<Stack spacing={1} p={2}>
<FileDownload
Expand All @@ -51,7 +173,7 @@ export default function Files({ model }: FilesProps) {
) : (
<EmptyBlob text={`No files found for model ${model.name}`} />
),
[entryFiles.length, model.id, model.name, sortedEntryFiles, mutateEntryFiles],
[entryFiles.length, sortedEntryFiles, sortFilesByValue, model.name, model.id, mutateEntryFiles],
)

const handleAddNewFiles = useCallback(
Expand Down Expand Up @@ -124,26 +246,52 @@ export default function Files({ model }: FilesProps) {
that are no longer needed, and also manually retrigger anti-virus scanning (if anti-virus scanning is
enabled).
</Typography>
<Box display='flex'>
<Box ml='auto'>
<Restricted action='createRelease' fallback={<Button disabled>Add new files</Button>}>
<>
<label htmlFor='add-files-button'>
<LoadingButton loading={isFilesUploading} fullWidth component='span' variant='outlined'>
Add new files
</LoadingButton>
</label>
<Input
multiple
id={'add-files-button'}
type='file'
onInput={handleAddNewFiles}
data-test='uploadFileButton'
/>
</>
</Restricted>
</Box>
</Box>
<Stack width='100%' direction='row' justifyContent='space-between' sx={{ px: 0.5 }}>
<Restricted action='createRelease' fallback={<Button disabled>Add new files</Button>}>
<>
<label htmlFor='add-files-button'>
<LoadingButton loading={isFilesUploading} fullWidth component='span' variant='outlined'>
Add new files
</LoadingButton>
</label>
<Input
multiple
id='add-files-button'
type='file'
onInput={handleAddNewFiles}
data-test='uploadFileButton'
/>
</>
</Restricted>
<Button
onClick={handleMenuButtonClick}
endIcon={anchorEl ? <ExpandLess /> : <ExpandMore />}
sx={{ width: '170px' }}
>
<Stack sx={{ minWidth: '150px' }} direction='row' spacing={2} justifyContent='space-evenly'>
{checkAscOrDesc(SortingDirection.ASC) ? (
<Sort color='primary' />
) : (
<Sort sx={{ transform: 'scaleY(-1)' }} color='primary' />
)}
{orderByButtonTitle}
</Stack>
</Button>
</Stack>
<Menu
open={menuOpen}
slotProps={{ list: { dense: true } }}
anchorEl={anchorEl}
onClose={handleMenuButtonClose}
sx={{ minWidth: '200px' }}
>
{orderByMenuListItems('name', 'Name')}
{orderByMenuListItems('createdAt', 'Date uploaded')}
{orderByMenuListItems('updatedAt', 'Date updated')}
<Divider />
{ascOrDescMenuListItems(SortingDirection.ASC)}
{ascOrDescMenuListItems(SortingDirection.DESC)}
</Menu>
{currentFileUploadProgress && (
<>
<LinearProgress
Expand Down
Loading