diff --git a/libs/remix-ui/file-explorer/src/lib/file-explorer-menu.tsx b/libs/remix-ui/file-explorer/src/lib/file-explorer-menu.tsx
new file mode 100644
index 0000000000..1bf52d37f4
--- /dev/null
+++ b/libs/remix-ui/file-explorer/src/lib/file-explorer-menu.tsx
@@ -0,0 +1,290 @@
+import React, { useState, useEffect } from 'react' //eslint-disable-line
+import { FileExplorerMenuProps } from './types'
+import * as helper from '../../../../../apps/remix-ide/src/lib/helper'
+import * as async from 'async'
+import Gists from 'gists'
+import QueryParams from '../../../../../apps/remix-ide/src/lib/query-params'
+
+const queryParams = new QueryParams()
+
+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)
+ })
+ }
+ })
+}
+
+export const FileExplorerMenu = (props: FileExplorerMenuProps) => {
+ const [state, setState] = useState({
+ menuItems: [
+ {
+ action: 'createNewFile',
+ title: 'Create New File',
+ icon: 'fas fa-plus-circle'
+ },
+ {
+ action: 'publishToGist',
+ title: 'Publish all [browser] explorer files to a github gist',
+ icon: 'fab fa-github'
+ },
+ {
+ action: 'uploadFile',
+ title: 'Add Local file to the Browser Storage Explorer',
+ icon: 'far fa-folder-open'
+ },
+ {
+ 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: () => {},
+ uploadFile
+ }
+
+ setState(prevState => {
+ return { ...prevState, actions }
+ })
+ }, [])
+
+ const createNewFile = (parentFolder = 'browser/Folder 2') => {
+ // const self = this
+ // modalDialogCustom.prompt('Create new file', 'File Name (e.g Untitled.sol)', 'Untitled.sol', (input) => {
+ // if (!input) input = 'New file'
+ // get filename from state (state.newFileName)
+ const fileManager = props.fileManager
+ const newFileName = parentFolder + '/' + 'unnamed' + Math.floor(Math.random() * 101)
+
+ helper.createNonClashingName(newFileName, props.files, async (error, newName) => {
+ // if (error) return tooltip('Failed to create file ' + newName + ' ' + error)
+ if (error) return
+ const createFile = await fileManager.writeFile(newName, '')
+
+ if (!createFile) {
+ // tooltip('Failed to create file ' + newName)
+ } else {
+ props.addFile(parentFolder, newFileName)
+ await fileManager.open(newName)
+ }
+ })
+ // }, null, true)
+ }
+
+ 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 = props.files
+
+ function loadFile (name: string): void {
+ const fileReader = new FileReader()
+
+ fileReader.onload = async function (event) {
+ if (helper.checkSpecialChars(file.name)) {
+ // modalDialogCustom.alert('Special characters are not allowed')
+ return
+ }
+ const success = await files.set(name, event.target.result)
+
+ if (!success) {
+ // modalDialogCustom.alert('Failed to create file ' + name)
+ } else {
+ props.addFile(props.title, name)
+ await props.fileManager.open(name)
+ }
+ }
+ fileReader.readAsText(file)
+ }
+ const name = files.type + '/' + file.name
+
+ files.exists(name, (error, exist) => {
+ if (error) console.log(error)
+ if (!exist) {
+ loadFile(name)
+ } else {
+ // modalDialogCustom.confirm('Confirm overwrite', `The file ${name} already exists! Would you like to overwrite it?`, () => { loadFile() })
+ }
+ })
+ })
+ }
+
+ const publishToGist = () => {
+ // modalDialogCustom.confirm(
+ // '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.',
+ // () => { this.toGist() }
+ toGist()
+ // )
+ }
+
+ const toGist = (id?: string) => {
+ const proccedResult = function (error, data) {
+ if (error) {
+ // modalDialogCustom.alert('Failed to manage gist: ' + error)
+ console.log('Failed to manage gist: ' + error)
+ } else {
+ if (data.html_url) {
+ // modalDialogCustom.confirm('Gist is ready', `The gist is at ${data.html_url}. Would you like to open it in a new window?`, () => {
+ // window.open(data.html_url, '_blank')
+ // })
+ } else {
+ // modalDialogCustom.alert(data.message + ' ' + data.documentation_url + ' ' + JSON.stringify(data.errors, null, '\t'))
+ }
+ }
+ }
+
+ /**
+ * This function is to get the original content of given gist
+ * @params id is the gist id to fetch
+ */
+ async function getOriginalFiles (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(props.files, folder, (error, packaged) => {
+ if (error) {
+ console.log(error)
+ // modalDialogCustom.alert('Failed to create gist: ' + error.message)
+ } else {
+ // check for token
+ if (!props.accessToken) {
+ // modalDialogCustom.alert(
+ // 'Remix requires an access token (which includes gists creation permission). Please go to the settings tab to create one.'
+ // )
+ } 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: props.accessToken })
+
+ if (id) {
+ const originalFileList = 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]
+ })
+
+ // tooltip('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
+ // tooltip('Creating a new gist ...')
+ gists.create({
+ description: description,
+ public: true,
+ files: packaged
+ }, (error, result) => {
+ proccedResult(error, result)
+ })
+ }
+ }
+ }
+ })
+ }
+
+ return (
+ <>
+ { props.title }
+ {
+ state.menuItems.map(({ action, title, icon }, index) => {
+ if (action === 'uploadFile') {
+ return (
+
+ )
+ } else {
+ return (
+ {
+ e.stopPropagation()
+ if (action === 'createNewFile') {
+ createNewFile()
+ } else if (action === 'publishToGist') {
+ publishToGist()
+ } else {
+ state.actions[action]()
+ }
+ }}
+ className={'newFile ' + icon + ' remixui_newFile'}
+ title={title}
+ key={index}
+ >
+
+ )
+ }
+ })}
+
+ >
+ )
+}
+
+export default FileExplorerMenu
diff --git a/libs/remix-ui/file-explorer/src/lib/file-explorer.tsx b/libs/remix-ui/file-explorer/src/lib/file-explorer.tsx
index 6b1882fd7e..ca1e1d8cb9 100644
--- a/libs/remix-ui/file-explorer/src/lib/file-explorer.tsx
+++ b/libs/remix-ui/file-explorer/src/lib/file-explorer.tsx
@@ -1,131 +1,33 @@
import React, { useEffect, useState, useRef } from 'react' // eslint-disable-line
import { TreeView, TreeViewItem } from '@remix-ui/tree-view' // eslint-disable-line
import { DragDropContext, Droppable, Draggable } from 'react-beautiful-dnd' // eslint-disable-line
-import * as async from 'async'
-import * as Gists from 'gists'
-import * as helper from '../../../../../apps/remix-ide/src/lib/helper'
-import QueryParams from '../../../../../apps/remix-ide/src/lib/query-params'
+import { FileExplorerMenu } from './file-explorer-menu' // eslint-disable-line
import { FileExplorerProps, File } from './types'
import './css/file-explorer.css'
-const queryParams = new QueryParams()
-
-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)
- })
- }
- })
-}
-
export const FileExplorer = (props: FileExplorerProps) => {
const { files, name, registry, plugin } = props
- 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 = props.files
-
- function loadFile () {
- const fileReader = new FileReader()
-
- fileReader.onload = async function (event) {
- if (helper.checkSpecialChars(file.name)) {
- // modalDialogCustom.alert('Special characters are not allowed')
- return
- }
- const success = await files.set(name, event.target.result)
-
- if (!success) {
- // modalDialogCustom.alert('Failed to create file ' + name)
- } else {
- // self.events.trigger('focus', [name])
- }
- }
- fileReader.readAsText(file)
- }
- const name = files.type + '/' + file.name
-
- files.exists(name, (error, exist) => {
- if (error) console.log(error)
- if (!exist) {
- loadFile()
- } else {
- // modalDialogCustom.confirm('Confirm overwrite', `The file ${name} already exists! Would you like to overwrite it?`, () => { loadFile() })
- }
- })
- })
- }
const containerRef = useRef(null)
const [state, setState] = useState({
focusElement: [],
focusPath: null,
- menuItems: [
- {
- action: 'createNewFile',
- title: 'Create New File',
- icon: 'fas fa-plus-circle'
- },
- {
- action: 'publishToGist',
- title: 'Publish all [browser] explorer files to a github gist',
- icon: 'fab fa-github'
- },
- {
- action: 'uploadFile',
- title: 'Add Local file to the Browser Storage Explorer',
- icon: 'far fa-folder-open'
- },
- {
- action: 'updateGist',
- title: 'Update the current [gist] explorer',
- icon: 'fab fa-github'
- }
- ].filter(item => props.menuItems && props.menuItems.find((name) => { return name === item.action })),
files: [],
- actions: {},
fileManager: null,
- tokenAccess: null,
+ accessToken: null,
ctrlKey: false,
newFileName: ''
})
useEffect(() => {
(async () => {
- console.log('registry: ', registry)
const fileManager = registry.get('filemanager').api
const config = registry.get('config').api
- const tokenAccess = config.get('settings/gist-access-token').api
+ const accessToken = config.get('settings/gist-access-token')
const files = await fetchDirectoryContent(name)
- const actions = {
- updateGist: () => {},
- uploadFile,
- publishToGist
- }
setState(prevState => {
- return { ...prevState, fileManager, tokenAccess, files, actions }
+ return { ...prevState, fileManager, accessToken, files }
})
})()
}, [])
@@ -183,153 +85,35 @@ export const FileExplorer = (props: FileExplorerProps) => {
return [...folders, ...files]
}
- const extractNameFromKey = (key) => {
+ const extractNameFromKey = (key: string):string => {
const keyPath = key.split('/')
return keyPath[keyPath.length - 1]
}
- const createNewFile = (parentFolder = 'browser') => {
- // const self = this
- // modalDialogCustom.prompt('Create new file', 'File Name (e.g Untitled.sol)', 'Untitled.sol', (input) => {
- // if (!input) input = 'New file'
- // get filename from state (state.newFileName)
- const fileManager = state.fileManager
- const newFileName = parentFolder + '/' + 'unnamed' + Math.floor(Math.random() * 101)
-
- helper.createNonClashingName(newFileName, files, async (error, newName) => {
- // if (error) return tooltip('Failed to create file ' + newName + ' ' + error)
- if (error) return
- const createFile = await fileManager.writeFile(newName, '')
-
- if (!createFile) {
- // tooltip('Failed to create file ' + newName)
- } else {
- if (parentFolder === name) {
- // const updatedFiles = await resolveDirectory(parentFolder, state.files)
-
- setState(prevState => {
- return {
- ...prevState,
- files: [...prevState.files, {
- path: newFileName,
- name: extractNameFromKey(newFileName),
- isDirectory: false
- }]
- }
- })
- }
- await fileManager.open(newName)
- if (newName.includes('_test.sol')) {
- plugin.events.trigger('newTestFileCreated', [newName])
+ const addFile = async (parentFolder: string, newFileName: string) => {
+ if (parentFolder === name) {
+ setState(prevState => {
+ return {
+ ...prevState,
+ files: [...prevState.files, {
+ path: newFileName,
+ name: extractNameFromKey(newFileName),
+ isDirectory: false
+ }],
+ focusElement: [newFileName]
}
- }
- })
- // }, null, true)
- }
-
- const publishToGist = () => {
- // modalDialogCustom.confirm(
- // '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.',
- // () => { this.toGist() }
- toGist()
- // )
- }
+ })
+ } else {
+ const updatedFiles = await resolveDirectory(parentFolder, state.files)
- const toGist = (id?: string) => {
- const proccedResult = function (error, data) {
- if (error) {
- // modalDialogCustom.alert('Failed to manage gist: ' + error)
- console.log('Failed to manage gist: ' + error)
- } else {
- if (data.html_url) {
- // modalDialogCustom.confirm('Gist is ready', `The gist is at ${data.html_url}. Would you like to open it in a new window?`, () => {
- // window.open(data.html_url, '_blank')
- // })
- } else {
- // modalDialogCustom.alert(data.message + ' ' + data.documentation_url + ' ' + JSON.stringify(data.errors, null, '\t'))
- }
- }
+ setState(prevState => {
+ return { ...prevState, files: updatedFiles, focusElement: [newFileName] }
+ })
}
-
- /**
- * This function is to get the original content of given gist
- * @params id is the gist id to fetch
- */
- async function getOriginalFiles (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 (newFileName.includes('_test.sol')) {
+ plugin.events.trigger('newTestFileCreated', [newFileName])
}
-
- // 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(files, folder, (error, packaged) => {
- if (error) {
- console.log(error)
- // modalDialogCustom.alert('Failed to create gist: ' + error.message)
- } else {
- // check for token
- if (!state.tokenAccess) {
- // modalDialogCustom.alert(
- // 'Remix requires an access token (which includes gists creation permission). Please go to the settings tab to create one.'
- // )
- } 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.tokenAccess })
-
- if (id) {
- const originalFileList = 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]
- })
-
- // tooltip('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
- // tooltip('Creating a new gist ...')
- gists.create({
- description: description,
- public: true,
- files: packaged
- }, (error, result) => {
- proccedResult(error, result)
- })
- }
- }
- }
- })
}
// self._components = {}
@@ -445,52 +229,6 @@ export const FileExplorer = (props: FileExplorerProps) => {
}
}
- const renderMenuItems = () => {
- let items
- if (state.menuItems) {
- items = state.menuItems.map(({ action, title, icon }, index) => {
- if (action === 'uploadFile') {
- return (
-
- )
- } else {
- return (
- {
- e.stopPropagation()
- action === 'createNewFile' ? createNewFile() : state.actions[action]()
- }}
- className={'newFile ' + icon + ' remixui_newFile'}
- title={title}
- key={index}
- >
-
- )
- }
- })
- }
- return (
- <>
- { name }
- {items}
- >
- )
- }
-
const renderFiles = (file, index) => {
if (file.isDirectory) {
return (
@@ -566,7 +304,18 @@ export const FileExplorer = (props: FileExplorerProps) => {
}}
>
-
+
+ }
+ expand={true}>
{(provided) => (
diff --git a/libs/remix-ui/file-explorer/src/lib/types/index.ts b/libs/remix-ui/file-explorer/src/lib/types/index.ts
index e8cfa73f38..08ba0b9477 100644
--- a/libs/remix-ui/file-explorer/src/lib/types/index.ts
+++ b/libs/remix-ui/file-explorer/src/lib/types/index.ts
@@ -13,3 +13,12 @@ export interface File {
isDirectory: boolean,
child?: File[]
}
+
+export interface FileExplorerMenuProps {
+ title: string,
+ menuItems: string[],
+ fileManager: any,
+ addFile: (parent: string, fileName: string) => void,
+ files: any,
+ accessToken: string
+}