files can be dragged and drop

pull/2620/head
Seth Samuel 2 years ago
parent a2863dbd26
commit dfa83f8822
  1. 29
      apps/remix-ide/src/app/files/fileManager.ts
  2. 90
      libs/remix-ui/tree-view/src/lib/tree-view-item/tree-view-item.tsx
  3. 5
      libs/remix-ui/tree-view/src/types/index.ts
  4. 10
      libs/remix-ui/workspace/src/lib/actions/index.ts
  5. 30
      libs/remix-ui/workspace/src/lib/components/file-explorer.tsx
  6. 6
      libs/remix-ui/workspace/src/lib/components/file-render.tsx
  7. 5
      libs/remix-ui/workspace/src/lib/contexts/index.ts
  8. 11
      libs/remix-ui/workspace/src/lib/providers/FileSystemProvider.tsx
  9. 2
      libs/remix-ui/workspace/src/lib/remix-ui-workspace.tsx
  10. 4
      libs/remix-ui/workspace/src/lib/types/index.ts

@ -817,6 +817,35 @@ class FileManager extends Plugin {
return exists
}
/**
* Moves a file to a new folder
* @param {string} src path of the source file
* @param {string} dest path of the destrination file
* @returns {void}
*/
async moveFile(src: string, dest: string) {
try {
src = this.normalize(src)
dest = this.normalize(dest)
src = this.limitPluginScope(src)
dest = this.limitPluginScope(dest)
await this._handleExists(src, `Cannot copy from ${src}. Path does not exist.`)
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)
let movedFilePath = dest + ( '/' + `${helper.extractNameFromKey(src)}`)
movedFilePath = await helper.createNonClashingNameAsync(movedFilePath, this)
await this.writeFile(movedFilePath, content)
await this.remove(src)
} catch (e) {
throw new Error(e)
}
}
}
module.exports = FileManager

@ -1,27 +1,83 @@
import React, { useState, useEffect } from 'react' // eslint-disable-line
import { TreeViewItemProps } from '../../types'
import React, { useState, useEffect, useRef, useMemo, useContext } from 'react'; // eslint-disable-line
import { TreeViewItemProps } from '../../types';
import './tree-view-item.css'
import './tree-view-item.css';
import { MoveContext } from '../../../../workspace/src/lib/components/file-explorer'
export const TreeViewItem = (props: TreeViewItemProps) => {
const { id, children, label, labelClass, expand, iconX = 'fas fa-caret-right', iconY = 'fas fa-caret-down', icon, controlBehaviour = false, innerRef, showIcon = true, ...otherProps } = props
const [isExpanded, setIsExpanded] = useState(false)
const { id, children, label, labelClass, expand, iconX = 'fas fa-caret-right', iconY = 'fas fa-caret-down', icon, controlBehaviour = false, innerRef, file, showIcon = true, ...otherProps } = props;
const [isExpanded, setIsExpanded] = useState(false);
const dragRef = useRef<HTMLLIElement>();
const moveContext = useContext(MoveContext)
useEffect(() => {
setIsExpanded(expand)
}, [expand])
setIsExpanded(expand);
}, [expand]);
const handleDrop = (event: React.DragEvent<HTMLLIElement>, isDirectory: boolean,path: string) => {
event.preventDefault()
if (isDirectory) {
moveContext.moveFile( path);
}
};
const handleDragover = (
event: React.DragEvent<HTMLLIElement>,
isDirectory: boolean,
path: string,
type: string
) => {
//Checks if the folder is opened
event.preventDefault();
if (isDirectory !== null && !props.expandedPaths.includes(path)) {
props.handleClickFolder(path, type);
}
};
const handleDrag = (event: React.DragEvent<HTMLLIElement>, path: string) => {
if (moveContext.dragged !== path) {
moveContext.currentlyMoved(path);
}
};
return (
<li ref={innerRef} key={`treeViewLi${id}`} data-id={`treeViewLi${id}`} className='li_tv' {...otherProps}>
<div key={`treeViewDiv${id}`} data-id={`treeViewDiv${id}`} className={`d-flex flex-row align-items-center ${labelClass}`} onClick={() => !controlBehaviour && setIsExpanded(!isExpanded)}>
{ children && showIcon ? <div className={isExpanded ? `px-1 ${iconY} caret caret_tv` : `px-1 ${iconX} caret caret_tv`} style={{ visibility: children ? 'visible' : 'hidden' }}></div> : icon ? <div className={`pr-3 pl-1 ${icon} caret caret_tv`}></div> : null }
<span className='w-100 pl-1'>
{ label }
</span>
<li draggable ref={dragRef} key={`treeViewLi${id}`} data-id={`treeViewLi${id}`} className="li_tv" {...otherProps} id={file ? file.path : ""}
onDragOver={event => {
if (file && file.isDirectory) {
handleDragover(event, file.isDirectory, file.path, file.type);
}}}
onDrop={(event) => {handleDrop(event, file ? file.isDirectory : false ,file ? file.path : null)}}
onDragStart={event => {
if (file) {
handleDrag(event, file.path);
}
}}
>
<div
key={`treeViewDiv${id}`}
data-id={`treeViewDiv${id}`}
className={`d-flex flex-row align-items-center ${labelClass}`}
onClick={() => !controlBehaviour && setIsExpanded(!isExpanded)}
>
{children && showIcon ? (
<div
className={
isExpanded
? `px-1 ${iconY} caret caret_tv`
: `px-1 ${iconX} caret caret_tv`
}
style={{ visibility: children ? 'visible' : 'hidden' }}
></div>
) : icon ? (
<div className={`pr-3 pl-1 ${icon} caret caret_tv`}></div>
) : null}
<span className="w-100 pl-1">{label}</span>
</div>
{ isExpanded ? children : null }
{isExpanded ? children : null}
</li>
)
}
);
};
export default TreeViewItem
export default TreeViewItem;

