diff --git a/apps/remix-ide/src/app/files/fileManager.ts b/apps/remix-ide/src/app/files/fileManager.ts index e24680e91f..0a80758214 100644 --- a/apps/remix-ide/src/app/files/fileManager.ts +++ b/apps/remix-ide/src/app/files/fileManager.ts @@ -904,6 +904,50 @@ class FileManager extends Plugin { return exists } + + async moveFileIsAllowed (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 move ${src}. Path does not exist.`) + await this._handleExists(dest, `Cannot move content into ${dest}. Path does not exist.`) + await this._handleIsFile(src, `Cannot move ${src}. Path is not a file.`) + await this._handleIsDir(dest, `Cannot move content into ${dest}. Path is not directory.`) + const fileName = helper.extractNameFromKey(src) + + if (await this.exists(dest + '/' + fileName)) { + return false + } + return true + } catch (e) { + console.log(e) + return false + } + } + + async moveDirIsAllowed (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 move ${src}. Path does not exist.`) + await this._handleExists(dest, `Cannot move content into ${dest}. Path does not exist.`) + await this._handleIsDir(src, `Cannot move ${src}. Path is not directory.`) + await this._handleIsDir(dest, `Cannot move content into ${dest}. Path is not directory.`) + const dirName = helper.extractNameFromKey(src) + if (await this.exists(dest + '/' + dirName) || src === dest) { + return false + } + return true + } catch (e) { + console.log(e) + return false + } + } + /** * Moves a file to a new folder * @param {string} src path of the source file diff --git a/apps/remix-ide/src/app/tabs/locales/en/filePanel.json b/apps/remix-ide/src/app/tabs/locales/en/filePanel.json index cdc270f480..0554b0ffa7 100644 --- a/apps/remix-ide/src/app/tabs/locales/en/filePanel.json +++ b/apps/remix-ide/src/app/tabs/locales/en/filePanel.json @@ -73,6 +73,7 @@ "filePanel.features": "Features", "filePanel.upgradeability": "Upgradeability", "filePanel.ok": "OK", + "filePanel.yes": "Yes", "filePanel.cancel": "Cancel", "filePanel.createNewWorkspace": "create a new workspace", "filePanel.connectToLocalhost": "connect to localhost", @@ -115,6 +116,8 @@ "filePanel.validationErrorMsg": "Special characters are not allowed", "filePanel.reservedKeyword": "Reserved Keyword", "filePanel.reservedKeywordMsg": "File name contains Remix reserved keywords. \"{content}\"", + "filePanel.moveFile": "Moving files", + "filePanel.moveFileMsg1": "Are you sure you want to move {src} to {dest}?", "filePanel.movingFileFailed": "Moving File Failed", "filePanel.movingFileFailedMsg": "Unexpected error while moving file: {src}", "filePanel.movingFolderFailed": "Moving Folder Failed", diff --git a/libs/remix-ui/drag-n-drop/src/lib/drag-n-drop.tsx b/libs/remix-ui/drag-n-drop/src/lib/drag-n-drop.tsx index 6d991d9663..6427dbed12 100644 --- a/libs/remix-ui/drag-n-drop/src/lib/drag-n-drop.tsx +++ b/libs/remix-ui/drag-n-drop/src/lib/drag-n-drop.tsx @@ -27,6 +27,11 @@ export const Draggable = (props: DraggableType) => { destination = props.file, context = useContext(MoveContext) + // delay timer + const [timer, setTimer] = useState() + // folder to open + const [folderToOpen, setFolderToOpen] = useState() + const handleDrop = (event: React.DragEvent) => { event.preventDefault() @@ -50,8 +55,15 @@ export const Draggable = (props: DraggableType) => { const handleDragover = (event: React.DragEvent) => { //Checks if the folder is opened event.preventDefault() - if (destination.isDirectory && !props.expandedPath.includes(destination.path)) { - props.handleClickFolder(destination.path, destination.type) + if (destination.isDirectory && !props.expandedPath.includes(destination.path) && folderToOpen !== destination.path && props.handleClickFolder) { + setFolderToOpen(destination.path) + timer && clearTimeout(timer) + setTimer( + setTimeout(() => { + props.handleClickFolder(destination.path, destination.type) + setFolderToOpen(null) + }, 600) + ) } } @@ -75,7 +87,12 @@ export const Draggable = (props: DraggableType) => { onDrop={(event) => { handleDrop(event) }} - onDragStart={() => { + onDragStart={(event) => { + if (destination && destination.path === '/'){ + event.preventDefault() + event.stopPropagation + } else + if (destination) { handleDrag() } diff --git a/libs/remix-ui/workspace/src/lib/actions/index.ts b/libs/remix-ui/workspace/src/lib/actions/index.ts index e34b9db417..9b8691319d 100644 --- a/libs/remix-ui/workspace/src/lib/actions/index.ts +++ b/libs/remix-ui/workspace/src/lib/actions/index.ts @@ -514,3 +514,16 @@ export const moveFolder = async (src: string, dest: string) => { dispatch(displayPopUp('Oops! An error ocurred while performing moveDir operation.' + error)) } } + +export const moveFileIsAllowed = async (src: string, dest: string) => { + const fileManager = plugin.fileManager + const isAllowed = await fileManager.moveFileIsAllowed(src, dest) + return isAllowed +} + +export const moveFolderIsAllowed = async (src: string, dest: string) => { + const fileManager = plugin.fileManager + const isAllowed = await fileManager.moveDirIsAllowed(src, dest) + return isAllowed +} + 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 b264735b32..69e072ff40 100644 --- a/libs/remix-ui/workspace/src/lib/components/file-explorer.tsx +++ b/libs/remix-ui/workspace/src/lib/components/file-explorer.tsx @@ -2,14 +2,17 @@ import React, {useEffect, useState, useRef, SyntheticEvent} from 'react' // esli import {useIntl} from 'react-intl' import {TreeView} from '@remix-ui/tree-view' // eslint-disable-line import {FileExplorerMenu} from './file-explorer-menu' // eslint-disable-line -import {FileExplorerProps, WorkSpaceState} from '../types' +import {FileExplorerContextMenu} from './file-explorer-context-menu' // eslint-disable-line +import {FileExplorerProps, FileType, WorkSpaceState} from '../types' 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' +import {Drag, Draggable} from '@remix-ui/drag-n-drop' import {ROOT_PATH} from '../utils/constants' +import { fileKeySort } from '../utils' +import { moveFileIsAllowed, moveFolderIsAllowed } from '../actions' export const FileExplorer = (props: FileExplorerProps) => { const intl = useIntl() @@ -31,6 +34,7 @@ export const FileExplorer = (props: FileExplorerProps) => { } = props const [state, setState] = useState(workspaceState) const treeRef = useRef(null) + const [childrenKeys, setChildrenKeys] = useState([]) useEffect(() => { if (contextMenuItems) { @@ -288,32 +292,62 @@ export const FileExplorer = (props: FileExplorerProps) => { props.dispatchHandleExpandPath(expandPath) } - const handleFileMove = (dest: string, src: string) => { + const handleFileMove = async (dest: string, src: string) => { + if(await moveFileIsAllowed(src, dest) === false) return try { - props.dispatchMoveFile(src, dest) + props.modal( + intl.formatMessage({ id: 'filePanel.moveFile' }), + intl.formatMessage({ id: 'filePanel.moveFileMsg1' }, { src, dest }), + intl.formatMessage({ id: 'filePanel.yes' }), + () => props.dispatchMoveFile(src, dest), + intl.formatMessage({ id: 'filePanel.cancel' }), + () => {} + ) } catch (error) { props.modal( - intl.formatMessage({id: 'filePanel.movingFileFailed'}), - intl.formatMessage({id: 'filePanel.movingFileFailedMsg'}, {src}), - intl.formatMessage({id: 'filePanel.close'}), + intl.formatMessage({ id: 'filePanel.movingFileFailed' }), + intl.formatMessage({ id: 'filePanel.movingFileFailedMsg' }, { src }), + intl.formatMessage({ id: 'filePanel.close' }), async () => {} ) } } - const handleFolderMove = (dest: string, src: string) => { + const handleFolderMove = async (dest: string, src: string) => { + if(await moveFolderIsAllowed(src, dest) === false) return try { - props.dispatchMoveFolder(src, dest) + props.modal( + intl.formatMessage({ id: 'filePanel.moveFile' }), + intl.formatMessage({ id: 'filePanel.moveFileMsg1' }, { src, dest }), + intl.formatMessage({ id: 'filePanel.yes' }), + () => props.dispatchMoveFolder(src, dest), + intl.formatMessage({ id: 'filePanel.cancel' }), + () => {} + ) } catch (error) { props.modal( - intl.formatMessage({id: 'filePanel.movingFolderFailed'}), - intl.formatMessage({id: 'filePanel.movingFolderFailedMsg'}, {src}), - intl.formatMessage({id: 'filePanel.close'}), + intl.formatMessage({ id: 'filePanel.movingFolderFailed' }), + intl.formatMessage({ id: 'filePanel.movingFolderFailedMsg' }, { src }), + intl.formatMessage({ id: 'filePanel.close' }), async () => {} ) } } + useEffect(() => { + if (files[ROOT_PATH]){ + try { + const children: FileType[] = files[ROOT_PATH] as any + setChildrenKeys(fileKeySort(children)) + } catch (error) { + setChildrenKeys(Object.keys(files[ROOT_PATH])) + } + } else{ + setChildrenKeys([]) + } + }, [props]) + + return (
@@ -339,10 +373,10 @@ export const FileExplorer = (props: FileExplorerProps) => {
-
+
{files[ROOT_PATH] && - Object.keys(files[ROOT_PATH]).map((key, index) => ( + childrenKeys.map((key, index) => ( { }
+ +
+
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 3fcc358531..a23ed76df3 100644 --- a/libs/remix-ui/workspace/src/lib/components/file-render.tsx +++ b/libs/remix-ui/workspace/src/lib/components/file-render.tsx @@ -8,6 +8,7 @@ import {getPathIcon} from '@remix-ui/helper' import {FileLabel} from './file-label' import {fileDecoration, FileDecorationIcons} from '@remix-ui/file-decorators' import {Draggable} from '@remix-ui/drag-n-drop' +import { fileKeySort } from '../utils' export interface RenderFileProps { file: FileType @@ -30,6 +31,7 @@ export const FileRender = (props: RenderFileProps) => { const [file, setFile] = useState({} as FileType) const [hover, setHover] = useState(false) const [icon, setIcon] = useState('') + const [childrenKeys, setChildrenKeys] = useState([]) useEffect(() => { if (props.file && props.file.path && props.file.type) { @@ -38,6 +40,19 @@ export const FileRender = (props: RenderFileProps) => { } }, [props.file]) + useEffect(() => { + if (file.child) { + try { + const children: FileType[] = file.child as any + setChildrenKeys(fileKeySort(children)) + } catch (e) { + setChildrenKeys(Object.keys(file.child)) + } + } else { + setChildrenKeys([]) + } + }, [file.child, props.expandPath, props.file]) + const labelClass = props.focusEdit.element === file.path ? 'bg-light' @@ -108,7 +123,7 @@ export const FileRender = (props: RenderFileProps) => { > {file.child ? ( - {Object.keys(file.child).map((key, index) => ( + {childrenKeys.map((key, index) => ( { + const directories = Object.keys(children).filter((key: string) => children[key].isDirectory && children[key].name !== '') + + // sort case insensitive + directories.sort((a: string, b: string) => a.toLowerCase().localeCompare(b.toLowerCase())) + + const fileKeys = Object.keys(children).filter((key: string) => !children[key].isDirectory && children[key].name !== '') + // sort case insensitive + fileKeys.sort((a: string, b: string) => a.toLowerCase().localeCompare(b.toLowerCase())) + + // find the children with a blank name + const blankChildren = Object.keys(children).filter((key: string) => children[key].name === '') + + const keys = [...directories, ...fileKeys, ...blankChildren] + return keys +} \ No newline at end of file