diff --git a/apps/remix-ide-e2e/src/tests/file_explorer_dragdrop.test.ts b/apps/remix-ide-e2e/src/tests/file_explorer_dragdrop.test.ts index edcf90d881d..949bf86d59f 100644 --- a/apps/remix-ide-e2e/src/tests/file_explorer_dragdrop.test.ts +++ b/apps/remix-ide-e2e/src/tests/file_explorer_dragdrop.test.ts @@ -3,101 +3,101 @@ import { NightwatchBrowser } from 'nightwatch' import init from '../helpers/init' const checkBrowserIsChrome = function (browser: NightwatchBrowser) { - return browser.browserName.indexOf('chrome') > -1 + return browser.browserName.indexOf('chrome') > -1 } module.exports = { - '@disabled': true, - before: function (browser: NightwatchBrowser, done: VoidFunction) { - init(browser, done) - }, - 'drag and drop file from root to contracts #group1 ': function (browser: NightwatchBrowser) { - if (checkBrowserIsChrome(browser)) { - browser - .clickLaunchIcon('filePanel') - .waitForElementVisible('*[data-id="treeViewLitreeViewItemcontracts"]') - .findElement('*[data-id="treeViewLitreeViewItemcontracts"]', (el) => { - console.log((el as any).value.getId()) - const id = (el as any).value.getId() - browser - .waitForElementVisible('li[data-id="treeViewLitreeViewItemREADME.txt"]') - .dragAndDrop('li[data-id="treeViewLitreeViewItemREADME.txt"]', id) - .waitForElementPresent('[data-id="fileSystemModalDialogModalFooter-react"] .modal-ok') - .execute(function () { (document.querySelector('[data-id="fileSystemModalDialogModalFooter-react"] .modal-ok') as HTMLElement).click() }) - .waitForElementVisible('li[data-id="treeViewLitreeViewItemcontracts/README.txt"]') - }) - } - }, + '@disabled': true, + before: function (browser: NightwatchBrowser, done: VoidFunction) { + init(browser, done) + }, + 'drag and drop file from root to contracts #group1 ': function (browser: NightwatchBrowser) { + if (checkBrowserIsChrome(browser)) { + browser + .clickLaunchIcon('filePanel') + .waitForElementVisible('*[data-id="treeViewLitreeViewItemcontracts"]') + .findElement('*[data-id="treeViewLitreeViewItemcontracts"]', (el) => { + console.log((el as any).value.getId()) + const id = (el as any).value.getId() + browser + .waitForElementVisible('li[data-id="treeViewLitreeViewItemREADME.txt"]') + .dragAndDrop('li[data-id="treeViewLitreeViewItemREADME.txt"]', id) + .waitForElementPresent('[data-id="fileSystemModalDialogModalFooter-react"] .modal-ok') + .execute(function () { (document.querySelector('[data-id="fileSystemModalDialogModalFooter-react"] .modal-ok') as HTMLElement).click() }) + .waitForElementVisible('li[data-id="treeViewLitreeViewItemcontracts/README.txt"]') + }) + } + }, - 'drag and drop file from contracts to root #group1': function (browser: NightwatchBrowser) { - if (checkBrowserIsChrome(browser)) { - browser.findElement('*[data-id="treeViewUltreeViewMenu"]', (el) => { - console.log((el as any).value.getId()) - const id = (el as any).value.getId() - browser - .waitForElementVisible('li[data-id="treeViewLitreeViewItemcontracts/1_Storage.sol"]') - .dragAndDrop('li[data-id="treeViewLitreeViewItemcontracts/1_Storage.sol"]', id) - .waitForElementPresent('[data-id="fileSystemModalDialogModalFooter-react"] .modal-ok') - .execute(function () { (document.querySelector('[data-id="fileSystemModalDialogModalFooter-react"] .modal-ok') as HTMLElement).click() }) - }) - browser.pause(1000) - .waitForElementVisible('li[data-id="treeViewLitreeViewItem1_Storage.sol"]') - } - }, - 'drag and drop scripts from root to contracts #group1': function (browser: NightwatchBrowser) { - if (checkBrowserIsChrome(browser)) { - browser - .waitForElementVisible('*[data-id="treeViewLitreeViewItemcontracts"]') - .findElement('*[data-id="treeViewLitreeViewItemcontracts"]', (el) => { - console.log((el as any).value.getId()) - const id = (el as any).value.getId() - browser - .waitForElementVisible('li[data-id="treeViewLitreeViewItemscripts"]') - .dragAndDrop('li[data-id="treeViewLitreeViewItemscripts"]', id) - .waitForElementPresent('[data-id="fileSystemModalDialogModalFooter-react"] .modal-ok') - .execute(function () { (document.querySelector('[data-id="fileSystemModalDialogModalFooter-react"] .modal-ok') as HTMLElement).click() }) - .waitForElementVisible('li[data-id="treeViewLitreeViewItemcontracts/scripts"]') - }) - } - }, - 'drag scripts from contracts to root #group1': function (browser: NightwatchBrowser) { - if (checkBrowserIsChrome(browser)) { - browser.findElement('*[data-id="treeViewUltreeViewMenu"]', (el) => { - console.log((el as any).value.getId()) - const id = (el as any).value.getId() - browser - .waitForElementVisible('li[data-id="treeViewLitreeViewItemcontracts/scripts"]') - .dragAndDrop('li[data-id="treeViewLitreeViewItemcontracts/scripts"]', id) - .waitForElementPresent('[data-id="fileSystemModalDialogModalFooter-react"] .modal-ok') - .execute(function () { (document.querySelector('[data-id="fileSystemModalDialogModalFooter-react"] .modal-ok') as HTMLElement).click() }) - }) - browser.pause(1000) - .waitForElementVisible('li[data-id="treeViewLitreeViewItemscripts"]') - } - }, - 'drag into nested folder': function (browser: NightwatchBrowser) { - if (checkBrowserIsChrome(browser)) { - browser.waitForElementVisible('li[data-id="treeViewLitreeViewItemscripts"]') - .rightClick('li[data-id="treeViewLitreeViewItemscripts"]') - .waitForElementPresent('[data-id="contextMenuItemnewFolder') - .click('[data-id="contextMenuItemnewFolder') - .waitForElementVisible('*[data-id$="fileExplorerTreeItemInput"]') - .sendKeys('*[data-id$="fileExplorerTreeItemInput"]', 'nested') - .sendKeys('*[data-id$="fileExplorerTreeItemInput"]', browser.Keys.ENTER) - .findElement('*[data-id="treeViewLitreeViewItemscripts/nested"]', (el) => { - console.log((el as any).value.getId()) - const id = (el as any).value.getId() - browser - .waitForElementVisible('li[data-id="treeViewLitreeViewItemcontracts/README.txt"]') - .dragAndDrop('li[data-id="treeViewLitreeViewItemcontracts/README.txt"]', id) - .waitForElementPresent('[data-id="fileSystemModalDialogModalFooter-react"] .modal-ok') - .execute(function () { (document.querySelector('[data-id="fileSystemModalDialogModalFooter-react"] .modal-ok') as HTMLElement).click() }) - .waitForElementVisible('li[data-id="treeViewLitreeViewItemscripts/nested/README.txt"]') - }) - } + 'drag and drop file from contracts to root #group1': function (browser: NightwatchBrowser) { + if (checkBrowserIsChrome(browser)) { + browser.findElement('*[data-id="treeViewUltreeViewMenu"]', (el) => { + console.log((el as any).value.getId()) + const id = (el as any).value.getId() + browser + .waitForElementVisible('li[data-id="treeViewLitreeViewItemcontracts/1_Storage.sol"]') + .dragAndDrop('li[data-id="treeViewLitreeViewItemcontracts/1_Storage.sol"]', id) + .waitForElementPresent('[data-id="fileSystemModalDialogModalFooter-react"] .modal-ok') + .execute(function () { (document.querySelector('[data-id="fileSystemModalDialogModalFooter-react"] .modal-ok') as HTMLElement).click() }) + }) + browser.pause(1000) + .waitForElementVisible('li[data-id="treeViewLitreeViewItem1_Storage.sol"]') + } + }, + 'drag and drop scripts from root to contracts #group1': function (browser: NightwatchBrowser) { + if (checkBrowserIsChrome(browser)) { + browser + .waitForElementVisible('*[data-id="treeViewLitreeViewItemcontracts"]') + .findElement('*[data-id="treeViewLitreeViewItemcontracts"]', (el) => { + console.log((el as any).value.getId()) + const id = (el as any).value.getId() + browser + .waitForElementVisible('div[data-id="treeViewDivDraggableItemscripts"]') + .dragAndDrop('div[data-id="treeViewDivDraggableItemscripts"]', id) + .waitForElementPresent('[data-id="fileSystemModalDialogModalFooter-react"] .modal-ok') + .execute(function () { (document.querySelector('[data-id="fileSystemModalDialogModalFooter-react"] .modal-ok') as HTMLElement).click() }) + .waitForElementVisible('li[data-id="treeViewLitreeViewItemcontracts/scripts"]') + }) + } + }, + 'drag scripts from contracts to root #group1': function (browser: NightwatchBrowser) { + if (checkBrowserIsChrome(browser)) { + browser.findElement('*[data-id="treeViewUltreeViewMenu"]', (el) => { + console.log((el as any).value.getId()) + const id = (el as any).value.getId() + browser + .waitForElementVisible('li[data-id="treeViewLitreeViewItemcontracts/scripts"]') + .dragAndDrop('li[data-id="treeViewLitreeViewItemcontracts/scripts"]', id) + .waitForElementPresent('[data-id="fileSystemModalDialogModalFooter-react"] .modal-ok') + .execute(function () { (document.querySelector('[data-id="fileSystemModalDialogModalFooter-react"] .modal-ok') as HTMLElement).click() }) + }) + browser.pause(1000) + .waitForElementVisible('li[data-id="treeViewLitreeViewItemscripts"]') } + }, + 'drag into nested folder': function (browser: NightwatchBrowser) { + if (checkBrowserIsChrome(browser)) { + browser.waitForElementVisible('li[data-id="treeViewLitreeViewItemscripts"]') + .rightClick('li[data-id="treeViewLitreeViewItemscripts"]') + .waitForElementPresent('[data-id="contextMenuItemnewFolder') + .click('[data-id="contextMenuItemnewFolder') + .waitForElementVisible('*[data-id$="fileExplorerTreeItemInput"]') + .sendKeys('*[data-id$="fileExplorerTreeItemInput"]', 'nested') + .sendKeys('*[data-id$="fileExplorerTreeItemInput"]', browser.Keys.ENTER) + .findElement('*[data-id="treeViewLitreeViewItemscripts/nested"]', (el) => { + console.log((el as any).value.getId()) + const id = (el as any).value.getId() + browser + .waitForElementVisible('li[data-id="treeViewLitreeViewItemcontracts/README.txt"]') + .dragAndDrop('li[data-id="treeViewLitreeViewItemcontracts/README.txt"]', id) + .waitForElementPresent('[data-id="fileSystemModalDialogModalFooter-react"] .modal-ok') + .execute(function () { (document.querySelector('[data-id="fileSystemModalDialogModalFooter-react"] .modal-ok') as HTMLElement).click() }) + .waitForElementVisible('li[data-id="treeViewLitreeViewItemscripts/nested/README.txt"]') + }) + } + } -} \ No newline at end of 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 3b790fa706d..3847cae628f 100644 --- a/apps/remix-ide/src/app/tabs/locales/en/filePanel.json +++ b/apps/remix-ide/src/app/tabs/locales/en/filePanel.json @@ -136,4 +136,4 @@ "filePanel.movingFolderFailedMsg": "Unexpected error while moving folder: {src}", "filePanel.workspaceActions": "Workspace actions", "filePanel.saveCodeSample": "This code-sample workspace will not be persisted. Click here to save it." -} \ No newline at end of file +} diff --git a/libs/remix-ui/workspace/src/lib/components/file-explorer-hovericons.tsx b/libs/remix-ui/workspace/src/lib/components/file-explorer-hovericons.tsx new file mode 100644 index 00000000000..c6b299f66f4 --- /dev/null +++ b/libs/remix-ui/workspace/src/lib/components/file-explorer-hovericons.tsx @@ -0,0 +1,100 @@ +import React, { useState } from 'react' +import { CustomTooltip } from '@remix-ui/helper' +import { FormattedMessage } from 'react-intl' + +export type FileHoverIconsProps = { + file: any + handleNewFolderOp?: any + handleNewFileOp?: any + renamePathOp?: (path: string, type: string, isNew?: boolean) => void + deletePathOp?: (path: string | string[]) => void | Promise +} + +export function FileHoverIcons(props: FileHoverIconsProps) { + const [mouseOver, setMouseOver] = useState(false) + return ( + <> + {
+ { + props.file.isDirectory ? ( + <> + } + tooltipId={`filePanel.createNewFolder.${props.file.path}`} + tooltipClasses="text-nowrap" + > + { + e.stopPropagation() + await props.handleNewFolderOp(props.file.path) + }} + style={{ cursor: mouseOver ? 'pointer' : 'default' }} + onMouseEnter={(e) => setMouseOver(true)} + onMouseLeave={(e) => setMouseOver(false)} + > + + } + tooltipId={`fileExplorer.createNewFile.${props.file.path}`} + tooltipClasses="text-nowrap" + > + { + e.stopPropagation() + await props.handleNewFileOp(props.file.path) + }} + style={{ cursor: mouseOver ? 'pointer' : 'default' }} + onMouseEnter={(e) => setMouseOver(true)} + onMouseLeave={(e) => setMouseOver(false)} + > + + + ) : null + } + } + tooltipId={`filePanel.rename.${props.file.path}`} + tooltipClasses="text-nowrap" + > + { + e.stopPropagation() + props.renamePathOp(props.file.path, props.file.type) + }} + style={{ cursor: mouseOver ? 'pointer' : 'default' }} + onMouseEnter={(e) => setMouseOver(true)} + onMouseLeave={(e) => setMouseOver(false)} + > + + } + tooltipId={`filePanel.deleteItem.${props.file.path}`} + tooltipClasses="text-nowrap" + > + { + e.stopPropagation() + await props.deletePathOp(props.file.path) + }} + style={{ cursor: mouseOver ? 'pointer' : 'default' }} + onMouseEnter={(e) => setMouseOver(true)} + onMouseLeave={(e) => setMouseOver(false)} + > + +
+ } + + ) +} 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 39e20428d42..6e2010f80b2 100644 --- a/libs/remix-ui/workspace/src/lib/components/file-explorer.tsx +++ b/libs/remix-ui/workspace/src/lib/components/file-explorer.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState, useRef, SyntheticEvent, useTransition } from 'react' // eslint-disable-line +import React, { useEffect, useState, useRef, SyntheticEvent } from 'react' // eslint-disable-line import { useIntl } from 'react-intl' import { TreeView } from '@remix-ui/tree-view' // eslint-disable-line import { FileExplorerMenu } from './file-explorer-menu' // eslint-disable-line @@ -27,12 +27,13 @@ export const FileExplorer = (props: FileExplorerProps) => { handleContextMenu, handleNewFileInput, handleNewFolderInput, + deletePath, uploadFile, uploadFolder, fileState } = props const [state, setState] = useState(workspaceState) - const [isPending, startTransition] = useTransition(); + // const [isPending, startTransition] = useTransition(); const treeRef = useRef(null) useEffect(() => { @@ -130,6 +131,7 @@ export const FileExplorer = (props: FileExplorerProps) => { const renamePath = async (oldPath: string, newPath: string) => { try { + if (oldPath === newPath) return props.dispatchRenamePath(oldPath, newPath) } catch (error) { props.modal( @@ -404,6 +406,10 @@ export const FileExplorer = (props: FileExplorerProps) => { moveFile={handleFileMove} moveFolder={handleFolderMove} handleClickFolder={handleClickFolder} + createNewFile={props.createNewFile} + createNewFolder={props.createNewFolder} + deletePath={deletePath} + editPath={props.editModeOn} /> diff --git a/libs/remix-ui/workspace/src/lib/components/flat-tree-drop.tsx b/libs/remix-ui/workspace/src/lib/components/flat-tree-drop.tsx index e1d7fc72c57..de274562f7f 100644 --- a/libs/remix-ui/workspace/src/lib/components/flat-tree-drop.tsx +++ b/libs/remix-ui/workspace/src/lib/components/flat-tree-drop.tsx @@ -1,4 +1,4 @@ -import React, { SyntheticEvent, startTransition, useEffect, useRef, useState } from 'react' +import React, { SyntheticEvent, useEffect, useRef, useState } from 'react' import { FileType } from '../types' import { getEventTarget } from '../utils/getEventTarget' import { extractParentFromKey } from '@remix-ui/helper' @@ -23,7 +23,6 @@ export const FlatTreeDrop = (props: FlatTreeDropProps) => { const onDragOver = async (e: SyntheticEvent) => { e.preventDefault() const target = await getEventTarget(e) - if (!target || !target.path) { clearTimeout(timer) setFolderToOpen(null) @@ -36,7 +35,7 @@ export const FlatTreeDrop = (props: FlatTreeDropProps) => { setFolderToOpen(null) } if (dragDestination && dragDestination.isDirectory && !expandPath.includes(dragDestination.path) && folderToOpen !== dragDestination.path && props.handleClickFolder) { - + setFolderToOpen(dragDestination.path) timer && clearTimeout(timer) setTimer( @@ -61,7 +60,6 @@ export const FlatTreeDrop = (props: FlatTreeDropProps) => { } else { dragDestination = getFlatTreeItem(target.path) } - if (dragDestination.isDirectory) { if (dragSource.isDirectory) { moveFolder(dragDestination.path, dragSource.path) @@ -84,4 +82,4 @@ export const FlatTreeDrop = (props: FlatTreeDropProps) => { onDrop={onDrop} onDragOver={onDragOver} className="d-flex h-100" >{props.children}) -} \ No newline at end of file +} diff --git a/libs/remix-ui/workspace/src/lib/components/flat-tree.tsx b/libs/remix-ui/workspace/src/lib/components/flat-tree.tsx index f86cd2873c1..4bdddffe5bd 100644 --- a/libs/remix-ui/workspace/src/lib/components/flat-tree.tsx +++ b/libs/remix-ui/workspace/src/lib/components/flat-tree.tsx @@ -7,6 +7,8 @@ import { FlatTreeItemInput } from './flat-tree-item-input'; import { FlatTreeDrop } from './flat-tree-drop'; import { getEventTarget } from '../utils/getEventTarget'; import { fileDecoration, FileDecorationIcons } from '@remix-ui/file-decorators'; +import { FileHoverIcons } from './file-explorer-hovericons'; +import { deletePath } from '../actions'; export default function useOnScreen(ref: RefObject) { @@ -36,6 +38,10 @@ interface FlatTreeProps { moveFile: (dest: string, src: string) => void moveFolder: (dest: string, src: string) => void fileState: fileDecoration[] + createNewFile?: any + createNewFolder?: any + deletePath?: (path: string | string[]) => void | Promise + editPath?: (path: string, type: string, isNew?: boolean) => void } let mouseTimer: any = { @@ -44,7 +50,7 @@ let mouseTimer: any = { } export const FlatTree = (props: FlatTreeProps) => { - const { files, flatTree, expandPath, focusEdit, editModeOff, handleTreeClick, moveFile, moveFolder, fileState, focusElement, handleClickFolder } = props + const { files, flatTree, expandPath, focusEdit, editModeOff, handleTreeClick, moveFile, moveFolder, fileState, focusElement, handleClickFolder, deletePath, editPath } = props const [hover, setHover] = useState('') const [mouseOverTarget, setMouseOverTarget] = useState<{ path: string, @@ -178,42 +184,62 @@ export const FlatTree = (props: FlatTreeProps) => { } }, [focusEdit]) + const showIcons = (file: FileType) => + file.path === hover && !isDragging ? ( +
+ +
+ ) : null const Row = (index: number) => { const node = Object.keys(flatTree)[index] const file = flatTree[node] - return (
  • setHover(file.path)} - onMouseOut={() => setHover(file.path)} - data-type={file.isDirectory ? 'folder' : 'file'} - data-path={`${file.path}`} - data-id={`treeViewLitreeViewItem${file.path}`} - > -
    - {getIndentLevelDiv(file.path)} - -
    - {focusEdit && file.path && focusEdit.element === file.path ? - : - <>
    - {file.name} + return ( +
  • { + setHover(file.path) + }} + onMouseOut={() => { + setHover('') + }} + data-type={file.isDirectory ? 'folder' : 'file'} + data-path={`${file.path}`} + data-id={`treeViewLitreeViewItem${file.path}`} + > +
    + {getIndentLevelDiv(file.path)} -
    - {getFileStateIcons(file)} - - } - -
  • ) +
    + {focusEdit && file.path && focusEdit.element === file.path ? + : + <>
    + {file.name} +
    +
    + {showIcons(file)} + {getFileStateIcons(file)} +
    + + } + + ) } return (<> diff --git a/libs/remix-ui/workspace/src/lib/css/file-explorer.css b/libs/remix-ui/workspace/src/lib/css/file-explorer.css index c49a0a54a72..16f0ffca51b 100644 --- a/libs/remix-ui/workspace/src/lib/css/file-explorer.css +++ b/libs/remix-ui/workspace/src/lib/css/file-explorer.css @@ -60,4 +60,12 @@ ul { [contenteditable] { -webkit-user-select: text; user-select: text; -} \ No newline at end of file +} + +.remixui_icons { + +} + +.remixui_icons:hover { + color: var(--text); +} 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 400aa1de2ce..e71d94ffd97 100644 --- a/libs/remix-ui/workspace/src/lib/remix-ui-workspace.tsx +++ b/libs/remix-ui/workspace/src/lib/remix-ui-workspace.tsx @@ -796,7 +796,7 @@ export function Workspace() { +