Merge branch 'master' into refactoring-static-analyser

pull/1104/head
David Zagi 4 years ago committed by GitHub
commit bb2ca8fe7b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 6
      apps/remix-ide-e2e/src/commands/addFile.ts
  2. 9
      apps/remix-ide-e2e/src/helpers/init.ts
  3. 12
      apps/remix-ide-e2e/src/tests/fileExplorer.test.ts
  4. 6
      apps/remix-ide-e2e/src/tests/gist.spec.ts
  5. 3
      apps/remix-ide-e2e/src/tests/solidityUnittests.spec.ts
  6. 2
      apps/remix-ide-e2e/src/tests/url.spec.ts
  7. 6
      apps/remix-ide/src/app/compiler/compiler-imports.js
  8. 5
      apps/remix-ide/src/app/editor/contextView.js
  9. 5
      apps/remix-ide/src/app/files/fileManager.js
  10. 4
      apps/remix-ide/src/app/files/fileProvider.js
  11. 9
      apps/remix-ide/src/app/files/remixDProvider.js
  12. 2
      apps/remix-ide/src/app/files/workspaceFileProvider.js
  13. 19
      apps/remix-ide/src/app/panels/file-panel.js
  14. 4
      apps/remix-ide/src/app/tabs/testTab/testTab.js
  15. 5
      apps/remix-ide/src/app/ui/renderer.js
  16. 35
      apps/remix-ide/src/lib/helper.js
  17. 293
      libs/remix-ui/file-explorer/src/lib/actions/fileSystem.ts
  18. 429
      libs/remix-ui/file-explorer/src/lib/file-explorer.tsx
  19. 344
      libs/remix-ui/file-explorer/src/lib/reducers/fileSystem.ts
  20. 2
      libs/remix-ui/file-explorer/src/lib/types/index.ts
  21. 13
      libs/remix-ui/file-explorer/src/lib/utils/index.ts
  22. 73
      libs/remixd/src/services/remixdClient.ts
  23. 1
      package.json

