This commit is contained in:
2024-04-21 14:42:52 +02:00
parent 4b69674ede
commit 8a25f53c99
10700 changed files with 55767 additions and 14201 deletions

View File

@ -0,0 +1,489 @@
import { ChangeEvent, ReactElement, useCallback, useRef, useState } from 'react'
import { useParams } from 'react-router-dom'
import {
Button,
ButtonGroup,
IconButton,
Menu,
MenuButton,
MenuDivider,
MenuItem,
MenuList,
Portal,
Spacer,
Slider,
SliderTrack,
SliderFilledTrack,
SliderThumb,
} from '@chakra-ui/react'
import { useSWRConfig } from 'swr'
import cx from 'classnames'
import FileAPI, { List, SortBy, SortOrder } from '@/client/api/file'
import { ltEditorPermission, ltOwnerPermission } from '@/client/api/permission'
import downloadFile from '@/helpers/download-file'
import mapFileList from '@/helpers/map-file-list'
import useFileListSearchParams from '@/hooks/use-file-list-params'
import {
IconAdd,
IconFileCopy,
IconMoreVert,
IconDownload,
IconEdit,
IconArrowTopRight,
IconGroup,
IconDelete,
IconUpload,
IconRefresh,
IconGridView,
IconArrowDownward,
IconArrowUpward,
IconCheck,
IconSelectCheckBox,
IconCheckBoxOutlineBlank,
IconLibraryAddCheck,
IconExpandMore,
IconClose,
IconList,
} from '@/lib'
import { uploadAdded, UploadDecorator } from '@/store/entities/uploads'
import { useAppDispatch, useAppSelector } from '@/store/hook'
import {
sortByUpdated,
viewTypeToggled,
selectionModeToggled,
sortOrderToggled,
} from '@/store/ui/files'
import {
copyModalDidOpen,
createModalDidOpen,
deleteModalDidOpen,
iconScaleUpdated,
moveModalDidOpen,
renameModalDidOpen,
selectionUpdated,
sharingModalDidOpen,
} from '@/store/ui/files'
import { uploadsDrawerOpened } from '@/store/ui/uploads-drawer'
import { FileViewType } from '@/types/file'
const ICON_SCALE_SLIDER_STEP = 0.25
const ICON_SCALE_SLIDER_MIN = 1
const ICON_SCALE_SLIDER_MAX = ICON_SCALE_SLIDER_STEP * 9
export type FileToolbarProps = {
list?: List
}
const FileToolbar = ({ list }: FileToolbarProps) => {
const dispatch = useAppDispatch()
const { mutate } = useSWRConfig()
const { id, fileId } = useParams()
const [isRefreshing, setIsRefreshing] = useState(false)
const fileCount = useAppSelector(
(state) => state.entities.files.list?.data.length,
)
const selectionCount = useAppSelector(
(state) => state.ui.files.selection.length,
)
const singleFile = useAppSelector((state) =>
state.ui.files.selection.length === 1
? list?.data.find((e) => e.id === state.ui.files.selection[0])
: null,
)
const iconScale = useAppSelector((state) => state.ui.files.iconScale)
const viewType = useAppSelector((state) => state.ui.files.viewType)
const sortBy = useAppSelector((state) => state.ui.files.sortBy)
const sortOrder = useAppSelector((state) => state.ui.files.sortOrder)
const hasOwnerPermission = useAppSelector(
(state) =>
list?.data.findIndex(
(f) =>
state.ui.files.selection.findIndex(
(s) => f.id === s && ltOwnerPermission(f.permission),
) !== -1,
) === -1,
)
const hasEditorPermission = useAppSelector(
(state) =>
list?.data.findIndex(
(f) =>
state.ui.files.selection.findIndex(
(s) => f.id === s && ltEditorPermission(f.permission),
) !== -1,
) === -1,
)
const isSelectionMode = useAppSelector(
(state) => state.ui.files.isSelectionMode,
)
const fileUploadInput = useRef<HTMLInputElement>(null)
const folderUploadInput = useRef<HTMLInputElement>(null)
const fileListSearchParams = useFileListSearchParams()
const { data: folder } = FileAPI.useGetById(fileId)
const stackClassName = cx('flex', 'flex-row', 'gap-0.5')
const handleFileChange = useCallback(
async (event: ChangeEvent<HTMLInputElement>) => {
const files = mapFileList(event.target.files)
if (files.length === 0) {
return
}
for (const file of files) {
dispatch(
uploadAdded(
new UploadDecorator({
workspaceId: id!,
parentId: fileId!,
file,
}).value,
),
)
}
dispatch(uploadsDrawerOpened())
if (fileUploadInput && fileUploadInput.current) {
fileUploadInput.current.value = ''
}
if (folderUploadInput && folderUploadInput.current) {
folderUploadInput.current.value = ''
}
},
[id, fileId, dispatch],
)
const handleIconScaleChange = useCallback(
(value: number) => {
dispatch(iconScaleUpdated(value))
},
[dispatch],
)
const handleRefresh = useCallback(async () => {
setIsRefreshing(true)
dispatch(selectionUpdated([]))
await mutate<List>(`/files/${fileId}/list?${fileListSearchParams}`)
setIsRefreshing(false)
}, [fileId, fileListSearchParams, mutate, dispatch])
const handleSortByChange = useCallback(
(value: SortBy) => {
dispatch(sortByUpdated(value))
},
[dispatch],
)
const handleSortOrderToggle = useCallback(() => {
dispatch(sortOrderToggled())
}, [dispatch])
const handleViewTypeToggle = useCallback(() => {
dispatch(viewTypeToggled())
}, [dispatch])
const handleSelectAllClick = useCallback(() => {
if (list?.data) {
dispatch(selectionUpdated(list?.data.map((f) => f.id)))
}
}, [list?.data, dispatch])
const handleToggleSelection = useCallback(() => {
dispatch(selectionUpdated([]))
dispatch(selectionModeToggled())
}, [dispatch])
const getSortByIcon = useCallback(
(value: SortBy): ReactElement => {
if (value === sortBy) {
return <IconCheck />
} else {
return <IconCheck className={cx('text-transparent')} />
}
},
[sortBy],
)
const getSortOrderIcon = useCallback(() => {
if (sortOrder === SortOrder.Asc) {
return <IconArrowDownward />
} else if (sortOrder === SortOrder.Desc) {
return <IconArrowUpward />
}
}, [sortOrder])
const getViewTypeIcon = useCallback(() => {
if (viewType === FileViewType.Grid) {
return <IconList />
} else if (viewType === FileViewType.List) {
return <IconGridView />
}
}, [viewType])
return (
<>
<div className={stackClassName}>
<ButtonGroup isAttached>
<Menu>
<MenuButton
as={Button}
variant="solid"
colorScheme="blue"
leftIcon={<IconExpandMore />}
isDisabled={
!folder || ltEditorPermission(folder.permission) || !list
}
>
Upload
</MenuButton>
<Portal>
<MenuList>
<MenuItem
icon={<IconAdd />}
onClick={() => fileUploadInput?.current?.click()}
>
Upload Files
</MenuItem>
<MenuItem
icon={<IconUpload />}
onClick={() => folderUploadInput?.current?.click()}
>
Upload Folder
</MenuItem>
</MenuList>
</Portal>
</Menu>
<Button
variant="outline"
colorScheme="blue"
leftIcon={<IconAdd />}
isDisabled={
!folder || ltEditorPermission(folder.permission) || !list
}
onClick={() => dispatch(createModalDidOpen())}
>
New Folder
</Button>
</ButtonGroup>
<div className={stackClassName}>
{selectionCount > 0 && hasOwnerPermission && (
<Button
leftIcon={<IconGroup />}
onClick={() => dispatch(sharingModalDidOpen())}
>
Sharing
</Button>
)}
{singleFile?.type === 'file' && (
<Button
leftIcon={<IconDownload />}
onClick={() => downloadFile(singleFile)}
>
Download
</Button>
)}
{selectionCount > 0 && hasOwnerPermission && (
<Button
leftIcon={<IconDelete />}
className={cx('text-red-500')}
onClick={() => dispatch(deleteModalDidOpen())}
>
Delete
</Button>
)}
{selectionCount > 0 ? (
<Menu>
<MenuButton
as={IconButton}
icon={<IconMoreVert />}
variant="solid"
aria-label=""
isDisabled={!list}
/>
<Portal>
<MenuList zIndex="dropdown">
<MenuItem
icon={<IconGroup />}
isDisabled={selectionCount === 0 || !hasOwnerPermission}
onClick={() => dispatch(sharingModalDidOpen())}
>
Sharing
</MenuItem>
<MenuItem
icon={<IconDownload />}
isDisabled={singleFile?.type !== 'file'}
onClick={() => {
if (singleFile) {
downloadFile(singleFile)
}
}}
>
Download
</MenuItem>
<MenuDivider />
<MenuItem
icon={<IconDelete />}
className={cx('text-red-500')}
isDisabled={selectionCount === 0 || !hasOwnerPermission}
onClick={() => dispatch(deleteModalDidOpen())}
>
Delete
</MenuItem>
<MenuItem
icon={<IconEdit />}
isDisabled={selectionCount !== 1 || !hasEditorPermission}
onClick={() => dispatch(renameModalDidOpen())}
>
Rename
</MenuItem>
<MenuItem
icon={<IconArrowTopRight />}
isDisabled={selectionCount === 0 || !hasEditorPermission}
onClick={() => dispatch(moveModalDidOpen())}
>
Move
</MenuItem>
<MenuItem
icon={<IconFileCopy />}
isDisabled={selectionCount === 0 || !hasEditorPermission}
onClick={() => dispatch(copyModalDidOpen())}
>
Copy
</MenuItem>
{isSelectionMode ? (
<>
<MenuDivider />
<MenuItem
icon={<IconSelectCheckBox />}
onClick={handleSelectAllClick}
>
Select All
</MenuItem>
<MenuItem
icon={<IconCheckBoxOutlineBlank />}
onClick={() => dispatch(selectionUpdated([]))}
>
Unselect All
</MenuItem>
</>
) : null}
</MenuList>
</Portal>
</Menu>
) : null}
</div>
{fileCount ? (
<IconButton
icon={isSelectionMode ? <IconClose /> : <IconLibraryAddCheck />}
isDisabled={!list}
variant="solid"
aria-label=""
onClick={handleToggleSelection}
/>
) : null}
<IconButton
icon={<IconRefresh />}
isLoading={isRefreshing}
isDisabled={!list}
variant="solid"
aria-label=""
onClick={handleRefresh}
/>
<Spacer />
<div className={cx('flex', 'flex-row', 'gap-2.5')}>
<Slider
className={cx('w-[120px]')}
value={iconScale}
min={ICON_SCALE_SLIDER_MIN}
max={ICON_SCALE_SLIDER_MAX}
step={ICON_SCALE_SLIDER_STEP}
isDisabled={!list}
onChange={handleIconScaleChange}
>
<SliderTrack>
<div className={cx('relative')} />
<SliderFilledTrack />
</SliderTrack>
<SliderThumb boxSize={8}>
<IconGridView className={cx('text-gray-500')} />
</SliderThumb>
</Slider>
<div className={stackClassName}>
<IconButton
icon={getSortOrderIcon()}
variant="solid"
aria-label=""
isDisabled={!list}
onClick={handleSortOrderToggle}
/>
<IconButton
icon={getViewTypeIcon()}
variant="solid"
aria-label=""
isDisabled={!list}
onClick={handleViewTypeToggle}
/>
<Menu>
<MenuButton
as={IconButton}
icon={<IconMoreVert />}
variant="solid"
aria-label=""
isDisabled={!list}
/>
<Portal>
<MenuList zIndex="dropdown">
<MenuItem
icon={getSortByIcon(SortBy.Name)}
onClick={() => handleSortByChange(SortBy.Name)}
>
Sort By Name
</MenuItem>
<MenuItem
icon={getSortByIcon(SortBy.Kind)}
onClick={() => handleSortByChange(SortBy.Kind)}
>
Sort By Kind
</MenuItem>
<MenuItem
icon={getSortByIcon(SortBy.Size)}
onClick={() => handleSortByChange(SortBy.Size)}
>
Sort By Size
</MenuItem>
<MenuItem
icon={getSortByIcon(SortBy.DateCreated)}
onClick={() => handleSortByChange(SortBy.DateCreated)}
>
Sort By Date Created
</MenuItem>
<MenuItem
icon={getSortByIcon(SortBy.DateModified)}
onClick={() => handleSortByChange(SortBy.DateModified)}
>
Sort By Date Modified
</MenuItem>
</MenuList>
</Portal>
</Menu>
</div>
</div>
</div>
<input
ref={fileUploadInput}
className={cx('hidden')}
type="file"
multiple
onChange={handleFileChange}
/>
<input
ref={folderUploadInput}
className={cx('hidden')}
type="file"
/* @ts-expect-error intentionaly ignored */
directory=""
webkitdirectory=""
mozdirectory=""
onChange={handleFileChange}
/>
</>
)
}
export default FileToolbar