@ -1,4 +1,6 @@
/* eslint-disable no-undef */
import { FileType } from 'libs/remix-ui/workspace/src/lib/types/index'
export interface TreeViewProps {
children?: React.ReactNode,
id?: string
@ -23,4 +25,7 @@ export interface TreeViewItemProps {
onContextMenu?: (...args: any) => void,
onBlur?: (...args: any) => void,
showIcon?: boolean
expandedPaths?: string[];
handleClickFolder?: (path: string, type: string) => void;
file?: FileType
}

@ -462,3 +462,13 @@ const saveAs = (blob, name) => {
}
}, 0) // 40s
}
export const moveFile = async (src: string, dest: string) => {
const fileManager = plugin.fileManager
try {
await fileManager.moveFile(src, dest)
} catch (error) {
dispatch(displayPopUp('Oops! An error ocurred while performing moveFile operation.' + error))
}
}

@ -11,6 +11,18 @@ import { checkSpecialChars, extractNameFromKey, extractParentFromKey, joinPath }
// eslint-disable-next-line @typescript-eslint/no-unused-vars
import { FileRender } from './file-render'
interface MoveContextType{
dragged: string,
moveFile: (dest: string) => void
currentlyMoved: (path: string) => void
}
export const MoveContext = React.createContext<MoveContextType>({
dragged:"",
moveFile:( )=> {},
currentlyMoved: () => {}
})
export const FileExplorer = (props: FileExplorerProps) => {
const { name, contextMenuItems, removedContextMenuItems, files, fileState } = props
const [state, setState] = useState<FileExplorerState>({
@ -36,6 +48,7 @@ export const FileExplorer = (props: FileExplorerProps) => {
})
const [canPaste, setCanPaste] = useState(false)
const treeRef = useRef<HTMLDivElement>(null)
const [dragged, setDragged] = useState<string>("")
useEffect(() => {
if (contextMenuItems) {
@ -409,7 +422,21 @@ export const FileExplorer = (props: FileExplorerProps) => {
props.dispatchHandleExpandPath(expandPath)
}
return (
<MoveContext.Provider value={{
dragged: dragged,
moveFile: ( dest: string) => {
try {
props.dispatchMoveFile(dragged, dest)
} catch (error) {
props.modal('Moving File Failed', 'Unexpected error while moving file: ' + dragged, 'Close', async () => {})
}
},
currentlyMoved:(path)=>{
setDragged(path)
}
}}>
<div ref={treeRef} tabIndex={0} style={{ outline: "none" }}>
<TreeView id='treeView'>
<TreeViewItem id="treeViewItem"
@ -444,6 +471,7 @@ export const FileExplorer = (props: FileExplorerProps) => {
handleClickFolder={handleClickFolder}
handleContextMenu={handleContextMenu}
key={index}
/>)
}
</TreeView>
@ -473,7 +501,7 @@ export const FileExplorer = (props: FileExplorerProps) => {
/>
}
</div>
)
</MoveContext.Provider> )
}
export default FileExplorer

@ -90,6 +90,9 @@ export const FileRender = (props: RenderFileProps) => {
expand={props.expandPath.includes(file.path)}
onMouseOver={handleMouseOver}
onMouseOut={handleMouseOut}
file={file}
handleClickFolder={props.handleClickFolder}
expandedPaths={props.expandPath}
>
{
file.child ? <TreeView id={`treeView${file.path}`} key={`treeView${file.path}`} {...spreadProps }>{
@ -132,6 +135,9 @@ export const FileRender = (props: RenderFileProps) => {
labelClass={labelClass}
onMouseOver={handleMouseOver}
onMouseOut={handleMouseOut}
file={file}
handleClickFolder={props.handleClickFolder}
expandedPaths={props.expandPath}
/>
)
}

@ -4,7 +4,7 @@ import { BrowserState } from '../reducers/workspace'
export const FileSystemContext = createContext<{
fs: BrowserState,
modal:(title: string | JSX.Element, message: string | JSX.Element, okLabel: string, okFn: () => void, cancelLabel?: string, cancelFn?: () => void) => void,
modal:(title: string, message: string | JSX.Element, okLabel: string, okFn: () => void, cancelLabel?: string, cancelFn?: () => void) => void,
dispatchInitWorkspace:() => Promise<void>,
dispatchFetchDirectory:(path: string) => Promise<void>,
dispatchAddInputField:(path: string, type: 'file' | 'folder') => Promise<void>,
@ -29,6 +29,7 @@ export const FileSystemContext = createContext<{
dispatchHandleClickFile: (path: string, type: 'file' | 'folder' | 'gist') => Promise<void>
dispatchHandleExpandPath: (paths: string[]) => Promise<void>,
dispatchHandleDownloadFiles: () => Promise<void>,
dispatchHandleRestoreBackup: () => Promise<void>,
dispatchHandleRestoreBackup: () => Promise<void>
dispatchMoveFile: (src: string, dest: string) => Promise<void>,
dispatchCloneRepository: (url: string) => Promise<void>
}>(null)

@ -5,9 +5,7 @@ import { Toaster } from '@remix-ui/toaster' // eslint-disable-line
// eslint-disable-next-line @typescript-eslint/no-unused-vars
import { FileSystemContext } from '../contexts'
import { browserReducer, browserInitialState } from '../reducers/workspace'
import { initWorkspace, fetchDirectory, removeInputField, deleteWorkspace, clearPopUp, publishToGist, createNewFile, setFocusElement, createNewFolder,
deletePath, renamePath, copyFile, copyFolder, runScript, emitContextMenuEvent, handleClickFile, handleExpandPath, addInputField, createWorkspace,
fetchWorkspaceDirectory, renameWorkspace, switchToWorkspace, uploadFile, handleDownloadFiles, restoreBackupZip, cloneRepository } from '../actions'
import { initWorkspace, fetchDirectory, removeInputField, deleteWorkspace, clearPopUp, publishToGist, createNewFile, setFocusElement, createNewFolder, deletePath, renamePath, copyFile, copyFolder, runScript, emitContextMenuEvent, handleClickFile, handleExpandPath, addInputField, createWorkspace, fetchWorkspaceDirectory, renameWorkspace, switchToWorkspace, uploadFile, handleDownloadFiles, restoreBackupZip, moveFile, cloneRepository } from '../actions'
import { Modal, WorkspaceProps, WorkspaceTemplate } from '../types'
// eslint-disable-next-line @typescript-eslint/no-unused-vars
import { Workspace } from '../remix-ui-workspace'
@ -129,6 +127,10 @@ export const FileSystemProvider = (props: WorkspaceProps) => {
await cloneRepository(url)
}
const dispatchMoveFile = async (src: string, dest: string) => {
await moveFile(src, dest)
}
useEffect(() => {
dispatchInitWorkspace()
}, [])
@ -231,7 +233,8 @@ export const FileSystemProvider = (props: WorkspaceProps) => {
dispatchHandleExpandPath,
dispatchHandleDownloadFiles,
dispatchHandleRestoreBackup,
dispatchCloneRepository
dispatchCloneRepository,
dispatchMoveFile
}
return (
<FileSystemContext.Provider value={value}>

@ -330,6 +330,7 @@ export function Workspace () {
dispatchRemoveInputField={global.dispatchRemoveInputField}
dispatchAddInputField={global.dispatchAddInputField}
dispatchHandleExpandPath={global.dispatchHandleExpandPath}
dispatchMoveFile={global.dispatchMoveFile}
/>
}
</div>
@ -367,6 +368,7 @@ export function Workspace () {
dispatchRemoveInputField={global.dispatchRemoveInputField}
dispatchAddInputField={global.dispatchAddInputField}
dispatchHandleExpandPath={global.dispatchHandleExpandPath}
dispatchMoveFile={global.dispatchMoveFile}
/>
}
</div>

@ -14,7 +14,7 @@ export interface JSONStandardInput {
};
}
export type MenuItems = action[]
export type WorkspaceTemplate = 'gist-template' | 'code-template' | 'remixDefault' | 'blank' | 'ozerc20' | 'zeroxErc20' | 'ozerc721'
export type WorkspaceTemplate = 'gist-template' | 'code-template' | 'remixDefault' | 'blank' | 'erc20'
export interface WorkspaceProps {
plugin: {
setWorkspace: ({ name: string, isLocalhost: boolean }, setEvent: boolean) => void,
@ -98,6 +98,8 @@ export interface FileExplorerProps {
dispatchRemoveInputField:(path: string) => Promise<void>,
dispatchAddInputField:(path: string, type: 'file' | 'folder') => Promise<void>,
dispatchHandleExpandPath: (paths: string[]) => Promise<void>
dispatchMoveFile: (src: string, dest: string) => Promise<void>,
}
export interface FileExplorerMenuProps {

Loading…
Cancel
Save