Complete file upload

pull/668/head
ioedeveloper 4 years ago
parent 568324e84c
commit 9b0e58c41e
  1. 290
      libs/remix-ui/file-explorer/src/lib/file-explorer-menu.tsx
  2. 323
      libs/remix-ui/file-explorer/src/lib/file-explorer.tsx
  3. 9
      libs/remix-ui/file-explorer/src/lib/types/index.ts

@ -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 (
<>
<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()
uploadFile(e.target)
}}
multiple />
</label>
)
} else {
return (
<span
id={action}
data-id={'fileExplorerNewFile' + action}
onClick={(e) => {
e.stopPropagation()
if (action === 'createNewFile') {
createNewFile()
} else if (action === 'publishToGist') {
publishToGist()
} else {
state.actions[action]()
}
}}
className={'newFile ' + icon + ' remixui_newFile'}
title={title}
key={index}
>
</span>
)
}
})}
</span>
</>
)
}
export default FileExplorerMenu

@ -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 (
<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={({ stopPropagation, target }) => {
stopPropagation()
uploadFile(target)
}}
multiple />
</label>
)
} else {
return (
<span
id={action}
data-id={'fileExplorerNewFile' + action}
onClick={(e) => {
e.stopPropagation()
action === 'createNewFile' ? createNewFile() : state.actions[action]()
}}
className={'newFile ' + icon + ' remixui_newFile'}
title={title}
key={index}
>
</span>
)
}
})
}
return (
<>
<span className='remixui_label' title={name} data-path={name} style={{ fontWeight: 'bold' }}>{ name }</span>
<span className="remixui_menu">{items}</span>
</>
)
}
const renderFiles = (file, index) => {
if (file.isDirectory) {
return (
@ -566,7 +304,18 @@ export const FileExplorer = (props: FileExplorerProps) => {
}}
>
<TreeView id='treeView'>
<TreeViewItem id="treeViewItem" label={renderMenuItems()} expand={true}>
<TreeViewItem id="treeViewItem"
label={
<FileExplorerMenu
title={name}
menuItems={props.menuItems}
addFile={addFile}
files={props.files}
fileManager={state.fileManager}
accessToken={state.accessToken}
/>
}
expand={true}>
<DragDropContext onDragEnd={onDragEnd}>
<Droppable droppableId='droppableTreeView'>
{(provided) => (

@ -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
}

Loading…
Cancel
Save