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/jest.preset.js b/jest.preset.js new file mode 100644 index 0000000000..a7ccccd3f1 --- /dev/null +++ b/jest.preset.js @@ -0,0 +1,4 @@ + + const nxPreset = require('@nrwl/jest/preset'); + + module.exports = { ...nxPreset } \ No newline at end of file diff --git a/libs/remix-ui/drag-n-drop/.babelrc b/libs/remix-ui/drag-n-drop/.babelrc new file mode 100644 index 0000000000..09d67939cc --- /dev/null +++ b/libs/remix-ui/drag-n-drop/.babelrc @@ -0,0 +1,4 @@ +{ + "presets": ["@nrwl/react/babel"], + "plugins": [] +} diff --git a/libs/remix-ui/drag-n-drop/.eslintrc.json b/libs/remix-ui/drag-n-drop/.eslintrc.json new file mode 100644 index 0000000000..0d43d424e3 --- /dev/null +++ b/libs/remix-ui/drag-n-drop/.eslintrc.json @@ -0,0 +1,19 @@ +{ + "env": { + "browser": true, + "es6": true + }, + "extends": "../../../.eslintrc.json", + "globals": { + "Atomics": "readonly", + "SharedArrayBuffer": "readonly" + }, + "parserOptions": { + "ecmaVersion": 11, + "sourceType": "module" + }, + "rules": { + "no-unused-vars": "off", + "@typescript-eslint/no-unused-vars": "error" + } +} diff --git a/libs/remix-ui/drag-n-drop/README.md b/libs/remix-ui/drag-n-drop/README.md new file mode 100644 index 0000000000..cbe7a6ff0e --- /dev/null +++ b/libs/remix-ui/drag-n-drop/README.md @@ -0,0 +1,7 @@ +# remix-ui-drag-n-drop + +This library was generated with [Nx](https://nx.dev). + +## Running unit tests + +Run `nx test remix-ui-drag-n-drop` to execute the unit tests via [Jest](https://jestjs.io). diff --git a/libs/remix-ui/drag-n-drop/jest.config.js b/libs/remix-ui/drag-n-drop/jest.config.js new file mode 100644 index 0000000000..1a5729e026 --- /dev/null +++ b/libs/remix-ui/drag-n-drop/jest.config.js @@ -0,0 +1,9 @@ +module.exports = { + displayName: 'remix-ui-drag-n-drop', + preset: '../../../jest.preset.js', + transform: { + '^.+\\.[tj]sx?$': 'babel-jest' + }, + moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'], + coverageDirectory: '../../../coverage/libs/remix-ui/drag-n-drop' +}; diff --git a/libs/remix-ui/drag-n-drop/src/index.ts b/libs/remix-ui/drag-n-drop/src/index.ts new file mode 100644 index 0000000000..40aad7289f --- /dev/null +++ b/libs/remix-ui/drag-n-drop/src/index.ts @@ -0,0 +1,2 @@ + +export * from './lib/remix-ui-drag-n-drop'; diff --git a/libs/remix-ui/drag-n-drop/src/lib/remix-ui-drag-n-drop.tsx b/libs/remix-ui/drag-n-drop/src/lib/remix-ui-drag-n-drop.tsx new file mode 100644 index 0000000000..39433bbe51 --- /dev/null +++ b/libs/remix-ui/drag-n-drop/src/lib/remix-ui-drag-n-drop.tsx @@ -0,0 +1,112 @@ +import React, { + createContext, + ReactNode, + useContext, + useRef, + useState, +} from "react"; + + +export interface FileType { + path: string, + name: string, + isDirectory: boolean, + type: 'folder' | 'file' | 'gist', + child?: File[] +} + +interface MoveContextType { + dragged: string; + isDraggable?: boolean; + moveFile: (dest: string, dragged: string) => void; + currentlyMoved: (path: string) => void; +} +interface DraggableType { + children: ReactNode; + file: FileType; + isDraggable?: boolean; + expandedPath: string[]; + handleClickFolder: (path: string, type: string) => void; +} + +interface DragType { + children: ReactNode; + onFileMoved: (dest: string, dragged: string) => void; +} + +export const MoveContext = createContext({ + dragged: "", + moveFile: () => {}, + currentlyMoved: () => {}, +}); + +export const Drag = (props: DragType) => { + const [dragged, setDragged] = useState(""); + + return ( + { + setDragged(() => path); + }, + }} + > + {props.children} + + ); +}; + +export const Draggable = (props: DraggableType) => { + const dragRef = useRef(null), + file = props.file, + context = useContext(MoveContext); + + const handleDrop = (event: React.DragEvent) => { + event.preventDefault(); + + if (file.isDirectory) { + context.moveFile(file.path, context.dragged); + } + }; + + const handleDragover = (event: React.DragEvent) => { + //Checks if the folder is opened + event.preventDefault(); + if (file.isDirectory && !props.expandedPath.includes(file.path)) { + props.handleClickFolder(file.path, file.type); + } + }; + const handleDrag = () => { + if (context.dragged !== file.path) { + context.currentlyMoved(file.path); + } + }; + + if (props.isDraggable) { + return <>{props.children}; + } + + return ( + { + handleDrop(event); + }} + onDragStart={() => { + if (file) { + handleDrag(); + } + }} + onDragOver={(event) => { + if (file && file.isDirectory) { + handleDragover(event); + } + }} + > + {props.children} + + ); +}; diff --git a/libs/remix-ui/drag-n-drop/tsconfig.json b/libs/remix-ui/drag-n-drop/tsconfig.json new file mode 100644 index 0000000000..29741baf11 --- /dev/null +++ b/libs/remix-ui/drag-n-drop/tsconfig.json @@ -0,0 +1,23 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "jsx": "react-jsx", + "allowJs": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "forceConsistentCasingInFileNames": true, + "strict": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true + }, + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + }, + { + "path": "./tsconfig.spec.json" + } + ] +} \ No newline at end of file diff --git a/libs/remix-ui/drag-n-drop/tsconfig.lib.json b/libs/remix-ui/drag-n-drop/tsconfig.lib.json new file mode 100644 index 0000000000..b560bc4dec --- /dev/null +++ b/libs/remix-ui/drag-n-drop/tsconfig.lib.json @@ -0,0 +1,13 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "types": ["node"] + }, + "files": [ + "../../../node_modules/@nrwl/react/typings/cssmodule.d.ts", + "../../../node_modules/@nrwl/react/typings/image.d.ts" + ], + "exclude": ["**/*.spec.ts", "**/*.spec.tsx"], + "include": ["**/*.js", "**/*.jsx", "**/*.ts", "**/*.tsx"] +} diff --git a/libs/remix-ui/drag-n-drop/tsconfig.spec.json b/libs/remix-ui/drag-n-drop/tsconfig.spec.json new file mode 100644 index 0000000000..1798b378a9 --- /dev/null +++ b/libs/remix-ui/drag-n-drop/tsconfig.spec.json @@ -0,0 +1,15 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "module": "commonjs", + "types": ["jest", "node"] + }, + "include": [ + "**/*.spec.ts", + "**/*.spec.tsx", + "**/*.spec.js", + "**/*.spec.jsx", + "**/*.d.ts" + ] +} diff --git a/libs/remix-ui/tree-view/src/types/index.ts b/libs/remix-ui/tree-view/src/types/index.ts index 4f6bfc9e02..9827b7f2d4 100644 --- a/libs/remix-ui/tree-view/src/types/index.ts +++ b/libs/remix-ui/tree-view/src/types/index.ts @@ -23,4 +23,5 @@ export interface TreeViewItemProps { onContextMenu?: (...args: any) => void, onBlur?: (...args: any) => void, showIcon?: boolean + expandedPaths?: string[]; } 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..bd11d75fb5 100644 --- a/libs/remix-ui/workspace/src/lib/components/file-explorer.tsx +++ b/libs/remix-ui/workspace/src/lib/components/file-explorer.tsx @@ -10,6 +10,7 @@ import '../css/file-explorer.css' import { checkSpecialChars, extractNameFromKey, extractParentFromKey, joinPath } from '@remix-ui/helper' // eslint-disable-next-line @typescript-eslint/no-unused-vars import { FileRender } from './file-render' +import { Drag } from "@remix-ui/drag-n-drop" export const FileExplorer = (props: FileExplorerProps) => { const { name, contextMenuItems, removedContextMenuItems, files, fileState } = props @@ -409,7 +410,16 @@ export const FileExplorer = (props: FileExplorerProps) => { props.dispatchHandleExpandPath(expandPath) } + const handleFileMove = (dest: string, dragged:string)=>{ + try { + props.dispatchMoveFile(dragged, dest) + } catch (error) { + props.modal('Moving File Failed', 'Unexpected error while moving file: ' + dragged, 'Close', async () => {}) + } +} + return ( +
{ handleClickFolder={handleClickFolder} handleContextMenu={handleContextMenu} key={index} + />) } @@ -473,6 +484,7 @@ export const FileExplorer = (props: FileExplorerProps) => { /> }
+
) } 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..d535a0ca89 100644 --- a/libs/remix-ui/workspace/src/lib/components/file-render.tsx +++ b/libs/remix-ui/workspace/src/lib/components/file-render.tsx @@ -7,7 +7,7 @@ import { getPathIcon } from '@remix-ui/helper' // eslint-disable-next-line @typescript-eslint/no-unused-vars import { FileLabel } from './file-label' import { fileDecoration, FileDecorationIcons } from '@remix-ui/file-decorators' - +import {Draggable} from "@remix-ui/drag-n-drop" @@ -82,7 +82,14 @@ export const FileRender = (props: RenderFileProps) => { iconX='pr-3 fa fa-folder' iconY='pr-3 fa fa-folder-open' key={`${file.path + props.index}`} - label={} + label={<> +
+ + + + +
+ } onClick={handleFolderClick} onContextMenu={handleContextMenu} labelClass={labelClass} @@ -90,6 +97,7 @@ export const FileRender = (props: RenderFileProps) => { expand={props.expandPath.includes(file.path)} onMouseOver={handleMouseOver} onMouseOut={handleMouseOut} + expandedPaths={props.expandPath} > { file.child ? { @@ -119,11 +127,13 @@ export const FileRender = (props: RenderFileProps) => { id={`treeViewItem${file.path}`} key={`treeView${file.path}`} label={ - <> -
- - -
+ <> +
+ + + + +
} onClick={handleFileClick} @@ -132,6 +142,7 @@ export const FileRender = (props: RenderFileProps) => { labelClass={labelClass} onMouseOver={handleMouseOver} onMouseOut={handleMouseOut} + 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..2ba8b7394b 100644 --- a/libs/remix-ui/workspace/src/lib/contexts/index.ts +++ b/libs/remix-ui/workspace/src/lib/contexts/index.ts @@ -29,6 +29,21 @@ export const FileSystemContext = createContext<{ dispatchHandleClickFile: (path: string, type: 'file' | 'folder' | 'gist') => Promise dispatchHandleExpandPath: (paths: string[]) => Promise, dispatchHandleDownloadFiles: () => Promise, - dispatchHandleRestoreBackup: () => Promise, + dispatchHandleRestoreBackup: () => Promise dispatchCloneRepository: (url: string) => Promise + dispatchMoveFile: (src: string, dest: string) => Promise, + }>(null) + +interface MoveContextType{ + dragged: string, + moveFile: (dest: string) => void + currentlyMoved: (path: string) => void +} + +export const MoveContext = createContext({ + dragged:"", + moveFile:( )=> {}, + currentlyMoved: () => {} +}) + \ No newline at end of file diff --git a/libs/remix-ui/workspace/src/lib/providers/FileSystemProvider.tsx b/libs/remix-ui/workspace/src/lib/providers/FileSystemProvider.tsx index f754869931..985f531e9a 100644 --- a/libs/remix-ui/workspace/src/lib/providers/FileSystemProvider.tsx +++ b/libs/remix-ui/workspace/src/lib/providers/FileSystemProvider.tsx @@ -7,7 +7,7 @@ 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' + fetchWorkspaceDirectory, renameWorkspace, switchToWorkspace, uploadFile, handleDownloadFiles, restoreBackupZip, cloneRepository, moveFile } from '../actions' import { Modal, WorkspaceProps, WorkspaceTemplate } from '../types' // eslint-disable-next-line @typescript-eslint/no-unused-vars import { Workspace } from '../remix-ui-workspace' @@ -128,7 +128,9 @@ export const FileSystemProvider = (props: WorkspaceProps) => { const dispatchCloneRepository = async (url: string) => { 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 ( @@ -244,3 +247,4 @@ export const FileSystemProvider = (props: WorkspaceProps) => { } export default FileSystemProvider + 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..bdb308587b 100644 --- a/libs/remix-ui/workspace/src/lib/types/index.ts +++ b/libs/remix-ui/workspace/src/lib/types/index.ts @@ -98,6 +98,7 @@ 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 { diff --git a/nx.json b/nx.json index aeaf1074ad..c38fc5aad7 100644 --- a/nx.json +++ b/nx.json @@ -204,6 +204,9 @@ }, "etherscan": { "tags": [] + }, + "remix-ui-drag-n-drop": { + "tags": [] } }, "targetDependencies": { @@ -214,4 +217,5 @@ } ] } + } \ No newline at end of file diff --git a/tsconfig.base.json b/tsconfig.base.json index 9174b23427..63f825d949 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -89,7 +89,8 @@ "libs/remix-ui/permission-handler/src/index.ts" ], "@remix-ui/file-decorators": ["libs/remix-ui/file-decorators/src/index.ts"], - "@remix-ui/tooltip-popup": ["libs/remix-ui/tooltip-popup/src/index.ts"] + "@remix-ui/tooltip-popup": ["libs/remix-ui/tooltip-popup/src/index.ts"], + "@remix-ui/drag-n-drop": ["libs/remix-ui/drag-n-drop/src/index.ts"] } }, "exclude": ["node_modules", "tmp"] diff --git a/workspace.json b/workspace.json index f14281de0d..8c0a1168db 100644 --- a/workspace.json +++ b/workspace.json @@ -1516,6 +1516,26 @@ } } }, + "remix-ui-drag-n-drop": { + "root": "libs/remix-ui/drag-n-drop", + "sourceRoot": "libs/remix-ui/drag-n-drop/src", + "projectType": "library", + "architect": { + "lint": { + "builder": "@nrwl/linter:lint", + "options": { + "linter": "eslint", + "config": "libs/remix-ui/drag-n-drop/.eslintrc.json", + "tsConfig": [ + "libs/remix-ui/drag-n-drop/tsconfig.lib.json" + ], + "exclude": [ + "**/node_modules/**" + ] + } + } + } + }, "vyper": { "root": "apps/vyper", "sourceRoot": "apps/vyper/src",