Uploa, Download working on workspace

pull/3657/head
Seth Samuel 2 years ago committed by Aniket
parent 802562133d
commit e1fbfcfbb1
  1. 43
      libs/remix-ui/workspace/src/lib/components/file-explorer-context-menu.tsx
  2. 2
      libs/remix-ui/workspace/src/lib/components/file-explorer-menu.tsx
  3. 198
      libs/remix-ui/workspace/src/lib/components/file-explorer.tsx
  4. 31
      libs/remix-ui/workspace/src/lib/components/upload-file.tsx
  5. 531
      libs/remix-ui/workspace/src/lib/remix-ui-workspace.tsx
  6. 48
      libs/remix-ui/workspace/src/lib/types/index.ts
  7. 29
      libs/remix-ui/workspace/src/lib/utils/index.ts

@ -1,9 +1,10 @@
import React, { useRef, useEffect } from 'react' // eslint-disable-line
import React, { useRef, useEffect, useState } from 'react' // eslint-disable-line
import { useIntl } from 'react-intl'
import { action, FileExplorerContextMenuProps } from '../types'
import '../css/file-explorer-context-menu.css'
import { customAction } from '@remixproject/plugin-api'
import UploadFile from './upload-file'
declare global {
interface Window {
@ -13,9 +14,11 @@ declare global {
const _paq = window._paq = window._paq || [] //eslint-disable-line
export const FileExplorerContextMenu = (props: FileExplorerContextMenuProps) => {
const { actions, createNewFile, createNewFolder, deletePath, renamePath, downloadPath, hideContextMenu, pushChangesToGist, publishFileToGist, publishFolderToGist, copy, copyFileName, copyPath, paste, runScript, emit, pageX, pageY, path, type, focus, ...otherProps } = props
const { actions, createNewFile, createNewFolder, deletePath, renamePath, hideContextMenu, pushChangesToGist, publishFileToGist, publishFolderToGist, copy, copyFileName, copyPath, paste, runScript, emit, pageX, pageY, path, type, focus, downloadPath, uploadFile,...otherProps } = props
const contextMenuRef = useRef(null)
const intl = useIntl()
const [showFileExplorer, setShowFileExplorer] = useState(false)
useEffect(() => {
contextMenuRef.current.focus()
}, [])
@ -37,6 +40,11 @@ export const FileExplorerContextMenu = (props: FileExplorerContextMenuProps) =>
* 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(focus[0].key === "contextMenu"){
return true
}
if (nonRootFocus.length > 1) {
for (const element of nonRootFocus) {
if (!itemMatchesCondition(item, element.type, element.key)) return false
@ -65,6 +73,29 @@ export const FileExplorerContextMenu = (props: FileExplorerContextMenuProps) =>
const menu = () => {
return actions.filter(item => filterItem(item)).map((item, index) => {
if(item.name === "Upload File"){
return <li
id={`menuitem${item.name.toLowerCase()}`}
key={index}
className='remixui_liitem'
onClick={()=>{
_paq.push(['trackEvent', 'fileExplorer', 'contextMenu', 'uploadFile'])
setShowFileExplorer(true)
}}
>{intl.formatMessage({id: `filePanel.${item.id}`, defaultMessage: item.label || item.name})}</li>
}
if(item.name === "Load a Local File"){
return <li
id={`menuitem${item.name.toLowerCase()}`}
key={index}
className='remixui_liitem'
onClick={()=>{
_paq.push(['trackEvent', 'fileExplorer', 'contextMenu', 'uploadFile'])
setShowFileExplorer(true)
}}
>{intl.formatMessage({id: `filePanel.${item.id}`, defaultMessage: item.label || item.name})}</li>
}
return <li
id={`menuitem${item.name.toLowerCase()}`}
key={index}
@ -128,6 +159,10 @@ export const FileExplorerContextMenu = (props: FileExplorerContextMenuProps) =>
deletePath(getPath())
_paq.push(['trackEvent', 'fileExplorer', 'contextMenu', 'deleteAll'])
break
case 'Publish Workspace to Gist':
_paq.push(['trackEvent', 'fileExplorer', 'contextMenu', 'publishWorkspace'])
publishFolderToGist(path, type)
break
default:
_paq.push(['trackEvent', 'fileExplorer', 'customAction', `${item.id}/${item.name}`])
emit && emit({ ...item, path: [path] } as customAction)
@ -137,7 +172,7 @@ export const FileExplorerContextMenu = (props: FileExplorerContextMenuProps) =>
}}>{intl.formatMessage({id: `filePanel.${item.id}`, defaultMessage: item.label || item.name})}</li>
})
}
return (
<div
id="menuItemsContainer"
@ -148,6 +183,8 @@ export const FileExplorerContextMenu = (props: FileExplorerContextMenuProps) =>
tabIndex={500}
{...otherProps}
>
{showFileExplorer && <UploadFile onUpload={(target)=> {
uploadFile(target); }} multiple />}
<ul id='remixui_menuitems'>{menu()}</ul>
</div>
)

@ -1,5 +1,5 @@
import { CustomTooltip } from '@remix-ui/helper'
import React, { useState, useEffect } from 'react' //eslint-disable-line
import React, { useState, useEffect, } from 'react' //eslint-disable-line
import { FormattedMessage } from 'react-intl'
import { Placement } from 'react-bootstrap/esm/Overlay'
import { FileExplorerMenuProps } from '../types'

@ -2,7 +2,7 @@ import React, { useEffect, useState, useRef, SyntheticEvent } from 'react' // es
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 { FileExplorerProps, MenuItems, FileExplorerState, WorkSpaceState } from '../types'
import { customAction } from '@remixproject/plugin-api'
import { contextMenuActions } from '../utils'
@ -14,29 +14,9 @@ import { Drag } from "@remix-ui/drag-n-drop"
import { ROOT_PATH } from '../utils/constants'
export const FileExplorer = (props: FileExplorerProps) => {
const { name, contextMenuItems, removedContextMenuItems, files, fileState } = 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: [ROOT_PATH, 'gist-'],
copyElement: []
})
const [canPaste, setCanPaste] = useState(false)
const { name, contextMenuItems, removedContextMenuItems, files, workspaceState, toGist, addMenuItems,
removeMenuItems, handleContextMenu, handleNewFileInput, handleNewFolderInput, uploadFile, fileState } = props
const [state, setState] = useState<WorkSpaceState>( workspaceState)
const treeRef = useRef<HTMLDivElement>(null)
useEffect(() => {
@ -59,6 +39,10 @@ export const FileExplorer = (props: FileExplorerProps) => {
}
}, [props.focusEdit])
useEffect(() => {
setState(workspaceState)
}, [workspaceState])
useEffect(() => {
if (treeRef.current) {
const keyPressHandler = (e: KeyboardEvent) => {
@ -87,62 +71,11 @@ export const FileExplorer = (props: FileExplorerProps) => {
}
}, [treeRef.current])
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) : ROOT_PATH
else return ROOT_PATH
}
}
const createNewFile = async (newFilePath: string) => {
try {
props.dispatchCreateNewFile(newFilePath, ROOT_PATH)
@ -159,13 +92,6 @@ export const FileExplorer = (props: FileExplorerProps) => {
}
}
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)
@ -218,37 +144,6 @@ export const FileExplorer = (props: FileExplorerProps) => {
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') => {
if (!state.ctrlKey) {
@ -294,26 +189,6 @@ export const FileExplorer = (props: FileExplorerProps) => {
}
}
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 = 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)
@ -366,51 +241,7 @@ export const FileExplorer = (props: FileExplorerProps) => {
}
}
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) || ROOT_PATH : 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
@ -425,17 +256,6 @@ export const FileExplorer = (props: FileExplorerProps) => {
props.dispatchHandleExpandPath(expandPath)
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const handleCopyFileNameClick = (path: string, _type: string) => {
const fileName = extractNameFromKey(path)
navigator.clipboard.writeText(fileName)
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const handleCopyFilePathClick = (path: string, _type: string) => {
navigator.clipboard.writeText(path)
}
const handleFileMove = (dest: string, src: string) => {
try {
props.dispatchMoveFile(src, dest)

@ -0,0 +1,31 @@
import React, { useEffect, useRef } from "react";
type UploadFileProps = {
onUpload: (target: EventTarget & HTMLInputElement, files?: FileList) => void;
accept?: string;
multiple?: boolean;
};
const UploadFile = (props: UploadFileProps) => {
const ref = useRef<HTMLInputElement>();
useEffect(() => {
ref.current.click();
ref.current.onchange= (event)=>{
//@ts-ignore
props.onUpload(event.target, event.target.files);
}
}, []);
return (
<input
ref={ref}
style={{ display: "none" }}
accept={props.accept}
multiple={props.multiple}
type="file"
/>
);
};
export default UploadFile;

@ -1,12 +1,19 @@
import React, { useState, useEffect, useRef, useContext, SyntheticEvent, ChangeEvent, KeyboardEvent } from 'react' // eslint-disable-line
import React, { useState, useEffect, useRef, useContext, SyntheticEvent, ChangeEvent, KeyboardEvent, MouseEvent } from 'react' // eslint-disable-line
import { FormattedMessage, useIntl } from 'react-intl'
import { Dropdown } from 'react-bootstrap'
import { CustomIconsToggle, CustomMenu, CustomToggle, CustomTooltip } from '@remix-ui/helper'
import { CustomIconsToggle, CustomMenu, CustomToggle, CustomTooltip, extractNameFromKey, extractParentFromKey } from '@remix-ui/helper'
import { FileExplorer } from './components/file-explorer' // eslint-disable-line
import { FileSystemContext } from './contexts'
import './css/remix-ui-workspace.css'
import { ROOT_PATH, TEMPLATE_NAMES } from './utils/constants'
import { HamburgerMenu } from './components/workspace-hamburger'
import { CopyElementType, FileFocusContextType } from './types'
import { MenuItems, WorkSpaceState } from './types'
import { contextMenuActions } from './utils'
import FileExplorerContextMenu from './components/file-explorer-context-menu'
import { customAction } from '@remixproject/plugin-api'
const _paq = window._paq = window._paq || []
const canUpload = window.File || window.FileReader || window.FileList || window.Blob
@ -35,6 +42,60 @@ export function Workspace () {
const initGitRepoRef = useRef<HTMLInputElement>()
const filteredBranches = selectedWorkspace ? (selectedWorkspace.branches || []).filter(branch => branch.name.includes(branchFilter) && branch.name !== 'HEAD').slice(0, 20) : []
const currentBranch = selectedWorkspace ? selectedWorkspace.currentBranch : null
const [focusContext, setFocusContext] = useState<FileFocusContextType>()
const [copyElement, setCopyElement] = useState<CopyElementType>()
const [contextType, setContextType] = useState<"file" | "folder">(null);
const [showContextMenu, setShowContextMenu] = useState(false)
const [canPaste, setCanPaste] = useState(false)
const [state, setState] = useState<WorkSpaceState>({
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: [ROOT_PATH, 'gist-'],
copyElement: []
})
useEffect(() => {
if (canPaste) {
addMenuItems([{
id: 'paste',
name: 'Paste',
type: ['folder', 'file', 'workspace'],
path: [],
extension: [],
pattern: [],
multiselect: false,
label: ''
}])
} else {
removeMenuItems([{
id: 'paste',
name: 'Paste',
type: ['folder', 'file', 'workspace'],
path: [],
extension: [],
pattern: [],
multiselect: false,
label: ''
}])
}
}, [canPaste])
useEffect(() => {
let workspaceName = localStorage.getItem('currentWorkspace')
@ -114,6 +175,23 @@ export function Workspace () {
)
}
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 cloneGitRepository = () => {
global.modal(
intl.formatMessage({ id: 'filePanel.workspace.clone' }),
@ -271,6 +349,158 @@ export function Workspace () {
}
}
const handleCopyClick = (path: string, type: 'folder' | 'gist' | 'file' | 'workspace') => {
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) || ROOT_PATH : dest
state.copyElement.map(({ key, type }) => {
type === 'file' ? copyFile(key, dest) : copyFolder(key, dest)
})
}
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 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 getFocusedFolder = () => {
const focusElement = global.fs.focusElement
if (focusElement[0]) {
if (focusElement[0].type === 'folder' && focusElement[0].key) return focusElement[0].key
else if (focusElement[0].type === 'gist' && focusElement[0].key) return focusElement[0].key
else if (focusElement[0].type === 'file' && focusElement[0].key) return extractParentFromKey(focusElement[0].key) ? extractParentFromKey(focusElement[0].key) : ROOT_PATH
else return ROOT_PATH
}
}
const uploadFile = (target) => {
const parentFolder = getFocusedFolder()
const expandPath = [...new Set([...global.fs.browser.expandPath, parentFolder])]
global.dispatchHandleExpandPath(expandPath)
global.dispatchUploadFile(target, parentFolder)
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const handleCopyFileNameClick = (path: string, _type: string) => {
const fileName = extractNameFromKey(path)
navigator.clipboard.writeText(fileName)
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const handleCopyFilePathClick = (path: string, _type: string) => {
navigator.clipboard.writeText(path)
}
const hideContextMenu = () => {
setState(prevState => {
return { ...prevState, focusContext: { element: null, x: 0, y: 0, type: '' }, showContextMenu: false }
})
}
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 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 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 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 toGist = (path?: string, type?: string) => {
global.dispatchPublishToGist(path, type)
}
const editModeOn = (path: string, type: string, isNew = false) => {
if (global.fs.readonly) return global.toast('Cannot write/modify file system in read only mode.')
setState(prevState => {
return { ...prevState, focusEdit: { ...prevState.focusEdit, element: path, isNew, type } }
})
}
const handleNewFileInput = async (parentFolder?: string) => {
if (!parentFolder) parentFolder = getFocusedFolder()
const expandPath = [...new Set([...global.fs.browser.expandPath, parentFolder])]
await global.dispatchAddInputField(parentFolder, 'file')
global.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([...global.fs.browser.expandPath, parentFolder])]
await global.dispatchAddInputField(parentFolder, 'folder')
global.dispatchHandleExpandPath(expandPath)
editModeOn(parentFolder + '/blank', 'folder', true)
}
const toggleDropdown = (isOpen: boolean) => {
setShowDropdown(isOpen)
}
@ -426,10 +656,239 @@ export function Workspace () {
)
}
const handleCanCopy = (path: string, type: 'folder' | 'gist' | 'file') => {
setCopyElement({ key: path, type })
}
const handleCloseContextMenu = () => {
setShowContextMenu(false)
};
const workspaceMenuIcons = [
<CustomTooltip
placement="right"
tooltipId="createWorkspaceTooltip"
tooltipClasses="text-nowrap"
tooltipText={<FormattedMessage id='filePanel.workspace.create' />}
>
<div
data-id='workspaceCreate'
onClick={() => {
createWorkspace()
_paq.push(['trackEvent', 'fileExplorer', 'workspaceMenu', 'workspaceCreate'])
hideIconsMenu(!showIconsMenu)
}}
key={`workspacesCreate-fe-ws`}
>
<span
hidden={currentWorkspace === LOCALHOST}
id='workspaceCreate'
data-id='workspaceCreate'
onClick={() => {
createWorkspace()
_paq.push(['trackEvent', 'fileExplorer', 'workspaceMenu', 'workspaceCreate'])
hideIconsMenu(!showIconsMenu)
}}
className='far fa-plus pl-2'
>
</span>
<span className="pl-3"><FormattedMessage id='filePanel.create' /></span>
</div>
</CustomTooltip>,
<CustomTooltip
placement="right-start"
tooltipId="createWorkspaceTooltip"
tooltipClasses="text-nowrap"
tooltipText={<FormattedMessage id='filePanel.workspace.delete' />}
>
<div
data-id='workspaceDelete'
onClick={() => {
deleteCurrentWorkspace()
_paq.push(['trackEvent', 'fileExplorer', 'workspaceMenu', 'workspaceDelete'])
hideIconsMenu(!showIconsMenu)
}}
key={`workspacesDelete-fe-ws`}
>
<span
hidden={currentWorkspace === LOCALHOST || currentWorkspace === NO_WORKSPACE}
id='workspaceDelete'
data-id='workspaceDelete'
onClick={() => {
deleteCurrentWorkspace()
_paq.push(['trackEvent', 'fileExplorer', 'workspaceMenu', 'workspaceDelete'])
hideIconsMenu(!showIconsMenu)
}}
className='far fa-trash pl-2'
>
</span>
<span className="pl-3"><FormattedMessage id='filePanel.delete' /></span>
</div>
</CustomTooltip>,
<CustomTooltip
placement='right-start'
tooltipClasses="text-nowrap"
tooltipId="workspaceRenametooltip"
tooltipText={<FormattedMessage id='filePanel.workspace.rename' />}
>
<div onClick={() => {
renameCurrentWorkspace()
_paq.push(['trackEvent', 'fileExplorer', 'workspaceMenu', 'workspaceRename'])
hideIconsMenu(!showIconsMenu)
}}
data-id='workspaceRename'
key={`workspacesRename-fe-ws`}
>
<span
hidden={currentWorkspace === LOCALHOST || currentWorkspace === NO_WORKSPACE}
id='workspaceRename'
data-id='workspaceRename'
onClick={() => {
renameCurrentWorkspace()
_paq.push(['trackEvent', 'fileExplorer', 'workspaceMenu', 'workspaceRename'])
hideIconsMenu(!showIconsMenu)
}}
className='far fa-edit pl-2'>
</span>
<span className="pl-3"><FormattedMessage id='filePanel.rename' /></span>
</div>
</CustomTooltip>,
<Dropdown.Divider className="border mb-0 mt-0" />,
<CustomTooltip
placement="right-start"
tooltipId="cloneWorkspaceTooltip"
tooltipClasses="text-nowrap"
tooltipText={<FormattedMessage id='filePanel.workspace.clone' />}
>
<div
data-id='cloneGitRepository'
onClick={() => {
cloneGitRepository()
_paq.push(['trackEvent', 'fileExplorer', 'workspaceMenu', 'cloneGitRepository'])
hideIconsMenu(!showIconsMenu)
}}
key={`cloneGitRepository-fe-ws`}
>
<span
hidden={currentWorkspace === LOCALHOST}
id='cloneGitRepository'
data-id='cloneGitRepository'
onClick={() => {
cloneGitRepository()
_paq.push(['trackEvent', 'fileExplorer', 'workspaceMenu', 'cloneGitRepository'])
hideIconsMenu(!showIconsMenu)
}}
className='fab fa-github pl-2'
>
</span>
<span className="pl-3"><FormattedMessage id='filePanel.clone' /></span>
</div>
</CustomTooltip>,
<Dropdown.Divider className="border mt-0 mb-0 remixui_menuhr" style={{ pointerEvents: 'none' }}/>,
<CustomTooltip
placement="right-start"
tooltipId="createWorkspaceTooltip"
tooltipClasses="text-nowrap"
tooltipText={<FormattedMessage id='filePanel.workspace.download' />}
>
<div
data-id='workspacesDownload'
onClick={() => {
downloadWorkspaces()
_paq.push(['trackEvent', 'fileExplorer', 'workspaceMenu', 'workspacesDownload'])
hideIconsMenu(!showIconsMenu)
}}
key={`workspacesDownload-fe-ws`}
>
<span
hidden={currentWorkspace === LOCALHOST || currentWorkspace === NO_WORKSPACE}
id='workspacesDownload'
data-id='workspacesDownload'
onClick={() => {
downloadWorkspaces()
_paq.push(['trackEvent', 'fileExplorer', 'workspaceMenu', 'workspacesDownload'])
hideIconsMenu(!showIconsMenu)
}}
className='far fa-download pl-2 '
>
</span>
<span className="pl-3"><FormattedMessage id='filePanel.download' /></span>
</div>
</CustomTooltip>,
<CustomTooltip
placement="right-start"
tooltipId="createWorkspaceTooltip"
tooltipClasses="text-nowrap"
tooltipText={<FormattedMessage id='filePanel.workspace.restore' />}
>
<div
data-id='workspacesRestore'
onClick={() => {
restoreBackup()
_paq.push(['trackEvent', 'fileExplorer', 'workspaceMenu', 'workspacesRestore'])
hideIconsMenu(!showIconsMenu)
}}
key={`workspacesRestore-fe-ws`}
>
<span
hidden={currentWorkspace === LOCALHOST}
id='workspacesRestore'
data-id='workspacesRestore'
onClick={() => {
restoreBackup()
_paq.push(['trackEvent', 'fileExplorer', 'workspaceMenu', 'workspacesRestore'])
hideIconsMenu(!showIconsMenu)
}}
className='far fa-upload pl-2'
>
</span>
<span className="pl-3"><FormattedMessage id='filePanel.restore' /></span>
</div>
</CustomTooltip>,
<Dropdown.Divider className="border mt-0 mb-0 remixui_menuhr" style={{ pointerEvents: 'none' }}/>,
<CustomTooltip
placement="right-start"
tooltipId="createSolGHActionTooltip"
tooltipClasses="text-nowrap"
tooltipText={<FormattedMessage id='filePanel.workspace.solghaction' />}
>
<div
data-id='soliditygithubaction'
onClick={(e) => {
e.stopPropagation()
addGithubAction()
_paq.push(['trackEvent', 'fileExplorer', 'workspaceMenu', 'addSolidityTesting'])
hideIconsMenu(!showIconsMenu)
}}
>
<span
hidden={currentWorkspace === LOCALHOST}
id='soliditygithubaction'
data-id='soliditygithubaction'
onClick={(e) => {
e.stopPropagation()
addGithubAction()
_paq.push(['trackEvent', 'fileExplorer', 'workspaceMenu', 'addSolidityTesting'])
hideIconsMenu(!showIconsMenu)
}}
className='fab fa-github pl-2'
>
</span>
<span className="pl-3">{<FormattedMessage id='filePanel.solghaction' />}</span>
</div>
</CustomTooltip>
]
return (
<div className='d-flex flex-column justify-content-between h-100'>
<div className='remixui_container overflow-auto' style={{ maxHeight: selectedWorkspace && selectedWorkspace.isGitRepo ? '95%' : '100%' }}>
<div className='d-flex flex-column w-100 mb-1 remixui_fileexplorer' data-id="remixUIWorkspaceExplorer" onClick={resetFocus}>
<div className='remixui_container overflow-auto' style={{ maxHeight: selectedWorkspace && selectedWorkspace.isGitRepo ? '95%' : '100%' }} onContextMenu={(e)=>{
e.preventDefault()
handleContextMenu(e.pageX, e.pageY, ROOT_PATH, "workspace", 'workspace')
}
}>
<div className='d-flex flex-column w-100 remixui_fileexplorer' data-id="remixUIWorkspaceExplorer" onClick={resetFocus}>
<div>
<header>
<div className="mx-2 mb-2 d-flex flex-column">
@ -537,19 +996,20 @@ export function Workspace () {
</div>
</header>
</div>
<div className='h-100 mb-4 pb-4 remixui_fileExplorerTree' onFocus={() => { toggleDropdown(false) }}>
<div className='h-100 remixui_fileExplorerTree' onFocus={() => { toggleDropdown(false) }}>
<div className='h-100'>
{ (global.fs.browser.isRequestingWorkspace || global.fs.browser.isRequestingCloning) && <div className="text-center py-5"><i className="fas fa-spinner fa-pulse fa-2x"></i></div>}
{ !(global.fs.browser.isRequestingWorkspace || global.fs.browser.isRequestingCloning) &&
(global.fs.mode === 'browser') && (currentWorkspace !== NO_WORKSPACE) &&
<div className='h-100 remixui_treeview' data-id='filePanelFileExplorerTree'>
<FileExplorer
fileState={global.fs.browser.fileState}
name={currentWorkspace}
menuItems={['createNewFile', 'createNewFolder', 'publishToGist', canUpload ? 'uploadFile' : '', canUpload ? 'uploadFolder' : '']}
contextMenuItems={global.fs.browser.contextMenu.registeredMenuItems}
removedContextMenuItems={global.fs.browser.contextMenu.removedMenuItems}
files={global.fs.browser.files}
fileState={global.fs.browser.fileState}
workspaceState={state}
expandPath={global.fs.browser.expandPath}
focusEdit={global.fs.focusEdit}
focusElement={global.fs.focusElement}
@ -578,12 +1038,26 @@ export function Workspace () {
dispatchHandleExpandPath={global.dispatchHandleExpandPath}
dispatchMoveFile={global.dispatchMoveFile}
dispatchMoveFolder={global.dispatchMoveFolder}
contextType={contextType}
closeContextMenu={handleCloseContextMenu}
dispatchCanCopy={handleCanCopy}
handleCopyClick={handleCopyClick}
handlePasteClick={handlePasteClick}
addMenuItems={addMenuItems}
removeMenuItems={removeMenuItems}
handleContextMenu={handleContextMenu}
uploadFile={uploadFile}
getFocusedFolder={getFocusedFolder}
toGist={toGist}
editModeOn={editModeOn}
handleNewFileInput={handleNewFileInput}
handleNewFolderInput={handleNewFolderInput}
/>
</div>
}
{ global.fs.localhost.isRequestingLocalhost && <div className="text-center py-5"><i className="fas fa-spinner fa-pulse fa-2x"></i></div> }
{ (global.fs.mode === 'localhost' && global.fs.localhost.isSuccessfulLocalhost) &&
<div className='h-100 filesystemexplorer pb-4 mb-4 remixui_treeview'>
<div className='h-100 filesystemexplorer remixui_treeview'>
<FileExplorer
name='localhost'
menuItems={['createNewFile', 'createNewFolder']}
@ -591,6 +1065,7 @@ export function Workspace () {
removedContextMenuItems={global.fs.localhost.contextMenu.removedMenuItems}
files={global.fs.localhost.files}
fileState={[]}
workspaceState={state}
expandPath={global.fs.localhost.expandPath}
focusEdit={global.fs.focusEdit}
focusElement={global.fs.focusElement}
@ -619,6 +1094,20 @@ export function Workspace () {
dispatchHandleExpandPath={global.dispatchHandleExpandPath}
dispatchMoveFile={global.dispatchMoveFile}
dispatchMoveFolder={global.dispatchMoveFolder}
contextType={contextType}
closeContextMenu={handleCloseContextMenu}
dispatchCanCopy={handleCanCopy}
handleCopyClick={handleCopyClick}
handlePasteClick={handlePasteClick}
addMenuItems={addMenuItems}
removeMenuItems={removeMenuItems}
handleContextMenu={handleContextMenu}
uploadFile={uploadFile}
getFocusedFolder={getFocusedFolder}
toGist={toGist}
editModeOn={editModeOn}
handleNewFileInput={handleNewFileInput}
handleNewFolderInput={handleNewFolderInput}
/>
</div>
}
@ -685,8 +1174,34 @@ export function Workspace () {
</div>
</div>
}
{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}
copyFileName={handleCopyFileNameClick}
copyPath={handleCopyFilePathClick}
emit={emitContextMenuEvent}
pageX={state.focusContext.x}
pageY={state.focusContext.y}
path={state.focusContext.element}
type={state.focusContext.type}
focus={global.fs.focusElement}
pushChangesToGist={pushChangesToGist}
publishFolderToGist={publishFolderToGist}
publishFileToGist={publishFileToGist}
uploadFile={uploadFile}
downloadPath={global.dispatchDownloadPath}
/>
}
</div>
)
}
export default Workspace
export default Workspace

@ -5,7 +5,7 @@ import { fileDecoration } from '@remix-ui/file-decorators'
import { RemixAppManager } from 'libs/remix-ui/plugin-manager/src/types'
import { ViewPlugin } from '@remixproject/engine-web'
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 action = { name: string, type?: Array<'folder' | 'gist' | 'file' | 'workspace'>, path?: string[], extension?: string[], pattern?: string[], id: string, multiselect: boolean, label: string, sticky?: boolean }
export interface JSONStandardInput {
language: "Solidity";
settings?: any,
@ -82,6 +82,7 @@ export interface FileExplorerProps {
contextMenuItems: MenuItems,
removedContextMenuItems: MenuItems,
files: { [x: string]: Record<string, FileType> },
workspaceState: WorkSpaceState,
fileState: fileDecoration[],
expandPath: string[],
focusEdit: string,
@ -111,7 +112,21 @@ export interface FileExplorerProps {
dispatchAddInputField:(path: string, type: 'file' | 'folder') => Promise<void>,
dispatchHandleExpandPath: (paths: string[]) => Promise<void>,
dispatchMoveFile: (src: string, dest: string) => Promise<void>,
dispatchMoveFolder: (src: string, dest: string) => Promise<void>
dispatchMoveFolder: (src: string, dest: string) => Promise<void>,
contextType?: 'file' | 'folder',
closeContextMenu?: () => void,
dispatchCanCopy?: (path: string, type: 'folder' | 'gist' | 'file') => void
handlePasteClick: (dest: string, destType: string) => void
handleCopyClick: (path: string, type: 'folder' | 'gist' | 'file' | 'workspace') => void
addMenuItems: (items: MenuItems) => void
removeMenuItems: (items: MenuItems) => void
handleContextMenu: (pageX: number, pageY: number, path: string, content: string, type: string) => void
uploadFile: (target) => void
getFocusedFolder: () => string
editModeOn: (path: string, type: string, isNew: boolean) => void
toGist: (path?: string, type?: string) => void
handleNewFileInput: (parentFolder?: string) => Promise<void>
handleNewFolderInput: (parentFolder?: string) => Promise<void>
}
type Placement = import('react-overlays/usePopper').Placement
export interface FileExplorerMenuProps {
@ -149,27 +164,23 @@ export interface FileExplorerContextMenuProps {
copyFileName?: (path: string, type: string) => void
copyPath?: (path: string, type: string) => void
generateUml?: (path: string) => Promise<void>
uploadFile?: (target: EventTarget & HTMLInputElement) => void
}
export interface FileExplorerState {
export interface WorkSpaceState {
ctrlKey: boolean
newFileName: string
actions: {
id: string
name: string
type?: Array<'folder' | 'gist' | 'file'>
type?: Array<'folder' | 'gist' | 'file' | 'workspace'>
path?: string[]
extension?: string[]
pattern?: string[]
multiselect: boolean
label: string
}[]
focusContext: {
element: string
x: number
y: number
type: string
}
focusContext: FileFocusContextType
focusEdit: {
element: string
type: string
@ -179,8 +190,17 @@ export interface FileExplorerState {
mouseOverElement: string
showContextMenu: boolean
reservedKeywords: string[]
copyElement: {
key: string
type: 'folder' | 'gist' | 'file'
}[]
copyElement: CopyElementType[]
}
export type FileFocusContextType = {
element: string
x: number
y: number
type: string
}
export type CopyElementType = {
key: string
type: 'folder' | 'gist' | 'file' | 'workspace'
}

@ -1,15 +1,15 @@
import { MenuItems } from '../types'
import { WorkspaceProps, MenuItems } from '../types'
export const contextMenuActions: MenuItems = [{
id: 'newFile',
name: 'New File',
type: ['folder', 'gist'],
type: ['folder', 'gist', 'workspace'],
multiselect: false,
label: ''
}, {
id: 'newFolder',
name: 'New Folder',
type: ['folder', 'gist'],
type: ['folder', 'gist', 'workspace'],
multiselect: false,
label: ''
}, {
@ -27,7 +27,7 @@ export const contextMenuActions: MenuItems = [{
},{
id: 'download',
name: 'Download',
type: ['file', 'folder'],
type: ['file', 'folder', 'workspace'],
multiselect: false,
label: ''
}, {
@ -78,4 +78,23 @@ export const contextMenuActions: MenuItems = [{
type: ['folder', 'file'],
multiselect: true,
label: ''
}]
},{
id: 'uploadFile',
name: 'Load a Local File',
type: ['folder', 'gist', 'workspace'],
multiselect: false,
label: 'Load a Local File'
}, {
id: 'publishToGist',
name: 'Push changes to gist',
type: ['folder', 'gist'],
multiselect: false,
label: 'Publish all to Gist'
},
{
id: 'publishWorkspace',
name: 'Publish Workspace to Gist',
type: ['workspace'],
multiselect: false,
label: ''
}]
Loading…
Cancel
Save