diff --git a/libs/remix-ui/workspace/src/lib/actions/events.ts b/libs/remix-ui/workspace/src/lib/actions/events.ts index 647451a3e0..711b268e34 100644 --- a/libs/remix-ui/workspace/src/lib/actions/events.ts +++ b/libs/remix-ui/workspace/src/lib/actions/events.ts @@ -1,7 +1,7 @@ import { extractParentFromKey } from '@remix-ui/helper' import React from 'react' import { action } from '../types' -import { displayNotification, displayPopUp, fileAddedSuccess, fileRemovedSuccess, fileRenamedSuccess, folderAddedSuccess, removeContextMenuItem, rootFolderChangedSuccess, setContextMenuItem } from './payload' +import { displayNotification, displayPopUp, fileAddedSuccess, fileRemovedSuccess, fileRenamedSuccess, folderAddedSuccess, loadLocalhostError, loadLocalhostRequest, loadLocalhostSuccess, removeContextMenuItem, rootFolderChangedSuccess, setContextMenuItem, setMode } from './payload' import { addInputField, createWorkspace, fetchWorkspaceDirectory, renameWorkspace, switchToWorkspace, uploadFile } from './workspace' const queuedEvents = [] @@ -61,28 +61,25 @@ export const listenOnProviderEvents = (provider) => async (reducerDispatch: Reac await executeEvent('fileRenamed', oldPath, newPath) }) - // provider.event.on('disconnected', () => { - // dispatch(setMode('browser')) - // }) - - provider.event.on('connected', async () => { - fetchWorkspaceDirectory('/') - // setState(prevState => { - // return { ...prevState, hideRemixdExplorer: false, loadingLocalhost: false } - // }) - }) - provider.event.on('disconnected', async () => { + plugin.fileManager.setMode('browser') + dispatch(setMode('browser')) + dispatch(loadLocalhostError('Remixd disconnected!')) const workspaceProvider = plugin.fileProviders.workspace await switchToWorkspace(workspaceProvider.workspace) }) + provider.event.on('connected', async () => { + plugin.fileManager.setMode('localhost') + dispatch(setMode('localhost')) + fetchWorkspaceDirectory('/') + dispatch(loadLocalhostSuccess()) + }) + provider.event.on('loadingLocalhost', async () => { await switchToWorkspace(LOCALHOST) - // setState(prevState => { - // return { ...prevState, loadingLocalhost: true } - // }) + dispatch(loadLocalhostRequest()) }) provider.event.on('fileExternallyChanged', async (path: string, file: { content: string }) => { diff --git a/libs/remix-ui/workspace/src/lib/actions/index.ts b/libs/remix-ui/workspace/src/lib/actions/index.ts new file mode 100644 index 0000000000..70f9d5d165 --- /dev/null +++ b/libs/remix-ui/workspace/src/lib/actions/index.ts @@ -0,0 +1,378 @@ +import React from 'react' +import { extractNameFromKey, createNonClashingNameAsync } from '@remix-ui/helper' +import Gists from 'gists' +import { customAction } from '@remixproject/plugin-api/lib/file-system/file-panel/type' +import { displayNotification, displayPopUp, fetchDirectoryError, fetchDirectoryRequest, fetchDirectorySuccess, focusElement, hideNotification, hidePopUp, removeInputFieldSuccess, setCurrentWorkspace, setDeleteWorkspace, setExpandPath, setMode, setWorkspaces } from './payload' +import { listenOnPluginEvents, listenOnProviderEvents } from './events' +import { createWorkspaceTemplate, loadWorkspacePreset, setPlugin } from './workspace' + +export * from './events' +export * from './workspace' + +const QueryParams = require('../../../../../../apps/remix-ide/src/lib/query-params') +const queryParams = new QueryParams() + +let plugin, dispatch: React.Dispatch + +export const initWorkspace = (filePanelPlugin) => async (reducerDispatch: React.Dispatch) => { + if (filePanelPlugin) { + plugin = filePanelPlugin + dispatch = reducerDispatch + setPlugin(plugin, dispatch) + const workspaceProvider = filePanelPlugin.fileProviders.workspace + const localhostProvider = filePanelPlugin.fileProviders.localhost + const params = queryParams.get() + const workspaces = await getWorkspaces() || [] + + dispatch(setWorkspaces(workspaces)) + if (params.gist) { + await createWorkspaceTemplate('gist-sample', 'gist-template') + await loadWorkspacePreset('gist-template') + dispatch(setCurrentWorkspace('gist-sample')) + } else if (params.code || params.url) { + await createWorkspaceTemplate('code-sample', 'code-template') + await loadWorkspacePreset('code-template') + dispatch(setCurrentWorkspace('code-sample')) + } else { + if (workspaces.length === 0) { + await createWorkspaceTemplate('default_workspace', 'default-template') + await loadWorkspacePreset('default-template') + dispatch(setCurrentWorkspace('default_workspace')) + } else { + if (workspaces.length > 0) { + workspaceProvider.setWorkspace(workspaces[workspaces.length - 1]) + dispatch(setCurrentWorkspace(workspaces[workspaces.length - 1])) + } + } + } + + listenOnPluginEvents(plugin) + listenOnProviderEvents(workspaceProvider)(dispatch) + listenOnProviderEvents(localhostProvider)(dispatch) + dispatch(setMode('browser')) + } +} + +export const fetchDirectory = async (path: string) => { + const provider = plugin.fileManager.currentFileProvider() + const promise = new Promise((resolve) => { + provider.resolveDirectory(path, (error, fileTree) => { + if (error) console.error(error) + + resolve(fileTree) + }) + }) + + dispatch(fetchDirectoryRequest(promise)) + promise.then((fileTree) => { + dispatch(fetchDirectorySuccess(path, fileTree)) + }).catch((error) => { + dispatch(fetchDirectoryError({ error })) + }) + return promise +} + +export const removeInputField = async (path: string) => { + const provider = plugin.fileManager.currentFileProvider() + const promise = new Promise((resolve) => { + provider.resolveDirectory(path, (error, fileTree) => { + if (error) console.error(error) + + resolve(fileTree) + }) + }) + + promise.then((files) => { + dispatch(removeInputFieldSuccess(path, files)) + }).catch((error) => { + console.error(error) + }) + return promise +} + +export const deleteWorkspace = async (workspaceName: string) => { + await deleteWorkspaceFromProvider(workspaceName) + await dispatch(setDeleteWorkspace(workspaceName)) +} + +export const publishToGist = async (path?: string, type?: string) => { + // 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) + dispatch(displayNotification('Publish to gist Failed', 'Failed to create gist: ' + error.message, 'Close', null, async () => {})) + } +} + +export const clearPopUp = async () => { + dispatch(hidePopUp()) +} + +export const createNewFile = async (path: string, rootDir: string) => { + const fileManager = plugin.fileManager + const newName = await createNonClashingNameAsync(path, fileManager) + const createFile = await fileManager.writeFile(newName, '') + + if (!createFile) { + return dispatch(displayPopUp('Failed to create file ' + newName)) + } else { + const path = newName.indexOf(rootDir + '/') === 0 ? newName.replace(rootDir + '/', '') : newName + + await fileManager.open(path) + setFocusElement([{ key: path, type: 'file' }]) + } +} + +export const setFocusElement = async (elements: { key: string, type: 'file' | 'folder' | 'gist' }[]) => { + dispatch(focusElement(elements)) +} + +export const createNewFolder = async (path: string, rootDir: string) => { + const fileManager = plugin.fileManager + const dirName = path + '/' + const exists = await fileManager.exists(dirName) + + if (exists) { + return dispatch(displayNotification('Rename File Failed', `A file or folder ${extractNameFromKey(path)} already exists at this location. Please choose a different name.`, 'Close', null, () => {})) + } + await fileManager.mkdir(dirName) + path = path.indexOf(rootDir + '/') === 0 ? path.replace(rootDir + '/', '') : path + dispatch(focusElement([{ key: path, type: 'folder' }])) +} + +export const deletePath = async (path: string[]) => { + const fileManager = plugin.fileManager + + for (const p of path) { + try { + await fileManager.remove(p) + } catch (e) { + const isDir = await fileManager.isDirectory(p) + + dispatch(displayPopUp(`Failed to remove ${isDir ? 'folder' : 'file'} ${p}.`)) + } + } +} + +export const renamePath = async (oldPath: string, newPath: string) => { + const fileManager = plugin.fileManager + const exists = await fileManager.exists(newPath) + + if (exists) { + dispatch(displayNotification('Rename File Failed', `A file or folder ${extractNameFromKey(newPath)} already exists at this location. Please choose a different name.`, 'Close', null, () => {})) + } else { + await fileManager.rename(oldPath, newPath) + } +} + +export const copyFile = async (src: string, dest: string) => { + const fileManager = plugin.fileManager + + try { + fileManager.copyFile(src, dest) + } catch (error) { + console.log('Oops! An error ocurred while performing copyFile operation.' + error) + dispatch(displayPopUp('Oops! An error ocurred while performing copyFile operation.' + error)) + } +} + +export const copyFolder = async (src: string, dest: string) => { + const fileManager = plugin.fileManager + + try { + fileManager.copyDir(src, dest) + } catch (error) { + console.log('Oops! An error ocurred while performing copyDir operation.' + error) + dispatch(displayPopUp('Oops! An error ocurred while performing copyDir operation.' + error)) + } +} + +export const runScript = async (path: string) => { + const provider = plugin.fileManager.currentFileProvider() + + provider.get(path, (error, content: string) => { + if (error) { + dispatch(displayPopUp(error)) + return console.log(error) + } + plugin.call('scriptRunner', 'execute', content) + }) +} + +export const emitContextMenuEvent = async (cmd: customAction) => { + plugin.call(cmd.id, cmd.name, cmd) +} + +export const handleClickFile = async (path: string, type: 'file' | 'folder' | 'gist') => { + plugin.fileManager.open(path) + dispatch(focusElement([{ key: path, type }])) +} + +export const handleExpandPath = (paths: string[]) => { + dispatch(setExpandPath(paths)) +} + +const deleteWorkspaceFromProvider = async (workspaceName: string) => { + const workspacesPath = plugin.fileProviders.workspace.workspacesPath + + await plugin.fileManager.closeAllFiles() + plugin.fileProviders.browser.remove(workspacesPath + '/' + workspaceName) + plugin.emit('deleteWorkspace', { name: workspaceName }) +} + +const getWorkspaces = async (): Promise | undefined => { + try { + const workspaces: string[] = await new Promise((resolve, reject) => { + const workspacesPath = plugin.fileProviders.workspace.workspacesPath + + plugin.fileProviders.browser.resolveDirectory('/' + workspacesPath, (error, items) => { + if (error) { + console.error(error) + return reject(error) + } + resolve(Object.keys(items) + .filter((item) => items[item].isDirectory) + .map((folder) => folder.replace(workspacesPath + '/', ''))) + }) + }) + + plugin.setWorkspaces(workspaces) + return workspaces + } catch (e) { + dispatch(displayNotification('Workspaces', 'Workspaces have not been created on your system. Please use "Migrate old filesystem to workspace" on the home page to transfer your files or start by creating a new workspace in the File Explorers.', 'OK', null, () => { dispatch(hideNotification()) }, null)) + console.log(e) + } +} + +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 || [] +} diff --git a/libs/remix-ui/workspace/src/lib/actions/payload.ts b/libs/remix-ui/workspace/src/lib/actions/payload.ts index 0d9de3db64..75a55c962c 100644 --- a/libs/remix-ui/workspace/src/lib/actions/payload.ts +++ b/libs/remix-ui/workspace/src/lib/actions/payload.ts @@ -207,3 +207,22 @@ export const setExpandPath = (paths: string[]) => { payload: paths } } + +export const loadLocalhostError = (error: any) => { + return { + type: 'LOAD_LOCALHOST_ERROR', + payload: error + } +} + +export const loadLocalhostRequest = () => { + return { + type: 'LOAD_LOCALHOST_REQUEST' + } +} + +export const loadLocalhostSuccess = () => { + return { + type: 'LOAD_LOCALHOST_SUCCESS' + } +} diff --git a/libs/remix-ui/workspace/src/lib/actions/workspace.ts b/libs/remix-ui/workspace/src/lib/actions/workspace.ts index d95c5c85a8..9f0a752d6c 100644 --- a/libs/remix-ui/workspace/src/lib/actions/workspace.ts +++ b/libs/remix-ui/workspace/src/lib/actions/workspace.ts @@ -1,75 +1,19 @@ -import React from 'react' import { bufferToHex, keccakFromString } from 'ethereumjs-util' import axios, { AxiosResponse } from 'axios' -import { checkSpecialChars, checkSlash, extractNameFromKey, createNonClashingNameAsync } from '@remix-ui/helper' -import Gists from 'gists' -import { customAction } from '@remixproject/plugin-api/lib/file-system/file-panel/type' -import { addInputFieldSuccess, createWorkspaceError, createWorkspaceRequest, createWorkspaceSuccess, displayNotification, displayPopUp, fetchDirectoryError, fetchDirectoryRequest, fetchDirectorySuccess, fetchWorkspaceDirectoryError, fetchWorkspaceDirectoryRequest, fetchWorkspaceDirectorySuccess, focusElement, hideNotification, hidePopUp, removeInputFieldSuccess, setCurrentWorkspace, setDeleteWorkspace, setExpandPath, setMode, setRenameWorkspace, setWorkspaces } from './payload' -import { listenOnPluginEvents, listenOnProviderEvents } from './events' +import { addInputFieldSuccess, createWorkspaceError, createWorkspaceRequest, createWorkspaceSuccess, displayNotification, fetchWorkspaceDirectoryError, fetchWorkspaceDirectoryRequest, fetchWorkspaceDirectorySuccess, hideNotification, setCurrentWorkspace, setMode, setRenameWorkspace } from './payload' +import { checkSlash, checkSpecialChars } from '@remix-ui/helper' -const QueryParams = require('../../../../../../apps/remix-ide/src/lib/query-params') const examples = require('../../../../../../apps/remix-ide/src/app/editor/examples') +const QueryParams = require('../../../../../../apps/remix-ide/src/lib/query-params') + const LOCALHOST = ' - connect to localhost - ' const NO_WORKSPACE = ' - none - ' const queryParams = new QueryParams() - let plugin, dispatch: React.Dispatch -export const initWorkspace = (filePanelPlugin) => async (reducerDispatch: React.Dispatch) => { - if (filePanelPlugin) { - plugin = filePanelPlugin - dispatch = reducerDispatch - const workspaceProvider = filePanelPlugin.fileProviders.workspace - const localhostProvider = filePanelPlugin.fileProviders.localhost - const params = queryParams.get() - const workspaces = await getWorkspaces() || [] - - dispatch(setWorkspaces(workspaces)) - if (params.gist) { - await createWorkspaceTemplate('gist-sample', 'gist-template') - await loadWorkspacePreset('gist-template') - dispatch(setCurrentWorkspace('gist-sample')) - } else if (params.code || params.url) { - await createWorkspaceTemplate('code-sample', 'code-template') - await loadWorkspacePreset('code-template') - dispatch(setCurrentWorkspace('code-sample')) - } else { - if (workspaces.length === 0) { - await createWorkspaceTemplate('default_workspace', 'default-template') - await loadWorkspacePreset('default-template') - dispatch(setCurrentWorkspace('default_workspace')) - } else { - if (workspaces.length > 0) { - workspaceProvider.setWorkspace(workspaces[workspaces.length - 1]) - dispatch(setCurrentWorkspace(workspaces[workspaces.length - 1])) - } - } - } - - listenOnPluginEvents(plugin) - listenOnProviderEvents(workspaceProvider)(dispatch) - listenOnProviderEvents(localhostProvider)(dispatch) - dispatch(setMode('browser')) - } -} - -export const fetchDirectory = async (path: string) => { - const provider = plugin.fileManager.currentFileProvider() - const promise = new Promise((resolve) => { - provider.resolveDirectory(path, (error, fileTree) => { - if (error) console.error(error) - - resolve(fileTree) - }) - }) - - dispatch(fetchDirectoryRequest(promise)) - promise.then((fileTree) => { - dispatch(fetchDirectorySuccess(path, fileTree)) - }).catch((error) => { - dispatch(fetchDirectoryError({ error })) - }) - return promise +export const setPlugin = (filePanelPlugin, reducerDispatch) => { + plugin = filePanelPlugin + dispatch = reducerDispatch } export const addInputField = async (type: 'file' | 'folder', path: string) => { @@ -90,24 +34,6 @@ export const addInputField = async (type: 'file' | 'folder', path: string) => { return promise } -export const removeInputField = async (path: string) => { - const provider = plugin.fileManager.currentFileProvider() - const promise = new Promise((resolve) => { - provider.resolveDirectory(path, (error, fileTree) => { - if (error) console.error(error) - - resolve(fileTree) - }) - }) - - promise.then((files) => { - dispatch(removeInputFieldSuccess(path, files)) - }).catch((error) => { - console.error(error) - }) - return promise -} - export const createWorkspace = async (workspaceName: string) => { const promise = createWorkspaceTemplate(workspaceName, 'default-template') @@ -123,280 +49,7 @@ export const createWorkspace = async (workspaceName: string) => { return promise } -export const fetchWorkspaceDirectory = async (path: string) => { - const provider = plugin.fileManager.currentFileProvider() - const promise = new Promise((resolve) => { - provider.resolveDirectory(path, (error, fileTree) => { - if (error) console.error(error) - - resolve(fileTree) - }) - }) - - dispatch(fetchWorkspaceDirectoryRequest(promise)) - promise.then((fileTree) => { - dispatch(fetchWorkspaceDirectorySuccess(path, fileTree)) - }).catch((error) => { - dispatch(fetchWorkspaceDirectoryError({ error })) - }) - return promise -} - -export const switchToWorkspace = async (name: string) => { - await plugin.fileManager.closeAllFiles() - if (name === LOCALHOST) { - plugin.fileManager.setMode('localhost') - const isActive = await plugin.call('manager', 'isActive', 'remixd') - - if (!isActive) await plugin.call('manager', 'activatePlugin', 'remixd') - dispatch(setMode('localhost')) - plugin.emit('setWorkspace', { name: LOCALHOST, isLocalhost: true }) - } else if (name === NO_WORKSPACE) { - plugin.fileProviders.workspace.clearWorkspace() - dispatch(setCurrentWorkspace(null)) - } else { - plugin.fileManager.setMode('browser') - const isActive = await plugin.call('manager', 'isActive', 'remixd') - - if (isActive) plugin.call('manager', 'deactivatePlugin', 'remixd') - await plugin.fileProviders.workspace.setWorkspace(name) - - dispatch(setMode('browser')) - dispatch(setCurrentWorkspace(name)) - plugin.emit('setWorkspace', { name, isLocalhost: false }) - } -} - -export const renameWorkspace = async (oldName: string, workspaceName: string) => { - await renameWorkspaceFromProvider(oldName, workspaceName) - await dispatch(setRenameWorkspace(oldName, workspaceName)) -} - -export const deleteWorkspace = async (workspaceName: string) => { - await deleteWorkspaceFromProvider(workspaceName) - await dispatch(setDeleteWorkspace(workspaceName)) -} - -export const publishToGist = async (path?: string, type?: string) => { - // 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) - dispatch(displayNotification('Publish to gist Failed', 'Failed to create gist: ' + error.message, 'Close', null, async () => {})) - } -} - -export const clearPopUp = async () => { - dispatch(hidePopUp()) -} - -export const uploadFile = async (target, targetFolder: string) => { - // 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 workspaceProvider = plugin.fileProviders.workspace - const loadFile = (name: string): void => { - const fileReader = new FileReader() - - fileReader.onload = async function (event) { - if (checkSpecialChars(file.name)) { - dispatch(displayNotification('File Upload Failed', 'Special characters are not allowed', 'Close', null, async () => {})) - return - } - const success = await workspaceProvider.set(name, event.target.result) - - if (!success) { - return dispatch(displayNotification('File Upload Failed', 'Failed to create file ' + name, 'Close', null, async () => {})) - } - const config = plugin.registry.get('config').api - const editor = plugin.registry.get('editor').api - - if ((config.get('currentFile') === name) && (editor.currentContent() !== event.target.result)) { - editor.setText(event.target.result) - } - } - fileReader.readAsText(file) - } - const name = `${targetFolder}/${file.name}` - - workspaceProvider.exists(name).then(exist => { - if (!exist) { - loadFile(name) - } else { - dispatch(displayNotification('Confirm overwrite', `The file ${name} already exists! Would you like to overwrite it?`, 'OK', null, () => { - loadFile(name) - }, () => {})) - } - }).catch(error => { - if (error) console.log(error) - }) - }) -} - -export const createNewFile = async (path: string, rootDir: string) => { - const fileManager = plugin.fileManager - const newName = await createNonClashingNameAsync(path, fileManager) - const createFile = await fileManager.writeFile(newName, '') - - if (!createFile) { - return dispatch(displayPopUp('Failed to create file ' + newName)) - } else { - const path = newName.indexOf(rootDir + '/') === 0 ? newName.replace(rootDir + '/', '') : newName - - await fileManager.open(path) - setFocusElement([{ key: path, type: 'file' }]) - } -} - -export const setFocusElement = async (elements: { key: string, type: 'file' | 'folder' | 'gist' }[]) => { - dispatch(focusElement(elements)) -} - -export const createNewFolder = async (path: string, rootDir: string) => { - const fileManager = plugin.fileManager - const dirName = path + '/' - const exists = await fileManager.exists(dirName) - - if (exists) { - return dispatch(displayNotification('Rename File Failed', `A file or folder ${extractNameFromKey(path)} already exists at this location. Please choose a different name.`, 'Close', null, () => {})) - } - await fileManager.mkdir(dirName) - path = path.indexOf(rootDir + '/') === 0 ? path.replace(rootDir + '/', '') : path - dispatch(focusElement([{ key: path, type: 'folder' }])) -} - -export const deletePath = async (path: string[]) => { - const fileManager = plugin.fileManager - - for (const p of path) { - try { - await fileManager.remove(p) - } catch (e) { - const isDir = await fileManager.isDirectory(p) - - dispatch(displayPopUp(`Failed to remove ${isDir ? 'folder' : 'file'} ${p}.`)) - } - } -} - -export const renamePath = async (oldPath: string, newPath: string) => { - const fileManager = plugin.fileManager - const exists = await fileManager.exists(newPath) - - if (exists) { - dispatch(displayNotification('Rename File Failed', `A file or folder ${extractNameFromKey(newPath)} already exists at this location. Please choose a different name.`, 'Close', null, () => {})) - } else { - await fileManager.rename(oldPath, newPath) - } -} - -export const copyFile = async (src: string, dest: string) => { - const fileManager = plugin.fileManager - - try { - fileManager.copyFile(src, dest) - } catch (error) { - console.log('Oops! An error ocurred while performing copyFile operation.' + error) - dispatch(displayPopUp('Oops! An error ocurred while performing copyFile operation.' + error)) - } -} - -export const copyFolder = async (src: string, dest: string) => { - const fileManager = plugin.fileManager - - try { - fileManager.copyDir(src, dest) - } catch (error) { - console.log('Oops! An error ocurred while performing copyDir operation.' + error) - dispatch(displayPopUp('Oops! An error ocurred while performing copyDir operation.' + error)) - } -} - -export const runScript = async (path: string) => { - const provider = plugin.fileManager.currentFileProvider() - - provider.get(path, (error, content: string) => { - if (error) { - dispatch(displayPopUp(error)) - return console.log(error) - } - plugin.call('scriptRunner', 'execute', content) - }) -} - -export const emitContextMenuEvent = async (cmd: customAction) => { - plugin.call(cmd.id, cmd.name, cmd) -} - -export const handleClickFile = async (path: string, type: 'file' | 'folder' | 'gist') => { - plugin.fileManager.open(path) - dispatch(focusElement([{ key: path, type }])) -} - -export const handleExpandPath = (paths: string[]) => { - dispatch(setExpandPath(paths)) -} - -const createWorkspaceTemplate = async (workspaceName: string, template: 'gist-template' | 'code-template' | 'default-template' = 'default-template') => { +export const createWorkspaceTemplate = async (workspaceName: string, 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') if (await workspaceExists(workspaceName) && template === 'default-template') throw new Error('workspace already exists') @@ -407,7 +60,7 @@ const createWorkspaceTemplate = async (workspaceName: string, template: 'gist-te } } -const loadWorkspacePreset = async (template: 'gist-template' | 'code-template' | 'default-template' = 'default-template') => { +export const loadWorkspacePreset = async (template: 'gist-template' | 'code-template' | 'default-template' = 'default-template') => { const workspaceProvider = plugin.fileProviders.workspace const params = queryParams.get() @@ -482,7 +135,7 @@ const loadWorkspacePreset = async (template: 'gist-template' | 'code-template' | } } -const workspaceExists = async (name: string) => { +export const workspaceExists = async (name: string) => { const workspaceProvider = plugin.fileProviders.workspace const browserProvider = plugin.fileProviders.browser const workspacePath = 'browser/' + workspaceProvider.workspacesPath + '/' + name @@ -490,7 +143,31 @@ const workspaceExists = async (name: string) => { return browserProvider.exists(workspacePath) } -const renameWorkspaceFromProvider = async (oldName: string, workspaceName: string) => { +export const fetchWorkspaceDirectory = async (path: string) => { + const provider = plugin.fileManager.currentFileProvider() + const promise = new Promise((resolve) => { + provider.resolveDirectory(path, (error, fileTree) => { + if (error) console.error(error) + + resolve(fileTree) + }) + }) + + dispatch(fetchWorkspaceDirectoryRequest(promise)) + promise.then((fileTree) => { + dispatch(fetchWorkspaceDirectorySuccess(path, fileTree)) + }).catch((error) => { + dispatch(fetchWorkspaceDirectoryError({ error })) + }) + return promise +} + +export const renameWorkspace = async (oldName: string, workspaceName: string) => { + await renameWorkspaceFromProvider(oldName, workspaceName) + await dispatch(setRenameWorkspace(oldName, workspaceName)) +} + +export const renameWorkspaceFromProvider = async (oldName: string, workspaceName: string) => { if (!workspaceName) throw new Error('name cannot be empty') if (checkSpecialChars(workspaceName) || checkSlash(workspaceName)) throw new Error('special characters are not allowed') if (await workspaceExists(workspaceName)) throw new Error('workspace already exists') @@ -502,110 +179,69 @@ const renameWorkspaceFromProvider = async (oldName: string, workspaceName: strin plugin.emit('renameWorkspace', { name: workspaceName }) } -const deleteWorkspaceFromProvider = async (workspaceName: string) => { - const workspacesPath = plugin.fileProviders.workspace.workspacesPath - +export const switchToWorkspace = async (name: string) => { await plugin.fileManager.closeAllFiles() - plugin.fileProviders.browser.remove(workspacesPath + '/' + workspaceName) - plugin.emit('deleteWorkspace', { name: workspaceName }) -} - -const getWorkspaces = async (): Promise | undefined => { - try { - const workspaces: string[] = await new Promise((resolve, reject) => { - const workspacesPath = plugin.fileProviders.workspace.workspacesPath + if (name === LOCALHOST) { + const isActive = await plugin.call('manager', 'isActive', 'remixd') - plugin.fileProviders.browser.resolveDirectory('/' + workspacesPath, (error, items) => { - if (error) { - console.error(error) - return reject(error) - } - resolve(Object.keys(items) - .filter((item) => items[item].isDirectory) - .map((folder) => folder.replace(workspacesPath + '/', ''))) - }) - }) + if (!isActive) await plugin.call('manager', 'activatePlugin', 'remixd') + dispatch(setMode('localhost')) + plugin.emit('setWorkspace', { name: LOCALHOST, isLocalhost: true }) + } else if (name === NO_WORKSPACE) { + plugin.fileProviders.workspace.clearWorkspace() + dispatch(setCurrentWorkspace(null)) + } else { + const isActive = await plugin.call('manager', 'isActive', 'remixd') - plugin.setWorkspaces(workspaces) - return workspaces - } catch (e) { - dispatch(displayNotification('Workspaces', 'Workspaces have not been created on your system. Please use "Migrate old filesystem to workspace" on the home page to transfer your files or start by creating a new workspace in the File Explorers.', 'OK', null, () => { dispatch(hideNotification()) }, null)) - console.log(e) + if (isActive) plugin.call('manager', 'deactivatePlugin', 'remixd') + await plugin.fileProviders.workspace.setWorkspace(name) + dispatch(setMode('browser')) + dispatch(setCurrentWorkspace(name)) + plugin.emit('setWorkspace', { name, isLocalhost: false }) } } -const packageGistFiles = async (directory) => { - return new Promise((resolve, reject) => { +export const uploadFile = async (target, targetFolder: string) => { + // 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 workspaceProvider = plugin.fileProviders.workspace - const isFile = workspaceProvider.isFile(directory) - const ret = {} + const loadFile = (name: string): void => { + const fileReader = new FileReader() - 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) - } - } - }) -} + fileReader.onload = async function (event) { + if (checkSpecialChars(file.name)) { + dispatch(displayNotification('File Upload Failed', 'Special characters are not allowed', 'Close', null, async () => {})) + return + } + const success = await workspaceProvider.set(name, event.target.result) -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)) - } - } -} + if (!success) { + return dispatch(displayNotification('File Upload Failed', 'Failed to create file ' + name, 'Close', null, async () => {})) + } + const config = plugin.registry.get('config').api + const editor = plugin.registry.get('editor').api -/** - * 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 [] - } + if ((config.get('currentFile') === name) && (editor.currentContent() !== event.target.result)) { + editor.setText(event.target.result) + } + } + fileReader.readAsText(file) + } + const name = `${targetFolder}/${file.name}` - const url = `https://api.github.com/gists/${id}` - const res = await fetch(url) - const data = await res.json() - return data.files || [] + workspaceProvider.exists(name).then(exist => { + if (!exist) { + loadFile(name) + } else { + dispatch(displayNotification('Confirm overwrite', `The file ${name} already exists! Would you like to overwrite it?`, 'OK', null, () => { + loadFile(name) + }, () => {})) + } + }).catch(error => { + if (error) console.log(error) + }) + }) } diff --git a/libs/remix-ui/workspace/src/lib/components/file-explorer.tsx b/libs/remix-ui/workspace/src/lib/components/file-explorer.tsx index ccbcb2d0b4..baa6a73f0c 100644 --- a/libs/remix-ui/workspace/src/lib/components/file-explorer.tsx +++ b/libs/remix-ui/workspace/src/lib/components/file-explorer.tsx @@ -10,7 +10,6 @@ import '../css/file-explorer.css' import { checkSpecialChars, extractNameFromKey, extractParentFromKey, joinPath } from '@remix-ui/helper' // eslint-disable-next-line @typescript-eslint/no-unused-vars import { FileRender } from './file-render' -import { handleExpandPath } from '../actions/workspace' export const FileExplorer = (props: FileExplorerProps) => { const { name, contextMenuItems, removedContextMenuItems, files } = props @@ -404,7 +403,7 @@ export const FileExplorer = (props: FileExplorerProps) => { } else { expandPath = [...new Set(props.expandPath.filter(key => key && (typeof key === 'string') && !key.startsWith(props.name)))] } - handleExpandPath(expandPath) + props.dispatchHandleExpandPath(expandPath) } return ( diff --git a/libs/remix-ui/workspace/src/lib/providers/FileSystemProvider.tsx b/libs/remix-ui/workspace/src/lib/providers/FileSystemProvider.tsx index 2f49277c6f..f2e18f3c07 100644 --- a/libs/remix-ui/workspace/src/lib/providers/FileSystemProvider.tsx +++ b/libs/remix-ui/workspace/src/lib/providers/FileSystemProvider.tsx @@ -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, clearPopUp, publishToGist, uploadFile, createNewFile, setFocusElement, createNewFolder, deletePath, renamePath, copyFile, copyFolder, runScript, emitContextMenuEvent, handleClickFile, handleExpandPath } from '../actions/workspace' +import { initWorkspace, fetchDirectory, removeInputField, deleteWorkspace, clearPopUp, publishToGist, createNewFile, setFocusElement, createNewFolder, deletePath, renamePath, copyFile, copyFolder, runScript, emitContextMenuEvent, handleClickFile, handleExpandPath, addInputField, createWorkspace, fetchWorkspaceDirectory, renameWorkspace, switchToWorkspace, uploadFile } from '../actions' import { Modal, WorkspaceProps } from '../types' // eslint-disable-next-line @typescript-eslint/no-unused-vars import { Workspace } from '../remix-ui-workspace' diff --git a/libs/remix-ui/workspace/src/lib/reducers/workspace.ts b/libs/remix-ui/workspace/src/lib/reducers/workspace.ts index 208137ecbe..4aac385368 100644 --- a/libs/remix-ui/workspace/src/lib/reducers/workspace.ts +++ b/libs/remix-ui/workspace/src/lib/reducers/workspace.ts @@ -533,6 +533,44 @@ export const browserReducer = (state = browserInitialState, action: Action) => { } } + case 'LOAD_LOCALHOST_REQUEST': { + return { + ...state, + localhost: { + ...state.localhost, + isRequesting: true, + isSuccessful: false, + error: null + } + } + } + + case 'LOAD_LOCALHOST_SUCCESS': { + return { + ...state, + localhost: { + ...state.localhost, + isRequesting: false, + isSuccessful: true, + error: null + } + } + } + + case 'LOAD_LOCALHOST_ERROR': { + const payload = action.payload as string + + return { + ...state, + localhost: { + ...state.localhost, + isRequesting: false, + isSuccessful: false, + error: payload + } + } + } + default: throw new Error() } diff --git a/libs/remix-ui/workspace/src/lib/remix-ui-workspace.tsx b/libs/remix-ui/workspace/src/lib/remix-ui-workspace.tsx index 0ba8a40794..861719aad4 100644 --- a/libs/remix-ui/workspace/src/lib/remix-ui-workspace.tsx +++ b/libs/remix-ui/workspace/src/lib/remix-ui-workspace.tsx @@ -1,7 +1,6 @@ import React, { useState, useEffect, useRef, useContext } from 'react' // eslint-disable-line import { FileExplorer } from './components/file-explorer' // eslint-disable-line import './css/remix-ui-workspace.css' -import { WorkspaceState } from './types' import { FileSystemContext } from './contexts' const canUpload = window.File || window.FileReader || window.FileList || window.Blob @@ -9,11 +8,6 @@ const canUpload = window.File || window.FileReader || window.FileList || window. export function Workspace () { const LOCALHOST = ' - connect to localhost - ' const NO_WORKSPACE = ' - none - ' - const [state] = useState({ - hideRemixdExplorer: true, - displayNewFile: false, - loadingLocalhost: false - }) const [currentWorkspace, setCurrentWorkspace] = useState(NO_WORKSPACE) const global = useContext(FileSystemContext) const workspaceRenameInput = useRef() @@ -215,9 +209,9 @@ export function Workspace () { } { - state.loadingLocalhost ?
+ global.fs.localhost.isRequesting ?
:
- { global.fs.mode === 'localhost' && + { global.fs.mode === 'localhost' && global.fs.localhost.isSuccessful &&