From dfa83f8822bcb83f9b4f00fdd31ca512c89ccfd9 Mon Sep 17 00:00:00 2001 From: Seth Samuel Date: Tue, 12 Jul 2022 17:52:08 +0100 Subject: [PATCH] files can be dragged and drop --- apps/remix-ide/src/app/files/fileManager.ts | 29 ++++++ .../src/lib/tree-view-item/tree-view-item.tsx | 90 +++++++++++++++---- libs/remix-ui/tree-view/src/types/index.ts | 5 ++ .../workspace/src/lib/actions/index.ts | 10 +++ .../src/lib/components/file-explorer.tsx | 30 ++++++- .../src/lib/components/file-render.tsx | 6 ++ .../workspace/src/lib/contexts/index.ts | 5 +- .../src/lib/providers/FileSystemProvider.tsx | 11 ++- .../workspace/src/lib/remix-ui-workspace.tsx | 4 +- .../remix-ui/workspace/src/lib/types/index.ts | 4 +- 10 files changed, 168 insertions(+), 26 deletions(-) diff --git a/apps/remix-ide/src/app/files/fileManager.ts b/apps/remix-ide/src/app/files/fileManager.ts index 44b604b8d4..7366fc9589 100644 --- a/apps/remix-ide/src/app/files/fileManager.ts +++ b/apps/remix-ide/src/app/files/fileManager.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 diff --git a/libs/remix-ui/tree-view/src/lib/tree-view-item/tree-view-item.tsx b/libs/remix-ui/tree-view/src/lib/tree-view-item/tree-view-item.tsx index e03e624977..3352e44b43 100644 --- a/libs/remix-ui/tree-view/src/lib/tree-view-item/tree-view-item.tsx +++ b/libs/remix-ui/tree-view/src/lib/tree-view-item/tree-view-item.tsx @@ -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(); + const moveContext = useContext(MoveContext) useEffect(() => { - setIsExpanded(expand) - }, [expand]) + setIsExpanded(expand); + }, [expand]); + const handleDrop = (event: React.DragEvent, isDirectory: boolean,path: string) => { + event.preventDefault() + + if (isDirectory) { + moveContext.moveFile( path); + } + }; + + const handleDragover = ( + event: React.DragEvent, + 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, path: string) => { + if (moveContext.dragged !== path) { + moveContext.currentlyMoved(path); + } + }; + return ( -
  • -
    !controlBehaviour && setIsExpanded(!isExpanded)}> - { children && showIcon ?
    : icon ?
    : null } - - { label } - +
  • { + 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); + } + }} + > +
    !controlBehaviour && setIsExpanded(!isExpanded)} + > + {children && showIcon ? ( +
    + ) : icon ? ( +
    + ) : null} + {label}
    - { isExpanded ? children : null } + {isExpanded ? children : null}
  • - ) -} + ); +}; -export default TreeViewItem +export default TreeViewItem; diff --git a/libs/remix-ui/tree-view/src/types/index.ts b/libs/remix-ui/tree-view/src/types/index.ts index 4f6bfc9e02..8f20c1b8a2 100644 --- a/libs/remix-ui/tree-view/src/types/index.ts +++ b/libs/remix-ui/tree-view/src/types/index.ts @@ -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 } diff --git a/libs/remix-ui/workspace/src/lib/actions/index.ts b/libs/remix-ui/workspace/src/lib/actions/index.ts index 31fe71a453..eaf65dba08 100644 --- a/libs/remix-ui/workspace/src/lib/actions/index.ts +++ b/libs/remix-ui/workspace/src/lib/actions/index.ts @@ -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)) + } +} \ No newline at end of file diff --git a/libs/remix-ui/workspace/src/lib/components/file-explorer.tsx b/libs/remix-ui/workspace/src/lib/components/file-explorer.tsx index 3ccefdb2db..528589afc1 100644 --- a/libs/remix-ui/workspace/src/lib/components/file-explorer.tsx +++ b/libs/remix-ui/workspace/src/lib/components/file-explorer.tsx @@ -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({ + dragged:"", + moveFile:( )=> {}, + currentlyMoved: () => {} +}) + export const FileExplorer = (props: FileExplorerProps) => { const { name, contextMenuItems, removedContextMenuItems, files, fileState } = props const [state, setState] = useState({ @@ -36,6 +48,7 @@ export const FileExplorer = (props: FileExplorerProps) => { }) const [canPaste, setCanPaste] = useState(false) const treeRef = useRef(null) + const [dragged, setDragged] = useState("") useEffect(() => { if (contextMenuItems) { @@ -409,7 +422,21 @@ export const FileExplorer = (props: FileExplorerProps) => { props.dispatchHandleExpandPath(expandPath) } + return ( + { + try { + props.dispatchMoveFile(dragged, dest) + } catch (error) { + props.modal('Moving File Failed', 'Unexpected error while moving file: ' + dragged, 'Close', async () => {}) + } + }, + currentlyMoved:(path)=>{ + setDragged(path) + } + }}>
    { handleClickFolder={handleClickFolder} handleContextMenu={handleContextMenu} key={index} + />) } @@ -473,7 +501,7 @@ export const FileExplorer = (props: FileExplorerProps) => { /> }
    - ) +
    ) } export default FileExplorer diff --git a/libs/remix-ui/workspace/src/lib/components/file-render.tsx b/libs/remix-ui/workspace/src/lib/components/file-render.tsx index 164689899b..5a9f3652b7 100644 --- a/libs/remix-ui/workspace/src/lib/components/file-render.tsx +++ b/libs/remix-ui/workspace/src/lib/components/file-render.tsx @@ -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 ? { @@ -132,6 +135,9 @@ export const FileRender = (props: RenderFileProps) => { labelClass={labelClass} onMouseOver={handleMouseOver} onMouseOut={handleMouseOut} + file={file} + handleClickFolder={props.handleClickFolder} + expandedPaths={props.expandPath} /> ) } diff --git a/libs/remix-ui/workspace/src/lib/contexts/index.ts b/libs/remix-ui/workspace/src/lib/contexts/index.ts index b71dc9e373..56bba8e036 100644 --- a/libs/remix-ui/workspace/src/lib/contexts/index.ts +++ b/libs/remix-ui/workspace/src/lib/contexts/index.ts @@ -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, dispatchFetchDirectory:(path: string) => Promise, dispatchAddInputField:(path: string, type: 'file' | 'folder') => Promise, @@ -29,6 +29,7 @@ export const FileSystemContext = createContext<{ dispatchHandleClickFile: (path: string, type: 'file' | 'folder' | 'gist') => Promise dispatchHandleExpandPath: (paths: string[]) => Promise, dispatchHandleDownloadFiles: () => Promise, - dispatchHandleRestoreBackup: () => Promise, + dispatchHandleRestoreBackup: () => Promise + dispatchMoveFile: (src: string, dest: string) => Promise, dispatchCloneRepository: (url: string) => Promise }>(null) diff --git a/libs/remix-ui/workspace/src/lib/providers/FileSystemProvider.tsx b/libs/remix-ui/workspace/src/lib/providers/FileSystemProvider.tsx index f754869931..fbf1af48dc 100644 --- a/libs/remix-ui/workspace/src/lib/providers/FileSystemProvider.tsx +++ b/libs/remix-ui/workspace/src/lib/providers/FileSystemProvider.tsx @@ -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 ( diff --git a/libs/remix-ui/workspace/src/lib/remix-ui-workspace.tsx b/libs/remix-ui/workspace/src/lib/remix-ui-workspace.tsx index b56c83b5de..f96a252c26 100644 --- a/libs/remix-ui/workspace/src/lib/remix-ui-workspace.tsx +++ b/libs/remix-ui/workspace/src/lib/remix-ui-workspace.tsx @@ -330,6 +330,7 @@ export function Workspace () { dispatchRemoveInputField={global.dispatchRemoveInputField} dispatchAddInputField={global.dispatchAddInputField} dispatchHandleExpandPath={global.dispatchHandleExpandPath} + dispatchMoveFile={global.dispatchMoveFile} /> } @@ -367,6 +368,7 @@ export function Workspace () { dispatchRemoveInputField={global.dispatchRemoveInputField} dispatchAddInputField={global.dispatchAddInputField} dispatchHandleExpandPath={global.dispatchHandleExpandPath} + dispatchMoveFile={global.dispatchMoveFile} /> } @@ -378,4 +380,4 @@ export function Workspace () { ) } -export default Workspace +export default Workspace \ No newline at end of file diff --git a/libs/remix-ui/workspace/src/lib/types/index.ts b/libs/remix-ui/workspace/src/lib/types/index.ts index 93b6d9f8c9..cc78ca6a14 100644 --- a/libs/remix-ui/workspace/src/lib/types/index.ts +++ b/libs/remix-ui/workspace/src/lib/types/index.ts @@ -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, dispatchAddInputField:(path: string, type: 'file' | 'folder') => Promise, dispatchHandleExpandPath: (paths: string[]) => Promise + dispatchMoveFile: (src: string, dest: string) => Promise, + } export interface FileExplorerMenuProps {