Merge pull request #1209 from ethereum/copy-menu

Copy & Paste Files and Folders
pull/1195/head^2
David Disu 4 years ago committed by GitHub
commit 0354295bd5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      apps/remix-ide-e2e/src/tests/fileManager_api.spec.ts
  2. 49
      apps/remix-ide/src/app/files/fileManager.js
  3. 5
      apps/remix-ide/src/lib/helper.js
  4. 8
      libs/remix-ui/file-explorer/src/lib/file-explorer-context-menu.tsx
  5. 100
      libs/remix-ui/file-explorer/src/lib/file-explorer.tsx
  6. 4
      libs/remix-ui/file-explorer/src/lib/types/index.ts

@ -147,7 +147,7 @@ const executeReadFile = `
const executeCopyFile = ` const executeCopyFile = `
const run = async () => { const run = async () => {
await remix.call('fileManager', 'copyFile', 'contracts/3_Ballot.sol', 'new_contract.sol') await remix.call('fileManager', 'copyFile', 'contracts/3_Ballot.sol', '/', 'new_contract.sol')
const result = await remix.call('fileManager', 'readFile', 'new_contract.sol') const result = await remix.call('fileManager', 'readFile', 'new_contract.sol')
console.log(result) console.log(result)

@ -22,7 +22,7 @@ const profile = {
icon: 'assets/img/fileManager.webp', icon: 'assets/img/fileManager.webp',
permission: true, permission: true,
version: packageJson.version, version: packageJson.version,
methods: ['file', 'exists', 'open', 'writeFile', 'readFile', 'copyFile', 'rename', 'mkdir', 'readdir', 'remove', 'getCurrentFile', 'getFile', 'getFolder', 'setFile', 'switchFile'], methods: ['file', 'exists', 'open', 'writeFile', 'readFile', 'copyFile', 'copyDir', 'rename', 'mkdir', 'readdir', 'remove', 'getCurrentFile', 'getFile', 'getFolder', 'setFile', 'switchFile'],
kind: 'file-system' kind: 'file-system'
} }
const errorMsg = { const errorMsg = {
@ -213,21 +213,58 @@ class FileManager extends Plugin {
* @param {string} dest path of the destrination file * @param {string} dest path of the destrination file
* @returns {void} * @returns {void}
*/ */
async copyFile (src, dest) { async copyFile (src, dest, customName) {
try { try {
src = this.limitPluginScope(src) src = this.limitPluginScope(src)
dest = this.limitPluginScope(dest) dest = this.limitPluginScope(dest)
await this._handleExists(src, `Cannot copy from ${src}`) await this._handleExists(src, `Cannot copy from ${src}. Path does not exist.`)
await this._handleIsFile(src, `Cannot copy from ${src}`) await this._handleIsFile(src, `Cannot copy from ${src}. Path is not a file.`)
await this._handleIsFile(dest, `Cannot paste content into ${dest}`) await this._handleExists(dest, `Cannot paste content into ${dest}. Path does not exist.`)
await this._handleIsDir(dest, `Cannot paste content into ${dest}. Path is not directory.`)
const content = await this.readFile(src) const content = await this.readFile(src)
const copiedFileName = customName ? '/' + customName : '/' + `Copy_${helper.extractNameFromKey(src)}`
await this.writeFile(dest, content) await this.writeFile(dest + copiedFileName, content)
} catch (e) { } catch (e) {
throw new Error(e) throw new Error(e)
} }
} }
/**
* Upsert a directory with the content of the source directory
* @param {string} src path of the source dir
* @param {string} dest path of the destination dir
* @returns {void}
*/
async copyDir (src, dest) {
try {
src = this.limitPluginScope(src)
dest = this.limitPluginScope(dest)
await this._handleExists(src, `Cannot copy from ${src}. Path does not exist.`)
await this._handleIsDir(src, `Cannot copy from ${src}. Path is not a directory.`)
await this._handleExists(dest, `Cannot paste content into ${dest}. Path does not exist.`)
await this._handleIsDir(dest, `Cannot paste content into ${dest}. Path is not directory.`)
await this.inDepthCopy(src, dest)
} catch (e) {
throw new Error(e)
}
}
async inDepthCopy (src, dest, count = 0) {
const content = await this.readdir(src)
const copiedFolderPath = count === 0 ? dest + '/' + `Copy_${helper.extractNameFromKey(src)}` : dest + '/' + helper.extractNameFromKey(src)
await this.mkdir(copiedFolderPath)
for (const [key, value] of Object.entries(content)) {
if (!value.isDirectory) {
await this.copyFile(key, copiedFolderPath, helper.extractNameFromKey(key))
} else {
await this.inDepthCopy(key, copiedFolderPath, count + 1)
}
}
}
/** /**
* Change the path of a file/directory * Change the path of a file/directory
* @param {string} oldPath current path of the file/directory * @param {string} oldPath current path of the file/directory

@ -106,6 +106,11 @@ module.exports = {
paths = paths.filter((value) => value !== '').map((path) => path.replace(/^\/|\/$/g, '')) // remove first and last slash) paths = paths.filter((value) => value !== '').map((path) => path.replace(/^\/|\/$/g, '')) // remove first and last slash)
if (paths.length === 1) return paths[0] if (paths.length === 1) return paths[0]
return paths.join('/') return paths.join('/')
},
extractNameFromKey (key) {
const keyPath = key.split('/')
return keyPath[keyPath.length - 1]
} }
} }

@ -4,7 +4,7 @@ import { FileExplorerContextMenuProps } from './types'
import './css/file-explorer-context-menu.css' import './css/file-explorer-context-menu.css'
export const FileExplorerContextMenu = (props: FileExplorerContextMenuProps) => { export const FileExplorerContextMenu = (props: FileExplorerContextMenuProps) => {
const { actions, createNewFile, createNewFolder, deletePath, renamePath, hideContextMenu, pushChangesToGist, publishFileToGist, publishFolderToGist, runScript, emit, pageX, pageY, path, type, ...otherProps } = props const { actions, createNewFile, createNewFolder, deletePath, renamePath, hideContextMenu, pushChangesToGist, publishFileToGist, publishFolderToGist, copy, paste, runScript, emit, pageX, pageY, path, type, ...otherProps } = props
const contextMenuRef = useRef(null) const contextMenuRef = useRef(null)
useEffect(() => { useEffect(() => {
@ -61,6 +61,12 @@ export const FileExplorerContextMenu = (props: FileExplorerContextMenuProps) =>
case 'Run': case 'Run':
runScript(path) runScript(path)
break break
case 'Copy':
copy(path, type)
break
case 'Paste':
paste(path, type)
break
default: default:
emit && emit(item.id, path) emit && emit(item.id, path)
break break

@ -23,7 +23,6 @@ export const FileExplorer = (props: FileExplorerProps) => {
key: '', key: '',
type: 'folder' type: 'folder'
}], }],
focusPath: null,
files: [], files: [],
fileManager: null, fileManager: null,
ctrlKey: false, ctrlKey: false,
@ -56,6 +55,13 @@ export const FileExplorer = (props: FileExplorerProps) => {
path: [], path: [],
extension: [], extension: [],
pattern: [] pattern: []
}, {
id: 'run',
name: 'Run',
type: [],
path: [],
extension: ['.js'],
pattern: []
}, { }, {
id: 'pushChangesToGist', id: 'pushChangesToGist',
name: 'Push changes to gist', name: 'Push changes to gist',
@ -78,11 +84,11 @@ export const FileExplorer = (props: FileExplorerProps) => {
extension: [], extension: [],
pattern: [] pattern: []
}, { }, {
id: 'run', id: 'copy',
name: 'Run', name: 'Copy',
type: [], type: ['folder', 'file'],
path: [], path: [],
extension: ['.js'], extension: [],
pattern: [] pattern: []
}], }],
focusContext: { focusContext: {
@ -112,8 +118,10 @@ export const FileExplorer = (props: FileExplorerProps) => {
toasterMsg: '', toasterMsg: '',
mouseOverElement: null, mouseOverElement: null,
showContextMenu: false, showContextMenu: false,
reservedKeywords: [name, 'gist-'] reservedKeywords: [name, 'gist-'],
copyElement: []
}) })
const [canPaste, setCanPaste] = useState(false)
const [fileSystem, dispatch] = useReducer(fileSystemReducer, fileSystemInitialState) const [fileSystem, dispatch] = useReducer(fileSystemReducer, fileSystemInitialState)
const editRef = useRef(null) const editRef = useRef(null)
@ -176,12 +184,7 @@ export const FileExplorer = (props: FileExplorerProps) => {
useEffect(() => { useEffect(() => {
if (contextMenuItems) { if (contextMenuItems) {
setState(prevState => { addMenuItems(contextMenuItems)
// filter duplicate items
const items = contextMenuItems.filter(({ name }) => prevState.actions.findIndex(action => action.name === name) === -1)
return { ...prevState, actions: [...prevState.actions, ...items] }
})
} }
}, [contextMenuItems]) }, [contextMenuItems])
@ -223,6 +226,38 @@ export const FileExplorer = (props: FileExplorerProps) => {
} }
}, [state.modals]) }, [state.modals])
useEffect(() => {
if (canPaste) {
addMenuItems([{
id: 'paste',
name: 'Paste',
type: ['folder', 'file'],
path: [],
extension: [],
pattern: []
}])
} else {
removeMenuItems(['paste'])
}
}, [canPaste])
const addMenuItems = (items: { id: string, name: string, type: string[], path: string[], extension: string[], pattern: string[] }[]) => {
setState(prevState => {
// filter duplicate items
const actions = items.filter(({ name }) => prevState.actions.findIndex(action => action.name === name) === -1)
return { ...prevState, actions: [...prevState.actions, ...actions] }
})
}
const removeMenuItems = (ids: string[]) => {
setState(prevState => {
const actions = prevState.actions.filter(({ id }) => ids.findIndex(value => value === id) === -1)
return { ...prevState, actions }
})
}
const extractNameFromKey = (key: string):string => { const extractNameFromKey = (key: string):string => {
const keyPath = key.split('/') const keyPath = key.split('/')
@ -378,6 +413,26 @@ export const FileExplorer = (props: FileExplorerProps) => {
}) })
} }
const copyFile = (src: string, dest: string) => {
const fileManager = state.fileManager
try {
fileManager.copyFile(src, dest)
} catch (error) {
console.log('Oops! An error ocurred while performing copyFile operation.' + error)
}
}
const copyFolder = (src: string, dest: string) => {
const fileManager = state.fileManager
try {
fileManager.copyDir(src, dest)
} catch (error) {
console.log('Oops! An error ocurred while performing copyDir operation.' + error)
}
}
const publishToGist = (path?: string, type?: string) => { const publishToGist = (path?: string, type?: string) => {
modal('Create a public gist', `Are you sure you want to anonymously publish all your files in the ${name} workspace as a public gist on github.com?`, 'OK', () => toGist(path, type), 'Cancel', () => {}) modal('Create a public gist', `Are you sure you want to anonymously publish all your files in the ${name} workspace as a public gist on github.com?`, 'OK', () => toGist(path, type), 'Cancel', () => {})
} }
@ -695,6 +750,25 @@ export const FileExplorer = (props: FileExplorerProps) => {
}) })
} }
const handleCopyClick = (path: string, type: string) => {
setState(prevState => {
return { ...prevState, copyElement: [{ key: path, type }] }
})
setCanPaste(true)
toast(`Copied to clipboard ${path}`)
}
const handlePasteClick = (dest: string, destType: string) => {
dest = destType === 'file' ? extractParentFromKey(dest) || props.name : dest
state.copyElement.map(({ key, type }) => {
type === 'file' ? copyFile(key, dest) : copyFolder(key, dest)
})
setState(prevState => {
return { ...prevState, copyElement: [] }
})
setCanPaste(false)
}
const label = (file: File) => { const label = (file: File) => {
return ( return (
<div <div
@ -868,6 +942,8 @@ export const FileExplorer = (props: FileExplorerProps) => {
deletePath={deletePath} deletePath={deletePath}
renamePath={editModeOn} renamePath={editModeOn}
runScript={runScript} runScript={runScript}
copy={handleCopyClick}
paste={handlePasteClick}
emit={emitContextMenuEvent} emit={emitContextMenuEvent}
pageX={state.focusContext.x} pageX={state.focusContext.x}
pageY={state.focusContext.y} pageY={state.focusContext.y}

@ -46,5 +46,7 @@ export interface FileExplorerContextMenuProps {
pageY: number, pageY: number,
path: string, path: string,
type: string, type: string,
onMouseOver?: (...args) => void onMouseOver?: (...args) => void,
copy?: (path: string, type: string) => void
paste?: (destination: string, type: string) => void
} }

Loading…
Cancel
Save