Merge pull request #2620 from iamsethsamuel/drag-n-drop

files can now be dragged and drop
pull/2747/head^2
Joseph Izang 2 years ago committed by GitHub
commit 574cd0b833
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 29
      apps/remix-ide/src/app/files/fileManager.ts
  2. 4
      jest.preset.js
  3. 4
      libs/remix-ui/drag-n-drop/.babelrc
  4. 19
      libs/remix-ui/drag-n-drop/.eslintrc.json
  5. 7
      libs/remix-ui/drag-n-drop/README.md
  6. 9
      libs/remix-ui/drag-n-drop/jest.config.js
  7. 2
      libs/remix-ui/drag-n-drop/src/index.ts
  8. 112
      libs/remix-ui/drag-n-drop/src/lib/remix-ui-drag-n-drop.tsx
  9. 23
      libs/remix-ui/drag-n-drop/tsconfig.json
  10. 13
      libs/remix-ui/drag-n-drop/tsconfig.lib.json
  11. 15
      libs/remix-ui/drag-n-drop/tsconfig.spec.json
  12. 1
      libs/remix-ui/tree-view/src/types/index.ts
  13. 10
      libs/remix-ui/workspace/src/lib/actions/index.ts
  14. 12
      libs/remix-ui/workspace/src/lib/components/file-explorer.tsx
  15. 17
      libs/remix-ui/workspace/src/lib/components/file-render.tsx
  16. 17
      libs/remix-ui/workspace/src/lib/contexts/index.ts
  17. 10
      libs/remix-ui/workspace/src/lib/providers/FileSystemProvider.tsx
  18. 2
      libs/remix-ui/workspace/src/lib/remix-ui-workspace.tsx
  19. 1
      libs/remix-ui/workspace/src/lib/types/index.ts
  20. 4
      nx.json
  21. 3
      tsconfig.base.json
  22. 20
      workspace.json

@ -817,6 +817,35 @@ class FileManager extends Plugin {
return exists 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 module.exports = FileManager

@ -0,0 +1,4 @@
const nxPreset = require('@nrwl/jest/preset');
module.exports = { ...nxPreset }

@ -0,0 +1,4 @@
{
"presets": ["@nrwl/react/babel"],
"plugins": []
}

@ -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"
}
}

@ -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).

@ -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'
};

@ -0,0 +1,2 @@
export * from './lib/remix-ui-drag-n-drop';

@ -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<MoveContextType>({
dragged: "",
moveFile: () => {},
currentlyMoved: () => {},
});
export const Drag = (props: DragType) => {
const [dragged, setDragged] = useState<string>("");
return (
<MoveContext.Provider
value={{
dragged: dragged,
moveFile: props.onFileMoved,
currentlyMoved: (path) => {
setDragged(() => path);
},
}}
>
{props.children}
</MoveContext.Provider>
);
};
export const Draggable = (props: DraggableType) => {
const dragRef = useRef<HTMLSpanElement | null>(null),
file = props.file,
context = useContext(MoveContext);
const handleDrop = (event: React.DragEvent<HTMLSpanElement>) => {
event.preventDefault();
if (file.isDirectory) {
context.moveFile(file.path, context.dragged);
}
};
const handleDragover = (event: React.DragEvent<HTMLSpanElement>) => {
//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 (
<span
ref={dragRef}
draggable
onDrop={(event) => {
handleDrop(event);
}}
onDragStart={() => {
if (file) {
handleDrag();
}
}}
onDragOver={(event) => {
if (file && file.isDirectory) {
handleDragover(event);
}
}}
>
{props.children}
</span>
);
};

@ -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"
}
]
}

@ -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"]
}

@ -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"
]
}

@ -23,4 +23,5 @@ export interface TreeViewItemProps {
onContextMenu?: (...args: any) => void, onContextMenu?: (...args: any) => void,
onBlur?: (...args: any) => void, onBlur?: (...args: any) => void,
showIcon?: boolean showIcon?: boolean
expandedPaths?: string[];
} }

@ -462,3 +462,13 @@ const saveAs = (blob, name) => {
} }
}, 0) // 40s }, 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))
}
}

