Skip to content

Implement and Enhance File Move Functionality #2341

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 13 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,787 changes: 1,749 additions & 38 deletions package-lock.json

Large diffs are not rendered by default.

5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@
"@storybook/addon-a11y": "^6.5.16",
"@storybook/addon-actions": "^6.5.16",
"@storybook/addon-controls": "^6.5.16",
"@storybook/addon-coverage": "^0.0.8",
"@storybook/addon-coverage": "^1.0.5",
"@storybook/addon-essentials": "^6.5.16",
"@storybook/addon-knobs": "^5.3.19",
"@storybook/addon-links": "^6.5.16",
Expand All @@ -132,12 +132,13 @@
"@storybook/manager-webpack5": "^6.5.16",
"@storybook/preset-create-react-app": "^4.1.2",
"@storybook/react": "^6.5.16",
"@storybook/test-runner": "^0.17.0",
"@storybook/test-runner": "^0.16.0",
"@svgr/cli": "^5.4.0",
"@types/esm": "^3.2.0",
"@types/jest": "^29.5.14",
"@types/node": "^14.18.36",
"@types/path-browserify": "^1.0.0",
"@types/prop-types": "^15.7.14",
"@typescript-eslint/parser": "^5.62.0",
"aegir": "^42.2.2",
"autoprefixer": "^10.4.7",
Expand Down
1 change: 1 addition & 0 deletions public/locales/en/app.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
"copy": "Copy",
"create": "Create",
"remove": "Remove",
"move": "Move",
"download": "Download",
"edit": "Edit",
"import": "Import",
Expand Down
7 changes: 7 additions & 0 deletions public/locales/en/files.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,13 @@
"descriptionFile": "Choose a new name for this file.",
"descriptionFolder": "Choose a new name for this folder."
},
"moveModal": {
"titleFiles": "{count, plural, one {Move file} other {Move {count} files}}",
"titleFolder": "Move folder",
"descriptionFiles": "{count, plural, one {Choose a new location for this file.} other {Choose a new location for these {count} files.}}",
"descriptionFolder": "Choose a new location for this folder.",
"directoryCreationInfo": "If the directory does not exist, it will be created automatically"
},
"removeModal": {
"titleItem": "{count, plural, one {Remove item? {name}} other {Remove {count} items?}}",
"titleFolder": "{count, plural, one {Remove folder? {name}} other {Remove {count} folders?}}",
Expand Down
51 changes: 51 additions & 0 deletions src/bundles/files/actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,57 @@ const actions = () => ({
}
},

/**
* Fetches directory contents with optimized performance
* @param {string} path - Directory path to fetch
*/
doFetchDirectory: (path) => perform(ACTIONS.DIRECTORY_FETCH, async (ipfs) => {
if (!ipfs) {
throw new Error('IPFS is not available')
}

try {
const resolvedPath = path.startsWith('/ipns') ? await last(ipfs.name.resolve(path)) : path

const stats = await stat(ipfs, resolvedPath)

if (stats.type !== 'directory') {
return {
path,
type: stats.type,
content: []
}
}

const entries = []
for await (const entry of ipfs.ls(stats.cid)) {
if (entry.type === 'dir') {
entries.push({
name: entry.name,
type: entry.type,
path: entry.path,
size: entry.size,
cid: entry.cid
})
}
}

entries.sort((a, b) => a.name.localeCompare(b.name))

return {
path,
type: 'directory',
content: entries
}
} catch (err) {
return {
path,
type: 'unknown',
content: []
}
}
}),

/**
* Reads data from a CID with optional offset and length.
* @param {import('multiformats/cid').CID} cid - The CID to read from
Expand Down
2 changes: 2 additions & 0 deletions src/bundles/files/consts.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ export const ACTIONS = {
WRITE_UPDATED: ('FILES_WRITE_UPDATED'),
/** @type {'FILES_UPDATE_SORT'} */
UPDATE_SORT: ('FILES_UPDATE_SORT'),
/** @type {'FILES_DIRECTORY_FETCH'} */
DIRECTORY_FETCH: ('FILES_DIRECTORY_FETCH'),
/** @type {'FILES_READ'} */
READ_FILE: ('FILES_READ')
}
Expand Down
2 changes: 1 addition & 1 deletion src/components/overlay/Overlay.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ function Overlay ({ children, show, onLeave, className, hidden, ...props }) {
<Modal
{...props}
show={show}
className={`${className} fixed top-0 left-0 right-0 bottom-0 z-max flex items-center justify-around`}
className={`${className} modal fixed top-0 left-0 right-0 bottom-0 z-max flex items-center justify-around`}
renderBackdrop={renderBackdrop}
onKeyDown={handleKeyDown}
onBackdropClick={onLeave}>
Expand Down
11 changes: 8 additions & 3 deletions src/files/FilesPage.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import { ViewList, ViewModule } from '../icons/stroke-icons.js'
import { getJoyrideLocales } from '../helpers/i8n.js'

// Icons
import Modals, { DELETE, NEW_FOLDER, SHARE, ADD_BY_CAR, RENAME, ADD_BY_PATH, BULK_CID_IMPORT, SHORTCUTS, CLI_TUTOR_MODE, PINNING, PUBLISH } from './modals/Modals.js'
import Modals, { DELETE, NEW_FOLDER, SHARE, RENAME, ADD_BY_CAR, ADD_BY_PATH, BULK_CID_IMPORT, CLI_TUTOR_MODE, SHORTCUTS, PINNING, PUBLISH, MOVE } from './modals/Modals.js'

import Header from './header/Header.js'
import FileImportStatus from './file-import-status/FileImportStatus.js'
Expand All @@ -29,7 +29,7 @@ const FilesPage = ({
doFetchPinningServices, doFilesFetch, doPinsFetch, doFilesSizeGet, doFilesDownloadLink, doFilesDownloadCarLink, doFilesWrite, doAddCarFile, doFilesBulkCidImport, doFilesAddPath, doUpdateHash,
doFilesUpdateSorting, doFilesNavigateTo, doFilesMove, doSetCliOptions, doFetchRemotePins, remotePins, pendingPins, failedPins,
ipfsProvider, ipfsConnected, doFilesMakeDir, doFilesShareLink, doFilesDelete, doSetPinning, onRemotePinClick, doPublishIpnsKey,
files, filesPathInfo, pinningServices, toursEnabled, handleJoyrideCallback, isCliTutorModeEnabled, cliOptions, t
files, filesPathInfo, doFetchDirectory, pinningServices, toursEnabled, handleJoyrideCallback, isCliTutorModeEnabled, cliOptions, t
}) => {
const { doExploreUserProvidedPath } = useExplore()
const contextMenuRef = useRef()
Expand Down Expand Up @@ -220,7 +220,8 @@ const FilesPage = ({
onDownload,
onAddFiles,
onNavigate: doFilesNavigateTo,
onMove: doFilesMove,
onMove: (files) => showModal(MOVE, files),
upperDir: files.upper,
handleContextMenuClick: handleContextMenu,
// TODO: Implement this
onDismissFailedPin: () => {}
Expand All @@ -243,6 +244,7 @@ const FilesPage = ({
share={() => showModal(SHARE, selectedFiles)}
setPinning={() => showModal(PINNING, selectedFiles)}
download={() => onDownload(selectedFiles)}
move={() => showModal(MOVE, selectedFiles)}
inspect={() => onInspect(selectedFiles[0].cid)}
count={selectedFiles.length}
isMfs={filesPathInfo.isMfs}
Expand Down Expand Up @@ -364,6 +366,7 @@ const FilesPage = ({

<Modals
done={hideModal}
mainFiles={files}
root={files ? files.path : null}
onMove={doFilesMove}
onMakeDir={doFilesMakeDir}
Expand All @@ -373,6 +376,7 @@ const FilesPage = ({
onAddByCar={onAddByCar}
onBulkCidImport={onBulkCidImport}
onPinningSet={doSetPinning}
onFetchDirectory={doFetchDirectory}
onPublish={doPublishIpnsKey}
cliOptions={cliOptions}
{ ...modals } />
Expand Down Expand Up @@ -432,6 +436,7 @@ export default connect(
'selectIsCliTutorModalOpen',
'doOpenCliTutorModal',
'doSetCliOptions',
'doFetchDirectory',
'selectCliOptions',
'doSetPinning',
'doPublishIpnsKey',
Expand Down
55 changes: 45 additions & 10 deletions src/files/breadcrumbs/Breadcrumbs.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import React, { useEffect, useState, useRef, useMemo } from 'react'
import classNames from 'classnames'
import PropTypes from 'prop-types'
import { basename, join } from 'path'
import { connect } from 'redux-bundler-react'
import { withTranslation } from 'react-i18next'
import { useDrop } from 'react-dnd'
Expand All @@ -14,21 +13,57 @@ import './Breadcrumbs.css'
const DropableBreadcrumb = ({ index, link, immutable, onAddFiles, onMove, onClick, onContextMenuHandle, getPathInfo, checkIfPinned }) => {
const [{ isOver }, drop] = useDrop({
accept: [NativeTypes.FILE, 'FILE'],
drop: async ({ files, filesPromise, path: filePath }) => {
if (files) {
drop: async (_, monitor) => {
const item = monitor.getItem()

if (item.files) {
(async () => {
const files = await filesPromise
onAddFiles(await normalizeFiles(files), link.path)
const files = await item.filesPromise
onAddFiles(normalizeFiles(files), link.path)
})()
} else {
const src = filePath
const dst = join(link.path, basename(filePath))

try { await onMove(src, dst) } catch (e) { console.error(e) }
const src = item.path

try {
const selectedFiles = Array.isArray(item.selectedFiles) ? item.selectedFiles : []
const isDraggedFileSelected = selectedFiles.length > 0 && selectedFiles.some(file => file.path === src)

if (isDraggedFileSelected) {
const moveOperations = selectedFiles.map(file => {
const fileName = file.path.split('/').pop()
const destinationPath = `${link.path}/${fileName}`
return [file.path, destinationPath]
})
for (const [src, dst] of moveOperations) {
try {
await onMove(src, dst)
} catch (err) {
console.error('Failed to move file:', { src, dst, error: err })
}
}
} else {
const fileName = src.split('/').pop()
const destinationPath = `${link.path}/${fileName}`
await onMove(src, destinationPath)
}
} catch (err) {
console.error('Error during file move operation:', err)
}
}
},
canDrop: (_, monitor) => {
const item = monitor.getItem()
if (!item) return false

if (item.path === link.path) return false

if (item.parentPath === link.path) return false

return true
},
collect: (monitor) => ({
isOver: monitor.isOver()
isOver: monitor.isOver(),
canDrop: monitor.canDrop()
})
})

Expand Down
54 changes: 42 additions & 12 deletions src/files/file/File.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import React, { useRef } from 'react'
import PropTypes from 'prop-types'
import { join, basename } from 'path'
import { withTranslation } from 'react-i18next'
import classnames from 'classnames'
import { normalizeFiles, humanSize } from '../../lib/files.js'
Expand Down Expand Up @@ -31,22 +30,39 @@ const File = ({
}

const [, drag, preview] = useDrag({
item: { name, size, cid, path, pinned, type: 'FILE' },
canDrag: !cantDrag && isMfs
item: {
name,
size,
cid,
path,
pinned,
type: 'FILE',
parentPath: path.substring(0, path.lastIndexOf('/')),
selectedFiles: selected ? (window.__selectedFiles || []) : []
},
canDrag: !cantDrag && isMfs,
collect: (monitor) => ({
isDragging: monitor.isDragging()
})
})

const checkIfDir = (monitor) => {
if (!isMfs) return false
const item = monitor.getItem()
if (!item) return false

if (item.name) {
return type === 'directory' &&
name !== item.name &&
!selected
if (type !== 'directory') return false

if (item.path) {
if (item.path === path) return false

const itemParentPath = item.path.substring(0, item.path.lastIndexOf('/'))
if (itemParentPath === path) return false

return true
}

return type === 'directory'
return true
}

const [{ isOver, canDrop }, drop] = useDrop({
Expand All @@ -61,12 +77,26 @@ const File = ({
})()
} else {
const src = item.path
const dst = join(path, basename(item.path))

onMove(src, dst)
const selectedFiles = Array.isArray(item.selectedFiles) ? item.selectedFiles : []
const isDraggedFileSelected = selectedFiles.length > 0 && selectedFiles.some(file => file.path === src)

if (isDraggedFileSelected) {
selectedFiles.forEach(file => {
const fileName = file.path.split('/').pop()
const destinationPath = `${path}/${fileName}`
onMove(file.path, destinationPath)
})
} else {
const fileName = src.split('/').pop()
const destinationPath = `${path}/${fileName}`
onMove(src, destinationPath)
}
}
},
canDrop: (_, monitor) => checkIfDir(monitor),
canDrop: (_, monitor) => {
const canDrop = checkIfDir(monitor)
return canDrop
},
collect: (monitor) => ({
canDrop: checkIfDir(monitor),
isOver: monitor.isOver()
Expand Down
5 changes: 5 additions & 0 deletions src/files/files-grid/files-grid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,11 @@ const FilesGrid = ({
const keyHandler = useCallback((e: KeyboardEvent) => {
const focusedFile = focused == null ? null : files.find(el => el.name === focused)

if (((e.target as HTMLElement).tagName === 'INPUT' ||
(e.target as HTMLElement).tagName === 'TEXTAREA' ||
(e.target as HTMLElement).tagName === 'SELECT') &&
(e.target as HTMLElement).closest('.modal')) return

gridRef.current?.focus?.()

if (e.key === 'Escape') {
Expand Down
Loading