feat: sync and update with all changes from fork (#720)
* feat: sync and update with all changes from fork * refactor: extract clipboard copy logic into custom hook * fix: correct spelling of DEFAULT_REFRESH_FREQUENCY_MS in Stamps and WalletBalance providers * refactor(ui-tests): replace fixed sleeps with condition-based waits * fix: handle null values for size and granteeCount in infoGroups * fix(lint): add newline at end of file in useClipboardCopy hook * fix(ui-tests): page.goto URL * refactor: update import paths for useClipboardCopy --------- Co-authored-by: Ferenc Sárai <sarai.ferenc@gmail.com>
This commit is contained in:
@@ -1,52 +1,69 @@
|
||||
import { ReactElement, useEffect, useLayoutEffect, useRef, useState, useContext, useMemo, useCallback } from 'react'
|
||||
import './FileBrowser.scss'
|
||||
import { FileBrowserHeader } from './FileBrowserHeader/FileBrowserHeader'
|
||||
import { FileBrowserContent } from './FileBrowserContent/FileBrowserContent'
|
||||
import { useContextMenu } from '../../hooks/useContextMenu'
|
||||
import { NotificationBar } from '../NotificationBar/NotificationBar'
|
||||
import { FileAction, FileTransferType, TransferStatus, ViewType } from '../../constants/transfers'
|
||||
import { FileProgressNotification } from '../FileProgressNotification/FileProgressNotification'
|
||||
import { PostageBatch } from '@ethersphere/bee-js'
|
||||
import { DriveInfo } from '@solarpunkltd/file-manager-lib'
|
||||
import React, {
|
||||
ReactElement,
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useLayoutEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react'
|
||||
|
||||
import { useSearch } from '../../../../pages/filemanager/SearchContext'
|
||||
import { useView } from '../../../../pages/filemanager/ViewContext'
|
||||
import { Context as FMContext } from '../../../../providers/FileManager'
|
||||
import { useTransfers } from '../../hooks/useTransfers'
|
||||
import { useSearch } from '../../../../pages/filemanager/SearchContext'
|
||||
import { useFileFiltering } from '../../hooks/useFileFiltering'
|
||||
import { useDragAndDrop } from '../../hooks/useDragAndDrop'
|
||||
import { useBulkActions } from '../../hooks/useBulkActions'
|
||||
import { SortKey, SortDir, useSorting } from '../../hooks/useSorting'
|
||||
|
||||
import { Point, Dir, safeSetState, getFileId } from '../../utils/common'
|
||||
import { computeContextMenuPosition } from '../../utils/ui'
|
||||
import { FileBrowserTopBar } from './FileBrowserTopBar/FileBrowserTopBar'
|
||||
import { handleDestroyAndForgetDrive } from '../../utils/bee'
|
||||
import { Context as SettingsContext } from '../../../../providers/Settings'
|
||||
import { ErrorModal } from '../ErrorModal/ErrorModal'
|
||||
import { FileBrowserModals } from './FileBrowserModals'
|
||||
import { FileBrowserContextMenu } from './FileBrowserMenu/FileBrowserContextMenu'
|
||||
import { DriveInfo, FileInfo } from '@solarpunkltd/file-manager-lib'
|
||||
import { FileAction, FileTransferType, TransferStatus, ViewType } from '../../constants/transfers'
|
||||
import { useBulkActions } from '../../hooks/useBulkActions'
|
||||
import { useContextMenu } from '../../hooks/useContextMenu'
|
||||
import { useDragAndDrop } from '../../hooks/useDragAndDrop'
|
||||
import { useFileFiltering } from '../../hooks/useFileFiltering'
|
||||
import { SortDir, SortKey, useSorting } from '../../hooks/useSorting'
|
||||
import { useTransfers } from '../../hooks/useTransfers'
|
||||
import { handleDestroyAndForgetDrive } from '../../utils/bee'
|
||||
import { Dir, getFileId, Point, safeSetState } from '../../utils/common'
|
||||
import { computeContextMenuPosition } from '../../utils/ui'
|
||||
import { ProgressDestroyModal } from '../DestroyDriveModal/DestroyDriveModal'
|
||||
import { ErrorModal } from '../ErrorModal/ErrorModal'
|
||||
import { FileProgressNotification } from '../FileProgressNotification/FileProgressNotification'
|
||||
import { NotificationBar } from '../NotificationBar/NotificationBar'
|
||||
|
||||
const renderDestroySpinner = (
|
||||
isDestroying: boolean,
|
||||
isProgressModalOpen: boolean,
|
||||
currentDrive: DriveInfo | undefined,
|
||||
setter: () => void,
|
||||
) => {
|
||||
import { FileBrowserContent } from './FileBrowserContent/FileBrowserContent'
|
||||
import { FileBrowserHeader } from './FileBrowserHeader/FileBrowserHeader'
|
||||
import { FileBrowserContextMenu } from './FileBrowserMenu/FileBrowserContextMenu'
|
||||
import { FileBrowserTopBar } from './FileBrowserTopBar/FileBrowserTopBar'
|
||||
import { FileBrowserModals } from './FileBrowserModals'
|
||||
|
||||
import './FileBrowser.scss'
|
||||
|
||||
function DestroyProgressModal({
|
||||
isDestroying,
|
||||
isProgressModalOpen,
|
||||
currentDrive,
|
||||
onMinimize,
|
||||
}: {
|
||||
isDestroying: boolean
|
||||
isProgressModalOpen: boolean
|
||||
currentDrive?: DriveInfo
|
||||
onMinimize: () => void
|
||||
}) {
|
||||
if (isProgressModalOpen && isDestroying && currentDrive) {
|
||||
return <ProgressDestroyModal drive={currentDrive} onMinimize={setter} />
|
||||
return <ProgressDestroyModal drive={currentDrive} onMinimize={onMinimize} />
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
const showDestroyModal = (isDestroying: boolean, setter: () => void) => {
|
||||
function DestroyingOverlay({ isDestroying, onClick }: { isDestroying: boolean; onClick: () => void }) {
|
||||
if (!isDestroying) return null
|
||||
|
||||
return (
|
||||
<div className="fm-refresh-overlay" aria-busy="true" aria-live="polite">
|
||||
<div
|
||||
className="fm-refresh-content"
|
||||
onClick={setter}
|
||||
onClick={onClick}
|
||||
style={{ cursor: 'pointer' }}
|
||||
title="Click to show progress modal"
|
||||
>
|
||||
@@ -57,6 +74,22 @@ const showDestroyModal = (isDestroying: boolean, setter: () => void) => {
|
||||
)
|
||||
}
|
||||
|
||||
function ErrorModalBlock({
|
||||
showError,
|
||||
label,
|
||||
onOk,
|
||||
}: {
|
||||
showError: boolean
|
||||
label: string
|
||||
onOk: () => void
|
||||
}): ReactElement | null {
|
||||
if (!showError) {
|
||||
return null
|
||||
}
|
||||
|
||||
return <ErrorModal label={label} onClick={onOk} />
|
||||
}
|
||||
|
||||
const extractFilesFromClipboardEvent = (e: React.ClipboardEvent): File[] => {
|
||||
const out: File[] = []
|
||||
const items = e.clipboardData?.items ?? []
|
||||
@@ -78,6 +111,67 @@ interface FileBrowserProps {
|
||||
setErrorMessage?: (error: string) => void
|
||||
}
|
||||
|
||||
type FileBrowserContextMenuBlockProps = {
|
||||
showContext: boolean
|
||||
contextRef: React.RefObject<HTMLDivElement | null>
|
||||
safePos: { x: number; y: number }
|
||||
dropDir: Dir
|
||||
drives: DriveInfo[]
|
||||
view: ViewType
|
||||
bulk: ReturnType<typeof useBulkActions>
|
||||
adminStamp: PostageBatch | undefined
|
||||
doRefresh: () => void
|
||||
onContextUploadFile: () => void
|
||||
setConfirmBulkRestore: (b: boolean) => void
|
||||
setShowBulkDeleteModal: (b: boolean) => void
|
||||
setShowDestroyDriveModal: (b: boolean) => void
|
||||
}
|
||||
|
||||
function FileBrowserContextMenuBlock({
|
||||
showContext,
|
||||
contextRef,
|
||||
safePos,
|
||||
dropDir,
|
||||
drives,
|
||||
view,
|
||||
bulk,
|
||||
adminStamp,
|
||||
doRefresh,
|
||||
onContextUploadFile,
|
||||
setConfirmBulkRestore,
|
||||
setShowBulkDeleteModal,
|
||||
setShowDestroyDriveModal,
|
||||
}: FileBrowserContextMenuBlockProps): ReactElement | null {
|
||||
if (!showContext) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={contextRef}
|
||||
className="fm-file-browser-context-menu fm-context-menu"
|
||||
style={{ top: safePos.y, left: safePos.x }}
|
||||
data-drop={dropDir}
|
||||
onMouseDown={e => e.stopPropagation()}
|
||||
onClick={e => e.stopPropagation()}
|
||||
>
|
||||
<FileBrowserContextMenu
|
||||
drives={drives}
|
||||
view={view}
|
||||
selectedFilesCount={bulk.selectedFiles.length}
|
||||
onRefresh={doRefresh}
|
||||
enableRefresh={Boolean(adminStamp)}
|
||||
onUploadFile={onContextUploadFile}
|
||||
onBulkDownload={() => bulk.bulkDownload(bulk.selectedFiles)}
|
||||
onBulkRestore={() => setConfirmBulkRestore(true)}
|
||||
onBulkDelete={() => setShowBulkDeleteModal(true)}
|
||||
onBulkDestroy={() => setShowDestroyDriveModal(true)}
|
||||
onBulkForget={() => bulk.bulkForget(bulk.selectedFiles)}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function FileBrowser({ errorMessage, setErrorMessage }: FileBrowserProps): ReactElement {
|
||||
const { showContext, pos, contextRef, handleContextMenu, handleCloseContext } = useContextMenu<HTMLDivElement>()
|
||||
const { view, setActualItemView } = useView()
|
||||
@@ -120,11 +214,14 @@ export function FileBrowser({ errorMessage, setErrorMessage }: FileBrowserProps)
|
||||
const q = query.trim().toLowerCase()
|
||||
const isSearchMode = q.length > 0
|
||||
|
||||
const getDriveName = (fi: FileInfo): string => {
|
||||
const match = drives.find(d => d.id.toString() === fi.driveId.toString())
|
||||
const getDriveName = useCallback(
|
||||
(driveId: string): string => {
|
||||
const match = drives.find(d => d.id.toString() === driveId)
|
||||
|
||||
return match?.name ?? ''
|
||||
}
|
||||
return match?.name ?? ''
|
||||
},
|
||||
[drives],
|
||||
)
|
||||
|
||||
const openTopbarMenu = (anchorEl: HTMLElement) => {
|
||||
const r = anchorEl.getBoundingClientRect()
|
||||
@@ -133,9 +230,7 @@ export function FileBrowser({ errorMessage, setErrorMessage }: FileBrowserProps)
|
||||
const minY = (bodyRect?.top ?? 0) + 8
|
||||
const clickY = Math.max(Math.round(r.bottom + 6), minY)
|
||||
const fakeEvt = {
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
preventDefault: () => {},
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
stopPropagation: () => {},
|
||||
clientX: clickX,
|
||||
clientY: clickY,
|
||||
@@ -379,7 +474,6 @@ export function FileBrowser({ errorMessage, setErrorMessage }: FileBrowserProps)
|
||||
const showDragOverlay = isDragging && Boolean(currentDrive)
|
||||
const fileCountText = bulk.selectedFiles.length === 1 ? 'file' : 'files'
|
||||
|
||||
// Memoize onBulk object to prevent FileBrowserContent rerenders
|
||||
const onBulk = useMemo(
|
||||
() => ({
|
||||
download: () => bulk.bulkDownload(bulk.selectedFiles),
|
||||
@@ -445,42 +539,32 @@ export function FileBrowser({ errorMessage, setErrorMessage }: FileBrowserProps)
|
||||
onBulk={onBulk}
|
||||
setErrorMessage={setErrorMessage}
|
||||
/>
|
||||
{showError && (
|
||||
<ErrorModal
|
||||
label={errorMessage || 'An error occurred'}
|
||||
onClick={() => {
|
||||
setShowError(false)
|
||||
setErrorMessage?.('')
|
||||
<ErrorModalBlock
|
||||
showError={Boolean(showError)}
|
||||
label={errorMessage || 'An error occurred'}
|
||||
onOk={() => {
|
||||
setShowError(false)
|
||||
setErrorMessage?.('')
|
||||
|
||||
return
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
return
|
||||
}}
|
||||
/>
|
||||
|
||||
{showContext && (
|
||||
<div
|
||||
ref={contextRef}
|
||||
className="fm-file-browser-context-menu fm-context-menu"
|
||||
style={{ top: safePos.y, left: safePos.x }}
|
||||
data-drop={dropDir}
|
||||
onMouseDown={e => e.stopPropagation()}
|
||||
onClick={e => e.stopPropagation()}
|
||||
>
|
||||
<FileBrowserContextMenu
|
||||
drives={drives}
|
||||
view={view}
|
||||
selectedFilesCount={bulk.selectedFiles.length}
|
||||
onRefresh={doRefresh}
|
||||
enableRefresh={Boolean(fm?.adminStamp)}
|
||||
onUploadFile={onContextUploadFile}
|
||||
onBulkDownload={() => bulk.bulkDownload(bulk.selectedFiles)}
|
||||
onBulkRestore={() => setConfirmBulkRestore(true)}
|
||||
onBulkDelete={() => setShowBulkDeleteModal(true)}
|
||||
onBulkDestroy={() => setShowDestroyDriveModal(true)}
|
||||
onBulkForget={() => bulk.bulkForget(bulk.selectedFiles)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<FileBrowserContextMenuBlock
|
||||
showContext={showContext}
|
||||
contextRef={contextRef}
|
||||
safePos={safePos}
|
||||
dropDir={dropDir}
|
||||
drives={drives}
|
||||
view={view}
|
||||
bulk={bulk}
|
||||
adminStamp={fm?.adminStamp}
|
||||
doRefresh={doRefresh}
|
||||
onContextUploadFile={onContextUploadFile}
|
||||
setConfirmBulkRestore={setConfirmBulkRestore}
|
||||
setShowBulkDeleteModal={setShowBulkDeleteModal}
|
||||
setShowDestroyDriveModal={setShowDestroyDriveModal}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{showDragOverlay && (
|
||||
@@ -537,9 +621,13 @@ export function FileBrowser({ errorMessage, setErrorMessage }: FileBrowserProps)
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showDestroyModal(isDestroying, () => setIsProgressModalOpen(true))}
|
||||
|
||||
{renderDestroySpinner(isDestroying, isProgressModalOpen, currentDrive, () => setIsProgressModalOpen(false))}
|
||||
<DestroyingOverlay isDestroying={isDestroying} onClick={() => setIsProgressModalOpen(true)} />
|
||||
<DestroyProgressModal
|
||||
isDestroying={isDestroying}
|
||||
isProgressModalOpen={isProgressModalOpen}
|
||||
currentDrive={currentDrive}
|
||||
onMinimize={() => setIsProgressModalOpen(false)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="fm-file-browser-footer">
|
||||
@@ -556,7 +644,7 @@ export function FileBrowser({ errorMessage, setErrorMessage }: FileBrowserProps)
|
||||
type={FileTransferType.Download}
|
||||
open={isDownloading}
|
||||
items={downloadItems}
|
||||
onRowClose={name => cancelOrDismissDownload(name)}
|
||||
onRowClose={(name: string) => cancelOrDismissDownload(name)}
|
||||
onCloseAll={() => dismissAllDownloads()}
|
||||
/>
|
||||
<NotificationBar setErrorMessage={setErrorMessage} />
|
||||
|
||||
Reference in New Issue
Block a user