490 lines
15 KiB
TypeScript
490 lines
15 KiB
TypeScript
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
|