all
This commit is contained in:
489
Downloads/Voltaserve/ui/src/components/file/file-toolbar.tsx
Normal file
489
Downloads/Voltaserve/ui/src/components/file/file-toolbar.tsx
Normal 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
|
Reference in New Issue
Block a user