@ -6,7 +6,7 @@ import { Toaster } from '@remix-ui/toaster' // eslint-disable-line
import Gists from 'gists'
import Gists from 'gists'
import { FileExplorerMenu } from './file-explorer-menu' // eslint-disable-line
import { FileExplorerMenu } from './file-explorer-menu' // eslint-disable-line
import { FileExplorerContextMenu } from './file-explorer-context-menu' // eslint-disable-line
import { FileExplorerContextMenu } from './file-explorer-context-menu' // eslint-disable-line
import { FileExplorerProps , File } from './types'
import { FileExplorerProps , File , MenuItems } from './types'
import { fileSystemReducer , fileSystemInitialState } from './reducers/fileSystem'
import { fileSystemReducer , fileSystemInitialState } from './reducers/fileSystem'
import { fetchDirectory , init , resolveDirectory , addInputField , removeInputField } from './actions/fileSystem'
import { fetchDirectory , init , resolveDirectory , addInputField , removeInputField } from './actions/fileSystem'
import * as helper from '../../../../../apps/remix-ide/src/lib/helper'
import * as helper from '../../../../../apps/remix-ide/src/lib/helper'
@ -19,8 +19,10 @@ const queryParams = new QueryParams()
export const FileExplorer = ( props : FileExplorerProps ) = > {
export const FileExplorer = ( props : FileExplorerProps ) = > {
const { name , registry , plugin , focusRoot , contextMenuItems , displayInput , externalUploads } = props
const { name , registry , plugin , focusRoot , contextMenuItems , displayInput , externalUploads } = props
const [ state , setState ] = useState ( {
const [ state , setState ] = useState ( {
focusElement : [ { key : '' , type : 'folder' } ] ,
focusElement : [ {
focusPath : null ,
key : '' ,
type : 'folder'
} ] ,
files : [ ] ,
files : [ ] ,
fileManager : null ,
fileManager : null ,
ctrlKey : false ,
ctrlKey : false ,
@ -28,45 +30,83 @@ export const FileExplorer = (props: FileExplorerProps) => {
actions : [ {
actions : [ {
id : 'newFile' ,
id : 'newFile' ,
name : 'New File' ,
name : 'New File' ,
type : [ 'folder' ] ,
type : [ 'folder' , 'gist' ] ,
path : [ ] ,
path : [ ] ,
extension : [ ] ,
extension : [ ] ,
pattern : [ ]
pattern : [ ] ,
multiselect : false
} , {
} , {
id : 'newFolder' ,
id : 'newFolder' ,
name : 'New Folder' ,
name : 'New Folder' ,
type : [ 'folder' ] ,
type : [ 'folder' , 'gist' ] ,
path : [ ] ,
path : [ ] ,
extension : [ ] ,
extension : [ ] ,
pattern : [ ]
pattern : [ ] ,
multiselect : false
} , {
} , {
id : 'rename' ,
id : 'rename' ,
name : 'Rename' ,
name : 'Rename' ,
type : [ 'file' , 'folder' ] ,
type : [ 'file' , 'folder' ] ,
path : [ ] ,
path : [ ] ,
extension : [ ] ,
extension : [ ] ,
pattern : [ ]
pattern : [ ] ,
multiselect : false
} , {
} , {
id : 'delete' ,
id : 'delete' ,
name : 'Delete' ,
name : 'Delete' ,
type : [ 'file' , 'folder' , 'multi ' ] ,
type : [ 'file' , 'folder' , 'gist ' ] ,
path : [ ] ,
path : [ ] ,
extension : [ ] ,
extension : [ ] ,
pattern : [ ]
pattern : [ ] ,
multiselect : false
} , {
id : 'run' ,
name : 'Run' ,
type : [ ] ,
path : [ ] ,
extension : [ '.js' ] ,
pattern : [ ] ,
multiselect : false
} , {
} , {
id : 'pushChangesToGist' ,
id : 'pushChangesToGist' ,
name : 'Push changes to gist' ,
name : 'Push changes to gist' ,
type : [ ] ,
type : [ 'gist' ] ,
path : [ ] ,
path : [ ] ,
extension : [ ] ,
extension : [ ] ,
pattern : [ '^browser/gists/([0-9]|[a-z])*$' ]
pattern : [ ] ,
multiselect : false
} , {
} , {
id : 'run' ,
id : 'publishFolderToGist ' ,
name : 'Run ' ,
name : 'Publish folder to gist ' ,
type : [ ] ,
type : [ 'folder' ] ,
path : [ ] ,
path : [ ] ,
extension : [ '.js' ] ,
extension : [ ] ,
pattern : [ ]
pattern : [ ] ,
multiselect : false
} , {
id : 'publishFileToGist' ,
name : 'Publish file to gist' ,
type : [ 'file' ] ,
path : [ ] ,
extension : [ ] ,
pattern : [ ] ,
multiselect : false
} , {
id : 'copy' ,
name : 'Copy' ,
type : [ 'folder' , 'file' ] ,
path : [ ] ,
extension : [ ] ,
pattern : [ ] ,
multiselect : false
} , {
id : 'deleteAll' ,
name : 'Delete All' ,
type : [ 'folder' , 'file' ] ,
path : [ ] ,
extension : [ ] ,
pattern : [ ] ,
multiselect : true
} ] ,
} ] ,
focusContext : {
focusContext : {
element : null ,
element : null ,
@ -85,30 +125,26 @@ export const FileExplorer = (props: FileExplorerProps) => {
hide : true ,
hide : true ,
title : '' ,
title : '' ,
message : '' ,
message : '' ,
children : < > < / > ,
okLabel : '' ,
ok : {
okFn : ( ) = > { } ,
label : '' ,
cancelLabel : '' ,
fn : ( ) = > { }
cancelFn : ( ) = > { } ,
} ,
cancel : {
label : '' ,
fn : ( ) = > { }
} ,
handleHide : null
handleHide : null
} ,
} ,
modals : [ ] ,
modals : [ ] ,
toasterMsg : '' ,
toasterMsg : '' ,
mouseOverElement : null ,
mouseOverElement : null ,
showContextMenu : false
showContextMenu : false ,
reservedKeywords : [ name , 'gist-' ] ,
copyElement : [ ]
} )
} )
const [ canPaste , setCanPaste ] = useState ( false )
const [ fileSystem , dispatch ] = useReducer ( fileSystemReducer , fileSystemInitialState )
const [ fileSystem , dispatch ] = useReducer ( fileSystemReducer , fileSystemInitialState )
const editRef = useRef ( null )
const editRef = useRef ( null )
useEffect ( ( ) = > {
useEffect ( ( ) = > {
if ( props . filesProvider ) {
init ( props . filesProvider , props . name , props . plugin , props . registry ) ( dispatch )
init ( props . filesProvider , props . name , props . plugin , props . registry ) ( dispatch )
}
} , [ ] )
} , [ props . filesProvider , props . name ] )
useEffect ( ( ) = > {
useEffect ( ( ) = > {
const provider = fileSystem . provider . provider
const provider = fileSystem . provider . provider
@ -120,13 +156,7 @@ export const FileExplorer = (props: FileExplorerProps) => {
useEffect ( ( ) = > {
useEffect ( ( ) = > {
if ( fileSystem . notification . message ) {
if ( fileSystem . notification . message ) {
modal ( fileSystem . notification . title , fileSystem . notification . message , {
modal ( fileSystem . notification . title , fileSystem . notification . message , fileSystem . notification . labelOk , fileSystem . notification . actionOk , fileSystem . notification . labelCancel , fileSystem . notification . actionCancel )
label : fileSystem.notification.labelOk ,
fn : fileSystem.notification.actionOk
} , {
label : fileSystem.notification.labelCancel ,
fn : fileSystem.notification.actionCancel
} )
}
}
} , [ fileSystem . notification . message ] )
} , [ fileSystem . notification . message ] )
@ -169,12 +199,7 @@ export const FileExplorer = (props: FileExplorerProps) => {
useEffect ( ( ) = > {
useEffect ( ( ) = > {
if ( contextMenuItems ) {
if ( contextMenuItems ) {
setState ( prevState = > {
addMenuItems ( contextMenuItems )
// filter duplicate items
const items = contextMenuItems . filter ( ( { name } ) = > prevState . actions . findIndex ( action = > action . name === name ) === - 1 )
return { . . . prevState , actions : [ . . . prevState . actions , . . . items ] }
} )
}
}
} , [ contextMenuItems ] )
} , [ contextMenuItems ] )
@ -199,10 +224,11 @@ export const FileExplorer = (props: FileExplorerProps) => {
hide : false ,
hide : false ,
title : prevState.modals [ 0 ] . title ,
title : prevState.modals [ 0 ] . title ,
message : prevState.modals [ 0 ] . message ,
message : prevState.modals [ 0 ] . message ,
ok : prevState.modals [ 0 ] . ok ,
okLabel : prevState.modals [ 0 ] . okLabel ,
cancel : prevState.modals [ 0 ] . cancel ,
okFn : prevState.modals [ 0 ] . okFn ,
handleHide : prevState.modals [ 0 ] . handleHide ,
cancelLabel : prevState.modals [ 0 ] . cancelLabel ,
children : prevState.modals [ 0 ] . children
cancelFn : prevState.modals [ 0 ] . cancelFn ,
handleHide : prevState.modals [ 0 ] . handleHide
}
}
prevState . modals . shift ( )
prevState . modals . shift ( )
@ -240,6 +266,39 @@ export const FileExplorer = (props: FileExplorerProps) => {
}
}
} , [ ] )
} , [ ] )
useEffect ( ( ) = > {
if ( canPaste ) {
addMenuItems ( [ {
id : 'paste' ,
name : 'Paste' ,
type : [ 'folder' , 'file' ] ,
path : [ ] ,
extension : [ ] ,
pattern : [ ] ,
multiselect : false
} ] )
} else {
removeMenuItems ( [ 'paste' ] )
}
} , [ canPaste ] )
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 = ( ids : string [ ] ) = > {
setState ( prevState = > {
const actions = prevState . actions . filter ( ( { id } ) = > ids . findIndex ( value = > value === id ) === - 1 )
return { . . . prevState , actions }
} )
}
const extractNameFromKey = ( key : string ) : string = > {
const extractNameFromKey = ( key : string ) : string = > {
const keyPath = key . split ( '/' )
const keyPath = key . split ( '/' )
@ -254,6 +313,20 @@ export const FileExplorer = (props: FileExplorerProps) => {
return keyPath . join ( '/' )
return keyPath . join ( '/' )
}
}
const hasReservedKeyword = ( content : string ) : boolean = > {
if ( state . reservedKeywords . findIndex ( value = > content . startsWith ( value ) ) !== - 1 ) return true
else return false
}
const getFocusedFolder = ( ) = > {
if ( state . focusElement [ 0 ] ) {
if ( state . focusElement [ 0 ] . type === 'folder' && state . focusElement [ 0 ] . key ) return state . focusElement [ 0 ] . key
else if ( state . focusElement [ 0 ] . type === 'gist' && state . focusElement [ 0 ] . key ) return state . focusElement [ 0 ] . key
else if ( state . focusElement [ 0 ] . type === 'file' && state . focusElement [ 0 ] . key ) return extractParentFromKey ( state . focusElement [ 0 ] . key ) ? extractParentFromKey ( state . focusElement [ 0 ] . key ) : name
else return name
}
}
const createNewFile = async ( newFilePath : string ) = > {
const createNewFile = async ( newFilePath : string ) = > {
const fileManager = state . fileManager
const fileManager = state . fileManager
@ -272,10 +345,7 @@ export const FileExplorer = (props: FileExplorerProps) => {
} )
} )
}
}
} catch ( error ) {
} catch ( error ) {
return modal ( 'File Creation Failed' , typeof error === 'string' ? error : error.message , {
return modal ( 'File Creation Failed' , typeof error === 'string' ? error : error.message , 'Close' , async ( ) = > { } )
label : 'Close' ,
fn : async ( ) = > { }
} , null )
}
}
}
}
@ -287,35 +357,26 @@ export const FileExplorer = (props: FileExplorerProps) => {
const exists = await fileManager . exists ( dirName )
const exists = await fileManager . exists ( dirName )
if ( exists ) {
if ( exists ) {
return modal ( 'Rename File Failed' , ` A file or folder ${ extractNameFromKey ( newFolderPath ) } already exists at this location. Please choose a different name. ` , {
return modal ( 'Rename File Failed' , ` A file or folder ${ extractNameFromKey ( newFolderPath ) } already exists at this location. Please choose a different name. ` , 'Close' , ( ) = > { } )
label : 'Close' ,
fn : ( ) = > { }
} , null )
}
}
await fileManager . mkdir ( dirName )
await fileManager . mkdir ( dirName )
setState ( prevState = > {
setState ( prevState = > {
return { . . . prevState , focusElement : [ { key : newFolderPath , type : 'folder' } ] }
return { . . . prevState , focusElement : [ { key : newFolderPath , type : 'folder' } ] }
} )
} )
} catch ( e ) {
} catch ( e ) {
return modal ( 'Folder Creation Failed' , typeof e === 'string' ? e : e.message , {
return modal ( 'Folder Creation Failed' , typeof e === 'string' ? e : e.message , 'Close' , async ( ) = > { } )
label : 'Close' ,
fn : async ( ) = > { }
} , null )
}
}
}
}
const deletePath = async ( path : string | string [ ] ) = > {
const deletePath = async ( path : string | string [ ] ) = > {
const filesProvider = fileSystem . provider . provider
const filesProvider = fileSystem . provider . provider
if ( ! Array . isArray ( path ) ) path = [ path ]
if ( ! Array . isArray ( path ) ) path = [ path ]
const children : React.ReactFragment = < 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 >
for ( const p of path ) {
for ( const p of path ) {
if ( filesProvider . isReadOnly ( p ) ) {
if ( filesProvider . isReadOnly ( p ) ) {
return toast ( 'cannot delete file. ' + name + ' is a read only explorer' )
return toast ( 'cannot delete file. ' + name + ' is a read only explorer' )
}
}
}
}
modal ( ` Delete ${ path . length > 1 ? 'items' : 'item' } ` , '' , {
modal ( ` Delete ${ path . length > 1 ? 'items' : 'item' } ` , deleteMessage ( path ) , 'OK' , async ( ) = > {
label : 'OK' ,
fn : async ( ) = > {
const fileManager = state . fileManager
const fileManager = state . fileManager
for ( const p of path ) {
for ( const p of path ) {
try {
try {
@ -325,11 +386,7 @@ export const FileExplorer = (props: FileExplorerProps) => {
toast ( ` Failed to remove ${ isDir ? 'folder' : 'file' } ${ p } . ` )
toast ( ` Failed to remove ${ isDir ? 'folder' : 'file' } ${ p } . ` )
}
}
}
}
}
} , 'Cancel' , ( ) = > { } )
} , {
label : 'Cancel' ,
fn : ( ) = > { }
} , children )
}
}
const renamePath = async ( oldPath : string , newPath : string ) = > {
const renamePath = async ( oldPath : string , newPath : string ) = > {
@ -338,18 +395,12 @@ export const FileExplorer = (props: FileExplorerProps) => {
const exists = await fileManager . exists ( newPath )
const exists = await fileManager . exists ( newPath )
if ( exists ) {
if ( exists ) {
modal ( 'Rename File Failed' , ` A file or folder ${ extractNameFromKey ( newPath ) } already exists at this location. Please choose a different name. ` , {
modal ( 'Rename File Failed' , ` A file or folder ${ extractNameFromKey ( newPath ) } already exists at this location. Please choose a different name. ` , 'Close' , ( ) = > { } )
label : 'Close' ,
fn : ( ) = > { }
} , null )
} else {
} else {
await fileManager . rename ( oldPath , newPath )
await fileManager . rename ( oldPath , newPath )
}
}
} catch ( error ) {
} catch ( error ) {
modal ( 'Rename File Failed' , 'Unexpected error while renaming: ' + typeof error === 'string' ? error : error.message , {
modal ( 'Rename File Failed' , 'Unexpected error while renaming: ' + typeof error === 'string' ? error : error.message , 'Close' , async ( ) = > { } )
label : 'Close' ,
fn : async ( ) = > { }
} , null )
}
}
}
}
@ -359,7 +410,7 @@ export const FileExplorer = (props: FileExplorerProps) => {
// the files module. Please ask the user here if they want to overwrite
// the files module. Please ask the user here if they want to overwrite
// a file and then just use `files.add`. The file explorer will
// a file and then just use `files.add`. The file explorer will
// pick that up via the 'fileAdded' event from the files module.
// pick that up via the 'fileAdded' event from the files module.
const parentFolder = state . focusElement [ 0 ] ? state . focusElement [ 0 ] . type === 'folder' ? state . focusElement [ 0 ] . key : extractParentFromKey ( state . focusElement [ 0 ] . key ) : name
const parentFolder = getFocusedFolder ( )
const expandPath = [ . . . new Set ( [ . . . state . expandPath , parentFolder ] ) ]
const expandPath = [ . . . new Set ( [ . . . state . expandPath , parentFolder ] ) ]
setState ( prevState = > {
setState ( prevState = > {
@ -372,19 +423,13 @@ export const FileExplorer = (props: FileExplorerProps) => {
fileReader . onload = async function ( event ) {
fileReader . onload = async function ( event ) {
if ( helper . checkSpecialChars ( file . name ) ) {
if ( helper . checkSpecialChars ( file . name ) ) {
modal ( 'File Upload Failed' , 'Special characters are not allowed' , {
modal ( 'File Upload Failed' , 'Special characters are not allowed' , 'Close' , async ( ) = > { } )
label : 'Close' ,
fn : async ( ) = > { }
} , null )
return
return
}
}
const success = await filesProvider . set ( name , event . target . result )
const success = await filesProvider . set ( name , event . target . result )
if ( ! success ) {
if ( ! success ) {
return modal ( 'File Upload Failed' , 'Failed to create file ' + name , {
return modal ( 'File Upload Failed' , 'Failed to create file ' + name , 'Close' , async ( ) = > { } )
label : 'Close' ,
fn : async ( ) = > { }
} , null )
}
}
const config = registry . get ( 'config' ) . api
const config = registry . get ( 'config' ) . api
const editor = registry . get ( 'editor' ) . api
const editor = registry . get ( 'editor' ) . api
@ -401,15 +446,9 @@ export const FileExplorer = (props: FileExplorerProps) => {
if ( ! exist ) {
if ( ! exist ) {
loadFile ( name )
loadFile ( name )
} else {
} else {
modal ( 'Confirm overwrite' , ` The file ${ name } already exists! Would you like to overwrite it? ` , {
modal ( 'Confirm overwrite' , ` The file ${ name } already exists! Would you like to overwrite it? ` , 'OK' , ( ) = > {
label : 'OK' ,
fn : ( ) = > {
loadFile ( name )
loadFile ( name )
}
} , 'Cancel' , ( ) = > { } )
} , {
label : 'Cancel' ,
fn : ( ) = > { }
} )
}
}
} ) . catch ( error = > {
} ) . catch ( error = > {
if ( error ) console . log ( error )
if ( error ) console . log ( error )
@ -417,42 +456,56 @@ export const FileExplorer = (props: FileExplorerProps) => {
} )
} )
}
}
const publishToGist = ( ) = > {
const copyFile = ( src : string , dest : string ) = > {
modal ( 'Create a public gist' , ` Are you sure you want to anonymously publish all your files in the ${ name } workspace as a public gist on github.com? ` , {
const fileManager = state . fileManager
label : 'OK' ,
fn : toGist
try {
} , {
fileManager . copyFile ( src , dest )
label : 'Cancel' ,
} catch ( error ) {
fn : ( ) = > { }
console . log ( 'Oops! An error ocurred while performing copyFile operation.' + error )
} )
}
}
const copyFolder = ( src : string , dest : string ) = > {
const fileManager = state . fileManager
try {
fileManager . copyDir ( src , dest )
} catch ( error ) {
console . log ( 'Oops! An error ocurred while performing copyDir operation.' + error )
}
}
const publishToGist = ( path? : string , type ? : string ) = > {
modal ( 'Create a public gist' , ` Are you sure you want to anonymously publish all your files in the ${ name } workspace as a public gist on github.com? ` , 'OK' , ( ) = > toGist ( path , type ) , 'Cancel' , ( ) = > { } )
}
}
const toGist = ( id? : string ) = > {
const pushChangesToGist = ( path? : string , type ? : string ) = > {
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 ) = > {
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 ) = > {
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 toGist = ( path? : string , type ? : string ) = > {
const filesProvider = fileSystem . provider . provider
const filesProvider = fileSystem . provider . provider
const proccedResult = function ( error , data ) {
const proccedResult = function ( error , data ) {
if ( error ) {
if ( error ) {
modal ( 'Publish to gist Failed' , 'Failed to manage gist: ' + error , {
modal ( 'Publish to gist Failed' , 'Failed to manage gist: ' + error , 'Close' , ( ) = > { } )
label : 'Close' ,
fn : async ( ) = > { }
} , null )
} else {
} else {
if ( data . html_url ) {
if ( data . html_url ) {
modal ( 'Gist is ready' , ` The gist is at ${ data . html_url } . Would you like to open it in a new window? ` , {
modal ( 'Gist is ready' , ` The gist is at ${ data . html_url } . Would you like to open it in a new window? ` , 'OK' , ( ) = > {
label : 'OK' ,
fn : ( ) = > {
window . open ( data . html_url , '_blank' )
window . open ( data . html_url , '_blank' )
}
} , 'Cancel' , ( ) = > { } )
} , {
label : 'Cancel' ,
fn : ( ) = > { }
} )
} else {
} else {
const error = JSON . stringify ( data . errors , null , '\t' ) || ''
const error = JSON . stringify ( data . errors , null , '\t' ) || ''
const message = data . message === 'Not Found' ? data . message + '. Please make sure the API token has right to create a gist.' : data . message
const message = data . message === 'Not Found' ? data . message + '. Please make sure the API token has right to create a gist.' : data . message
modal ( 'Publish to gist Failed' , message + ' ' + data . documentation_url + ' ' + error , {
modal ( 'Publish to gist Failed' , message + ' ' + data . documentation_url + ' ' + error , 'Close' , ( ) = > { } )
label : 'Close' ,
fn : async ( ) = > { }
} , null )
}
}
}
}
}
}
@ -473,25 +526,20 @@ export const FileExplorer = (props: FileExplorerProps) => {
}
}
// If 'id' is not defined, it is not a gist update but a creation so we have to take the files from the browser explorer.
// If 'id' is not defined, it is not a gist update but a creation so we have to take the files from the browser explorer.
const folder = id ? '/gists/' + id : '/'
const folder = path || '/'
const id = type === 'gist' ? extractNameFromKey ( path ) . split ( '-' ) [ 1 ] : null
packageFiles ( filesProvider , folder , async ( error , packaged ) = > {
packageFiles ( filesProvider , folder , async ( error , packaged ) = > {
if ( error ) {
if ( error ) {
console . log ( error )
console . log ( error )
modal ( 'Publish to gist Failed' , 'Failed to create gist: ' + error . message , {
modal ( 'Publish to gist Failed' , 'Failed to create gist: ' + error . message , 'Close' , async ( ) = > { } )
label : 'Close' ,
fn : async ( ) = > { }
} , null )
} else {
} else {
// check for token
// check for token
const config = registry . get ( 'config' ) . api
const config = registry . get ( 'config' ) . api
const accessToken = config . get ( 'settings/gist-access-token' )
const accessToken = config . get ( 'settings/gist-access-token' )
if ( ! accessToken ) {
if ( ! accessToken ) {
modal ( 'Authorize Token' , 'Remix requires an access token (which includes gists creation permission). Please go to the settings tab to create one.' , {
modal ( 'Authorize Token' , 'Remix requires an access token (which includes gists creation permission). Please go to the settings tab to create one.' , 'Close' , ( ) = > { } )
label : 'Close' ,
fn : async ( ) = > { }
} , null )
} else {
} else {
const description = 'Created using remix-ide: Realtime Ethereum Contract Compiler and Runtime. \n Load this file by pasting this gists URL or ID at https://remix.ethereum.org/#version=' +
const description = 'Created using remix-ide: Realtime Ethereum Contract Compiler and Runtime. \n Load this file by pasting this gists URL or ID at https://remix.ethereum.org/#version=' +
queryParams . get ( ) . version + '&optimize=' + queryParams . get ( ) . optimize + '&runs=' + queryParams . get ( ) . runs + '&gist='
queryParams . get ( ) . version + '&optimize=' + queryParams . get ( ) . optimize + '&runs=' + queryParams . get ( ) . runs + '&gist='
@ -563,17 +611,18 @@ export const FileExplorer = (props: FileExplorerProps) => {
} )
} )
}
}
const modal = ( title : string , message : string , ok : { label : string , f n: ( ) = > void } , cancel : { label : string , fn : ( ) = > void } , children? :React.ReactNode ) = > {
const modal = ( title : string , message : string | JSX . Element , okLabel : string , okF n : ( ) = > void , cancelLabel? : string , cancelFn ? : ( ) = > void ) = > {
setState ( prevState = > {
setState ( prevState = > {
return {
return {
. . . prevState ,
. . . prevState ,
modals : [ . . . prevState . modals ,
modals : [ . . . prevState . modals ,
{
{
message ,
message ,
children ,
title ,
title ,
ok ,
okLabel ,
cancel ,
okFn ,
cancelLabel ,
cancelFn ,
handleHide : handleHideModal
handleHide : handleHideModal
} ]
} ]
}
}
@ -586,27 +635,30 @@ export const FileExplorer = (props: FileExplorerProps) => {
} )
} )
}
}
const handleClickFile = ( path : string ) = > {
const handleClickFile = ( path : string , type : string ) = > {
path = path . indexOf ( props . name + '/' ) === 0 ? path . replace ( props . name + '/' , '' ) : path
path = path . indexOf ( props . name + '/' ) === 0 ? path . replace ( props . name + '/' , '' ) : path
if ( ! state . ctrlKey ) {
if ( ! state . ctrlKey ) {
state . fileManager . open ( path )
state . fileManager . open ( path )
setState ( prevState = > {
setState ( prevState = > {
return { . . . prevState , focusElement : [ { key : path , type : 'file' } ] }
return { . . . prevState , focusElement : [ { key : path , type } ] }
} )
} )
} else {
} else {
if ( state . focusElement . findIndex ( item = > item . key === path ) !== - 1 ) {
if ( state . focusElement . findIndex ( item = > item . key === path ) !== - 1 ) {
setState ( prevState = > {
setState ( prevState = > {
return { . . . prevState , focusElement : [ . . . prevState . focusElement . filter ( item = > item . key !== path ) ] }
return { . . . prevState , focusElement : prevState.focusElement. filter ( item = > item . key !== path ) }
} )
} )
} else {
} else {
setState ( prevState = > {
setState ( prevState = > {
return { . . . prevState , focusElement : [ . . . prevState . focusElement , { key : path , type : 'file' } ] }
const nonRootFocus = prevState . focusElement . filter ( ( el ) = > { return ! ( el . key === '' && el . type === 'folder' ) } )
nonRootFocus . push ( { key : path , type } )
return { . . . prevState , focusElement : nonRootFocus }
} )
} )
}
}
}
}
}
}
const handleClickFolder = async ( path : string ) = > {
const handleClickFolder = async ( path : string , type : string ) = > {
if ( state . ctrlKey ) {
if ( state . ctrlKey ) {
if ( state . focusElement . findIndex ( item = > item . key === path ) !== - 1 ) {
if ( state . focusElement . findIndex ( item = > item . key === path ) !== - 1 ) {
setState ( prevState = > {
setState ( prevState = > {
@ -614,7 +666,10 @@ export const FileExplorer = (props: FileExplorerProps) => {
} )
} )
} else {
} else {
setState ( prevState = > {
setState ( prevState = > {
return { . . . prevState , focusElement : [ . . . prevState . focusElement , { key : path , type : 'folder' } ] }
const nonRootFocus = prevState . focusElement . filter ( ( el ) = > { return ! ( el . key === '' && el . type === 'folder' ) } )
nonRootFocus . push ( { key : path , type } )
return { . . . prevState , focusElement : nonRootFocus }
} )
} )
}
}
} else {
} else {
@ -628,22 +683,22 @@ export const FileExplorer = (props: FileExplorerProps) => {
}
}
setState ( prevState = > {
setState ( prevState = > {
return { . . . prevState , focusElement : [ { key : path , type : 'folder' } ] , expandPath }
return { . . . prevState , focusElement : [ { key : path , type } ] , expandPath }
} )
} )
}
}
}
}
const handleContextMenuFile = ( pageX : number , pageY : number , path : string , content : string ) = > {
const handleContextMenuFile = ( pageX : number , pageY : number , path : string , content : string , type : string ) = > {
if ( ! content ) return
if ( ! content ) return
setState ( prevState = > {
setState ( prevState = > {
return { . . . prevState , focusContext : { element : path , x : pageX , y : pageY , type : 'file' } , focusEdit : { . . . prevState . focusEdit , lastEdit : content } , showContextMenu : prevState.focusEdit.element !== path }
return { . . . prevState , focusContext : { element : path , x : pageX , y : pageY , type } , focusEdit : { . . . prevState . focusEdit , lastEdit : content } , showContextMenu : prevState.focusEdit.element !== path }
} )
} )
}
}
const handleContextMenuFolder = ( pageX : number , pageY : number , path : string , content : string ) = > {
const handleContextMenuFolder = ( pageX : number , pageY : number , path : string , content : string , type : string ) = > {
if ( ! content ) return
if ( ! content ) return
setState ( prevState = > {
setState ( prevState = > {
return { . . . prevState , focusContext : { element : path , x : pageX , y : pageY , type : 'folder' } , focusEdit : { . . . prevState . focusEdit , lastEdit : content } , showContextMenu : prevState.focusEdit.element !== path }
return { . . . prevState , focusContext : { element : path , x : pageX , y : pageY , type } , focusEdit : { . . . prevState . focusEdit , lastEdit : content } , showContextMenu : prevState.focusEdit.element !== path }
} )
} )
}
}
@ -684,14 +739,20 @@ export const FileExplorer = (props: FileExplorerProps) => {
} )
} )
}
}
if ( helper . checkSpecialChars ( content ) ) {
if ( helper . checkSpecialChars ( content ) ) {
modal ( 'Validation Error' , 'Special characters are not allowed' , {
modal ( 'Validation Error' , 'Special characters are not allowed' , 'OK' , ( ) = > { } )
label : 'OK' ,
fn : ( ) = > { }
} , null )
} else {
} else {
if ( state . focusEdit . isNew ) {
if ( state . focusEdit . isNew ) {
if ( hasReservedKeyword ( content ) ) {
removeInputField ( parentFolder ) ( dispatch )
modal ( 'Reserved Keyword' , ` File name contains remix reserved keywords. ' ${ content } ' ` , 'Close' , ( ) = > { } )
} else {
state . focusEdit . type === 'file' ? createNewFile ( joinPath ( parentFolder , content ) ) : createNewFolder ( joinPath ( parentFolder , content ) )
state . focusEdit . type === 'file' ? createNewFile ( joinPath ( parentFolder , content ) ) : createNewFolder ( joinPath ( parentFolder , content ) )
removeInputField ( parentFolder ) ( dispatch )
removeInputField ( parentFolder ) ( dispatch )
}
} else {
if ( hasReservedKeyword ( content ) ) {
editRef . current . textContent = state . focusEdit . lastEdit
modal ( 'Reserved Keyword' , ` File name contains remix reserved keywords. ' ${ content } ' ` , 'Close' , ( ) = > { } )
} else {
} else {
const oldPath : string = state . focusEdit . element
const oldPath : string = state . focusEdit . element
const oldName = extractNameFromKey ( oldPath )
const oldName = extractNameFromKey ( oldPath )
@ -700,6 +761,7 @@ export const FileExplorer = (props: FileExplorerProps) => {
editRef . current . textContent = extractNameFromKey ( oldPath )
editRef . current . textContent = extractNameFromKey ( oldPath )
renamePath ( oldPath , newPath )
renamePath ( oldPath , newPath )
}
}
}
setState ( prevState = > {
setState ( prevState = > {
return { . . . prevState , focusEdit : { element : null , isNew : false , type : '' , lastEdit : '' } }
return { . . . prevState , focusEdit : { element : null , isNew : false , type : '' , lastEdit : '' } }
} )
} )
@ -708,7 +770,7 @@ export const FileExplorer = (props: FileExplorerProps) => {
}
}
const handleNewFileInput = async ( parentFolder? : string ) = > {
const handleNewFileInput = async ( parentFolder? : string ) = > {
if ( ! parentFolder ) parentFolder = state . focusElement [ 0 ] ? state . focusElement [ 0 ] . type === 'folder' ? state . focusElement [ 0 ] . key ? state . focusElement [ 0 ] . key : name : extractParentFromKey ( state . focusElement [ 0 ] . key ) ? extractParentFromKey ( state . focusElement [ 0 ] . key ) : name : name
if ( ! parentFolder ) parentFolder = getFocusedFolder ( )
const expandPath = [ . . . new Set ( [ . . . state . expandPath , parentFolder ] ) ]
const expandPath = [ . . . new Set ( [ . . . state . expandPath , parentFolder ] ) ]
await addInputField ( fileSystem . provider . provider , 'file' , parentFolder ) ( dispatch )
await addInputField ( fileSystem . provider . provider , 'file' , parentFolder ) ( dispatch )
@ -719,7 +781,7 @@ export const FileExplorer = (props: FileExplorerProps) => {
}
}
const handleNewFolderInput = async ( parentFolder? : string ) = > {
const handleNewFolderInput = async ( parentFolder? : string ) = > {
if ( ! parentFolder ) parentFolder = state . focusElement [ 0 ] ? state . focusElement [ 0 ] . type === 'folder' ? state . focusElement [ 0 ] . key ? state . focusElement [ 0 ] . key : name : extractParentFromKey ( state . focusElement [ 0 ] . key ) ? extractParentFromKey ( state . focusElement [ 0 ] . key ) : name : name
if ( ! parentFolder ) parentFolder = getFocusedFolder ( )
else if ( ( parentFolder . indexOf ( '.sol' ) !== - 1 ) || ( parentFolder . indexOf ( '.js' ) !== - 1 ) ) parentFolder = extractParentFromKey ( parentFolder )
else if ( ( parentFolder . indexOf ( '.sol' ) !== - 1 ) || ( parentFolder . indexOf ( '.js' ) !== - 1 ) ) parentFolder = extractParentFromKey ( parentFolder )
const expandPath = [ . . . new Set ( [ . . . state . expandPath , parentFolder ] ) ]
const expandPath = [ . . . new Set ( [ . . . state . expandPath , parentFolder ] ) ]
@ -749,6 +811,32 @@ export const FileExplorer = (props: FileExplorerProps) => {
} )
} )
}
}
const handleCopyClick = ( path : string , type : string ) = > {
setState ( prevState = > {
return { . . . prevState , copyElement : [ { key : path , type } ] }
} )
setCanPaste ( true )
toast ( ` Copied to clipboard ${ path } ` )
}
const handlePasteClick = ( dest : string , destType : string ) = > {
dest = destType === 'file' ? extractParentFromKey ( dest ) || props.name : dest
state . copyElement . map ( ( { key , type } ) = > {
type === 'file' ? copyFile ( key , dest ) : copyFolder ( key , dest )
} )
}
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 label = ( file : File ) = > {
const label = ( file : File ) = > {
return (
return (
< div
< div
@ -795,12 +883,12 @@ export const FileExplorer = (props: FileExplorerProps) => {
label = { label ( file ) }
label = { label ( file ) }
onClick = { ( e ) = > {
onClick = { ( e ) = > {
e . stopPropagation ( )
e . stopPropagation ( )
if ( state . focusEdit . element !== file . path ) handleClickFolder ( file . path )
if ( state . focusEdit . element !== file . path ) handleClickFolder ( file . path , file . type )
} }
} }
onContextMenu = { ( e ) = > {
onContextMenu = { ( e ) = > {
e . preventDefault ( )
e . preventDefault ( )
e . stopPropagation ( )
e . stopPropagation ( )
handleContextMenuFolder ( e . pageX , e . pageY , file . path , e . target . textContent )
handleContextMenuFolder ( e . pageX , e . pageY , file . path , e . target . textContent , file . type )
} }
} }
labelClass = { labelClass }
labelClass = { labelClass }
controlBehaviour = { state . ctrlKey }
controlBehaviour = { state . ctrlKey }
@ -832,12 +920,12 @@ export const FileExplorer = (props: FileExplorerProps) => {
label = { label ( file ) }
label = { label ( file ) }
onClick = { ( e ) = > {
onClick = { ( e ) = > {
e . stopPropagation ( )
e . stopPropagation ( )
if ( state . focusEdit . element !== file . path ) handleClickFile ( file . path )
if ( state . focusEdit . element !== file . path ) handleClickFile ( file . path , file . type )
} }
} }
onContextMenu = { ( e ) = > {
onContextMenu = { ( e ) = > {
e . preventDefault ( )
e . preventDefault ( )
e . stopPropagation ( )
e . stopPropagation ( )
handleContextMenuFile ( e . pageX , e . pageY , file . path , e . target . textContent )
handleContextMenuFile ( e . pageX , e . pageY , file . path , e . target . textContent , file . type )
} }
} }
icon = { icon }
icon = { icon }
labelClass = { labelClass }
labelClass = { labelClass }
@ -906,21 +994,25 @@ export const FileExplorer = (props: FileExplorerProps) => {
message = { state . focusModal . message }
message = { state . focusModal . message }
children = { state . focusModal . children }
children = { state . focusModal . children }
hide = { state . focusModal . hide }
hide = { state . focusModal . hide }
ok = { state . focusModal . ok }
okLabel = { state . focusModal . okLabel }
cancel = { state . focusModal . cancel }
okFn = { state . focusModal . okFn }
cancelLabel = { state . focusModal . cancelLabel }
cancelFn = { state . focusModal . cancelFn }
handleHide = { handleHideModal }
handleHide = { handleHideModal }
/ >
/ >
}
}
< Toaster message = { state . toasterMsg } / >
< Toaster message = { state . toasterMsg } / >
{ state . showContextMenu &&
{ state . showContextMenu &&
< FileExplorerContextMenu
< FileExplorerContextMenu
actions = { state . actions }
actions = { state . focusElement . length > 1 ? state . actions . filter ( item = > item . multiselect ) : state . actions . filter ( item = > ! item . multiselect ) }
hideContextMenu = { hideContextMenu }
hideContextMenu = { hideContextMenu }
createNewFile = { handleNewFileInput }
createNewFile = { handleNewFileInput }
createNewFolder = { handleNewFolderInput }
createNewFolder = { handleNewFolderInput }
deletePath = { deletePath }
deletePath = { deletePath }
renamePath = { editModeOn }
renamePath = { editModeOn }
runScript = { runScript }
runScript = { runScript }
copy = { handleCopyClick }
paste = { handlePasteClick }
emit = { emitContextMenuEvent }
emit = { emitContextMenuEvent }
pageX = { state . focusContext . x }
pageX = { state . focusContext . x }
pageY = { state . focusContext . y }
pageY = { state . focusContext . y }
@ -931,6 +1023,9 @@ export const FileExplorer = (props: FileExplorerProps) => {
e . stopPropagation ( )
e . stopPropagation ( )
handleMouseOver ( state . focusContext . element )
handleMouseOver ( state . focusContext . element )
} }
} }
pushChangesToGist = { pushChangesToGist }
publishFolderToGist = { publishFolderToGist }
publishFileToGist = { publishFileToGist }
/ >
/ >
}
}
< / div >
< / div >
@ -940,12 +1035,34 @@ export const FileExplorer = (props: FileExplorerProps) => {
export default FileExplorer
export default FileExplorer
async function packageFiles ( filesProvider , directory , callback ) {
async function packageFiles ( filesProvider , directory , callback ) {
const isFile = filesProvider . isFile ( directory )
const ret = { }
const ret = { }
if ( isFile ) {
try {
filesProvider . get ( directory , ( error , content ) = > {
if ( error ) throw new Error ( 'An error ocurred while getting file content. ' + directory )
if ( /^\s+$/ . test ( content ) || ! content . length ) {
content = '// this line is added to create a gist. Empty file is not allowed.'
}
directory = directory . replace ( /\//g , '...' )
ret [ directory ] = { content }
callback ( null , ret )
} )
} catch ( e ) {
return callback ( e )
}
} else {
try {
try {
await filesProvider . copyFolderToJson ( directory , ( { path , content } ) = > {
await filesProvider . copyFolderToJson ( directory , ( { path , content } ) = > {
if ( /^\s+$/ . test ( content ) || ! content . length ) {
if ( /^\s+$/ . test ( content ) || ! content . length ) {
content = '// this line is added to create a gist. Empty file is not allowed.'
content = '// this line is added to create a gist. Empty file is not allowed.'
}
}
if ( path . indexOf ( 'gist-' ) === 0 ) {
path = path . split ( '/' )
path . shift ( )
path = path . join ( '/' )
}
path = path . replace ( /\//g , '...' )
path = path . replace ( /\//g , '...' )
ret [ path ] = { content }
ret [ path ] = { content }
} )
} )
@ -954,6 +1071,7 @@ async function packageFiles (filesProvider, directory, callback) {
return callback ( e )
return callback ( e )
}
}
}
}
}
function joinPath ( . . . paths ) {
function joinPath ( . . . paths ) {
paths = paths . filter ( ( value ) = > value !== '' ) . map ( ( path ) = > path . replace ( /^\/|\/$/g , '' ) ) // remove first and last slash)
paths = paths . filter ( ( value ) = > value !== '' ) . map ( ( path ) = > path . replace ( /^\/|\/$/g , '' ) ) // remove first and last slash)