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 { 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' |
||||
|
||||
declare global { |
@ -1,5 +1,5 @@ |
||||
import React, { useState, useEffect } from 'react' //eslint-disable-line
|
||||
import { FileExplorerMenuProps } from './types' |
||||
import { FileExplorerMenuProps } from '../types' |
||||
|
||||
export const FileExplorerMenu = (props: FileExplorerMenuProps) => { |
||||
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