commit
1db0caf435
@ -0,0 +1,293 @@ |
||||
import React from 'react' |
||||
import { File } from '../types' |
||||
import { extractNameFromKey, extractParentFromKey } from '../utils' |
||||
|
||||
export const fetchDirectoryError = (error: any) => { |
||||
return { |
||||
type: 'FETCH_DIRECTORY_ERROR', |
||||
payload: error |
||||
} |
||||
} |
||||
|
||||
export const fetchDirectoryRequest = (promise: Promise<any>) => { |
||||
return { |
||||
type: 'FETCH_DIRECTORY_REQUEST', |
||||
payload: promise |
||||
} |
||||
} |
||||
|
||||
export const fetchDirectorySuccess = (path: string, files: File[]) => { |
||||
return { |
||||
type: 'FETCH_DIRECTORY_SUCCESS', |
||||
payload: { path, files } |
||||
} |
||||
} |
||||
|
||||
export const fileSystemReset = () => { |
||||
return { |
||||
type: 'FILESYSTEM_RESET' |
||||
} |
||||
} |
||||
|
||||
const normalize = (parent, filesList, newInputType?: string): any => { |
||||
const folders = {} |
||||
const files = {} |
||||
|
||||
Object.keys(filesList || {}).forEach(key => { |
||||
key = key.replace(/^\/|\/$/g, '') // remove first and last slash
|
||||
let path = key |
||||
path = path.replace(/^\/|\/$/g, '') // remove first and last slash
|
||||
|
||||
if (filesList[key].isDirectory) { |
||||
folders[extractNameFromKey(key)] = { |
||||
path, |
||||
name: extractNameFromKey(path), |
||||
isDirectory: filesList[key].isDirectory |
||||
} |
||||
} else { |
||||
files[extractNameFromKey(key)] = { |
||||
path, |
||||
name: extractNameFromKey(path), |
||||
isDirectory: filesList[key].isDirectory |
||||
} |
||||
} |
||||
}) |
||||
|
||||
if (newInputType === 'folder') { |
||||
const path = parent + '/blank' |
||||
|
||||
folders[path] = { |
||||
path: path, |
||||
name: '', |
||||
isDirectory: true |
||||
} |
||||
} else if (newInputType === 'file') { |
||||
const path = parent + '/blank' |
||||
|
||||
files[path] = { |
||||
path: path, |
||||
name: '', |
||||
isDirectory: false |
||||
} |
||||
} |
||||
|
||||
return Object.assign({}, folders, files) |
||||
} |
||||
|
||||
const fetchDirectoryContent = async (provider, folderPath: string, newInputType?: string): Promise<any> => { |
||||
return new Promise((resolve) => { |
||||
provider.resolveDirectory(folderPath, (error, fileTree) => { |
||||
if (error) console.error(error) |
||||
const files = normalize(folderPath, fileTree, newInputType) |
||||
|
||||
resolve({ [extractNameFromKey(folderPath)]: files }) |
||||
}) |
||||
}) |
||||
} |
||||
|
||||
export const fetchDirectory = (provider, path: string) => (dispatch: React.Dispatch<any>) => { |
||||
const promise = fetchDirectoryContent(provider, path) |
||||
|
||||
dispatch(fetchDirectoryRequest(promise)) |
||||
promise.then((files) => { |
||||
dispatch(fetchDirectorySuccess(path, files)) |
||||
}).catch((error) => { |
||||
dispatch(fetchDirectoryError({ error })) |
||||
}) |
||||
return promise |
||||
} |
||||
|
||||
export const resolveDirectoryError = (error: any) => { |
||||
return { |
||||
type: 'RESOLVE_DIRECTORY_ERROR', |
||||
payload: error |
||||
} |
||||
} |
||||
|
||||
export const resolveDirectoryRequest = (promise: Promise<any>) => { |
||||
return { |
||||
type: 'RESOLVE_DIRECTORY_REQUEST', |
||||
payload: promise |
||||
} |
||||
} |
||||
|
||||
export const resolveDirectorySuccess = (path: string, files: File[]) => { |
||||
return { |
||||
type: 'RESOLVE_DIRECTORY_SUCCESS', |
||||
payload: { path, files } |
||||
} |
||||
} |
||||
|
||||
export const resolveDirectory = (provider, path: string) => (dispatch: React.Dispatch<any>) => { |
||||
const promise = fetchDirectoryContent(provider, path) |
||||
|
||||
dispatch(resolveDirectoryRequest(promise)) |
||||
promise.then((files) => { |
||||
dispatch(resolveDirectorySuccess(path, files)) |
||||
}).catch((error) => { |
||||
dispatch(resolveDirectoryError({ error })) |
||||
}) |
||||
return promise |
||||
} |
||||
|
||||
export const fetchProviderError = (error: any) => { |
||||
return { |
||||
type: 'FETCH_PROVIDER_ERROR', |
||||
payload: error |
||||
} |
||||
} |
||||
|
||||
export const fetchProviderRequest = (promise: Promise<any>) => { |
||||
return { |
||||
type: 'FETCH_PROVIDER_REQUEST', |
||||
payload: promise |
||||
} |
||||
} |
||||
|
||||
export const fetchProviderSuccess = (provider: any) => { |
||||
return { |
||||
type: 'FETCH_PROVIDER_SUCCESS', |
||||
payload: provider |
||||
} |
||||
} |
||||
|
||||
export const fileAddedSuccess = (path: string, files) => { |
||||
return { |
||||
type: 'FILE_ADDED', |
||||
payload: { path, files } |
||||
} |
||||
} |
||||
|
||||
export const folderAddedSuccess = (path: string, files) => { |
||||
return { |
||||
type: 'FOLDER_ADDED', |
||||
payload: { path, files } |
||||
} |
||||
} |
||||
|
||||
export const fileRemovedSuccess = (path: string, removePath: string) => { |
||||
return { |
||||
type: 'FILE_REMOVED', |
||||
payload: { path, removePath } |
||||
} |
||||
} |
||||
|
||||
export const fileRenamedSuccess = (path: string, removePath: string, files) => { |
||||
return { |
||||
type: 'FILE_RENAMED', |
||||
payload: { path, removePath, files } |
||||
} |
||||
} |
||||
|
||||
export const init = (provider, workspaceName: string, plugin, registry) => (dispatch: React.Dispatch<any>) => { |
||||
if (provider) { |
||||
provider.event.register('fileAdded', async (filePath) => { |
||||
if (extractParentFromKey(filePath) === '/.workspaces') return |
||||
const path = extractParentFromKey(filePath) || provider.workspace || provider.type || '' |
||||
const data = await fetchDirectoryContent(provider, path) |
||||
|
||||
dispatch(fileAddedSuccess(path, data)) |
||||
if (filePath.includes('_test.sol')) { |
||||
plugin.event.trigger('newTestFileCreated', [filePath]) |
||||
} |
||||
}) |
||||
provider.event.register('folderAdded', async (folderPath) => { |
||||
if (extractParentFromKey(folderPath) === '/.workspaces') return |
||||
const path = extractParentFromKey(folderPath) || provider.workspace || provider.type || '' |
||||
const data = await fetchDirectoryContent(provider, path) |
||||
|
||||
dispatch(folderAddedSuccess(path, data)) |
||||
}) |
||||
provider.event.register('fileRemoved', async (removePath) => { |
||||
const path = extractParentFromKey(removePath) || provider.workspace || provider.type || '' |
||||
|
||||
dispatch(fileRemovedSuccess(path, removePath)) |
||||
}) |
||||
provider.event.register('fileRenamed', async (oldPath) => { |
||||
const path = extractParentFromKey(oldPath) || provider.workspace || provider.type || '' |
||||
const data = await fetchDirectoryContent(provider, path) |
||||
|
||||
dispatch(fileRenamedSuccess(path, oldPath, data)) |
||||
}) |
||||
provider.event.register('fileExternallyChanged', async (path: string, file: { content: string }) => { |
||||
const config = registry.get('config').api |
||||
const editor = registry.get('editor').api |
||||
|
||||
if (config.get('currentFile') === path && editor.currentContent() !== file.content) { |
||||
if (provider.isReadOnly(path)) return editor.setText(file.content) |
||||
dispatch(displayNotification( |
||||
path + ' changed', |
||||
'This file has been changed outside of Remix IDE.', |
||||
'Replace by the new content', 'Keep the content displayed in Remix', |
||||
() => { |
||||
editor.setText(file.content) |
||||
} |
||||
)) |
||||
} |
||||
}) |
||||
provider.event.register('fileRenamedError', async () => { |
||||
dispatch(displayNotification('File Renamed Failed', '', 'Ok', 'Cancel')) |
||||
}) |
||||
provider.event.register('rootFolderChanged', async () => { |
||||
workspaceName = provider.workspace || provider.type || '' |
||||
fetchDirectory(provider, workspaceName)(dispatch) |
||||
}) |
||||
dispatch(fetchProviderSuccess(provider)) |
||||
dispatch(setCurrentWorkspace(workspaceName)) |
||||
} else { |
||||
dispatch(fetchProviderError('No provider available')) |
||||
} |
||||
} |
||||
|
||||
export const setCurrentWorkspace = (name: string) => { |
||||
return { |
||||
type: 'SET_CURRENT_WORKSPACE', |
||||
payload: name |
||||
} |
||||
} |
||||
|
||||
export const addInputFieldSuccess = (path: string, files: File[]) => { |
||||
return { |
||||
type: 'ADD_INPUT_FIELD', |
||||
payload: { path, files } |
||||
} |
||||
} |
||||
|
||||
export const addInputField = (provider, type: string, path: string) => (dispatch: React.Dispatch<any>) => { |
||||
const promise = fetchDirectoryContent(provider, path, type) |
||||
|
||||
promise.then((files) => { |
||||
dispatch(addInputFieldSuccess(path, files)) |
||||
}).catch((error) => { |
||||
console.error(error) |
||||
}) |
||||
return promise |
||||
} |
||||
|
||||
export const removeInputFieldSuccess = (path: string) => { |
||||
return { |
||||
type: 'REMOVE_INPUT_FIELD', |
||||
payload: { path } |
||||
} |
||||
} |
||||
|
||||
export const removeInputField = (path: string) => (dispatch: React.Dispatch<any>) => { |
||||
return dispatch(removeInputFieldSuccess(path)) |
||||
} |
||||
|
||||
export const displayNotification = (title: string, message: string, labelOk: string, labelCancel: string, actionOk?: (...args) => void, actionCancel?: (...args) => void) => { |
||||
return { |
||||
type: 'DISPLAY_NOTIFICATION', |
||||
payload: { title, message, labelOk, labelCancel, actionOk, actionCancel } |
||||
} |
||||
} |
||||
|
||||
export const hideNotification = () => { |
||||
return { |
||||
type: 'DISPLAY_NOTIFICATION' |
||||
} |
||||
} |
||||
|
||||
export const closeNotificationModal = () => (dispatch: React.Dispatch<any>) => { |
||||
dispatch(hideNotification()) |
||||
} |
@ -0,0 +1,344 @@ |
||||
import * as _ from 'lodash' |
||||
import { extractNameFromKey } from '../utils' |
||||
interface Action { |
||||
type: string; |
||||
payload: Record<string, any>; |
||||
} |
||||
|
||||
export const fileSystemInitialState = { |
||||
files: { |
||||
files: [], |
||||
expandPath: [], |
||||
workspaceName: null, |
||||
blankPath: null, |
||||
isRequesting: false, |
||||
isSuccessful: false, |
||||
error: null |
||||
}, |
||||
provider: { |
||||
provider: null, |
||||
isRequesting: false, |
||||
isSuccessful: false, |
||||
error: null |
||||
}, |
||||
notification: { |
||||
title: null, |
||||
message: null, |
||||
actionOk: () => {}, |
||||
actionCancel: () => {}, |
||||
labelOk: null, |
||||
labelCancel: null |
||||
} |
||||
} |
||||
|
||||
export const fileSystemReducer = (state = fileSystemInitialState, action: Action) => { |
||||
switch (action.type) { |
||||
case 'FETCH_DIRECTORY_REQUEST': { |
||||
return { |
||||
...state, |
||||
files: { |
||||
...state.files, |
||||
isRequesting: true, |
||||
isSuccessful: false, |
||||
error: null |
||||
} |
||||
} |
||||
} |
||||
case 'FETCH_DIRECTORY_SUCCESS': { |
||||
return { |
||||
...state, |
||||
files: { |
||||
...state.files, |
||||
files: action.payload.files, |
||||
isRequesting: false, |
||||
isSuccessful: true, |
||||
error: null |
||||
} |
||||
} |
||||
} |
||||
case 'FETCH_DIRECTORY_ERROR': { |
||||
return { |
||||
...state, |
||||
files: { |
||||
...state.files, |
||||
isRequesting: false, |
||||
isSuccessful: false, |
||||
error: action.payload |
||||
} |
||||
} |
||||
} |
||||
case 'RESOLVE_DIRECTORY_REQUEST': { |
||||
return { |
||||
...state, |
||||
files: { |
||||
...state.files, |
||||
isRequesting: true, |
||||
isSuccessful: false, |
||||
error: null |
||||
} |
||||
} |
||||
} |
||||
case 'RESOLVE_DIRECTORY_SUCCESS': { |
||||
return { |
||||
...state, |
||||
files: { |
||||
...state.files, |
||||
files: resolveDirectory(state.files.workspaceName, action.payload.path, state.files.files, action.payload.files), |
||||
isRequesting: false, |
||||
isSuccessful: true, |
||||
error: null |
||||
} |
||||
} |
||||
} |
||||
case 'RESOLVE_DIRECTORY_ERROR': { |
||||
return { |
||||
...state, |
||||
files: { |
||||
...state.files, |
||||
isRequesting: false, |
||||
isSuccessful: false, |
||||
error: action.payload |
||||
} |
||||
} |
||||
} |
||||
case 'FETCH_PROVIDER_REQUEST': { |
||||
return { |
||||
...state, |
||||
provider: { |
||||
...state.provider, |
||||
isRequesting: true, |
||||
isSuccessful: false, |
||||
error: null |
||||
} |
||||
} |
||||
} |
||||
case 'FETCH_PROVIDER_SUCCESS': { |
||||
return { |
||||
...state, |
||||
provider: { |
||||
...state.provider, |
||||
provider: action.payload, |
||||
isRequesting: false, |
||||
isSuccessful: true, |
||||
error: null |
||||
} |
||||
} |
||||
} |
||||
case 'FETCH_PROVIDER_ERROR': { |
||||
return { |
||||
...state, |
||||
provider: { |
||||
...state.provider, |
||||
isRequesting: false, |
||||
isSuccessful: false, |
||||
error: action.payload |
||||
} |
||||
} |
||||
} |
||||
case 'SET_CURRENT_WORKSPACE': { |
||||
return { |
||||
...state, |
||||
files: { |
||||
...state.files, |
||||
workspaceName: action.payload |
||||
} |
||||
} |
||||
} |
||||
case 'ADD_INPUT_FIELD': { |
||||
return { |
||||
...state, |
||||
files: { |
||||
...state.files, |
||||
files: addInputField(state.files.workspaceName, action.payload.path, state.files.files, action.payload.files), |
||||
blankPath: action.payload.path, |
||||
isRequesting: false, |
||||
isSuccessful: true, |
||||
error: null |
||||
} |
||||
} |
||||
} |
||||
case 'REMOVE_INPUT_FIELD': { |
||||
return { |
||||
...state, |
||||
files: { |
||||
...state.files, |
||||
files: removeInputField(state.files.workspaceName, state.files.blankPath, state.files.files), |
||||
blankPath: null, |
||||
isRequesting: false, |
||||
isSuccessful: true, |
||||
error: null |
||||
} |
||||
} |
||||
} |
||||
case 'FILE_ADDED': { |
||||
return { |
||||
...state, |
||||
files: { |
||||
...state.files, |
||||
files: fileAdded(state.files.workspaceName, action.payload.path, state.files.files, action.payload.files), |
||||
expandPath: [...new Set([...state.files.expandPath, action.payload.path])], |
||||
isRequesting: false, |
||||
isSuccessful: true, |
||||
error: null |
||||
} |
||||
} |
||||
} |
||||
case 'FOLDER_ADDED': { |
||||
return { |
||||
...state, |
||||
files: { |
||||
...state.files, |
||||
files: folderAdded(state.files.workspaceName, action.payload.path, state.files.files, action.payload.files), |
||||
expandPath: [...new Set([...state.files.expandPath, action.payload.path])], |
||||
isRequesting: false, |
||||
isSuccessful: true, |
||||
error: null |
||||
} |
||||
} |
||||
} |
||||
case 'FILE_REMOVED': { |
||||
return { |
||||
...state, |
||||
files: { |
||||
...state.files, |
||||
files: fileRemoved(state.files.workspaceName, action.payload.path, action.payload.removePath, state.files.files), |
||||
isRequesting: false, |
||||
isSuccessful: true, |
||||
error: null |
||||
} |
||||
} |
||||
} |
||||
case 'FILE_RENAMED': { |
||||
return { |
||||
...state, |
||||
files: { |
||||
...state.files, |
||||
files: fileRenamed(state.files.workspaceName, action.payload.path, action.payload.removePath, state.files.files, action.payload.files), |
||||
isRequesting: false, |
||||
isSuccessful: true, |
||||
error: null |
||||
} |
||||
} |
||||
} |
||||
case 'DISPLAY_NOTIFICATION': { |
||||
return { |
||||
...state, |
||||
notification: { |
||||
title: action.payload.title, |
||||
message: action.payload.message, |
||||
actionOk: action.payload.actionOk || fileSystemInitialState.notification.actionOk, |
||||
actionCancel: action.payload.actionCancel || fileSystemInitialState.notification.actionCancel, |
||||
labelOk: action.payload.labelOk, |
||||
labelCancel: action.payload.labelCancel |
||||
} |
||||
} |
||||
} |
||||
case 'HIDE_NOTIFICATION': { |
||||
return { |
||||
...state, |
||||
notification: fileSystemInitialState.notification |
||||
} |
||||
} |
||||
default: |
||||
throw new Error() |
||||
} |
||||
} |
||||
|
||||
const resolveDirectory = (root, path: string, files, content) => { |
||||
if (path === root) return { [root]: { ...content[root], ...files[root] } } |
||||
const pathArr: string[] = path.split('/').filter(value => value) |
||||
|
||||
if (pathArr[0] !== root) pathArr.unshift(root) |
||||
const _path = pathArr.map((key, index) => index > 1 ? ['child', key] : key).reduce((acc: string[], cur) => { |
||||
return Array.isArray(cur) ? [...acc, ...cur] : [...acc, cur] |
||||
}, []) |
||||
|
||||
const prevFiles = _.get(files, _path) |
||||
|
||||
files = _.set(files, _path, { |
||||
isDirectory: true, |
||||
path, |
||||
name: extractNameFromKey(path), |
||||
child: { ...content[pathArr[pathArr.length - 1]], ...(prevFiles ? prevFiles.child : {}) } |
||||
}) |
||||
|
||||
return files |
||||
} |
||||
|
||||
const removePath = (root, path: string, pathName, files) => { |
||||
const pathArr: string[] = path.split('/').filter(value => value) |
||||
|
||||
if (pathArr[0] !== root) pathArr.unshift(root) |
||||
const _path = pathArr.map((key, index) => index > 1 ? ['child', key] : key).reduce((acc: string[], cur) => { |
||||
return Array.isArray(cur) ? [...acc, ...cur] : [...acc, cur] |
||||
}, []) |
||||
const prevFiles = _.get(files, _path) |
||||
|
||||
prevFiles && prevFiles.child && prevFiles.child[pathName] && delete prevFiles.child[pathName] |
||||
files = _.set(files, _path, { |
||||
isDirectory: true, |
||||
path, |
||||
name: extractNameFromKey(path), |
||||
child: prevFiles ? prevFiles.child : {} |
||||
}) |
||||
|
||||
return files |
||||
} |
||||
|
||||
const addInputField = (root, path: string, files, content) => { |
||||
if (path === root) return { [root]: { ...content[root], ...files[root] } } |
||||
const result = resolveDirectory(root, path, files, content) |
||||
|
||||
return result |
||||
} |
||||
|
||||
const removeInputField = (root, path: string, files) => { |
||||
if (path === root) { |
||||
delete files[root][path + '/' + 'blank'] |
||||
return files |
||||
} |
||||
return removePath(root, path, path + '/' + 'blank', files) |
||||
} |
||||
|
||||
const fileAdded = (root, path: string, files, content) => { |
||||
return resolveDirectory(root, path, files, content) |
||||
} |
||||
|
||||
const folderAdded = (root, path: string, files, content) => { |
||||
return resolveDirectory(root, path, files, content) |
||||
} |
||||
|
||||
const fileRemoved = (root, path: string, removedPath: string, files) => { |
||||
if (path === root) { |
||||
delete files[root][removedPath] |
||||
|
||||
return files |
||||
} |
||||
return removePath(root, path, extractNameFromKey(removedPath), files) |
||||
} |
||||
|
||||
const fileRenamed = (root, path: string, removePath: string, files, content) => { |
||||
if (path === root) { |
||||
const allFiles = { [root]: { ...content[root], ...files[root] } } |
||||
|
||||
delete allFiles[root][extractNameFromKey(removePath) || removePath] |
||||
return allFiles |
||||
} |
||||
const pathArr: string[] = path.split('/').filter(value => value) |
||||
|
||||
if (pathArr[0] !== root) pathArr.unshift(root) |
||||
const _path = pathArr.map((key, index) => index > 1 ? ['child', key] : key).reduce((acc: string[], cur) => { |
||||
return Array.isArray(cur) ? [...acc, ...cur] : [...acc, cur] |
||||
}, []) |
||||
const prevFiles = _.get(files, _path) |
||||
|
||||
delete prevFiles.child[extractNameFromKey(removePath)] |
||||
files = _.set(files, _path, { |
||||
isDirectory: true, |
||||
path, |
||||
name: extractNameFromKey(path), |
||||
child: { ...content[pathArr[pathArr.length - 1]], ...prevFiles.child } |
||||
}) |
||||
|
||||
return files |
||||
} |
@ -0,0 +1,13 @@ |
||||
export const extractNameFromKey = (key: string): string => { |
||||
const keyPath = key.split('/') |
||||
|
||||
return keyPath[keyPath.length - 1] |
||||
} |
||||
|
||||
export const extractParentFromKey = (key: string):string => { |
||||
if (!key) return |
||||
const keyPath = key.split('/') |
||||
keyPath.pop() |
||||
|
||||
return keyPath.join('/') |
||||
} |
Loading…
Reference in new issue