* feat: add file manager module - Complete file manager implementation with UI/UX - Add drive management functionality - Add file upload/download with progress tracking - Add stamp integration and handling - Add bulk operations and context menus Co-authored-by: Roland Seres <roland.seres90@gmail.com> Co-authored-by: nidishk <nidishkrishnan45@gmail.com>
This commit is contained in:
@@ -0,0 +1,71 @@
|
||||
.fm-drive-item-info {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
flex-direction: column;
|
||||
color: rgb(31, 41, 55);
|
||||
font-weight: 500;
|
||||
transition: background-color 0.3s;
|
||||
|
||||
.fm-drive-item-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: rgb(237, 129, 49);
|
||||
font-weight: 700;
|
||||
}
|
||||
}
|
||||
|
||||
.fm-drive-item-header {
|
||||
display: flex;
|
||||
font-weight: 700;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.fm-drive-item-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.fm-drive-item-container {
|
||||
display: flex;
|
||||
position: relative;
|
||||
justify-content: space-between;
|
||||
padding: 16px;
|
||||
padding-left: 40px;
|
||||
|
||||
&:hover {
|
||||
background-color: rgb(209, 209, 209);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&.fm-drive-item-container-selected {
|
||||
background-color: rgb(209, 209, 209);
|
||||
}
|
||||
}
|
||||
|
||||
.fm-drive-item-capacity {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.fm-drive-item-actions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
align-items: end;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.fm-pointer {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.fm-drive-item-context-menu {
|
||||
position: fixed;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.fm-disabled { opacity: 0.5; pointer-events: none; }
|
||||
@@ -0,0 +1,218 @@
|
||||
import { ReactElement, useState, useContext, useEffect, useRef, useMemo } from 'react'
|
||||
import { createPortal } from 'react-dom'
|
||||
import Drive from 'remixicon-react/HardDrive2LineIcon'
|
||||
import DriveFill from 'remixicon-react/HardDrive2FillIcon'
|
||||
import MoreFill from 'remixicon-react/MoreFillIcon'
|
||||
import './DriveItem.scss'
|
||||
import { ProgressBar } from '../../ProgressBar/ProgressBar'
|
||||
import { ContextMenu } from '../../ContextMenu/ContextMenu'
|
||||
import { useContextMenu } from '../../../hooks/useContextMenu'
|
||||
import { Button } from '../../Button/Button'
|
||||
import { DestroyDriveModal } from '../../DestroyDriveModal/DestroyDriveModal'
|
||||
import { UpgradeDriveModal } from '../../UpgradeDriveModal/UpgradeDriveModal'
|
||||
import { ViewType } from '../../../constants/transfers'
|
||||
import { useView } from '../../../../../pages/filemanager/ViewContext'
|
||||
import { Context as FMContext } from '../../../../../providers/FileManager'
|
||||
import { PostageBatch } from '@ethersphere/bee-js'
|
||||
import { DriveInfo } from '@solarpunkltd/file-manager-lib'
|
||||
import { calculateStampCapacityMetrics, handleDestroyDrive } from '../../../utils/bee'
|
||||
import { Context as SettingsContext } from '../../../../../providers/Settings'
|
||||
|
||||
interface DriveItemProps {
|
||||
drive: DriveInfo
|
||||
stamp: PostageBatch
|
||||
isSelected: boolean
|
||||
setErrorMessage?: (error: string) => void
|
||||
}
|
||||
|
||||
export function DriveItem({ drive, stamp, isSelected, setErrorMessage }: DriveItemProps): ReactElement {
|
||||
const { fm, setShowError, refreshStamp } = useContext(FMContext)
|
||||
const { beeApi } = useContext(SettingsContext)
|
||||
|
||||
const [isHovered, setIsHovered] = useState(false)
|
||||
const [isDestroyDriveModalOpen, setIsDestroyDriveModalOpen] = useState(false)
|
||||
const [isUpgradeDriveModalOpen, setIsUpgradeDriveModalOpen] = useState(false)
|
||||
const isMountedRef = useRef(true)
|
||||
const [isUpgrading, setIsUpgrading] = useState(false)
|
||||
const [actualStamp, setActualStamp] = useState<PostageBatch>(stamp)
|
||||
|
||||
const { showContext, pos, contextRef, setPos, setShowContext } = useContextMenu<HTMLDivElement>()
|
||||
|
||||
const { setView, setActualItemView } = useView()
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
isMountedRef.current = false
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
setActualStamp(stamp)
|
||||
}, [stamp])
|
||||
|
||||
function handleMenuClick(e: React.MouseEvent) {
|
||||
setShowContext(true)
|
||||
setPos({ x: e.clientX, y: e.clientY })
|
||||
}
|
||||
|
||||
function handleDestroyDriveClick() {
|
||||
setShowContext(false)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const id = drive.id.toString()
|
||||
const batchId = stamp.batchID.toString()
|
||||
|
||||
const onStart = (e: Event) => {
|
||||
const { driveId } = (e as CustomEvent).detail || {}
|
||||
|
||||
if (driveId === id) {
|
||||
setIsUpgrading(true)
|
||||
}
|
||||
}
|
||||
|
||||
const onEnd = async (e: Event) => {
|
||||
const { driveId, success, error } = (e as CustomEvent).detail || {}
|
||||
|
||||
if (!success) {
|
||||
if (error) {
|
||||
setErrorMessage?.(error)
|
||||
}
|
||||
|
||||
setShowError(true)
|
||||
}
|
||||
|
||||
if (driveId === id) {
|
||||
setIsUpgrading(false)
|
||||
|
||||
const upgradedStamp = await refreshStamp(batchId)
|
||||
|
||||
if (!isMountedRef.current) return
|
||||
|
||||
if (upgradedStamp) {
|
||||
setActualStamp(upgradedStamp)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('fm:drive-upgrade-start', onStart as EventListener)
|
||||
window.addEventListener('fm:drive-upgrade-end', onEnd as EventListener)
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('fm:drive-upgrade-start', onStart as EventListener)
|
||||
window.removeEventListener('fm:drive-upgrade-end', onEnd as EventListener)
|
||||
}
|
||||
}, [drive.id, setShowError, setErrorMessage, stamp.batchID, refreshStamp])
|
||||
|
||||
const { capacityPct, usedSize, totalSize } = useMemo(
|
||||
() => calculateStampCapacityMetrics(actualStamp, drive),
|
||||
[actualStamp, drive],
|
||||
)
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`fm-drive-item-container${isSelected ? ' fm-drive-item-container-selected' : ''}`}
|
||||
onClick={() => {
|
||||
setView(ViewType.File)
|
||||
setActualItemView?.(drive.name)
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="fm-drive-item-info"
|
||||
onMouseEnter={() => setIsHovered(true)}
|
||||
onMouseLeave={() => setIsHovered(false)}
|
||||
>
|
||||
<div className="fm-drive-item-header">
|
||||
<div className="fm-drive-item-icon">{isHovered ? <DriveFill size="16px" /> : <Drive size="16px" />}</div>
|
||||
<div>{drive.name}</div>
|
||||
</div>
|
||||
<div className="fm-drive-item-content">
|
||||
<div className="fm-drive-item-capacity">
|
||||
Capacity <ProgressBar value={capacityPct} width="64px" /> {usedSize} / {totalSize}
|
||||
</div>
|
||||
<div className="fm-drive-item-capacity">
|
||||
Expiry date: {actualStamp.duration.toEndDate().toLocaleDateString()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="fm-drive-item-actions">
|
||||
<MoreFill
|
||||
size="13"
|
||||
className={`fm-pointer${isUpgrading ? ' fm-disabled' : ''}`}
|
||||
onClick={!isUpgrading ? handleMenuClick : undefined}
|
||||
aria-disabled={isUpgrading ? 'true' : 'false'}
|
||||
/>
|
||||
{showContext &&
|
||||
createPortal(
|
||||
<div
|
||||
ref={contextRef}
|
||||
className="fm-drive-item-context-menu"
|
||||
style={{
|
||||
top: pos.y,
|
||||
left: pos.x,
|
||||
}}
|
||||
>
|
||||
<ContextMenu>
|
||||
<div
|
||||
className="fm-context-item red"
|
||||
onClick={() => {
|
||||
handleDestroyDriveClick()
|
||||
setIsDestroyDriveModalOpen(true)
|
||||
}}
|
||||
>
|
||||
Destroy entire drive
|
||||
</div>
|
||||
</ContextMenu>
|
||||
</div>,
|
||||
|
||||
document.body,
|
||||
)}
|
||||
<Button
|
||||
label="Upgrade"
|
||||
variant="primary"
|
||||
size="small"
|
||||
disabled={isUpgrading}
|
||||
onClick={() => setIsUpgradeDriveModalOpen(true)}
|
||||
/>
|
||||
</div>
|
||||
{isUpgradeDriveModalOpen && (
|
||||
<UpgradeDriveModal
|
||||
stamp={actualStamp}
|
||||
drive={drive}
|
||||
onCancelClick={() => setIsUpgradeDriveModalOpen(false)}
|
||||
setErrorMessage={setErrorMessage}
|
||||
/>
|
||||
)}
|
||||
|
||||
{isUpgrading && (
|
||||
<div className="fm-drive-item-creating-overlay" aria-live="polite">
|
||||
<div className="fm-mini-spinner" />
|
||||
<span>Upgrading drive…</span>
|
||||
</div>
|
||||
)}
|
||||
{isDestroyDriveModalOpen && (
|
||||
<DestroyDriveModal
|
||||
drive={drive}
|
||||
onCancelClick={() => setIsDestroyDriveModalOpen(false)}
|
||||
doDestroy={async () => {
|
||||
setIsDestroyDriveModalOpen(false)
|
||||
|
||||
await handleDestroyDrive(
|
||||
beeApi,
|
||||
fm,
|
||||
drive,
|
||||
() => {
|
||||
setIsDestroyDriveModalOpen(false)
|
||||
},
|
||||
e => {
|
||||
setIsDestroyDriveModalOpen(false)
|
||||
setErrorMessage?.(`Error destroying drive: ${drive.name}: ${e}`)
|
||||
setShowError(true)
|
||||
},
|
||||
)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
import { ReactElement, useState, useContext } from 'react'
|
||||
import { createPortal } from 'react-dom'
|
||||
import Drive from 'remixicon-react/HardDrive2LineIcon'
|
||||
import DriveFill from 'remixicon-react/HardDrive2FillIcon'
|
||||
import MoreFill from 'remixicon-react/MoreFillIcon'
|
||||
import { ContextMenu } from '../../ContextMenu/ContextMenu'
|
||||
import { useContextMenu } from '../../../hooks/useContextMenu'
|
||||
import { DriveInfo } from '@solarpunkltd/file-manager-lib'
|
||||
import { Context as FMContext } from '../../../../../providers/FileManager'
|
||||
import { handleForgetDrive } from '../../../utils/bee'
|
||||
import { ConfirmModal } from '../../ConfirmModal/ConfirmModal'
|
||||
import './DriveItem.scss'
|
||||
|
||||
interface Props {
|
||||
drive: DriveInfo
|
||||
onForgot?: () => Promise<void> | void
|
||||
setErrorMessage?: (error: string) => void
|
||||
}
|
||||
|
||||
export function ExpiredDriveItem({ drive, onForgot, setErrorMessage }: Props): ReactElement {
|
||||
const { fm, setShowError } = useContext(FMContext)
|
||||
const [isHovered, setIsHovered] = useState(false)
|
||||
const [showForgetConfirm, setShowForgetConfirm] = useState(false)
|
||||
const { showContext, pos, contextRef, setPos, setShowContext } = useContextMenu<HTMLDivElement>()
|
||||
|
||||
function handleMenuClick(e: React.MouseEvent) {
|
||||
setShowContext(true)
|
||||
setPos({ x: e.clientX, y: e.clientY })
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="fm-drive-item-container fm-expired"
|
||||
onMouseEnter={() => setIsHovered(true)}
|
||||
onMouseLeave={() => setIsHovered(false)}
|
||||
>
|
||||
<div className="fm-drive-item-info">
|
||||
<div className="fm-drive-item-header">
|
||||
<div className="fm-drive-item-icon">{isHovered ? <DriveFill size="16px" /> : <Drive size="16px" />}</div>
|
||||
<div>{drive.name}</div>
|
||||
</div>
|
||||
<div className="fm-drive-item-content">
|
||||
<div className="fm-drive-item-capacity">Stamp expired — files unavailable</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="fm-drive-item-actions">
|
||||
<MoreFill
|
||||
size="13"
|
||||
className="fm-pointer"
|
||||
onClick={handleMenuClick}
|
||||
aria-label={`More actions for ${drive.name}`}
|
||||
/>
|
||||
|
||||
{showContext &&
|
||||
createPortal(
|
||||
<div
|
||||
ref={contextRef}
|
||||
className="fm-drive-item-context-menu"
|
||||
style={{ top: pos.y, left: pos.x }}
|
||||
onClick={e => e.stopPropagation()}
|
||||
>
|
||||
<ContextMenu>
|
||||
<div
|
||||
className="fm-context-item red"
|
||||
onClick={() => {
|
||||
setShowContext(false)
|
||||
setShowForgetConfirm(true)
|
||||
}}
|
||||
>
|
||||
Forget drive
|
||||
</div>
|
||||
</ContextMenu>
|
||||
</div>,
|
||||
document.body,
|
||||
)}
|
||||
</div>
|
||||
|
||||
{showForgetConfirm && (
|
||||
<ConfirmModal
|
||||
title="Forget drive?"
|
||||
message={
|
||||
<>
|
||||
This will remove metadata for the drive with expired stamp <b>Drive Name: {drive.name}</b>{' '}
|
||||
<b>Batch Id: {`${drive.batchId.toString().slice(0, 4)}...${drive.batchId.toString().slice(-4)}`}</b>
|
||||
</>
|
||||
}
|
||||
confirmLabel="Forget drive"
|
||||
cancelLabel="Keep"
|
||||
onCancel={() => setShowForgetConfirm(false)}
|
||||
onConfirm={async () => {
|
||||
if (!fm) return
|
||||
|
||||
await handleForgetDrive(
|
||||
fm,
|
||||
drive,
|
||||
async () => {
|
||||
setShowForgetConfirm(false)
|
||||
await onForgot?.()
|
||||
},
|
||||
() => {
|
||||
setShowForgetConfirm(false)
|
||||
setErrorMessage?.(`Failed to forget drive ${drive.name}`)
|
||||
setShowError(true)
|
||||
},
|
||||
)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
.fm-sidebar {
|
||||
min-width: 370px;
|
||||
box-sizing: border-box;
|
||||
height: calc(100vh - 120px);
|
||||
background-color: rgb(237, 237, 237);
|
||||
border-right: 1px solid rgb(146, 146, 146);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.fm-sidebar-content {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.fm-sidebar-item {
|
||||
padding: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
color: rgb(31, 41, 55);
|
||||
font-weight: 700;
|
||||
transition: background-color 0.3s;
|
||||
|
||||
.fm-sidebar-item-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: rgb(237, 129, 49);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: rgb(209, 209, 209);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&.fm-trash-item {
|
||||
padding: 8px;
|
||||
padding-left: 36px;
|
||||
}
|
||||
}
|
||||
|
||||
.fm-sidebar-drive-creation {
|
||||
padding: 12px;
|
||||
border-top: 1px solid rgb(146, 146, 146);
|
||||
}
|
||||
|
||||
.fm-drive-items-container {
|
||||
opacity: 0;
|
||||
max-height: 0;
|
||||
overflow: hidden;
|
||||
transition: opacity 0.3s, max-height 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
will-change: opacity, max-height;
|
||||
|
||||
&.fm-drive-items-container-open {
|
||||
opacity: 1;
|
||||
max-height: 1000px;
|
||||
}
|
||||
}
|
||||
|
||||
.fm-trash-item {
|
||||
color: rgb(31, 41, 55);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.fm-drive-item-creating {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.fm-drive-item-creating .fm-drive-item-info,
|
||||
.fm-drive-item-creating .fm-drive-item-actions {
|
||||
filter: blur(2px);
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.fm-drive-item-creating-overlay {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 10;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
background: rgba(255, 255, 255, 0.55);
|
||||
-webkit-backdrop-filter: blur(2px);
|
||||
backdrop-filter: blur(2px);
|
||||
font-weight: 600;
|
||||
color: #111827;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.fm-mini-spinner {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
border-radius: 50%;
|
||||
border: 2px solid rgba(0, 0, 0, 0.2);
|
||||
border-top-color: rgba(0, 0, 0, 0.7);
|
||||
animation: fmSpin 0.8s linear infinite;
|
||||
}
|
||||
|
||||
.fm-drive-item-container.fm-expired {
|
||||
background: rgba(220, 38, 38, 0.08);
|
||||
border: 1px solid rgba(220, 38, 38, 0.2);
|
||||
}
|
||||
|
||||
.fm-drive-item-container.fm-expired .fm-drive-item-capacity {
|
||||
color: #dc2626;
|
||||
}
|
||||
|
||||
@keyframes fmSpin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
@@ -0,0 +1,269 @@
|
||||
import { ReactElement, useContext, useEffect, useState } from 'react'
|
||||
|
||||
import './Sidebar.scss'
|
||||
import Add from 'remixicon-react/AddLineIcon'
|
||||
import Folder from 'remixicon-react/Folder3LineIcon'
|
||||
import FolderFill from 'remixicon-react/Folder3FillIcon'
|
||||
import ArrowRight from 'remixicon-react/ArrowRightSLineIcon'
|
||||
import ArrowDown from 'remixicon-react/ArrowDownSLineIcon'
|
||||
import Delete from 'remixicon-react/DeleteBin6LineIcon'
|
||||
import DeleteFill from 'remixicon-react/DeleteBin6FillIcon'
|
||||
import History from 'remixicon-react/HistoryLineIcon'
|
||||
import HistoryFill from 'remixicon-react/HistoryFillIcon'
|
||||
import { DriveItem } from './DriveItem/DriveItem'
|
||||
import { ExpiredDriveItem } from './DriveItem/ExpiredDriveItem'
|
||||
import { CreateDriveModal } from '../CreateDriveModal/CreateDriveModal'
|
||||
import { ViewType } from '../../constants/transfers'
|
||||
import { PostageBatch } from '@ethersphere/bee-js'
|
||||
import { Context as SettingsContext } from '../../../../providers/Settings'
|
||||
import { useView } from '../../../../pages/filemanager/ViewContext'
|
||||
import { Context as FMContext } from '../../../../providers/FileManager'
|
||||
import { getUsableStamps } from '../../utils/bee'
|
||||
import { DriveInfo } from '@solarpunkltd/file-manager-lib'
|
||||
|
||||
interface SidebarProps {
|
||||
loading: boolean
|
||||
errorMessage?: string
|
||||
setErrorMessage?: (error: string) => void
|
||||
}
|
||||
|
||||
export function Sidebar({ setErrorMessage, loading }: SidebarProps): ReactElement {
|
||||
const [hovered, setHovered] = useState<string | null>(null)
|
||||
const [isMyDrivesOpen, setIsMyDriveOpen] = useState(true)
|
||||
const [isTrashOpen, setIsTrashOpen] = useState(false)
|
||||
const [isCreateDriveOpen, setIsCreateDriveOpen] = useState(false)
|
||||
const [usableStamps, setUsableStamps] = useState<PostageBatch[]>([])
|
||||
const [isDriveCreationInProgress, setIsDriveCreationInProgress] = useState(false)
|
||||
const [isExpiredOpen, setIsExpiredOpen] = useState(false)
|
||||
|
||||
const { beeApi } = useContext(SettingsContext)
|
||||
const { setView, view } = useView()
|
||||
const {
|
||||
fm,
|
||||
currentDrive,
|
||||
currentStamp,
|
||||
drives,
|
||||
expiredDrives,
|
||||
setCurrentDrive,
|
||||
setCurrentStamp,
|
||||
setShowError,
|
||||
syncDrives,
|
||||
} = useContext(FMContext)
|
||||
|
||||
useEffect(() => {
|
||||
let isMounted = true
|
||||
|
||||
const getStamps = async () => {
|
||||
const stamps = await getUsableStamps(beeApi)
|
||||
|
||||
if (isMounted) {
|
||||
setUsableStamps([...stamps])
|
||||
}
|
||||
}
|
||||
|
||||
if (beeApi) {
|
||||
getStamps()
|
||||
}
|
||||
|
||||
return () => {
|
||||
isMounted = false
|
||||
}
|
||||
}, [beeApi, drives])
|
||||
|
||||
useEffect(() => {
|
||||
if (!fm || drives.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!currentDrive) {
|
||||
const firstDrive = drives[0]
|
||||
setCurrentDrive(firstDrive)
|
||||
setView(ViewType.File)
|
||||
}
|
||||
|
||||
if (currentDrive && !currentStamp && usableStamps.length > 0) {
|
||||
const correspondingStamp = usableStamps.find(s => s.batchID.toString() === currentDrive.batchId.toString())
|
||||
|
||||
if (correspondingStamp) {
|
||||
setCurrentStamp(correspondingStamp)
|
||||
}
|
||||
}
|
||||
}, [fm, drives, currentDrive, currentStamp, usableStamps, setCurrentDrive, setCurrentStamp, setView])
|
||||
|
||||
const isCurrent = (di: DriveInfo) => currentDrive?.id.toString() === di.id.toString()
|
||||
|
||||
return (
|
||||
<div className="fm-sidebar">
|
||||
<div className="fm-sidebar-content">
|
||||
{!loading && (
|
||||
<div className="fm-sidebar-item" onClick={() => setIsCreateDriveOpen(true)}>
|
||||
<div className="fm-sidebar-item-icon">
|
||||
<Add size="16px" />
|
||||
</div>
|
||||
<div>Create new drive</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isCreateDriveOpen && (
|
||||
<CreateDriveModal
|
||||
onCancelClick={() => setIsCreateDriveOpen(false)}
|
||||
onDriveCreated={() => {
|
||||
setIsCreateDriveOpen(false)
|
||||
setIsDriveCreationInProgress(false)
|
||||
}}
|
||||
onCreationStarted={() => setIsDriveCreationInProgress(true)}
|
||||
onCreationError={(name: string) => {
|
||||
setIsDriveCreationInProgress(false)
|
||||
setErrorMessage?.(`Error creating drive: ${name}`)
|
||||
setShowError(true)
|
||||
|
||||
return
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div
|
||||
className="fm-sidebar-item"
|
||||
onMouseEnter={() => setHovered('my-drives')}
|
||||
onMouseLeave={() => setHovered(null)}
|
||||
onClick={() => setIsMyDriveOpen(!isMyDrivesOpen)}
|
||||
>
|
||||
<div className="fm-sidebar-item-icon">
|
||||
{isMyDrivesOpen ? <ArrowDown size="16px" /> : <ArrowRight size="16px" />}
|
||||
</div>
|
||||
<div className="fm-sidebar-item-icon" style={{ opacity: hovered === 'my-drives' ? 1 : 1 }}>
|
||||
{hovered === 'my-drives' ? <FolderFill size="16px" /> : <Folder size="16px" />}
|
||||
</div>
|
||||
<div>My Drives</div>
|
||||
</div>
|
||||
|
||||
{isMyDrivesOpen && isDriveCreationInProgress && (
|
||||
<div className="fm-drive-item-container fm-drive-item-creating" aria-live="polite">
|
||||
<div className="fm-drive-item-info">
|
||||
<div className="fm-drive-item-header">
|
||||
<div className="fm-drive-item-icon">
|
||||
<Folder size="16px" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="fm-drive-item-content">
|
||||
<div className="fm-drive-item-capacity">Initializing drive metadata</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="fm-drive-item-actions" />
|
||||
<div className="fm-drive-item-creating-overlay">
|
||||
<div className="fm-mini-spinner" />
|
||||
<span>Please wait…</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{isMyDrivesOpen &&
|
||||
drives.map(d => {
|
||||
const isSelected = isCurrent(d) && view === ViewType.File
|
||||
const localStamp = usableStamps.find(s => s.batchID.toString() === d.batchId.toString() && !d.isAdmin)
|
||||
const stamp = isSelected && currentStamp ? currentStamp : localStamp
|
||||
|
||||
return (
|
||||
stamp && (
|
||||
<div
|
||||
key={d.id.toString()}
|
||||
onClick={() => {
|
||||
setCurrentDrive(d)
|
||||
setCurrentStamp(stamp)
|
||||
setView(ViewType.File)
|
||||
}}
|
||||
>
|
||||
<DriveItem drive={d} stamp={stamp} isSelected={isSelected} setErrorMessage={setErrorMessage} />
|
||||
</div>
|
||||
)
|
||||
)
|
||||
})}
|
||||
|
||||
{expiredDrives.length > 0 && (
|
||||
<>
|
||||
<div
|
||||
className="fm-sidebar-item"
|
||||
onMouseEnter={() => setHovered('expired')}
|
||||
onMouseLeave={() => setHovered(null)}
|
||||
onClick={() => setIsExpiredOpen(prev => !prev)}
|
||||
>
|
||||
<div className="fm-sidebar-item-icon">
|
||||
{isExpiredOpen ? <ArrowDown size="16px" /> : <ArrowRight size="16px" />}
|
||||
</div>
|
||||
<div className="fm-sidebar-item-icon">
|
||||
{hovered === 'expired' ? <HistoryFill size="16px" /> : <History size="16px" />}
|
||||
</div>
|
||||
<div>Expired drives</div>
|
||||
</div>
|
||||
|
||||
{isExpiredOpen && (
|
||||
<div className="fm-drive-items-container fm-drive-items-container-open">
|
||||
{expiredDrives.map(d => (
|
||||
<div
|
||||
key={`${d.id.toString()}-expired`}
|
||||
onClick={() => {
|
||||
setCurrentDrive(d)
|
||||
setView(ViewType.Expired)
|
||||
}}
|
||||
>
|
||||
<ExpiredDriveItem
|
||||
drive={d}
|
||||
onForgot={async () => {
|
||||
await syncDrives()
|
||||
setCurrentDrive(drives.length > 0 ? drives[0] : undefined)
|
||||
setView(ViewType.File)
|
||||
}}
|
||||
setErrorMessage={setErrorMessage}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
<div
|
||||
className="fm-sidebar-item"
|
||||
onMouseEnter={() => setHovered(ViewType.Trash)}
|
||||
onMouseLeave={() => setHovered(null)}
|
||||
onClick={() => setIsTrashOpen(!isTrashOpen)}
|
||||
>
|
||||
<div className="fm-sidebar-item-icon">
|
||||
{isTrashOpen ? <ArrowDown size="16px" /> : <ArrowRight size="16px" />}
|
||||
</div>
|
||||
<div className="fm-sidebar-item-icon">
|
||||
{hovered === ViewType.Trash ? <DeleteFill size="16px" /> : <Delete size="16px" />}
|
||||
</div>
|
||||
<div>Trash</div>
|
||||
</div>
|
||||
|
||||
{isTrashOpen && (
|
||||
<div className="fm-drive-items-container fm-drive-items-container-open">
|
||||
{drives.map(d => {
|
||||
const selected = isCurrent(d) && view === ViewType.Trash
|
||||
const stamp = usableStamps.find(s => s.batchID.toString() === d.batchId.toString() && !d.isAdmin)
|
||||
|
||||
return (
|
||||
<div
|
||||
key={`${d.id.toString()}-trash`}
|
||||
className={`fm-sidebar-item fm-trash-item${selected ? ' is-selected' : ''}`}
|
||||
onClick={() => {
|
||||
setCurrentDrive(d)
|
||||
setCurrentStamp(stamp)
|
||||
setView(ViewType.Trash)
|
||||
}}
|
||||
title={`${d.name} Trash`}
|
||||
>
|
||||
{d.name} Trash
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{isDriveCreationInProgress && (
|
||||
<div className="fm-sidebar-drive-creation">Creating drive, please do not reload</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user