@ -1,20 +1,39 @@
import React , { useEffect , useState , useRef , SyntheticEvent } from 'react' // eslint-disable-line
import React , { useEffect , useState , useRef , SyntheticEvent } from 'react' // eslint-disable-line
import { TreeView , TreeViewItem } from '@remix-ui/tree-view' // eslint-disable-line
import { TreeView , TreeViewItem } from '@remix-ui/tree-view' // eslint-disable-line
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 , WorkSpaceState } from '../types'
import { FileExplorerProps , WorkSpaceState } from '../types'
import '../css/file-explorer.css'
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"
import { Drag } from '@remix-ui/drag-n-drop'
import { ROOT_PATH } from '../utils/constants'
import { ROOT_PATH } from '../utils/constants'
export const FileExplorer = ( props : FileExplorerProps ) = > {
export const FileExplorer = ( props : FileExplorerProps ) = > {
const { name , contextMenuItems , removedContextMenuItems , files , workspaceState , toGist , addMenuItems ,
const {
removeMenuItems , handleContextMenu , handleNewFileInput , handleNewFolderInput , uploadFile , uploadFolder , fileState } = props
name ,
const [ state , setState ] = useState < WorkSpaceState > ( workspaceState )
contextMenuItems ,
removedContextMenuItems ,
files ,
workspaceState ,
toGist ,
addMenuItems ,
removeMenuItems ,
handleContextMenu ,
handleNewFileInput ,
handleNewFolderInput ,
uploadFile ,
uploadFolder ,
fileState
} = props
const [ state , setState ] = useState < WorkSpaceState > ( workspaceState )
const treeRef = useRef < HTMLDivElement > ( null )
const treeRef = useRef < HTMLDivElement > ( null )
useEffect ( ( ) = > {
useEffect ( ( ) = > {
@ -31,8 +50,16 @@ export const FileExplorer = (props: FileExplorerProps) => {
useEffect ( ( ) = > {
useEffect ( ( ) = > {
if ( props . focusEdit ) {
if ( props . focusEdit ) {
setState ( prevState = > {
setState ( ( prevState ) = > {
return { . . . prevState , focusEdit : { element : props.focusEdit , type : 'file' , isNew : true , lastEdit : null } }
return {
. . . prevState ,
focusEdit : {
element : props.focusEdit ,
type : 'file' ,
isNew : true ,
lastEdit : null
}
}
} )
} )
}
}
} , [ props . focusEdit ] )
} , [ props . focusEdit ] )
@ -45,16 +72,16 @@ export const FileExplorer = (props: FileExplorerProps) => {
if ( treeRef . current ) {
if ( treeRef . current ) {
const keyPressHandler = ( e : KeyboardEvent ) = > {
const keyPressHandler = ( e : KeyboardEvent ) = > {
if ( e . shiftKey ) {
if ( e . shiftKey ) {
setState ( prevState = > {
setState ( ( prevState ) = > {
return { . . . prevState , ctrlKey : true }
return { . . . prevState , ctrlKey : true }
} )
} )
}
}
}
}
const keyUpHandler = ( e : KeyboardEvent ) = > {
const keyUpHandler = ( e : KeyboardEvent ) = > {
if ( ! e . shiftKey ) {
if ( ! e . shiftKey ) {
setState ( prevState = > {
setState ( ( prevState ) = > {
return { . . . prevState , ctrlKey : false }
return { . . . prevState , ctrlKey : false }
} )
} )
}
}
}
}
@ -70,7 +97,11 @@ export const FileExplorer = (props: FileExplorerProps) => {
} , [ treeRef . current ] )
} , [ treeRef . current ] )
const hasReservedKeyword = ( content : string ) : boolean = > {
const hasReservedKeyword = ( content : string ) : boolean = > {
if ( state . reservedKeywords . findIndex ( value = > content . startsWith ( value ) ) !== - 1 ) return true
if (
state . reservedKeywords . findIndex ( ( value ) = > content . startsWith ( value ) ) !==
- 1
)
return true
else return false
else return false
}
}
@ -78,7 +109,12 @@ export const FileExplorer = (props: FileExplorerProps) => {
try {
try {
props . dispatchCreateNewFile ( newFilePath , ROOT_PATH )
props . dispatchCreateNewFile ( newFilePath , ROOT_PATH )
} catch ( error ) {
} catch ( error ) {
return props . modal ( 'File Creation Failed' , typeof error === 'string' ? error : error.message , 'Close' , async ( ) = > { } )
return props . modal (
'File Creation Failed' ,
typeof error === 'string' ? error : error.message ,
'Close' ,
async ( ) = > { }
)
}
}
}
}
@ -86,7 +122,12 @@ export const FileExplorer = (props: FileExplorerProps) => {
try {
try {
props . dispatchCreateNewFolder ( newFolderPath , ROOT_PATH )
props . dispatchCreateNewFolder ( newFolderPath , ROOT_PATH )
} catch ( e ) {
} catch ( e ) {
return props . modal ( 'Folder Creation Failed' , typeof e === 'string' ? e : e.message , 'Close' , async ( ) = > { } )
return props . modal (
'Folder Creation Failed' ,
typeof e === 'string' ? e : e.message ,
'Close' ,
async ( ) = > { }
)
}
}
}
}
@ -94,42 +135,66 @@ export const FileExplorer = (props: FileExplorerProps) => {
try {
try {
props . dispatchRenamePath ( oldPath , newPath )
props . dispatchRenamePath ( oldPath , newPath )
} catch ( error ) {
} catch ( error ) {
props . modal ( 'Rename File Failed' , 'Unexpected error while renaming: ' + typeof error === 'string' ? error : error.message , 'Close' , async ( ) = > { } )
props . modal (
'Rename File Failed' ,
'Unexpected error while renaming: ' + typeof error === 'string'
? error
: error . message ,
'Close' ,
async ( ) = > { }
)
}
}
}
}
const publishToGist = ( path? : string , type ? : string ) = > {
const publishToGist = ( path? : string , type ? : string ) = > {
props . 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' , ( ) = > { } )
props . 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 handleClickFile = ( path : string , type : 'folder' | 'file' | 'gist' ) = > {
const handleClickFile = ( path : string , type : 'folder' | 'file' | 'gist' ) = > {
if ( ! state . ctrlKey ) {
if ( ! state . ctrlKey ) {
props . dispatchHandleClickFile ( path , type )
props . dispatchHandleClickFile ( path , type )
} else {
} else {
if ( props . focusElement . findIndex ( item = > item . key === path ) !== - 1 ) {
if ( props . focusElement . findIndex ( ( item ) = > item . key === path ) !== - 1 ) {
const focusElement = props . focusElement . filter ( item = > item . key !== path )
const focusElement = props . focusElement . filter (
( item ) = > item . key !== path
)
props . dispatchSetFocusElement ( focusElement )
props . dispatchSetFocusElement ( focusElement )
} else {
} else {
const nonRootFocus = props . focusElement . filter ( ( el ) = > { return ! ( el . key === '' && el . type === 'folder' ) } )
const nonRootFocus = props . focusElement . filter ( ( el ) = > {
return ! ( el . key === '' && el . type === 'folder' )
} )
nonRootFocus . push ( { key : path , type } )
nonRootFocus . push ( { key : path , type } )
props . dispatchSetFocusElement ( nonRootFocus )
props . dispatchSetFocusElement ( nonRootFocus )
}
}
}
}
}
}
const handleClickFolder = async ( path : string , type : 'folder' | 'file' | 'gist' ) = > {
const handleClickFolder = async (
path : string ,
type : 'folder' | 'file' | 'gist'
) = > {
if ( state . ctrlKey ) {
if ( state . ctrlKey ) {
if ( props . focusElement . findIndex ( item = > item . key === path ) !== - 1 ) {
if ( props . focusElement . findIndex ( ( item ) = > item . key === path ) !== - 1 ) {
const focusElement = props . focusElement . filter ( item = > item . key !== path )
const focusElement = props . focusElement . filter (
( item ) = > item . key !== path
)
props . dispatchSetFocusElement ( focusElement )
props . dispatchSetFocusElement ( focusElement )
} else {
} else {
const nonRootFocus = props . focusElement . filter ( ( el ) = > { return ! ( el . key === '' && el . type === 'folder' ) } )
const nonRootFocus = props . focusElement . filter ( ( el ) = > {
return ! ( el . key === '' && el . type === 'folder' )
} )
nonRootFocus . push ( { key : path , type } )
nonRootFocus . push ( { key : path , type } )
props . dispatchSetFocusElement ( nonRootFocus )
props . dispatchSetFocusElement ( nonRootFocus )
}
}
} else {
} else {
@ -139,10 +204,16 @@ export const FileExplorer = (props: FileExplorerProps) => {
expandPath = [ . . . new Set ( [ . . . props . expandPath , path ] ) ]
expandPath = [ . . . new Set ( [ . . . props . expandPath , path ] ) ]
props . dispatchFetchDirectory ( path )
props . dispatchFetchDirectory ( path )
} else {
} else {
expandPath = [ . . . new Set ( props . expandPath . filter ( key = > key && ( typeof key === 'string' ) && ! key . startsWith ( path ) ) ) ]
expandPath = [
. . . new Set (
props . expandPath . filter (
( key ) = > key && typeof key === 'string' && ! key . startsWith ( path )
)
)
]
}
}
props . dispatchSetFocusElement ( [ { key : path , type } ] )
props . dispatchSetFocusElement ( [ { key : path , type } ] )
props . dispatchHandleExpandPath ( expandPath )
props . dispatchHandleExpandPath ( expandPath )
}
}
}
}
@ -151,37 +222,63 @@ export const FileExplorer = (props: FileExplorerProps) => {
if ( typeof content === 'string' ) content = content . trim ( )
if ( typeof content === 'string' ) content = content . trim ( )
const parentFolder = extractParentFromKey ( state . focusEdit . element )
const parentFolder = extractParentFromKey ( state . focusEdit . element )
if ( ! content || ( content . trim ( ) === '' ) ) {
if ( ! content || content . trim ( ) === '' ) {
if ( state . focusEdit . isNew ) {
if ( state . focusEdit . isNew ) {
props . dispatchRemoveInputField ( parentFolder )
props . dispatchRemoveInputField ( parentFolder )
setState ( prevState = > {
setState ( ( prevState ) = > {
return { . . . prevState , focusEdit : { element : null , isNew : false , type : '' , lastEdit : '' } }
return {
. . . prevState ,
focusEdit : { element : null , isNew : false , type : '' , lastEdit : '' }
}
} )
} )
} else {
} else {
setState ( prevState = > {
setState ( ( prevState ) = > {
return { . . . prevState , focusEdit : { element : null , isNew : false , type : '' , lastEdit : '' } }
return {
. . . prevState ,
focusEdit : { element : null , isNew : false , type : '' , lastEdit : '' }
}
} )
} )
}
}
} else {
} else {
if ( state . focusEdit . lastEdit === content ) {
if ( state . focusEdit . lastEdit === content ) {
return setState ( prevState = > {
return setState ( ( prevState ) = > {
return { . . . prevState , focusEdit : { element : null , isNew : false , type : '' , lastEdit : '' } }
return {
. . . prevState ,
focusEdit : { element : null , isNew : false , type : '' , lastEdit : '' }
}
} )
} )
}
}
if ( checkSpecialChars ( content ) ) {
if ( checkSpecialChars ( content ) ) {
props . modal ( 'Validation Error' , 'Special characters are not allowed' , 'OK' , ( ) = > { } )
props . modal (
'Validation Error' ,
'Special characters are not allowed' ,
'OK' ,
( ) = > { }
)
} else {
} else {
if ( state . focusEdit . isNew ) {
if ( state . focusEdit . isNew ) {
if ( hasReservedKeyword ( content ) ) {
if ( hasReservedKeyword ( content ) ) {
props . dispatchRemoveInputField ( parentFolder )
props . dispatchRemoveInputField ( parentFolder )
props . modal ( 'Reserved Keyword' , ` File name contains Remix reserved keywords. ' ${ content } ' ` , 'Close' , ( ) = > { } )
props . modal (
'Reserved Keyword' ,
` File name contains Remix reserved keywords. ' ${ content } ' ` ,
'Close' ,
( ) = > { }
)
} else {
} 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 ) )
props . dispatchRemoveInputField ( parentFolder )
props . dispatchRemoveInputField ( parentFolder )
}
}
} else {
} else {
if ( hasReservedKeyword ( content ) ) {
if ( hasReservedKeyword ( content ) ) {
props . modal ( 'Reserved Keyword' , ` File name contains Remix reserved keywords. ' ${ content } ' ` , 'Close' , ( ) = > { } )
props . modal (
'Reserved Keyword' ,
` File name contains Remix reserved keywords. ' ${ content } ' ` ,
'Close' ,
( ) = > { }
)
} else {
} else {
if ( state . focusEdit . element ) {
if ( state . focusEdit . element ) {
const oldPath : string = state . focusEdit . element
const oldPath : string = state . focusEdit . element
@ -192,24 +289,39 @@ export const FileExplorer = (props: FileExplorerProps) => {
}
}
}
}
}
}
setState ( prevState = > {
setState ( ( prevState ) = > {
return { . . . prevState , focusEdit : { element : null , isNew : false , type : '' , lastEdit : '' } }
return {
. . . prevState ,
focusEdit : { element : null , isNew : false , type : '' , lastEdit : '' }
}
} )
} )
}
}
}
}
}
}
const handleFileExplorerMenuClick = ( e : SyntheticEvent ) = > {
const handleFileExplorerMenuClick = ( e : SyntheticEvent ) = > {
e . stopPropagation ( )
e . stopPropagation ( )
if ( e && ( e . target as any ) . getAttribute ( 'data-id' ) === 'fileExplorerUploadFileuploadFile' ) return // we don't want to let propagate the input of type file
if (
if ( e && ( e . target as any ) . getAttribute ( 'data-id' ) === 'fileExplorerFileUpload' ) return // we don't want to let propagate the input of type file
e &&
( e . target as any ) . getAttribute ( 'data-id' ) ===
'fileExplorerUploadFileuploadFile'
)
return // we don't want to let propagate the input of type file
if (
e &&
( e . target as any ) . getAttribute ( 'data-id' ) === 'fileExplorerFileUpload'
)
return // we don't want to let propagate the input of type file
let expandPath = [ ]
let expandPath = [ ]
if ( ! props . expandPath . includes ( ROOT_PATH ) ) {
if ( ! props . expandPath . includes ( ROOT_PATH ) ) {
expandPath = [ ROOT_PATH , . . . new Set ( [ . . . props . expandPath ] ) ]
expandPath = [ ROOT_PATH , . . . new Set ( [ . . . props . expandPath ] ) ]
} else {
} else {
expandPath = [ . . . new Set ( props . expandPath . filter ( key = > key && ( typeof key === 'string' ) ) ) ]
expandPath = [
. . . new Set (
props . expandPath . filter ( ( key ) = > key && typeof key === 'string' )
)
]
}
}
props . dispatchHandleExpandPath ( expandPath )
props . dispatchHandleExpandPath ( expandPath )
}
}
@ -218,7 +330,12 @@ export const FileExplorer = (props: FileExplorerProps) => {
try {
try {
props . dispatchMoveFile ( src , dest )
props . dispatchMoveFile ( src , dest )
} catch ( error ) {
} catch ( error ) {
props . modal ( 'Moving File Failed' , 'Unexpected error while moving file: ' + src , 'Close' , async ( ) = > { } )
props . modal (
'Moving File Failed' ,
'Unexpected error while moving file: ' + src ,
'Close' ,
async ( ) = > { }
)
}
}
}
}
@ -226,15 +343,21 @@ export const FileExplorer = (props: FileExplorerProps) => {
try {
try {
props . dispatchMoveFolder ( src , dest )
props . dispatchMoveFolder ( src , dest )
} catch ( error ) {
} catch ( error ) {
props . modal ( 'Moving Folder Failed' , 'Unexpected error while moving folder: ' + src , 'Close' , async ( ) = > { } )
props . modal (
'Moving Folder Failed' ,
'Unexpected error while moving folder: ' + src ,
'Close' ,
async ( ) = > { }
)
}
}
}
}
return (
return (
< Drag onFileMoved = { handleFileMove } onFolderMoved = { handleFolderMove } >
< Drag onFileMoved = { handleFileMove } onFolderMoved = { handleFolderMove } >
< 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"
controlBehaviour = { true }
controlBehaviour = { true }
label = {
label = {
< div onClick = { handleFileExplorerMenuClick } >
< div onClick = { handleFileExplorerMenuClick } >
@ -249,28 +372,30 @@ export const FileExplorer = (props: FileExplorerProps) => {
/ >
/ >
< / div >
< / div >
}
}
expand = { true } >
expand = { true }
< div className = 'pb-4 mb-4' >
>
< TreeView id = 'treeViewMenu' >
< div className = "pb-4 mb-4" >
{
< TreeView id = "treeViewMenu" >
files [ ROOT_PATH ] && Object . keys ( files [ ROOT_PATH ] ) . map ( ( key , index ) = > < FileRender
{ files [ ROOT_PATH ] &&
file = { files [ ROOT_PATH ] [ key ] }
Object . keys ( files [ ROOT_PATH ] ) . map ( ( key , index ) = > (
fileDecorations = { fileState }
< FileRender
index = { index }
file = { files [ ROOT_PATH ] [ key ] }
focusContext = { state . focusContext }
fileDecorations = { fileState }
focusEdit = { state . focusEdit }
index = { index }
focusElement = { props . focusElement }
focusContext = { state . focusContext }
ctrlKey = { state . ctrlKey }
focusEdit = { state . focusEdit }
expandPath = { props . expandPath }
focusElement = { props . focusElement }
editModeOff = { editModeOff }
ctrlKey = { state . ctrlKey }
handleClickFile = { handleClickFile }
expandPath = { props . expandPath }
handleClickFolder = { handleClickFolder }
editModeOff = { editModeOff }
handleContextMenu = { handleContextMenu }
handleClickFile = { handleClickFile }
key = { index }
handleClickFolder = { handleClickFolder }
showIconsMenu = { props . showIconsMenu }
handleContextMenu = { handleContextMenu }
hideIconsMenu = { props . hideIconsMenu }
key = { index }
/ > )
showIconsMenu = { props . showIconsMenu }
}
hideIconsMenu = { props . hideIconsMenu }
/ >
) ) }
< / TreeView >
< / TreeView >
< / div >
< / div >
< / TreeViewItem >
< / TreeViewItem >