@ -10,6 +10,7 @@ import '../css/file-explorer.css'
import { checkSpecialChars, extractNameFromKey, extractParentFromKey, joinPath } from '@remix-ui/helper' import { checkSpecialChars, extractNameFromKey, extractParentFromKey, joinPath } from '@remix-ui/helper'
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
import { FileRender } from './file-render' import { FileRender } from './file-render'
import { Drag } from "@remix-ui/drag-n-drop"
export const FileExplorer = (props: FileExplorerProps) => { export const FileExplorer = (props: FileExplorerProps) => {
const { name, contextMenuItems, removedContextMenuItems, files, fileState } = props const { name, contextMenuItems, removedContextMenuItems, files, fileState } = props
@ -409,7 +410,16 @@ export const FileExplorer = (props: FileExplorerProps) => {
props.dispatchHandleExpandPath(expandPath) 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 ( return (
<Drag onFileMoved={handleFileMove}>
<div ref={treeRef} tabIndex={0} style={{ outline: "none" }}> <div ref={treeRef} tabIndex={0} style={{ outline: "none" }}>
<TreeView id='treeView'> <TreeView id='treeView'>
<TreeViewItem id="treeViewItem" <TreeViewItem id="treeViewItem"
@ -444,6 +454,7 @@ export const FileExplorer = (props: FileExplorerProps) => {
handleClickFolder={handleClickFolder} handleClickFolder={handleClickFolder}
handleContextMenu={handleContextMenu} handleContextMenu={handleContextMenu}
key={index} key={index}
/>) />)
} }
</TreeView> </TreeView>
@ -473,6 +484,7 @@ export const FileExplorer = (props: FileExplorerProps) => {
/> />
} }
</div> </div>
</Drag>
) )
} }

