publish to gist

pull/1575/head
ioedeveloper 3 years ago
parent 34c2b0c09f
commit f7db279447
  1. 153
      libs/remix-ui/file-explorer/src/lib/file-explorer.tsx
  2. 2
      libs/remix-ui/file-explorer/src/lib/types/index.ts
  3. 165
      libs/remix-ui/workspace/src/lib/actions/workspace.ts
  4. 3
      libs/remix-ui/workspace/src/lib/contexts/index.ts
  5. 16
      libs/remix-ui/workspace/src/lib/providers/FileSystemProvider.tsx
  6. 22
      libs/remix-ui/workspace/src/lib/reducers/workspace.ts
  7. 4
      libs/remix-ui/workspace/src/lib/remix-ui-workspace.tsx

@ -2,22 +2,18 @@ import React, { useEffect, useState, useRef, useReducer, useContext } from 'reac
// import { DragDropContext, Droppable, Draggable } from 'react-beautiful-dnd' // eslint-disable-line
import { TreeView, TreeViewItem } from '@remix-ui/tree-view' // eslint-disable-line
import { Toaster } from '@remix-ui/toaster' // eslint-disable-line
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, MenuItems, FileExplorerState } from './types'
import * as helper from '../../../../../apps/remix-ide/src/lib/helper'
import QueryParams from '../../../../../apps/remix-ide/src/lib/query-params'
import { FileSystemContext } from '@remix-ui/workspace'
import { customAction } from '@remixproject/plugin-api/lib/file-system/file-panel'
import { contextMenuActions } from './utils'
import './css/file-explorer.css'
const queryParams = new QueryParams()
export const FileExplorer = (props: FileExplorerProps) => {
const { name, registry, plugin, focusRoot, contextMenuItems, displayInput, externalUploads, removedContextMenuItems, resetFocus, files } = props
const { name, plugin, focusRoot, contextMenuItems, displayInput, externalUploads, removedContextMenuItems, resetFocus, files } = props
const [state, setState] = useState<FileExplorerState>({
focusElement: [{
key: '',
@ -72,16 +68,6 @@ export const FileExplorer = (props: FileExplorerProps) => {
}
}, [state.focusEdit.element])
useEffect(() => {
(async () => {
const fileManager = registry.get('filemanager').api
setState(prevState => {
return { ...prevState, fileManager, expandPath: [name] }
})
})()
}, [name])
useEffect(() => {
if (focusRoot) {
setState(prevState => {
@ -375,103 +361,7 @@ export const FileExplorer = (props: FileExplorerProps) => {
}
const toGist = (path?: string, type?: string) => {
const filesProvider = fileSystem.provider.provider
const proccedResult = function (error, data) {
if (error) {
global.modal('Publish to gist Failed', 'Failed to manage gist: ' + error, 'Close', () => {})
} else {
if (data.html_url) {
global.modal('Gist is ready', `The gist is at ${data.html_url}. Would you like to open it in a new window?`, 'OK', () => {
window.open(data.html_url, '_blank')
}, 'Cancel', () => {})
} else {
const error = JSON.stringify(data.errors, null, '\t') || ''
const message = data.message === 'Not Found' ? data.message + '. Please make sure the API token has right to create a gist.' : data.message
global.modal('Publish to gist Failed', message + ' ' + data.documentation_url + ' ' + error, 'Close', () => {})
}
}
}
/**
* 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 = path || '/'
const id = type === 'gist' ? extractNameFromKey(path).split('-')[1] : null
packageFiles(filesProvider, folder, async (error, packaged) => {
if (error) {
console.log(error)
global.modal('Publish to gist Failed', 'Failed to create gist: ' + error.message, 'Close', async () => {})
} else {
// check for token
const config = registry.get('config').api
const accessToken = config.get('settings/gist-access-token')
if (!accessToken) {
global.modal('Authorize Token', 'Remix requires an access token (which includes gists creation permission). Please go to the settings tab to create one.', 'Close', () => {})
} 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: 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)
})
}
}
}
})
global.dispatchPublishToGist(path, type)
}
const runScript = async (path: string) => {
@ -878,45 +768,6 @@ export const FileExplorer = (props: FileExplorerProps) => {
export default FileExplorer
async function packageFiles (filesProvider, directory, callback) {
const isFile = filesProvider.isFile(directory)
const ret = {}
if (isFile) {
try {
filesProvider.get(directory, (error, content) => {
if (error) throw new Error('An error ocurred while getting file content. ' + directory)
if (/^\s+$/.test(content) || !content.length) {
content = '// this line is added to create a gist. Empty file is not allowed.'
}
directory = directory.replace(/\//g, '...')
ret[directory] = { content }
callback(null, ret)
})
} catch (e) {
return callback(e)
}
} else {
try {
await filesProvider.copyFolderToJson(directory, ({ path, content }) => {
if (/^\s+$/.test(content) || !content.length) {
content = '// this line is added to create a gist. Empty file is not allowed.'
}
if (path.indexOf('gist-') === 0) {
path = path.split('/')
path.shift()
path = path.join('/')
}
path = path.replace(/\//g, '...')
ret[path] = { content }
})
callback(null, ret)
} catch (e) {
return callback(e)
}
}
}
function joinPath (...paths) {
paths = paths.filter((value) => value !== '').map((path) => path.replace(/^\/|\/$/g, '')) // remove first and last slash)
if (paths.length === 1) return paths[0]

@ -4,8 +4,6 @@ export type MenuItems = action[] // eslint-disable-line no-use-before-define
/* eslint-disable-next-line */
export interface FileExplorerProps {
name: string,
registry: any,
filesProvider: any,
menuItems?: string[],
plugin: any,
focusRoot: boolean,

@ -1,7 +1,8 @@
import React from 'react'
import { bufferToHex, keccakFromString } from 'ethereumjs-util'
import axios, { AxiosResponse } from 'axios'
import { checkSpecialChars, checkSlash, extractParentFromKey } from '@remix-ui/helper'
import { checkSpecialChars, checkSlash, extractParentFromKey, extractNameFromKey } from '@remix-ui/helper'
import Gists from 'gists'
const QueryParams = require('../../../../../../apps/remix-ide/src/lib/query-params')
const examples = require('../../../../../../apps/remix-ide/src/app/editor/examples')
@ -9,6 +10,7 @@ const queuedEvents = []
const pendingEvents = {}
const LOCALHOST = ' - connect to localhost - '
const NO_WORKSPACE = ' - none - '
const queryParams = new QueryParams()
let plugin, dispatch: React.Dispatch<any>
@ -179,6 +181,19 @@ const setDeleteWorkspace = (workspaceName: string) => {
}
}
const displayPopUp = (message: string) => {
return {
type: 'DISPLAY_POPUP_MESSAGE',
payload: message
}
}
const hidePopUp = () => {
return {
type: 'HIDE_POPUP_MESSAGE'
}
}
const createWorkspaceTemplate = async (workspaceName: string, setDefaults = true, template: 'gist-template' | 'code-template' | 'default-template' = 'default-template') => {
if (!workspaceName) throw new Error('workspace name cannot be empty')
if (checkSpecialChars(workspaceName) || checkSlash(workspaceName)) throw new Error('special characters are not allowed')
@ -188,7 +203,6 @@ const createWorkspaceTemplate = async (workspaceName: string, setDefaults = true
await workspaceProvider.createWorkspace(workspaceName)
if (setDefaults) {
const queryParams = new QueryParams()
const params = queryParams.get()
switch (template) {
@ -316,6 +330,82 @@ const getWorkspaces = async (): Promise<string[]> | undefined => {
}
}
const packageGistFiles = async (directory) => {
return new Promise((resolve, reject) => {
const workspaceProvider = plugin.fileProviders.workspace
const isFile = workspaceProvider.isFile(directory)
const ret = {}
if (isFile) {
try {
workspaceProvider.get(directory, (error, content) => {
if (error) throw new Error('An error ocurred while getting file content. ' + directory)
if (/^\s+$/.test(content) || !content.length) {
content = '// this line is added to create a gist. Empty file is not allowed.'
}
directory = directory.replace(/\//g, '...')
ret[directory] = { content }
return resolve(ret)
})
} catch (e) {
return reject(e)
}
} else {
try {
(async () => {
await workspaceProvider.copyFolderToJson(directory, ({ path, content }) => {
if (/^\s+$/.test(content) || !content.length) {
content = '// this line is added to create a gist. Empty file is not allowed.'
}
if (path.indexOf('gist-') === 0) {
path = path.split('/')
path.shift()
path = path.join('/')
}
path = path.replace(/\//g, '...')
ret[path] = { content }
})
resolve(ret)
})()
} catch (e) {
return reject(e)
}
}
})
}
const handleGistResponse = (error, data) => {
if (error) {
dispatch(displayNotification('Publish to gist Failed', 'Failed to manage gist: ' + error, 'Close', null))
} else {
if (data.html_url) {
dispatch(displayNotification('Gist is ready', `The gist is at ${data.html_url}. Would you like to open it in a new window?`, 'OK', 'Cancel', () => {
window.open(data.html_url, '_blank')
}, () => {}))
} else {
const error = JSON.stringify(data.errors, null, '\t') || ''
const message = data.message === 'Not Found' ? data.message + '. Please make sure the API token has right to create a gist.' : data.message
dispatch(displayNotification('Publish to gist Failed', message + ' ' + data.documentation_url + ' ' + error, 'Close', 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 || []
}
const listenOnEvents = (provider) => {
provider.event.on('fileAdded', async (filePath: string) => {
await executeEvent('fileAdded', filePath)
@ -390,7 +480,6 @@ export const initWorkspace = (filePanelPlugin) => async (reducerDispatch: React.
dispatch = reducerDispatch
const workspaceProvider = filePanelPlugin.fileProviders.workspace
const localhostProvider = filePanelPlugin.fileProviders.localhost
const queryParams = new QueryParams()
const params = queryParams.get()
const workspaces = await getWorkspaces() || []
@ -546,6 +635,76 @@ export const deleteWorkspace = (workspaceName: string) => async (dispatch: React
await dispatch(setDeleteWorkspace(workspaceName))
}
export const publishToGist = (path?: string, type?: string) => async (dispatch: React.Dispatch<any>) => {
// 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 = path || '/'
const id = type === 'gist' ? extractNameFromKey(path).split('-')[1] : null
try {
const packaged = await packageGistFiles(folder)
// check for token
const config = plugin.registry.get('config').api
const accessToken = config.get('settings/gist-access-token')
if (!accessToken) {
dispatch(displayNotification('Authorize Token', 'Remix requires an access token (which includes gists creation permission). Please go to the settings tab to create one.', 'Close', 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: 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]
})
dispatch(displayPopUp('Saving gist (' + id + ') ...'))
gists.edit({
description: description,
public: true,
files: allItems,
id: id
}, (error, result) => {
handleGistResponse(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
dispatch(displayPopUp('Creating a new gist ...'))
gists.create({
description: description,
public: true,
files: packaged
}, (error, result) => {
handleGistResponse(error, result)
})
}
}
} catch (error) {
console.log(error)
displayNotification('Publish to gist Failed', 'Failed to create gist: ' + error.message, 'Close', null, async () => {})
}
}
export const clearPopUp = () => async (dispatch: React.Dispatch<any>) => {
dispatch(hidePopUp())
}
const fileAdded = async (filePath: string) => {
await dispatch(fileAddedSuccess(filePath))
if (filePath.includes('_test.sol')) {

@ -13,5 +13,6 @@ export const FileSystemContext = createContext<{
dispatchFetchWorkspaceDirectory: (path: string) => Promise<void>,
dispatchSwitchToWorkspace: (name: string) => Promise<void>,
dispatchRenameWorkspace: (oldName: string, workspaceName: string) => Promise<void>,
dispatchDeleteWorkspace: (workspaceName: string) => Promise<void>
dispatchDeleteWorkspace: (workspaceName: string) => Promise<void>,
dispatchPublishToGist: (path?: string, type?: string) => Promise<void>
}>(null)

@ -5,7 +5,7 @@ import { Toaster } from '@remix-ui/toaster' // eslint-disable-line
// eslint-disable-next-line @typescript-eslint/no-unused-vars
import { FileSystemContext } from '../contexts'
import { browserReducer, browserInitialState } from '../reducers/workspace'
import { initWorkspace, fetchDirectory, addInputField, removeInputField, createWorkspace, fetchWorkspaceDirectory, switchToWorkspace, renameWorkspace, deleteWorkspace } from '../actions/workspace'
import { initWorkspace, fetchDirectory, addInputField, removeInputField, createWorkspace, fetchWorkspaceDirectory, switchToWorkspace, renameWorkspace, deleteWorkspace, clearPopUp, publishToGist } from '../actions/workspace'
import { Modal, WorkspaceProps } from '../types'
// eslint-disable-next-line @typescript-eslint/no-unused-vars
import { Workspace } from '../remix-ui-workspace'
@ -62,6 +62,10 @@ export const FileSystemProvider = (props: WorkspaceProps) => {
await deleteWorkspace(workspaceName)(fsDispatch)
}
const dispatchPublishToGist = async (path?: string, type?: string) => {
await publishToGist(path, type)(fsDispatch)
}
useEffect(() => {
if (modals.length > 0) {
setFocusModal(() => {
@ -101,6 +105,12 @@ export const FileSystemProvider = (props: WorkspaceProps) => {
}
}, [fs.notification])
useEffect(() => {
if (fs.popup) {
toast(fs.popup)
}
}, [fs.popup])
const handleHideModal = () => {
setFocusModal(modal => {
return { ...modal, hide: true, message: null }
@ -116,6 +126,7 @@ export const FileSystemProvider = (props: WorkspaceProps) => {
const handleToaster = () => {
setFocusToaster('')
clearPopUp()(fsDispatch)
}
const toast = (toasterMsg: string) => {
@ -137,7 +148,8 @@ export const FileSystemProvider = (props: WorkspaceProps) => {
dispatchFetchWorkspaceDirectory,
dispatchSwitchToWorkspace,
dispatchRenameWorkspace,
dispatchDeleteWorkspace
dispatchDeleteWorkspace,
dispatchPublishToGist
}
return (
<FileSystemContext.Provider value={value}>

@ -31,7 +31,8 @@ export interface BrowserState {
labelOk: string,
labelCancel: string
},
readonly: boolean
readonly: boolean,
popup: string
}
export const browserInitialState: BrowserState = {
@ -61,7 +62,8 @@ export const browserInitialState: BrowserState = {
labelOk: '',
labelCancel: ''
},
readonly: false
readonly: false,
popup: ''
}
export const browserReducer = (state = browserInitialState, action: Action) => {
@ -431,6 +433,22 @@ export const browserReducer = (state = browserInitialState, action: Action) => {
}
}
case 'DISPLAY_POPUP_MESSAGE': {
const payload = action.payload as string
return {
...state,
popup: payload
}
}
case 'HIDE_POPUP_MESSAGE': {
return {
...state,
popup: ''
}
}
default:
throw new Error()
}

@ -231,8 +231,6 @@ export function Workspace (props: WorkspaceProps) {
{ (global.fs.mode === 'browser') && (currentWorkspace !== NO_WORKSPACE) &&
<FileExplorer
name={currentWorkspace}
registry={props.plugin.registry}
filesProvider={props.plugin.workspace}
menuItems={['createNewFile', 'createNewFolder', 'publishToGist', canUpload ? 'uploadFile' : '']}
plugin={props.plugin}
focusRoot={state.reset}
@ -251,8 +249,6 @@ export function Workspace (props: WorkspaceProps) {
{ global.fs.mode === 'localhost' &&
<FileExplorer
name='localhost'
registry={props.plugin.registry}
filesProvider={props.plugin.localhost}
menuItems={['createNewFile', 'createNewFolder']}
plugin={props.plugin}
focusRoot={state.reset}

Loading…
Cancel
Save