Switch to workspaces

pull/1575/head
ioedeveloper 3 years ago
parent 38b9bbf5d7
commit daf5015488
  1. 4
      libs/remix-ui/toaster/src/lib/toaster.tsx
  2. 102
      libs/remix-ui/workspace/src/lib/actions/workspace.ts
  3. 6
      libs/remix-ui/workspace/src/lib/contexts/index.ts
  4. 47
      libs/remix-ui/workspace/src/lib/providers/FileSystemProvider.tsx
  5. 127
      libs/remix-ui/workspace/src/lib/reducers/workspace.ts
  6. 45
      libs/remix-ui/workspace/src/lib/remix-ui-workspace.tsx
  7. 1
      libs/remix-ui/workspace/src/lib/types/index.ts

@ -6,7 +6,8 @@ import './toaster.css'
/* eslint-disable-next-line */
export interface ToasterProps {
message: string
timeOut?: number
timeOut?: number,
handleHide?: () => void
}
export const Toaster = (props: ToasterProps) => {
@ -59,6 +60,7 @@ export const Toaster = (props: ToasterProps) => {
if (state.timeOutId) {
clearTimeout(state.timeOutId)
}
props.handleHide && props.handleHide()
setState(prevState => {
return { ...prevState, message: '', hide: true, hiding: false, timeOutId: null, showModal: false }
})

@ -7,6 +7,8 @@ const QueryParams = require('../../../../../../apps/remix-ide/src/lib/query-para
const examples = require('../../../../../../apps/remix-ide/src/app/editor/examples')
const queuedEvents = []
const pendingEvents = {}
const LOCALHOST = ' - connect to localhost - '
const NO_WORKSPACE = ' - none - '
let plugin, dispatch: React.Dispatch<any>
@ -121,6 +123,48 @@ const setReadOnlyMode = (mode: boolean) => {
}
}
const createWorkspaceError = (error: any) => {
return {
type: 'CREATE_WORKSPACE_ERROR',
payload: error
}
}
const createWorkspaceRequest = (promise: Promise<any>) => {
return {
type: 'CREATE_WORKSPACE_REQUEST',
payload: promise
}
}
const createWorkspaceSuccess = (workspaceName: string) => {
return {
type: 'CREATE_WORKSPACE_SUCCESS',
payload: workspaceName
}
}
const fetchWorkspaceDirectoryError = (error: any) => {
return {
type: 'FETCH_WORKSPACE_DIRECTORY_ERROR',
payload: error
}
}
const fetchWorkspaceDirectoryRequest = (promise: Promise<any>) => {
return {
type: 'FETCH_WORKSPACE_DIRECTORY_REQUEST',
payload: promise
}
}
const fetchWorkspaceDirectorySuccess = (path: string, fileTree) => {
return {
type: 'FETCH_WORKSPACE_DIRECTORY_SUCCESS',
payload: { path, fileTree }
}
}
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')
@ -244,6 +288,7 @@ const listenOnEvents = (provider) => {
})
provider.event.on('folderAdded', async (folderPath: string) => {
if (folderPath.indexOf('/.workspaces') === 0) return
await executeEvent('folderAdded', folderPath)
})
@ -322,6 +367,7 @@ export const initWorkspace = (filePanelPlugin) => async (reducerDispatch: React.
const params = queryParams.get()
const workspaces = await getWorkspaces() || []
dispatch(setWorkspaces(workspaces))
if (params.gist) {
await createWorkspaceTemplate('gist-sample', true, 'gist-template')
dispatch(setCurrentWorkspace('gist-sample'))
@ -405,6 +451,62 @@ export const removeInputField = (path: string) => (dispatch: React.Dispatch<any>
return promise
}
export const createWorkspace = (workspaceName: string) => (dispatch: React.Dispatch<any>) => {
const promise = createWorkspaceTemplate(workspaceName, true, 'default-template')
dispatch(createWorkspaceRequest(promise))
promise.then(async () => {
await plugin.fileManager.closeAllFiles()
dispatch(createWorkspaceSuccess(workspaceName))
}).catch((error) => {
dispatch(createWorkspaceError({ error }))
})
return promise
}
export const fetchWorkspaceDirectory = (path: string) => (dispatch: React.Dispatch<any>) => {
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 = (name: string) => async (dispatch: React.Dispatch<any>) => {
await plugin.fileManager.closeAllFiles()
if (name === LOCALHOST) {
plugin.fileProviders.workspace.clearWorkspace()
const isActive = await plugin.call('manager', 'isActive', 'remixd')
if (!isActive) plugin.call('manager', 'activatePlugin', 'remixd')
plugin.fileManager.setMode('localhost')
dispatch(setMode('localhost'))
plugin.emit('setWorkspace', { name: LOCALHOST, isLocalhost: true })
} else if (name === NO_WORKSPACE) {
plugin.fileProviders.workspace.clearWorkspace()
} else {
const isActive = await plugin.call('manager', 'isActive', 'remixd')
if (isActive) plugin.call('manager', 'deactivatePlugin', 'remixd')
await plugin.fileProviders.workspace.setWorkspace(name)
plugin.fileManager.setMode('browser')
dispatch(setMode('browser'))
dispatch(setCurrentWorkspace(name))
plugin.emit('setWorkspace', { name, isLocalhost: false })
}
}
const fileAdded = async (filePath: string) => {
await dispatch(fileAddedSuccess(filePath))
if (filePath.includes('_test.sol')) {

@ -7,5 +7,9 @@ export const FileSystemContext = createContext<{
dispatchInitWorkspace:() => Promise<void>,
dispatchFetchDirectory:(path: string) => Promise<void>,
dispatchAddInputField:(path: string, type: 'file' | 'folder') => Promise<void>,
dispatchRemoveInputField:(path: string) => Promise<void>
dispatchRemoveInputField:(path: string) => Promise<void>,
dispatchCreateWorkspace: (workspaceName: string) => Promise<void>,
toast: (toasterMsg: string) => void,
dispatchFetchWorkspaceDirectory: (path: string) => void,
dispatchSwitchToWorkspace: (name: string) => void
}>(null)

@ -1,10 +1,11 @@
// eslint-disable-next-line @typescript-eslint/no-unused-vars
import React, { useReducer, useState, useEffect } from 'react'
import { ModalDialog } from '@remix-ui/modal-dialog' // eslint-disable-line
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 } from '../actions/workspace'
import { initWorkspace, fetchDirectory, addInputField, removeInputField, createWorkspace, fetchWorkspaceDirectory, switchToWorkspace } from '../actions/workspace'
import { Modal, WorkspaceProps } from '../types'
// eslint-disable-next-line @typescript-eslint/no-unused-vars
import { Workspace } from '../remix-ui-workspace'
@ -22,6 +23,8 @@ export const FileSystemProvider = (props: WorkspaceProps) => {
cancelFn: () => {}
})
const [modals, setModals] = useState<Modal[]>([])
const [focusToaster, setFocusToaster] = useState<string>('')
const [toasters, setToasters] = useState<string[]>([])
const dispatchInitWorkspace = async () => {
await initWorkspace(plugin)(fsDispatch)
@ -39,6 +42,18 @@ export const FileSystemProvider = (props: WorkspaceProps) => {
await removeInputField(path)(fsDispatch)
}
const dispatchCreateWorkspace = async (workspaceName: string) => {
await createWorkspace(workspaceName)(fsDispatch)
}
const dispatchFetchWorkspaceDirectory = async (path: string) => {
await fetchWorkspaceDirectory(path)(fsDispatch)
}
const dispatchSwitchToWorkspace = async (name: string) => {
await switchToWorkspace(name)(fsDispatch)
}
useEffect(() => {
if (modals.length > 0) {
setFocusModal(() => {
@ -60,6 +75,18 @@ export const FileSystemProvider = (props: WorkspaceProps) => {
}
}, [modals])
useEffect(() => {
if (toasters.length > 0) {
setFocusToaster(() => {
return toasters[0]
})
const toasterList = toasters.slice()
toasterList.shift()
setToasters(toasterList)
}
}, [toasters])
useEffect(() => {
if (fs.notification.title) {
modal(fs.notification.title, fs.notification.message, fs.notification.labelOk, fs.notification.actionOk, fs.notification.labelCancel, fs.notification.actionCancel)
@ -79,18 +106,34 @@ export const FileSystemProvider = (props: WorkspaceProps) => {
})
}
const handleToaster = () => {
setFocusToaster('')
}
const toast = (toasterMsg: string) => {
setToasters(messages => {
messages.push(toasterMsg)
return [...messages]
})
}
const value = {
fs,
modal,
toast,
dispatchInitWorkspace,
dispatchFetchDirectory,
dispatchAddInputField,
dispatchRemoveInputField
dispatchRemoveInputField,
dispatchCreateWorkspace,
dispatchFetchWorkspaceDirectory,
dispatchSwitchToWorkspace
}
return (
<FileSystemContext.Provider value={value}>
<Workspace plugin={plugin} />
<ModalDialog id='fileSystem' { ...focusModal } handleHide={ handleHideModal } />
<Toaster message={focusToaster} handleHide={handleToaster} />
</FileSystemContext.Provider>
)
}

@ -1,5 +1,4 @@
import { extractNameFromKey, File } from '@remix-ui/file-explorer'
import { extractParentFromKey } from '@remix-ui/helper'
import * as _ from 'lodash'
interface Action {
type: string
@ -159,6 +158,57 @@ export const browserReducer = (state = browserInitialState, action: Action) => {
}
}
case 'FETCH_WORKSPACE_DIRECTORY_REQUEST': {
return {
...state,
browser: {
...state.browser,
isRequesting: state.mode === 'browser',
isSuccessful: false,
error: null
},
localhost: {
...state.localhost,
isRequesting: state.mode === 'localhost',
isSuccessful: false,
error: null
}
}
}
case 'FETCH_WORKSPACE_DIRECTORY_SUCCESS': {
const payload = action.payload as { path: string, fileTree }
return {
...state,
browser: {
...state.browser,
files: state.mode === 'browser' ? fetchWorkspaceDirectoryContent(state, payload) : state.browser.files,
isRequesting: false,
isSuccessful: true,
error: null
}
}
}
case 'FETCH_WORKSPACE_DIRECTORY_ERROR': {
return {
...state,
browser: {
...state.browser,
isRequesting: false,
isSuccessful: false,
error: state.mode === 'browser' ? action.payload : null
},
localhost: {
...state.localhost,
isRequesting: false,
isSuccessful: false,
error: state.mode === 'localhost' ? action.payload : null
}
}
}
case 'DISPLAY_NOTIFICATION': {
const payload = action.payload as { title: string, message: string, actionOk: () => void, actionCancel: () => void, labelOk: string, labelCancel: string }
@ -208,12 +258,12 @@ export const browserReducer = (state = browserInitialState, action: Action) => {
browser: {
...state.browser,
files: state.mode === 'browser' ? fetchDirectoryContent(state, payload) : state.browser.files,
expandPath: state.mode === 'browser' ? [...new Set([...state.browser.expandPath, payload])] : state.browser.expandPath
expandPath: state.mode === 'browser' ? [...new Set([...state.browser.expandPath, payload.path])] : state.browser.expandPath
},
localhost: {
...state.localhost,
files: state.mode === 'localhost' ? fetchDirectoryContent(state, payload) : state.localhost.files,
expandPath: state.mode === 'localhost' ? [...new Set([...state.localhost.expandPath, payload])] : state.localhost.expandPath
expandPath: state.mode === 'localhost' ? [...new Set([...state.localhost.expandPath, payload.path])] : state.localhost.expandPath
}
}
}
@ -305,6 +355,46 @@ export const browserReducer = (state = browserInitialState, action: Action) => {
}
}
case 'CREATE_WORKSPACE_REQUEST': {
return {
...state,
browser: {
...state.browser,
isRequesting: true,
isSuccessful: false,
error: null
}
}
}
case 'CREATE_WORKSPACE_SUCCESS': {
const payload = action.payload as string
return {
...state,
browser: {
...state.browser,
currentWorkspace: payload,
workspaces: state.browser.workspaces.includes(payload) ? state.browser.workspaces : [...state.browser.workspaces, action.payload],
isRequesting: false,
isSuccessful: true,
error: null
}
}
}
case 'CREATE_WORKSPACE_ERROR': {
return {
...state,
browser: {
...state.browser,
isRequesting: false,
isSuccessful: false,
error: action.payload
}
}
}
default:
throw new Error()
}
@ -333,6 +423,7 @@ const fileRemoved = (state: BrowserState, path: string): { [x: string]: Record<s
// IDEA: Modify function to remove blank input field without fetching content
const fetchDirectoryContent = (state: BrowserState, payload: { fileTree, path: string, type?: 'file' | 'folder' }, deletePath?: string) => {
if (!payload.fileTree) return state.mode === 'browser' ? state.browser.files : state[state.mode].files
if (state.mode === 'browser') {
if (payload.path === state.browser.currentWorkspace) {
let files = normalize(payload.fileTree, payload.path, payload.type)
@ -345,9 +436,13 @@ const fetchDirectoryContent = (state: BrowserState, payload: { fileTree, path: s
const _path = splitPath(state, payload.path)
const prevFiles = _.get(files, _path)
prevFiles.child = _.merge(normalize(payload.fileTree, payload.path, payload.type), prevFiles.child)
if (deletePath) delete prevFiles.child[deletePath]
files = _.set(files, _path, prevFiles)
if (prevFiles) {
prevFiles.child = _.merge(normalize(payload.fileTree, payload.path, payload.type), prevFiles.child)
if (deletePath) delete prevFiles.child[deletePath]
files = _.set(files, _path, prevFiles)
} else if (payload.fileTree && payload.path) {
files = { [payload.path]: normalize(payload.fileTree, payload.path, payload.type) }
}
return files
}
} else {
@ -362,14 +457,28 @@ const fetchDirectoryContent = (state: BrowserState, payload: { fileTree, path: s
const _path = splitPath(state, payload.path)
const prevFiles = _.get(files, _path)
prevFiles.child = _.merge(normalize(payload.fileTree, payload.path, payload.type), prevFiles.child)
if (deletePath) delete prevFiles.child[deletePath]
files = _.set(files, _path, prevFiles)
if (prevFiles) {
prevFiles.child = _.merge(normalize(payload.fileTree, payload.path, payload.type), prevFiles.child)
if (deletePath) delete prevFiles.child[deletePath]
files = _.set(files, _path, prevFiles)
} else {
files = { [payload.path]: normalize(payload.fileTree, payload.path, payload.type) }
}
return files
}
}
}
const fetchWorkspaceDirectoryContent = (state: BrowserState, payload: { fileTree, path: string }): { [x: string]: Record<string, File> } => {
if (state.mode === 'browser') {
const files = normalize(payload.fileTree, payload.path)
return { [payload.path]: files }
} else {
return fetchDirectoryContent(state, payload)
}
}
const normalize = (filesList, directory?: string, newInputType?: 'folder' | 'file'): Record<string, File> => {
const folders = {}
const files = {}

@ -1,7 +1,6 @@
import React, { useState, useEffect, useRef, useContext } from 'react' // eslint-disable-line
import { FileExplorer } from '@remix-ui/file-explorer' // eslint-disable-line
import './remix-ui-workspace.css'
import { Toaster } from '@remix-ui/toaster' // eslint-disable-line
import { WorkspaceProps, WorkspaceState } from './types'
import { FileSystemContext } from './contexts'
@ -16,8 +15,7 @@ export function Workspace (props: WorkspaceProps) {
displayNewFile: false,
externalUploads: null,
uploadFileEvent: null,
loadingLocalhost: false,
toasterMsg: ''
loadingLocalhost: false
})
const [currentWorkspace, setCurrentWorkspace] = useState<string>(NO_WORKSPACE)
const global = useContext(FileSystemContext)
@ -29,11 +27,11 @@ export function Workspace (props: WorkspaceProps) {
useEffect(() => {
if (global.fs.mode === 'browser') {
setCurrentWorkspace(global.fs.browser.currentWorkspace)
global.dispatchFetchDirectory(global.fs.browser.currentWorkspace)
global.dispatchFetchWorkspaceDirectory(global.fs.browser.currentWorkspace)
} else if (global.fs.mode === 'localhost') {
global.dispatchFetchDirectory('localhost')
global.dispatchFetchWorkspaceDirectory('localhost')
}
}, [global.fs.browser.currentWorkspace, global.fs.localhost.sharedFolder])
}, [global.fs.browser.currentWorkspace, global.fs.localhost.sharedFolder, global.fs.mode])
useEffect(() => {
if (global.fs.mode === 'localhost') setCurrentWorkspace(LOCALHOST)
@ -50,10 +48,6 @@ export function Workspace (props: WorkspaceProps) {
return createWorkspace()
}
props.plugin.request.setWorkspace = (workspaceName) => {
return setWorkspace(workspaceName)
}
// props.plugin.request.createNewFile = async () => {
// if (!state.workspaces.length) await createNewWorkspace('default_workspace')
// props.plugin.resetNewFile()
@ -76,19 +70,13 @@ export function Workspace (props: WorkspaceProps) {
await props.plugin.fileManager.closeAllFiles()
await props.plugin.createWorkspace(workspaceName)
await setWorkspace(workspaceName)
toast('New default workspace has been created.')
global.toast('New default workspace has been created.')
} catch (e) {
global.modal('Create Default Workspace', e.message, 'OK', onFinishRenameWorkspace, '')
console.error(e)
}
}
const toast = (message: string) => {
setState(prevState => {
return { ...prevState, toasterMsg: message }
})
}
/* workspace creation, renaming and deletion */
const renameCurrentWorkspace = () => {
@ -127,9 +115,7 @@ export function Workspace (props: WorkspaceProps) {
const workspaceName = workspaceCreateInput.current.value
try {
await props.plugin.fileManager.closeAllFiles()
await props.plugin.createWorkspace(workspaceName)
await setWorkspace(workspaceName)
await global.dispatchCreateWorkspace(workspaceName)
} catch (e) {
global.modal('Create Workspace', e.message, 'OK', () => {}, '')
console.error(e)
@ -152,20 +138,8 @@ export function Workspace (props: WorkspaceProps) {
})
}
const setWorkspace = async (name) => {
await props.plugin.fileManager.closeAllFiles()
if (name === LOCALHOST) {
props.plugin.workspace.clearWorkspace()
} else if (name === NO_WORKSPACE) {
props.plugin.workspace.clearWorkspace()
} else {
await props.plugin.workspace.setWorkspace(name)
}
await props.plugin.setWorkspace({ name, isLocalhost: name === LOCALHOST }, !(name === LOCALHOST || name === NO_WORKSPACE))
props.plugin.getWorkspaces()
setState(prevState => {
return { ...prevState, currentWorkspace: name }
})
const switchWorkspace = async (name: string) => {
global.dispatchSwitchToWorkspace(name)
}
const createModalMessage = () => {
@ -186,7 +160,6 @@ export function Workspace (props: WorkspaceProps) {
return (
<div className='remixui_container'>
<Toaster message={state.toasterMsg} />
<div className='remixui_fileexplorer' onClick={() => resetFocus(true)}>
<div>
<header>
@ -228,7 +201,7 @@ export function Workspace (props: WorkspaceProps) {
title='Delete'>
</span>
</span>
<select id="workspacesSelect" value={currentWorkspace} data-id="workspacesSelect" onChange={(e) => setWorkspace(e.target.value)} className="form-control custom-select">
<select id="workspacesSelect" value={currentWorkspace} data-id="workspacesSelect" onChange={(e) => switchWorkspace(e.target.value)} className="form-control custom-select">
{
global.fs.browser.workspaces
.map((folder, index) => {

@ -34,7 +34,6 @@ export interface WorkspaceState {
externalUploads: EventTarget & HTMLInputElement
uploadFileEvent: EventTarget & HTMLInputElement
loadingLocalhost: boolean
toasterMsg: string
}
export interface Modal {

Loading…
Cancel
Save