@ -18,9 +18,9 @@ function addFile (browser: NightwatchBrowser, name: string, content: NightwatchC
.clickLaunchIcon('filePanel') .clickLaunchIcon('filePanel')
.click('li[data-id="treeViewLitreeViewItemREADME.txt"]') // focus on root directory .click('li[data-id="treeViewLitreeViewItemREADME.txt"]') // focus on root directory
.click('.newFile') .click('.newFile')
.waitForElementContainsText('*[data-id="treeViewLitreeViewItem/blank"]', '', 60000) .waitForElementContainsText('*[data-id$="/blank"]', '', 60000)
.sendKeys('*[data-id="treeViewLitreeViewItem/blank"] .remixui_items', name) .sendKeys('*[data-id$="/blank"] .remixui_items', name)
.sendKeys('*[data-id="treeViewLitreeViewItem/blank"] .remixui_items', browser.Keys.ENTER) .sendKeys('*[data-id$="/blank"] .remixui_items', browser.Keys.ENTER)
.pause(2000) .pause(2000)
.waitForElementVisible(`li[data-id="treeViewLitreeViewItem${name}"]`, 60000) .waitForElementVisible(`li[data-id="treeViewLitreeViewItem${name}"]`, 60000)
.setEditorValue(content.content) .setEditorValue(content.content)

@ -2,11 +2,18 @@ import { NightwatchBrowser } from 'nightwatch'
require('dotenv').config() require('dotenv').config()
export default function (browser: NightwatchBrowser, callback: VoidFunction, url?: string, preloadPlugins = true): void { export default function (browser: NightwatchBrowser, callback: VoidFunction, url?: string, preloadPlugins = true, closeWorkspaceAlert = true): void {
browser browser
.url(url || 'http://127.0.0.1:8080') .url(url || 'http://127.0.0.1:8080')
.pause(5000) .pause(5000)
.switchBrowserTab(0) .switchBrowserTab(0)
.perform((done) => {
if (closeWorkspaceAlert) {
browser.waitForElementVisible('*[data-id="modalDialogModalBody"]', 60000)
.modalFooterOKClick()
}
done()
})
.fullscreenWindow(() => { .fullscreenWindow(() => {
if (preloadPlugins) { if (preloadPlugins) {
initModules(browser, () => { initModules(browser, () => {

@ -22,9 +22,9 @@ module.exports = {
.click('li[data-id="treeViewLitreeViewItemREADME.txt"]') // focus on root directory .click('li[data-id="treeViewLitreeViewItemREADME.txt"]') // focus on root directory
.click('*[data-id="fileExplorerNewFilecreateNewFile"]') .click('*[data-id="fileExplorerNewFilecreateNewFile"]')
.pause(1000) .pause(1000)
.waitForElementVisible('*[data-id="treeViewLitreeViewItem/blank"]') .waitForElementVisible('*[data-id$="/blank"]')
.sendKeys('*[data-id="treeViewLitreeViewItem/blank"] .remixui_items', '5_New_contract.sol') .sendKeys('*[data-id$="/blank"] .remixui_items', '5_New_contract.sol')
.sendKeys('*[data-id="treeViewLitreeViewItem/blank"] .remixui_items', browser.Keys.ENTER) .sendKeys('*[data-id$="/blank"] .remixui_items', browser.Keys.ENTER)
.waitForElementVisible('*[data-id="treeViewLitreeViewItem5_New_contract.sol"]', 7000) .waitForElementVisible('*[data-id="treeViewLitreeViewItem5_New_contract.sol"]', 7000)
}, },
@ -49,9 +49,9 @@ module.exports = {
.click('li[data-id="treeViewLitreeViewItemREADME.txt"]') // focus on root directory .click('li[data-id="treeViewLitreeViewItemREADME.txt"]') // focus on root directory
.click('[data-id="fileExplorerNewFilecreateNewFolder"]') .click('[data-id="fileExplorerNewFilecreateNewFolder"]')
.pause(1000) .pause(1000)
.waitForElementVisible('*[data-id="treeViewLitreeViewItem/blank"]') .waitForElementVisible('*[data-id$="/blank"]')
.sendKeys('*[data-id="treeViewLitreeViewItem/blank"] .remixui_items', 'Browser_Tests') .sendKeys('*[data-id$="/blank"] .remixui_items', 'Browser_Tests')
.sendKeys('*[data-id="treeViewLitreeViewItem/blank"] .remixui_items', browser.Keys.ENTER) .sendKeys('*[data-id$="/blank"] .remixui_items', browser.Keys.ENTER)
.waitForElementVisible('*[data-id="treeViewLitreeViewItemBrowser_Tests"]') .waitForElementVisible('*[data-id="treeViewLitreeViewItemBrowser_Tests"]')
}, },

@ -29,9 +29,9 @@ module.exports = {
.waitForElementVisible('*[data-id="fileExplorerNewFilecreateNewFolder"]') .waitForElementVisible('*[data-id="fileExplorerNewFilecreateNewFolder"]')
.click('[data-id="fileExplorerNewFilecreateNewFolder"]') .click('[data-id="fileExplorerNewFilecreateNewFolder"]')
.pause(1000) .pause(1000)
.waitForElementVisible('*[data-id="treeViewLitreeViewItem/blank"]') .waitForElementVisible('*[data-id$="/blank"]')
.sendKeys('*[data-id="treeViewLitreeViewItem/blank"] .remixui_items', 'Browser_Tests') .sendKeys('*[data-id$="/blank"] .remixui_items', 'Browser_Tests')
.sendKeys('*[data-id="treeViewLitreeViewItem/blank"] .remixui_items', browser.Keys.ENTER) .sendKeys('*[data-id$="/blank"] .remixui_items', browser.Keys.ENTER)
.waitForElementVisible('*[data-id="treeViewLitreeViewItemBrowser_Tests"]') .waitForElementVisible('*[data-id="treeViewLitreeViewItemBrowser_Tests"]')
.addFile('File.sol', { content: '' }) .addFile('File.sol', { content: '' })
.click('*[data-id="fileExplorerNewFilepublishToGist"]') .click('*[data-id="fileExplorerNewFilepublishToGist"]')

@ -179,9 +179,8 @@ function runTests (browser: NightwatchBrowser) {
.click('*[data-id="treeViewLitreeViewItemcontracts"]') .click('*[data-id="treeViewLitreeViewItemcontracts"]')
.openFile('contracts/3_Ballot.sol') .openFile('contracts/3_Ballot.sol')
.clickLaunchIcon('solidityUnitTesting') .clickLaunchIcon('solidityUnitTesting')
.pause(500)
.setValue('*[data-id="uiPathInput"]', 'tests')
.pause(2000) .pause(2000)
.verify.attributeEquals('*[data-id="uiPathInput"]', 'value', 'tests')
.scrollAndClick('#runTestsTabRunAction') .scrollAndClick('#runTestsTabRunAction')
.waitForElementVisible('*[data-id="testTabSolidityUnitTestsOutputheader"]', 120000) .waitForElementVisible('*[data-id="testTabSolidityUnitTestsOutputheader"]', 120000)
.waitForElementPresent('#solidityUnittestsOutput div[class^="testPass"]', 60000) .waitForElementPresent('#solidityUnittestsOutput div[class^="testPass"]', 60000)

@ -10,7 +10,7 @@ const sources = [
module.exports = { module.exports = {
before: function (browser: NightwatchBrowser, done: VoidFunction) { before: function (browser: NightwatchBrowser, done: VoidFunction) {
init(browser, done, 'http://127.0.0.1:8080/#optimize=true&runs=300&evmVersion=istanbul&version=soljson-v0.7.4+commit.3f05b770.js&code=cHJhZ21hIHNvbGlkaXR5ID49MC42LjAgPDAuNy4wOwoKaW1wb3J0ICJodHRwczovL2dpdGh1Yi5jb20vT3BlblplcHBlbGluL29wZW56ZXBwZWxpbi1jb250cmFjdHMvYmxvYi9tYXN0ZXIvY29udHJhY3RzL2FjY2Vzcy9Pd25hYmxlLnNvbCI7Cgpjb250cmFjdCBHZXRQYWlkIGlzIE93bmFibGUgewogIGZ1bmN0aW9uIHdpdGhkcmF3KCkgZXh0ZXJuYWwgb25seU93bmVyIHsKICB9Cn0') init(browser, done, 'http://127.0.0.1:8080/#optimize=true&runs=300&evmVersion=istanbul&version=soljson-v0.7.4+commit.3f05b770.js&code=cHJhZ21hIHNvbGlkaXR5ID49MC42LjAgPDAuNy4wOwoKaW1wb3J0ICJodHRwczovL2dpdGh1Yi5jb20vT3BlblplcHBlbGluL29wZW56ZXBwZWxpbi1jb250cmFjdHMvYmxvYi9tYXN0ZXIvY29udHJhY3RzL2FjY2Vzcy9Pd25hYmxlLnNvbCI7Cgpjb250cmFjdCBHZXRQYWlkIGlzIE93bmFibGUgewogIGZ1bmN0aW9uIHdpdGhkcmF3KCkgZXh0ZXJuYWwgb25seU93bmVyIHsKICB9Cn0', true, false)
}, },
'@sources': function () { '@sources': function () {

@ -121,9 +121,7 @@ module.exports = class CompilerImports extends Plugin {
if (provider.type === 'localhost' && !provider.isConnected()) { if (provider.type === 'localhost' && !provider.isConnected()) {
return reject(new Error(`file provider ${provider.type} not available while trying to resolve ${url}`)) return reject(new Error(`file provider ${provider.type} not available while trying to resolve ${url}`))
} }
provider.exists(url, (error, exist) => { provider.exists(url).then(exist => {
if (error) return reject(error)
/* /*
if the path is absolute and the file does not exist, we can stop here if the path is absolute and the file does not exist, we can stop here
Doesn't make sense to try to resolve "localhost/node_modules/localhost/node_modules/<path>" and we'll end in an infinite loop. Doesn't make sense to try to resolve "localhost/node_modules/localhost/node_modules/<path>" and we'll end in an infinite loop.
@ -162,6 +160,8 @@ module.exports = class CompilerImports extends Plugin {
if (error) return reject(error) if (error) return reject(error)
resolve(content) resolve(content)
}) })
}).catch(error => {
return reject(error)
}) })
} }
}) })

@ -109,10 +109,11 @@ class ContextView {
if (filename !== this._deps.config.get('currentFile')) { if (filename !== this._deps.config.get('currentFile')) {
const provider = this._deps.fileManager.fileProviderOf(filename) const provider = this._deps.fileManager.fileProviderOf(filename)
if (provider) { if (provider) {
provider.exists(filename, (error, exist) => { provider.exists(filename).then(exist => {
if (error) return console.log(error)
this._deps.fileManager.open(filename) this._deps.fileManager.open(filename)
jumpToLine(lineColumn) jumpToLine(lineColumn)
}).catch(error => {
if (error) return console.log(error)
}) })
} }
} else { } else {

@ -121,10 +121,7 @@ class FileManager extends Plugin {
try { try {
path = this.limitPluginScope(path) path = this.limitPluginScope(path)
const provider = this.fileProviderOf(path) const provider = this.fileProviderOf(path)
const result = provider.exists(path, (err, result) => { const result = provider.exists(path)
if (err) return false
return result
})
return result return result
} catch (e) { } catch (e) {

@ -63,11 +63,11 @@ class FileProvider {
}) })
} }
exists (path, cb) { async exists (path) {
// todo check the type (directory/file) as well #2386 // todo check the type (directory/file) as well #2386
// currently it is not possible to have a file and folder with same path // currently it is not possible to have a file and folder with same path
const ret = this._exists(path) const ret = this._exists(path)
if (cb) cb(null, ret)
return ret return ret
} }

@ -74,16 +74,15 @@ module.exports = class RemixDProvider extends FileProvider {
}) })
} }
exists (path, cb) { exists (path) {
if (!this._isReady) return cb && cb('provider not ready') if (!this._isReady) throw new Error('provider not ready')
const unprefixedpath = this.removePrefix(path) const unprefixedpath = this.removePrefix(path)
return this._appManager.call('remixd', 'exists', { path: unprefixedpath }) return this._appManager.call('remixd', 'exists', { path: unprefixedpath })
.then((result) => { .then((result) => {
if (cb) return cb(null, result)
return result return result
}).catch((error) => { })
if (cb) return cb(error) .catch((error) => {
throw new Error(error) throw new Error(error)
}) })
} }

@ -33,7 +33,7 @@ class WorkspaceFileProvider extends FileProvider {
if (!this.workspace) this.createWorkspace() if (!this.workspace) this.createWorkspace()
path = path.replace(/^\/|\/$/g, '') // remove first and last slash path = path.replace(/^\/|\/$/g, '') // remove first and last slash
if (path.startsWith(this.workspacesPath + '/' + this.workspace)) return path if (path.startsWith(this.workspacesPath + '/' + this.workspace)) return path
if (path.startsWith(this.workspace)) return this.workspacesPath + '/' + this.workspace if (path.startsWith(this.workspace)) return path.replace(this.workspace, this.workspacesPath + '/' + this.workspace)
path = super.removePrefix(path) path = super.removePrefix(path)
let ret = this.workspacesPath + '/' + this.workspace + '/' + (path === '/' ? '' : path) let ret = this.workspacesPath + '/' + this.workspace + '/' + (path === '/' ? '' : path)

@ -188,8 +188,11 @@ module.exports = class Filepanel extends ViewPlugin {
const browserProvider = this._deps.fileProviders.browser const browserProvider = this._deps.fileProviders.browser
const workspacePath = 'browser/' + workspaceProvider.workspacesPath + '/' + name const workspacePath = 'browser/' + workspaceProvider.workspacesPath + '/' + name
const workspaceRootPath = 'browser/' + workspaceProvider.workspacesPath const workspaceRootPath = 'browser/' + workspaceProvider.workspacesPath
if (!browserProvider.exists(workspaceRootPath)) browserProvider.createDir(workspaceRootPath) const workspaceRootPathExists = await browserProvider.exists(workspaceRootPath)
if (!browserProvider.exists(workspacePath)) browserProvider.createDir(workspacePath) const workspacePathExists = await browserProvider.exists(workspacePath)
if (!workspaceRootPathExists) browserProvider.createDir(workspaceRootPath)
if (!workspacePathExists) browserProvider.createDir(workspacePath)
} }
async workspaceExists (name) { async workspaceExists (name) {
@ -209,11 +212,13 @@ module.exports = class Filepanel extends ViewPlugin {
workspaceProvider.setWorkspace(workspaceName) workspaceProvider.setWorkspace(workspaceName)
await this.request.setWorkspace(workspaceName) // tells the react component to switch to that workspace await this.request.setWorkspace(workspaceName) // tells the react component to switch to that workspace
for (const file in examples) { for (const file in examples) {
try { setTimeout(async () => { // space creation of files to give react ui time to update.
await workspaceProvider.set(examples[file].name, examples[file].content) try {
} catch (error) { await workspaceProvider.set(examples[file].name, examples[file].content)
console.error(error) } catch (error) {
} console.error(error)
}
}, 10)
} }
} }
} }

@ -18,7 +18,9 @@ class TestTabLogic {
// Checking to ignore the value which contains only whitespaces // Checking to ignore the value which contains only whitespaces
if (!path || !(/\S/.test(path))) return if (!path || !(/\S/.test(path))) return
const fileProvider = this.fileManager.fileProviderOf(path.split('/')[0]) const fileProvider = this.fileManager.fileProviderOf(path.split('/')[0])
fileProvider.exists(path, (e, res) => { if (!res) fileProvider.createDir(path) }) fileProvider.exists(path).then(res => {
if (!res) fileProvider.createDir(path)
})
} }
pathExists (path) { pathExists (path) {

@ -39,10 +39,11 @@ Renderer.prototype._errorClick = function (errFile, errLine, errCol) {
// TODO: refactor with this._components.contextView.jumpTo // TODO: refactor with this._components.contextView.jumpTo
var provider = self._deps.fileManager.fileProviderOf(errFile) var provider = self._deps.fileManager.fileProviderOf(errFile)
if (provider) { if (provider) {
provider.exists(errFile, (error, exist) => { provider.exists(errFile).then(exist => {
if (error) return console.log(error)
self._deps.fileManager.open(errFile) self._deps.fileManager.open(errFile)
editor.gotoLine(errLine, errCol) editor.gotoLine(errLine, errCol)
}).catch(error => {
if (error) return console.log(error)
}) })
} }
} else { } else {

@ -36,14 +36,12 @@ module.exports = {
async.whilst( async.whilst(
() => { return exist }, () => { return exist },
(callback) => { (callback) => {
fileProvider.exists(name + counter + prefix + '.' + ext, (error, currentExist) => { fileProvider.exists(name + counter + prefix + '.' + ext).then(currentExist => {
if (error) { exist = currentExist
callback(error) if (exist) counter = (counter | 0) + 1
} else { callback()
exist = currentExist }).catch(error => {
if (exist) counter = (counter | 0) + 1 if (error) console.log(error)
callback()
}
}) })
}, },
(error) => { cb(error, name + counter + prefix + '.' + ext) } (error) => { cb(error, name + counter + prefix + '.' + ext) }
@ -52,6 +50,27 @@ module.exports = {
createNonClashingName (name, fileProvider, cb) { createNonClashingName (name, fileProvider, cb) {
this.createNonClashingNameWithPrefix(name, fileProvider, '', cb) this.createNonClashingNameWithPrefix(name, fileProvider, '', cb)
}, },
async createNonClashingNameAsync (name, fileManager, prefix = '') {
if (!name) name = 'Undefined'
let counter = ''
let ext = 'sol'
const reg = /(.*)\.([^.]+)/g
const split = reg.exec(name)
if (split) {
name = split[1]
ext = split[2]
}
let exist = true
do {
const isDuplicate = await fileManager.exists(name + counter + prefix + '.' + ext)
if (isDuplicate) counter = (counter | 0) + 1
else exist = false
} while (exist)
return name + counter + prefix + '.' + ext
},
checkSpecialChars (name) { checkSpecialChars (name) {
return name.match(/[:*?"<>\\'|]/) != null return name.match(/[:*?"<>\\'|]/) != null
}, },

@ -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())
}

@ -1,4 +1,4 @@
import React, { useEffect, useState, useRef } from 'react' // eslint-disable-line import React, { useEffect, useState, useRef, useReducer } from 'react' // eslint-disable-line
// import { DragDropContext, Droppable, Draggable } from 'react-beautiful-dnd' // eslint-disable-line // import { DragDropContext, Droppable, Draggable } from 'react-beautiful-dnd' // eslint-disable-line
import { TreeView, TreeViewItem } from '@remix-ui/tree-view' // eslint-disable-line import { TreeView, TreeViewItem } from '@remix-ui/tree-view' // eslint-disable-line
import { ModalDialog } from '@remix-ui/modal-dialog' // eslint-disable-line import { ModalDialog } from '@remix-ui/modal-dialog' // eslint-disable-line
@ -7,6 +7,8 @@ import Gists from 'gists'
import { FileExplorerMenu } from './file-explorer-menu' // eslint-disable-line import { FileExplorerMenu } from './file-explorer-menu' // eslint-disable-line
import { FileExplorerContextMenu } from './file-explorer-context-menu' // eslint-disable-line import { FileExplorerContextMenu } from './file-explorer-context-menu' // eslint-disable-line
import { FileExplorerProps, File } from './types' import { FileExplorerProps, File } from './types'
import { fileSystemReducer, fileSystemInitialState } from './reducers/fileSystem'
import { fetchDirectory, init, resolveDirectory, addInputField, removeInputField } from './actions/fileSystem'
import * as helper from '../../../../../apps/remix-ide/src/lib/helper' import * as helper from '../../../../../apps/remix-ide/src/lib/helper'
import QueryParams from '../../../../../apps/remix-ide/src/lib/query-params' import QueryParams from '../../../../../apps/remix-ide/src/lib/query-params'
@ -15,7 +17,7 @@ import './css/file-explorer.css'
const queryParams = new QueryParams() const queryParams = new QueryParams()
export const FileExplorer = (props: FileExplorerProps) => { export const FileExplorer = (props: FileExplorerProps) => {
const { filesProvider, name, registry, plugin, focusRoot, contextMenuItems, displayInput, externalUploads } = props const { name, registry, plugin, focusRoot, contextMenuItems, displayInput, externalUploads } = props
const [state, setState] = useState({ const [state, setState] = useState({
focusElement: [{ focusElement: [{
key: '', key: '',
@ -24,10 +26,51 @@ export const FileExplorer = (props: FileExplorerProps) => {
focusPath: null, focusPath: null,
files: [], files: [],
fileManager: null, fileManager: null,
filesProvider,
ctrlKey: false, ctrlKey: false,
newFileName: '', newFileName: '',
actions: [], actions: [{
id: 'newFile',
name: 'New File',
type: ['folder'],
path: [],
extension: [],
pattern: []
}, {
id: 'newFolder',
name: 'New Folder',
type: ['folder'],
path: [],
extension: [],
pattern: []
}, {
id: 'rename',
name: 'Rename',
type: ['file', 'folder'],
path: [],
extension: [],
pattern: []
}, {
id: 'delete',
name: 'Delete',
type: ['file', 'folder'],
path: [],
extension: [],
pattern: []
}, {
id: 'pushChangesToGist',
name: 'Push changes to gist',
type: [],
path: [],
extension: [],
pattern: ['^browser/gists/([0-9]|[a-z])*$']
}, {
id: 'run',
name: 'Run',
type: [],
path: [],
extension: ['.js'],
pattern: []
}],
focusContext: { focusContext: {
element: null, element: null,
x: null, x: null,
@ -60,8 +103,43 @@ export const FileExplorer = (props: FileExplorerProps) => {
mouseOverElement: null, mouseOverElement: null,
showContextMenu: false showContextMenu: false
}) })
const [fileSystem, dispatch] = useReducer(fileSystemReducer, fileSystemInitialState)
const editRef = useRef(null) const editRef = useRef(null)
useEffect(() => {
if (props.filesProvider) {
init(props.filesProvider, props.name, props.plugin, props.registry)(dispatch)
}
}, [props.filesProvider, props.name])
useEffect(() => {
const provider = fileSystem.provider.provider
if (provider) {
fetchDirectory(provider, props.name)(dispatch)
}
}, [fileSystem.provider.provider, props.name])
useEffect(() => {
if (fileSystem.notification.message) {
modal(fileSystem.notification.title, fileSystem.notification.message, {
label: fileSystem.notification.labelOk,
fn: fileSystem.notification.actionOk
}, {
label: fileSystem.notification.labelCancel,
fn: fileSystem.notification.actionCancel
})
}
}, [fileSystem.notification.message])
useEffect(() => {
if (fileSystem.files.expandPath.length > 0) {
setState(prevState => {
return { ...prevState, expandPath: [...new Set([...prevState.expandPath, ...fileSystem.files.expandPath])] }
})
}
}, [fileSystem.files.expandPath])
useEffect(() => { useEffect(() => {
if (state.focusEdit.element) { if (state.focusEdit.element) {
setTimeout(() => { setTimeout(() => {
@ -75,95 +153,13 @@ export const FileExplorer = (props: FileExplorerProps) => {
useEffect(() => { useEffect(() => {
(async () => { (async () => {
const fileManager = registry.get('filemanager').api const fileManager = registry.get('filemanager').api
const files = await fetchDirectoryContent(name)
const actions = [{
id: 'newFile',
name: 'New File',
type: ['folder'],
path: [],
extension: [],
pattern: []
}, {
id: 'newFolder',
name: 'New Folder',
type: ['folder'],
path: [],
extension: [],
pattern: []
}, {
id: 'rename',
name: 'Rename',
type: ['file', 'folder'],
path: [],
extension: [],
pattern: []
}, {
id: 'delete',
name: 'Delete',
type: ['file', 'folder'],
path: [],
extension: [],
pattern: []
}, {
id: 'pushChangesToGist',
name: 'Push changes to gist',
type: [],
path: [],
extension: [],
pattern: ['^browser/gists/([0-9]|[a-z])*$']
}, {
id: 'run',
name: 'Run',
type: [],
path: [],
extension: ['.js'],
pattern: []
}]
setState(prevState => { setState(prevState => {
return { ...prevState, fileManager, files, actions, expandPath: [name] } return { ...prevState, fileManager, expandPath: [name] }
}) })
})() })()
}, [name]) }, [name])
useEffect(() => {
if (state.fileManager) {
filesProvider.event.register('fileExternallyChanged', fileExternallyChanged)
filesProvider.event.register('fileRenamedError', fileRenamedError)
filesProvider.event.register('rootFolderChanged', rootFolderChanged)
}
}, [state.fileManager])
useEffect(() => {
const { expandPath } = state
const expandFn = async () => {
let files = state.files
for (let i = 0; i < expandPath.length; i++) {
files = await resolveDirectory(expandPath[i], files)
await setState(prevState => {
return { ...prevState, files }
})
}
}
if (expandPath && expandPath.length > 0) {
expandFn()
}
}, [state.expandPath])
useEffect(() => {
// unregister event to update state in callback
if (filesProvider.event.registered.fileAdded) filesProvider.event.unregister('fileAdded', fileAdded)
if (filesProvider.event.registered.folderAdded) filesProvider.event.unregister('folderAdded', folderAdded)
if (filesProvider.event.registered.fileRemoved) filesProvider.event.unregister('fileRemoved', fileRemoved)
if (filesProvider.event.registered.fileRenamed) filesProvider.event.unregister('fileRenamed', fileRenamed)
filesProvider.event.register('fileAdded', fileAdded)
filesProvider.event.register('folderAdded', folderAdded)
filesProvider.event.register('fileRemoved', fileRemoved)
filesProvider.event.register('fileRenamed', fileRenamed)
}, [state.files])
useEffect(() => { useEffect(() => {
if (focusRoot) { if (focusRoot) {
setState(prevState => { setState(prevState => {
@ -220,82 +216,6 @@ export const FileExplorer = (props: FileExplorerProps) => {
} }
}, [state.modals]) }, [state.modals])
const resolveDirectory = async (folderPath, dir: File[], isChild = false): Promise<File[]> => {
if (!isChild && (state.focusEdit.element === '/blank') && state.focusEdit.isNew && (dir.findIndex(({ path }) => path === '/blank') === -1)) {
dir = state.focusEdit.type === 'file' ? [...dir, {
path: state.focusEdit.element,
name: '',
isDirectory: false
}] : [{
path: state.focusEdit.element,
name: '',
isDirectory: true
}, ...dir]
}
dir = await Promise.all(dir.map(async (file) => {
if (file.path === folderPath) {
if ((extractParentFromKey(state.focusEdit.element) === folderPath) && state.focusEdit.isNew) {
file.child = state.focusEdit.type === 'file' ? [...await fetchDirectoryContent(folderPath), {
path: state.focusEdit.element,
name: '',
isDirectory: false
}] : [{
path: state.focusEdit.element,
name: '',
isDirectory: true
}, ...await fetchDirectoryContent(folderPath)]
} else {
file.child = await fetchDirectoryContent(folderPath)
}
return file
} else if (file.child) {
file.child = await resolveDirectory(folderPath, file.child, true)
return file
} else {
return file
}
}))
return dir
}
const fetchDirectoryContent = async (folderPath: string): Promise<File[]> => {
return new Promise((resolve) => {
filesProvider.resolveDirectory(folderPath, (_error, fileTree) => {
const files = normalize(fileTree)
resolve(files)
})
})
}
const normalize = (filesList): File[] => {
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.push({
path,
name: extractNameFromKey(path),
isDirectory: filesList[key].isDirectory
})
} else {
files.push({
path,
name: extractNameFromKey(path),
isDirectory: filesList[key].isDirectory
})
}
})
return [...folders, ...files]
}
const extractNameFromKey = (key: string):string => { const extractNameFromKey = (key: string):string => {
const keyPath = key.split('/') const keyPath = key.split('/')
@ -310,29 +230,23 @@ export const FileExplorer = (props: FileExplorerProps) => {
return keyPath.join('/') return keyPath.join('/')
} }
const createNewFile = (newFilePath: string) => { const createNewFile = async (newFilePath: string) => {
const fileManager = state.fileManager const fileManager = state.fileManager
try { try {
helper.createNonClashingName(newFilePath, filesProvider, async (error, newName) => { const newName = await helper.createNonClashingNameAsync(newFilePath, fileManager)
if (error) { const createFile = await fileManager.writeFile(newName, '')
modal('Create File Failed', error, {
label: 'Close',
fn: async () => {}
}, null)
} else {
const createFile = await fileManager.writeFile(newName, '')
if (!createFile) { if (!createFile) {
return toast('Failed to create file ' + newName) return toast('Failed to create file ' + newName)
} else { } else {
await fileManager.open(newName) const path = newName.indexOf(props.name + '/') === 0 ? newName.replace(props.name + '/', '') : newName
setState(prevState => {
return { ...prevState, focusElement: [{ key: newName, type: 'file' }] } await fileManager.open(path)
}) setState(prevState => {
} return { ...prevState, focusElement: [{ key: newName, type: 'file' }] }
} })
}) }
} catch (error) { } catch (error) {
return modal('File Creation Failed', typeof error === 'string' ? error : error.message, { return modal('File Creation Failed', typeof error === 'string' ? error : error.message, {
label: 'Close', label: 'Close',
@ -367,6 +281,8 @@ export const FileExplorer = (props: FileExplorerProps) => {
} }
const deletePath = async (path: string) => { const deletePath = async (path: string) => {
const filesProvider = fileSystem.provider.provider
if (filesProvider.isReadOnly(path)) { if (filesProvider.isReadOnly(path)) {
return toast('cannot delete file. ' + name + ' is a read only explorer') return toast('cannot delete file. ' + name + ' is a read only explorer')
} }
@ -410,106 +326,8 @@ export const FileExplorer = (props: FileExplorerProps) => {
} }
} }
const removePath = (path: string, files: File[]): File[] => {
return files.map(file => {
if (file.path === path) {
return null
} else if (file.child) {
const childFiles = removePath(path, file.child)
file.child = childFiles.filter(file => file)
return file
} else {
return file
}
})
}
const fileAdded = async (filePath: string) => {
const pathArr = filePath.split('/')
const expandPath = pathArr.map((path, index) => {
return [...pathArr.slice(0, index)].join('/')
}).filter(path => path && (path !== props.name))
const files = await fetchDirectoryContent(props.name)
setState(prevState => {
const uniquePaths = [...new Set([...prevState.expandPath, ...expandPath])]
return { ...prevState, files, expandPath: uniquePaths }
})
if (filePath.includes('_test.sol')) {
plugin.event.trigger('newTestFileCreated', [filePath])
}
}
const folderAdded = async (folderPath: string) => {
const pathArr = folderPath.split('/')
const expandPath = pathArr.map((path, index) => {
return [...pathArr.slice(0, index)].join('/')
}).filter(path => path && (path !== props.name))
const files = await fetchDirectoryContent(props.name)
setState(prevState => {
const uniquePaths = [...new Set([...prevState.expandPath, ...expandPath])]
return { ...prevState, files, expandPath: uniquePaths }
})
}
const fileExternallyChanged = (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 (filesProvider.isReadOnly(path)) return editor.setText(file.content)
modal(path + ' changed', 'This file has been changed outside of Remix IDE.', {
label: 'Replace by the new content',
fn: () => {
editor.setText(file.content)
}
}, {
label: 'Keep the content displayed in Remix',
fn: () => {}
})
}
}
const fileRemoved = (filePath) => {
const files = removePath(filePath, state.files)
const updatedFiles = files.filter(file => file)
setState(prevState => {
return { ...prevState, files: updatedFiles }
})
}
const fileRenamed = async () => {
const files = await fetchDirectoryContent(props.name)
setState(prevState => {
return { ...prevState, files, expandPath: [...prevState.expandPath] }
})
}
// register to event of the file provider
// files.event.register('fileRenamed', fileRenamed)
const fileRenamedError = (error: string) => {
modal('File Renamed Failed', error, {
label: 'Close',
fn: () => {}
}, null)
}
// register to event of the file provider
// files.event.register('rootFolderChanged', rootFolderChanged)
const rootFolderChanged = async () => {
const files = await fetchDirectoryContent(name)
setState(prevState => {
return { ...prevState, files }
})
}
const uploadFile = (target) => { const uploadFile = (target) => {
const filesProvider = fileSystem.provider.provider
// TODO The file explorer is merely a view on the current state of // 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 // 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 // a file and then just use `files.add`. The file explorer will
@ -552,8 +370,7 @@ export const FileExplorer = (props: FileExplorerProps) => {
} }
const name = `${parentFolder}/${file.name}` const name = `${parentFolder}/${file.name}`
filesProvider.exists(name, (error, exist) => { filesProvider.exists(name).then(exist => {
if (error) console.log(error)
if (!exist) { if (!exist) {
loadFile(name) loadFile(name)
} else { } else {
@ -567,6 +384,8 @@ export const FileExplorer = (props: FileExplorerProps) => {
fn: () => {} fn: () => {}
}) })
} }
}).catch(error => {
if (error) console.log(error)
}) })
}) })
} }
@ -582,6 +401,7 @@ export const FileExplorer = (props: FileExplorerProps) => {
} }
const toGist = (id?: string) => { const toGist = (id?: string) => {
const filesProvider = fileSystem.provider.provider
const proccedResult = function (error, data) { const proccedResult = function (error, data) {
if (error) { if (error) {
modal('Publish to gist Failed', 'Failed to manage gist: ' + error, { modal('Publish to gist Failed', 'Failed to manage gist: ' + error, {
@ -698,6 +518,8 @@ export const FileExplorer = (props: FileExplorerProps) => {
} }
const runScript = async (path: string) => { const runScript = async (path: string) => {
const filesProvider = fileSystem.provider.provider
filesProvider.get(path, (error, content: string) => { filesProvider.get(path, (error, content: string) => {
if (error) return console.log(error) if (error) return console.log(error)
plugin.call('scriptRunner', 'execute', content) plugin.call('scriptRunner', 'execute', content)
@ -737,6 +559,7 @@ export const FileExplorer = (props: FileExplorerProps) => {
} }
const handleClickFile = (path: string) => { const handleClickFile = (path: string) => {
path = path.indexOf(props.name + '/') === 0 ? path.replace(props.name + '/', '') : path
state.fileManager.open(path) state.fileManager.open(path)
setState(prevState => { setState(prevState => {
return { ...prevState, focusElement: [{ key: path, type: 'file' }] } return { ...prevState, focusElement: [{ key: path, type: 'file' }] }
@ -759,6 +582,7 @@ export const FileExplorer = (props: FileExplorerProps) => {
if (!state.expandPath.includes(path)) { if (!state.expandPath.includes(path)) {
expandPath = [...new Set([...state.expandPath, path])] expandPath = [...new Set([...state.expandPath, path])]
resolveDirectory(fileSystem.provider.provider, path)(dispatch)
} else { } else {
expandPath = [...new Set(state.expandPath.filter(key => key && (typeof key === 'string') && !key.startsWith(path)))] expandPath = [...new Set(state.expandPath.filter(key => key && (typeof key === 'string') && !key.startsWith(path)))]
} }
@ -790,7 +614,7 @@ export const FileExplorer = (props: FileExplorerProps) => {
} }
const editModeOn = (path: string, type: string, isNew: boolean = false) => { const editModeOn = (path: string, type: string, isNew: boolean = false) => {
if (filesProvider.isReadOnly(path)) return if (fileSystem.provider.provider.isReadOnly(path)) return
setState(prevState => { setState(prevState => {
return { ...prevState, focusEdit: { ...prevState.focusEdit, element: path, isNew, type } } return { ...prevState, focusEdit: { ...prevState.focusEdit, element: path, isNew, type } }
}) })
@ -802,11 +626,9 @@ export const FileExplorer = (props: FileExplorerProps) => {
if (!content || (content.trim() === '')) { if (!content || (content.trim() === '')) {
if (state.focusEdit.isNew) { if (state.focusEdit.isNew) {
const files = removePath(state.focusEdit.element, state.files) removeInputField(parentFolder)(dispatch)
const updatedFiles = files.filter(file => file)
setState(prevState => { setState(prevState => {
return { ...prevState, files: updatedFiles, focusEdit: { element: null, isNew: false, type: '', lastEdit: '' } } return { ...prevState, focusEdit: { element: null, isNew: false, type: '', lastEdit: '' } }
}) })
} else { } else {
editRef.current.textContent = state.focusEdit.lastEdit editRef.current.textContent = state.focusEdit.lastEdit
@ -829,12 +651,7 @@ export const FileExplorer = (props: FileExplorerProps) => {
} else { } else {
if (state.focusEdit.isNew) { if (state.focusEdit.isNew) {
state.focusEdit.type === 'file' ? createNewFile(joinPath(parentFolder, content)) : createNewFolder(joinPath(parentFolder, content)) state.focusEdit.type === 'file' ? createNewFile(joinPath(parentFolder, content)) : createNewFolder(joinPath(parentFolder, content))
const files = removePath(state.focusEdit.element, state.files) removeInputField(parentFolder)(dispatch)
const updatedFiles = files.filter(file => file)
setState(prevState => {
return { ...prevState, files: updatedFiles }
})
} else { } else {
const oldPath: string = state.focusEdit.element const oldPath: string = state.focusEdit.element
const oldName = extractNameFromKey(oldPath) const oldName = extractNameFromKey(oldPath)
@ -851,9 +668,10 @@ export const FileExplorer = (props: FileExplorerProps) => {
} }
const handleNewFileInput = async (parentFolder?: string) => { const handleNewFileInput = async (parentFolder?: string) => {
if (!parentFolder) parentFolder = state.focusElement[0] ? state.focusElement[0].type === 'folder' ? state.focusElement[0].key : extractParentFromKey(state.focusElement[0].key) : name if (!parentFolder) parentFolder = state.focusElement[0] ? state.focusElement[0].type === 'folder' ? state.focusElement[0].key ? state.focusElement[0].key : name : extractParentFromKey(state.focusElement[0].key) ? extractParentFromKey(state.focusElement[0].key) : name : name
const expandPath = [...new Set([...state.expandPath, parentFolder])] const expandPath = [...new Set([...state.expandPath, parentFolder])]
await addInputField(fileSystem.provider.provider, 'file', parentFolder)(dispatch)
setState(prevState => { setState(prevState => {
return { ...prevState, expandPath } return { ...prevState, expandPath }
}) })
@ -861,10 +679,11 @@ export const FileExplorer = (props: FileExplorerProps) => {
} }
const handleNewFolderInput = async (parentFolder?: string) => { const handleNewFolderInput = async (parentFolder?: string) => {
if (!parentFolder) parentFolder = state.focusElement[0] ? state.focusElement[0].type === 'folder' ? state.focusElement[0].key : extractParentFromKey(state.focusElement[0].key) : name if (!parentFolder) parentFolder = state.focusElement[0] ? state.focusElement[0].type === 'folder' ? state.focusElement[0].key ? state.focusElement[0].key : name : extractParentFromKey(state.focusElement[0].key) ? extractParentFromKey(state.focusElement[0].key) : name : name
else if ((parentFolder.indexOf('.sol') !== -1) || (parentFolder.indexOf('.js') !== -1)) parentFolder = extractParentFromKey(parentFolder) else if ((parentFolder.indexOf('.sol') !== -1) || (parentFolder.indexOf('.js') !== -1)) parentFolder = extractParentFromKey(parentFolder)
const expandPath = [...new Set([...state.expandPath, parentFolder])] const expandPath = [...new Set([...state.expandPath, parentFolder])]
await addInputField(fileSystem.provider.provider, 'folder', parentFolder)(dispatch)
setState(prevState => { setState(prevState => {
return { ...prevState, expandPath } return { ...prevState, expandPath }
}) })
@ -915,12 +734,16 @@ export const FileExplorer = (props: FileExplorerProps) => {
} }
const renderFiles = (file: File, index: number) => { const renderFiles = (file: File, index: number) => {
if (!file || !file.path || typeof file === 'string' || typeof file === 'number' || typeof file === 'boolean') return
const labelClass = state.focusEdit.element === file.path const labelClass = state.focusEdit.element === file.path
? 'bg-light' : state.focusElement.findIndex(item => item.key === file.path) !== -1 ? 'bg-light' : state.focusElement.findIndex(item => item.key === file.path) !== -1
? 'bg-secondary' : state.mouseOverElement === file.path ? 'bg-secondary' : state.mouseOverElement === file.path
? 'bg-light border' : (state.focusContext.element === file.path) && (state.focusEdit.element !== file.path) ? 'bg-light border' : (state.focusContext.element === file.path) && (state.focusEdit.element !== file.path)
? 'bg-light border' : '' ? 'bg-light border' : ''
const icon = helper.getPathIcon(file.path) const icon = helper.getPathIcon(file.path)
const spreadProps = {
onClick: (e) => e.stopPropagation()
}
if (file.isDirectory) { if (file.isDirectory) {
return ( return (
@ -952,12 +775,12 @@ export const FileExplorer = (props: FileExplorerProps) => {
}} }}
> >
{ {
file.child ? <TreeView id={`treeView${file.path}`} key={index}>{ file.child ? <TreeView id={`treeView${file.path}`} key={`treeView${file.path}`} {...spreadProps }>{
file.child.map((file, index) => { Object.keys(file.child).map((key, index) => {
return renderFiles(file, index) return renderFiles(file.child[key], index)
}) })
} }
</TreeView> : <TreeView id={`treeView${file.path}`} key={index} /> </TreeView> : <TreeView id={`treeView${file.path}`} key={`treeView${file.path}`} {...spreadProps }/>
} }
</TreeViewItem> </TreeViewItem>
) )
@ -965,7 +788,7 @@ export const FileExplorer = (props: FileExplorerProps) => {
return ( return (
<TreeViewItem <TreeViewItem
id={`treeViewItem${file.path}`} id={`treeViewItem${file.path}`}
key={index} key={`treeView${file.path}`}
label={label(file)} label={label(file)}
onClick={(e) => { onClick={(e) => {
e.stopPropagation() e.stopPropagation()
@ -1028,8 +851,8 @@ export const FileExplorer = (props: FileExplorerProps) => {
<div className='pb-2'> <div className='pb-2'>
<TreeView id='treeViewMenu'> <TreeView id='treeViewMenu'>
{ {
state.files.map((file, index) => { fileSystem.files.files[props.name] && Object.keys(fileSystem.files.files[props.name]).map((key, index) => {
return renderFiles(file, index) return renderFiles(fileSystem.files.files[props.name][key], index)
}) })
} }
</TreeView> </TreeView>

@ -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
}

@ -6,7 +6,7 @@ export interface FileExplorerProps {
menuItems?: string[], menuItems?: string[],
plugin: any, plugin: any,
focusRoot: boolean, focusRoot: boolean,
contextMenuItems: { name: string, type: string[], path: string[], extension: string[], pattern: string[] }[], contextMenuItems: { id: string, name: string, type: string[], path: string[], extension: string[], pattern: string[] }[],
displayInput?: boolean, displayInput?: boolean,
externalUploads?: EventTarget & HTMLInputElement externalUploads?: EventTarget & HTMLInputElement
} }

@ -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('/')
}

@ -85,11 +85,10 @@ export class RemixdClient extends PluginClient {
} }
} }
set (args: SharedFolderArgs): Promise<void> { set (args: SharedFolderArgs) {
try { try {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
if (this.readOnly) return reject(new Error('Cannot write file: read-only mode selected')) if (this.readOnly) return reject(new Error('Cannot write file: read-only mode selected'))
const isFolder = args.path.endsWith('/')
const path = utils.absolutePath(args.path, this.currentSharedFolder) const path = utils.absolutePath(args.path, this.currentSharedFolder)
const exists = fs.existsSync(path) const exists = fs.existsSync(path)
@ -99,31 +98,25 @@ export class RemixdClient extends PluginClient {
return reject(new Error('trying to write "undefined" ! stopping.')) return reject(new Error('trying to write "undefined" ! stopping.'))
} }
this.trackDownStreamUpdate[path] = path this.trackDownStreamUpdate[path] = path
if (isFolder) { if (!exists && args.path.indexOf('/') !== -1) {
fs.mkdirp(path).then(() => { // the last element is the filename and we should remove it
let splitPath = args.path.split('/') this.createDir({ path: args.path.substr(0, args.path.lastIndexOf('/')) })
}
splitPath = splitPath.filter(dir => dir) try {
const dir = '/' + splitPath.join('/') fs.writeFile(path, args.content, 'utf8', (error: Error) => {
if (error) {
this.emit('folderAdded', dir) console.log(error)
resolve() return reject(error)
}).catch((e: Error) => reject(e)) }
resolve(true)
})
} catch (e) {
return reject(e)
}
if (!exists) {
this.emit('fileAdded', args.path)
} else { } else {
fs.ensureFile(path).then(() => { this.emit('fileChanged', args.path)
fs.writeFile(path, args.content, 'utf8', (error: Error) => {
if (error) {
console.log(error)
return reject(error)
}
resolve()
})
}).catch((e: Error) => reject(e))
if (!exists) {
this.emit('fileAdded', args.path)
} else {
this.emit('fileChanged', args.path)
}
} }
}) })
} catch (error) { } catch (error) {
@ -131,24 +124,22 @@ export class RemixdClient extends PluginClient {
} }
} }
createDir (args: SharedFolderArgs): Promise<void> { createDir (args: SharedFolderArgs) {
try { try {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
if (this.readOnly) return reject(new Error('Cannot create folder: read-only mode selected')) if (this.readOnly) return reject(new Error('Cannot create folder: read-only mode selected'))
const path = utils.absolutePath(args.path, this.currentSharedFolder) const paths = args.path.split('/').filter(value => value)
const exists = fs.existsSync(path) if (paths.length && paths[0] === '') paths.shift()
let currentCheck = ''
if (exists && !isRealPath(path)) return reject(new Error('')) paths.forEach((value) => {
this.trackDownStreamUpdate[path] = path currentCheck = currentCheck ? currentCheck + '/' + value : value
fs.mkdirp(path).then(() => { const path = utils.absolutePath(currentCheck, this.currentSharedFolder)
let splitPath = args.path.split('/') if (!fs.existsSync(path)) {
fs.mkdirp(path)
splitPath = splitPath.filter(dir => dir) this.emit('folderAdded', currentCheck)
const dir = '/' + splitPath.join('/') }
})
this.emit('folderAdded', dir) resolve(true)
resolve()
}).catch((e: Error) => reject(e))
}) })
} catch (error) { } catch (error) {
throw new Error(error) throw new Error(error)

@ -161,6 +161,7 @@
"jszip": "^3.6.0", "jszip": "^3.6.0",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"latest-version": "^5.1.0", "latest-version": "^5.1.0",
"lodash": "^4.17.21",
"merge": "^1.2.0", "merge": "^1.2.0",
"npm-install-version": "^6.0.2", "npm-install-version": "^6.0.2",
"react": "16.13.1", "react": "16.13.1",

Loading…
Cancel
Save