Move file-explorer to workspace directory

pull/1575/head
ioedeveloper 3 years ago
parent 0b6579f444
commit 3550cda699
  1. 135
      libs/remix-ui/workspace/src/lib/components/file-explorer-context-menu.tsx
  2. 98
      libs/remix-ui/workspace/src/lib/components/file-explorer-menu.tsx
  3. 654
      libs/remix-ui/workspace/src/lib/components/file-explorer.tsx
  4. 28
      libs/remix-ui/workspace/src/lib/css/file-explorer-context-menu.css
  5. 56
      libs/remix-ui/workspace/src/lib/css/file-explorer.css
  6. 61
      libs/remix-ui/workspace/src/lib/css/remix-ui-workspace.css
  7. 4
      libs/remix-ui/workspace/src/lib/providers/FileSystemProvider.tsx
  8. 85
      libs/remix-ui/workspace/src/lib/types/index.ts
  9. 61
      libs/remix-ui/workspace/src/lib/utils/index.ts

@ -0,0 +1,135 @@
import React, { useRef, useEffect } from 'react' // eslint-disable-line
import { action, FileExplorerContextMenuProps } from '../types'
import './css/file-explorer-context-menu.css'
import { customAction } from '@remixproject/plugin-api/lib/file-system/file-panel'
declare global {
interface Window {
_paq: any
}
}
const _paq = window._paq = window._paq || [] //eslint-disable-line
export const FileExplorerContextMenu = (props: FileExplorerContextMenuProps) => {
const { actions, createNewFile, createNewFolder, deletePath, renamePath, hideContextMenu, pushChangesToGist, publishFileToGist, publishFolderToGist, copy, paste, runScript, emit, pageX, pageY, path, type, focus, ...otherProps } = props
const contextMenuRef = useRef(null)
useEffect(() => {
contextMenuRef.current.focus()
}, [])
useEffect(() => {
const menuItemsContainer = contextMenuRef.current
const boundary = menuItemsContainer.getBoundingClientRect()
if (boundary.bottom > (window.innerHeight || document.documentElement.clientHeight)) {
menuItemsContainer.style.position = 'fixed'
menuItemsContainer.style.bottom = '10px'
menuItemsContainer.style.top = null
}
}, [pageX, pageY])
const filterItem = (item: action) => {
/**
* if there are multiple elements focused we need to take this and all conditions must be met
* for example : 'downloadAsZip' with type ['file','folder'] will work on files and folders when multiple are selected
**/
const nonRootFocus = focus.filter((el) => { return !(el.key === '' && el.type === 'folder') })
if (nonRootFocus.length > 1) {
for (const element of nonRootFocus) {
if (!itemMatchesCondition(item, element.type, element.key)) return false
}
return true
} else {
return itemMatchesCondition(item, type, path)
}
}
const itemMatchesCondition = (item: action, itemType: string, itemPath: string) => {
if (item.type && Array.isArray(item.type) && (item.type.findIndex(name => name === itemType) !== -1)) return true
else if (item.path && Array.isArray(item.path) && (item.path.findIndex(key => key === itemPath) !== -1)) return true
else if (item.extension && Array.isArray(item.extension) && (item.extension.findIndex(ext => itemPath.endsWith(ext)) !== -1)) return true
else if (item.pattern && Array.isArray(item.pattern) && (item.pattern.filter(value => itemPath.match(new RegExp(value))).length > 0)) return true
else return false
}
const getPath = () => {
if (focus.length > 1) {
return focus.map((element) => element.key)
} else {
return path
}
}
const menu = () => {
return actions.filter(item => filterItem(item)).map((item, index) => {
return <li
id={`menuitem${item.name.toLowerCase()}`}
key={index}
className='remixui_liitem'
onClick={(e) => {
e.stopPropagation()
switch (item.name) {
case 'New File':
createNewFile(path)
break
case 'New Folder':
createNewFolder(path)
break
case 'Rename':
renamePath(path, type)
break
case 'Delete':
deletePath(getPath())
break
case 'Push changes to gist':
_paq.push(['trackEvent', 'fileExplorer', 'pushToChangesoGist'])
pushChangesToGist(path, type)
break
case 'Publish folder to gist':
_paq.push(['trackEvent', 'fileExplorer', 'publishFolderToGist'])
publishFolderToGist(path, type)
break
case 'Publish file to gist':
_paq.push(['trackEvent', 'fileExplorer', 'publishFileToGist'])
publishFileToGist(path, type)
break
case 'Run':
_paq.push(['trackEvent', 'fileExplorer', 'runScript'])
runScript(path)
break
case 'Copy':
copy(path, type)
break
case 'Paste':
paste(path, type)
break
case 'Delete All':
deletePath(getPath())
break
default:
_paq.push(['trackEvent', 'fileExplorer', 'customAction', `${item.id}/${item.name}`])
emit && emit({ ...item, path: [path] } as customAction)
break
}
hideContextMenu()
}}>{item.label || item.name}</li>
})
}
return (
<div
id="menuItemsContainer"
className="p-1 remixui_contextContainer bg-light shadow border"
style={{ left: pageX, top: pageY }}
ref={contextMenuRef}
onBlur={hideContextMenu}
tabIndex={500}
{...otherProps}
>
<ul id='remixui_menuitems'>{menu()}</ul>
</div>
)
}
export default FileExplorerContextMenu

