commit
03437f3295
@ -0,0 +1,56 @@ |
|||||||
|
.remixui_container { |
||||||
|
display : flex; |
||||||
|
flex-direction : row; |
||||||
|
width : 100%; |
||||||
|
height : 100%; |
||||||
|
box-sizing : border-box; |
||||||
|
} |
||||||
|
.remixui_fileexplorer { |
||||||
|
display : flex; |
||||||
|
flex-direction : column; |
||||||
|
position : relative; |
||||||
|
width : 100%; |
||||||
|
padding-left : 6px; |
||||||
|
padding-top : 6px; |
||||||
|
} |
||||||
|
.remixui_fileExplorerTree { |
||||||
|
cursor : default; |
||||||
|
} |
||||||
|
.remixui_gist { |
||||||
|
padding : 10px; |
||||||
|
} |
||||||
|
.remixui_gist i { |
||||||
|
cursor : pointer; |
||||||
|
} |
||||||
|
.remixui_gist i:hover { |
||||||
|
color : orange; |
||||||
|
} |
||||||
|
.remixui_connectToLocalhost { |
||||||
|
padding : 10px; |
||||||
|
} |
||||||
|
.remixui_connectToLocalhost i { |
||||||
|
cursor : pointer; |
||||||
|
} |
||||||
|
.remixui_connectToLocalhost i:hover { |
||||||
|
color : var(--secondary) |
||||||
|
} |
||||||
|
.remixui_uploadFile { |
||||||
|
padding : 10px; |
||||||
|
} |
||||||
|
.remixui_uploadFile label:hover { |
||||||
|
color : var(--secondary) |
||||||
|
} |
||||||
|
.remixui_uploadFile label { |
||||||
|
cursor : pointer; |
||||||
|
} |
||||||
|
.remixui_treeview { |
||||||
|
overflow-y : auto; |
||||||
|
} |
||||||
|
.remixui_dialog { |
||||||
|
display: flex; |
||||||
|
flex-direction: column; |
||||||
|
} |
||||||
|
.remixui_dialogParagraph { |
||||||
|
margin-bottom: 2em; |
||||||
|
word-break: break-word; |
||||||
|
} |
@ -0,0 +1,4 @@ |
|||||||
|
{ |
||||||
|
"presets": ["@nrwl/react/babel"], |
||||||
|
"plugins": [] |
||||||
|
} |
@ -0,0 +1,19 @@ |
|||||||
|
{ |
||||||
|
"env": { |
||||||
|
"browser": true, |
||||||
|
"es6": true |
||||||
|
}, |
||||||
|
"extends": "../../../.eslintrc", |
||||||
|
"globals": { |
||||||
|
"Atomics": "readonly", |
||||||
|
"SharedArrayBuffer": "readonly" |
||||||
|
}, |
||||||
|
"parserOptions": { |
||||||
|
"ecmaVersion": 11, |
||||||
|
"sourceType": "module" |
||||||
|
}, |
||||||
|
"rules": { |
||||||
|
"no-unused-vars": "off", |
||||||
|
"@typescript-eslint/no-unused-vars": "error" |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,7 @@ |
|||||||
|
# remix-ui-file-explorer |
||||||
|
|
||||||
|
This library was generated with [Nx](https://nx.dev). |
||||||
|
|
||||||
|
## Running unit tests |
||||||
|
|
||||||
|
Run `nx test remix-ui-file-explorer` to execute the unit tests via [Jest](https://jestjs.io). |
@ -0,0 +1 @@ |
|||||||
|
export * from './lib/file-explorer' |
@ -0,0 +1,28 @@ |
|||||||
|
.remixui_contextContainer |
||||||
|
{ |
||||||
|
display: block; |
||||||
|
position: fixed; |
||||||
|
border-radius: 2px; |
||||||
|
z-index: 1000; |
||||||
|
box-shadow: 0 0 4px var(--dark); |
||||||
|
} |
||||||
|
.remixui_contextContainer:focus { |
||||||
|
outline: none; |
||||||
|
} |
||||||
|
.remixui_liitem |
||||||
|
{ |
||||||
|
padding: 2px; |
||||||
|
padding-left: 6px; |
||||||
|
cursor: pointer; |
||||||
|
color: var(--text-dark); |
||||||
|
background-color: var(--light); |
||||||
|
} |
||||||
|
.remixui_liitem:hover |
||||||
|
{ |
||||||
|
background-color: var(--secondary); |
||||||
|
} |
||||||
|
#remixui_menuitems |
||||||
|
{ |
||||||
|
list-style: none; |
||||||
|
margin: 0px; |
||||||
|
} |
@ -0,0 +1,55 @@ |
|||||||
|
.remixui_label { |
||||||
|
margin-top : 4px; |
||||||
|
} |
||||||
|
.remixui_leaf { |
||||||
|
overflow : hidden; |
||||||
|
text-overflow : ellipsis; |
||||||
|
width : 90%; |
||||||
|
margin-bottom : 0px; |
||||||
|
} |
||||||
|
.remixui_fileexplorer { |
||||||
|
box-sizing : border-box; |
||||||
|
} |
||||||
|
input[type="file"] { |
||||||
|
display: none; |
||||||
|
} |
||||||
|
.remixui_folder, |
||||||
|
.remixui_file { |
||||||
|
font-size : 14px; |
||||||
|
cursor : pointer; |
||||||
|
} |
||||||
|
.remixui_file { |
||||||
|
padding : 4px; |
||||||
|
} |
||||||
|
.remixui_newFile { |
||||||
|
padding-right : 10px; |
||||||
|
} |
||||||
|
.remixui_newFile i { |
||||||
|
cursor : pointer; |
||||||
|
} |
||||||
|
.remixui_newFile:hover { |
||||||
|
transform : scale(1.3); |
||||||
|
} |
||||||
|
.remixui_menu { |
||||||
|
margin-left : 20px; |
||||||
|
} |
||||||
|
.remixui_items { |
||||||
|
display : inline |
||||||
|
} |
||||||
|
.remixui_remove { |
||||||
|
margin-left : auto; |
||||||
|
padding-left : 5px; |
||||||
|
padding-right : 5px; |
||||||
|
} |
||||||
|
.remixui_activeMode { |
||||||
|
display : flex; |
||||||
|
width : 100%; |
||||||
|
margin-right : 10px; |
||||||
|
padding-right : 19px; |
||||||
|
} |
||||||
|
.remixui_activeMode > div { |
||||||
|
min-width : 10px; |
||||||
|
} |
||||||
|
ul { |
||||||
|
padding : 0; |
||||||
|
} |
@ -0,0 +1,81 @@ |
|||||||
|
import React, { useRef, useEffect } from 'react' // eslint-disable-line
|
||||||
|
import { FileExplorerContextMenuProps } from './types' |
||||||
|
|
||||||
|
import './css/file-explorer-context-menu.css' |
||||||
|
|
||||||
|
export const FileExplorerContextMenu = (props: FileExplorerContextMenuProps) => { |
||||||
|
const { actions, createNewFile, createNewFolder, deletePath, renamePath, hideContextMenu, publishToGist, runScript, pageX, pageY, path, type, ...otherProps } = props |
||||||
|
const contextMenuRef = useRef(null) |
||||||
|
|
||||||
|
useEffect(() => { |
||||||
|
contextMenuRef.current.focus() |
||||||
|
}, []) |
||||||
|
|
||||||
|
useEffect(() => { |
||||||
|
const menuItemsContainer = contextMenuRef.current |
||||||
|
const boundary = menuItemsContainer.getBoundingClientRect() |
||||||
|
|
||||||
|
if (boundary.bottom > (window.innerHeight || document.documentElement.clientHeight)) { |
||||||
|
menuItemsContainer.style.position = 'absolute' |
||||||
|
menuItemsContainer.style.bottom = '10px' |
||||||
|
menuItemsContainer.style.top = null |
||||||
|
} |
||||||
|
}, [pageX, pageY]) |
||||||
|
|
||||||
|
const menu = () => { |
||||||
|
return actions.filter(item => { |
||||||
|
if (item.type.findIndex(name => name === type) !== -1) return true |
||||||
|
else if (item.path.findIndex(key => key === path) !== -1) return true |
||||||
|
else if (item.extension.findIndex(ext => path.endsWith(ext)) !== -1) return true |
||||||
|
else if (item.pattern.filter(value => path.match(new RegExp(value))).length > 0) return true |
||||||
|
else return false |
||||||
|
}).map((item, index) => { |
||||||
|
return <li |
||||||
|
id={`menuitem${item.name.toLowerCase()}`} |
||||||
|
key={index} |
||||||
|
className='remixui_liitem' |
||||||
|
onClick={(e) => { |
||||||
|
e.stopPropagation() |
||||||
|
switch (item.name) { |
||||||
|
case 'New File': |
||||||
|
createNewFile(path) |
||||||
|
break |
||||||
|
case 'New Folder': |
||||||
|
createNewFolder(path) |
||||||
|
break |
||||||
|
case 'Rename': |
||||||
|
renamePath(path, type) |
||||||
|
break |
||||||
|
case 'Delete': |
||||||
|
deletePath(path) |
||||||
|
break |
||||||
|
case 'Push changes to gist': |
||||||
|
publishToGist() |
||||||
|
break |
||||||
|
case 'Run': |
||||||
|
runScript(path) |
||||||
|
break |
||||||
|
default: |
||||||
|
break |
||||||
|
} |
||||||
|
hideContextMenu() |
||||||
|
}}>{item.name}</li> |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
return ( |
||||||
|
<div |
||||||
|
id="menuItemsContainer" |
||||||
|
className="p-1 remixui_contextContainer bg-light shadow border" |
||||||
|
style={{ left: pageX, top: pageY }} |
||||||
|
ref={contextMenuRef} |
||||||
|
onBlur={hideContextMenu} |
||||||
|
tabIndex={500} |
||||||
|
{...otherProps} |
||||||
|
> |
||||||
|
<ul id='remixui_menuitems'>{menu()}</ul> |
||||||
|
</div> |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
export default FileExplorerContextMenu |
@ -0,0 +1,97 @@ |
|||||||
|
import React, { useState, useEffect } from 'react' //eslint-disable-line
|
||||||
|
import { FileExplorerMenuProps } from './types' |
||||||
|
|
||||||
|
export const FileExplorerMenu = (props: FileExplorerMenuProps) => { |
||||||
|
const [state, setState] = useState({ |
||||||
|
menuItems: [ |
||||||
|
{ |
||||||
|
action: 'createNewFile', |
||||||
|
title: 'Create New File', |
||||||
|
icon: 'far fa-file' |
||||||
|
}, |
||||||
|
{ |
||||||
|
action: 'createNewFolder', |
||||||
|
title: 'Create New Folder', |
||||||
|
icon: 'far fa-folder' |
||||||
|
}, |
||||||
|
{ |
||||||
|
action: 'publishToGist', |
||||||
|
title: 'Publish all [browser] explorer files to a github gist', |
||||||
|
icon: 'fab fa-github' |
||||||
|
}, |
||||||
|
{ |
||||||
|
action: 'uploadFile', |
||||||
|
title: 'Load a local file into Remix\'s browser folder', |
||||||
|
icon: 'fa fa-upload' |
||||||
|
}, |
||||||
|
{ |
||||||
|
action: 'updateGist', |
||||||
|
title: 'Update the current [gist] explorer', |
||||||
|
icon: 'fab fa-github' |
||||||
|
} |
||||||
|
].filter(item => props.menuItems && props.menuItems.find((name) => { return name === item.action })), |
||||||
|
actions: {} |
||||||
|
}) |
||||||
|
|
||||||
|
useEffect(() => { |
||||||
|
const actions = { |
||||||
|
updateGist: () => {} |
||||||
|
} |
||||||
|
|
||||||
|
setState(prevState => { |
||||||
|
return { ...prevState, actions } |
||||||
|
}) |
||||||
|
}, []) |
||||||
|
|
||||||
|
return ( |
||||||
|
<> |
||||||
|
<span className='remixui_label' title={props.title} data-path={props.title} style={{ fontWeight: 'bold' }}>{ props.title }</span> |
||||||
|
<span className="remixui_menu">{ |
||||||
|
state.menuItems.map(({ action, title, icon }, index) => { |
||||||
|
if (action === 'uploadFile') { |
||||||
|
return ( |
||||||
|
<label |
||||||
|
id={action} |
||||||
|
data-id={'fileExplorerUploadFile' + action } |
||||||
|
className={icon + ' mb-0 remixui_newFile'} |
||||||
|
title={title} |
||||||
|
key={index} |
||||||
|
> |
||||||
|
<input id="fileUpload" data-id="fileExplorerFileUpload" type="file" onChange={(e) => { |
||||||
|
e.stopPropagation() |
||||||
|
props.uploadFile(e.target) |
||||||
|
}} |
||||||
|
multiple /> |
||||||
|
</label> |
||||||
|
) |
||||||
|
} else { |
||||||
|
return ( |
||||||
|
<span |
||||||
|
id={action} |
||||||
|
data-id={'fileExplorerNewFile' + action} |
||||||
|
onClick={(e) => { |
||||||
|
e.stopPropagation() |
||||||
|
if (action === 'createNewFile') { |
||||||
|
props.createNewFile() |
||||||
|
} else if (action === 'createNewFolder') { |
||||||
|
props.createNewFolder() |
||||||
|
} else if (action === 'publishToGist') { |
||||||
|
props.publishToGist() |
||||||
|
} else { |
||||||
|
state.actions[action]() |
||||||
|
} |
||||||
|
}} |
||||||
|
className={'newFile ' + icon + ' remixui_newFile'} |
||||||
|
title={title} |
||||||
|
key={index} |
||||||
|
> |
||||||
|
</span> |
||||||
|
) |
||||||
|
} |
||||||
|
})} |
||||||
|
</span> |
||||||
|
</> |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
export default FileExplorerMenu |
@ -0,0 +1,947 @@ |
|||||||
|
import React, { useEffect, useState, useRef } from 'react' // eslint-disable-line
|
||||||
|
// import { DragDropContext, Droppable, Draggable } from 'react-beautiful-dnd' // eslint-disable-line
|
||||||
|
import { TreeView, TreeViewItem } from '@remix-ui/tree-view' // eslint-disable-line
|
||||||
|
import { ModalDialog } from '@remix-ui/modal-dialog' // eslint-disable-line
|
||||||
|
import { Toaster } from '@remix-ui/toaster' // eslint-disable-line
|
||||||
|
import * as async from 'async' |
||||||
|
import Gists from 'gists' |
||||||
|
import { FileExplorerMenu } from './file-explorer-menu' // eslint-disable-line
|
||||||
|
import { FileExplorerContextMenu } from './file-explorer-context-menu' // eslint-disable-line
|
||||||
|
import { FileExplorerProps, File } from './types' |
||||||
|
import * as helper from '../../../../../apps/remix-ide/src/lib/helper' |
||||||
|
import QueryParams from '../../../../../apps/remix-ide/src/lib/query-params' |
||||||
|
|
||||||
|
import './css/file-explorer.css' |
||||||
|
|
||||||
|
const queryParams = new QueryParams() |
||||||
|
|
||||||
|
export const FileExplorer = (props: FileExplorerProps) => { |
||||||
|
const { filesProvider, name, registry, plugin } = props |
||||||
|
const [state, setState] = useState({ |
||||||
|
focusElement: [{ |
||||||
|
key: name, |
||||||
|
type: 'folder' |
||||||
|
}], |
||||||
|
focusPath: null, |
||||||
|
files: [], |
||||||
|
fileManager: null, |
||||||
|
accessToken: null, |
||||||
|
ctrlKey: false, |
||||||
|
newFileName: '', |
||||||
|
actions: [], |
||||||
|
focusContext: { |
||||||
|
element: null, |
||||||
|
x: null, |
||||||
|
y: null |
||||||
|
}, |
||||||
|
focusEdit: { |
||||||
|
element: null, |
||||||
|
type: '', |
||||||
|
isNew: false, |
||||||
|
lastEdit: '' |
||||||
|
}, |
||||||
|
expandPath: [], |
||||||
|
modalOptions: { |
||||||
|
hide: true, |
||||||
|
title: '', |
||||||
|
message: '', |
||||||
|
ok: { |
||||||
|
label: 'Ok', |
||||||
|
fn: null |
||||||
|
}, |
||||||
|
cancel: { |
||||||
|
label: 'Cancel', |
||||||
|
fn: null |
||||||
|
}, |
||||||
|
handleHide: null |
||||||
|
}, |
||||||
|
toasterMsg: '' |
||||||
|
}) |
||||||
|
const editRef = useRef(null) |
||||||
|
|
||||||
|
useEffect(() => { |
||||||
|
if (state.focusEdit.element) { |
||||||
|
setTimeout(() => { |
||||||
|
if (editRef && editRef.current) { |
||||||
|
editRef.current.focus() |
||||||
|
} |
||||||
|
}, 150) |
||||||
|
} |
||||||
|
}, [state.focusEdit.element]) |
||||||
|
|
||||||
|
useEffect(() => { |
||||||
|
(async () => { |
||||||
|
const fileManager = registry.get('filemanager').api |
||||||
|
const config = registry.get('config').api |
||||||
|
const accessToken = config.get('settings/gist-access-token') |
||||||
|
const files = await fetchDirectoryContent(name) |
||||||
|
const actions = [{ |
||||||
|
name: 'New File', |
||||||
|
type: ['folder'], |
||||||
|
path: [], |
||||||
|
extension: [], |
||||||
|
pattern: [] |
||||||
|
}, { |
||||||
|
name: 'New Folder', |
||||||
|
type: ['folder'], |
||||||
|
path: [], |
||||||
|
extension: [], |
||||||
|
pattern: [] |
||||||
|
}, { |
||||||
|
name: 'Rename', |
||||||
|
type: ['file', 'folder'], |
||||||
|
path: [], |
||||||
|
extension: [], |
||||||
|
pattern: [] |
||||||
|
}, { |
||||||
|
name: 'Delete', |
||||||
|
type: ['file', 'folder'], |
||||||
|
path: [], |
||||||
|
extension: [], |
||||||
|
pattern: [] |
||||||
|
}, { |
||||||
|
name: 'Push changes to gist', |
||||||
|
type: [], |
||||||
|
path: [], |
||||||
|
extension: [], |
||||||
|
pattern: ['^browser/gists/([0-9]|[a-z])*$'] |
||||||
|
}, { |
||||||
|
name: 'Run', |
||||||
|
type: [], |
||||||
|
path: [], |
||||||
|
extension: ['.js'], |
||||||
|
pattern: [] |
||||||
|
}] |
||||||
|
|
||||||
|
setState(prevState => { |
||||||
|
return { ...prevState, fileManager, accessToken, files, actions } |
||||||
|
}) |
||||||
|
})() |
||||||
|
}, []) |
||||||
|
|
||||||
|
useEffect(() => { |
||||||
|
if (state.fileManager) { |
||||||
|
filesProvider.event.register('fileExternallyChanged', fileExternallyChanged) |
||||||
|
filesProvider.event.register('fileRenamedError', fileRenamedError) |
||||||
|
} |
||||||
|
}, [state.fileManager]) |
||||||
|
|
||||||
|
useEffect(() => { |
||||||
|
const { expandPath } = state |
||||||
|
const expandFn = async () => { |
||||||
|
let files = state.files |
||||||
|
|
||||||
|
for (let i = 0; i < expandPath.length; i++) { |
||||||
|
files = await resolveDirectory(expandPath[i], files) |
||||||
|
await setState(prevState => { |
||||||
|
return { ...prevState, files } |
||||||
|
}) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if (expandPath && expandPath.length > 0) { |
||||||
|
expandFn() |
||||||
|
} |
||||||
|
}, [state.expandPath]) |
||||||
|
|
||||||
|
useEffect(() => { |
||||||
|
// unregister event to update state in callback
|
||||||
|
if (filesProvider.event.registered.fileAdded) filesProvider.event.unregister('fileAdded', fileAdded) |
||||||
|
if (filesProvider.event.registered.folderAdded) filesProvider.event.unregister('folderAdded', folderAdded) |
||||||
|
if (filesProvider.event.registered.fileRemoved) filesProvider.event.unregister('fileRemoved', fileRemoved) |
||||||
|
if (filesProvider.event.registered.fileRenamed) filesProvider.event.unregister('fileRenamed', fileRenamed) |
||||||
|
filesProvider.event.register('fileAdded', fileAdded) |
||||||
|
filesProvider.event.register('folderAdded', folderAdded) |
||||||
|
filesProvider.event.register('fileRemoved', fileRemoved) |
||||||
|
filesProvider.event.register('fileRenamed', fileRenamed) |
||||||
|
}, [state.files]) |
||||||
|
|
||||||
|
const resolveDirectory = async (folderPath, dir: File[], isChild = false): Promise<File[]> => { |
||||||
|
if (!isChild && (state.focusEdit.element === 'browser/blank') && state.focusEdit.isNew && (dir.findIndex(({ path }) => path === 'browser/blank') === -1)) { |
||||||
|
dir = state.focusEdit.type === 'file' ? [...dir, { |
||||||
|
path: state.focusEdit.element, |
||||||
|
name: '', |
||||||
|
isDirectory: false |
||||||
|
}] : [{ |
||||||
|
path: state.focusEdit.element, |
||||||
|
name: '', |
||||||
|
isDirectory: true |
||||||
|
}, ...dir] |
||||||
|
} |
||||||
|
dir = await Promise.all(dir.map(async (file) => { |
||||||
|
if (file.path === folderPath) { |
||||||
|
if ((extractParentFromKey(state.focusEdit.element) === folderPath) && state.focusEdit.isNew) { |
||||||
|
file.child = state.focusEdit.type === 'file' ? [...await fetchDirectoryContent(folderPath), { |
||||||
|
path: state.focusEdit.element, |
||||||
|
name: '', |
||||||
|
isDirectory: false |
||||||
|
}] : [{ |
||||||
|
path: state.focusEdit.element, |
||||||
|
name: '', |
||||||
|
isDirectory: true |
||||||
|
}, ...await fetchDirectoryContent(folderPath)] |
||||||
|
} else { |
||||||
|
file.child = await fetchDirectoryContent(folderPath) |
||||||
|
} |
||||||
|
return file |
||||||
|
} else if (file.child) { |
||||||
|
file.child = await resolveDirectory(folderPath, file.child, true) |
||||||
|
return file |
||||||
|
} else { |
||||||
|
return file |
||||||
|
} |
||||||
|
})) |
||||||
|
|
||||||
|
return dir |
||||||
|
} |
||||||
|
|
||||||
|
const fetchDirectoryContent = async (folderPath: string): Promise<File[]> => { |
||||||
|
return new Promise((resolve) => { |
||||||
|
filesProvider.resolveDirectory(folderPath, (error, fileTree) => { |
||||||
|
if (error) console.error(error) |
||||||
|
const files = normalize(folderPath, fileTree) |
||||||
|
|
||||||
|
resolve(files) |
||||||
|
}) |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
const normalize = (path, filesList): File[] => { |
||||||
|
const folders = [] |
||||||
|
const files = [] |
||||||
|
const prefix = path.split('/')[0] |
||||||
|
|
||||||
|
Object.keys(filesList || {}).forEach(key => { |
||||||
|
const path = prefix + '/' + key |
||||||
|
|
||||||
|
if (filesList[key].isDirectory) { |
||||||
|
folders.push({ |
||||||
|
path, |
||||||
|
name: extractNameFromKey(path), |
||||||
|
isDirectory: filesList[key].isDirectory |
||||||
|
}) |
||||||
|
} else { |
||||||
|
files.push({ |
||||||
|
path, |
||||||
|
name: extractNameFromKey(path), |
||||||
|
isDirectory: filesList[key].isDirectory |
||||||
|
}) |
||||||
|
} |
||||||
|
}) |
||||||
|
|
||||||
|
return [...folders, ...files] |
||||||
|
} |
||||||
|
|
||||||
|
const extractNameFromKey = (key: string):string => { |
||||||
|
const keyPath = key.split('/') |
||||||
|
|
||||||
|
return keyPath[keyPath.length - 1] |
||||||
|
} |
||||||
|
|
||||||
|
const extractParentFromKey = (key: string):string => { |
||||||
|
if (!key) return |
||||||
|
const keyPath = key.split('/') |
||||||
|
keyPath.pop() |
||||||
|
|
||||||
|
return keyPath.join('/') |
||||||
|
} |
||||||
|
|
||||||
|
const createNewFile = (newFilePath: string) => { |
||||||
|
const fileManager = state.fileManager |
||||||
|
|
||||||
|
helper.createNonClashingName(newFilePath, filesProvider, async (error, newName) => { |
||||||
|
if (error) { |
||||||
|
modal('Create File Failed', error, { |
||||||
|
label: 'Close', |
||||||
|
fn: async () => {} |
||||||
|
}, null) |
||||||
|
} else { |
||||||
|
const createFile = await fileManager.writeFile(newName, '') |
||||||
|
|
||||||
|
if (!createFile) { |
||||||
|
toast('Failed to create file ' + newName) |
||||||
|
} |
||||||
|
} |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
const createNewFolder = async (newFolderPath: string) => { |
||||||
|
const fileManager = state.fileManager |
||||||
|
const dirName = newFolderPath + '/' |
||||||
|
|
||||||
|
try { |
||||||
|
const exists = await fileManager.exists(dirName) |
||||||
|
|
||||||
|
if (exists) return |
||||||
|
await fileManager.mkdir(dirName) |
||||||
|
// addFolder(parentFolder, newFolderPath)
|
||||||
|
} catch (e) { |
||||||
|
console.log('error: ', e) |
||||||
|
toast('Failed to create folder: ' + newFolderPath) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
const deletePath = async (path: string) => { |
||||||
|
if (filesProvider.isReadOnly(path)) { |
||||||
|
return toast('cannot delete file. ' + name + ' is a read only explorer') |
||||||
|
} |
||||||
|
const isDir = state.fileManager.isDirectory(path) |
||||||
|
|
||||||
|
modal('Delete file', `Are you sure you want to delete ${path} ${isDir ? 'folder' : 'file'}?`, { |
||||||
|
label: 'Ok', |
||||||
|
fn: async () => { |
||||||
|
try { |
||||||
|
const fileManager = state.fileManager |
||||||
|
|
||||||
|
await fileManager.remove(path) |
||||||
|
} catch (e) { |
||||||
|
toast(`Failed to remove file ${path}.`) |
||||||
|
} |
||||||
|
} |
||||||
|
}, { |
||||||
|
label: 'Cancel', |
||||||
|
fn: () => {} |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
const renamePath = async (oldPath: string, newPath: string) => { |
||||||
|
try { |
||||||
|
const fileManager = state.fileManager |
||||||
|
const exists = await fileManager.exists(newPath) |
||||||
|
|
||||||
|
if (exists) { |
||||||
|
modal('Rename File Failed', 'File name already exists', { |
||||||
|
label: 'Close', |
||||||
|
fn: () => {} |
||||||
|
}, null) |
||||||
|
} else { |
||||||
|
await fileManager.rename(oldPath, newPath) |
||||||
|
} |
||||||
|
} catch (error) { |
||||||
|
modal('Rename File Failed', 'Unexpected error while renaming: ' + error, { |
||||||
|
label: 'Close', |
||||||
|
fn: async () => {} |
||||||
|
}, null) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
const removePath = (path: string, files: File[]): File[] => { |
||||||
|
return files.map(file => { |
||||||
|
if (file.path === path) { |
||||||
|
return null |
||||||
|
} else if (file.child) { |
||||||
|
const childFiles = removePath(path, file.child) |
||||||
|
|
||||||
|
file.child = childFiles.filter(file => file) |
||||||
|
return file |
||||||
|
} else { |
||||||
|
return file |
||||||
|
} |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
const fileAdded = async (filePath: string) => { |
||||||
|
const pathArr = filePath.split('/') |
||||||
|
const expandPath = pathArr.map((path, index) => { |
||||||
|
return [...pathArr.slice(0, index)].join('/') |
||||||
|
}).filter(path => path && (path !== props.name)) |
||||||
|
const files = await fetchDirectoryContent(props.name) |
||||||
|
|
||||||
|
setState(prevState => { |
||||||
|
const uniquePaths = [...new Set([...prevState.expandPath, ...expandPath])] |
||||||
|
|
||||||
|
return { ...prevState, files, expandPath: uniquePaths, focusElement: [{ key: filePath, type: 'file' }] } |
||||||
|
}) |
||||||
|
if (filePath.includes('_test.sol')) { |
||||||
|
plugin.event.trigger('newTestFileCreated', [filePath]) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
const folderAdded = async (folderPath: string) => { |
||||||
|
const pathArr = folderPath.split('/') |
||||||
|
const expandPath = pathArr.map((path, index) => { |
||||||
|
return [...pathArr.slice(0, index)].join('/') |
||||||
|
}).filter(path => path && (path !== props.name)) |
||||||
|
const files = await fetchDirectoryContent(props.name) |
||||||
|
|
||||||
|
setState(prevState => { |
||||||
|
const uniquePaths = [...new Set([...prevState.expandPath, ...expandPath])] |
||||||
|
|
||||||
|
return { ...prevState, files, expandPath: uniquePaths, focusElement: [{ key: folderPath, type: 'folder' }] } |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
const fileExternallyChanged = (path: string, file: { content: string }) => { |
||||||
|
const config = registry.get('config').api |
||||||
|
|
||||||
|
if (config.get('currentFile') === path && registry.editor.currentContent() && registry.editor.currentContent() !== file.content) { |
||||||
|
if (filesProvider.isReadOnly(path)) return registry.editor.setText(file.content) |
||||||
|
modal(path + ' changed', 'This file has been changed outside of Remix IDE.', { |
||||||
|
label: 'Replace by the new content', |
||||||
|
fn: () => { |
||||||
|
registry.editor.setText(file.content) |
||||||
|
} |
||||||
|
}, { |
||||||
|
label: 'Keep the content displayed in Remix', |
||||||
|
fn: () => {} |
||||||
|
}) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
const fileRemoved = (filePath) => { |
||||||
|
const files = removePath(filePath, state.files) |
||||||
|
const updatedFiles = files.filter(file => file) |
||||||
|
|
||||||
|
setState(prevState => { |
||||||
|
return { ...prevState, files: updatedFiles } |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
const fileRenamed = async () => { |
||||||
|
const files = await fetchDirectoryContent(props.name) |
||||||
|
|
||||||
|
setState(prevState => { |
||||||
|
return { ...prevState, files, expandPath: [...prevState.expandPath] } |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
// register to event of the file provider
|
||||||
|
// files.event.register('fileRenamed', fileRenamed)
|
||||||
|
const fileRenamedError = (error: string) => { |
||||||
|
modal('File Renamed Failed', error, { |
||||||
|
label: 'Close', |
||||||
|
fn: () => {} |
||||||
|
}, null) |
||||||
|
} |
||||||
|
|
||||||
|
const uploadFile = (target) => { |
||||||
|
// TODO The file explorer is merely a view on the current state of
|
||||||
|
// 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
|
||||||
|
// pick that up via the 'fileAdded' event from the files module.
|
||||||
|
|
||||||
|
[...target.files].forEach((file) => { |
||||||
|
const files = filesProvider |
||||||
|
|
||||||
|
const loadFile = (name: string): void => { |
||||||
|
const fileReader = new FileReader() |
||||||
|
|
||||||
|
fileReader.onload = async function (event) { |
||||||
|
if (helper.checkSpecialChars(file.name)) { |
||||||
|
modal('File Upload Failed', 'Special characters are not allowed', { |
||||||
|
label: 'Close', |
||||||
|
fn: async () => {} |
||||||
|
}, null) |
||||||
|
return |
||||||
|
} |
||||||
|
const success = await files.set(name, event.target.result) |
||||||
|
|
||||||
|
if (!success) { |
||||||
|
modal('File Upload Failed', 'Failed to create file ' + name, { |
||||||
|
label: 'Close', |
||||||
|
fn: async () => {} |
||||||
|
}, null) |
||||||
|
} |
||||||
|
} |
||||||
|
fileReader.readAsText(file) |
||||||
|
} |
||||||
|
const name = files.type + '/' + file.name |
||||||
|
|
||||||
|
files.exists(name, (error, exist) => { |
||||||
|
if (error) console.log(error) |
||||||
|
if (!exist) { |
||||||
|
loadFile(name) |
||||||
|
} else { |
||||||
|
modal('Confirm overwrite', `The file ${name} already exists! Would you like to overwrite it?`, { |
||||||
|
label: 'Ok', |
||||||
|
fn: () => { |
||||||
|
loadFile(name) |
||||||
|
} |
||||||
|
}, { |
||||||
|
label: 'Cancel', |
||||||
|
fn: () => {} |
||||||
|
}) |
||||||
|
} |
||||||
|
}) |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
const publishToGist = () => { |
||||||
|
modal('Create a public gist', 'Are you sure you want to publish all your files in browser directory anonymously as a public gist on github.com? Note: this will not include directories.', { |
||||||
|
label: 'Ok', |
||||||
|
fn: toGist |
||||||
|
}, { |
||||||
|
label: 'Cancel', |
||||||
|
fn: () => {} |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
const toGist = (id?: string) => { |
||||||
|
const proccedResult = function (error, data) { |
||||||
|
if (error) { |
||||||
|
modal('Publish to gist Failed', 'Failed to manage gist: ' + error, { |
||||||
|
label: 'Close', |
||||||
|
fn: async () => {} |
||||||
|
}, null) |
||||||
|
} else { |
||||||
|
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?`, { |
||||||
|
label: 'Ok', |
||||||
|
fn: () => { |
||||||
|
window.open(data.html_url, '_blank') |
||||||
|
} |
||||||
|
}, { |
||||||
|
label: 'Cancel', |
||||||
|
fn: () => {} |
||||||
|
}) |
||||||
|
} else { |
||||||
|
modal('Publish to gist Failed', data.message + ' ' + data.documentation_url + ' ' + JSON.stringify(data.errors, null, '\t'), { |
||||||
|
label: 'Close', |
||||||
|
fn: async () => {} |
||||||
|
}, null) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* This function is to get the original content of given gist |
||||||
|
* @params id is the gist id to fetch |
||||||
|
*/ |
||||||
|
const getOriginalFiles = async (id) => { |
||||||
|
if (!id) { |
||||||
|
return [] |
||||||
|
} |
||||||
|
|
||||||
|
const url = `https://api.github.com/gists/${id}` |
||||||
|
const res = await fetch(url) |
||||||
|
const data = await res.json() |
||||||
|
return data.files || [] |
||||||
|
} |
||||||
|
|
||||||
|
// 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 ? 'browser/gists/' + id : 'browser/' |
||||||
|
|
||||||
|
packageFiles(filesProvider, folder, async (error, packaged) => { |
||||||
|
if (error) { |
||||||
|
console.log(error) |
||||||
|
modal('Publish to gist Failed', 'Failed to create gist: ' + error.message, { |
||||||
|
label: 'Close', |
||||||
|
fn: async () => {} |
||||||
|
}, null) |
||||||
|
} else { |
||||||
|
// check for token
|
||||||
|
if (!state.accessToken) { |
||||||
|
modal('Authorize Token', 'Remix requires an access token (which includes gists creation permission). Please go to the settings tab to create one.', { |
||||||
|
label: 'Close', |
||||||
|
fn: async () => {} |
||||||
|
}, null) |
||||||
|
} 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=' + |
||||||
|
queryParams.get().version + '&optimize=' + queryParams.get().optimize + '&runs=' + queryParams.get().runs + '&gist=' |
||||||
|
const gists = new Gists({ token: state.accessToken }) |
||||||
|
|
||||||
|
if (id) { |
||||||
|
const originalFileList = await getOriginalFiles(id) |
||||||
|
// Telling the GIST API to remove files
|
||||||
|
const updatedFileList = Object.keys(packaged) |
||||||
|
const allItems = Object.keys(originalFileList) |
||||||
|
.filter(fileName => updatedFileList.indexOf(fileName) === -1) |
||||||
|
.reduce((acc, deleteFileName) => ({ |
||||||
|
...acc, |
||||||
|
[deleteFileName]: null |
||||||
|
}), originalFileList) |
||||||
|
// adding new files
|
||||||
|
updatedFileList.forEach((file) => { |
||||||
|
const _items = file.split('/') |
||||||
|
const _fileName = _items[_items.length - 1] |
||||||
|
allItems[_fileName] = packaged[file] |
||||||
|
}) |
||||||
|
|
||||||
|
toast('Saving gist (' + id + ') ...') |
||||||
|
gists.edit({ |
||||||
|
description: description, |
||||||
|
public: true, |
||||||
|
files: allItems, |
||||||
|
id: id |
||||||
|
}, (error, result) => { |
||||||
|
proccedResult(error, result) |
||||||
|
if (!error) { |
||||||
|
for (const key in allItems) { |
||||||
|
if (allItems[key] === null) delete allItems[key] |
||||||
|
} |
||||||
|
} |
||||||
|
}) |
||||||
|
} else { |
||||||
|
// id is not existing, need to create a new gist
|
||||||
|
toast('Creating a new gist ...') |
||||||
|
gists.create({ |
||||||
|
description: description, |
||||||
|
public: true, |
||||||
|
files: packaged |
||||||
|
}, (error, result) => { |
||||||
|
proccedResult(error, result) |
||||||
|
}) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
const runScript = async (path: string) => { |
||||||
|
filesProvider.get(path, (error, content: string) => { |
||||||
|
if (error) return console.log(error) |
||||||
|
plugin.call('scriptRunner', 'execute', content) |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
const handleHideModal = () => { |
||||||
|
setState(prevState => { |
||||||
|
return { ...prevState, modalOptions: { ...state.modalOptions, hide: true } } |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
const modal = (title: string, message: string, ok: { label: string, fn: () => void }, cancel: { label: string, fn: () => void }) => { |
||||||
|
setState(prevState => { |
||||||
|
return { |
||||||
|
...prevState, |
||||||
|
modalOptions: { |
||||||
|
...prevState.modalOptions, |
||||||
|
hide: false, |
||||||
|
message, |
||||||
|
title, |
||||||
|
ok, |
||||||
|
cancel, |
||||||
|
handleHide: handleHideModal |
||||||
|
} |
||||||
|
} |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
const toast = (message: string) => { |
||||||
|
setState(prevState => { |
||||||
|
return { ...prevState, toasterMsg: message } |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
const handleClickFile = (path: string) => { |
||||||
|
state.fileManager.open(path) |
||||||
|
setState(prevState => { |
||||||
|
return { ...prevState, focusElement: [{ key: path, type: 'file' }] } |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
const handleClickFolder = async (path: string) => { |
||||||
|
if (state.ctrlKey) { |
||||||
|
if (state.focusElement.findIndex(item => item.key === path) !== -1) { |
||||||
|
setState(prevState => { |
||||||
|
return { ...prevState, focusElement: [...prevState.focusElement.filter(item => item.key !== path)] } |
||||||
|
}) |
||||||
|
} else { |
||||||
|
setState(prevState => { |
||||||
|
return { ...prevState, focusElement: [...prevState.focusElement, { key: path, type: 'folder' }] } |
||||||
|
}) |
||||||
|
} |
||||||
|
} else { |
||||||
|
let expandPath = [] |
||||||
|
|
||||||
|
if (!state.expandPath.includes(path)) { |
||||||
|
expandPath = [...new Set([...state.expandPath, path])] |
||||||
|
} else { |
||||||
|
expandPath = [...new Set(state.expandPath.filter(key => key && (typeof key === 'string') && !key.startsWith(path)))] |
||||||
|
} |
||||||
|
|
||||||
|
setState(prevState => { |
||||||
|
return { ...prevState, focusElement: [{ key: path, type: 'folder' }], expandPath } |
||||||
|
}) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
const handleContextMenuFile = (pageX: number, pageY: number, path: string, content: string) => { |
||||||
|
if (!content) return |
||||||
|
setState(prevState => { |
||||||
|
return { ...prevState, focusContext: { element: path, x: pageX, y: pageY }, focusEdit: { ...prevState.focusEdit, lastEdit: content } } |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
const handleContextMenuFolder = (pageX: number, pageY: number, path: string, content: string) => { |
||||||
|
if (!content) return |
||||||
|
setState(prevState => { |
||||||
|
return { ...prevState, focusContext: { element: path, x: pageX, y: pageY }, focusEdit: { ...prevState.focusEdit, lastEdit: content } } |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
const hideContextMenu = () => { |
||||||
|
setState(prevState => { |
||||||
|
return { ...prevState, focusContext: { element: null, x: 0, y: 0 } } |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
const editModeOn = (path: string, type: string, isNew: boolean = false) => { |
||||||
|
if (filesProvider.isReadOnly(path)) return |
||||||
|
setState(prevState => { |
||||||
|
return { ...prevState, focusEdit: { ...prevState.focusEdit, element: path, isNew, type } } |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
const editModeOff = async (content: string) => { |
||||||
|
const parentFolder = extractParentFromKey(state.focusEdit.element) |
||||||
|
|
||||||
|
if (!content || (content.trim() === '')) { |
||||||
|
if (state.focusEdit.isNew) { |
||||||
|
const files = removePath(state.focusEdit.element, state.files) |
||||||
|
const updatedFiles = files.filter(file => file) |
||||||
|
|
||||||
|
setState(prevState => { |
||||||
|
return { ...prevState, files: updatedFiles, focusEdit: { element: null, isNew: false, type: '', lastEdit: '' } } |
||||||
|
}) |
||||||
|
} else { |
||||||
|
editRef.current.textContent = state.focusEdit.lastEdit |
||||||
|
setState(prevState => { |
||||||
|
return { ...prevState, focusEdit: { element: null, isNew: false, type: '', lastEdit: '' } } |
||||||
|
}) |
||||||
|
} |
||||||
|
} else { |
||||||
|
if (state.focusEdit.lastEdit === content) { |
||||||
|
return setState(prevState => { |
||||||
|
return { ...prevState, focusEdit: { element: null, isNew: false, type: '', lastEdit: '' } } |
||||||
|
}) |
||||||
|
} |
||||||
|
if (helper.checkSpecialChars(content)) { |
||||||
|
modal('Validation Error', 'Special characters are not allowed', { |
||||||
|
label: 'Ok', |
||||||
|
fn: () => {} |
||||||
|
}, null) |
||||||
|
} else { |
||||||
|
if (state.focusEdit.isNew) { |
||||||
|
state.focusEdit.type === 'file' ? createNewFile(parentFolder + '/' + content) : createNewFolder(parentFolder + '/' + content) |
||||||
|
const files = removePath(state.focusEdit.element, state.files) |
||||||
|
const updatedFiles = files.filter(file => file) |
||||||
|
|
||||||
|
setState(prevState => { |
||||||
|
return { ...prevState, files: updatedFiles } |
||||||
|
}) |
||||||
|
} else { |
||||||
|
const oldPath: string = state.focusEdit.element |
||||||
|
const oldName = extractNameFromKey(oldPath) |
||||||
|
const newPath = oldPath.replace(oldName, content) |
||||||
|
|
||||||
|
editRef.current.textContent = extractNameFromKey(oldPath) |
||||||
|
renamePath(oldPath, newPath) |
||||||
|
} |
||||||
|
setState(prevState => { |
||||||
|
return { ...prevState, focusEdit: { element: null, isNew: false, type: '', lastEdit: '' } } |
||||||
|
}) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
const handleNewFileInput = async (parentFolder?: string) => { |
||||||
|
if (!parentFolder) parentFolder = state.focusElement[0] ? state.focusElement[0].type === 'folder' ? state.focusElement[0].key : extractParentFromKey(state.focusElement[0].key) : name |
||||||
|
const expandPath = [...new Set([...state.expandPath, parentFolder])] |
||||||
|
|
||||||
|
setState(prevState => { |
||||||
|
return { ...prevState, expandPath } |
||||||
|
}) |
||||||
|
editModeOn(parentFolder + '/blank', 'file', true) |
||||||
|
} |
||||||
|
|
||||||
|
const handleNewFolderInput = async (parentFolder?: string) => { |
||||||
|
if (!parentFolder) parentFolder = state.focusElement[0] ? state.focusElement[0].type === 'folder' ? state.focusElement[0].key : extractParentFromKey(state.focusElement[0].key) : name |
||||||
|
else if ((parentFolder.indexOf('.sol') !== -1) || (parentFolder.indexOf('.js') !== -1)) parentFolder = extractParentFromKey(parentFolder) |
||||||
|
const expandPath = [...new Set([...state.expandPath, parentFolder])] |
||||||
|
|
||||||
|
setState(prevState => { |
||||||
|
return { ...prevState, expandPath } |
||||||
|
}) |
||||||
|
editModeOn(parentFolder + '/blank', 'folder', true) |
||||||
|
} |
||||||
|
|
||||||
|
const handleEditInput = (event) => { |
||||||
|
if (event.which === 13) { |
||||||
|
event.preventDefault() |
||||||
|
editModeOff(editRef.current.innerText) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
const label = (file: File) => { |
||||||
|
return ( |
||||||
|
<div |
||||||
|
className='remixui_items d-inline-block w-100' |
||||||
|
ref={state.focusEdit.element === file.path ? editRef : null} |
||||||
|
suppressContentEditableWarning={true} |
||||||
|
contentEditable={state.focusEdit.element === file.path} |
||||||
|
onKeyDown={handleEditInput} |
||||||
|
onBlur={(e) => { |
||||||
|
e.stopPropagation() |
||||||
|
editModeOff(editRef.current.innerText) |
||||||
|
}} |
||||||
|
> |
||||||
|
<span |
||||||
|
title={file.path} |
||||||
|
className={'remixui_label ' + (file.isDirectory ? 'folder' : 'remixui_leaf')} |
||||||
|
data-path={file.path} |
||||||
|
> |
||||||
|
{ file.name } |
||||||
|
</span> |
||||||
|
</div> |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
const renderFiles = (file: File, index: number) => { |
||||||
|
if (file.isDirectory) { |
||||||
|
return ( |
||||||
|
<div key={index}> |
||||||
|
<TreeViewItem |
||||||
|
id={`treeViewItem${file.path}`} |
||||||
|
iconX='pr-3 fa fa-folder' |
||||||
|
iconY='pr-3 fa fa-folder-open' |
||||||
|
key={`${file.path + index}`} |
||||||
|
label={label(file)} |
||||||
|
onClick={(e) => { |
||||||
|
e.stopPropagation() |
||||||
|
if (state.focusEdit.element !== file.path) handleClickFolder(file.path) |
||||||
|
}} |
||||||
|
onContextMenu={(e) => { |
||||||
|
e.preventDefault() |
||||||
|
e.stopPropagation() |
||||||
|
handleContextMenuFolder(e.pageX, e.pageY, file.path, e.target.textContent) |
||||||
|
}} |
||||||
|
labelClass={ state.focusEdit.element === file.path ? 'bg-light' : state.focusElement.findIndex(item => item.key === file.path) !== -1 ? 'bg-secondary' : '' } |
||||||
|
controlBehaviour={ state.ctrlKey } |
||||||
|
expand={state.expandPath.includes(file.path)} |
||||||
|
> |
||||||
|
{ |
||||||
|
file.child ? <TreeView id={`treeView${file.path}`} key={index}>{ |
||||||
|
file.child.map((file, index) => { |
||||||
|
return renderFiles(file, index) |
||||||
|
}) |
||||||
|
} |
||||||
|
</TreeView> : <TreeView id={`treeView${file.path}`} key={index} /> |
||||||
|
} |
||||||
|
</TreeViewItem> |
||||||
|
{ ((state.focusContext.element === file.path) && (state.focusEdit.element !== file.path)) && |
||||||
|
<FileExplorerContextMenu |
||||||
|
actions={state.actions} |
||||||
|
hideContextMenu={hideContextMenu} |
||||||
|
createNewFile={handleNewFileInput} |
||||||
|
createNewFolder={handleNewFolderInput} |
||||||
|
deletePath={deletePath} |
||||||
|
renamePath={editModeOn} |
||||||
|
extractParentFromKey={extractParentFromKey} |
||||||
|
publishToGist={publishToGist} |
||||||
|
pageX={state.focusContext.x} |
||||||
|
pageY={state.focusContext.y} |
||||||
|
path={file.path} |
||||||
|
type='folder' |
||||||
|
/> |
||||||
|
} |
||||||
|
</div> |
||||||
|
) |
||||||
|
} else { |
||||||
|
return ( |
||||||
|
<div key={index}> |
||||||
|
<TreeViewItem |
||||||
|
id={`treeViewItem${file.path}`} |
||||||
|
key={index} |
||||||
|
label={label(file)} |
||||||
|
onClick={(e) => { |
||||||
|
e.stopPropagation() |
||||||
|
if (state.focusEdit.element !== file.path) handleClickFile(file.path) |
||||||
|
}} |
||||||
|
onContextMenu={(e) => { |
||||||
|
e.preventDefault() |
||||||
|
e.stopPropagation() |
||||||
|
handleContextMenuFile(e.pageX, e.pageY, file.path, e.target.textContent) |
||||||
|
}} |
||||||
|
icon='far fa-file' |
||||||
|
labelClass={ state.focusEdit.element === file.path ? 'bg-light' : state.focusElement.findIndex(item => item.key === file.path) !== -1 ? 'bg-secondary' : '' } |
||||||
|
/> |
||||||
|
{ ((state.focusContext.element === file.path) && (state.focusEdit.element !== file.path)) && |
||||||
|
<FileExplorerContextMenu |
||||||
|
actions={state.actions} |
||||||
|
hideContextMenu={hideContextMenu} |
||||||
|
createNewFile={handleNewFileInput} |
||||||
|
createNewFolder={handleNewFolderInput} |
||||||
|
deletePath={deletePath} |
||||||
|
renamePath={editModeOn} |
||||||
|
runScript={runScript} |
||||||
|
pageX={state.focusContext.x} |
||||||
|
pageY={state.focusContext.y} |
||||||
|
path={file.path} |
||||||
|
type='file' |
||||||
|
/> |
||||||
|
} |
||||||
|
</div> |
||||||
|
) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return ( |
||||||
|
<div> |
||||||
|
<TreeView id='treeView'> |
||||||
|
<TreeViewItem id="treeViewItem" |
||||||
|
label={ |
||||||
|
<FileExplorerMenu |
||||||
|
title={name} |
||||||
|
menuItems={props.menuItems} |
||||||
|
createNewFile={handleNewFileInput} |
||||||
|
createNewFolder={handleNewFolderInput} |
||||||
|
publishToGist={publishToGist} |
||||||
|
uploadFile={uploadFile} |
||||||
|
fileManager={state.fileManager} |
||||||
|
/> |
||||||
|
} |
||||||
|
expand={true}> |
||||||
|
<div className='pb-2'> |
||||||
|
<TreeView id='treeViewMenu'> |
||||||
|
{ |
||||||
|
state.files.map((file, index) => { |
||||||
|
return renderFiles(file, index) |
||||||
|
}) |
||||||
|
} |
||||||
|
</TreeView> |
||||||
|
</div> |
||||||
|
</TreeViewItem> |
||||||
|
</TreeView> |
||||||
|
{ |
||||||
|
props.name && <ModalDialog |
||||||
|
id={ props.name } |
||||||
|
title={ state.modalOptions.title } |
||||||
|
message={ state.modalOptions.message } |
||||||
|
hide={ state.modalOptions.hide } |
||||||
|
ok={ state.modalOptions.ok } |
||||||
|
cancel={ state.modalOptions.cancel } |
||||||
|
handleHide={ handleHideModal } |
||||||
|
/> |
||||||
|
} |
||||||
|
<Toaster message={state.toasterMsg} /> |
||||||
|
</div> |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
export default FileExplorer |
||||||
|
|
||||||
|
function packageFiles (filesProvider, directory, callback) { |
||||||
|
const ret = {} |
||||||
|
filesProvider.resolveDirectory(directory, (error, files) => { |
||||||
|
if (error) callback(error) |
||||||
|
else { |
||||||
|
async.eachSeries(Object.keys(files), (path, cb) => { |
||||||
|
if (filesProvider.isDirectory(path)) { |
||||||
|
cb() |
||||||
|
} else { |
||||||
|
filesProvider.get(path, (error, content) => { |
||||||
|
if (error) return cb(error) |
||||||
|
if (/^\s+$/.test(content) || !content.length) { |
||||||
|
content = '// this line is added to create a gist. Empty file is not allowed.' |
||||||
|
} |
||||||
|
ret[path] = { content } |
||||||
|
cb() |
||||||
|
}) |
||||||
|
} |
||||||
|
}, (error) => { |
||||||
|
callback(error, ret) |
||||||
|
}) |
||||||
|
} |
||||||
|
}) |
||||||
|
} |
@ -0,0 +1,41 @@ |
|||||||
|
/* eslint-disable-next-line */ |
||||||
|
export interface FileExplorerProps { |
||||||
|
name: string, |
||||||
|
registry: any, |
||||||
|
filesProvider: any, |
||||||
|
menuItems?: string[], |
||||||
|
plugin: any |
||||||
|
} |
||||||
|
|
||||||
|
export interface File { |
||||||
|
path: string, |
||||||
|
name: string, |
||||||
|
isDirectory: boolean, |
||||||
|
child?: File[] |
||||||
|
} |
||||||
|
|
||||||
|
export interface FileExplorerMenuProps { |
||||||
|
title: string, |
||||||
|
menuItems: string[], |
||||||
|
fileManager: any, |
||||||
|
createNewFile: (folder?: string) => void, |
||||||
|
createNewFolder: (parentFolder?: string) => void, |
||||||
|
publishToGist: () => void, |
||||||
|
uploadFile: (target: EventTarget & HTMLInputElement) => void |
||||||
|
} |
||||||
|
|
||||||
|
export interface FileExplorerContextMenuProps { |
||||||
|
actions: { name: string, type: string[], path: string[], extension: string[], pattern: string[] }[], |
||||||
|
createNewFile: (folder?: string) => void, |
||||||
|
createNewFolder: (parentFolder?: string) => void, |
||||||
|
deletePath: (path: string) => void, |
||||||
|
renamePath: (path: string, type: string) => void, |
||||||
|
hideContextMenu: () => void, |
||||||
|
extractParentFromKey?: (key: string) => string, |
||||||
|
publishToGist?: () => void, |
||||||
|
runScript?: (path: string) => void, |
||||||
|
pageX: number, |
||||||
|
pageY: number, |
||||||
|
path: string, |
||||||
|
type: string |
||||||
|
} |
@ -0,0 +1,16 @@ |
|||||||
|
{ |
||||||
|
"extends": "../../../tsconfig.json", |
||||||
|
"compilerOptions": { |
||||||
|
"jsx": "react", |
||||||
|
"allowJs": true, |
||||||
|
"esModuleInterop": true, |
||||||
|
"allowSyntheticDefaultImports": true |
||||||
|
}, |
||||||
|
"files": [], |
||||||
|
"include": [], |
||||||
|
"references": [ |
||||||
|
{ |
||||||
|
"path": "./tsconfig.lib.json" |
||||||
|
} |
||||||
|
] |
||||||
|
} |
@ -0,0 +1,13 @@ |
|||||||
|
{ |
||||||
|
"extends": "./tsconfig.json", |
||||||
|
"compilerOptions": { |
||||||
|
"outDir": "../../../dist/out-tsc", |
||||||
|
"types": ["node"] |
||||||
|
}, |
||||||
|
"files": [ |
||||||
|
"../../../node_modules/@nrwl/react/typings/cssmodule.d.ts", |
||||||
|
"../../../node_modules/@nrwl/react/typings/image.d.ts" |
||||||
|
], |
||||||
|
"exclude": ["**/*.spec.ts", "**/*.spec.tsx"], |
||||||
|
"include": ["**/*.js", "**/*.jsx", "**/*.ts", "**/*.tsx"] |
||||||
|
} |
@ -1,9 +1,12 @@ |
|||||||
export interface ModalDialogProps { |
export interface ModalDialogProps { |
||||||
|
id?: string |
||||||
title?: string, |
title?: string, |
||||||
content?: JSX.Element, |
message?: string, |
||||||
ok?: {label:string, fn: () => void}, |
ok?: { label: string, fn: () => void }, |
||||||
cancel?: {label:string, fn: () => void}, |
cancel: { label: string, fn: () => void }, |
||||||
focusSelector?: string, |
modalClass?: string, |
||||||
opts?: {class: string, hideClose?: boolean}, |
showCancelIcon?: boolean, |
||||||
hide: () => void |
hide: boolean, |
||||||
|
handleHide: (hideState?: boolean) => void, |
||||||
|
children?: React.ReactNode |
||||||
} |
} |
||||||
|
@ -1,248 +1,19 @@ |
|||||||
{ |
{ |
||||||
"rules": { |
|
||||||
"array-callback-return": "warn", |
|
||||||
"dot-location": ["warn", "property"], |
|
||||||
"eqeqeq": ["warn", "smart"], |
|
||||||
"new-parens": "warn", |
|
||||||
"no-caller": "warn", |
|
||||||
"no-cond-assign": ["warn", "except-parens"], |
|
||||||
"no-const-assign": "warn", |
|
||||||
"no-control-regex": "warn", |
|
||||||
"no-delete-var": "warn", |
|
||||||
"no-dupe-args": "warn", |
|
||||||
"no-dupe-keys": "warn", |
|
||||||
"no-duplicate-case": "warn", |
|
||||||
"no-empty-character-class": "warn", |
|
||||||
"no-empty-pattern": "warn", |
|
||||||
"no-eval": "warn", |
|
||||||
"no-ex-assign": "warn", |
|
||||||
"no-extend-native": "warn", |
|
||||||
"no-extra-bind": "warn", |
|
||||||
"no-extra-label": "warn", |
|
||||||
"no-fallthrough": "warn", |
|
||||||
"no-func-assign": "warn", |
|
||||||
"no-implied-eval": "warn", |
|
||||||
"no-invalid-regexp": "warn", |
|
||||||
"no-iterator": "warn", |
|
||||||
"no-label-var": "warn", |
|
||||||
"no-labels": ["warn", { "allowLoop": true, "allowSwitch": false }], |
|
||||||
"no-lone-blocks": "warn", |
|
||||||
"no-loop-func": "warn", |
|
||||||
"no-mixed-operators": [ |
|
||||||
"warn", |
|
||||||
{ |
|
||||||
"groups": [ |
|
||||||
["&", "|", "^", "~", "<<", ">>", ">>>"], |
|
||||||
["==", "!=", "===", "!==", ">", ">=", "<", "<="], |
|
||||||
["&&", "||"], |
|
||||||
["in", "instanceof"] |
|
||||||
], |
|
||||||
"allowSamePrecedence": false |
|
||||||
} |
|
||||||
], |
|
||||||
"no-multi-str": "warn", |
|
||||||
"no-native-reassign": "warn", |
|
||||||
"no-negated-in-lhs": "warn", |
|
||||||
"no-new-func": "warn", |
|
||||||
"no-new-object": "warn", |
|
||||||
"no-new-symbol": "warn", |
|
||||||
"no-new-wrappers": "warn", |
|
||||||
"no-obj-calls": "warn", |
|
||||||
"no-octal": "warn", |
|
||||||
"no-octal-escape": "warn", |
|
||||||
"no-redeclare": "warn", |
|
||||||
"no-regex-spaces": "warn", |
|
||||||
"no-restricted-syntax": ["warn", "WithStatement"], |
|
||||||
"no-script-url": "warn", |
|
||||||
"no-self-assign": "warn", |
|
||||||
"no-self-compare": "warn", |
|
||||||
"no-sequences": "warn", |
|
||||||
"no-shadow-restricted-names": "warn", |
|
||||||
"no-sparse-arrays": "warn", |
|
||||||
"no-template-curly-in-string": "warn", |
|
||||||
"no-this-before-super": "warn", |
|
||||||
"no-throw-literal": "warn", |
|
||||||
"no-restricted-globals": [ |
|
||||||
"error", |
|
||||||
"addEventListener", |
|
||||||
"blur", |
|
||||||
"close", |
|
||||||
"closed", |
|
||||||
"confirm", |
|
||||||
"defaultStatus", |
|
||||||
"defaultstatus", |
|
||||||
"event", |
|
||||||
"external", |
|
||||||
"find", |
|
||||||
"focus", |
|
||||||
"frameElement", |
|
||||||
"frames", |
|
||||||
"history", |
|
||||||
"innerHeight", |
|
||||||
"innerWidth", |
|
||||||
"length", |
|
||||||
"location", |
|
||||||
"locationbar", |
|
||||||
"menubar", |
|
||||||
"moveBy", |
|
||||||
"moveTo", |
|
||||||
"name", |
|
||||||
"onblur", |
|
||||||
"onerror", |
|
||||||
"onfocus", |
|
||||||
"onload", |
|
||||||
"onresize", |
|
||||||
"onunload", |
|
||||||
"open", |
|
||||||
"opener", |
|
||||||
"opera", |
|
||||||
"outerHeight", |
|
||||||
"outerWidth", |
|
||||||
"pageXOffset", |
|
||||||
"pageYOffset", |
|
||||||
"parent", |
|
||||||
"print", |
|
||||||
"removeEventListener", |
|
||||||
"resizeBy", |
|
||||||
"resizeTo", |
|
||||||
"screen", |
|
||||||
"screenLeft", |
|
||||||
"screenTop", |
|
||||||
"screenX", |
|
||||||
"screenY", |
|
||||||
"scroll", |
|
||||||
"scrollbars", |
|
||||||
"scrollBy", |
|
||||||
"scrollTo", |
|
||||||
"scrollX", |
|
||||||
"scrollY", |
|
||||||
"self", |
|
||||||
"status", |
|
||||||
"statusbar", |
|
||||||
"stop", |
|
||||||
"toolbar", |
|
||||||
"top" |
|
||||||
], |
|
||||||
"no-unexpected-multiline": "warn", |
|
||||||
"no-unreachable": "warn", |
|
||||||
"no-unused-expressions": [ |
|
||||||
"error", |
|
||||||
{ |
|
||||||
"allowShortCircuit": true, |
|
||||||
"allowTernary": true, |
|
||||||
"allowTaggedTemplates": true |
|
||||||
} |
|
||||||
], |
|
||||||
"no-unused-labels": "warn", |
|
||||||
"no-useless-computed-key": "warn", |
|
||||||
"no-useless-concat": "warn", |
|
||||||
"no-useless-escape": "warn", |
|
||||||
"no-useless-rename": [ |
|
||||||
"warn", |
|
||||||
{ |
|
||||||
"ignoreDestructuring": false, |
|
||||||
"ignoreImport": false, |
|
||||||
"ignoreExport": false |
|
||||||
} |
|
||||||
], |
|
||||||
"no-with": "warn", |
|
||||||
"no-whitespace-before-property": "warn", |
|
||||||
"react-hooks/exhaustive-deps": "warn", |
|
||||||
"require-yield": "warn", |
|
||||||
"rest-spread-spacing": ["warn", "never"], |
|
||||||
"strict": ["warn", "never"], |
|
||||||
"unicode-bom": ["warn", "never"], |
|
||||||
"use-isnan": "warn", |
|
||||||
"valid-typeof": "warn", |
|
||||||
"no-restricted-properties": [ |
|
||||||
"error", |
|
||||||
{ |
|
||||||
"object": "require", |
|
||||||
"property": "ensure", |
|
||||||
"message": "Please use import() instead. More info: https://facebook.github.io/create-react-app/docs/code-splitting" |
|
||||||
}, |
|
||||||
{ |
|
||||||
"object": "System", |
|
||||||
"property": "import", |
|
||||||
"message": "Please use import() instead. More info: https://facebook.github.io/create-react-app/docs/code-splitting" |
|
||||||
} |
|
||||||
], |
|
||||||
"getter-return": "warn", |
|
||||||
"import/first": "error", |
|
||||||
"import/no-amd": "error", |
|
||||||
"import/no-webpack-loader-syntax": "error", |
|
||||||
"react/forbid-foreign-prop-types": ["warn", { "allowInPropTypes": true }], |
|
||||||
"react/jsx-no-comment-textnodes": "warn", |
|
||||||
"react/jsx-no-duplicate-props": "warn", |
|
||||||
"react/jsx-no-target-blank": "warn", |
|
||||||
"react/jsx-no-undef": "error", |
|
||||||
"react/jsx-pascal-case": ["warn", { "allowAllCaps": true, "ignore": [] }], |
|
||||||
"react/jsx-uses-react": "warn", |
|
||||||
"react/jsx-uses-vars": "warn", |
|
||||||
"react/no-danger-with-children": "warn", |
|
||||||
"react/no-direct-mutation-state": "warn", |
|
||||||
"react/no-is-mounted": "warn", |
|
||||||
"react/no-typos": "error", |
|
||||||
"react/react-in-jsx-scope": "error", |
|
||||||
"react/require-render-return": "error", |
|
||||||
"react/style-prop-object": "warn", |
|
||||||
"react/jsx-no-useless-fragment": "warn", |
|
||||||
"jsx-a11y/accessible-emoji": "warn", |
|
||||||
"jsx-a11y/alt-text": "warn", |
|
||||||
"jsx-a11y/anchor-has-content": "warn", |
|
||||||
"jsx-a11y/anchor-is-valid": [ |
|
||||||
"warn", |
|
||||||
{ "aspects": ["noHref", "invalidHref"] } |
|
||||||
], |
|
||||||
"jsx-a11y/aria-activedescendant-has-tabindex": "warn", |
|
||||||
"jsx-a11y/aria-props": "warn", |
|
||||||
"jsx-a11y/aria-proptypes": "warn", |
|
||||||
"jsx-a11y/aria-role": "warn", |
|
||||||
"jsx-a11y/aria-unsupported-elements": "warn", |
|
||||||
"jsx-a11y/heading-has-content": "warn", |
|
||||||
"jsx-a11y/iframe-has-title": "warn", |
|
||||||
"jsx-a11y/img-redundant-alt": "warn", |
|
||||||
"jsx-a11y/no-access-key": "warn", |
|
||||||
"jsx-a11y/no-distracting-elements": "warn", |
|
||||||
"jsx-a11y/no-redundant-roles": "warn", |
|
||||||
"jsx-a11y/role-has-required-aria-props": "warn", |
|
||||||
"jsx-a11y/role-supports-aria-props": "warn", |
|
||||||
"jsx-a11y/scope": "warn", |
|
||||||
"react-hooks/rules-of-hooks": "error", |
|
||||||
"default-case": "off", |
|
||||||
"no-dupe-class-members": "off", |
|
||||||
"no-undef": "off", |
|
||||||
"@typescript-eslint/consistent-type-assertions": "warn", |
|
||||||
"no-array-constructor": "off", |
|
||||||
"@typescript-eslint/no-array-constructor": "warn", |
|
||||||
"@typescript-eslint/no-namespace": "error", |
|
||||||
"no-use-before-define": "off", |
|
||||||
"@typescript-eslint/no-use-before-define": [ |
|
||||||
"warn", |
|
||||||
{ |
|
||||||
"functions": false, |
|
||||||
"classes": false, |
|
||||||
"variables": false, |
|
||||||
"typedefs": false |
|
||||||
} |
|
||||||
], |
|
||||||
"no-unused-vars": "off", |
|
||||||
"@typescript-eslint/no-unused-vars": [ |
|
||||||
"warn", |
|
||||||
{ "args": "none", "ignoreRestSiblings": true } |
|
||||||
], |
|
||||||
"no-useless-constructor": "off", |
|
||||||
"@typescript-eslint/no-useless-constructor": "warn" |
|
||||||
}, |
|
||||||
"env": { |
"env": { |
||||||
"browser": true, |
"browser": true, |
||||||
"commonjs": true, |
"es6": true |
||||||
"es6": true, |
}, |
||||||
"jest": true, |
"extends": "../../../.eslintrc", |
||||||
"node": true |
"globals": { |
||||||
|
"Atomics": "readonly", |
||||||
|
"SharedArrayBuffer": "readonly" |
||||||
}, |
}, |
||||||
"settings": { "react": { "version": "detect" } }, |
"parserOptions": { |
||||||
"plugins": ["import", "jsx-a11y", "react", "react-hooks"], |
"ecmaVersion": 11, |
||||||
"extends": ["../../../.eslintrc"], |
"sourceType": "module" |
||||||
"ignorePatterns": ["!**/*"] |
}, |
||||||
} |
"rules": { |
||||||
|
"no-unused-vars": "off", |
||||||
|
"@typescript-eslint/no-unused-vars": "error" |
||||||
|
} |
||||||
|
} |
||||||
|
@ -1,2 +1,2 @@ |
|||||||
export * from './lib/tree-view-item/tree-view-item'; |
export * from './lib/tree-view-item/tree-view-item' |
||||||
export * from './lib/remix-ui-tree-view'; |
export * from './lib/remix-ui-tree-view' |
||||||
|
@ -1,13 +1,22 @@ |
|||||||
export interface TreeViewProps { |
export interface TreeViewProps { |
||||||
children?: React.ReactNode, |
children?: React.ReactNode, |
||||||
id: string |
id?: string |
||||||
} |
} |
||||||
|
|
||||||
export interface TreeViewItemProps { |
export interface TreeViewItemProps { |
||||||
children?: React.ReactNode, |
children?: React.ReactNode, |
||||||
id: string, |
id?: string, |
||||||
label: string | number | React.ReactNode, |
label: string | number | React.ReactNode, |
||||||
expand?: boolean, |
expand?: boolean, |
||||||
onClick?: VoidFunction, |
onClick?: (...args: any) => void, |
||||||
className?: string |
onInput?: (...args: any) => void, |
||||||
} |
className?: string, |
||||||
|
iconX?: string, |
||||||
|
iconY?: string, |
||||||
|
icon?: string, |
||||||
|
labelClass?: string, |
||||||
|
controlBehaviour?: boolean |
||||||
|
innerRef?: any, |
||||||
|
onContextMenu?: (...args: any) => void, |
||||||
|
onBlur?: (...args: any) => void |
||||||
|
} |
||||||
|
Loading…
Reference in new issue