@ -1,12 +1,18 @@
import React , { useState , useEffect , useRef , useContext , SyntheticEvent , ChangeEvent , KeyboardEvent } from 'react' // eslint-disable-line
import React , { useState , useEffect , useRef , useContext , SyntheticEvent , ChangeEvent , KeyboardEvent , MouseEvent } from 'react' // eslint-disable-line
import { FormattedMessage , useIntl } from 'react-intl'
import { FormattedMessage , useIntl } from 'react-intl'
import { Dropdown } from 'react-bootstrap'
import { Dropdown } from 'react-bootstrap'
import { CustomIconsToggle , CustomMenu , CustomToggle , CustomTooltip } from '@remix-ui/helper'
import { CustomIconsToggle , CustomMenu , CustomToggle , CustomTooltip , extractNameFromKey , extractParentFromKey } from '@remix-ui/helper'
import { FileExplorer } from './components/file-explorer' // eslint-disable-line
import { FileExplorer } from './components/file-explorer' // eslint-disable-line
import { FileSystemContext } from './contexts'
import { FileSystemContext } from './contexts'
import './css/remix-ui-workspace.css'
import './css/remix-ui-workspace.css'
import { ROOT_PATH , TEMPLATE_NAMES } from './utils/constants'
import { ROOT_PATH , TEMPLATE_NAMES } from './utils/constants'
import { HamburgerMenu } from './components/workspace-hamburger'
import { HamburgerMenu } from './components/workspace-hamburger'
import { MenuItems , WorkSpaceState } from './types'
import { contextMenuActions } from './utils'
import FileExplorerContextMenu from './components/file-explorer-context-menu'
import { customAction } from '@remixproject/plugin-api'
const _paq = window . _paq = window . _paq || [ ]
const _paq = window . _paq = window . _paq || [ ]
const canUpload = window . File || window . FileReader || window . FileList || window . Blob
const canUpload = window . File || window . FileReader || window . FileList || window . Blob
@ -36,6 +42,56 @@ export function Workspace () {
const filteredBranches = selectedWorkspace ? ( selectedWorkspace . branches || [ ] ) . filter ( branch = > branch . name . includes ( branchFilter ) && branch . name !== 'HEAD' ) . slice ( 0 , 20 ) : [ ]
const filteredBranches = selectedWorkspace ? ( selectedWorkspace . branches || [ ] ) . filter ( branch = > branch . name . includes ( branchFilter ) && branch . name !== 'HEAD' ) . slice ( 0 , 20 ) : [ ]
const currentBranch = selectedWorkspace ? selectedWorkspace.currentBranch : null
const currentBranch = selectedWorkspace ? selectedWorkspace.currentBranch : null
const [ canPaste , setCanPaste ] = useState ( false )
const [ state , setState ] = useState < WorkSpaceState > ( {
ctrlKey : false ,
newFileName : '' ,
actions : contextMenuActions ,
focusContext : {
element : null ,
x : null ,
y : null ,
type : ''
} ,
focusEdit : {
element : null ,
type : '' ,
isNew : false ,
lastEdit : ''
} ,
mouseOverElement : null ,
showContextMenu : false ,
reservedKeywords : [ ROOT_PATH , 'gist-' ] ,
copyElement : [ ]
} )
useEffect ( ( ) = > {
if ( canPaste ) {
addMenuItems ( [ {
id : 'paste' ,
name : 'Paste' ,
type : [ 'folder' , 'file' , 'workspace' ] ,
path : [ ] ,
extension : [ ] ,
pattern : [ ] ,
multiselect : false ,
label : ''
} ] )
} else {
removeMenuItems ( [ {
id : 'paste' ,
name : 'Paste' ,
type : [ 'folder' , 'file' , 'workspace' ] ,
path : [ ] ,
extension : [ ] ,
pattern : [ ] ,
multiselect : false ,
label : ''
} ] )
}
} , [ canPaste ] )
useEffect ( ( ) = > {
useEffect ( ( ) = > {
let workspaceName = localStorage . getItem ( 'currentWorkspace' )
let workspaceName = localStorage . getItem ( 'currentWorkspace' )
if ( ! workspaceName && global . fs . browser . workspaces . length ) {
if ( ! workspaceName && global . fs . browser . workspaces . length ) {
@ -114,6 +170,23 @@ export function Workspace () {
)
)
}
}
const addMenuItems = ( items : MenuItems ) = > {
setState ( prevState = > {
// filter duplicate items
const actions = items . filter ( ( { name } ) = > prevState . actions . findIndex ( action = > action . name === name ) === - 1 )
return { . . . prevState , actions : [ . . . prevState . actions , . . . actions ] }
} )
}
const removeMenuItems = ( items : MenuItems ) = > {
setState ( prevState = > {
const actions = prevState . actions . filter ( ( { id , name } ) = > items . findIndex ( item = > id === item . id && name === item . name ) === - 1 )
return { . . . prevState , actions }
} )
}
const cloneGitRepository = ( ) = > {
const cloneGitRepository = ( ) = > {
global . modal (
global . modal (
intl . formatMessage ( { id : 'filePanel.workspace.clone' } ) ,
intl . formatMessage ( { id : 'filePanel.workspace.clone' } ) ,
@ -271,6 +344,174 @@ export function Workspace () {
}
}
}
}
const handleCopyClick = ( path : string , type : 'folder' | 'gist' | 'file' | 'workspace' ) = > {
setState ( prevState = > {
return { . . . prevState , copyElement : [ { key : path , type } ] }
} )
setCanPaste ( true )
global . toast ( ` Copied to clipboard ${ path } ` )
}
const handlePasteClick = ( dest : string , destType : string ) = > {
dest = destType === 'file' ? extractParentFromKey ( dest ) || ROOT_PATH : dest
state . copyElement . map ( ( { key , type } ) = > {
type === 'file' ? copyFile ( key , dest ) : copyFolder ( key , dest )
} )
}
const downloadPath = async ( path : string ) = > {
try {
global . dispatchDownloadPath ( path )
} catch ( error ) {
global . modal ( 'Download Failed' , 'Unexpected error while downloading: ' + typeof error === 'string' ? error : error.message , 'Close' , async ( ) = > { } )
}
}
const copyFile = ( src : string , dest : string ) = > {
try {
global . dispatchCopyFile ( src , dest )
} catch ( error ) {
global . modal ( 'Copy File Failed' , 'Unexpected error while copying file: ' + src , 'Close' , async ( ) = > { } )
}
}
const copyFolder = ( src : string , dest : string ) = > {
try {
global . dispatchCopyFolder ( src , dest )
} catch ( error ) {
global . modal ( 'Copy Folder Failed' , 'Unexpected error while copying folder: ' + src , 'Close' , async ( ) = > { } )
}
}
const handleContextMenu = ( pageX : number , pageY : number , path : string , content : string , type : string ) = > {
if ( ! content ) return
setState ( prevState = > {
return { . . . prevState , focusContext : { element : path , x : pageX , y : pageY , type } , focusEdit : { . . . prevState . focusEdit , lastEdit : content } , showContextMenu : prevState.focusEdit.element !== path }
} )
}
const getFocusedFolder = ( ) = > {
const focusElement = global . fs . focusElement
if ( focusElement [ 0 ] ) {
if ( focusElement [ 0 ] . type === 'folder' && focusElement [ 0 ] . key ) return focusElement [ 0 ] . key
else if ( focusElement [ 0 ] . type === 'gist' && focusElement [ 0 ] . key ) return focusElement [ 0 ] . key
else if ( focusElement [ 0 ] . type === 'file' && focusElement [ 0 ] . key ) return extractParentFromKey ( focusElement [ 0 ] . key ) ? extractParentFromKey ( focusElement [ 0 ] . key ) : ROOT_PATH
else return ROOT_PATH
}
}
const uploadFile = ( target ) = > {
const parentFolder = getFocusedFolder ( )
const expandPath = [ . . . new Set ( [ . . . global . fs . browser . expandPath , parentFolder ] ) ]
global . dispatchHandleExpandPath ( expandPath )
global . dispatchUploadFile ( target , parentFolder )
}
const uploadFolder = ( target ) = > {
const parentFolder = getFocusedFolder ( )
const expandPath = [ . . . new Set ( [ . . . global . fs . browser . expandPath , parentFolder ] ) ]
global . dispatchHandleExpandPath ( expandPath )
global . dispatchUploadFolder ( target , parentFolder )
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const handleCopyFileNameClick = ( path : string , _type : string ) = > {
const fileName = extractNameFromKey ( path )
navigator . clipboard . writeText ( fileName )
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const handleCopyFilePathClick = ( path : string , _type : string ) = > {
navigator . clipboard . writeText ( path )
}
const hideContextMenu = ( ) = > {
setState ( prevState = > {
return { . . . prevState , focusContext : { element : null , x : 0 , y : 0 , type : '' } , showContextMenu : false }
} )
}
const runScript = async ( path : string ) = > {
try {
global . dispatchRunScript ( path )
} catch ( error ) {
global . toast ( 'Run script failed' )
}
}
const emitContextMenuEvent = ( cmd : customAction ) = > {
try {
global . dispatchEmitContextMenuEvent ( cmd )
} catch ( error ) {
global . toast ( error )
}
}
const pushChangesToGist = ( path? : string , type ? : string ) = > {
global . modal ( 'Create a public gist' , 'Are you sure you want to push changes to remote gist file on github.com?' , 'OK' , ( ) = > toGist ( path , type ) , 'Cancel' , ( ) = > { } )
}
const publishFolderToGist = ( path? : string , type ? : string ) = > {
global . modal ( 'Create a public gist' , ` Are you sure you want to anonymously publish all your files in the ${ path } folder as a public gist on github.com? ` , 'OK' , ( ) = > toGist ( path , type ) , 'Cancel' , ( ) = > { } )
}
const publishFileToGist = ( path? : string , type ? : string ) = > {
global . modal ( 'Create a public gist' , ` Are you sure you want to anonymously publish ${ path } file as a public gist on github.com? ` , 'OK' , ( ) = > toGist ( path , type ) , 'Cancel' , ( ) = > { } )
}
const deleteMessage = ( path : string [ ] ) = > {
return (
< div >
< div > Are you sure you want to delete { path . length > 1 ? 'these items' : 'this item' } ? < / div >
{
path . map ( ( item , i ) = > ( < li key = { i } > { item } < / li > ) )
}
< / div >
)
}
const deletePath = async ( path : string [ ] ) = > {
if ( global . fs . readonly ) return global . toast ( 'cannot delete file. ' + name + ' is a read only explorer' )
if ( ! Array . isArray ( path ) ) path = [ path ]
global . modal ( ` Delete ${ path . length > 1 ? 'items' : 'item' } ` , deleteMessage ( path ) , 'OK' , ( ) = > { global . dispatchDeletePath ( path ) } , 'Cancel' , ( ) = > { } )
}
const toGist = ( path? : string , type ? : string ) = > {
global . dispatchPublishToGist ( path , type )
}
const editModeOn = ( path : string , type : string , isNew = false ) = > {
if ( global . fs . readonly ) return global . toast ( 'Cannot write/modify file system in read only mode.' )
setState ( prevState = > {
return { . . . prevState , focusEdit : { . . . prevState . focusEdit , element : path , isNew , type } }
} )
}
const handleNewFileInput = async ( parentFolder? : string ) = > {
if ( ! parentFolder ) parentFolder = getFocusedFolder ( )
const expandPath = [ . . . new Set ( [ . . . global . fs . browser . expandPath , parentFolder ] ) ]
await global . dispatchAddInputField ( parentFolder , 'file' )
global . dispatchHandleExpandPath ( expandPath )
editModeOn ( parentFolder + '/blank' , 'file' , true )
}
const handleNewFolderInput = async ( parentFolder? : string ) = > {
if ( ! parentFolder ) parentFolder = getFocusedFolder ( )
else if ( ( parentFolder . indexOf ( '.sol' ) !== - 1 ) || ( parentFolder . indexOf ( '.js' ) !== - 1 ) ) parentFolder = extractParentFromKey ( parentFolder )
const expandPath = [ . . . new Set ( [ . . . global . fs . browser . expandPath , parentFolder ] ) ]
await global . dispatchAddInputField ( parentFolder , 'folder' )
global . dispatchHandleExpandPath ( expandPath )
editModeOn ( parentFolder + '/blank' , 'folder' , true )
}
const toggleDropdown = ( isOpen : boolean ) = > {
const toggleDropdown = ( isOpen : boolean ) = > {
setShowDropdown ( isOpen )
setShowDropdown ( isOpen )
}
}
@ -428,8 +669,12 @@ export function Workspace () {
return (
return (
< div className = 'd-flex flex-column justify-content-between h-100' >
< div className = 'd-flex flex-column justify-content-between h-100' >
< div className = 'remixui_container overflow-auto' style = { { maxHeight : selectedWorkspace && selectedWorkspace . isGitRepo ? '95%' : '100%' } } >
< div className = 'remixui_container overflow-auto' style = { { maxHeight : selectedWorkspace && selectedWorkspace . isGitRepo ? '95%' : '100%' } } onContextMenu = { ( e ) = > {
< div className = 'd-flex flex-column w-100 mb-1 remixui_fileexplorer' data - id = "remixUIWorkspaceExplorer" onClick = { resetFocus } >
e . preventDefault ( )
handleContextMenu ( e . pageX , e . pageY , ROOT_PATH , "workspace" , 'workspace' )
}
} >
< div className = 'd-flex flex-column w-100 remixui_fileexplorer' data - id = "remixUIWorkspaceExplorer" onClick = { resetFocus } >
< div >
< div >
< header >
< header >
< div className = "mx-2 mb-2 d-flex flex-column" >
< div className = "mx-2 mb-2 d-flex flex-column" >
@ -455,7 +700,7 @@ export function Workspace () {
createWorkspace ( )
createWorkspace ( )
_paq . push ( [ 'trackEvent' , 'fileExplorer' , 'workspaceMenu' , 'workspaceCreate' ] )
_paq . push ( [ 'trackEvent' , 'fileExplorer' , 'workspaceMenu' , 'workspaceCreate' ] )
} }
} }
style = { { fontSize : 'large ' } }
style = { { fontSize : 'medium ' } }
className = 'far fa-plus remixui_menuicon d-flex align-self-end'
className = 'far fa-plus remixui_menuicon d-flex align-self-end'
>
>
< / span >
< / span >
@ -537,19 +782,20 @@ export function Workspace () {
< / div >
< / div >
< / header >
< / header >
< / div >
< / div >
< div className = 'h-100 mb-4 pb-4 remixui_fileExplorerTree' onFocus = { ( ) = > { toggleDropdown ( false ) } } >
< div className = 'h-100 remixui_fileExplorerTree' onFocus = { ( ) = > { toggleDropdown ( false ) } } >
< div className = 'h-100' >
< div className = 'h-100' >
{ ( global . fs . browser . isRequestingWorkspace || global . fs . browser . isRequestingCloning ) && < div className = "text-center py-5" > < i className = "fas fa-spinner fa-pulse fa-2x" > < / i > < / div > }
{ ( global . fs . browser . isRequestingWorkspace || global . fs . browser . isRequestingCloning ) && < div className = "text-center py-5" > < i className = "fas fa-spinner fa-pulse fa-2x" > < / i > < / div > }
{ ! ( global . fs . browser . isRequestingWorkspace || global . fs . browser . isRequestingCloning ) &&
{ ! ( global . fs . browser . isRequestingWorkspace || global . fs . browser . isRequestingCloning ) &&
( global . fs . mode === 'browser' ) && ( currentWorkspace !== NO_WORKSPACE ) &&
( global . fs . mode === 'browser' ) && ( currentWorkspace !== NO_WORKSPACE ) &&
< div className = 'h-100 remixui_treeview' data - id = 'filePanelFileExplorerTree' >
< div className = 'h-100 remixui_treeview' data - id = 'filePanelFileExplorerTree' >
< FileExplorer
< FileExplorer
fileState = { global . fs . browser . fileState }
name = { currentWorkspace }
name = { currentWorkspace }
menuItems = { [ 'createNewFile' , 'createNewFolder' , 'publishToGist' , canUpload ? 'uploadFile' : '' , canUpload ? 'uploadFolder' : '' ] }
menuItems = { [ 'createNewFile' , 'createNewFolder' , 'publishToGist' , canUpload ? 'uploadFile' : '' , canUpload ? 'uploadFolder' : '' ] }
contextMenuItems = { global . fs . browser . contextMenu . registeredMenuItems }
contextMenuItems = { global . fs . browser . contextMenu . registeredMenuItems }
removedContextMenuItems = { global . fs . browser . contextMenu . removedMenuItems }
removedContextMenuItems = { global . fs . browser . contextMenu . removedMenuItems }
files = { global . fs . browser . files }
files = { global . fs . browser . files }
fil eState= { global . f s. browser . fileS tate}
workspac eState= { state }
expandPath = { global . fs . browser . expandPath }
expandPath = { global . fs . browser . expandPath }
focusEdit = { global . fs . focusEdit }
focusEdit = { global . fs . focusEdit }
focusElement = { global . fs . focusElement }
focusElement = { global . fs . focusElement }
@ -578,12 +824,24 @@ export function Workspace () {
dispatchHandleExpandPath = { global . dispatchHandleExpandPath }
dispatchHandleExpandPath = { global . dispatchHandleExpandPath }
dispatchMoveFile = { global . dispatchMoveFile }
dispatchMoveFile = { global . dispatchMoveFile }
dispatchMoveFolder = { global . dispatchMoveFolder }
dispatchMoveFolder = { global . dispatchMoveFolder }
handleCopyClick = { handleCopyClick }
handlePasteClick = { handlePasteClick }
addMenuItems = { addMenuItems }
removeMenuItems = { removeMenuItems }
handleContextMenu = { handleContextMenu }
uploadFile = { uploadFile }
uploadFolder = { uploadFolder }
getFocusedFolder = { getFocusedFolder }
toGist = { toGist }
editModeOn = { editModeOn }
handleNewFileInput = { handleNewFileInput }
handleNewFolderInput = { handleNewFolderInput }
/ >
/ >
< / div >
< / div >
}
}
{ global . fs . localhost . isRequestingLocalhost && < div className = "text-center py-5" > < i className = "fas fa-spinner fa-pulse fa-2x" > < / i > < / div > }
{ global . fs . localhost . isRequestingLocalhost && < div className = "text-center py-5" > < i className = "fas fa-spinner fa-pulse fa-2x" > < / i > < / div > }
{ ( global . fs . mode === 'localhost' && global . fs . localhost . isSuccessfulLocalhost ) &&
{ ( global . fs . mode === 'localhost' && global . fs . localhost . isSuccessfulLocalhost ) &&
< div className = 'h-100 filesystemexplorer pb-4 mb-4 remixui_treeview' >
< div className = 'h-100 filesystemexplorer remixui_treeview' >
< FileExplorer
< FileExplorer
name = 'localhost'
name = 'localhost'
menuItems = { [ 'createNewFile' , 'createNewFolder' ] }
menuItems = { [ 'createNewFile' , 'createNewFolder' ] }
@ -591,6 +849,7 @@ export function Workspace () {
removedContextMenuItems = { global . fs . localhost . contextMenu . removedMenuItems }
removedContextMenuItems = { global . fs . localhost . contextMenu . removedMenuItems }
files = { global . fs . localhost . files }
files = { global . fs . localhost . files }
fileState = { [ ] }
fileState = { [ ] }
workspaceState = { state }
expandPath = { global . fs . localhost . expandPath }
expandPath = { global . fs . localhost . expandPath }
focusEdit = { global . fs . focusEdit }
focusEdit = { global . fs . focusEdit }
focusElement = { global . fs . focusElement }
focusElement = { global . fs . focusElement }
@ -619,6 +878,18 @@ export function Workspace () {
dispatchHandleExpandPath = { global . dispatchHandleExpandPath }
dispatchHandleExpandPath = { global . dispatchHandleExpandPath }
dispatchMoveFile = { global . dispatchMoveFile }
dispatchMoveFile = { global . dispatchMoveFile }
dispatchMoveFolder = { global . dispatchMoveFolder }
dispatchMoveFolder = { global . dispatchMoveFolder }
handleCopyClick = { handleCopyClick }
handlePasteClick = { handlePasteClick }
addMenuItems = { addMenuItems }
removeMenuItems = { removeMenuItems }
handleContextMenu = { handleContextMenu }
uploadFile = { uploadFile }
uploadFolder = { uploadFolder }
getFocusedFolder = { getFocusedFolder }
toGist = { toGist }
editModeOn = { editModeOn }
handleNewFileInput = { handleNewFileInput }
handleNewFolderInput = { handleNewFolderInput }
/ >
/ >
< / div >
< / div >
}
}
@ -685,6 +956,32 @@ export function Workspace () {
< / div >
< / div >
< / div >
< / div >
}
}
{ state . showContextMenu && < FileExplorerContextMenu
actions = { global . fs . focusElement . length > 1 ? state . actions . filter ( item = > item . multiselect ) : state . actions . filter ( item = > ! item . multiselect ) }
hideContextMenu = { hideContextMenu }
createNewFile = { handleNewFileInput }
createNewFolder = { handleNewFolderInput }
deletePath = { deletePath }
renamePath = { editModeOn }
runScript = { runScript }
copy = { handleCopyClick }
paste = { handlePasteClick }
copyFileName = { handleCopyFileNameClick }
copyPath = { handleCopyFilePathClick }
emit = { emitContextMenuEvent }
pageX = { state . focusContext . x }
pageY = { state . focusContext . y }
path = { state . focusContext . element }
type = { state . focusContext . type }
focus = { global . fs . focusElement }
pushChangesToGist = { pushChangesToGist }
publishFolderToGist = { publishFolderToGist }
publishFileToGist = { publishFileToGist }
uploadFile = { uploadFile }
downloadPath = { downloadPath }
/ >
}
< / div >
< / div >
)
)
}
}