@ -0,0 +1,98 @@
import React, { useState, useEffect } from 'react' //eslint-disable-line
import { FileExplorerMenuProps } from '../types'
export const FileExplorerMenu = (props: FileExplorerMenuProps) => {
const [state, setState] = useState({
menuItems: [
{
action: 'createNewFile',
title: 'Create New File',
icon: 'far fa-file'
},
{
action: 'createNewFolder',
title: 'Create New Folder',
icon: 'far fa-folder'
},
{
action: 'publishToGist',
title: 'Publish all the current workspace files (only root) to a github gist',
icon: 'fab fa-github'
},
{
action: 'uploadFile',
title: 'Load a local file into current workspace',
icon: 'fa fa-upload'
},
{
action: 'updateGist',
title: 'Update the current [gist] explorer',
icon: 'fab fa-github'
}
].filter(item => props.menuItems && props.menuItems.find((name) => { return name === item.action })),
actions: {}
})
useEffect(() => {
const actions = {
updateGist: () => {}
}
setState(prevState => {
return { ...prevState, actions }
})
}, [])
return (
<>
<span className='remixui_label' title={props.title} data-path={props.title} style={{ fontWeight: 'bold' }}>{ props.title }</span>
<span className="pl-2">{
state.menuItems.map(({ action, title, icon }, index) => {
if (action === 'uploadFile') {
return (
<label
id={action}
data-id={'fileExplorerUploadFile' + action }
className={icon + ' mb-0 remixui_newFile'}
title={title}
key={index}
>
<input id="fileUpload" data-id="fileExplorerFileUpload" type="file" onChange={(e) => {
e.stopPropagation()
props.uploadFile(e.target)
e.target.value = null
}}
multiple />
</label>
)
} else {
return (
<span
id={action}
data-id={'fileExplorerNewFile' + action}
onClick={(e) => {
e.stopPropagation()
if (action === 'createNewFile') {
props.createNewFile()
} else if (action === 'createNewFolder') {
props.createNewFolder()
} else if (action === 'publishToGist') {
props.publishToGist()
} else {
state.actions[action]()
}
}}
className={'newFile ' + icon + ' remixui_newFile'}
title={title}
key={index}
>
</span>
)
}
})}
</span>
</>
)
}
export default FileExplorerMenu

