Merge branch 'master' of https://github.com/ethereum/remix-project into e2epluginapi2
commit
8f8061f4d8
@ -1,22 +0,0 @@ |
|||||||
import { NightwatchBrowser } from 'nightwatch' |
|
||||||
import EventEmitter from 'events' |
|
||||||
|
|
||||||
// fix for editor scroll
|
|
||||||
class ScrollEditor extends EventEmitter { |
|
||||||
command (this: NightwatchBrowser, direction: 'up' | 'down', numberOfTimes: number): NightwatchBrowser { |
|
||||||
const browser = this.api |
|
||||||
|
|
||||||
browser.waitForElementPresent('.ace_text-input') |
|
||||||
for (let i = 0; i < numberOfTimes; i++) { |
|
||||||
if (direction.toLowerCase() === 'up') browser.sendKeys('.ace_text-input', browser.Keys.ARROW_UP) |
|
||||||
if (direction.toLowerCase() === 'down') browser.sendKeys('.ace_text-input', browser.Keys.ARROW_DOWN) |
|
||||||
} |
|
||||||
browser.perform((done) => { |
|
||||||
done() |
|
||||||
this.emit('complete') |
|
||||||
}) |
|
||||||
return this |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
module.exports = ScrollEditor |
|
@ -0,0 +1,17 @@ |
|||||||
|
import { NightwatchBrowser } from 'nightwatch' |
||||||
|
import EventEmitter from 'events' |
||||||
|
|
||||||
|
class ScrollToLine extends EventEmitter { |
||||||
|
command (this: NightwatchBrowser, line: number): NightwatchBrowser { |
||||||
|
this.api.execute(function (line) { |
||||||
|
const elem: any = document.getElementById('editorView') |
||||||
|
|
||||||
|
elem.gotoLine(line) |
||||||
|
}, [line], () => { |
||||||
|
this.emit('complete') |
||||||
|
}) |
||||||
|
return this |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
module.exports = ScrollToLine |
@ -1,84 +0,0 @@ |
|||||||
'use strict' |
|
||||||
const SourceHighlighter = require('./sourceHighlighter') |
|
||||||
|
|
||||||
class SourceHighlighters { |
|
||||||
constructor () { |
|
||||||
this.highlighters = {} |
|
||||||
} |
|
||||||
|
|
||||||
highlight (position, filePath, hexColor, from) { |
|
||||||
// eslint-disable-next-line
|
|
||||||
try { |
|
||||||
if (!this.highlighters[from]) this.highlighters[from] = [] |
|
||||||
const sourceHighlight = new SourceHighlighter() |
|
||||||
if ( |
|
||||||
!this.highlighters[from].length || |
|
||||||
(this.highlighters[from].length && !this.highlighters[from].find((el) => { |
|
||||||
return el.source === filePath && el.position === position |
|
||||||
})) |
|
||||||
) { |
|
||||||
sourceHighlight.currentSourceLocationFromfileName(position, filePath, hexColor) |
|
||||||
this.highlighters[from].push(sourceHighlight) |
|
||||||
} |
|
||||||
} catch (e) { |
|
||||||
throw e |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
// highlights all locations for @from plugin
|
|
||||||
highlightAllFrom (from) { |
|
||||||
// eslint-disable-next-line
|
|
||||||
try { |
|
||||||
if (!this.highlighters[from]) return |
|
||||||
let sourceHighlight |
|
||||||
for (const index in this.highlighters[from]) { |
|
||||||
sourceHighlight = new SourceHighlighter() |
|
||||||
sourceHighlight.currentSourceLocationFromfileName( |
|
||||||
this.highlighters[from][index].position, |
|
||||||
this.highlighters[from][index].source, |
|
||||||
this.highlighters[from][index].style |
|
||||||
) |
|
||||||
this.highlighters[from][index] = sourceHighlight |
|
||||||
} |
|
||||||
} catch (e) { |
|
||||||
throw e |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
discardHighlight (from) { |
|
||||||
if (this.highlighters[from]) { |
|
||||||
for (const index in this.highlighters[from]) this.highlighters[from][index].currentSourceLocation(null) |
|
||||||
} |
|
||||||
this.highlighters[from] = [] |
|
||||||
} |
|
||||||
|
|
||||||
discardAllHighlights () { |
|
||||||
for (const from in this.highlighters) { |
|
||||||
this.discardHighlight(from) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
hideHighlightsExcept (toStay) { |
|
||||||
for (const highlighter in this.highlighters) { |
|
||||||
for (const index in this.highlighters[highlighter]) { |
|
||||||
this.highlighters[highlighter][index].currentSourceLocation(null) |
|
||||||
} |
|
||||||
} |
|
||||||
this.highlightAllFrom(toStay) |
|
||||||
} |
|
||||||
|
|
||||||
discardHighlightAt (line, filePath, from) { |
|
||||||
if (this.highlighters[from]) { |
|
||||||
for (const index in this.highlighters[from]) { |
|
||||||
const highlight = this.highlighters[from][index] |
|
||||||
if (highlight.source === filePath && |
|
||||||
(highlight.position.start.line === line || highlight.position.end.line === line)) { |
|
||||||
highlight.currentSourceLocation(null) |
|
||||||
this.highlighters[from].splice(index, 1) |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
module.exports = SourceHighlighters |
|
@ -1,86 +0,0 @@ |
|||||||
'use strict' |
|
||||||
const csjs = require('csjs-inject') |
|
||||||
const globlalRegistry = require('../../global/registry') |
|
||||||
|
|
||||||
class SourceHighlighter { |
|
||||||
constructor (localRegistry) { |
|
||||||
this._components = {} |
|
||||||
this._components.registry = localRegistry || globlalRegistry |
|
||||||
// dependencies
|
|
||||||
this._deps = { |
|
||||||
editor: this._components.registry.get('editor').api, |
|
||||||
config: this._components.registry.get('config').api, |
|
||||||
fileManager: this._components.registry.get('filemanager').api, |
|
||||||
compilerArtefacts: this._components.registry.get('compilersartefacts').api |
|
||||||
} |
|
||||||
this.position = null |
|
||||||
this.statementMarker = null |
|
||||||
this.fullLineMarker = null |
|
||||||
this.source = null |
|
||||||
} |
|
||||||
|
|
||||||
currentSourceLocation (lineColumnPos, location) { |
|
||||||
if (this.statementMarker) this._deps.editor.removeMarker(this.statementMarker, this.source) |
|
||||||
if (this.fullLineMarker) this._deps.editor.removeMarker(this.fullLineMarker, this.source) |
|
||||||
const lastCompilationResult = this._deps.compilerArtefacts.__last |
|
||||||
if (location && location.file !== undefined && lastCompilationResult) { |
|
||||||
const path = lastCompilationResult.getSourceName(location.file) |
|
||||||
if (path) { |
|
||||||
this.currentSourceLocationFromfileName(lineColumnPos, path) |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
async currentSourceLocationFromfileName (lineColumnPos, filePath, style) { |
|
||||||
if (this.statementMarker) this._deps.editor.removeMarker(this.statementMarker, this.source) |
|
||||||
if (this.fullLineMarker) this._deps.editor.removeMarker(this.fullLineMarker, this.source) |
|
||||||
this.statementMarker = null |
|
||||||
this.fullLineMarker = null |
|
||||||
this.source = null |
|
||||||
if (lineColumnPos && lineColumnPos.start && lineColumnPos.end) { |
|
||||||
this.source = filePath |
|
||||||
this.style = style || 'var(--info)' |
|
||||||
// if (!this.source) this.source = this._deps.fileManager.currentFile()
|
|
||||||
if (this._deps.fileManager.currentFile() !== this.source) { |
|
||||||
await this._deps.fileManager.open(this.source) |
|
||||||
this.source = this._deps.fileManager.currentFile() |
|
||||||
} |
|
||||||
|
|
||||||
const css = csjs` |
|
||||||
.highlightcode { |
|
||||||
position:absolute; |
|
||||||
z-index:20; |
|
||||||
opacity: 0.3; |
|
||||||
background-color: ${this.style}; |
|
||||||
} |
|
||||||
.highlightcode_fullLine { |
|
||||||
position:absolute; |
|
||||||
z-index:20; |
|
||||||
opacity: 0.5; |
|
||||||
background-color: ${this.style}; |
|
||||||
} |
|
||||||
.customBackgroundColor { |
|
||||||
background-color: ${this.style}; |
|
||||||
} |
|
||||||
` |
|
||||||
|
|
||||||
this.statementMarker = this._deps.editor.addMarker(lineColumnPos, this.source, css.highlightcode.className + ' ' + css.customBackgroundColor.className + ' ' + `highlightLine${lineColumnPos.start.line}`) |
|
||||||
this._deps.editor.scrollToLine(lineColumnPos.start.line, true, true, function () {}) |
|
||||||
this.position = lineColumnPos |
|
||||||
if (lineColumnPos.start.line === lineColumnPos.end.line) { |
|
||||||
this.fullLineMarker = this._deps.editor.addMarker({ |
|
||||||
start: { |
|
||||||
line: lineColumnPos.start.line, |
|
||||||
column: 0 |
|
||||||
}, |
|
||||||
end: { |
|
||||||
line: lineColumnPos.start.line + 1, |
|
||||||
column: 0 |
|
||||||
} |
|
||||||
}, this.source, css.highlightcode_fullLine.className) |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
module.exports = SourceHighlighter |
|
@ -0,0 +1,8 @@ |
|||||||
|
# remix-ui-editor |
||||||
|
|
||||||
|
This library was generated with [Nx](https://nx.dev). |
||||||
|
Its purpose is to integrate Monaco editor as a react component inside Remix IDE. |
||||||
|
|
||||||
|
## Running unit tests |
||||||
|
|
||||||
|
Run `nx test remix-ui-editor` to execute the unit tests via [Jest](https://jestjs.io). |
@ -0,0 +1 @@ |
|||||||
|
export * from './lib/remix-ui-editor' |
@ -0,0 +1,135 @@ |
|||||||
|
export interface Action { |
||||||
|
type: string; |
||||||
|
payload: Record<string, any> |
||||||
|
monaco: any, |
||||||
|
editor: any |
||||||
|
} |
||||||
|
|
||||||
|
export const initialState = {} |
||||||
|
|
||||||
|
export const reducerActions = (models = initialState, action: Action) => { |
||||||
|
const monaco = action.monaco |
||||||
|
const editor = action.editor |
||||||
|
switch (action.type) { |
||||||
|
case 'ADD_MODEL': { |
||||||
|
if (!editor) return models |
||||||
|
const uri = action.payload.uri |
||||||
|
const value = action.payload.value |
||||||
|
const language = action.payload.language |
||||||
|
const readOnly = action.payload.readOnly |
||||||
|
if (models[uri]) return models // already existing
|
||||||
|
models[uri] = { language, uri, readOnly } |
||||||
|
const model = monaco.editor.createModel(value, language, monaco.Uri.parse(uri)) |
||||||
|
models[uri].model = model |
||||||
|
model.onDidChangeContent(() => action.payload.events.onDidChangeContent(uri)) |
||||||
|
return models |
||||||
|
} |
||||||
|
case 'DISPOSE_MODEL': { |
||||||
|
const uri = action.payload.uri |
||||||
|
const model = models[uri]?.model |
||||||
|
if (model) model.dispose() |
||||||
|
delete models[uri] |
||||||
|
return models |
||||||
|
} |
||||||
|
case 'SET_VALUE': { |
||||||
|
if (!editor) return models |
||||||
|
const uri = action.payload.uri |
||||||
|
const value = action.payload.value |
||||||
|
const model = models[uri]?.model |
||||||
|
if (model) { |
||||||
|
model.setValue(value) |
||||||
|
} |
||||||
|
return models |
||||||
|
} |
||||||
|
case 'REVEAL_LINE': { |
||||||
|
if (!editor) return models |
||||||
|
const line = action.payload.line |
||||||
|
const column = action.payload.column |
||||||
|
editor.revealLine(line) |
||||||
|
editor.setPosition({ column, lineNumber: line }) |
||||||
|
return models |
||||||
|
} |
||||||
|
case 'FOCUS': { |
||||||
|
if (!editor) return models |
||||||
|
editor.focus() |
||||||
|
return models |
||||||
|
} |
||||||
|
case 'SET_FONTSIZE': { |
||||||
|
if (!editor) return models |
||||||
|
const size = action.payload.size |
||||||
|
editor.updateOptions({ fontSize: size }) |
||||||
|
return models |
||||||
|
} |
||||||
|
case 'SET_WORDWRAP': { |
||||||
|
if (!editor) return models |
||||||
|
const wrap = action.payload.wrap |
||||||
|
editor.updateOptions({ wordWrap: wrap ? 'on' : 'off' }) |
||||||
|
return models |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export const reducerListener = (plugin, dispatch, monaco, editor, events) => { |
||||||
|
plugin.on('editor', 'addModel', (value, language, uri, readOnly) => { |
||||||
|
dispatch({ |
||||||
|
type: 'ADD_MODEL', |
||||||
|
payload: { uri, value, language, readOnly, events }, |
||||||
|
monaco, |
||||||
|
editor |
||||||
|
}) |
||||||
|
}) |
||||||
|
|
||||||
|
plugin.on('editor', 'disposeModel', (uri) => { |
||||||
|
dispatch({ |
||||||
|
type: 'DISPOSE_MODEL', |
||||||
|
payload: { uri }, |
||||||
|
monaco, |
||||||
|
editor |
||||||
|
}) |
||||||
|
}) |
||||||
|
|
||||||
|
plugin.on('editor', 'setValue', (uri, value) => { |
||||||
|
dispatch({ |
||||||
|
type: 'SET_VALUE', |
||||||
|
payload: { uri, value }, |
||||||
|
monaco, |
||||||
|
editor |
||||||
|
}) |
||||||
|
}) |
||||||
|
|
||||||
|
plugin.on('editor', 'revealLine', (line, column) => { |
||||||
|
dispatch({ |
||||||
|
type: 'REVEAL_LINE', |
||||||
|
payload: { line, column }, |
||||||
|
monaco, |
||||||
|
editor |
||||||
|
}) |
||||||
|
}) |
||||||
|
|
||||||
|
plugin.on('editor', 'focus', () => { |
||||||
|
dispatch({ |
||||||
|
type: 'FOCUS', |
||||||
|
payload: {}, |
||||||
|
monaco, |
||||||
|
editor |
||||||
|
}) |
||||||
|
}) |
||||||
|
|
||||||
|
plugin.on('editor', 'setFontSize', (size) => { |
||||||
|
dispatch({ |
||||||
|
type: 'SET_FONTSIZE', |
||||||
|
payload: { size }, |
||||||
|
monaco, |
||||||
|
editor |
||||||
|
}) |
||||||
|
}) |
||||||
|
|
||||||
|
plugin.on('editor', 'setWordWrap', (wrap) => { |
||||||
|
dispatch({ |
||||||
|
type: 'SET_WORDWRAP', |
||||||
|
payload: { wrap }, |
||||||
|
monaco, |
||||||
|
editor |
||||||
|
}) |
||||||
|
}) |
||||||
|
} |
@ -0,0 +1,11 @@ |
|||||||
|
.hover-row { |
||||||
|
white-space: pre; |
||||||
|
margin-left : 10px; |
||||||
|
background : var(--light); |
||||||
|
font-weight : bold; |
||||||
|
font-family : monospace; |
||||||
|
padding : 10px; |
||||||
|
border-radius : 10px; |
||||||
|
height: auto; |
||||||
|
width: auto; |
||||||
|
} |
@ -0,0 +1,256 @@ |
|||||||
|
import React, { useState, useRef, useEffect, useReducer } from 'react' // eslint-disable-line
|
||||||
|
import Editor from '@monaco-editor/react' |
||||||
|
import { reducerActions, reducerListener, initialState } from './actions/editor' |
||||||
|
|
||||||
|
import './remix-ui-editor.css' |
||||||
|
|
||||||
|
type cursorPosition = { |
||||||
|
startLineNumber: number, |
||||||
|
startColumn: number, |
||||||
|
endLineNumber: number, |
||||||
|
endColumn: number |
||||||
|
} |
||||||
|
|
||||||
|
type sourceAnnotation = { |
||||||
|
row: number, |
||||||
|
column: number, |
||||||
|
text: string, |
||||||
|
type: 'error' | 'warning' | 'info' |
||||||
|
hide: boolean |
||||||
|
from: string // plugin name
|
||||||
|
} |
||||||
|
|
||||||
|
type sourceMarker = { |
||||||
|
position: { |
||||||
|
start: { |
||||||
|
line: number |
||||||
|
column: number |
||||||
|
}, |
||||||
|
end: { |
||||||
|
line: number |
||||||
|
column: number |
||||||
|
} |
||||||
|
}, |
||||||
|
from: string // plugin name
|
||||||
|
hide: boolean |
||||||
|
} |
||||||
|
|
||||||
|
type sourceAnnotationMap = { |
||||||
|
[key: string]: [sourceAnnotation]; |
||||||
|
} |
||||||
|
|
||||||
|
type sourceMarkerMap = { |
||||||
|
[key: string]: [sourceMarker]; |
||||||
|
} |
||||||
|
|
||||||
|
/* eslint-disable-next-line */ |
||||||
|
export interface EditorUIProps { |
||||||
|
activated: boolean |
||||||
|
theme: string |
||||||
|
currentFile: string |
||||||
|
sourceAnnotationsPerFile: sourceAnnotationMap |
||||||
|
markerPerFile: sourceMarkerMap |
||||||
|
events: { |
||||||
|
onBreakPointAdded: (file: string, line: number) => void |
||||||
|
onBreakPointCleared: (file: string, line: number) => void |
||||||
|
onDidChangeContent: (file: string) => void |
||||||
|
onEditorMounted: () => void |
||||||
|
} |
||||||
|
plugin: { |
||||||
|
on: (plugin: string, event: string, listener: any) => void |
||||||
|
} |
||||||
|
editorAPI:{ |
||||||
|
findMatches: (uri: string, value: string) => any |
||||||
|
getFontSize: () => number, |
||||||
|
getValue: (uri: string) => string |
||||||
|
getCursorPosition: () => cursorPosition |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export const EditorUI = (props: EditorUIProps) => { |
||||||
|
const [, setCurrentBreakpoints] = useState({}) |
||||||
|
const [currentAnnotations, setCurrentAnnotations] = useState({}) |
||||||
|
const [currentMarkers, setCurrentMarkers] = useState({}) |
||||||
|
const editorRef = useRef(null) |
||||||
|
const monacoRef = useRef(null) |
||||||
|
const currentFileRef = useRef('') |
||||||
|
|
||||||
|
const [editorModelsState, dispatch] = useReducer(reducerActions, initialState) |
||||||
|
|
||||||
|
useEffect(() => { |
||||||
|
if (!monacoRef.current) return |
||||||
|
monacoRef.current.editor.setTheme(props.theme) |
||||||
|
}, [props.theme]) |
||||||
|
|
||||||
|
if (monacoRef.current) monacoRef.current.editor.setTheme(props.theme) |
||||||
|
|
||||||
|
const setAnnotationsbyFile = (uri) => { |
||||||
|
if (props.sourceAnnotationsPerFile[uri]) { |
||||||
|
const model = editorModelsState[uri]?.model |
||||||
|
const newAnnotations = [] |
||||||
|
for (const annotation of props.sourceAnnotationsPerFile[uri]) { |
||||||
|
if (!annotation.hide) { |
||||||
|
newAnnotations.push({ |
||||||
|
range: new monacoRef.current.Range(annotation.row + 1, 1, annotation.row + 1, 1), |
||||||
|
options: { |
||||||
|
isWholeLine: false, |
||||||
|
glyphMarginHoverMessage: { value: (annotation.from ? `from ${annotation.from}:\n` : '') + annotation.text }, |
||||||
|
glyphMarginClassName: `fal fa-exclamation-square text-${annotation.type === 'error' ? 'danger' : (annotation.type === 'warning' ? 'warning' : 'info')}` |
||||||
|
} |
||||||
|
}) |
||||||
|
} |
||||||
|
} |
||||||
|
setCurrentAnnotations(prevState => { |
||||||
|
prevState[uri] = model.deltaDecorations(currentAnnotations[uri] || [], newAnnotations) |
||||||
|
return prevState |
||||||
|
}) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
const setMarkerbyFile = (uri) => { |
||||||
|
if (props.markerPerFile[uri]) { |
||||||
|
const model = editorModelsState[uri]?.model |
||||||
|
const newMarkers = [] |
||||||
|
for (const marker of props.markerPerFile[uri]) { |
||||||
|
if (!marker.hide) { |
||||||
|
let isWholeLine = false |
||||||
|
if (marker.position.start.line === marker.position.end.line && marker.position.end.column - marker.position.start.column < 3) { |
||||||
|
// in this case we force highlighting the whole line (doesn't make sense to highlight 2 chars)
|
||||||
|
isWholeLine = true |
||||||
|
} |
||||||
|
newMarkers.push({ |
||||||
|
range: new monacoRef.current.Range(marker.position.start.line + 1, marker.position.start.column + 1, marker.position.end.line + 1, marker.position.end.column + 1), |
||||||
|
options: { |
||||||
|
isWholeLine, |
||||||
|
inlineClassName: `bg-info highlightLine${marker.position.start.line + 1}` |
||||||
|
} |
||||||
|
}) |
||||||
|
} |
||||||
|
} |
||||||
|
setCurrentMarkers(prevState => { |
||||||
|
prevState[uri] = model.deltaDecorations(currentMarkers[uri] || [], newMarkers) |
||||||
|
return prevState |
||||||
|
}) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
useEffect(() => { |
||||||
|
if (!editorRef.current) return |
||||||
|
currentFileRef.current = props.currentFile |
||||||
|
editorRef.current.setModel(editorModelsState[props.currentFile].model) |
||||||
|
editorRef.current.updateOptions({ readOnly: editorModelsState[props.currentFile].readOnly }) |
||||||
|
setAnnotationsbyFile(props.currentFile) |
||||||
|
setMarkerbyFile(props.currentFile) |
||||||
|
}, [props.currentFile]) |
||||||
|
|
||||||
|
useEffect(() => { |
||||||
|
setAnnotationsbyFile(props.currentFile) |
||||||
|
}, [JSON.stringify(props.sourceAnnotationsPerFile)]) |
||||||
|
|
||||||
|
useEffect(() => { |
||||||
|
setMarkerbyFile(props.currentFile) |
||||||
|
}, [JSON.stringify(props.markerPerFile)]) |
||||||
|
|
||||||
|
props.editorAPI.findMatches = (uri: string, value: string) => { |
||||||
|
if (!editorRef.current) return |
||||||
|
const model = editorModelsState[uri]?.model |
||||||
|
if (model) return model.findMatches(value) |
||||||
|
} |
||||||
|
|
||||||
|
props.editorAPI.getValue = (uri: string) => { |
||||||
|
if (!editorRef.current) return |
||||||
|
const model = editorModelsState[uri]?.model |
||||||
|
if (model) { |
||||||
|
return model.getValue() |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
props.editorAPI.getCursorPosition = () => { |
||||||
|
if (!monacoRef.current) return |
||||||
|
const model = editorModelsState[currentFileRef.current]?.model |
||||||
|
if (model) { |
||||||
|
return model.getOffsetAt(editorRef.current.getPosition()) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
props.editorAPI.getFontSize = () => { |
||||||
|
if (!editorRef.current) return |
||||||
|
return editorRef.current.getOption(42).fontSize |
||||||
|
} |
||||||
|
|
||||||
|
(window as any).addRemixBreakpoint = (position) => { // make it available from e2e testing...
|
||||||
|
const model = editorRef.current.getModel() |
||||||
|
if (model) { |
||||||
|
setCurrentBreakpoints(prevState => { |
||||||
|
const currentFile = currentFileRef.current |
||||||
|
if (!prevState[currentFile]) prevState[currentFile] = {} |
||||||
|
const decoration = Object.keys(prevState[currentFile]).filter((line) => parseInt(line) === position.lineNumber) |
||||||
|
if (decoration.length) { |
||||||
|
props.events.onBreakPointCleared(currentFile, position.lineNumber) |
||||||
|
model.deltaDecorations([prevState[currentFile][position.lineNumber]], []) |
||||||
|
delete prevState[currentFile][position.lineNumber] |
||||||
|
} else { |
||||||
|
props.events.onBreakPointAdded(currentFile, position.lineNumber) |
||||||
|
const decorationIds = model.deltaDecorations([], [{ |
||||||
|
range: new monacoRef.current.Range(position.lineNumber, 1, position.lineNumber, 1), |
||||||
|
options: { |
||||||
|
isWholeLine: false, |
||||||
|
glyphMarginClassName: 'fas fa-circle text-info' |
||||||
|
} |
||||||
|
}]) |
||||||
|
prevState[currentFile][position.lineNumber] = decorationIds[0] |
||||||
|
} |
||||||
|
return prevState |
||||||
|
}) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
function handleEditorDidMount (editor) { |
||||||
|
editorRef.current = editor |
||||||
|
monacoRef.current.editor.setTheme(props.theme) |
||||||
|
reducerListener(props.plugin, dispatch, monacoRef.current, editorRef.current, props.events) |
||||||
|
props.events.onEditorMounted() |
||||||
|
editor.onMouseUp((e) => { |
||||||
|
if (e && e.target && e.target.toString().startsWith('GUTTER')) { |
||||||
|
(window as any).addRemixBreakpoint(e.target.position) |
||||||
|
} |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
function handleEditorWillMount (monaco) { |
||||||
|
monacoRef.current = monaco |
||||||
|
// see https://microsoft.github.io/monaco-editor/playground.html#customizing-the-appearence-exposed-colors
|
||||||
|
const lightColor = window.getComputedStyle(document.documentElement).getPropertyValue('--light').trim() |
||||||
|
const infoColor = window.getComputedStyle(document.documentElement).getPropertyValue('--info').trim() |
||||||
|
const darkColor = window.getComputedStyle(document.documentElement).getPropertyValue('--dark').trim() |
||||||
|
const grayColor = window.getComputedStyle(document.documentElement).getPropertyValue('--gray-dark').trim() |
||||||
|
monaco.editor.defineTheme('remix-dark', { |
||||||
|
base: 'vs-dark', |
||||||
|
inherit: true, // can also be false to completely replace the builtin rules
|
||||||
|
rules: [{ background: darkColor.replace('#', '') }], |
||||||
|
colors: { |
||||||
|
'editor.background': darkColor, |
||||||
|
'editorSuggestWidget.background': lightColor, |
||||||
|
'editorSuggestWidget.selectedBackground': lightColor, |
||||||
|
'editorSuggestWidget.highlightForeground': infoColor, |
||||||
|
'editor.lineHighlightBorder': lightColor, |
||||||
|
'editor.lineHighlightBackground': grayColor, |
||||||
|
'editorGutter.background': lightColor |
||||||
|
} |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
return ( |
||||||
|
<Editor |
||||||
|
width="100%" |
||||||
|
height="100%" |
||||||
|
path={props.currentFile} |
||||||
|
language={editorModelsState[props.currentFile] ? editorModelsState[props.currentFile].language : 'text'} |
||||||
|
onMount={handleEditorDidMount} |
||||||
|
beforeMount={handleEditorWillMount} |
||||||
|
options= { { glyphMargin: true } } |
||||||
|
/> |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
export default EditorUI |
File diff suppressed because it is too large
Load Diff
@ -1,7 +0,0 @@ |
|||||||
# remix-ui-file-explorer |
|
||||||
|
|
||||||
This library was generated with [Nx](https://nx.dev). |
|
||||||
|
|
||||||
## Running unit tests |
|
||||||
|
|
||||||
Run `nx test remix-ui-file-explorer` to execute the unit tests via [Jest](https://jestjs.io). |
|
@ -1 +0,0 @@ |
|||||||
export * from './lib/file-explorer' |
|
@ -1,384 +0,0 @@ |
|||||||
import React from 'react' |
|
||||||
import { File } from '../types' |
|
||||||
import { extractNameFromKey, extractParentFromKey } from '../utils' |
|
||||||
|
|
||||||
const queuedEvents = [] |
|
||||||
const pendingEvents = {} |
|
||||||
let provider = null |
|
||||||
let plugin = null |
|
||||||
let dispatch: React.Dispatch<any> = null |
|
||||||
|
|
||||||
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).indexOf('gist-') === 0 ? extractNameFromKey(path).split('-')[1] : extractNameFromKey(path), |
|
||||||
isDirectory: filesList[key].isDirectory, |
|
||||||
type: extractNameFromKey(path).indexOf('gist-') === 0 ? 'gist' : 'folder' |
|
||||||
} |
|
||||||
} else { |
|
||||||
files[extractNameFromKey(key)] = { |
|
||||||
path, |
|
||||||
name: extractNameFromKey(path), |
|
||||||
isDirectory: filesList[key].isDirectory, |
|
||||||
type: 'file' |
|
||||||
} |
|
||||||
} |
|
||||||
}) |
|
||||||
|
|
||||||
if (newInputType === 'folder') { |
|
||||||
const path = parent + '/blank' |
|
||||||
|
|
||||||
folders[path] = { |
|
||||||
path: path, |
|
||||||
name: '', |
|
||||||
isDirectory: true, |
|
||||||
type: 'folder' |
|
||||||
} |
|
||||||
} else if (newInputType === 'file') { |
|
||||||
const path = parent + '/blank' |
|
||||||
|
|
||||||
files[path] = { |
|
||||||
path: path, |
|
||||||
name: '', |
|
||||||
isDirectory: false, |
|
||||||
type: 'file' |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
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 = (fileProvider, filePanel, registry) => (reducerDispatch: React.Dispatch<any>) => { |
|
||||||
provider = fileProvider |
|
||||||
plugin = filePanel |
|
||||||
dispatch = reducerDispatch |
|
||||||
if (provider) { |
|
||||||
provider.event.on('fileAdded', async (filePath) => { |
|
||||||
await executeEvent('fileAdded', filePath) |
|
||||||
}) |
|
||||||
provider.event.on('folderAdded', async (folderPath) => { |
|
||||||
await executeEvent('folderAdded', folderPath) |
|
||||||
}) |
|
||||||
provider.event.on('fileRemoved', async (removePath) => { |
|
||||||
await executeEvent('fileRemoved', removePath) |
|
||||||
}) |
|
||||||
provider.event.on('fileRenamed', async (oldPath) => { |
|
||||||
await executeEvent('fileRenamed', oldPath) |
|
||||||
}) |
|
||||||
provider.event.on('rootFolderChanged', async () => { |
|
||||||
await executeEvent('rootFolderChanged') |
|
||||||
}) |
|
||||||
provider.event.on('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.on('fileRenamedError', async () => { |
|
||||||
dispatch(displayNotification('File Renamed Failed', '', 'Ok', 'Cancel')) |
|
||||||
}) |
|
||||||
dispatch(fetchProviderSuccess(provider)) |
|
||||||
} 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()) |
|
||||||
} |
|
||||||
|
|
||||||
const fileAdded = async (filePath: string) => { |
|
||||||
if (extractParentFromKey(filePath) === '/.workspaces') return |
|
||||||
const path = extractParentFromKey(filePath) || provider.workspace || provider.type || '' |
|
||||||
const data = await fetchDirectoryContent(provider, path) |
|
||||||
|
|
||||||
await dispatch(fileAddedSuccess(path, data)) |
|
||||||
if (filePath.includes('_test.sol')) { |
|
||||||
plugin.emit('newTestFileCreated', filePath) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
const folderAdded = async (folderPath: string) => { |
|
||||||
if (extractParentFromKey(folderPath) === '/.workspaces') return |
|
||||||
const path = extractParentFromKey(folderPath) || provider.workspace || provider.type || '' |
|
||||||
const data = await fetchDirectoryContent(provider, path) |
|
||||||
|
|
||||||
await dispatch(folderAddedSuccess(path, data)) |
|
||||||
} |
|
||||||
|
|
||||||
const fileRemoved = async (removePath: string) => { |
|
||||||
const path = extractParentFromKey(removePath) || provider.workspace || provider.type || '' |
|
||||||
|
|
||||||
await dispatch(fileRemovedSuccess(path, removePath)) |
|
||||||
} |
|
||||||
|
|
||||||
const fileRenamed = async (oldPath: string) => { |
|
||||||
const path = extractParentFromKey(oldPath) || provider.workspace || provider.type || '' |
|
||||||
const data = await fetchDirectoryContent(provider, path) |
|
||||||
|
|
||||||
await dispatch(fileRenamedSuccess(path, oldPath, data)) |
|
||||||
} |
|
||||||
|
|
||||||
const rootFolderChanged = async () => { |
|
||||||
const workspaceName = provider.workspace || provider.type || '' |
|
||||||
|
|
||||||
await fetchDirectory(provider, workspaceName)(dispatch) |
|
||||||
} |
|
||||||
|
|
||||||
const executeEvent = async (eventName: 'fileAdded' | 'folderAdded' | 'fileRemoved' | 'fileRenamed' | 'rootFolderChanged', path?: string) => { |
|
||||||
if (Object.keys(pendingEvents).length) { |
|
||||||
return queuedEvents.push({ eventName, path }) |
|
||||||
} |
|
||||||
pendingEvents[eventName + path] = { eventName, path } |
|
||||||
switch (eventName) { |
|
||||||
case 'fileAdded': |
|
||||||
await fileAdded(path) |
|
||||||
delete pendingEvents[eventName + path] |
|
||||||
if (queuedEvents.length) { |
|
||||||
const next = queuedEvents.pop() |
|
||||||
|
|
||||||
await executeEvent(next.eventName, next.path) |
|
||||||
} |
|
||||||
break |
|
||||||
|
|
||||||
case 'folderAdded': |
|
||||||
await folderAdded(path) |
|
||||||
delete pendingEvents[eventName + path] |
|
||||||
if (queuedEvents.length) { |
|
||||||
const next = queuedEvents.pop() |
|
||||||
|
|
||||||
await executeEvent(next.eventName, next.path) |
|
||||||
} |
|
||||||
break |
|
||||||
|
|
||||||
case 'fileRemoved': |
|
||||||
await fileRemoved(path) |
|
||||||
delete pendingEvents[eventName + path] |
|
||||||
if (queuedEvents.length) { |
|
||||||
const next = queuedEvents.pop() |
|
||||||
|
|
||||||
await executeEvent(next.eventName, next.path) |
|
||||||
} |
|
||||||
break |
|
||||||
|
|
||||||
case 'fileRenamed': |
|
||||||
await fileRenamed(path) |
|
||||||
delete pendingEvents[eventName + path] |
|
||||||
if (queuedEvents.length) { |
|
||||||
const next = queuedEvents.pop() |
|
||||||
|
|
||||||
await executeEvent(next.eventName, next.path) |
|
||||||
} |
|
||||||
break |
|
||||||
|
|
||||||
case 'rootFolderChanged': |
|
||||||
await rootFolderChanged() |
|
||||||
delete pendingEvents[eventName + path] |
|
||||||
if (queuedEvents.length) { |
|
||||||
const next = queuedEvents.pop() |
|
||||||
|
|
||||||
await executeEvent(next.eventName, next.path) |
|
||||||
} |
|
||||||
break |
|
||||||
} |
|
||||||
} |
|
File diff suppressed because it is too large
Load Diff
@ -1,348 +0,0 @@ |
|||||||
import * as _ from 'lodash' |
|
||||||
import { extractNameFromKey } from '../utils' |
|
||||||
interface Action { |
|
||||||
type: string; |
|
||||||
payload: Record<string, any>; |
|
||||||
} |
|
||||||
|
|
||||||
export const fileSystemInitialState = { |
|
||||||
files: { |
|
||||||
files: [], |
|
||||||
expandPath: [], |
|
||||||
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.provider.provider, 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 'ADD_INPUT_FIELD': { |
|
||||||
return { |
|
||||||
...state, |
|
||||||
files: { |
|
||||||
...state.files, |
|
||||||
files: addInputField(state.provider.provider, 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.provider.provider, 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.provider.provider, 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.provider.provider, 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.provider.provider, 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.provider.provider, 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 = (provider, path: string, files, content) => { |
|
||||||
const root = provider.workspace || provider.type |
|
||||||
|
|
||||||
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).indexOf('gist-') === 0 ? extractNameFromKey(path).split('-')[1] : extractNameFromKey(path), |
|
||||||
type: extractNameFromKey(path).indexOf('gist-') === 0 ? 'gist' : 'folder', |
|
||||||
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) |
|
||||||
if (prevFiles) { |
|
||||||
prevFiles.child && prevFiles.child[pathName] && delete prevFiles.child[pathName] |
|
||||||
files = _.set(files, _path, { |
|
||||||
isDirectory: true, |
|
||||||
path, |
|
||||||
name: extractNameFromKey(path).indexOf('gist-') === 0 ? extractNameFromKey(path).split('-')[1] : extractNameFromKey(path), |
|
||||||
type: extractNameFromKey(path).indexOf('gist-') === 0 ? 'gist' : 'folder', |
|
||||||
child: prevFiles ? prevFiles.child : {} |
|
||||||
}) |
|
||||||
} |
|
||||||
|
|
||||||
return files |
|
||||||
} |
|
||||||
|
|
||||||
const addInputField = (provider, path: string, files, content) => { |
|
||||||
const root = provider.workspace || provider.type || '' |
|
||||||
|
|
||||||
if (path === root) return { [root]: { ...content[root], ...files[root] } } |
|
||||||
const result = resolveDirectory(provider, path, files, content) |
|
||||||
|
|
||||||
return result |
|
||||||
} |
|
||||||
|
|
||||||
const removeInputField = (provider, path: string, files) => { |
|
||||||
const root = provider.workspace || provider.type || '' |
|
||||||
|
|
||||||
if (path === root) { |
|
||||||
delete files[root][path + '/' + 'blank'] |
|
||||||
return files |
|
||||||
} |
|
||||||
return removePath(root, path, path + '/' + 'blank', files) |
|
||||||
} |
|
||||||
|
|
||||||
const fileAdded = (provider, path: string, files, content) => { |
|
||||||
return resolveDirectory(provider, path, files, content) |
|
||||||
} |
|
||||||
|
|
||||||
const folderAdded = (provider, path: string, files, content) => { |
|
||||||
return resolveDirectory(provider, path, files, content) |
|
||||||
} |
|
||||||
|
|
||||||
const fileRemoved = (provider, path: string, removedPath: string, files) => { |
|
||||||
const root = provider.workspace || provider.type || '' |
|
||||||
|
|
||||||
if (path === root) { |
|
||||||
delete files[root][removedPath] |
|
||||||
|
|
||||||
return files |
|
||||||
} |
|
||||||
return removePath(root, path, extractNameFromKey(removedPath), files) |
|
||||||
} |
|
||||||
|
|
||||||
const fileRenamed = (provider, path: string, removePath: string, files, content) => { |
|
||||||
const root = provider.workspace || provider.type || '' |
|
||||||
|
|
||||||
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).indexOf('gist-') === 0 ? extractNameFromKey(path).split('-')[1] : extractNameFromKey(path), |
|
||||||
type: extractNameFromKey(path).indexOf('gist-') === 0 ? 'gist' : 'folder', |
|
||||||
child: { ...content[pathArr[pathArr.length - 1]], ...prevFiles.child } |
|
||||||
}) |
|
||||||
|
|
||||||
return files |
|
||||||
} |
|
@ -1,59 +0,0 @@ |
|||||||
import { customAction } from '@remixproject/plugin-api/lib/file-system/file-panel' |
|
||||||
export type MenuItems = action[] // eslint-disable-line no-use-before-define
|
|
||||||
|
|
||||||
/* eslint-disable-next-line */ |
|
||||||
export interface FileExplorerProps { |
|
||||||
name: string, |
|
||||||
registry: any, |
|
||||||
filesProvider: any, |
|
||||||
menuItems?: string[], |
|
||||||
plugin: any, |
|
||||||
focusRoot: boolean, |
|
||||||
contextMenuItems: MenuItems, |
|
||||||
removedContextMenuItems: MenuItems, |
|
||||||
displayInput?: boolean, |
|
||||||
externalUploads?: EventTarget & HTMLInputElement, |
|
||||||
} |
|
||||||
|
|
||||||
export interface File { |
|
||||||
path: string, |
|
||||||
name: string, |
|
||||||
isDirectory: boolean, |
|
||||||
type: string, |
|
||||||
child?: File[] |
|
||||||
} |
|
||||||
|
|
||||||
export interface FileExplorerMenuProps { |
|
||||||
title: string, |
|
||||||
menuItems: string[], |
|
||||||
fileManager: any, |
|
||||||
createNewFile: (folder?: string) => void, |
|
||||||
createNewFolder: (parentFolder?: string) => void, |
|
||||||
publishToGist: (path?: string) => void, |
|
||||||
uploadFile: (target: EventTarget & HTMLInputElement) => void |
|
||||||
} |
|
||||||
|
|
||||||
export type action = { name: string, type: string[], path: string[], extension: string[], pattern: string[], id: string, multiselect: boolean, label: string } |
|
||||||
|
|
||||||
export interface FileExplorerContextMenuProps { |
|
||||||
actions: action[], |
|
||||||
createNewFile: (folder?: string) => void, |
|
||||||
createNewFolder: (parentFolder?: string) => void, |
|
||||||
deletePath: (path: string | string[]) => void, |
|
||||||
renamePath: (path: string, type: string) => void, |
|
||||||
hideContextMenu: () => void, |
|
||||||
publishToGist?: (path?: string, type?: string) => void, |
|
||||||
pushChangesToGist?: (path?: string, type?: string) => void, |
|
||||||
publishFolderToGist?: (path?: string, type?: string) => void, |
|
||||||
publishFileToGist?: (path?: string, type?: string) => void, |
|
||||||
runScript?: (path: string) => void, |
|
||||||
emit?: (cmd: customAction) => void, |
|
||||||
pageX: number, |
|
||||||
pageY: number, |
|
||||||
path: string, |
|
||||||
type: string, |
|
||||||
focus: {key:string, type:string}[], |
|
||||||
onMouseOver?: (...args) => void, |
|
||||||
copy?: (path: string, type: string) => void, |
|
||||||
paste?: (destination: string, type: string) => void |
|
||||||
} |
|
@ -1,13 +0,0 @@ |
|||||||
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('/') |
|
||||||
} |
|
@ -0,0 +1 @@ |
|||||||
|
{ "extends": "../../../.eslintrc", "rules": {}, "ignorePatterns": ["!**/*"] } |
@ -0,0 +1,3 @@ |
|||||||
|
# remix-ui-helper |
||||||
|
|
||||||
|
This library was generated with [Nx](https://nx.dev). |
@ -0,0 +1 @@ |
|||||||
|
export * from './lib/remix-ui-helper' |
@ -0,0 +1,63 @@ |
|||||||
|
export const extractNameFromKey = (key: string): string => { |
||||||
|
if (!key) return |
||||||
|
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('/') |
||||||
|
} |
||||||
|
|
||||||
|
export const checkSpecialChars = (name: string) => { |
||||||
|
return name.match(/[:*?"<>\\'|]/) != null |
||||||
|
} |
||||||
|
|
||||||
|
export const checkSlash = (name: string) => { |
||||||
|
return name.match(/\//) != null |
||||||
|
} |
||||||
|
|
||||||
|
export const createNonClashingNameAsync = async (name: string, 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) |
||||||
|
const counter = _counter || '' |
||||||
|
|
||||||
|
return name + counter + prefix + '.' + ext |
||||||
|
} |
||||||
|
|
||||||
|
export const joinPath = (...paths) => { |
||||||
|
paths = paths.filter((value) => value !== '').map((path) => path.replace(/^\/|\/$/g, '')) // remove first and last slash)
|
||||||
|
if (paths.length === 1) return paths[0] |
||||||
|
return paths.join('/') |
||||||
|
} |
||||||
|
|
||||||
|
export const getPathIcon = (path: string) => { |
||||||
|
return path.endsWith('.txt') |
||||||
|
? 'far fa-file-alt' : path.endsWith('.md') |
||||||
|
? 'far fa-file-alt' : path.endsWith('.sol') |
||||||
|
? 'fak fa-solidity-mono' : path.endsWith('.js') |
||||||
|
? 'fab fa-js' : path.endsWith('.json') |
||||||
|
? 'fas fa-brackets-curly' : path.endsWith('.vy') |
||||||
|
? 'fak fa-vyper-mono' : path.endsWith('.lex') |
||||||
|
? 'fak fa-lexon' : path.endsWith('.contract') |
||||||
|
? 'fab fa-ethereum' : 'far fa-file' |
||||||
|
} |
@ -0,0 +1,10 @@ |
|||||||
|
{ |
||||||
|
"extends": "../../../tsconfig.base.json", |
||||||
|
"files": [], |
||||||
|
"include": [], |
||||||
|
"references": [ |
||||||
|
{ |
||||||
|
"path": "./tsconfig.lib.json" |
||||||
|
} |
||||||
|
] |
||||||
|
} |
@ -0,0 +1,12 @@ |
|||||||
|
{ |
||||||
|
"extends": "./tsconfig.json", |
||||||
|
"compilerOptions": { |
||||||
|
"module": "commonjs", |
||||||
|
"outDir": "../../../dist/out-tsc", |
||||||
|
"declaration": true, |
||||||
|
"rootDir": "./src", |
||||||
|
"types": ["node"] |
||||||
|
}, |
||||||
|
"exclude": ["**/*.spec.ts"], |
||||||
|
"include": ["**/*.ts"] |
||||||
|
} |
@ -1 +1,2 @@ |
|||||||
export * from './lib/remix-ui-workspace' |
export * from './lib/providers/FileSystemProvider' |
||||||
|
export * from './lib/contexts' |
||||||
|
@ -0,0 +1,187 @@ |
|||||||
|
import { extractParentFromKey } from '@remix-ui/helper' |
||||||
|
import React from 'react' |
||||||
|
import { action } from '../types' |
||||||
|
import { displayNotification, displayPopUp, fileAddedSuccess, fileRemovedSuccess, fileRenamedSuccess, folderAddedSuccess, loadLocalhostError, loadLocalhostRequest, loadLocalhostSuccess, removeContextMenuItem, rootFolderChangedSuccess, setContextMenuItem, setMode, setReadOnlyMode } from './payload' |
||||||
|
import { addInputField, createWorkspace, deleteWorkspace, fetchWorkspaceDirectory, renameWorkspace, switchToWorkspace, uploadFile } from './workspace' |
||||||
|
|
||||||
|
const LOCALHOST = ' - connect to localhost - ' |
||||||
|
let plugin, dispatch: React.Dispatch<any> |
||||||
|
|
||||||
|
export const listenOnPluginEvents = (filePanelPlugin) => { |
||||||
|
plugin = filePanelPlugin |
||||||
|
|
||||||
|
plugin.on('filePanel', 'createWorkspaceReducerEvent', (name: string, isEmpty = false, cb: (err: Error, result?: string | number | boolean | Record<string, any>) => void) => { |
||||||
|
createWorkspace(name, isEmpty, cb) |
||||||
|
}) |
||||||
|
|
||||||
|
plugin.on('filePanel', 'renameWorkspaceReducerEvent', (oldName: string, workspaceName: string, cb: (err: Error, result?: string | number | boolean | Record<string, any>) => void) => { |
||||||
|
renameWorkspace(oldName, workspaceName, cb) |
||||||
|
}) |
||||||
|
|
||||||
|
plugin.on('filePanel', 'deleteWorkspaceReducerEvent', (workspaceName: string, cb: (err: Error, result?: string | number | boolean | Record<string, any>) => void) => { |
||||||
|
deleteWorkspace(workspaceName, cb) |
||||||
|
}) |
||||||
|
|
||||||
|
plugin.on('filePanel', 'registerContextMenuItemReducerEvent', (item: action, cb: (err: Error, result?: string | number | boolean | Record<string, any>) => void) => { |
||||||
|
registerContextMenuItem(item, cb) |
||||||
|
}) |
||||||
|
|
||||||
|
plugin.on('filePanel', 'removePluginActionsReducerEvent', (plugin, cb: (err: Error, result?: string | number | boolean | Record<string, any>) => void) => { |
||||||
|
removePluginActions(plugin, cb) |
||||||
|
}) |
||||||
|
|
||||||
|
plugin.on('filePanel', 'createNewFileInputReducerEvent', (path, cb: (err: Error, result?: string | number | boolean | Record<string, any>) => void) => { |
||||||
|
addInputField('file', path, cb) |
||||||
|
}) |
||||||
|
|
||||||
|
plugin.on('filePanel', 'uploadFileReducerEvent', (dir: string, target, cb: (err: Error, result?: string | number | boolean | Record<string, any>) => void) => { |
||||||
|
uploadFile(target, dir, cb) |
||||||
|
}) |
||||||
|
|
||||||
|
plugin.on('remixd', 'rootFolderChanged', async (path: string) => { |
||||||
|
rootFolderChanged(path) |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
export const listenOnProviderEvents = (provider) => (reducerDispatch: React.Dispatch<any>) => { |
||||||
|
dispatch = reducerDispatch |
||||||
|
|
||||||
|
provider.event.on('fileAdded', (filePath: string) => { |
||||||
|
fileAdded(filePath) |
||||||
|
}) |
||||||
|
|
||||||
|
provider.event.on('folderAdded', (folderPath: string) => { |
||||||
|
if (folderPath.indexOf('/.workspaces') === 0) return |
||||||
|
folderAdded(folderPath) |
||||||
|
}) |
||||||
|
|
||||||
|
provider.event.on('fileRemoved', (removePath: string) => { |
||||||
|
fileRemoved(removePath) |
||||||
|
}) |
||||||
|
|
||||||
|
provider.event.on('fileRenamed', (oldPath: string) => { |
||||||
|
fileRenamed(oldPath) |
||||||
|
}) |
||||||
|
|
||||||
|
provider.event.on('disconnected', async () => { |
||||||
|
plugin.fileManager.setMode('browser') |
||||||
|
dispatch(setMode('browser')) |
||||||
|
dispatch(loadLocalhostError('Remixd disconnected!')) |
||||||
|
const workspaceProvider = plugin.fileProviders.workspace |
||||||
|
|
||||||
|
await switchToWorkspace(workspaceProvider.workspace) |
||||||
|
}) |
||||||
|
|
||||||
|
provider.event.on('connected', () => { |
||||||
|
plugin.fileManager.setMode('localhost') |
||||||
|
dispatch(setMode('localhost')) |
||||||
|
fetchWorkspaceDirectory('/') |
||||||
|
dispatch(loadLocalhostSuccess()) |
||||||
|
}) |
||||||
|
|
||||||
|
provider.event.on('loadingLocalhost', async () => { |
||||||
|
await switchToWorkspace(LOCALHOST) |
||||||
|
dispatch(loadLocalhostRequest()) |
||||||
|
}) |
||||||
|
|
||||||
|
provider.event.on('fileExternallyChanged', (path: string, content: string) => { |
||||||
|
const config = plugin.registry.get('config').api |
||||||
|
const editor = plugin.registry.get('editor').api |
||||||
|
|
||||||
|
if (config.get('currentFile') === path && editor.currentContent() !== content) { |
||||||
|
if (provider.isReadOnly(path)) return editor.setText(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(content) |
||||||
|
} |
||||||
|
)) |
||||||
|
} |
||||||
|
}) |
||||||
|
|
||||||
|
provider.event.on('fileRenamedError', () => { |
||||||
|
dispatch(displayNotification('File Renamed Failed', '', 'Ok', 'Cancel')) |
||||||
|
}) |
||||||
|
|
||||||
|
provider.event.on('readOnlyModeChanged', (mode: boolean) => { |
||||||
|
dispatch(setReadOnlyMode(mode)) |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
const registerContextMenuItem = (item: action, cb?: (err: Error, result?: string | number | boolean | Record<string, any>) => void) => { |
||||||
|
if (!item) { |
||||||
|
cb && cb(new Error('Invalid register context menu argument')) |
||||||
|
return dispatch(displayPopUp('Invalid register context menu argument')) |
||||||
|
} |
||||||
|
if (!item.name || !item.id) { |
||||||
|
cb && cb(new Error('Item name and id is mandatory')) |
||||||
|
return dispatch(displayPopUp('Item name and id is mandatory')) |
||||||
|
} |
||||||
|
if (!item.type && !item.path && !item.extension && !item.pattern) { |
||||||
|
cb && cb(new Error('Invalid file matching criteria provided')) |
||||||
|
return dispatch(displayPopUp('Invalid file matching criteria provided')) |
||||||
|
} |
||||||
|
dispatch(setContextMenuItem(item)) |
||||||
|
cb && cb(null, item) |
||||||
|
} |
||||||
|
|
||||||
|
const removePluginActions = (plugin, cb: (err: Error, result?: string | number | boolean | Record<string, any>) => void) => { |
||||||
|
dispatch(removeContextMenuItem(plugin)) |
||||||
|
cb && cb(null, true) |
||||||
|
} |
||||||
|
|
||||||
|
const fileAdded = async (filePath: string) => { |
||||||
|
await dispatch(fileAddedSuccess(filePath)) |
||||||
|
if (filePath.includes('_test.sol')) { |
||||||
|
plugin.emit('newTestFileCreated', filePath) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
const folderAdded = async (folderPath: string) => { |
||||||
|
const provider = plugin.fileManager.currentFileProvider() |
||||||
|
const path = extractParentFromKey(folderPath) || provider.workspace || provider.type || '' |
||||||
|
|
||||||
|
const promise = new Promise((resolve) => { |
||||||
|
provider.resolveDirectory(path, (error, fileTree) => { |
||||||
|
if (error) console.error(error) |
||||||
|
|
||||||
|
resolve(fileTree) |
||||||
|
}) |
||||||
|
}) |
||||||
|
|
||||||
|
promise.then((files) => { |
||||||
|
folderPath = folderPath.replace(/^\/+/, '') |
||||||
|
dispatch(folderAddedSuccess(path, folderPath, files)) |
||||||
|
}).catch((error) => { |
||||||
|
console.error(error) |
||||||
|
}) |
||||||
|
return promise |
||||||
|
} |
||||||
|
|
||||||
|
const fileRemoved = async (removePath: string) => { |
||||||
|
await dispatch(fileRemovedSuccess(removePath)) |
||||||
|
} |
||||||
|
|
||||||
|
const fileRenamed = async (oldPath: string) => { |
||||||
|
const provider = plugin.fileManager.currentFileProvider() |
||||||
|
const path = extractParentFromKey(oldPath) || provider.workspace || provider.type || '' |
||||||
|
const promise = new Promise((resolve) => { |
||||||
|
provider.resolveDirectory(path, (error, fileTree) => { |
||||||
|
if (error) console.error(error) |
||||||
|
|
||||||
|
resolve(fileTree) |
||||||
|
}) |
||||||
|
}) |
||||||
|
|
||||||
|
promise.then((files) => { |
||||||
|
dispatch(fileRenamedSuccess(path, oldPath, files)) |
||||||
|
}).catch((error) => { |
||||||
|
console.error(error) |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
const rootFolderChanged = async (path) => { |
||||||
|
await dispatch(rootFolderChangedSuccess(path)) |
||||||
|
} |
@ -0,0 +1,332 @@ |
|||||||
|
import React from 'react' |
||||||
|
import { extractNameFromKey, createNonClashingNameAsync } from '@remix-ui/helper' |
||||||
|
import Gists from 'gists' |
||||||
|
import { customAction } from '@remixproject/plugin-api/lib/file-system/file-panel/type' |
||||||
|
import { displayNotification, displayPopUp, fetchDirectoryError, fetchDirectoryRequest, fetchDirectorySuccess, focusElement, fsInitializationCompleted, hidePopUp, removeInputFieldSuccess, setCurrentWorkspace, setExpandPath, setMode, setWorkspaces } from './payload' |
||||||
|
import { listenOnPluginEvents, listenOnProviderEvents } from './events' |
||||||
|
import { createWorkspaceTemplate, getWorkspaces, loadWorkspacePreset, setPlugin } from './workspace' |
||||||
|
|
||||||
|
export * from './events' |
||||||
|
export * from './workspace' |
||||||
|
|
||||||
|
const QueryParams = require('../../../../../../apps/remix-ide/src/lib/query-params') |
||||||
|
const queryParams = new QueryParams() |
||||||
|
|
||||||
|
let plugin, dispatch: React.Dispatch<any> |
||||||
|
|
||||||
|
export const initWorkspace = (filePanelPlugin) => async (reducerDispatch: React.Dispatch<any>) => { |
||||||
|
if (filePanelPlugin) { |
||||||
|
plugin = filePanelPlugin |
||||||
|
dispatch = reducerDispatch |
||||||
|
setPlugin(plugin, dispatch) |
||||||
|
const workspaceProvider = filePanelPlugin.fileProviders.workspace |
||||||
|
const localhostProvider = filePanelPlugin.fileProviders.localhost |
||||||
|
const params = queryParams.get() |
||||||
|
const workspaces = await getWorkspaces() || [] |
||||||
|
|
||||||
|
dispatch(setWorkspaces(workspaces)) |
||||||
|
if (params.gist) { |
||||||
|
await createWorkspaceTemplate('gist-sample', 'gist-template') |
||||||
|
plugin.setWorkspace({ name: 'gist-sample', isLocalhost: false }) |
||||||
|
dispatch(setCurrentWorkspace('gist-sample')) |
||||||
|
await loadWorkspacePreset('gist-template') |
||||||
|
} else if (params.code || params.url) { |
||||||
|
await createWorkspaceTemplate('code-sample', 'code-template') |
||||||
|
plugin.setWorkspace({ name: 'code-sample', isLocalhost: false }) |
||||||
|
dispatch(setCurrentWorkspace('code-sample')) |
||||||
|
const filePath = await loadWorkspacePreset('code-template') |
||||||
|
plugin.on('editor', 'editorMounted', () => plugin.fileManager.openFile(filePath)) |
||||||
|
} else { |
||||||
|
if (workspaces.length === 0) { |
||||||
|
await createWorkspaceTemplate('default_workspace', 'default-template') |
||||||
|
plugin.setWorkspace({ name: 'default_workspace', isLocalhost: false }) |
||||||
|
dispatch(setCurrentWorkspace('default_workspace')) |
||||||
|
await loadWorkspacePreset('default-template') |
||||||
|
} else { |
||||||
|
if (workspaces.length > 0) { |
||||||
|
workspaceProvider.setWorkspace(workspaces[workspaces.length - 1]) |
||||||
|
plugin.setWorkspace({ name: workspaces[workspaces.length - 1], isLocalhost: false }) |
||||||
|
dispatch(setCurrentWorkspace(workspaces[workspaces.length - 1])) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
listenOnPluginEvents(plugin) |
||||||
|
listenOnProviderEvents(workspaceProvider)(dispatch) |
||||||
|
listenOnProviderEvents(localhostProvider)(dispatch) |
||||||
|
dispatch(setMode('browser')) |
||||||
|
plugin.setWorkspaces(await getWorkspaces()) |
||||||
|
dispatch(fsInitializationCompleted()) |
||||||
|
plugin.emit('workspaceInitializationCompleted') |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export const fetchDirectory = async (path: string) => { |
||||||
|
const provider = plugin.fileManager.currentFileProvider() |
||||||
|
const promise = new Promise((resolve) => { |
||||||
|
provider.resolveDirectory(path, (error, fileTree) => { |
||||||
|
if (error) console.error(error) |
||||||
|
|
||||||
|
resolve(fileTree) |
||||||
|
}) |
||||||
|
}) |
||||||
|
|
||||||
|
dispatch(fetchDirectoryRequest(promise)) |
||||||
|
promise.then((fileTree) => { |
||||||
|
dispatch(fetchDirectorySuccess(path, fileTree)) |
||||||
|
}).catch((error) => { |
||||||
|
dispatch(fetchDirectoryError({ error })) |
||||||
|
}) |
||||||
|
return promise |
||||||
|
} |
||||||
|
|
||||||
|
export const removeInputField = async (path: string) => { |
||||||
|
dispatch(removeInputFieldSuccess(path)) |
||||||
|
} |
||||||
|
|
||||||
|
export const publishToGist = async (path?: string, type?: string) => { |
||||||
|
// If 'id' is not defined, it is not a gist update but a creation so we have to take the files from the browser explorer.
|
||||||
|
const folder = path || '/' |
||||||
|
const id = type === 'gist' ? extractNameFromKey(path).split('-')[1] : null |
||||||
|
try { |
||||||
|
const packaged = await packageGistFiles(folder) |
||||||
|
// check for token
|
||||||
|
const config = plugin.registry.get('config').api |
||||||
|
const accessToken = config.get('settings/gist-access-token') |
||||||
|
|
||||||
|
if (!accessToken) { |
||||||
|
dispatch(displayNotification('Authorize Token', 'Remix requires an access token (which includes gists creation permission). Please go to the settings tab to create one.', 'Close', null, () => {})) |
||||||
|
} else { |
||||||
|
const description = 'Created using remix-ide: Realtime Ethereum Contract Compiler and Runtime. \n Load this file by pasting this gists URL or ID at https://remix.ethereum.org/#version=' + |
||||||
|
queryParams.get().version + '&optimize=' + queryParams.get().optimize + '&runs=' + queryParams.get().runs + '&gist=' |
||||||
|
const gists = new Gists({ token: accessToken }) |
||||||
|
|
||||||
|
if (id) { |
||||||
|
const originalFileList = await getOriginalFiles(id) |
||||||
|
// Telling the GIST API to remove files
|
||||||
|
const updatedFileList = Object.keys(packaged) |
||||||
|
const allItems = Object.keys(originalFileList) |
||||||
|
.filter(fileName => updatedFileList.indexOf(fileName) === -1) |
||||||
|
.reduce((acc, deleteFileName) => ({ |
||||||
|
...acc, |
||||||
|
[deleteFileName]: null |
||||||
|
}), originalFileList) |
||||||
|
// adding new files
|
||||||
|
updatedFileList.forEach((file) => { |
||||||
|
const _items = file.split('/') |
||||||
|
const _fileName = _items[_items.length - 1] |
||||||
|
allItems[_fileName] = packaged[file] |
||||||
|
}) |
||||||
|
|
||||||
|
dispatch(displayPopUp('Saving gist (' + id + ') ...')) |
||||||
|
gists.edit({ |
||||||
|
description: description, |
||||||
|
public: true, |
||||||
|
files: allItems, |
||||||
|
id: id |
||||||
|
}, (error, result) => { |
||||||
|
handleGistResponse(error, result) |
||||||
|
if (!error) { |
||||||
|
for (const key in allItems) { |
||||||
|
if (allItems[key] === null) delete allItems[key] |
||||||
|
} |
||||||
|
} |
||||||
|
}) |
||||||
|
} else { |
||||||
|
// id is not existing, need to create a new gist
|
||||||
|
dispatch(displayPopUp('Creating a new gist ...')) |
||||||
|
gists.create({ |
||||||
|
description: description, |
||||||
|
public: true, |
||||||
|
files: packaged |
||||||
|
}, (error, result) => { |
||||||
|
handleGistResponse(error, result) |
||||||
|
}) |
||||||
|
} |
||||||
|
} |
||||||
|
} catch (error) { |
||||||
|
console.log(error) |
||||||
|
dispatch(displayNotification('Publish to gist Failed', 'Failed to create gist: ' + error.message, 'Close', null, async () => {})) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export const clearPopUp = async () => { |
||||||
|
dispatch(hidePopUp()) |
||||||
|
} |
||||||
|
|
||||||
|
export const createNewFile = async (path: string, rootDir: string) => { |
||||||
|
const fileManager = plugin.fileManager |
||||||
|
const newName = await createNonClashingNameAsync(path, fileManager) |
||||||
|
const createFile = await fileManager.writeFile(newName, '') |
||||||
|
|
||||||
|
if (!createFile) { |
||||||
|
return dispatch(displayPopUp('Failed to create file ' + newName)) |
||||||
|
} else { |
||||||
|
const path = newName.indexOf(rootDir + '/') === 0 ? newName.replace(rootDir + '/', '') : newName |
||||||
|
|
||||||
|
await fileManager.open(path) |
||||||
|
setFocusElement([{ key: path, type: 'file' }]) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export const setFocusElement = async (elements: { key: string, type: 'file' | 'folder' | 'gist' }[]) => { |
||||||
|
dispatch(focusElement(elements)) |
||||||
|
} |
||||||
|
|
||||||
|
export const createNewFolder = async (path: string, rootDir: string) => { |
||||||
|
const fileManager = plugin.fileManager |
||||||
|
const dirName = path + '/' |
||||||
|
const exists = await fileManager.exists(dirName) |
||||||
|
|
||||||
|
if (exists) { |
||||||
|
return dispatch(displayNotification('Rename File Failed', `A file or folder ${extractNameFromKey(path)} already exists at this location. Please choose a different name.`, 'Close', null, () => {})) |
||||||
|
} |
||||||
|
await fileManager.mkdir(dirName) |
||||||
|
path = path.indexOf(rootDir + '/') === 0 ? path.replace(rootDir + '/', '') : path |
||||||
|
dispatch(focusElement([{ key: path, type: 'folder' }])) |
||||||
|
} |
||||||
|
|
||||||
|
export const deletePath = async (path: string[]) => { |
||||||
|
const fileManager = plugin.fileManager |
||||||
|
|
||||||
|
for (const p of path) { |
||||||
|
try { |
||||||
|
await fileManager.remove(p) |
||||||
|
} catch (e) { |
||||||
|
const isDir = await fileManager.isDirectory(p) |
||||||
|
|
||||||
|
dispatch(displayPopUp(`Failed to remove ${isDir ? 'folder' : 'file'} ${p}.`)) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export const renamePath = async (oldPath: string, newPath: string) => { |
||||||
|
const fileManager = plugin.fileManager |
||||||
|
const exists = await fileManager.exists(newPath) |
||||||
|
|
||||||
|
if (exists) { |
||||||
|
dispatch(displayNotification('Rename File Failed', `A file or folder ${extractNameFromKey(newPath)} already exists at this location. Please choose a different name.`, 'Close', null, () => {})) |
||||||
|
} else { |
||||||
|
await fileManager.rename(oldPath, newPath) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export const copyFile = async (src: string, dest: string) => { |
||||||
|
const fileManager = plugin.fileManager |
||||||
|
|
||||||
|
try { |
||||||
|
fileManager.copyFile(src, dest) |
||||||
|
} catch (error) { |
||||||
|
dispatch(displayPopUp('Oops! An error ocurred while performing copyFile operation.' + error)) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export const copyFolder = async (src: string, dest: string) => { |
||||||
|
const fileManager = plugin.fileManager |
||||||
|
|
||||||
|
try { |
||||||
|
fileManager.copyDir(src, dest) |
||||||
|
} catch (error) { |
||||||
|
dispatch(displayPopUp('Oops! An error ocurred while performing copyDir operation.' + error)) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export const runScript = async (path: string) => { |
||||||
|
const provider = plugin.fileManager.currentFileProvider() |
||||||
|
|
||||||
|
provider.get(path, (error, content: string) => { |
||||||
|
if (error) { |
||||||
|
return dispatch(displayPopUp(error)) |
||||||
|
} |
||||||
|
plugin.call('scriptRunner', 'execute', content) |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
export const emitContextMenuEvent = async (cmd: customAction) => { |
||||||
|
plugin.call(cmd.id, cmd.name, cmd) |
||||||
|
} |
||||||
|
|
||||||
|
export const handleClickFile = async (path: string, type: 'file' | 'folder' | 'gist') => { |
||||||
|
plugin.fileManager.open(path) |
||||||
|
dispatch(focusElement([{ key: path, type }])) |
||||||
|
} |
||||||
|
|
||||||
|
export const handleExpandPath = (paths: string[]) => { |
||||||
|
dispatch(setExpandPath(paths)) |
||||||
|
} |
||||||
|
|
||||||
|
const packageGistFiles = (directory) => { |
||||||
|
return new Promise((resolve, reject) => { |
||||||
|
const workspaceProvider = plugin.fileProviders.workspace |
||||||
|
const isFile = workspaceProvider.isFile(directory) |
||||||
|
const ret = {} |
||||||
|
|
||||||
|
if (isFile) { |
||||||
|
try { |
||||||
|
workspaceProvider.get(directory, (error, content) => { |
||||||
|
if (error) throw new Error('An error ocurred while getting file content. ' + directory) |
||||||
|
if (/^\s+$/.test(content) || !content.length) { |
||||||
|
content = '// this line is added to create a gist. Empty file is not allowed.' |
||||||
|
} |
||||||
|
directory = directory.replace(/\//g, '...') |
||||||
|
ret[directory] = { content } |
||||||
|
return resolve(ret) |
||||||
|
}) |
||||||
|
} catch (e) { |
||||||
|
return reject(e) |
||||||
|
} |
||||||
|
} else { |
||||||
|
try { |
||||||
|
(async () => { |
||||||
|
await workspaceProvider.copyFolderToJson(directory, ({ path, content }) => { |
||||||
|
if (/^\s+$/.test(content) || !content.length) { |
||||||
|
content = '// this line is added to create a gist. Empty file is not allowed.' |
||||||
|
} |
||||||
|
if (path.indexOf('gist-') === 0) { |
||||||
|
path = path.split('/') |
||||||
|
path.shift() |
||||||
|
path = path.join('/') |
||||||
|
} |
||||||
|
path = path.replace(/\//g, '...') |
||||||
|
ret[path] = { content } |
||||||
|
}) |
||||||
|
resolve(ret) |
||||||
|
})() |
||||||
|
} catch (e) { |
||||||
|
return reject(e) |
||||||
|
} |
||||||
|
} |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
const handleGistResponse = (error, data) => { |
||||||
|
if (error) { |
||||||
|
dispatch(displayNotification('Publish to gist Failed', 'Failed to manage gist: ' + error, 'Close', null)) |
||||||
|
} else { |
||||||
|
if (data.html_url) { |
||||||
|
dispatch(displayNotification('Gist is ready', `The gist is at ${data.html_url}. Would you like to open it in a new window?`, 'OK', 'Cancel', () => { |
||||||
|
window.open(data.html_url, '_blank') |
||||||
|
}, () => {})) |
||||||
|
} else { |
||||||
|
const error = JSON.stringify(data.errors, null, '\t') || '' |
||||||
|
const message = data.message === 'Not Found' ? data.message + '. Please make sure the API token has right to create a gist.' : data.message |
||||||
|
|
||||||
|
dispatch(displayNotification('Publish to gist Failed', message + ' ' + data.documentation_url + ' ' + error, 'Close', null)) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* This function is to get the original content of given gist |
||||||
|
* @params id is the gist id to fetch |
||||||
|
*/ |
||||||
|
const getOriginalFiles = async (id) => { |
||||||
|
if (!id) { |
||||||
|
return [] |
||||||
|
} |
||||||
|
|
||||||
|
const url = `https://api.github.com/gists/${id}` |
||||||
|
const res = await fetch(url) |
||||||
|
const data = await res.json() |
||||||
|
return data.files || [] |
||||||
|
} |
@ -0,0 +1,234 @@ |
|||||||
|
import { action } from '../types' |
||||||
|
|
||||||
|
export const setCurrentWorkspace = (workspace: string) => { |
||||||
|
return { |
||||||
|
type: 'SET_CURRENT_WORKSPACE', |
||||||
|
payload: workspace |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export const setWorkspaces = (workspaces: string[]) => { |
||||||
|
return { |
||||||
|
type: 'SET_WORKSPACES', |
||||||
|
payload: workspaces |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export const setMode = (mode: 'browser' | 'localhost') => { |
||||||
|
return { |
||||||
|
type: 'SET_MODE', |
||||||
|
payload: mode |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
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, fileTree) => { |
||||||
|
return { |
||||||
|
type: 'FETCH_DIRECTORY_SUCCESS', |
||||||
|
payload: { path, fileTree } |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
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: 'HIDE_NOTIFICATION' |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export const fileAddedSuccess = (filePath: string) => { |
||||||
|
return { |
||||||
|
type: 'FILE_ADDED_SUCCESS', |
||||||
|
payload: filePath |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export const folderAddedSuccess = (path: string, folderPath: string, fileTree) => { |
||||||
|
return { |
||||||
|
type: 'FOLDER_ADDED_SUCCESS', |
||||||
|
payload: { path, folderPath, fileTree } |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export const fileRemovedSuccess = (removePath: string) => { |
||||||
|
return { |
||||||
|
type: 'FILE_REMOVED_SUCCESS', |
||||||
|
payload: removePath |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export const fileRenamedSuccess = (path: string, oldPath: string, fileTree) => { |
||||||
|
return { |
||||||
|
type: 'FILE_RENAMED_SUCCESS', |
||||||
|
payload: { path, oldPath, fileTree } |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export const rootFolderChangedSuccess = (path: string) => { |
||||||
|
return { |
||||||
|
type: 'ROOT_FOLDER_CHANGED', |
||||||
|
payload: path |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export const addInputFieldSuccess = (path: string, fileTree, type: 'file' | 'folder' | 'gist') => { |
||||||
|
return { |
||||||
|
type: 'ADD_INPUT_FIELD', |
||||||
|
payload: { path, fileTree, type } |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export const removeInputFieldSuccess = (path: string) => { |
||||||
|
return { |
||||||
|
type: 'REMOVE_INPUT_FIELD', |
||||||
|
payload: { path } |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export const setReadOnlyMode = (mode: boolean) => { |
||||||
|
return { |
||||||
|
type: 'SET_READ_ONLY_MODE', |
||||||
|
payload: mode |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export const createWorkspaceError = (error: any) => { |
||||||
|
return { |
||||||
|
type: 'CREATE_WORKSPACE_ERROR', |
||||||
|
payload: error |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export const createWorkspaceRequest = (promise: Promise<any>) => { |
||||||
|
return { |
||||||
|
type: 'CREATE_WORKSPACE_REQUEST', |
||||||
|
payload: promise |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export const createWorkspaceSuccess = (workspaceName: string) => { |
||||||
|
return { |
||||||
|
type: 'CREATE_WORKSPACE_SUCCESS', |
||||||
|
payload: workspaceName |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export const fetchWorkspaceDirectoryError = (error: any) => { |
||||||
|
return { |
||||||
|
type: 'FETCH_WORKSPACE_DIRECTORY_ERROR', |
||||||
|
payload: error |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export const fetchWorkspaceDirectoryRequest = (promise: Promise<any>) => { |
||||||
|
return { |
||||||
|
type: 'FETCH_WORKSPACE_DIRECTORY_REQUEST', |
||||||
|
payload: promise |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export const fetchWorkspaceDirectorySuccess = (path: string, fileTree) => { |
||||||
|
return { |
||||||
|
type: 'FETCH_WORKSPACE_DIRECTORY_SUCCESS', |
||||||
|
payload: { path, fileTree } |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export const setRenameWorkspace = (oldName: string, workspaceName: string) => { |
||||||
|
return { |
||||||
|
type: 'RENAME_WORKSPACE', |
||||||
|
payload: { oldName, workspaceName } |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export const setDeleteWorkspace = (workspaceName: string) => { |
||||||
|
return { |
||||||
|
type: 'DELETE_WORKSPACE', |
||||||
|
payload: workspaceName |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export const displayPopUp = (message: string) => { |
||||||
|
return { |
||||||
|
type: 'DISPLAY_POPUP_MESSAGE', |
||||||
|
payload: message |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export const hidePopUp = () => { |
||||||
|
return { |
||||||
|
type: 'HIDE_POPUP_MESSAGE' |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export const focusElement = (elements: { key: string, type: 'file' | 'folder' | 'gist' }[]) => { |
||||||
|
return { |
||||||
|
type: 'SET_FOCUS_ELEMENT', |
||||||
|
payload: elements |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export const setContextMenuItem = (item: action) => { |
||||||
|
return { |
||||||
|
type: 'SET_CONTEXT_MENU_ITEM', |
||||||
|
payload: item |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export const removeContextMenuItem = (plugin) => { |
||||||
|
return { |
||||||
|
type: 'REMOVE_CONTEXT_MENU_ITEM', |
||||||
|
payload: plugin |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export const setExpandPath = (paths: string[]) => { |
||||||
|
return { |
||||||
|
type: 'SET_EXPAND_PATH', |
||||||
|
payload: paths |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export const loadLocalhostError = (error: any) => { |
||||||
|
return { |
||||||
|
type: 'LOAD_LOCALHOST_ERROR', |
||||||
|
payload: error |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export const loadLocalhostRequest = () => { |
||||||
|
return { |
||||||
|
type: 'LOAD_LOCALHOST_REQUEST' |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export const loadLocalhostSuccess = () => { |
||||||
|
return { |
||||||
|
type: 'LOAD_LOCALHOST_SUCCESS' |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export const fsInitializationCompleted = () => { |
||||||
|
return { |
||||||
|
type: 'FS_INITIALIZATION_COMPLETED' |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,299 @@ |
|||||||
|
import React from 'react' |
||||||
|
import { bufferToHex, keccakFromString } from 'ethereumjs-util' |
||||||
|
import axios, { AxiosResponse } from 'axios' |
||||||
|
import { addInputFieldSuccess, createWorkspaceError, createWorkspaceRequest, createWorkspaceSuccess, displayNotification, fetchWorkspaceDirectoryError, fetchWorkspaceDirectoryRequest, fetchWorkspaceDirectorySuccess, hideNotification, setCurrentWorkspace, setDeleteWorkspace, setMode, setReadOnlyMode, setRenameWorkspace } from './payload' |
||||||
|
import { checkSlash, checkSpecialChars } from '@remix-ui/helper' |
||||||
|
|
||||||
|
const examples = require('../../../../../../apps/remix-ide/src/app/editor/examples') |
||||||
|
const QueryParams = require('../../../../../../apps/remix-ide/src/lib/query-params') |
||||||
|
|
||||||
|
const LOCALHOST = ' - connect to localhost - ' |
||||||
|
const NO_WORKSPACE = ' - none - ' |
||||||
|
const queryParams = new QueryParams() |
||||||
|
let plugin, dispatch: React.Dispatch<any> |
||||||
|
|
||||||
|
export const setPlugin = (filePanelPlugin, reducerDispatch) => { |
||||||
|
plugin = filePanelPlugin |
||||||
|
dispatch = reducerDispatch |
||||||
|
} |
||||||
|
|
||||||
|
export const addInputField = async (type: 'file' | 'folder', path: string, cb?: (err: Error, result?: string | number | boolean | Record<string, any>) => void) => { |
||||||
|
const provider = plugin.fileManager.currentFileProvider() |
||||||
|
const promise = new Promise((resolve, reject) => { |
||||||
|
provider.resolveDirectory(path, (error, fileTree) => { |
||||||
|
if (error) { |
||||||
|
cb && cb(error) |
||||||
|
return reject(error) |
||||||
|
} |
||||||
|
|
||||||
|
cb && cb(null, true) |
||||||
|
resolve(fileTree) |
||||||
|
}) |
||||||
|
}) |
||||||
|
|
||||||
|
promise.then((files) => { |
||||||
|
dispatch(addInputFieldSuccess(path, files, type)) |
||||||
|
}).catch((error) => { |
||||||
|
console.error(error) |
||||||
|
}) |
||||||
|
return promise |
||||||
|
} |
||||||
|
|
||||||
|
export const createWorkspace = async (workspaceName: string, isEmpty = false, cb?: (err: Error, result?: string | number | boolean | Record<string, any>) => void) => { |
||||||
|
await plugin.fileManager.closeAllFiles() |
||||||
|
const promise = createWorkspaceTemplate(workspaceName, 'default-template') |
||||||
|
|
||||||
|
dispatch(createWorkspaceRequest(promise)) |
||||||
|
promise.then(async () => { |
||||||
|
dispatch(createWorkspaceSuccess(workspaceName)) |
||||||
|
plugin.setWorkspace({ name: workspaceName, isLocalhost: false }) |
||||||
|
plugin.setWorkspaces(await getWorkspaces()) |
||||||
|
plugin.workspaceCreated(workspaceName) |
||||||
|
if (!isEmpty) await loadWorkspacePreset('default-template') |
||||||
|
cb && cb(null, workspaceName) |
||||||
|
}).catch((error) => { |
||||||
|
dispatch(createWorkspaceError({ error })) |
||||||
|
cb && cb(error) |
||||||
|
}) |
||||||
|
return promise |
||||||
|
} |
||||||
|
|
||||||
|
export const createWorkspaceTemplate = async (workspaceName: string, template: 'gist-template' | 'code-template' | 'default-template' = 'default-template') => { |
||||||
|
if (!workspaceName) throw new Error('workspace name cannot be empty') |
||||||
|
if (checkSpecialChars(workspaceName) || checkSlash(workspaceName)) throw new Error('special characters are not allowed') |
||||||
|
if (await workspaceExists(workspaceName) && template === 'default-template') throw new Error('workspace already exists') |
||||||
|
else { |
||||||
|
const workspaceProvider = plugin.fileProviders.workspace |
||||||
|
|
||||||
|
await workspaceProvider.createWorkspace(workspaceName) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export const loadWorkspacePreset = async (template: 'gist-template' | 'code-template' | 'default-template' = 'default-template') => { |
||||||
|
const workspaceProvider = plugin.fileProviders.workspace |
||||||
|
const params = queryParams.get() |
||||||
|
|
||||||
|
switch (template) { |
||||||
|
case 'code-template': |
||||||
|
// creates a new workspace code-sample and loads code from url params.
|
||||||
|
try { |
||||||
|
let path = ''; let content = '' |
||||||
|
|
||||||
|
if (params.code) { |
||||||
|
const hash = bufferToHex(keccakFromString(params.code)) |
||||||
|
|
||||||
|
path = 'contract-' + hash.replace('0x', '').substring(0, 10) + '.sol' |
||||||
|
content = atob(params.code) |
||||||
|
workspaceProvider.set(path, content) |
||||||
|
} |
||||||
|
if (params.url) { |
||||||
|
const data = await plugin.call('contentImport', 'resolve', params.url) |
||||||
|
|
||||||
|
path = data.cleanUrl |
||||||
|
content = data.content |
||||||
|
workspaceProvider.set(path, content) |
||||||
|
} |
||||||
|
return path |
||||||
|
} catch (e) { |
||||||
|
console.error(e) |
||||||
|
} |
||||||
|
break |
||||||
|
|
||||||
|
case 'gist-template': |
||||||
|
// creates a new workspace gist-sample and get the file from gist
|
||||||
|
try { |
||||||
|
const gistId = params.gist |
||||||
|
const response: AxiosResponse = await axios.get(`https://api.github.com/gists/${gistId}`) |
||||||
|
const data = response.data as { files: any } |
||||||
|
|
||||||
|
if (!data.files) { |
||||||
|
return dispatch(displayNotification('Gist load error', 'No files found', 'OK', null, () => { dispatch(hideNotification()) }, null)) |
||||||
|
} |
||||||
|
const obj = {} |
||||||
|
|
||||||
|
Object.keys(data.files).forEach((element) => { |
||||||
|
const path = element.replace(/\.\.\./g, '/') |
||||||
|
|
||||||
|
obj['/' + 'gist-' + gistId + '/' + path] = data.files[element] |
||||||
|
}) |
||||||
|
plugin.fileManager.setBatchFiles(obj, 'workspace', true, (errorLoadingFile) => { |
||||||
|
if (!errorLoadingFile) { |
||||||
|
const provider = plugin.fileManager.getProvider('workspace') |
||||||
|
|
||||||
|
provider.lastLoadedGistId = gistId |
||||||
|
} else { |
||||||
|
dispatch(displayNotification('', errorLoadingFile.message || errorLoadingFile, 'OK', null, () => {}, null)) |
||||||
|
} |
||||||
|
}) |
||||||
|
} catch (e) { |
||||||
|
dispatch(displayNotification('Gist load error', e.message, 'OK', null, () => { dispatch(hideNotification()) }, null)) |
||||||
|
console.error(e) |
||||||
|
} |
||||||
|
break |
||||||
|
|
||||||
|
case 'default-template': |
||||||
|
// creates a new workspace and populates it with default project template.
|
||||||
|
// insert example contracts
|
||||||
|
for (const file in examples) { |
||||||
|
try { |
||||||
|
await workspaceProvider.set(examples[file].name, examples[file].content) |
||||||
|
} catch (error) { |
||||||
|
console.error(error) |
||||||
|
} |
||||||
|
} |
||||||
|
break |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export const workspaceExists = async (name: string) => { |
||||||
|
const workspaceProvider = plugin.fileProviders.workspace |
||||||
|
const browserProvider = plugin.fileProviders.browser |
||||||
|
const workspacePath = 'browser/' + workspaceProvider.workspacesPath + '/' + name |
||||||
|
|
||||||
|
return browserProvider.exists(workspacePath) |
||||||
|
} |
||||||
|
|
||||||
|
export const fetchWorkspaceDirectory = async (path: string) => { |
||||||
|
if (!path) return |
||||||
|
const provider = plugin.fileManager.currentFileProvider() |
||||||
|
const promise = new Promise((resolve) => { |
||||||
|
provider.resolveDirectory(path, (error, fileTree) => { |
||||||
|
if (error) console.error(error) |
||||||
|
|
||||||
|
resolve(fileTree) |
||||||
|
}) |
||||||
|
}) |
||||||
|
|
||||||
|
dispatch(fetchWorkspaceDirectoryRequest(promise)) |
||||||
|
promise.then((fileTree) => { |
||||||
|
dispatch(fetchWorkspaceDirectorySuccess(path, fileTree)) |
||||||
|
}).catch((error) => { |
||||||
|
dispatch(fetchWorkspaceDirectoryError({ error })) |
||||||
|
}) |
||||||
|
return promise |
||||||
|
} |
||||||
|
|
||||||
|
export const renameWorkspace = async (oldName: string, workspaceName: string, cb?: (err: Error, result?: string | number | boolean | Record<string, any>) => void) => { |
||||||
|
await renameWorkspaceFromProvider(oldName, workspaceName) |
||||||
|
await dispatch(setRenameWorkspace(oldName, workspaceName)) |
||||||
|
plugin.setWorkspace({ name: workspaceName, isLocalhost: false }) |
||||||
|
plugin.workspaceRenamed(oldName, workspaceName) |
||||||
|
cb && cb(null, workspaceName) |
||||||
|
} |
||||||
|
|
||||||
|
export const renameWorkspaceFromProvider = async (oldName: string, workspaceName: string) => { |
||||||
|
if (!workspaceName) throw new Error('name cannot be empty') |
||||||
|
if (checkSpecialChars(workspaceName) || checkSlash(workspaceName)) throw new Error('special characters are not allowed') |
||||||
|
if (await workspaceExists(workspaceName)) throw new Error('workspace already exists') |
||||||
|
const browserProvider = plugin.fileProviders.browser |
||||||
|
const workspaceProvider = plugin.fileProviders.workspace |
||||||
|
const workspacesPath = workspaceProvider.workspacesPath |
||||||
|
browserProvider.rename('browser/' + workspacesPath + '/' + oldName, 'browser/' + workspacesPath + '/' + workspaceName, true) |
||||||
|
workspaceProvider.setWorkspace(workspaceName) |
||||||
|
plugin.setWorkspaces(await getWorkspaces()) |
||||||
|
} |
||||||
|
|
||||||
|
export const deleteWorkspace = async (workspaceName: string, cb?: (err: Error, result?: string | number | boolean | Record<string, any>) => void) => { |
||||||
|
await deleteWorkspaceFromProvider(workspaceName) |
||||||
|
await dispatch(setDeleteWorkspace(workspaceName)) |
||||||
|
plugin.workspaceDeleted(workspaceName) |
||||||
|
cb && cb(null, workspaceName) |
||||||
|
} |
||||||
|
|
||||||
|
const deleteWorkspaceFromProvider = async (workspaceName: string) => { |
||||||
|
const workspacesPath = plugin.fileProviders.workspace.workspacesPath |
||||||
|
|
||||||
|
await plugin.fileManager.closeAllFiles() |
||||||
|
plugin.fileProviders.browser.remove(workspacesPath + '/' + workspaceName) |
||||||
|
plugin.setWorkspaces(await getWorkspaces()) |
||||||
|
} |
||||||
|
|
||||||
|
export const switchToWorkspace = async (name: string) => { |
||||||
|
await plugin.fileManager.closeAllFiles() |
||||||
|
if (name === LOCALHOST) { |
||||||
|
const isActive = await plugin.call('manager', 'isActive', 'remixd') |
||||||
|
|
||||||
|
if (!isActive) await plugin.call('manager', 'activatePlugin', 'remixd') |
||||||
|
dispatch(setMode('localhost')) |
||||||
|
plugin.emit('setWorkspace', { name: null, isLocalhost: true }) |
||||||
|
} else if (name === NO_WORKSPACE) { |
||||||
|
plugin.fileProviders.workspace.clearWorkspace() |
||||||
|
plugin.setWorkspace({ name: null, isLocalhost: false }) |
||||||
|
dispatch(setCurrentWorkspace(null)) |
||||||
|
} else { |
||||||
|
const isActive = await plugin.call('manager', 'isActive', 'remixd') |
||||||
|
|
||||||
|
if (isActive) plugin.call('manager', 'deactivatePlugin', 'remixd') |
||||||
|
await plugin.fileProviders.workspace.setWorkspace(name) |
||||||
|
plugin.setWorkspace({ name, isLocalhost: false }) |
||||||
|
dispatch(setMode('browser')) |
||||||
|
dispatch(setCurrentWorkspace(name)) |
||||||
|
dispatch(setReadOnlyMode(false)) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export const uploadFile = async (target, targetFolder: string, cb?: (err: Error, result?: string | number | boolean | Record<string, any>) => void) => { |
||||||
|
// TODO The file explorer is merely a view on the current state of
|
||||||
|
// the files module. Please ask the user here if they want to overwrite
|
||||||
|
// a file and then just use `files.add`. The file explorer will
|
||||||
|
// pick that up via the 'fileAdded' event from the files module.
|
||||||
|
[...target.files].forEach((file) => { |
||||||
|
const workspaceProvider = plugin.fileProviders.workspace |
||||||
|
const loadFile = (name: string): void => { |
||||||
|
const fileReader = new FileReader() |
||||||
|
|
||||||
|
fileReader.onload = async function (event) { |
||||||
|
if (checkSpecialChars(file.name)) { |
||||||
|
return dispatch(displayNotification('File Upload Failed', 'Special characters are not allowed', 'Close', null, async () => {})) |
||||||
|
} |
||||||
|
const success = await workspaceProvider.set(name, event.target.result) |
||||||
|
|
||||||
|
if (!success) { |
||||||
|
return dispatch(displayNotification('File Upload Failed', 'Failed to create file ' + name, 'Close', null, async () => {})) |
||||||
|
} |
||||||
|
const config = plugin.registry.get('config').api |
||||||
|
const editor = plugin.registry.get('editor').api |
||||||
|
|
||||||
|
if ((config.get('currentFile') === name) && (editor.currentContent() !== event.target.result)) { |
||||||
|
editor.setText(event.target.result) |
||||||
|
} |
||||||
|
} |
||||||
|
fileReader.readAsText(file) |
||||||
|
cb && cb(null, true) |
||||||
|
} |
||||||
|
const name = `${targetFolder}/${file.name}` |
||||||
|
|
||||||
|
workspaceProvider.exists(name).then(exist => { |
||||||
|
if (!exist) { |
||||||
|
loadFile(name) |
||||||
|
} else { |
||||||
|
dispatch(displayNotification('Confirm overwrite', `The file ${name} already exists! Would you like to overwrite it?`, 'OK', null, () => { |
||||||
|
loadFile(name) |
||||||
|
}, () => {})) |
||||||
|
} |
||||||
|
}).catch(error => { |
||||||
|
cb && cb(error) |
||||||
|
if (error) console.log(error) |
||||||
|
}) |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
export const getWorkspaces = async (): Promise<string[]> | undefined => { |
||||||
|
try { |
||||||
|
const workspaces: string[] = await new Promise((resolve, reject) => { |
||||||
|
const workspacesPath = plugin.fileProviders.workspace.workspacesPath |
||||||
|
|
||||||
|
plugin.fileProviders.browser.resolveDirectory('/' + workspacesPath, (error, items) => { |
||||||
|
if (error) { |
||||||
|
return reject(error) |
||||||
|
} |
||||||
|
resolve(Object.keys(items) |
||||||
|
.filter((item) => items[item].isDirectory) |
||||||
|
.map((folder) => folder.replace(workspacesPath + '/', ''))) |
||||||
|
}) |
||||||
|
}) |
||||||
|
|
||||||
|
plugin.setWorkspaces(workspaces) |
||||||
|
return workspaces |
||||||
|
} catch (e) {} |
||||||
|
} |
@ -1,7 +1,7 @@ |
|||||||
import React, { useRef, useEffect } from 'react' // eslint-disable-line
|
import React, { useRef, useEffect } from 'react' // eslint-disable-line
|
||||||
import { action, FileExplorerContextMenuProps } from './types' |
import { action, FileExplorerContextMenuProps } from '../types' |
||||||
|
|
||||||
import './css/file-explorer-context-menu.css' |
import '../css/file-explorer-context-menu.css' |
||||||
import { customAction } from '@remixproject/plugin-api/lib/file-system/file-panel' |
import { customAction } from '@remixproject/plugin-api/lib/file-system/file-panel' |
||||||
|
|
||||||
declare global { |
declare global { |
@ -1,5 +1,5 @@ |
|||||||
import React, { useState, useEffect } from 'react' //eslint-disable-line
|
import React, { useState, useEffect } from 'react' //eslint-disable-line
|
||||||
import { FileExplorerMenuProps } from './types' |
import { FileExplorerMenuProps } from '../types' |
||||||
|
|
||||||
export const FileExplorerMenu = (props: FileExplorerMenuProps) => { |
export const FileExplorerMenu = (props: FileExplorerMenuProps) => { |
||||||
const [state, setState] = useState({ |
const [state, setState] = useState({ |
@ -0,0 +1,473 @@ |
|||||||
|
import React, { useEffect, useState, useContext, SyntheticEvent } from 'react' // eslint-disable-line
|
||||||
|
import { TreeView, TreeViewItem } from '@remix-ui/tree-view' // eslint-disable-line
|
||||||
|
import { FileExplorerMenu } from './file-explorer-menu' // eslint-disable-line
|
||||||
|
import { FileExplorerContextMenu } from './file-explorer-context-menu' // eslint-disable-line
|
||||||
|
import { FileExplorerProps, MenuItems, FileExplorerState } from '../types' |
||||||
|
import { customAction } from '@remixproject/plugin-api/lib/file-system/file-panel' |
||||||
|
import { contextMenuActions } from '../utils' |
||||||
|
|
||||||
|
import '../css/file-explorer.css' |
||||||
|
import { checkSpecialChars, extractNameFromKey, extractParentFromKey, joinPath } from '@remix-ui/helper' |
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
import { FileRender } from './file-render' |
||||||
|
|
||||||
|
export const FileExplorer = (props: FileExplorerProps) => { |
||||||
|
const { name, contextMenuItems, removedContextMenuItems, files } = props |
||||||
|
const [state, setState] = useState<FileExplorerState>({ |
||||||
|
ctrlKey: false, |
||||||
|
newFileName: '', |
||||||
|
actions: contextMenuActions, |
||||||
|
focusContext: { |
||||||
|
element: null, |
||||||
|
x: null, |
||||||
|
y: null, |
||||||
|
type: '' |
||||||
|
}, |
||||||
|
focusEdit: { |
||||||
|
element: null, |
||||||
|
type: '', |
||||||
|
isNew: false, |
||||||
|
lastEdit: '' |
||||||
|
}, |
||||||
|
mouseOverElement: null, |
||||||
|
showContextMenu: false, |
||||||
|
reservedKeywords: [name, 'gist-'], |
||||||
|
copyElement: [] |
||||||
|
}) |
||||||
|
const [canPaste, setCanPaste] = useState(false) |
||||||
|
|
||||||
|
useEffect(() => { |
||||||
|
if (contextMenuItems) { |
||||||
|
addMenuItems(contextMenuItems) |
||||||
|
} |
||||||
|
}, [contextMenuItems]) |
||||||
|
|
||||||
|
useEffect(() => { |
||||||
|
if (removedContextMenuItems) { |
||||||
|
removeMenuItems(removedContextMenuItems) |
||||||
|
} |
||||||
|
}, [contextMenuItems]) |
||||||
|
|
||||||
|
useEffect(() => { |
||||||
|
if (props.focusEdit) { |
||||||
|
setState(prevState => { |
||||||
|
return { ...prevState, focusEdit: { element: props.focusEdit, type: 'file', isNew: true, lastEdit: null } } |
||||||
|
}) |
||||||
|
} |
||||||
|
}, [props.focusEdit]) |
||||||
|
|
||||||
|
useEffect(() => { |
||||||
|
const keyPressHandler = (e: KeyboardEvent) => { |
||||||
|
if (e.shiftKey) { |
||||||
|
setState(prevState => { |
||||||
|
return { ...prevState, ctrlKey: true } |
||||||
|
}) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
const keyUpHandler = (e: KeyboardEvent) => { |
||||||
|
if (!e.shiftKey) { |
||||||
|
setState(prevState => { |
||||||
|
return { ...prevState, ctrlKey: false } |
||||||
|
}) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
document.addEventListener('keydown', keyPressHandler) |
||||||
|
document.addEventListener('keyup', keyUpHandler) |
||||||
|
return () => { |
||||||
|
document.removeEventListener('keydown', keyPressHandler) |
||||||
|
document.removeEventListener('keyup', keyUpHandler) |
||||||
|
} |
||||||
|
}, []) |
||||||
|
|
||||||
|
useEffect(() => { |
||||||
|
if (canPaste) { |
||||||
|
addMenuItems([{ |
||||||
|
id: 'paste', |
||||||
|
name: 'Paste', |
||||||
|
type: ['folder', 'file'], |
||||||
|
path: [], |
||||||
|
extension: [], |
||||||
|
pattern: [], |
||||||
|
multiselect: false, |
||||||
|
label: '' |
||||||
|
}]) |
||||||
|
} else { |
||||||
|
removeMenuItems([{ |
||||||
|
id: 'paste', |
||||||
|
name: 'Paste', |
||||||
|
type: ['folder', 'file'], |
||||||
|
path: [], |
||||||
|
extension: [], |
||||||
|
pattern: [], |
||||||
|
multiselect: false, |
||||||
|
label: '' |
||||||
|
}]) |
||||||
|
} |
||||||
|
}, [canPaste]) |
||||||
|
|
||||||
|
const addMenuItems = (items: MenuItems) => { |
||||||
|
setState(prevState => { |
||||||
|
// filter duplicate items
|
||||||
|
const actions = items.filter(({ name }) => prevState.actions.findIndex(action => action.name === name) === -1) |
||||||
|
|
||||||
|
return { ...prevState, actions: [...prevState.actions, ...actions] } |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
const removeMenuItems = (items: MenuItems) => { |
||||||
|
setState(prevState => { |
||||||
|
const actions = prevState.actions.filter(({ id, name }) => items.findIndex(item => id === item.id && name === item.name) === -1) |
||||||
|
return { ...prevState, actions } |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
const hasReservedKeyword = (content: string): boolean => { |
||||||
|
if (state.reservedKeywords.findIndex(value => content.startsWith(value)) !== -1) return true |
||||||
|
else return false |
||||||
|
} |
||||||
|
|
||||||
|
const getFocusedFolder = () => { |
||||||
|
if (props.focusElement[0]) { |
||||||
|
if (props.focusElement[0].type === 'folder' && props.focusElement[0].key) return props.focusElement[0].key |
||||||
|
else if (props.focusElement[0].type === 'gist' && props.focusElement[0].key) return props.focusElement[0].key |
||||||
|
else if (props.focusElement[0].type === 'file' && props.focusElement[0].key) return extractParentFromKey(props.focusElement[0].key) ? extractParentFromKey(props.focusElement[0].key) : name |
||||||
|
else return name |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
const createNewFile = async (newFilePath: string) => { |
||||||
|
try { |
||||||
|
props.dispatchCreateNewFile(newFilePath, props.name) |
||||||
|
} catch (error) { |
||||||
|
return props.modal('File Creation Failed', typeof error === 'string' ? error : error.message, 'Close', async () => {}) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
const createNewFolder = async (newFolderPath: string) => { |
||||||
|
try { |
||||||
|
props.dispatchCreateNewFolder(newFolderPath, props.name) |
||||||
|
} catch (e) { |
||||||
|
return props.modal('Folder Creation Failed', typeof e === 'string' ? e : e.message, 'Close', async () => {}) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
const deletePath = async (path: string[]) => { |
||||||
|
if (props.readonly) return props.toast('cannot delete file. ' + name + ' is a read only explorer') |
||||||
|
if (!Array.isArray(path)) path = [path] |
||||||
|
|
||||||
|
props.modal(`Delete ${path.length > 1 ? 'items' : 'item'}`, deleteMessage(path), 'OK', () => { props.dispatchDeletePath(path) }, 'Cancel', () => {}) |
||||||
|
} |
||||||
|
|
||||||
|
const renamePath = async (oldPath: string, newPath: string) => { |
||||||
|
try { |
||||||
|
props.dispatchRenamePath(oldPath, newPath) |
||||||
|
} catch (error) { |
||||||
|
props.modal('Rename File Failed', 'Unexpected error while renaming: ' + typeof error === 'string' ? error : error.message, 'Close', async () => {}) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
const uploadFile = (target) => { |
||||||
|
const parentFolder = getFocusedFolder() |
||||||
|
const expandPath = [...new Set([...props.expandPath, parentFolder])] |
||||||
|
|
||||||
|
props.dispatchHandleExpandPath(expandPath) |
||||||
|
props.dispatchUploadFile(target, parentFolder) |
||||||
|
} |
||||||
|
|
||||||
|
const copyFile = (src: string, dest: string) => { |
||||||
|
try { |
||||||
|
props.dispatchCopyFile(src, dest) |
||||||
|
} catch (error) { |
||||||
|
props.modal('Copy File Failed', 'Unexpected error while copying file: ' + src, 'Close', async () => {}) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
const copyFolder = (src: string, dest: string) => { |
||||||
|
try { |
||||||
|
props.dispatchCopyFolder(src, dest) |
||||||
|
} catch (error) { |
||||||
|
props.modal('Copy Folder Failed', 'Unexpected error while copying folder: ' + src, 'Close', async () => {}) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
const publishToGist = (path?: string, type?: string) => { |
||||||
|
props.modal('Create a public gist', `Are you sure you want to anonymously publish all your files in the ${name} workspace as a public gist on github.com?`, 'OK', () => toGist(path, type), 'Cancel', () => {}) |
||||||
|
} |
||||||
|
|
||||||
|
const pushChangesToGist = (path?: string, type?: string) => { |
||||||
|
props.modal('Create a public gist', 'Are you sure you want to push changes to remote gist file on github.com?', 'OK', () => toGist(path, type), 'Cancel', () => {}) |
||||||
|
} |
||||||
|
|
||||||
|
const publishFolderToGist = (path?: string, type?: string) => { |
||||||
|
props.modal('Create a public gist', `Are you sure you want to anonymously publish all your files in the ${path} folder as a public gist on github.com?`, 'OK', () => toGist(path, type), 'Cancel', () => {}) |
||||||
|
} |
||||||
|
|
||||||
|
const publishFileToGist = (path?: string, type?: string) => { |
||||||
|
props.modal('Create a public gist', `Are you sure you want to anonymously publish ${path} file as a public gist on github.com?`, 'OK', () => toGist(path, type), 'Cancel', () => {}) |
||||||
|
} |
||||||
|
|
||||||
|
const toGist = (path?: string, type?: string) => { |
||||||
|
props.dispatchPublishToGist(path, type) |
||||||
|
} |
||||||
|
|
||||||
|
const runScript = async (path: string) => { |
||||||
|
try { |
||||||
|
props.dispatchRunScript(path) |
||||||
|
} catch (error) { |
||||||
|
props.toast('Run script failed') |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
const emitContextMenuEvent = (cmd: customAction) => { |
||||||
|
try { |
||||||
|
props.dispatchEmitContextMenuEvent(cmd) |
||||||
|
} catch (error) { |
||||||
|
props.toast(error) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
const handleClickFile = (path: string, type: 'folder' | 'file' | 'gist') => { |
||||||
|
path = path.indexOf(props.name + '/') === 0 ? path.replace(props.name + '/', '') : path |
||||||
|
if (!state.ctrlKey) { |
||||||
|
props.dispatchHandleClickFile(path, type) |
||||||
|
} else { |
||||||
|
if (props.focusElement.findIndex(item => item.key === path) !== -1) { |
||||||
|
const focusElement = props.focusElement.filter(item => item.key !== path) |
||||||
|
|
||||||
|
props.dispatchSetFocusElement(focusElement) |
||||||
|
} else { |
||||||
|
const nonRootFocus = props.focusElement.filter((el) => { return !(el.key === '' && el.type === 'folder') }) |
||||||
|
|
||||||
|
nonRootFocus.push({ key: path, type }) |
||||||
|
props.dispatchSetFocusElement(nonRootFocus) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
const handleClickFolder = async (path: string, type: 'folder' | 'file' | 'gist') => { |
||||||
|
if (state.ctrlKey) { |
||||||
|
if (props.focusElement.findIndex(item => item.key === path) !== -1) { |
||||||
|
const focusElement = props.focusElement.filter(item => item.key !== path) |
||||||
|
|
||||||
|
props.dispatchSetFocusElement(focusElement) |
||||||
|
} else { |
||||||
|
const nonRootFocus = props.focusElement.filter((el) => { return !(el.key === '' && el.type === 'folder') }) |
||||||
|
|
||||||
|
nonRootFocus.push({ key: path, type }) |
||||||
|
props.dispatchSetFocusElement(nonRootFocus) |
||||||
|
} |
||||||
|
} else { |
||||||
|
let expandPath = [] |
||||||
|
|
||||||
|
if (!props.expandPath.includes(path)) { |
||||||
|
expandPath = [...new Set([...props.expandPath, path])] |
||||||
|
props.dispatchFetchDirectory(path) |
||||||
|
} else { |
||||||
|
expandPath = [...new Set(props.expandPath.filter(key => key && (typeof key === 'string') && !key.startsWith(path)))] |
||||||
|
} |
||||||
|
|
||||||
|
props.dispatchSetFocusElement([{ key: path, type }]) |
||||||
|
props.dispatchHandleExpandPath(expandPath) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
const handleContextMenu = (pageX: number, pageY: number, path: string, content: string, type: string) => { |
||||||
|
if (!content) return |
||||||
|
setState(prevState => { |
||||||
|
return { ...prevState, focusContext: { element: path, x: pageX, y: pageY, type }, focusEdit: { ...prevState.focusEdit, lastEdit: content }, showContextMenu: prevState.focusEdit.element !== path } |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
const hideContextMenu = () => { |
||||||
|
setState(prevState => { |
||||||
|
return { ...prevState, focusContext: { element: null, x: 0, y: 0, type: '' }, showContextMenu: false } |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
const editModeOn = (path: string, type: string, isNew: boolean = false) => { |
||||||
|
if (props.readonly) return props.toast('Cannot write/modify file system in read only mode.') |
||||||
|
setState(prevState => { |
||||||
|
return { ...prevState, focusEdit: { ...prevState.focusEdit, element: path, isNew, type } } |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
const editModeOff = async (content: string) => { |
||||||
|
if (typeof content === 'string') content = content.trim() |
||||||
|
const parentFolder = extractParentFromKey(state.focusEdit.element) |
||||||
|
|
||||||
|
if (!content || (content.trim() === '')) { |
||||||
|
if (state.focusEdit.isNew) { |
||||||
|
props.dispatchRemoveInputField(parentFolder) |
||||||
|
setState(prevState => { |
||||||
|
return { ...prevState, focusEdit: { element: null, isNew: false, type: '', lastEdit: '' } } |
||||||
|
}) |
||||||
|
} else { |
||||||
|
setState(prevState => { |
||||||
|
return { ...prevState, focusEdit: { element: null, isNew: false, type: '', lastEdit: '' } } |
||||||
|
}) |
||||||
|
} |
||||||
|
} else { |
||||||
|
if (state.focusEdit.lastEdit === content) { |
||||||
|
return setState(prevState => { |
||||||
|
return { ...prevState, focusEdit: { element: null, isNew: false, type: '', lastEdit: '' } } |
||||||
|
}) |
||||||
|
} |
||||||
|
if (checkSpecialChars(content)) { |
||||||
|
props.modal('Validation Error', 'Special characters are not allowed', 'OK', () => {}) |
||||||
|
} else { |
||||||
|
if (state.focusEdit.isNew) { |
||||||
|
if (hasReservedKeyword(content)) { |
||||||
|
props.dispatchRemoveInputField(parentFolder) |
||||||
|
props.modal('Reserved Keyword', `File name contains Remix reserved keywords. '${content}'`, 'Close', () => {}) |
||||||
|
} else { |
||||||
|
state.focusEdit.type === 'file' ? createNewFile(joinPath(parentFolder, content)) : createNewFolder(joinPath(parentFolder, content)) |
||||||
|
props.dispatchRemoveInputField(parentFolder) |
||||||
|
} |
||||||
|
} else { |
||||||
|
if (hasReservedKeyword(content)) { |
||||||
|
props.modal('Reserved Keyword', `File name contains Remix reserved keywords. '${content}'`, 'Close', () => {}) |
||||||
|
} else { |
||||||
|
if (state.focusEdit.element) { |
||||||
|
const oldPath: string = state.focusEdit.element |
||||||
|
const oldName = extractNameFromKey(oldPath) |
||||||
|
const newPath = oldPath.replace(oldName, content) |
||||||
|
|
||||||
|
renamePath(oldPath, newPath) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
setState(prevState => { |
||||||
|
return { ...prevState, focusEdit: { element: null, isNew: false, type: '', lastEdit: '' } } |
||||||
|
}) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
const handleNewFileInput = async (parentFolder?: string) => { |
||||||
|
if (!parentFolder) parentFolder = getFocusedFolder() |
||||||
|
const expandPath = [...new Set([...props.expandPath, parentFolder])] |
||||||
|
|
||||||
|
await props.dispatchAddInputField(parentFolder, 'file') |
||||||
|
props.dispatchHandleExpandPath(expandPath) |
||||||
|
editModeOn(parentFolder + '/blank', 'file', true) |
||||||
|
} |
||||||
|
|
||||||
|
const handleNewFolderInput = async (parentFolder?: string) => { |
||||||
|
if (!parentFolder) parentFolder = getFocusedFolder() |
||||||
|
else if ((parentFolder.indexOf('.sol') !== -1) || (parentFolder.indexOf('.js') !== -1)) parentFolder = extractParentFromKey(parentFolder) |
||||||
|
const expandPath = [...new Set([...props.expandPath, parentFolder])] |
||||||
|
|
||||||
|
await props.dispatchAddInputField(parentFolder, 'folder') |
||||||
|
props.dispatchHandleExpandPath(expandPath) |
||||||
|
editModeOn(parentFolder + '/blank', 'folder', true) |
||||||
|
} |
||||||
|
|
||||||
|
const handleCopyClick = (path: string, type: 'folder' | 'gist' | 'file') => { |
||||||
|
setState(prevState => { |
||||||
|
return { ...prevState, copyElement: [{ key: path, type }] } |
||||||
|
}) |
||||||
|
setCanPaste(true) |
||||||
|
props.toast(`Copied to clipboard ${path}`) |
||||||
|
} |
||||||
|
|
||||||
|
const handlePasteClick = (dest: string, destType: string) => { |
||||||
|
dest = destType === 'file' ? extractParentFromKey(dest) || props.name : dest |
||||||
|
state.copyElement.map(({ key, type }) => { |
||||||
|
type === 'file' ? copyFile(key, dest) : copyFolder(key, dest) |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
const deleteMessage = (path: string[]) => { |
||||||
|
return ( |
||||||
|
<div> |
||||||
|
<div>Are you sure you want to delete {path.length > 1 ? 'these items' : 'this item'}?</div> |
||||||
|
{ |
||||||
|
path.map((item, i) => (<li key={i}>{item}</li>)) |
||||||
|
} |
||||||
|
</div> |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
const handleFileExplorerMenuClick = (e: SyntheticEvent) => { |
||||||
|
e.stopPropagation() |
||||||
|
if (e && (e.target as any).getAttribute('data-id') === 'fileExplorerUploadFileuploadFile') return // we don't want to let propagate the input of type file
|
||||||
|
if (e && (e.target as any).getAttribute('data-id') === 'fileExplorerFileUpload') return // we don't want to let propagate the input of type file
|
||||||
|
let expandPath = [] |
||||||
|
|
||||||
|
if (!props.expandPath.includes(props.name)) { |
||||||
|
expandPath = [props.name, ...new Set([...props.expandPath])] |
||||||
|
} else { |
||||||
|
expandPath = [...new Set(props.expandPath.filter(key => key && (typeof key === 'string') && !key.startsWith(props.name)))] |
||||||
|
} |
||||||
|
props.dispatchHandleExpandPath(expandPath) |
||||||
|
} |
||||||
|
|
||||||
|
return ( |
||||||
|
<div> |
||||||
|
<TreeView id='treeView'> |
||||||
|
<TreeViewItem id="treeViewItem" |
||||||
|
controlBehaviour={true} |
||||||
|
label={ |
||||||
|
<div onClick={handleFileExplorerMenuClick}> |
||||||
|
<FileExplorerMenu |
||||||
|
title={''} |
||||||
|
menuItems={props.menuItems} |
||||||
|
createNewFile={handleNewFileInput} |
||||||
|
createNewFolder={handleNewFolderInput} |
||||||
|
publishToGist={publishToGist} |
||||||
|
uploadFile={uploadFile} |
||||||
|
/> |
||||||
|
</div> |
||||||
|
} |
||||||
|
expand={true}> |
||||||
|
<div className='pb-2'> |
||||||
|
<TreeView id='treeViewMenu'> |
||||||
|
{ |
||||||
|
files[props.name] && Object.keys(files[props.name]).map((key, index) => <FileRender |
||||||
|
file={files[props.name][key]} |
||||||
|
index={index} |
||||||
|
focusContext={state.focusContext} |
||||||
|
focusEdit={state.focusEdit} |
||||||
|
focusElement={props.focusElement} |
||||||
|
ctrlKey={state.ctrlKey} |
||||||
|
expandPath={props.expandPath} |
||||||
|
editModeOff={editModeOff} |
||||||
|
handleClickFile={handleClickFile} |
||||||
|
handleClickFolder={handleClickFolder} |
||||||
|
handleContextMenu={handleContextMenu} |
||||||
|
key={index} |
||||||
|
/>) |
||||||
|
} |
||||||
|
</TreeView> |
||||||
|
</div> |
||||||
|
</TreeViewItem> |
||||||
|
</TreeView> |
||||||
|
{ state.showContextMenu && |
||||||
|
<FileExplorerContextMenu |
||||||
|
actions={props.focusElement.length > 1 ? state.actions.filter(item => item.multiselect) : state.actions.filter(item => !item.multiselect)} |
||||||
|
hideContextMenu={hideContextMenu} |
||||||
|
createNewFile={handleNewFileInput} |
||||||
|
createNewFolder={handleNewFolderInput} |
||||||
|
deletePath={deletePath} |
||||||
|
renamePath={editModeOn} |
||||||
|
runScript={runScript} |
||||||
|
copy={handleCopyClick} |
||||||
|
paste={handlePasteClick} |
||||||
|
emit={emitContextMenuEvent} |
||||||
|
pageX={state.focusContext.x} |
||||||
|
pageY={state.focusContext.y} |
||||||
|
path={state.focusContext.element} |
||||||
|
type={state.focusContext.type} |
||||||
|
focus={props.focusElement} |
||||||
|
pushChangesToGist={pushChangesToGist} |
||||||
|
publishFolderToGist={publishFolderToGist} |
||||||
|
publishFileToGist={publishFileToGist} |
||||||
|
/> |
||||||
|
} |
||||||
|
</div> |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
export default FileExplorer |
@ -0,0 +1,67 @@ |
|||||||
|
// eslint-disable-next-line no-use-before-define
|
||||||
|
import React, { useEffect, useRef, useState } from 'react' |
||||||
|
import { FileType } from '../types' |
||||||
|
|
||||||
|
export interface FileLabelProps { |
||||||
|
file: FileType, |
||||||
|
focusEdit: { |
||||||
|
element: string |
||||||
|
type: string |
||||||
|
isNew: boolean |
||||||
|
lastEdit: string |
||||||
|
} |
||||||
|
editModeOff: (content: string) => void |
||||||
|
} |
||||||
|
|
||||||
|
export const FileLabel = (props: FileLabelProps) => { |
||||||
|
const { file, focusEdit, editModeOff } = props |
||||||
|
const [isEditable, setIsEditable] = useState<boolean>(false) |
||||||
|
const labelRef = useRef(null) |
||||||
|
|
||||||
|
useEffect(() => { |
||||||
|
if (focusEdit.element && file.path) { |
||||||
|
setIsEditable(focusEdit.element === file.path) |
||||||
|
} |
||||||
|
}, [file.path, focusEdit]) |
||||||
|
|
||||||
|
useEffect(() => { |
||||||
|
if (labelRef.current) { |
||||||
|
setTimeout(() => { |
||||||
|
labelRef.current.focus() |
||||||
|
}, 0) |
||||||
|
} |
||||||
|
}, [isEditable]) |
||||||
|
|
||||||
|
const handleEditInput = (event: React.KeyboardEvent<HTMLDivElement>) => { |
||||||
|
if (event.which === 13) { |
||||||
|
event.preventDefault() |
||||||
|
editModeOff(labelRef.current.innerText) |
||||||
|
labelRef.current.innerText = file.name |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
const handleEditBlur = (event: React.SyntheticEvent) => { |
||||||
|
event.stopPropagation() |
||||||
|
editModeOff(labelRef.current.innerText) |
||||||
|
labelRef.current.innerText = file.name |
||||||
|
} |
||||||
|
|
||||||
|
return ( |
||||||
|
<div |
||||||
|
className='remixui_items d-inline-block w-100' |
||||||
|
ref={isEditable ? labelRef : null} |
||||||
|
suppressContentEditableWarning={true} |
||||||
|
contentEditable={isEditable} |
||||||
|
onKeyDown={handleEditInput} |
||||||
|
onBlur={handleEditBlur} |
||||||
|
> |
||||||
|
<span |
||||||
|
title={file.path} |
||||||
|
className={'remixui_label ' + (file.isDirectory ? 'folder' : 'remixui_leaf')} |
||||||
|
data-path={file.path} |
||||||
|
> |
||||||
|
{ file.name } |
||||||
|
</span> |
||||||
|
</div> |
||||||
|
) |
||||||
|
} |
@ -0,0 +1,124 @@ |
|||||||
|
// eslint-disable-next-line no-use-before-define
|
||||||
|
import React, { SyntheticEvent, useEffect, useState } from 'react' |
||||||
|
import { FileType } from '../types' |
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
import { TreeView, TreeViewItem } from '@remix-ui/tree-view' |
||||||
|
import { getPathIcon } from '@remix-ui/helper' |
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
import { FileLabel } from './file-label' |
||||||
|
|
||||||
|
export interface RenderFileProps { |
||||||
|
file: FileType, |
||||||
|
index: number, |
||||||
|
focusEdit: { element: string, type: string, isNew: boolean, lastEdit: string }, |
||||||
|
focusElement: { key: string, type: 'file' | 'folder' | 'gist' }[], |
||||||
|
focusContext: { element: string, x: number, y: number, type: string }, |
||||||
|
ctrlKey: boolean, |
||||||
|
expandPath: string[], |
||||||
|
editModeOff: (content: string) => void, |
||||||
|
handleClickFolder: (path: string, type: string) => void, |
||||||
|
handleClickFile: (path: string, type: string) => void, |
||||||
|
handleContextMenu: (pageX: number, pageY: number, path: string, content: string, type: string) => void |
||||||
|
} |
||||||
|
|
||||||
|
export const FileRender = (props: RenderFileProps) => { |
||||||
|
const [file, setFile] = useState<FileType>({} as FileType) |
||||||
|
const [hover, setHover] = useState<boolean>(false) |
||||||
|
const [icon, setIcon] = useState<string>('') |
||||||
|
|
||||||
|
useEffect(() => { |
||||||
|
if (props.file && props.file.path && props.file.type) { |
||||||
|
setFile(props.file) |
||||||
|
setIcon(getPathIcon(props.file.path)) |
||||||
|
} |
||||||
|
}, [props.file]) |
||||||
|
|
||||||
|
const labelClass = props.focusEdit.element === file.path |
||||||
|
? 'bg-light' : props.focusElement.findIndex(item => item.key === file.path) !== -1 |
||||||
|
? 'bg-secondary' : hover |
||||||
|
? 'bg-light border' : (props.focusContext.element === file.path) && (props.focusEdit.element !== file.path) |
||||||
|
? 'bg-light border' : '' |
||||||
|
|
||||||
|
const spreadProps = { |
||||||
|
onClick: (e) => e.stopPropagation() |
||||||
|
} |
||||||
|
|
||||||
|
const handleFolderClick = (event: SyntheticEvent) => { |
||||||
|
event.stopPropagation() |
||||||
|
if (props.focusEdit.element !== file.path) props.handleClickFolder(file.path, file.type) |
||||||
|
} |
||||||
|
|
||||||
|
const handleFileClick = (event: SyntheticEvent) => { |
||||||
|
event.stopPropagation() |
||||||
|
if (props.focusEdit.element !== file.path) props.handleClickFile(file.path, file.type) |
||||||
|
} |
||||||
|
|
||||||
|
const handleContextMenu = (event: PointerEvent) => { |
||||||
|
event.preventDefault() |
||||||
|
event.stopPropagation() |
||||||
|
props.handleContextMenu(event.pageX, event.pageY, file.path, (event.target as HTMLElement).textContent, file.type) |
||||||
|
} |
||||||
|
|
||||||
|
const handleMouseOut = (event: SyntheticEvent) => { |
||||||
|
event.stopPropagation() |
||||||
|
setHover(false) |
||||||
|
} |
||||||
|
|
||||||
|
const handleMouseOver = (event: SyntheticEvent) => { |
||||||
|
event.stopPropagation() |
||||||
|
setHover(true) |
||||||
|
} |
||||||
|
|
||||||
|
if (file.isDirectory) { |
||||||
|
return ( |
||||||
|
<TreeViewItem |
||||||
|
id={`treeViewItem${file.path}`} |
||||||
|
iconX='pr-3 fa fa-folder' |
||||||
|
iconY='pr-3 fa fa-folder-open' |
||||||
|
key={`${file.path + props.index}`} |
||||||
|
label={<FileLabel file={file} focusEdit={props.focusEdit} editModeOff={props.editModeOff} />} |
||||||
|
onClick={handleFolderClick} |
||||||
|
onContextMenu={handleContextMenu} |
||||||
|
labelClass={labelClass} |
||||||
|
controlBehaviour={ props.ctrlKey } |
||||||
|
expand={props.expandPath.includes(file.path)} |
||||||
|
onMouseOver={handleMouseOver} |
||||||
|
onMouseOut={handleMouseOut} |
||||||
|
> |
||||||
|
{ |
||||||
|
file.child ? <TreeView id={`treeView${file.path}`} key={`treeView${file.path}`} {...spreadProps }>{ |
||||||
|
Object.keys(file.child).map((key, index) => <FileRender |
||||||
|
file={file.child[key]} |
||||||
|
index={index} |
||||||
|
focusContext={props.focusContext} |
||||||
|
focusEdit={props.focusEdit} |
||||||
|
focusElement={props.focusElement} |
||||||
|
ctrlKey={props.ctrlKey} |
||||||
|
editModeOff={props.editModeOff} |
||||||
|
handleClickFile={props.handleClickFile} |
||||||
|
handleClickFolder={props.handleClickFolder} |
||||||
|
handleContextMenu={props.handleContextMenu} |
||||||
|
expandPath={props.expandPath} |
||||||
|
key={index} |
||||||
|
/>) |
||||||
|
} |
||||||
|
</TreeView> : <TreeView id={`treeView${file.path}`} key={`treeView${file.path}`} {...spreadProps }/> |
||||||
|
} |
||||||
|
</TreeViewItem> |
||||||
|
) |
||||||
|
} else { |
||||||
|
return ( |
||||||
|
<TreeViewItem |
||||||
|
id={`treeViewItem${file.path}`} |
||||||
|
key={`treeView${file.path}`} |
||||||
|
label={<FileLabel file={file} focusEdit={props.focusEdit} editModeOff={props.editModeOff} />} |
||||||
|
onClick={handleFileClick} |
||||||
|
onContextMenu={handleContextMenu} |
||||||
|
icon={icon} |
||||||
|
labelClass={labelClass} |
||||||
|
onMouseOver={handleMouseOver} |
||||||
|
onMouseOut={handleMouseOut} |
||||||
|
/> |
||||||
|
) |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,32 @@ |
|||||||
|
import { customAction } from '@remixproject/plugin-api/lib/file-system/file-panel/type' |
||||||
|
import { createContext, SyntheticEvent } from 'react' |
||||||
|
import { BrowserState } from '../reducers/workspace' |
||||||
|
|
||||||
|
export const FileSystemContext = createContext<{ |
||||||
|
fs: BrowserState, |
||||||
|
// eslint-disable-next-line no-undef
|
||||||
|
modal:(title: string, message: string | JSX.Element, okLabel: string, okFn: () => void, cancelLabel?: string, cancelFn?: () => void) => void, |
||||||
|
dispatchInitWorkspace:() => Promise<void>, |
||||||
|
dispatchFetchDirectory:(path: string) => Promise<void>, |
||||||
|
dispatchAddInputField:(path: string, type: 'file' | 'folder') => Promise<void>, |
||||||
|
dispatchRemoveInputField:(path: string) => Promise<void>, |
||||||
|
dispatchCreateWorkspace: (workspaceName: string) => Promise<void>, |
||||||
|
toast: (toasterMsg: string) => void, |
||||||
|
dispatchFetchWorkspaceDirectory: (path: string) => Promise<void>, |
||||||
|
dispatchSwitchToWorkspace: (name: string) => Promise<void>, |
||||||
|
dispatchRenameWorkspace: (oldName: string, workspaceName: string) => Promise<void>, |
||||||
|
dispatchDeleteWorkspace: (workspaceName: string) => Promise<void>, |
||||||
|
dispatchPublishToGist: (path?: string, type?: string) => Promise<void>, |
||||||
|
dispatchUploadFile: (target?: SyntheticEvent, targetFolder?: string) => Promise<void>, |
||||||
|
dispatchCreateNewFile: (path: string, rootDir: string) => Promise<void>, |
||||||
|
dispatchSetFocusElement: (elements: { key: string, type: 'file' | 'folder' | 'gist' }[]) => Promise<void>, |
||||||
|
dispatchCreateNewFolder: (path: string, rootDir: string) => Promise<void>, |
||||||
|
dispatchDeletePath: (path: string[]) => Promise<void>, |
||||||
|
dispatchRenamePath: (oldPath: string, newPath: string) => Promise<void>, |
||||||
|
dispatchCopyFile: (src: string, dest: string) => Promise<void>, |
||||||
|
dispatchCopyFolder: (src: string, dest: string) => Promise<void>, |
||||||
|
dispatchRunScript: (path: string) => Promise<void>, |
||||||
|
dispatchEmitContextMenuEvent: (cmd: customAction) => Promise<void>, |
||||||
|
dispatchHandleClickFile: (path: string, type: 'file' | 'folder' | 'gist') => Promise<void> |
||||||
|
dispatchHandleExpandPath: (paths: string[]) => Promise<void> |
||||||
|
}>(null) |
@ -0,0 +1,230 @@ |
|||||||
|
// eslint-disable-next-line no-use-before-define
|
||||||
|
import React, { useReducer, useState, useEffect, SyntheticEvent } 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, removeInputField, deleteWorkspace, clearPopUp, publishToGist, createNewFile, setFocusElement, createNewFolder, deletePath, renamePath, copyFile, copyFolder, runScript, emitContextMenuEvent, handleClickFile, handleExpandPath, addInputField, createWorkspace, fetchWorkspaceDirectory, renameWorkspace, switchToWorkspace, uploadFile } from '../actions' |
||||||
|
import { Modal, WorkspaceProps } from '../types' |
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
import { Workspace } from '../remix-ui-workspace' |
||||||
|
import { customAction } from '@remixproject/plugin-api/lib/file-system/file-panel/type' |
||||||
|
|
||||||
|
export const FileSystemProvider = (props: WorkspaceProps) => { |
||||||
|
const { plugin } = props |
||||||
|
const [fs, fsDispatch] = useReducer(browserReducer, browserInitialState) |
||||||
|
const [focusModal, setFocusModal] = useState<Modal>({ |
||||||
|
hide: true, |
||||||
|
title: '', |
||||||
|
message: '', |
||||||
|
okLabel: '', |
||||||
|
okFn: () => {}, |
||||||
|
cancelLabel: '', |
||||||
|
cancelFn: () => {} |
||||||
|
}) |
||||||
|
const [modals, setModals] = useState<Modal[]>([]) |
||||||
|
const [focusToaster, setFocusToaster] = useState<string>('') |
||||||
|
const [toasters, setToasters] = useState<string[]>([]) |
||||||
|
|
||||||
|
const dispatchInitWorkspace = async () => { |
||||||
|
await initWorkspace(plugin)(fsDispatch) |
||||||
|
} |
||||||
|
|
||||||
|
const dispatchFetchDirectory = async (path: string) => { |
||||||
|
await fetchDirectory(path) |
||||||
|
} |
||||||
|
|
||||||
|
const dispatchAddInputField = async (path: string, type: 'file' | 'folder') => { |
||||||
|
await addInputField(type, path) |
||||||
|
} |
||||||
|
|
||||||
|
const dispatchRemoveInputField = async (path: string) => { |
||||||
|
await removeInputField(path) |
||||||
|
} |
||||||
|
|
||||||
|
const dispatchCreateWorkspace = async (workspaceName: string) => { |
||||||
|
await createWorkspace(workspaceName) |
||||||
|
} |
||||||
|
|
||||||
|
const dispatchFetchWorkspaceDirectory = async (path: string) => { |
||||||
|
await fetchWorkspaceDirectory(path) |
||||||
|
} |
||||||
|
|
||||||
|
const dispatchSwitchToWorkspace = async (name: string) => { |
||||||
|
await switchToWorkspace(name) |
||||||
|
} |
||||||
|
|
||||||
|
const dispatchRenameWorkspace = async (oldName: string, workspaceName: string) => { |
||||||
|
await renameWorkspace(oldName, workspaceName) |
||||||
|
} |
||||||
|
|
||||||
|
const dispatchDeleteWorkspace = async (workspaceName: string) => { |
||||||
|
await deleteWorkspace(workspaceName) |
||||||
|
} |
||||||
|
|
||||||
|
const dispatchPublishToGist = async (path?: string, type?: string) => { |
||||||
|
await publishToGist(path, type) |
||||||
|
} |
||||||
|
|
||||||
|
const dispatchUploadFile = async (target?: SyntheticEvent, targetFolder?: string) => { |
||||||
|
await uploadFile(target, targetFolder) |
||||||
|
} |
||||||
|
|
||||||
|
const dispatchCreateNewFile = async (path: string, rootDir: string) => { |
||||||
|
await createNewFile(path, rootDir) |
||||||
|
} |
||||||
|
|
||||||
|
const dispatchSetFocusElement = async (elements: { key: string, type: 'file' | 'folder' | 'gist' }[]) => { |
||||||
|
await setFocusElement(elements) |
||||||
|
} |
||||||
|
|
||||||
|
const dispatchCreateNewFolder = async (path: string, rootDir: string) => { |
||||||
|
await createNewFolder(path, rootDir) |
||||||
|
} |
||||||
|
|
||||||
|
const dispatchDeletePath = async (path: string[]) => { |
||||||
|
await deletePath(path) |
||||||
|
} |
||||||
|
|
||||||
|
const dispatchRenamePath = async (oldPath: string, newPath: string) => { |
||||||
|
await renamePath(oldPath, newPath) |
||||||
|
} |
||||||
|
|
||||||
|
const dispatchCopyFile = async (src: string, dest: string) => { |
||||||
|
await copyFile(src, dest) |
||||||
|
} |
||||||
|
|
||||||
|
const dispatchCopyFolder = async (src: string, dest: string) => { |
||||||
|
await copyFolder(src, dest) |
||||||
|
} |
||||||
|
|
||||||
|
const dispatchRunScript = async (path: string) => { |
||||||
|
await runScript(path) |
||||||
|
} |
||||||
|
|
||||||
|
const dispatchEmitContextMenuEvent = async (cmd: customAction) => { |
||||||
|
await emitContextMenuEvent(cmd) |
||||||
|
} |
||||||
|
|
||||||
|
const dispatchHandleClickFile = async (path: string, type: 'file' | 'folder' | 'gist') => { |
||||||
|
await handleClickFile(path, type) |
||||||
|
} |
||||||
|
|
||||||
|
const dispatchHandleExpandPath = async (paths: string[]) => { |
||||||
|
await handleExpandPath(paths) |
||||||
|
} |
||||||
|
|
||||||
|
useEffect(() => { |
||||||
|
dispatchInitWorkspace() |
||||||
|
}, []) |
||||||
|
|
||||||
|
useEffect(() => { |
||||||
|
if (modals.length > 0) { |
||||||
|
setFocusModal(() => { |
||||||
|
const focusModal = { |
||||||
|
hide: false, |
||||||
|
title: modals[0].title, |
||||||
|
message: modals[0].message, |
||||||
|
okLabel: modals[0].okLabel, |
||||||
|
okFn: modals[0].okFn, |
||||||
|
cancelLabel: modals[0].cancelLabel, |
||||||
|
cancelFn: modals[0].cancelFn |
||||||
|
} |
||||||
|
return focusModal |
||||||
|
}) |
||||||
|
const modalList = modals.slice() |
||||||
|
|
||||||
|
modalList.shift() |
||||||
|
setModals(modalList) |
||||||
|
} |
||||||
|
}, [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) |
||||||
|
} |
||||||
|
}, [fs.notification]) |
||||||
|
|
||||||
|
useEffect(() => { |
||||||
|
if (fs.popup) { |
||||||
|
toast(fs.popup) |
||||||
|
} |
||||||
|
}, [fs.popup]) |
||||||
|
|
||||||
|
const handleHideModal = () => { |
||||||
|
setFocusModal(modal => { |
||||||
|
return { ...modal, hide: true, message: null } |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
// eslint-disable-next-line no-undef
|
||||||
|
const modal = (title: string, message: string | JSX.Element, okLabel: string, okFn: () => void, cancelLabel?: string, cancelFn?: () => void) => { |
||||||
|
setModals(modals => { |
||||||
|
modals.push({ message, title, okLabel, okFn, cancelLabel, cancelFn }) |
||||||
|
return [...modals] |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
const handleToaster = () => { |
||||||
|
setFocusToaster('') |
||||||
|
clearPopUp() |
||||||
|
} |
||||||
|
|
||||||
|
const toast = (toasterMsg: string) => { |
||||||
|
setToasters(messages => { |
||||||
|
messages.push(toasterMsg) |
||||||
|
return [...messages] |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
const value = { |
||||||
|
fs, |
||||||
|
modal, |
||||||
|
toast, |
||||||
|
dispatchInitWorkspace, |
||||||
|
dispatchFetchDirectory, |
||||||
|
dispatchAddInputField, |
||||||
|
dispatchRemoveInputField, |
||||||
|
dispatchCreateWorkspace, |
||||||
|
dispatchFetchWorkspaceDirectory, |
||||||
|
dispatchSwitchToWorkspace, |
||||||
|
dispatchRenameWorkspace, |
||||||
|
dispatchDeleteWorkspace, |
||||||
|
dispatchPublishToGist, |
||||||
|
dispatchUploadFile, |
||||||
|
dispatchCreateNewFile, |
||||||
|
dispatchSetFocusElement, |
||||||
|
dispatchCreateNewFolder, |
||||||
|
dispatchDeletePath, |
||||||
|
dispatchRenamePath, |
||||||
|
dispatchCopyFile, |
||||||
|
dispatchCopyFolder, |
||||||
|
dispatchRunScript, |
||||||
|
dispatchEmitContextMenuEvent, |
||||||
|
dispatchHandleClickFile, |
||||||
|
dispatchHandleExpandPath |
||||||
|
} |
||||||
|
return ( |
||||||
|
<FileSystemContext.Provider value={value}> |
||||||
|
{ fs.initializingFS && <div className="text-center py-5"><i className="fas fa-spinner fa-pulse fa-2x"></i></div> } |
||||||
|
{ !fs.initializingFS && <Workspace /> } |
||||||
|
<ModalDialog id='fileSystem' { ...focusModal } handleHide={ handleHideModal } /> |
||||||
|
<Toaster message={focusToaster} handleHide={handleToaster} /> |
||||||
|
</FileSystemContext.Provider> |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
export default FileSystemProvider |
@ -0,0 +1,815 @@ |
|||||||
|
import { extractNameFromKey } from '@remix-ui/helper' |
||||||
|
import { action, FileType } from '../types' |
||||||
|
import * as _ from 'lodash' |
||||||
|
interface Action { |
||||||
|
type: string |
||||||
|
payload: any |
||||||
|
} |
||||||
|
export interface BrowserState { |
||||||
|
browser: { |
||||||
|
currentWorkspace: string, |
||||||
|
workspaces: string[], |
||||||
|
files: { [x: string]: Record<string, FileType> }, |
||||||
|
expandPath: string[] |
||||||
|
isRequestingDirectory: boolean, |
||||||
|
isSuccessfulDirectory: boolean, |
||||||
|
isRequestingWorkspace: boolean, |
||||||
|
isSuccessfulWorkspace: boolean, |
||||||
|
error: string, |
||||||
|
contextMenu: { |
||||||
|
registeredMenuItems: action[], |
||||||
|
removedMenuItems: action[], |
||||||
|
error: string |
||||||
|
} |
||||||
|
}, |
||||||
|
localhost: { |
||||||
|
sharedFolder: string, |
||||||
|
files: { [x: string]: Record<string, FileType> }, |
||||||
|
expandPath: string[], |
||||||
|
isRequestingDirectory: boolean, |
||||||
|
isSuccessfulDirectory: boolean, |
||||||
|
isRequestingLocalhost: boolean, |
||||||
|
isSuccessfulLocalhost: boolean, |
||||||
|
error: string, |
||||||
|
contextMenu: { |
||||||
|
registeredMenuItems: action[], |
||||||
|
removedMenuItems: action[], |
||||||
|
error: string |
||||||
|
} |
||||||
|
}, |
||||||
|
mode: 'browser' | 'localhost', |
||||||
|
notification: { |
||||||
|
title: string, |
||||||
|
message: string, |
||||||
|
actionOk: () => void, |
||||||
|
actionCancel: (() => void) | null, |
||||||
|
labelOk: string, |
||||||
|
labelCancel: string |
||||||
|
}, |
||||||
|
readonly: boolean, |
||||||
|
popup: string, |
||||||
|
focusEdit: string, |
||||||
|
focusElement: { key: string, type: 'file' | 'folder' | 'gist' }[], |
||||||
|
initializingFS: boolean |
||||||
|
} |
||||||
|
|
||||||
|
export const browserInitialState: BrowserState = { |
||||||
|
browser: { |
||||||
|
currentWorkspace: '', |
||||||
|
workspaces: [], |
||||||
|
files: {}, |
||||||
|
expandPath: [], |
||||||
|
isRequestingDirectory: false, |
||||||
|
isSuccessfulDirectory: false, |
||||||
|
isRequestingWorkspace: false, |
||||||
|
isSuccessfulWorkspace: false, |
||||||
|
error: null, |
||||||
|
contextMenu: { |
||||||
|
registeredMenuItems: [], |
||||||
|
removedMenuItems: [], |
||||||
|
error: null |
||||||
|
} |
||||||
|
}, |
||||||
|
localhost: { |
||||||
|
sharedFolder: '', |
||||||
|
files: {}, |
||||||
|
expandPath: [], |
||||||
|
isRequestingDirectory: false, |
||||||
|
isSuccessfulDirectory: false, |
||||||
|
isRequestingLocalhost: false, |
||||||
|
isSuccessfulLocalhost: false, |
||||||
|
error: null, |
||||||
|
contextMenu: { |
||||||
|
registeredMenuItems: [], |
||||||
|
removedMenuItems: [], |
||||||
|
error: null |
||||||
|
} |
||||||
|
}, |
||||||
|
mode: 'browser', |
||||||
|
notification: { |
||||||
|
title: '', |
||||||
|
message: '', |
||||||
|
actionOk: () => {}, |
||||||
|
actionCancel: () => {}, |
||||||
|
labelOk: '', |
||||||
|
labelCancel: '' |
||||||
|
}, |
||||||
|
readonly: false, |
||||||
|
popup: '', |
||||||
|
focusEdit: '', |
||||||
|
focusElement: [], |
||||||
|
initializingFS: true |
||||||
|
} |
||||||
|
|
||||||
|
export const browserReducer = (state = browserInitialState, action: Action) => { |
||||||
|
switch (action.type) { |
||||||
|
case 'SET_CURRENT_WORKSPACE': { |
||||||
|
const payload = action.payload as string |
||||||
|
const workspaces = state.browser.workspaces.includes(payload) ? state.browser.workspaces : [...state.browser.workspaces, action.payload] |
||||||
|
|
||||||
|
return { |
||||||
|
...state, |
||||||
|
browser: { |
||||||
|
...state.browser, |
||||||
|
currentWorkspace: payload, |
||||||
|
workspaces: workspaces.filter(workspace => workspace) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
case 'SET_WORKSPACES': { |
||||||
|
const payload = action.payload as string[] |
||||||
|
|
||||||
|
return { |
||||||
|
...state, |
||||||
|
browser: { |
||||||
|
...state.browser, |
||||||
|
workspaces: payload.filter(workspace => workspace) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
case 'SET_MODE': { |
||||||
|
const payload = action.payload as 'browser' | 'localhost' |
||||||
|
|
||||||
|
return { |
||||||
|
...state, |
||||||
|
mode: payload |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
case 'FETCH_DIRECTORY_REQUEST': { |
||||||
|
return { |
||||||
|
...state, |
||||||
|
browser: { |
||||||
|
...state.browser, |
||||||
|
isRequestingDirectory: state.mode === 'browser', |
||||||
|
isSuccessfulDirectory: false, |
||||||
|
error: null |
||||||
|
}, |
||||||
|
localhost: { |
||||||
|
...state.localhost, |
||||||
|
isRequestingDirectory: state.mode === 'localhost', |
||||||
|
isSuccessfulDirectory: false, |
||||||
|
error: null |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
case 'FETCH_DIRECTORY_SUCCESS': { |
||||||
|
const payload = action.payload as { path: string, fileTree } |
||||||
|
|
||||||
|
return { |
||||||
|
...state, |
||||||
|
browser: { |
||||||
|
...state.browser, |
||||||
|
files: state.mode === 'browser' ? fetchDirectoryContent(state, payload) : state.browser.files, |
||||||
|
isRequestingDirectory: false, |
||||||
|
isSuccessfulDirectory: true, |
||||||
|
error: null |
||||||
|
}, |
||||||
|
localhost: { |
||||||
|
...state.localhost, |
||||||
|
files: state.mode === 'localhost' ? fetchDirectoryContent(state, payload) : state.localhost.files, |
||||||
|
isRequestingDirectory: false, |
||||||
|
isSuccessfulDirectory: true, |
||||||
|
error: null |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
case 'FETCH_DIRECTORY_ERROR': { |
||||||
|
return { |
||||||
|
...state, |
||||||
|
browser: { |
||||||
|
...state.browser, |
||||||
|
isRequestingDirectory: false, |
||||||
|
isSuccessfulDirectory: false, |
||||||
|
error: state.mode === 'browser' ? action.payload : null |
||||||
|
}, |
||||||
|
localhost: { |
||||||
|
...state.localhost, |
||||||
|
isRequestingDirectory: false, |
||||||
|
isSuccessfulDirectory: false, |
||||||
|
error: state.mode === 'localhost' ? action.payload : null |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
case 'FETCH_WORKSPACE_DIRECTORY_REQUEST': { |
||||||
|
return { |
||||||
|
...state, |
||||||
|
browser: { |
||||||
|
...state.browser, |
||||||
|
isRequestingWorkspace: state.mode === 'browser', |
||||||
|
isSuccessfulWorkspace: false, |
||||||
|
error: null |
||||||
|
}, |
||||||
|
localhost: { |
||||||
|
...state.localhost, |
||||||
|
isRequestingWorkspace: state.mode === 'localhost', |
||||||
|
isSuccessfulWorkspace: 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, |
||||||
|
isRequestingWorkspace: false, |
||||||
|
isSuccessfulWorkspace: true, |
||||||
|
error: null |
||||||
|
}, |
||||||
|
localhost: { |
||||||
|
...state.localhost, |
||||||
|
files: state.mode === 'localhost' ? fetchWorkspaceDirectoryContent(state, payload) : state.localhost.files, |
||||||
|
isRequestingWorkspace: false, |
||||||
|
isSuccessfulWorkspace: true, |
||||||
|
error: null |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
case 'FETCH_WORKSPACE_DIRECTORY_ERROR': { |
||||||
|
return { |
||||||
|
...state, |
||||||
|
browser: { |
||||||
|
...state.browser, |
||||||
|
isRequestingWorkspace: false, |
||||||
|
isSuccessfulWorkspace: false, |
||||||
|
error: state.mode === 'browser' ? action.payload : null |
||||||
|
}, |
||||||
|
localhost: { |
||||||
|
...state.localhost, |
||||||
|
isRequestingWorkspace: false, |
||||||
|
isSuccessfulWorkspace: 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 } |
||||||
|
|
||||||
|
return { |
||||||
|
...state, |
||||||
|
notification: { |
||||||
|
title: payload.title, |
||||||
|
message: payload.message, |
||||||
|
actionOk: payload.actionOk || browserInitialState.notification.actionOk, |
||||||
|
actionCancel: payload.actionCancel || browserInitialState.notification.actionCancel, |
||||||
|
labelOk: payload.labelOk, |
||||||
|
labelCancel: payload.labelCancel |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
case 'HIDE_NOTIFICATION': { |
||||||
|
return { |
||||||
|
...state, |
||||||
|
notification: browserInitialState.notification |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
case 'FILE_ADDED_SUCCESS': { |
||||||
|
const payload = action.payload as string |
||||||
|
|
||||||
|
return { |
||||||
|
...state, |
||||||
|
browser: { |
||||||
|
...state.browser, |
||||||
|
files: state.mode === 'browser' ? fileAdded(state, payload) : state.browser.files, |
||||||
|
expandPath: state.mode === 'browser' ? [...new Set([...state.browser.expandPath, payload])] : state.browser.expandPath |
||||||
|
}, |
||||||
|
localhost: { |
||||||
|
...state.localhost, |
||||||
|
files: state.mode === 'localhost' ? fileAdded(state, payload) : state.localhost.files, |
||||||
|
expandPath: state.mode === 'localhost' ? [...new Set([...state.localhost.expandPath, payload])] : state.localhost.expandPath |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
case 'FOLDER_ADDED_SUCCESS': { |
||||||
|
const payload = action.payload as { path: string, folderPath: string, fileTree } |
||||||
|
|
||||||
|
return { |
||||||
|
...state, |
||||||
|
browser: { |
||||||
|
...state.browser, |
||||||
|
files: state.mode === 'browser' ? fetchDirectoryContent(state, payload) : state.browser.files, |
||||||
|
expandPath: state.mode === 'browser' ? [...new Set([...state.browser.expandPath, payload.folderPath])] : 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.folderPath])] : state.localhost.expandPath |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
case 'FILE_REMOVED_SUCCESS': { |
||||||
|
const payload = action.payload as string |
||||||
|
|
||||||
|
return { |
||||||
|
...state, |
||||||
|
browser: { |
||||||
|
...state.browser, |
||||||
|
files: state.mode === 'browser' ? fileRemoved(state, payload) : state.browser.files, |
||||||
|
expandPath: state.mode === 'browser' ? [...(state.browser.expandPath.filter(path => path !== payload))] : state.browser.expandPath |
||||||
|
}, |
||||||
|
localhost: { |
||||||
|
...state.localhost, |
||||||
|
files: state.mode === 'localhost' ? fileRemoved(state, payload) : state.localhost.files, |
||||||
|
expandPath: state.mode === 'localhost' ? [...(state.browser.expandPath.filter(path => path !== payload))] : state.localhost.expandPath |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
case 'ROOT_FOLDER_CHANGED': { |
||||||
|
const payload = action.payload as string |
||||||
|
|
||||||
|
return { |
||||||
|
...state, |
||||||
|
localhost: { |
||||||
|
...state.localhost, |
||||||
|
sharedFolder: payload |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
case 'ADD_INPUT_FIELD': { |
||||||
|
const payload = action.payload as { path: string, fileTree, type: 'file' | 'folder' } |
||||||
|
|
||||||
|
return { |
||||||
|
...state, |
||||||
|
browser: { |
||||||
|
...state.browser, |
||||||
|
files: state.mode === 'browser' ? fetchDirectoryContent(state, payload) : state.browser.files |
||||||
|
}, |
||||||
|
localhost: { |
||||||
|
...state.localhost, |
||||||
|
files: state.mode === 'localhost' ? fetchDirectoryContent(state, payload) : state.localhost.files |
||||||
|
}, |
||||||
|
focusEdit: payload.path + '/' + 'blank' |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
case 'REMOVE_INPUT_FIELD': { |
||||||
|
const payload = action.payload as { path: string, fileTree } |
||||||
|
|
||||||
|
return { |
||||||
|
...state, |
||||||
|
browser: { |
||||||
|
...state.browser, |
||||||
|
files: state.mode === 'browser' ? removeInputField(state, payload.path) : state.browser.files |
||||||
|
}, |
||||||
|
localhost: { |
||||||
|
...state.localhost, |
||||||
|
files: state.mode === 'localhost' ? removeInputField(state, payload.path) : state.localhost.files |
||||||
|
}, |
||||||
|
focusEdit: null |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
case 'SET_READ_ONLY_MODE': { |
||||||
|
const payload = action.payload as boolean |
||||||
|
|
||||||
|
return { |
||||||
|
...state, |
||||||
|
readonly: payload |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
case 'FILE_RENAMED_SUCCESS': { |
||||||
|
const payload = action.payload as { path: string, oldPath: string, fileTree } |
||||||
|
|
||||||
|
return { |
||||||
|
...state, |
||||||
|
browser: { |
||||||
|
...state.browser, |
||||||
|
files: state.mode === 'browser' ? fetchDirectoryContent(state, payload, payload.oldPath) : state.browser.files |
||||||
|
}, |
||||||
|
localhost: { |
||||||
|
...state.localhost, |
||||||
|
files: state.mode === 'localhost' ? fetchDirectoryContent(state, payload, payload.oldPath) : state.localhost.files |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
case 'CREATE_WORKSPACE_REQUEST': { |
||||||
|
return { |
||||||
|
...state, |
||||||
|
browser: { |
||||||
|
...state.browser, |
||||||
|
isRequestingWorkspace: true, |
||||||
|
isSuccessfulWorkspace: false, |
||||||
|
error: null |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
case 'CREATE_WORKSPACE_SUCCESS': { |
||||||
|
const payload = action.payload as string |
||||||
|
const workspaces = state.browser.workspaces.includes(payload) ? state.browser.workspaces : [...state.browser.workspaces, action.payload] |
||||||
|
|
||||||
|
return { |
||||||
|
...state, |
||||||
|
browser: { |
||||||
|
...state.browser, |
||||||
|
currentWorkspace: payload, |
||||||
|
workspaces: workspaces.filter(workspace => workspace), |
||||||
|
isRequestingWorkspace: false, |
||||||
|
isSuccessfulWorkspace: true, |
||||||
|
error: null |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
case 'CREATE_WORKSPACE_ERROR': { |
||||||
|
return { |
||||||
|
...state, |
||||||
|
browser: { |
||||||
|
...state.browser, |
||||||
|
isRequestingWorkspace: false, |
||||||
|
isSuccessfulWorkspace: false, |
||||||
|
error: action.payload |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
case 'RENAME_WORKSPACE': { |
||||||
|
const payload = action.payload as { oldName: string, workspaceName: string } |
||||||
|
const workspaces = state.browser.workspaces.filter(name => name && (name !== payload.oldName)) |
||||||
|
|
||||||
|
return { |
||||||
|
...state, |
||||||
|
browser: { |
||||||
|
...state.browser, |
||||||
|
currentWorkspace: payload.workspaceName, |
||||||
|
workspaces: [...workspaces, payload.workspaceName], |
||||||
|
expandPath: [] |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
case 'DELETE_WORKSPACE': { |
||||||
|
const payload = action.payload as string |
||||||
|
const workspaces = state.browser.workspaces.filter(name => name && (name !== payload)) |
||||||
|
|
||||||
|
return { |
||||||
|
...state, |
||||||
|
browser: { |
||||||
|
...state.browser, |
||||||
|
workspaces: workspaces |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
case 'DISPLAY_POPUP_MESSAGE': { |
||||||
|
const payload = action.payload as string |
||||||
|
|
||||||
|
return { |
||||||
|
...state, |
||||||
|
popup: payload |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
case 'HIDE_POPUP_MESSAGE': { |
||||||
|
return { |
||||||
|
...state, |
||||||
|
popup: '' |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
case 'SET_FOCUS_ELEMENT': { |
||||||
|
const payload = action.payload as { key: string, type: 'file' | 'folder' | 'gist' }[] |
||||||
|
|
||||||
|
return { |
||||||
|
...state, |
||||||
|
focusElement: payload |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
case 'SET_CONTEXT_MENU_ITEM': { |
||||||
|
const payload = action.payload as action |
||||||
|
|
||||||
|
return { |
||||||
|
...state, |
||||||
|
browser: { |
||||||
|
...state.browser, |
||||||
|
contextMenu: state.mode === 'browser' ? addContextMenuItem(state, payload) : state.browser.contextMenu |
||||||
|
}, |
||||||
|
localhost: { |
||||||
|
...state.localhost, |
||||||
|
contextMenu: state.mode === 'localhost' ? addContextMenuItem(state, payload) : state.localhost.contextMenu |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
case 'REMOVE_CONTEXT_MENU_ITEM': { |
||||||
|
const payload = action.payload |
||||||
|
|
||||||
|
return { |
||||||
|
...state, |
||||||
|
browser: { |
||||||
|
...state.browser, |
||||||
|
contextMenu: state.mode === 'browser' ? removeContextMenuItem(state, payload) : state.browser.contextMenu |
||||||
|
}, |
||||||
|
localhost: { |
||||||
|
...state.localhost, |
||||||
|
contextMenu: state.mode === 'localhost' ? removeContextMenuItem(state, payload) : state.localhost.contextMenu |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
case 'SET_EXPAND_PATH': { |
||||||
|
const payload = action.payload as string[] |
||||||
|
|
||||||
|
return { |
||||||
|
...state, |
||||||
|
browser: { |
||||||
|
...state.browser, |
||||||
|
expandPath: state.mode === 'browser' ? payload : state.browser.expandPath |
||||||
|
}, |
||||||
|
localhost: { |
||||||
|
...state.localhost, |
||||||
|
expandPath: state.mode === 'localhost' ? payload : state.localhost.expandPath |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
case 'LOAD_LOCALHOST_REQUEST': { |
||||||
|
return { |
||||||
|
...state, |
||||||
|
localhost: { |
||||||
|
...state.localhost, |
||||||
|
isRequestingLocalhost: true, |
||||||
|
isSuccessfulLocalhost: false, |
||||||
|
error: null |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
case 'LOAD_LOCALHOST_SUCCESS': { |
||||||
|
return { |
||||||
|
...state, |
||||||
|
localhost: { |
||||||
|
...state.localhost, |
||||||
|
isRequestingLocalhost: false, |
||||||
|
isSuccessfulLocalhost: true, |
||||||
|
error: null |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
case 'LOAD_LOCALHOST_ERROR': { |
||||||
|
const payload = action.payload as string |
||||||
|
|
||||||
|
return { |
||||||
|
...state, |
||||||
|
localhost: { |
||||||
|
...state.localhost, |
||||||
|
isRequestingLocalhost: false, |
||||||
|
isSuccessfulLocalhost: false, |
||||||
|
error: payload |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
case 'FS_INITIALIZATION_COMPLETED': { |
||||||
|
return { |
||||||
|
...state, |
||||||
|
initializingFS: false |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
default: |
||||||
|
throw new Error() |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
const fileAdded = (state: BrowserState, path: string): { [x: string]: Record<string, FileType> } => { |
||||||
|
let files = state.mode === 'browser' ? state.browser.files : state.localhost.files |
||||||
|
const _path = splitPath(state, path) |
||||||
|
|
||||||
|
files = _.set(files, _path, { |
||||||
|
path: path, |
||||||
|
name: extractNameFromKey(path), |
||||||
|
isDirectory: false, |
||||||
|
type: 'file' |
||||||
|
}) |
||||||
|
return files |
||||||
|
} |
||||||
|
|
||||||
|
const fileRemoved = (state: BrowserState, path: string): { [x: string]: Record<string, FileType> } => { |
||||||
|
const files = state.mode === 'browser' ? state.browser.files : state.localhost.files |
||||||
|
const _path = splitPath(state, path) |
||||||
|
|
||||||
|
_.unset(files, _path) |
||||||
|
return files |
||||||
|
} |
||||||
|
|
||||||
|
const removeInputField = (state: BrowserState, path: string): { [x: string]: Record<string, FileType> } => { |
||||||
|
let files = state.mode === 'browser' ? state.browser.files : state.localhost.files |
||||||
|
const root = state.mode === 'browser' ? state.browser.currentWorkspace : state.mode |
||||||
|
|
||||||
|
if (path === root) { |
||||||
|
delete files[root][path + '/' + 'blank'] |
||||||
|
return files |
||||||
|
} |
||||||
|
const _path = splitPath(state, path) |
||||||
|
const prevFiles = _.get(files, _path) |
||||||
|
|
||||||
|
if (prevFiles) { |
||||||
|
prevFiles.child && prevFiles.child[path + '/' + 'blank'] && delete prevFiles.child[path + '/' + 'blank'] |
||||||
|
files = _.set(files, _path, { |
||||||
|
isDirectory: true, |
||||||
|
path, |
||||||
|
name: extractNameFromKey(path).indexOf('gist-') === 0 ? extractNameFromKey(path).split('-')[1] : extractNameFromKey(path), |
||||||
|
type: extractNameFromKey(path).indexOf('gist-') === 0 ? 'gist' : 'folder', |
||||||
|
child: prevFiles ? prevFiles.child : {} |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
return files |
||||||
|
} |
||||||
|
|
||||||
|
// IDEA: Modify function to remove blank input field without fetching content
|
||||||
|
const fetchDirectoryContent = (state: BrowserState, payload: { fileTree, path: string, type?: 'file' | 'folder' }, deletePath?: string): { [x: string]: Record<string, FileType> } => { |
||||||
|
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) |
||||||
|
|
||||||
|
files = _.merge(files, state.browser.files[state.browser.currentWorkspace]) |
||||||
|
if (deletePath) delete files[deletePath] |
||||||
|
return { [state.browser.currentWorkspace]: files } |
||||||
|
} else { |
||||||
|
let files = state.browser.files |
||||||
|
const _path = splitPath(state, payload.path) |
||||||
|
const prevFiles = _.get(files, _path) |
||||||
|
|
||||||
|
if (prevFiles) { |
||||||
|
prevFiles.child = _.merge(normalize(payload.fileTree, payload.path, payload.type), prevFiles.child) |
||||||
|
if (deletePath) { |
||||||
|
if (deletePath.endsWith('/blank')) delete prevFiles.child[deletePath] |
||||||
|
else { |
||||||
|
deletePath = extractNameFromKey(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 { |
||||||
|
if (payload.path === state.mode || payload.path === '/') { |
||||||
|
let files = normalize(payload.fileTree, payload.path, payload.type) |
||||||
|
|
||||||
|
files = _.merge(files, state[state.mode].files[state.mode]) |
||||||
|
if (deletePath) delete files[deletePath] |
||||||
|
return { [state.mode]: files } |
||||||
|
} else { |
||||||
|
let files = state.localhost.files |
||||||
|
const _path = splitPath(state, payload.path) |
||||||
|
const prevFiles = _.get(files, _path) |
||||||
|
|
||||||
|
if (prevFiles) { |
||||||
|
prevFiles.child = _.merge(normalize(payload.fileTree, payload.path, payload.type), prevFiles.child) |
||||||
|
if (deletePath) { |
||||||
|
if (deletePath.endsWith('/blank')) delete prevFiles.child[deletePath] |
||||||
|
else { |
||||||
|
deletePath = extractNameFromKey(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, FileType> } => { |
||||||
|
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, FileType> => { |
||||||
|
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).indexOf('gist-') === 0 ? extractNameFromKey(path).split('-')[1] : extractNameFromKey(path), |
||||||
|
isDirectory: filesList[key].isDirectory, |
||||||
|
type: extractNameFromKey(path).indexOf('gist-') === 0 ? 'gist' : 'folder' |
||||||
|
} |
||||||
|
} else { |
||||||
|
files[extractNameFromKey(key)] = { |
||||||
|
path, |
||||||
|
name: extractNameFromKey(path), |
||||||
|
isDirectory: filesList[key].isDirectory, |
||||||
|
type: 'file' |
||||||
|
} |
||||||
|
} |
||||||
|
}) |
||||||
|
|
||||||
|
if (newInputType === 'folder') { |
||||||
|
const path = directory + '/blank' |
||||||
|
|
||||||
|
folders[path] = { |
||||||
|
path: path, |
||||||
|
name: '', |
||||||
|
isDirectory: true, |
||||||
|
type: 'folder' |
||||||
|
} |
||||||
|
} else if (newInputType === 'file') { |
||||||
|
const path = directory + '/blank' |
||||||
|
|
||||||
|
files[path] = { |
||||||
|
path: path, |
||||||
|
name: '', |
||||||
|
isDirectory: false, |
||||||
|
type: 'file' |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return Object.assign({}, folders, files) |
||||||
|
} |
||||||
|
|
||||||
|
const splitPath = (state: BrowserState, path: string): string[] | string => { |
||||||
|
const root = state.mode === 'browser' ? state.browser.currentWorkspace : 'localhost' |
||||||
|
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] |
||||||
|
}, []) |
||||||
|
|
||||||
|
return _path |
||||||
|
} |
||||||
|
|
||||||
|
const addContextMenuItem = (state: BrowserState, item: action): { registeredMenuItems: action[], removedMenuItems: action[], error: string } => { |
||||||
|
let registeredItems = state[state.mode].contextMenu.registeredMenuItems |
||||||
|
let removedItems = state[state.mode].contextMenu.removedMenuItems |
||||||
|
let error = null |
||||||
|
|
||||||
|
if (registeredItems.filter((o) => { |
||||||
|
return o.id === item.id && o.name === item.name |
||||||
|
}).length) { |
||||||
|
error = `Action ${item.name} already exists on ${item.id}` |
||||||
|
return { |
||||||
|
registeredMenuItems: registeredItems, |
||||||
|
removedMenuItems: removedItems, |
||||||
|
error |
||||||
|
} |
||||||
|
} |
||||||
|
registeredItems = [...registeredItems, item] |
||||||
|
removedItems = removedItems.filter(menuItem => item.id !== menuItem.id) |
||||||
|
return { |
||||||
|
registeredMenuItems: registeredItems, |
||||||
|
removedMenuItems: removedItems, |
||||||
|
error |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
const removeContextMenuItem = (state: BrowserState, plugin): { registeredMenuItems: action[], removedMenuItems: action[], error: string } => { |
||||||
|
let registeredItems = state[state.mode].contextMenu.registeredMenuItems |
||||||
|
const removedItems = state[state.mode].contextMenu.removedMenuItems |
||||||
|
const error = null |
||||||
|
|
||||||
|
registeredItems = registeredItems.filter((item) => { |
||||||
|
if (item.id !== plugin.name || item.sticky === true) return true |
||||||
|
else { |
||||||
|
removedItems.push(item) |
||||||
|
return false |
||||||
|
} |
||||||
|
}) |
||||||
|
return { |
||||||
|
registeredMenuItems: registeredItems, |
||||||
|
removedMenuItems: removedItems, |
||||||
|
error |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,155 @@ |
|||||||
|
import React from 'react' |
||||||
|
import { customAction } from '@remixproject/plugin-api/lib/file-system/file-panel' |
||||||
|
|
||||||
|
export type action = { name: string, type?: Array<'folder' | 'gist' | 'file'>, path?: string[], extension?: string[], pattern?: string[], id: string, multiselect: boolean, label: string, sticky?: boolean } |
||||||
|
|
||||||
|
export type MenuItems = action[] |
||||||
|
export interface WorkspaceProps { |
||||||
|
plugin: { |
||||||
|
setWorkspace: ({ name: string, isLocalhost: boolean }, setEvent: boolean) => void, |
||||||
|
createWorkspace: (name: string) => void, |
||||||
|
renameWorkspace: (oldName: string, newName: string) => void |
||||||
|
workspaceRenamed: ({ name: string }) => void, |
||||||
|
workspaceCreated: ({ name: string }) => void, |
||||||
|
workspaceDeleted: ({ name: string }) => void, |
||||||
|
workspace: any // workspace provider,
|
||||||
|
browser: any // browser provider
|
||||||
|
localhost: any // localhost provider
|
||||||
|
fileManager : any |
||||||
|
registry: any // registry
|
||||||
|
request: { |
||||||
|
createWorkspace: () => void, |
||||||
|
setWorkspace: (workspaceName: string) => void, |
||||||
|
createNewFile: () => void, |
||||||
|
uploadFile: (target: EventTarget & HTMLInputElement) => void, |
||||||
|
getCurrentWorkspace: () => void |
||||||
|
} // api request,
|
||||||
|
workspaces: any, |
||||||
|
registeredMenuItems: MenuItems // menu items
|
||||||
|
removedMenuItems: MenuItems |
||||||
|
initialWorkspace: string, |
||||||
|
resetNewFile: () => void, |
||||||
|
getWorkspaces: () => string[] |
||||||
|
} |
||||||
|
} |
||||||
|
export interface WorkspaceState { |
||||||
|
hideRemixdExplorer: boolean |
||||||
|
displayNewFile: boolean |
||||||
|
loadingLocalhost: boolean |
||||||
|
} |
||||||
|
|
||||||
|
export interface Modal { |
||||||
|
hide?: boolean |
||||||
|
title: string |
||||||
|
// eslint-disable-next-line no-undef
|
||||||
|
message: string | JSX.Element |
||||||
|
okLabel: string |
||||||
|
okFn: () => void |
||||||
|
cancelLabel: string |
||||||
|
cancelFn: () => void |
||||||
|
} |
||||||
|
|
||||||
|
export interface FileType { |
||||||
|
path: string, |
||||||
|
name: string, |
||||||
|
isDirectory: boolean, |
||||||
|
type: 'folder' | 'file' | 'gist', |
||||||
|
child?: File[] |
||||||
|
} |
||||||
|
|
||||||
|
/* eslint-disable-next-line */ |
||||||
|
export interface FileExplorerProps { |
||||||
|
name: string, |
||||||
|
menuItems?: string[], |
||||||
|
contextMenuItems: MenuItems, |
||||||
|
removedContextMenuItems: MenuItems, |
||||||
|
files: { [x: string]: Record<string, FileType> }, |
||||||
|
expandPath: string[], |
||||||
|
focusEdit: string, |
||||||
|
focusElement: { key: string, type: 'file' | 'folder' | 'gist' }[], |
||||||
|
dispatchCreateNewFile: (path: string, rootDir: string) => Promise<void>, |
||||||
|
// eslint-disable-next-line no-undef
|
||||||
|
modal:(title: string, message: string | JSX.Element, okLabel: string, okFn: () => void, cancelLabel?: string, cancelFn?: () => void) => void, |
||||||
|
dispatchCreateNewFolder: (path: string, rootDir: string) => Promise<void>, |
||||||
|
readonly: boolean, |
||||||
|
toast: (toasterMsg: string) => void, |
||||||
|
dispatchDeletePath: (path: string[]) => Promise<void>, |
||||||
|
dispatchRenamePath: (oldPath: string, newPath: string) => Promise<void>, |
||||||
|
dispatchUploadFile: (target?: React.SyntheticEvent, targetFolder?: string) => Promise<void>, |
||||||
|
dispatchCopyFile: (src: string, dest: string) => Promise<void>, |
||||||
|
dispatchCopyFolder: (src: string, dest: string) => Promise<void>, |
||||||
|
dispatchRunScript: (path: string) => Promise<void>, |
||||||
|
dispatchPublishToGist: (path?: string, type?: string) => Promise<void>, |
||||||
|
dispatchEmitContextMenuEvent: (cmd: customAction) => Promise<void>, |
||||||
|
dispatchHandleClickFile: (path: string, type: 'file' | 'folder' | 'gist') => Promise<void>, |
||||||
|
dispatchSetFocusElement: (elements: { key: string, type: 'file' | 'folder' | 'gist' }[]) => Promise<void>, |
||||||
|
dispatchFetchDirectory:(path: string) => Promise<void>, |
||||||
|
dispatchRemoveInputField:(path: string) => Promise<void>, |
||||||
|
dispatchAddInputField:(path: string, type: 'file' | 'folder') => Promise<void>, |
||||||
|
dispatchHandleExpandPath: (paths: string[]) => Promise<void> |
||||||
|
} |
||||||
|
|
||||||
|
export interface FileExplorerMenuProps { |
||||||
|
title: string, |
||||||
|
menuItems: string[], |
||||||
|
createNewFile: (folder?: string) => void, |
||||||
|
createNewFolder: (parentFolder?: string) => void, |
||||||
|
publishToGist: (path?: string) => void, |
||||||
|
uploadFile: (target: EventTarget & HTMLInputElement) => void |
||||||
|
} |
||||||
|
export interface FileExplorerContextMenuProps { |
||||||
|
actions: action[], |
||||||
|
createNewFile: (folder?: string) => void, |
||||||
|
createNewFolder: (parentFolder?: string) => void, |
||||||
|
deletePath: (path: string | string[]) => void, |
||||||
|
renamePath: (path: string, type: string) => void, |
||||||
|
hideContextMenu: () => void, |
||||||
|
publishToGist?: (path?: string, type?: string) => void, |
||||||
|
pushChangesToGist?: (path?: string, type?: string) => void, |
||||||
|
publishFolderToGist?: (path?: string, type?: string) => void, |
||||||
|
publishFileToGist?: (path?: string, type?: string) => void, |
||||||
|
runScript?: (path: string) => void, |
||||||
|
emit?: (cmd: customAction) => void, |
||||||
|
pageX: number, |
||||||
|
pageY: number, |
||||||
|
path: string, |
||||||
|
type: string, |
||||||
|
focus: {key:string, type:string}[], |
||||||
|
onMouseOver?: (...args) => void, |
||||||
|
copy?: (path: string, type: string) => void, |
||||||
|
paste?: (destination: string, type: string) => void |
||||||
|
} |
||||||
|
|
||||||
|
export interface FileExplorerState { |
||||||
|
ctrlKey: boolean |
||||||
|
newFileName: string |
||||||
|
actions: { |
||||||
|
id: string |
||||||
|
name: string |
||||||
|
type?: Array<'folder' | 'gist' | 'file'> |
||||||
|
path?: string[] |
||||||
|
extension?: string[] |
||||||
|
pattern?: string[] |
||||||
|
multiselect: boolean |
||||||
|
label: string |
||||||
|
}[] |
||||||
|
focusContext: { |
||||||
|
element: string |
||||||
|
x: number |
||||||
|
y: number |
||||||
|
type: string |
||||||
|
} |
||||||
|
focusEdit: { |
||||||
|
element: string |
||||||
|
type: string |
||||||
|
isNew: boolean |
||||||
|
lastEdit: string |
||||||
|
} |
||||||
|
mouseOverElement: string |
||||||
|
showContextMenu: boolean |
||||||
|
reservedKeywords: string[] |
||||||
|
copyElement: { |
||||||
|
key: string |
||||||
|
type: 'folder' | 'gist' | 'file' |
||||||
|
}[] |
||||||
|
} |
@ -0,0 +1,63 @@ |
|||||||
|
import { MenuItems } from '../types' |
||||||
|
|
||||||
|
export const contextMenuActions: MenuItems = [{ |
||||||
|
id: 'newFile', |
||||||
|
name: 'New File', |
||||||
|
type: ['folder', 'gist'], |
||||||
|
multiselect: false, |
||||||
|
label: '' |
||||||
|
}, { |
||||||
|
id: 'newFolder', |
||||||
|
name: 'New Folder', |
||||||
|
type: ['folder', 'gist'], |
||||||
|
multiselect: false, |
||||||
|
label: '' |
||||||
|
}, { |
||||||
|
id: 'rename', |
||||||
|
name: 'Rename', |
||||||
|
type: ['file', 'folder'], |
||||||
|
multiselect: false, |
||||||
|
label: '' |
||||||
|
}, { |
||||||
|
id: 'delete', |
||||||
|
name: 'Delete', |
||||||
|
type: ['file', 'folder', 'gist'], |
||||||
|
multiselect: false, |
||||||
|
label: '' |
||||||
|
}, { |
||||||
|
id: 'run', |
||||||
|
name: 'Run', |
||||||
|
extension: ['.js'], |
||||||
|
multiselect: false, |
||||||
|
label: '' |
||||||
|
}, { |
||||||
|
id: 'pushChangesToGist', |
||||||
|
name: 'Push changes to gist', |
||||||
|
type: ['gist'], |
||||||
|
multiselect: false, |
||||||
|
label: '' |
||||||
|
}, { |
||||||
|
id: 'publishFolderToGist', |
||||||
|
name: 'Publish folder to gist', |
||||||
|
type: ['folder'], |
||||||
|
multiselect: false, |
||||||
|
label: '' |
||||||
|
}, { |
||||||
|
id: 'publishFileToGist', |
||||||
|
name: 'Publish file to gist', |
||||||
|
type: ['file'], |
||||||
|
multiselect: false, |
||||||
|
label: '' |
||||||
|
}, { |
||||||
|
id: 'copy', |
||||||
|
name: 'Copy', |
||||||
|
type: ['folder', 'file'], |
||||||
|
multiselect: false, |
||||||
|
label: '' |
||||||
|
}, { |
||||||
|
id: 'deleteAll', |
||||||
|
name: 'Delete All', |
||||||
|
type: ['folder', 'file'], |
||||||
|
multiselect: true, |
||||||
|
label: '' |
||||||
|
}] |
File diff suppressed because it is too large
Load Diff
Loading…
Reference in new issue