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 { |
||||
id?: string |
||||
title?: string, |
||||
content?: JSX.Element, |
||||
ok?: {label:string, fn: () => void}, |
||||
cancel?: {label:string, fn: () => void}, |
||||
focusSelector?: string, |
||||
opts?: {class: string, hideClose?: boolean}, |
||||
hide: () => void |
||||
message?: string, |
||||
ok?: { label: string, fn: () => void }, |
||||
cancel: { label: string, fn: () => void }, |
||||
modalClass?: string, |
||||
showCancelIcon?: boolean, |
||||
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": { |
||||
"browser": true, |
||||
"commonjs": true, |
||||
"es6": true, |
||||
"jest": true, |
||||
"node": true |
||||
"browser": true, |
||||
"es6": true |
||||
}, |
||||
"extends": "../../../.eslintrc", |
||||
"globals": { |
||||
"Atomics": "readonly", |
||||
"SharedArrayBuffer": "readonly" |
||||
}, |
||||
"settings": { "react": { "version": "detect" } }, |
||||
"plugins": ["import", "jsx-a11y", "react", "react-hooks"], |
||||
"extends": ["../../../.eslintrc"], |
||||
"ignorePatterns": ["!**/*"] |
||||
"parserOptions": { |
||||
"ecmaVersion": 11, |
||||
"sourceType": "module" |
||||
}, |
||||
"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/remix-ui-tree-view'; |
||||
export * from './lib/tree-view-item/tree-view-item' |
||||
export * from './lib/remix-ui-tree-view' |
||||
|
@ -1,13 +1,22 @@ |
||||
export interface TreeViewProps { |
||||
children?: React.ReactNode, |
||||
id: string |
||||
id?: string |
||||
} |
||||
|
||||
export interface TreeViewItemProps { |
||||
children?: React.ReactNode, |
||||
id: string, |
||||
id?: string, |
||||
label: string | number | React.ReactNode, |
||||
expand?: boolean, |
||||
onClick?: VoidFunction, |
||||
className?: string |
||||
onClick?: (...args: any) => void, |
||||
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