multi select plus multi delete

terminal2
filip mertens 4 years ago
parent 9fa49102b6
commit 126bbcbbfa
  1. 48
      libs/remix-ui/file-explorer/src/lib/file-explorer-context-menu.tsx
  2. 101
      libs/remix-ui/file-explorer/src/lib/file-explorer.tsx
  3. 9
      libs/remix-ui/file-explorer/src/lib/types/index.ts
  4. 2
      libs/remix-ui/tree-view/src/lib/tree-view-item/tree-view-item.tsx
  5. 1
      package.json

@ -1,12 +1,11 @@
import React, { useRef, useEffect } from 'react' // eslint-disable-line
import { FileExplorerContextMenuProps } from './types'
import { action, FileExplorerContextMenuProps } from './types'
import './css/file-explorer-context-menu.css'
export const FileExplorerContextMenu = (props: FileExplorerContextMenuProps) => {
const { actions, createNewFile, createNewFolder, deletePath, renamePath, hideContextMenu, publishToGist, runScript, emit, pageX, pageY, path, type, ...otherProps } = props
const { actions, createNewFile, createNewFolder, deletePath, renamePath, hideContextMenu, publishToGist, runScript, emit, pageX, pageY, path, type, focus, ...otherProps } = props
const contextMenuRef = useRef(null)
useEffect(() => {
contextMenuRef.current.focus()
}, [])
@ -22,13 +21,42 @@ export const FileExplorerContextMenu = (props: FileExplorerContextMenuProps) =>
}
}, [pageX, pageY])
const filterItem = (item: action) => {
/**
* if there are multiple elements focused we need to take this and all conditions must be met plus the action must be set to 'multi'
* for example : 'downloadAsZip' with type ['file','folder','multi'] 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 (item.type.includes('multi'))
} 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 = () => {
const nonRootFocus = focus.filter((el) => { return !(el.key === '' && el.type === 'folder') })
if (nonRootFocus.length > 1) {
return nonRootFocus.map((element) => { return element.key })
} else {
return path
}
}
const menu = () => {
return actions.filter(item => {
if (item.type && Array.isArray(item.type) && (item.type.findIndex(name => name === type) !== -1)) return true
else if (item.path && Array.isArray(item.path) && (item.path.findIndex(key => key === path) !== -1)) return true
else if (item.extension && Array.isArray(item.extension) && (item.extension.findIndex(ext => path.endsWith(ext)) !== -1)) return true
else if (item.pattern && Array.isArray(item.pattern) && (item.pattern.filter(value => path.match(new RegExp(value))).length > 0)) return true
else return false
return filterItem(item)
}).map((item, index) => {
return <li
id={`menuitem${item.name.toLowerCase()}`}
@ -47,7 +75,7 @@ export const FileExplorerContextMenu = (props: FileExplorerContextMenuProps) =>
renamePath(path, type)
break
case 'Delete':
deletePath(path)
deletePath(getPath())
break
case 'Push changes to gist':
publishToGist()
@ -56,7 +84,7 @@ export const FileExplorerContextMenu = (props: FileExplorerContextMenuProps) =>
runScript(path)
break
default:
emit && emit(item.id, path)
emit && emit(item.id, getPath())
break
}
hideContextMenu()

@ -19,10 +19,7 @@ const queryParams = new QueryParams()
export const FileExplorer = (props: FileExplorerProps) => {
const { name, registry, plugin, focusRoot, contextMenuItems, displayInput, externalUploads } = props
const [state, setState] = useState({
focusElement: [{
key: '',
type: 'folder'
}],
focusElement: [{ key: '', type: 'folder' }],
focusPath: null,
files: [],
fileManager: null,
@ -52,7 +49,7 @@ export const FileExplorer = (props: FileExplorerProps) => {
}, {
id: 'delete',
name: 'Delete',
type: ['file', 'folder'],
type: ['file', 'folder', 'multi'],
path: [],
extension: [],
pattern: []
@ -70,6 +67,13 @@ export const FileExplorer = (props: FileExplorerProps) => {
path: [],
extension: ['.js'],
pattern: []
}, {
id: 'test',
name: 'test',
type: ['file', 'folder', 'multi'],
path: [],
extension: [],
pattern: []
}],
focusContext: {
element: null,
@ -88,6 +92,7 @@ export const FileExplorer = (props: FileExplorerProps) => {
hide: true,
title: '',
message: '',
children: <></>,
ok: {
label: '',
fn: () => {}
@ -203,7 +208,8 @@ export const FileExplorer = (props: FileExplorerProps) => {
message: prevState.modals[0].message,
ok: prevState.modals[0].ok,
cancel: prevState.modals[0].cancel,
handleHide: prevState.modals[0].handleHide
handleHide: prevState.modals[0].handleHide,
children: prevState.modals[0].children
}
prevState.modals.shift()
@ -216,6 +222,31 @@ export const FileExplorer = (props: FileExplorerProps) => {
}
}, [state.modals])
useEffect(() => {
const keyPressHandler = (e: KeyboardEvent) => {
if (e.key === 'Shift') {
setState(prevState => {
return { ...prevState, ctrlKey: true }
})
}
}
const keyUpHandler = (e: KeyboardEvent) => {
if (e.key === 'Shift') {
setState(prevState => {
return { ...prevState, ctrlKey: false }
})
}
}
document.addEventListener('keydown', keyPressHandler)
document.addEventListener('keyup', keyUpHandler)
return () => {
document.removeEventListener('keydown', keyPressHandler)
document.removeEventListener('keyup', keyUpHandler)
}
}, [])
const extractNameFromKey = (key: string):string => {
const keyPath = key.split('/')
@ -280,29 +311,32 @@ export const FileExplorer = (props: FileExplorerProps) => {
}
}
const deletePath = async (path: string) => {
const deletePath = async (path: string | string[]) => {
const filesProvider = fileSystem.provider.provider
if (filesProvider.isReadOnly(path)) {
return toast('cannot delete file. ' + name + ' is a read only explorer')
if (!Array.isArray(path)) path = [path]
const children: React.ReactFragment = <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>
for (const p of path) {
if (filesProvider.isReadOnly(p)) {
return toast('cannot delete file. ' + name + ' is a read only explorer')
}
}
const isDir = state.fileManager.isDirectory(path)
modal(`Delete ${isDir ? 'folder' : 'file'}`, `Are you sure you want to delete ${path} ${isDir ? 'folder' : 'file'}?`, {
modal(`Delete ${path.length > 1 ? 'items' : 'item'}`, '', {
label: 'OK',
fn: async () => {
try {
const fileManager = state.fileManager
await fileManager.remove(path)
} catch (e) {
toast(`Failed to remove ${isDir ? 'folder' : 'file'} ${path}.`)
const fileManager = state.fileManager
for (const p of path) {
try {
await fileManager.remove(p)
} catch (e) {
const isDir = state.fileManager.isDirectory(p)
toast(`Failed to remove ${isDir ? 'folder' : 'file'} ${p}.`)
}
}
}
}, {
label: 'Cancel',
fn: () => {}
})
}, children)
}
const renamePath = async (oldPath: string, newPath: string) => {
@ -526,7 +560,7 @@ export const FileExplorer = (props: FileExplorerProps) => {
})
}
const emitContextMenuEvent = (id: string, path: string) => {
const emitContextMenuEvent = (id: string, path: string | string[]) => {
plugin.emit(id, path)
}
@ -536,13 +570,14 @@ export const FileExplorer = (props: FileExplorerProps) => {
})
}
const modal = (title: string, message: string, ok: { label: string, fn: () => void }, cancel: { label: string, fn: () => void }) => {
const modal = (title: string, message: string, ok: { label: string, fn: () => void }, cancel: { label: string, fn: () => void }, children?:React.ReactNode) => {
setState(prevState => {
return {
...prevState,
modals: [...prevState.modals,
{
message,
children,
title,
ok,
cancel,
@ -560,10 +595,22 @@ export const FileExplorer = (props: FileExplorerProps) => {
const handleClickFile = (path: string) => {
path = path.indexOf(props.name + '/') === 0 ? path.replace(props.name + '/', '') : path
state.fileManager.open(path)
setState(prevState => {
return { ...prevState, focusElement: [{ key: path, type: 'file' }] }
})
if (!state.ctrlKey) {
state.fileManager.open(path)
setState(prevState => {
return { ...prevState, focusElement: [{ key: path, type: 'file' }] }
})
} else {
if (state.focusElement.findIndex(item => item.key === path) !== -1) {
setState(prevState => {
return { ...prevState, focusElement: [...prevState.focusElement.filter(item => item.key !== path)] }
})
} else {
setState(prevState => {
return { ...prevState, focusElement: [...prevState.focusElement, { key: path, type: 'file' }] }
})
}
}
}
const handleClickFolder = async (path: string) => {
@ -864,6 +911,7 @@ export const FileExplorer = (props: FileExplorerProps) => {
id={ props.name }
title={ state.focusModal.title }
message={ state.focusModal.message }
children={ state.focusModal.children }
hide={ state.focusModal.hide }
ok={ state.focusModal.ok }
cancel={ state.focusModal.cancel }
@ -885,6 +933,7 @@ export const FileExplorer = (props: FileExplorerProps) => {
pageY={state.focusContext.y}
path={state.focusContext.element}
type={state.focusContext.type}
focus={state.focusElement}
onMouseOver={(e) => {
e.stopPropagation()
handleMouseOver(state.focusContext.element)

@ -27,20 +27,21 @@ export interface FileExplorerMenuProps {
publishToGist: () => void,
uploadFile: (target: EventTarget & HTMLInputElement) => void
}
export type action = { name: string, type: string[], path: string[], extension: string[], pattern: string[], id: string }
export interface FileExplorerContextMenuProps {
actions: { name: string, type: string[], path: string[], extension: string[], pattern: string[], id: string }[],
actions: action[],
createNewFile: (folder?: string) => void,
createNewFolder: (parentFolder?: string) => void,
deletePath: (path: string) => void,
deletePath: (path: string | string[]) => void,
renamePath: (path: string, type: string) => void,
hideContextMenu: () => void,
publishToGist?: () => void,
runScript?: (path: string) => void,
emit?: (id: string, path: string) => void,
emit?: (id: string, path: string | string[]) => void,
pageX: number,
pageY: number,
path: string,
type: string,
focus: {key:string, type:string}[]
onMouseOver?: (...args) => void
}

@ -14,7 +14,7 @@ export const TreeViewItem = (props: TreeViewItemProps) => {
return (
<li ref={innerRef} key={`treeViewLi${id}`} data-id={`treeViewLi${id}`} className='li_tv' {...otherProps}>
<div key={`treeViewDiv${id}`} data-id={`treeViewDiv${id}`} className={`d-flex flex-row align-items-center ${labelClass}`} onClick={() => !controlBehaviour && setIsExpanded(!isExpanded)}>
{ controlBehaviour ? null : children ? <div className={isExpanded ? `px-1 ${iconY} caret caret_tv` : `px-1 ${iconX} caret caret_tv`} style={{ visibility: children ? 'visible' : 'hidden' }}></div> : icon ? <div className={`pr-3 pl-1 ${icon} caret caret_tv`}></div> : null }
{ children ? <div className={isExpanded ? `px-1 ${iconY} caret caret_tv` : `px-1 ${iconX} caret caret_tv`} style={{ visibility: children ? 'visible' : 'hidden' }}></div> : icon ? <div className={`pr-3 pl-1 ${icon} caret caret_tv`}></div> : null }
<span className='w-100 pl-1'>
{ label }
</span>

@ -159,7 +159,6 @@
"isbinaryfile": "^3.0.2",
"jquery": "^3.3.1",
"jszip": "^3.6.0",
"lodash": "^4.17.21",
"latest-version": "^5.1.0",
"lodash": "^4.17.21",
"merge": "^1.2.0",

Loading…
Cancel
Save