@ -7,7 +7,7 @@ import { getPathIcon } from '@remix-ui/helper'
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
import { FileLabel } from './file-label' import { FileLabel } from './file-label'
import { fileDecoration, FileDecorationIcons } from '@remix-ui/file-decorators' 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' iconX='pr-3 fa fa-folder'
iconY='pr-3 fa fa-folder-open' iconY='pr-3 fa fa-folder-open'
key={`${file.path + props.index}`} key={`${file.path + props.index}`}
label={<FileLabel fileDecorations={props.fileDecorations} file={file} focusEdit={props.focusEdit} editModeOff={props.editModeOff} />} label={<>
<div className="d-flex flex-row">
<Draggable isDraggable={props.focusEdit.element!==null} file={file} expandedPath={props.expandPath} handleClickFolder={props.handleClickFolder}>
<FileLabel fileDecorations={props.fileDecorations} file={file} focusEdit={props.focusEdit} editModeOff={props.editModeOff} />
</Draggable>
<FileDecorationIcons file={file} fileDecorations={props.fileDecorations}/>
</div>
</>}
onClick={handleFolderClick} onClick={handleFolderClick}
onContextMenu={handleContextMenu} onContextMenu={handleContextMenu}
labelClass={labelClass} labelClass={labelClass}
@ -90,6 +97,7 @@ export const FileRender = (props: RenderFileProps) => {
expand={props.expandPath.includes(file.path)} expand={props.expandPath.includes(file.path)}
onMouseOver={handleMouseOver} onMouseOver={handleMouseOver}
onMouseOut={handleMouseOut} onMouseOut={handleMouseOut}
expandedPaths={props.expandPath}
> >
{ {
file.child ? <TreeView id={`treeView${file.path}`} key={`treeView${file.path}`} {...spreadProps }>{ file.child ? <TreeView id={`treeView${file.path}`} key={`treeView${file.path}`} {...spreadProps }>{
@ -121,7 +129,9 @@ export const FileRender = (props: RenderFileProps) => {
label={ label={
<> <>
<div className="d-flex flex-row"> <div className="d-flex flex-row">
<FileLabel file={file} fileDecorations={props.fileDecorations} focusEdit={props.focusEdit} editModeOff={props.editModeOff} /> <Draggable isDraggable={props.focusEdit.element!==null} file={file} expandedPath={props.expandPath} handleClickFolder={props.handleClickFolder}>
<FileLabel fileDecorations={props.fileDecorations} file={file} focusEdit={props.focusEdit} editModeOff={props.editModeOff} />
</Draggable>
<FileDecorationIcons file={file} fileDecorations={props.fileDecorations}/> <FileDecorationIcons file={file} fileDecorations={props.fileDecorations}/>
</div> </div>
</> </>
@ -132,6 +142,7 @@ export const FileRender = (props: RenderFileProps) => {
labelClass={labelClass} labelClass={labelClass}
onMouseOver={handleMouseOver} onMouseOver={handleMouseOver}
onMouseOut={handleMouseOut} onMouseOut={handleMouseOut}
expandedPaths={props.expandPath}
/> />
) )
} }

@ -29,6 +29,21 @@ export const FileSystemContext = createContext<{
dispatchHandleClickFile: (path: string, type: 'file' | 'folder' | 'gist') => Promise<void> dispatchHandleClickFile: (path: string, type: 'file' | 'folder' | 'gist') => Promise<void>
dispatchHandleExpandPath: (paths: string[]) => Promise<void>, dispatchHandleExpandPath: (paths: string[]) => Promise<void>,
dispatchHandleDownloadFiles: () => Promise<void>, dispatchHandleDownloadFiles: () => Promise<void>,
dispatchHandleRestoreBackup: () => Promise<void>, dispatchHandleRestoreBackup: () => Promise<void>
dispatchCloneRepository: (url: string) => Promise<void> dispatchCloneRepository: (url: string) => Promise<void>
dispatchMoveFile: (src: string, dest: string) => Promise<void>,
}>(null) }>(null)
interface MoveContextType{
dragged: string,
moveFile: (dest: string) => void
currentlyMoved: (path: string) => void
}
export const MoveContext = createContext<MoveContextType>({
dragged:"",
moveFile:( )=> {},
currentlyMoved: () => {}
})

@ -7,7 +7,7 @@ import { FileSystemContext } from '../contexts'
import { browserReducer, browserInitialState } from '../reducers/workspace' import { browserReducer, browserInitialState } from '../reducers/workspace'
import { initWorkspace, fetchDirectory, removeInputField, deleteWorkspace, clearPopUp, publishToGist, createNewFile, setFocusElement, createNewFolder, import { initWorkspace, fetchDirectory, removeInputField, deleteWorkspace, clearPopUp, publishToGist, createNewFile, setFocusElement, createNewFolder,
deletePath, renamePath, copyFile, copyFolder, runScript, emitContextMenuEvent, handleClickFile, handleExpandPath, addInputField, createWorkspace, 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' import { Modal, WorkspaceProps, WorkspaceTemplate } from '../types'
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
import { Workspace } from '../remix-ui-workspace' import { Workspace } from '../remix-ui-workspace'
@ -128,7 +128,9 @@ export const FileSystemProvider = (props: WorkspaceProps) => {
const dispatchCloneRepository = async (url: string) => { const dispatchCloneRepository = async (url: string) => {
await cloneRepository(url) await cloneRepository(url)
} }
const dispatchMoveFile = async (src: string, dest: string) => {
await moveFile(src, dest)
}
useEffect(() => { useEffect(() => {
dispatchInitWorkspace() dispatchInitWorkspace()
}, []) }, [])
@ -231,7 +233,8 @@ export const FileSystemProvider = (props: WorkspaceProps) => {
dispatchHandleExpandPath, dispatchHandleExpandPath,
dispatchHandleDownloadFiles, dispatchHandleDownloadFiles,
dispatchHandleRestoreBackup, dispatchHandleRestoreBackup,
dispatchCloneRepository dispatchCloneRepository,
dispatchMoveFile
} }
return ( return (
<FileSystemContext.Provider value={value}> <FileSystemContext.Provider value={value}>
@ -244,3 +247,4 @@ export const FileSystemProvider = (props: WorkspaceProps) => {
} }
export default FileSystemProvider export default FileSystemProvider

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

@ -98,6 +98,7 @@ export interface FileExplorerProps {
dispatchRemoveInputField:(path: string) => Promise<void>, dispatchRemoveInputField:(path: string) => Promise<void>,
dispatchAddInputField:(path: string, type: 'file' | 'folder') => Promise<void>, dispatchAddInputField:(path: string, type: 'file' | 'folder') => Promise<void>,
dispatchHandleExpandPath: (paths: string[]) => Promise<void> dispatchHandleExpandPath: (paths: string[]) => Promise<void>
dispatchMoveFile: (src: string, dest: string) => Promise<void>,
} }
export interface FileExplorerMenuProps { export interface FileExplorerMenuProps {

@ -204,6 +204,9 @@
}, },
"etherscan": { "etherscan": {
"tags": [] "tags": []
},
"remix-ui-drag-n-drop": {
"tags": []
} }
}, },
"targetDependencies": { "targetDependencies": {
@ -214,4 +217,5 @@
} }
] ]
} }
} }

@ -89,7 +89,8 @@
"libs/remix-ui/permission-handler/src/index.ts" "libs/remix-ui/permission-handler/src/index.ts"
], ],
"@remix-ui/file-decorators": ["libs/remix-ui/file-decorators/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"] "exclude": ["node_modules", "tmp"]

@ -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": { "vyper": {
"root": "apps/vyper", "root": "apps/vyper",
"sourceRoot": "apps/vyper/src", "sourceRoot": "apps/vyper/src",

Loading…
Cancel
Save