@ -0,0 +1,654 @@
import React, { useEffect, useState, useRef, useContext } from 'react' // eslint-disable-line
// import { DragDropContext, Droppable, Draggable } from 'react-beautiful-dnd' // eslint-disable-line
import { TreeView, TreeViewItem } from '@remix-ui/tree-view' // eslint-disable-line
import { FileExplorerMenu } from './file-explorer-menu' // eslint-disable-line
import { FileExplorerContextMenu } from './file-explorer-context-menu' // eslint-disable-line
import { FileExplorerProps, File, MenuItems, FileExplorerState } from '../types'
import { FileSystemContext } from '@remix-ui/workspace'
import { customAction } from '@remixproject/plugin-api/lib/file-system/file-panel'
import { contextMenuActions } from '../utils'
import './css/file-explorer.css'
import { checkSpecialChars, extractParentFromKey, getPathIcon, joinPath } from '@remix-ui/helper'
export const FileExplorer = (props: FileExplorerProps) => {
const { name, focusRoot, contextMenuItems, externalUploads, removedContextMenuItems, resetFocus, 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: ''
},
expandPath: [name],
mouseOverElement: null,
showContextMenu: false,
reservedKeywords: [name, 'gist-'],
copyElement: []
})
const [canPaste, setCanPaste] = useState(false)
const editRef = useRef(null)
const global = useContext(FileSystemContext)
useEffect(() => {
if (global.fs.mode === 'browser') {
setState(prevState => {
return { ...prevState, expandPath: [...new Set([...prevState.expandPath, ...global.fs.browser.expandPath])] }
})
} else if (global.fs.mode === 'localhost') {
setState(prevState => {
return { ...prevState, expandPath: [...new Set([...prevState.expandPath, ...global.fs.localhost.expandPath])] }
})
}
}, [global.fs.browser.expandPath, global.fs.localhost.expandPath])
useEffect(() => {
if (state.focusEdit.element) {
setTimeout(() => {
if (editRef && editRef.current) {
editRef.current.focus()
}
}, 0)
}
}, [state.focusEdit.element])
useEffect(() => {
if (focusRoot) {
global.dispatchSetFocusElement([{ key: '', type: 'folder' }])
resetFocus(false)
}
}, [focusRoot])
useEffect(() => {
if (contextMenuItems) {
addMenuItems(contextMenuItems)
}
}, [contextMenuItems])
useEffect(() => {
if (removedContextMenuItems) {
removeMenuItems(removedContextMenuItems)
}
}, [contextMenuItems])
useEffect(() => {
if (global.fs.focusEdit) {
setState(prevState => {
return { ...prevState, focusEdit: { element: global.fs.focusEdit, type: 'file', isNew: true, lastEdit: null } }
})
}
}, [global.fs.focusEdit])
useEffect(() => {
if (externalUploads) {
uploadFile(externalUploads)
}
}, [externalUploads])
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 extractNameFromKey = (key: string):string => {
const keyPath = key.split('/')
return keyPath[keyPath.length - 1]
}
const hasReservedKeyword = (content: string): boolean => {
if (state.reservedKeywords.findIndex(value => content.startsWith(value)) !== -1) return true
else return false
}
const getFocusedFolder = () => {
if (global.fs.focusElement[0]) {
if (global.fs.focusElement[0].type === 'folder' && global.fs.focusElement[0].key) return global.fs.focusElement[0].key
else if (global.fs.focusElement[0].type === 'gist' && global.fs.focusElement[0].key) return global.fs.focusElement[0].key
else if (global.fs.focusElement[0].type === 'file' && global.fs.focusElement[0].key) return extractParentFromKey(global.fs.focusElement[0].key) ? extractParentFromKey(global.fs.focusElement[0].key) : name
else return name
}
}
const createNewFile = async (newFilePath: string) => {
try {
global.dispatchCreateNewFile(newFilePath, props.name)
} catch (error) {
return global.modal('File Creation Failed', typeof error === 'string' ? error : error.message, 'Close', async () => {})
}
}
const createNewFolder = async (newFolderPath: string) => {
try {
global.dispatchCreateNewFolder(newFolderPath, props.name)
} catch (e) {
return global.modal('Folder Creation Failed', typeof e === 'string' ? e : e.message, 'Close', async () => {})
}
}
const deletePath = async (path: string[]) => {
if (global.fs.readonly) return global.toast('cannot delete file. ' + name + ' is a read only explorer')
if (!Array.isArray(path)) path = [path]
global.modal(`Delete ${path.length > 1 ? 'items' : 'item'}`, deleteMessage(path), 'OK', () => { global.dispatchDeletePath(path) }, 'Cancel', () => {})
}
const renamePath = async (oldPath: string, newPath: string) => {
try {
global.dispatchRenamePath(oldPath, newPath)
} catch (error) {
global.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([...state.expandPath, parentFolder])]
setState(prevState => {
return { ...prevState, expandPath }
})
global.dispatchUploadFile(target, parentFolder)
}
const copyFile = (src: string, dest: string) => {
try {
global.dispatchCopyFile(src, dest)
} catch (error) {
global.modal('Copy File Failed', 'Unexpected error while copying file: ' + src, 'Close', async () => {})
}
}
const copyFolder = (src: string, dest: string) => {
try {
global.dispatchCopyFolder(src, dest)
} catch (error) {
global.modal('Copy Folder Failed', 'Unexpected error while copying folder: ' + src, 'Close', async () => {})
}
}
const publishToGist = (path?: string, type?: string) => {
global.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) => {
global.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) => {
global.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) => {
global.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) => {
global.dispatchPublishToGist(path, type)
}
const runScript = async (path: string) => {
try {
global.dispatchRunScript(path)
} catch (error) {
global.toast('Run script failed')
}
}
const emitContextMenuEvent = (cmd: customAction) => {
try {
global.dispatchEmitContextMenuEvent(cmd)
} catch (error) {
global.toast(error)
}
}
const handleClickFile = (path: string, type: 'folder' | 'file' | 'gist') => {
path = path.indexOf(props.name + '/') === 0 ? path.replace(props.name + '/', '') : path
if (!state.ctrlKey) {
global.dispatchHandleClickFile(path, type)
} else {
if (global.fs.focusElement.findIndex(item => item.key === path) !== -1) {
const focusElement = global.fs.focusElement.filter(item => item.key !== path)
global.dispatchSetFocusElement(focusElement)
} else {
const nonRootFocus = global.fs.focusElement.filter((el) => { return !(el.key === '' && el.type === 'folder') })
nonRootFocus.push({ key: path, type })
global.dispatchSetFocusElement(nonRootFocus)
}
}
}
const handleClickFolder = async (path: string, type: 'folder' | 'file' | 'gist') => {
if (state.ctrlKey) {
if (global.fs.focusElement.findIndex(item => item.key === path) !== -1) {
const focusElement = global.fs.focusElement.filter(item => item.key !== path)
global.dispatchSetFocusElement(focusElement)
} else {
const nonRootFocus = global.fs.focusElement.filter((el) => { return !(el.key === '' && el.type === 'folder') })
nonRootFocus.push({ key: path, type })
global.dispatchSetFocusElement(nonRootFocus)
}
} else {
let expandPath = []
if (!state.expandPath.includes(path)) {
expandPath = [...new Set([...state.expandPath, path])]
global.dispatchFetchDirectory(path)
} else {
expandPath = [...new Set(state.expandPath.filter(key => key && (typeof key === 'string') && !key.startsWith(path)))]
}
global.dispatchSetFocusElement([{ key: path, type }])
setState(prevState => {
return { ...prevState, expandPath }
})
}
}
const handleContextMenuFile = (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 handleContextMenuFolder = (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 (global.fs.readonly) return
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) {
global.dispatchRemoveInputField(parentFolder)
setState(prevState => {
return { ...prevState, focusEdit: { element: null, isNew: false, type: '', lastEdit: '' } }
})
} else {
editRef.current.textContent = state.focusEdit.lastEdit
setState(prevState => {
return { ...prevState, focusEdit: { element: null, isNew: false, type: '', lastEdit: '' } }
})
}
} else {
if (state.focusEdit.lastEdit === content) {
editRef.current.textContent = content
return setState(prevState => {
return { ...prevState, focusEdit: { element: null, isNew: false, type: '', lastEdit: '' } }
})
}
if (checkSpecialChars(content)) {
global.modal('Validation Error', 'Special characters are not allowed', 'OK', () => {})
} else {
if (state.focusEdit.isNew) {
if (hasReservedKeyword(content)) {
global.dispatchRemoveInputField(parentFolder)
global.modal('Reserved Keyword', `File name contains remix reserved keywords. '${content}'`, 'Close', () => {})
} else {
state.focusEdit.type === 'file' ? createNewFile(joinPath(parentFolder, content)) : createNewFolder(joinPath(parentFolder, content))
global.dispatchRemoveInputField(parentFolder)
}
} else {
if (hasReservedKeyword(content)) {
editRef.current.textContent = state.focusEdit.lastEdit
global.modal('Reserved Keyword', `File name contains remix reserved keywords. '${content}'`, 'Close', () => {})
} else {
const oldPath: string = state.focusEdit.element
const oldName = extractNameFromKey(oldPath)
const newPath = oldPath.replace(oldName, content)
editRef.current.textContent = extractNameFromKey(oldPath)
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([...state.expandPath, parentFolder])]
await global.dispatchAddInputField(parentFolder, 'file')
setState(prevState => {
return { ...prevState, 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([...state.expandPath, parentFolder])]
await global.dispatchAddInputField(parentFolder, 'folder')
setState(prevState => {
return { ...prevState, expandPath }
})
editModeOn(parentFolder + '/blank', 'folder', true)
}
const handleEditInput = (event) => {
if (event.which === 13) {
event.preventDefault()
editModeOff(editRef.current.innerText)
}
}
const handleMouseOver = (path: string) => {
setState(prevState => {
return { ...prevState, mouseOverElement: path }
})
}
const handleMouseOut = () => {
setState(prevState => {
return { ...prevState, mouseOverElement: null }
})
}
const handleCopyClick = (path: string, type: 'folder' | 'gist' | 'file') => {
setState(prevState => {
return { ...prevState, copyElement: [{ key: path, type }] }
})
setCanPaste(true)
global.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 label = (file: File) => {
const isEditable = (state.focusEdit.element === file.path) || (global.fs.focusEdit === file.path)
return (
<div
className='remixui_items d-inline-block w-100'
ref={ isEditable ? editRef : null}
suppressContentEditableWarning={true}
contentEditable={isEditable}
onKeyDown={handleEditInput}
onBlur={(e) => {
e.stopPropagation()
editModeOff(editRef.current.innerText)
}}
>
<span
title={file.path}
className={'remixui_label ' + (file.isDirectory ? 'folder' : 'remixui_leaf')}
data-path={file.path}
>
{ file.name }
</span>
</div>
)
}
const renderFiles = (file: File, index: number) => {
if (!file || !file.path || typeof file === 'string' || typeof file === 'number' || typeof file === 'boolean') return
const labelClass = state.focusEdit.element === file.path
? 'bg-light' : global.fs.focusElement.findIndex(item => item.key === file.path) !== -1
? 'bg-secondary' : state.mouseOverElement === file.path
? 'bg-light border' : (state.focusContext.element === file.path) && (state.focusEdit.element !== file.path)
? 'bg-light border' : ''
const icon = getPathIcon(file.path)
const spreadProps = {
onClick: (e) => e.stopPropagation()
}
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 + index}`}
label={label(file)}
onClick={(e) => {
e.stopPropagation()
if (state.focusEdit.element !== file.path) handleClickFolder(file.path, file.type)
}}
onContextMenu={(e) => {
e.preventDefault()
e.stopPropagation()
handleContextMenuFolder(e.pageX, e.pageY, file.path, e.target.textContent, file.type)
}}
labelClass={labelClass}
controlBehaviour={ state.ctrlKey }
expand={state.expandPath.includes(file.path)}
onMouseOver={(e) => {
e.stopPropagation()
handleMouseOver(file.path)
}}
onMouseOut={(e) => {
e.stopPropagation()
if (state.mouseOverElement === file.path) handleMouseOut()
}}
>
{
file.child ? <TreeView id={`treeView${file.path}`} key={`treeView${file.path}`} {...spreadProps }>{
Object.keys(file.child).map((key, index) => {
return renderFiles(file.child[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={label(file)}
onClick={(e) => {
e.stopPropagation()
if (state.focusEdit.element !== file.path) handleClickFile(file.path, file.type)
}}
onContextMenu={(e) => {
e.preventDefault()
e.stopPropagation()
handleContextMenuFile(e.pageX, e.pageY, file.path, e.target.textContent, file.type)
}}
icon={icon}
labelClass={labelClass}
onMouseOver={(e) => {
e.stopPropagation()
handleMouseOver(file.path)
}}
onMouseOut={(e) => {
e.stopPropagation()
if (state.mouseOverElement === file.path) handleMouseOut()
}}
/>
)
}
}
return (
<div>
<TreeView id='treeView'>
<TreeViewItem id="treeViewItem"
controlBehaviour={true}
label={
<div onClick={(e) => {
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 (!state.expandPath.includes(props.name)) {
expandPath = [props.name, ...new Set([...state.expandPath])]
} else {
expandPath = [...new Set(state.expandPath.filter(key => key && (typeof key === 'string') && !key.startsWith(props.name)))]
}
setState(prevState => {
return { ...prevState, expandPath }
})
resetFocus(true)
}}>
<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) => {
return renderFiles(files[props.name][key], index)
})
}
</TreeView>
</div>
</TreeViewItem>
</TreeView>
{ state.showContextMenu &&
<FileExplorerContextMenu
actions={global.fs.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={global.fs.focusElement}
onMouseOver={(e) => {
e.stopPropagation()
handleMouseOver(state.focusContext.element)
}}
pushChangesToGist={pushChangesToGist}
publishFolderToGist={publishFolderToGist}
publishFileToGist={publishFileToGist}
/>
}
</div>
)
}
export default FileExplorer

@ -0,0 +1,28 @@
.remixui_contextContainer
{
display: block;
position: fixed;
border-radius: 2px;
z-index: 1000;
box-shadow: 0 0 4px var(--dark);
}
.remixui_contextContainer:focus {
outline: none;
}
.remixui_liitem
{
padding: 2px;
padding-left: 6px;
cursor: pointer;
color: var(--text-dark);
background-color: var(--light);
}
.remixui_liitem:hover
{
background-color: var(--secondary);
}
#remixui_menuitems
{
list-style: none;
margin: 0px;
}

@ -0,0 +1,56 @@
.remixui_label {
margin-top : 4px;
}
.remixui_leaf {
overflow : hidden;
text-overflow : ellipsis;
width : 90%;
margin-bottom : 0px;
}
.remixui_fileexplorer {
box-sizing : border-box;
user-select : none;
}
input[type="file"] {
display: none;
}
.remixui_folder,
.remixui_file {
font-size : 14px;
cursor : pointer;
}
.remixui_file {
padding : 4px;
}
.remixui_newFile {
padding-right : 10px;
}
.remixui_newFile i {
cursor : pointer;
}
.remixui_newFile:hover {
transform : scale(1.3);
}
.remixui_menu {
margin-left : 20px;
}
.remixui_items {
display : inline
}
.remixui_remove {
margin-left : auto;
padding-left : 5px;
padding-right : 5px;
}
.remixui_activeMode {
display : flex;
width : 100%;
margin-right : 10px;
padding-right : 19px;
}
.remixui_activeMode > div {
min-width : 10px;
}
ul {
padding : 0;
}

@ -0,0 +1,61 @@
.remixui_container {
display : flex;
flex-direction : row;
width : 100%;
height : 100%;
box-sizing : border-box;
}
.remixui_fileexplorer {
display : flex;
flex-direction : column;
position : relative;
width : 100%;
padding-left : 6px;
padding-right : 6px;
padding-top : 6px;
}
.remixui_fileExplorerTree {
cursor : default;
}
.remixui_gist {
padding : 10px;
}
.remixui_gist i {
cursor : pointer;
}
.remixui_gist i:hover {
color : orange;
}
.remixui_connectToLocalhost {
padding : 10px;
}
.remixui_connectToLocalhost i {
cursor : pointer;
}
.remixui_connectToLocalhost i:hover {
color : var(--secondary)
}
.remixui_uploadFile {
padding : 10px;
}
.remixui_uploadFile label:hover {
color : var(--secondary)
}
.remixui_uploadFile label {
cursor : pointer;
}
.remixui_treeview {
overflow-y : auto;
}
.remixui_dialog {
display: flex;
flex-direction: column;
}
.remixui_dialogParagraph {
margin-bottom: 2em;
word-break: break-word;
}
.remixui_menuicon {
padding-right : 10px;
}

@ -104,11 +104,11 @@ export const FileSystemProvider = (props: WorkspaceProps) => {
} }
const dispatchEmitContextMenuEvent = async (cmd: customAction) => { const dispatchEmitContextMenuEvent = async (cmd: customAction) => {
await emitContextMenuEvent(cmd) await emitContextMenuEvent(cmd)()
} }
const dispatchHandleClickFile = async (path: string, type: 'file' | 'folder' | 'gist') => { const dispatchHandleClickFile = async (path: string, type: 'file' | 'folder' | 'gist') => {
await handleClickFile(path, type) await handleClickFile(path, type)(fsDispatch)
} }
useEffect(() => { useEffect(() => {

@ -1,4 +1,4 @@
import { MenuItems } from '@remix-ui/file-explorer' import { customAction } from '@remixproject/plugin-api/lib/file-system/file-panel'
export interface WorkspaceProps { export interface WorkspaceProps {
plugin: { plugin: {
setWorkspace: ({ name: string, isLocalhost: boolean }, setEvent: boolean) => void, setWorkspace: ({ name: string, isLocalhost: boolean }, setEvent: boolean) => void,
@ -53,3 +53,86 @@ export interface File {
type: 'folder' | 'file' | 'gist', type: 'folder' | 'file' | 'gist',
child?: File[] child?: File[]
} }
/* eslint-disable-next-line */
export interface FileExplorerProps {
name: string,
menuItems?: string[],
focusRoot: boolean,
contextMenuItems: MenuItems,
removedContextMenuItems: MenuItems,
displayInput?: boolean,
externalUploads?: EventTarget & HTMLInputElement,
resetFocus?: (value: boolean) => void,
files: { [x: string]: Record<string, File> }
}
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 type action = { name: string, type?: Array<'folder' | 'gist' | 'file'>, path?: string[], extension?: string[], pattern?: string[], id: string, multiselect: boolean, label: string }
export type MenuItems = action[]
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
}
expandPath: string[]
mouseOverElement: string
showContextMenu: boolean
reservedKeywords: string[]
copyElement: {
key: string
type: 'folder' | 'gist' | 'file'
}[]
}

@ -0,0 +1,61 @@
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: ''
}]
Loading…
Cancel
Save