Moved publish to gist and upload file logic to file explorer component

pull/668/head
ioedeveloper 4 years ago
parent 84b776599d
commit 3f09ff53c9
  1. 10
      libs/remix-ui/file-explorer/src/lib/file-explorer-context-menu.tsx
  2. 183
      libs/remix-ui/file-explorer/src/lib/file-explorer-menu.tsx
  3. 215
      libs/remix-ui/file-explorer/src/lib/file-explorer.tsx
  4. 6
      libs/remix-ui/file-explorer/src/lib/types/index.ts

@ -4,7 +4,7 @@ import { FileExplorerContextMenuProps } from './types'
import './css/file-explorer-context-menu.css' import './css/file-explorer-context-menu.css'
export const FileExplorerContextMenu = (props: FileExplorerContextMenuProps) => { export const FileExplorerContextMenu = (props: FileExplorerContextMenuProps) => {
const { actions, createNewFile, createNewFolder, deletePath, renamePath, hideContextMenu, pageX, pageY, path, type, ...otherProps } = props const { actions, createNewFile, createNewFolder, deletePath, renamePath, hideContextMenu, extractParentFromKey, publishToGist, pageX, pageY, path, type, ...otherProps } = props
const contextMenuRef = useRef(null) const contextMenuRef = useRef(null)
useEffect(() => { useEffect(() => {
@ -23,7 +23,10 @@ export const FileExplorerContextMenu = (props: FileExplorerContextMenuProps) =>
}, [pageX, pageY]) }, [pageX, pageY])
const menu = () => { const menu = () => {
return actions.filter(item => item.type.findIndex(name => name === type) !== -1).map((item, index) => { return actions.filter(item => item.type.findIndex(name => {
if ((name === 'browser/gists') && (type === 'folder') && (extractParentFromKey(path) === name)) return true // add publish to gist for gist folders
return name === type
}) !== -1).map((item, index) => {
return <li return <li
id={`menuitem${item.name.toLowerCase()}`} id={`menuitem${item.name.toLowerCase()}`}
key={index} key={index}
@ -43,6 +46,9 @@ export const FileExplorerContextMenu = (props: FileExplorerContextMenuProps) =>
case 'Delete': case 'Delete':
deletePath(path) deletePath(path)
break break
case 'Push changes to gist':
publishToGist()
break
default: default:
break break
} }

@ -1,36 +1,5 @@
import React, { useState, useEffect } from 'react' //eslint-disable-line import React, { useState, useEffect } from 'react' //eslint-disable-line
import { FileExplorerMenuProps } from './types' 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) => { export const FileExplorerMenu = (props: FileExplorerMenuProps) => {
const [state, setState] = useState({ const [state, setState] = useState({
@ -66,8 +35,7 @@ export const FileExplorerMenu = (props: FileExplorerMenuProps) => {
useEffect(() => { useEffect(() => {
const actions = { const actions = {
updateGist: () => {}, updateGist: () => {}
uploadFile
} }
setState(prevState => { setState(prevState => {
@ -75,151 +43,6 @@ export const FileExplorerMenu = (props: FileExplorerMenuProps) => {
}) })
}, []) }, [])
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 ( return (
<> <>
<span className='remixui_label' title={props.title} data-path={props.title} style={{ fontWeight: 'bold' }}>{ props.title }</span> <span className='remixui_label' title={props.title} data-path={props.title} style={{ fontWeight: 'bold' }}>{ props.title }</span>
@ -236,7 +59,7 @@ export const FileExplorerMenu = (props: FileExplorerMenuProps) => {
> >
<input id="fileUpload" data-id="fileExplorerFileUpload" type="file" onChange={(e) => { <input id="fileUpload" data-id="fileExplorerFileUpload" type="file" onChange={(e) => {
e.stopPropagation() e.stopPropagation()
uploadFile(e.target) props.uploadFile(e.target)
}} }}
multiple /> multiple />
</label> </label>
@ -253,7 +76,7 @@ export const FileExplorerMenu = (props: FileExplorerMenuProps) => {
} else if (action === 'createNewFolder') { } else if (action === 'createNewFolder') {
props.createNewFolder() props.createNewFolder()
} else if (action === 'publishToGist') { } else if (action === 'publishToGist') {
publishToGist() props.publishToGist()
} else { } else {
state.actions[action]() state.actions[action]()
} }

@ -3,13 +3,18 @@ import { DragDropContext, Droppable, Draggable } from 'react-beautiful-dnd' // e
import { TreeView, TreeViewItem } from '@remix-ui/tree-view' // 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 { ModalDialog } from '@remix-ui/modal-dialog' // eslint-disable-line
import { Toaster } from '@remix-ui/toaster' // 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 { FileExplorerMenu } from './file-explorer-menu' // eslint-disable-line
import { FileExplorerContextMenu } from './file-explorer-context-menu' // eslint-disable-line import { FileExplorerContextMenu } from './file-explorer-context-menu' // eslint-disable-line
import { FileExplorerProps, File } from './types' import { FileExplorerProps, File } from './types'
import * as helper from '../../../../../apps/remix-ide/src/lib/helper' 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' import './css/file-explorer.css'
const queryParams = new QueryParams()
export const FileExplorer = (props: FileExplorerProps) => { export const FileExplorer = (props: FileExplorerProps) => {
const { filesProvider, name, registry, plugin } = props const { filesProvider, name, registry, plugin } = props
const [state, setState] = useState({ const [state, setState] = useState({
@ -79,7 +84,7 @@ export const FileExplorer = (props: FileExplorerProps) => {
type: ['file', 'folder'] type: ['file', 'folder']
}, { }, {
name: 'Push changes to gist', name: 'Push changes to gist',
type: [] type: ['browser/gists']
}] }]
setState(prevState => { setState(prevState => {
@ -179,7 +184,7 @@ export const FileExplorer = (props: FileExplorerProps) => {
helper.createNonClashingName(newFilePath, filesProvider, async (error, newName) => { helper.createNonClashingName(newFilePath, filesProvider, async (error, newName) => {
if (error) { if (error) {
return modal('Create File Failed', error, { modal('Create File Failed', error, {
label: 'Close', label: 'Close',
fn: async () => {} fn: async () => {}
}, null) }, null)
@ -249,7 +254,7 @@ export const FileExplorer = (props: FileExplorerProps) => {
const exists = fileManager.exists(newPath) const exists = fileManager.exists(newPath)
if (exists) { if (exists) {
return modal('Rename File Failed', 'File name already exists', { modal('Rename File Failed', 'File name already exists', {
label: 'Close', label: 'Close',
fn: async () => {} fn: async () => {}
}, null) }, null)
@ -518,6 +523,179 @@ export const FileExplorer = (props: FileExplorerProps) => {
// } // }
// }) // })
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.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(props.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]
})
// 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)
})
}
}
}
})
}
const handleHideModal = () => { const handleHideModal = () => {
setState(prevState => { setState(prevState => {
return { ...prevState, modalOptions: { ...state.modalOptions, hide: true } } return { ...prevState, modalOptions: { ...state.modalOptions, hide: true } }
@ -754,6 +932,8 @@ export const FileExplorer = (props: FileExplorerProps) => {
createNewFolder={handleNewFolderInput} createNewFolder={handleNewFolderInput}
deletePath={deletePath} deletePath={deletePath}
renamePath={editModeOn} renamePath={editModeOn}
extractParentFromKey={extractParentFromKey}
publishToGist={publishToGist}
pageX={state.focusContext.x} pageX={state.focusContext.x}
pageY={state.focusContext.y} pageY={state.focusContext.y}
path={file.path} path={file.path}
@ -811,9 +991,9 @@ export const FileExplorer = (props: FileExplorerProps) => {
addFile={addFile} addFile={addFile}
createNewFile={handleNewFileInput} createNewFile={handleNewFileInput}
createNewFolder={handleNewFolderInput} createNewFolder={handleNewFolderInput}
files={filesProvider} publishToGist={publishToGist}
uploadFile={uploadFile}
fileManager={state.fileManager} fileManager={state.fileManager}
accessToken={state.accessToken}
/> />
} }
expand={true}> expand={true}>
@ -842,3 +1022,28 @@ export const FileExplorer = (props: FileExplorerProps) => {
} }
export default FileExplorer 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)
})
}
})
}

@ -21,8 +21,8 @@ export interface FileExplorerMenuProps {
addFile: (folder: string, fileName: string) => void, addFile: (folder: string, fileName: string) => void,
createNewFile: (folder?: string) => void, createNewFile: (folder?: string) => void,
createNewFolder: (parentFolder?: string) => void, createNewFolder: (parentFolder?: string) => void,
files: any, publishToGist: () => void,
accessToken: string uploadFile: (target: EventTarget & HTMLInputElement) => void
} }
export interface FileExplorerContextMenuProps { export interface FileExplorerContextMenuProps {
@ -32,6 +32,8 @@ export interface FileExplorerContextMenuProps {
deletePath: (path: string) => void, deletePath: (path: string) => void,
renamePath: (path: string, type: string) => void, renamePath: (path: string, type: string) => void,
hideContextMenu: () => void, hideContextMenu: () => void,
extractParentFromKey?: (key: string) => string,
publishToGist?: () => void,
pageX: number, pageX: number,
pageY: number, pageY: number,
path: string, path: string,

Loading…
Cancel
Save