* 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:
@@ -63,7 +63,7 @@ export function AccountChequebook(): ReactElement {
|
||||
/>
|
||||
<ExpandableListItemKey
|
||||
label="Chequebook contract address"
|
||||
value={chequebookAddress?.chequebookAddress || ''}
|
||||
value={chequebookAddress?.chequebookAddress.toString() || ''}
|
||||
/>
|
||||
</ExpandableList>
|
||||
<PeerBalances accounting={accounting} isLoadingUncashed={isLoadingUncashed} totalUncashed={totalUncashed} />
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
.fm-main {
|
||||
position: relative;
|
||||
border-left: 1px solid rgb(146, 146, 146);
|
||||
min-height: 100vh;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.fm-main-content {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.fm-main-content-file-browser {
|
||||
width: 100%;
|
||||
background-color: rgb(255, 255, 255) !important;
|
||||
}
|
||||
|
||||
.fm-loading {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100vh;
|
||||
gap: 12px;
|
||||
background: rgb(255, 255, 255);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.fm-spinner {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 9999px;
|
||||
border: 3px solid rgb(229, 231, 235);
|
||||
border-top-color: rgb(255, 140, 0);
|
||||
animation: fm-spin 0.8s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes fm-spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.fm-loading-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: rgb(17, 24, 39);
|
||||
}
|
||||
|
||||
.fm-loading-subtitle {
|
||||
font-size: 13px;
|
||||
color: rgb(107, 114, 128);
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
import { createContext, useContext, useMemo, useRef, useState, ReactNode, useCallback } from 'react'
|
||||
|
||||
type Scope = 'selected' | 'all'
|
||||
|
||||
export interface SearchState {
|
||||
query: string
|
||||
scope: Scope
|
||||
includeActive: boolean
|
||||
includeTrashed: boolean
|
||||
setQuery: (q: string) => void
|
||||
clear: () => void
|
||||
setScope: (s: Scope) => void
|
||||
setIncludeActive: (v: boolean) => void
|
||||
setIncludeTrashed: (v: boolean) => void
|
||||
}
|
||||
|
||||
const Ctx = createContext<SearchState | undefined>(undefined)
|
||||
|
||||
export function SearchProvider({ children }: { children: ReactNode }) {
|
||||
const [query, _setQuery] = useState('')
|
||||
const [scope, setScope] = useState<Scope>('all')
|
||||
const [includeActive, setIncludeActive] = useState(true)
|
||||
const [includeTrashed, setIncludeTrashed] = useState(true)
|
||||
|
||||
const preSearchState = useRef<{ scope: Scope; includeActive: boolean; includeTrashed: boolean } | null>(null)
|
||||
const inSearch = useRef(false)
|
||||
|
||||
const setQuery = useCallback(
|
||||
(q: string) => {
|
||||
const trimmed = q.trim()
|
||||
|
||||
if (!inSearch.current && trimmed.length > 0) {
|
||||
preSearchState.current = { scope, includeActive, includeTrashed }
|
||||
inSearch.current = true
|
||||
}
|
||||
|
||||
if (inSearch.current && trimmed.length === 0) {
|
||||
const prev = preSearchState.current
|
||||
|
||||
if (prev) {
|
||||
setScope(prev.scope)
|
||||
setIncludeActive(prev.includeActive)
|
||||
setIncludeTrashed(prev.includeTrashed)
|
||||
}
|
||||
preSearchState.current = null
|
||||
inSearch.current = false
|
||||
}
|
||||
|
||||
_setQuery(q)
|
||||
},
|
||||
[scope, includeActive, includeTrashed],
|
||||
)
|
||||
|
||||
const clear = useCallback(() => {
|
||||
setQuery('')
|
||||
}, [setQuery])
|
||||
|
||||
const value = useMemo<SearchState>(
|
||||
() => ({
|
||||
query,
|
||||
scope,
|
||||
includeActive,
|
||||
includeTrashed,
|
||||
setQuery,
|
||||
clear,
|
||||
setScope,
|
||||
setIncludeActive,
|
||||
setIncludeTrashed,
|
||||
}),
|
||||
[query, scope, includeActive, includeTrashed, clear, setQuery],
|
||||
)
|
||||
|
||||
return <Ctx.Provider value={value}>{children}</Ctx.Provider>
|
||||
}
|
||||
|
||||
export function useSearch(): SearchState {
|
||||
const v = useContext(Ctx)
|
||||
|
||||
if (!v) throw new Error('useFMSearch must be used within SearchProvider')
|
||||
|
||||
return v
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import { createContext, useContext, useState, ReactNode } from 'react'
|
||||
import { ViewType } from '../../modules/filemanager/constants/transfers'
|
||||
|
||||
interface ViewContextProps {
|
||||
view: ViewType
|
||||
setView: (view: ViewType) => void
|
||||
actualItemView?: string
|
||||
setActualItemView?: (view: string) => void
|
||||
}
|
||||
|
||||
const ViewContext = createContext<ViewContextProps | undefined>(undefined)
|
||||
|
||||
export function ViewProvider({ children }: { children: ReactNode }) {
|
||||
const [view, setView] = useState<ViewType>(ViewType.File)
|
||||
const [actualItemView, setActualItemView] = useState<string | undefined>(undefined)
|
||||
|
||||
return (
|
||||
<ViewContext.Provider
|
||||
value={{
|
||||
view,
|
||||
setView,
|
||||
actualItemView,
|
||||
setActualItemView,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</ViewContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export function useView() {
|
||||
const context = useContext(ViewContext)
|
||||
|
||||
if (!context) {
|
||||
throw new Error('useView must be used within a ViewProvider')
|
||||
}
|
||||
|
||||
return context
|
||||
}
|
||||
@@ -0,0 +1,229 @@
|
||||
import { ReactElement, useContext, useEffect, useState, useRef, useCallback, useMemo } from 'react'
|
||||
import './FileManager.scss'
|
||||
import { SearchProvider } from './SearchContext'
|
||||
import { ViewProvider } from './ViewContext'
|
||||
import { Header } from '../../modules/filemanager/components/Header/Header'
|
||||
import { Sidebar } from '../../modules/filemanager/components/Sidebar/Sidebar'
|
||||
import { AdminStatusBar } from '../../modules/filemanager/components/AdminStatusBar/AdminStatusBar'
|
||||
import { FileBrowser } from '../../modules/filemanager/components/FileBrowser/FileBrowser'
|
||||
import { InitialModal } from '../../modules/filemanager/components/InitialModal/InitialModal'
|
||||
import { Context as FMContext } from '../../providers/FileManager'
|
||||
import { Context as BeeContext, CheckState } from '../../providers/Bee'
|
||||
import { PrivateKeyModal } from '../../modules/filemanager/components/PrivateKeyModal/PrivateKeyModal'
|
||||
import { getSignerPk, removeSignerPk } from '../../../src/modules/filemanager/utils/common'
|
||||
import { ErrorModal } from '../../../src/modules/filemanager/components/ErrorModal/ErrorModal'
|
||||
import { ConfirmModal } from '../../modules/filemanager/components/ConfirmModal/ConfirmModal'
|
||||
import { Button } from '../../modules/filemanager/components/Button/Button'
|
||||
import { FormbricksIntegration } from '../../modules/filemanager/components/FormbricksIntegration/FormbricksIntegration'
|
||||
|
||||
export function FileManagerPage(): ReactElement {
|
||||
const isMountedRef = useRef(true)
|
||||
const [showInitialModal, setShowInitialModal] = useState(false)
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
const [hasAdminDrive, setHasAdminDrive] = useState(false)
|
||||
const [hasPk, setHasPk] = useState<boolean>(getSignerPk() !== undefined)
|
||||
const [showErrorModal, setShowErrorModal] = useState<boolean>(false)
|
||||
const [errorMessage, setErrorMessage] = useState<string>('')
|
||||
const [showResetModal, setShowResetModal] = useState<boolean>(false)
|
||||
const [isCreationInProgress, setIsCreationInProgress] = useState<boolean>(false)
|
||||
|
||||
const { status } = useContext(BeeContext)
|
||||
const { fm, shallReset, adminDrive, initializationError, init } = useContext(FMContext)
|
||||
|
||||
useEffect(() => {
|
||||
isMountedRef.current = true
|
||||
|
||||
return () => {
|
||||
isMountedRef.current = false
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (!hasPk) {
|
||||
setIsLoading(false)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
setShowResetModal(shallReset)
|
||||
|
||||
if (shallReset) {
|
||||
setShowInitialModal(true)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if (initializationError) {
|
||||
setIsLoading(false)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if (fm) {
|
||||
const hasAdminStamp = Boolean(fm.adminStamp)
|
||||
const tmpHasAdminDrive = Boolean(adminDrive)
|
||||
setHasAdminDrive(hasAdminStamp || tmpHasAdminDrive)
|
||||
setIsLoading(false)
|
||||
|
||||
setShowInitialModal(!(hasAdminStamp || tmpHasAdminDrive))
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
setIsLoading(true)
|
||||
}, [fm, hasPk, initializationError, adminDrive, shallReset])
|
||||
|
||||
const handlePrivateKeySaved = useCallback(async () => {
|
||||
if (!isMountedRef.current) return
|
||||
|
||||
setHasPk(true)
|
||||
|
||||
if (fm) {
|
||||
if (!isMountedRef.current) return
|
||||
|
||||
setIsLoading(false)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
setIsLoading(true)
|
||||
const manager = await init()
|
||||
|
||||
if (!isMountedRef.current) return
|
||||
|
||||
setIsLoading(false)
|
||||
|
||||
const hasAdminStamp = Boolean(manager?.adminStamp)
|
||||
const tmpHasAdminDrive = Boolean(adminDrive)
|
||||
|
||||
setShowInitialModal(!(hasAdminStamp || tmpHasAdminDrive))
|
||||
}, [fm, adminDrive, init])
|
||||
|
||||
const isEmptyState = useMemo(() => {
|
||||
return showInitialModal && !isLoading && !hasAdminDrive && !isCreationInProgress
|
||||
}, [showInitialModal, isLoading, hasAdminDrive, isCreationInProgress])
|
||||
const isInvalidState = useMemo(
|
||||
() => shallReset && fm && !isCreationInProgress,
|
||||
[shallReset, fm, isCreationInProgress],
|
||||
)
|
||||
|
||||
const loading = !fm?.adminStamp || !adminDrive
|
||||
|
||||
const isFormbricksActive = Boolean(fm && fm.adminStamp && adminDrive && !showInitialModal && !loading)
|
||||
|
||||
if (status.all !== CheckState.OK) {
|
||||
return (
|
||||
<div className="fm-main">
|
||||
<div className="fm-loading">
|
||||
<div className="fm-loading-title">Bee node error - cannot load File Manager</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (!hasPk) {
|
||||
return (
|
||||
<div className="fm-main">
|
||||
<PrivateKeyModal onSaved={handlePrivateKeySaved} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (initializationError && !isLoading && !shallReset) {
|
||||
return (
|
||||
<div className="fm-main">
|
||||
<div className="fm-loading">
|
||||
<div className="fm-loading-title">Failed to initialize File Manager, reload and try again </div>
|
||||
<div style={{ display: 'flex', justifyContent: 'center', marginTop: '16px' }}>
|
||||
<div style={{ minWidth: '120px' }}>
|
||||
<Button
|
||||
label={'OK'}
|
||||
variant="primary"
|
||||
disabled={false}
|
||||
onClick={() => {
|
||||
removeSignerPk()
|
||||
setHasPk(false)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (showResetModal) {
|
||||
return (
|
||||
<div className="fm-main">
|
||||
<ConfirmModal
|
||||
title="Reset File Manager State"
|
||||
message="Your File Manager state appears invalid. Please reset it to continue."
|
||||
confirmLabel="Proceed"
|
||||
onConfirm={() => {
|
||||
setShowResetModal(false)
|
||||
}}
|
||||
background={false}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (!showErrorModal && (isEmptyState || isInvalidState)) {
|
||||
return (
|
||||
<div className="fm-main">
|
||||
<InitialModal
|
||||
resetState={shallReset}
|
||||
handleVisibility={(isVisible: boolean) => setShowInitialModal(isVisible)}
|
||||
handleShowError={(flag: boolean) => setShowErrorModal(flag)}
|
||||
setIsCreationInProgress={(isCreating: boolean) => setIsCreationInProgress(isCreating)}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (!fm) {
|
||||
return (
|
||||
<div className="fm-main">
|
||||
<div className="fm-loading" aria-live="polite">
|
||||
<div className="fm-spinner" aria-hidden="true" />
|
||||
<div className="fm-loading-title">File manager loading…</div>
|
||||
<div className="fm-loading-subtitle">Please wait a few seconds</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (showErrorModal) {
|
||||
return (
|
||||
<ErrorModal
|
||||
label={'Error during admin state creation, try again'}
|
||||
onClick={() => {
|
||||
setShowErrorModal(false)
|
||||
setShowInitialModal(true)
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<SearchProvider>
|
||||
<ViewProvider>
|
||||
<div className="fm-main">
|
||||
<FormbricksIntegration isActive={isFormbricksActive} />
|
||||
<Header />
|
||||
<div className="fm-main-content">
|
||||
<Sidebar errorMessage={errorMessage} setErrorMessage={setErrorMessage} loading={loading} />
|
||||
<FileBrowser errorMessage={errorMessage} setErrorMessage={setErrorMessage} />
|
||||
</div>
|
||||
<AdminStatusBar
|
||||
adminStamp={fm?.adminStamp || null}
|
||||
adminDrive={adminDrive}
|
||||
loading={loading}
|
||||
isCreationInProgress={isCreationInProgress}
|
||||
setErrorMessage={setErrorMessage}
|
||||
/>
|
||||
</div>
|
||||
</ViewProvider>
|
||||
</SearchProvider>
|
||||
)
|
||||
}
|
||||
@@ -12,7 +12,7 @@ interface Props {
|
||||
export function AssetSyncing({ reference }: Props): ReactElement {
|
||||
const { beeApi } = useContext(SettingsContext)
|
||||
|
||||
const syncTimer = useRef<NodeJS.Timer>()
|
||||
const syncTimer = useRef<ReturnType<typeof setInterval> | null>(null)
|
||||
const [isRetrieveChecking, setIsRetrieveChecking] = useState<boolean>(false)
|
||||
const [syncProgress, setSyncProgress] = useState<number>(0)
|
||||
|
||||
@@ -22,7 +22,7 @@ export function AssetSyncing({ reference }: Props): ReactElement {
|
||||
let allTags: Tag[] = []
|
||||
let offset = 0
|
||||
const limit = 1000
|
||||
let tagsBatch
|
||||
let tagsBatch: Tag[]
|
||||
|
||||
do {
|
||||
tagsBatch = await beeApi.getAllTags({ limit, offset })
|
||||
@@ -32,7 +32,7 @@ export function AssetSyncing({ reference }: Props): ReactElement {
|
||||
|
||||
const tag = allTags.find(t => t.address === reference)
|
||||
|
||||
if (tag) {
|
||||
if (tag && tag.split > 0) {
|
||||
const progress = ((tag.seen + tag.synced) / tag.split) * 100
|
||||
setSyncProgress(progress)
|
||||
}
|
||||
@@ -44,6 +44,7 @@ export function AssetSyncing({ reference }: Props): ReactElement {
|
||||
return () => {
|
||||
if (syncTimer.current) {
|
||||
clearInterval(syncTimer.current)
|
||||
syncTimer.current = null
|
||||
}
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
@@ -52,6 +53,7 @@ export function AssetSyncing({ reference }: Props): ReactElement {
|
||||
useEffect(() => {
|
||||
if (syncProgress === 100 && syncTimer.current) {
|
||||
clearInterval(syncTimer.current)
|
||||
syncTimer.current = null
|
||||
}
|
||||
}, [syncProgress])
|
||||
|
||||
|
||||
@@ -55,7 +55,7 @@ const ChequebookDeployFund = (): ReactElement | null => {
|
||||
<ExpandableListItemNote>{text}</ExpandableListItemNote>
|
||||
{chequebookAddress && (
|
||||
<>
|
||||
<ExpandableListItemKey label="Chequebook Address" value={chequebookAddress.chequebookAddress} />
|
||||
<ExpandableListItemKey label="Chequebook Address" value={chequebookAddress.chequebookAddress.toString()} />
|
||||
<ExpandableListItemActions>
|
||||
<DepositModal />
|
||||
</ExpandableListItemActions>
|
||||
|
||||
Reference in New Issue
Block a user