|
|
@ -1,4 +1,4 @@ |
|
|
|
import React, { useEffect, useState, useRef, SyntheticEvent } from 'react' // eslint-disable-line
|
|
|
|
import React, { useEffect, useState, useRef, SyntheticEvent, useContext } from 'react' // eslint-disable-line
|
|
|
|
import { useIntl } from 'react-intl' |
|
|
|
import { useIntl } from 'react-intl' |
|
|
|
import { TreeView } from '@remix-ui/tree-view' // eslint-disable-line
|
|
|
|
import { TreeView } from '@remix-ui/tree-view' // eslint-disable-line
|
|
|
|
import { FileExplorerMenu } from './file-explorer-menu' // eslint-disable-line
|
|
|
|
import { FileExplorerMenu } from './file-explorer-menu' // eslint-disable-line
|
|
|
@ -9,8 +9,9 @@ import '../css/file-explorer.css' |
|
|
|
import { checkSpecialChars, extractNameFromKey, extractParentFromKey, getPathIcon, joinPath } from '@remix-ui/helper' |
|
|
|
import { checkSpecialChars, extractNameFromKey, extractParentFromKey, getPathIcon, joinPath } from '@remix-ui/helper' |
|
|
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
|
|
import { ROOT_PATH } from '../utils/constants' |
|
|
|
import { ROOT_PATH } from '../utils/constants' |
|
|
|
import { moveFileIsAllowed, moveFolderIsAllowed } from '../actions' |
|
|
|
import { moveFileIsAllowed, moveFilesIsAllowed, moveFolderIsAllowed, moveFoldersIsAllowed } from '../actions' |
|
|
|
import { FlatTree } from './flat-tree' |
|
|
|
import { FlatTree } from './flat-tree' |
|
|
|
|
|
|
|
import { FileSystemContext } from '../contexts' |
|
|
|
|
|
|
|
|
|
|
|
export const FileExplorer = (props: FileExplorerProps) => { |
|
|
|
export const FileExplorer = (props: FileExplorerProps) => { |
|
|
|
const intl = useIntl() |
|
|
|
const intl = useIntl() |
|
|
@ -36,6 +37,11 @@ export const FileExplorer = (props: FileExplorerProps) => { |
|
|
|
// const [isPending, startTransition] = useTransition();
|
|
|
|
// const [isPending, startTransition] = useTransition();
|
|
|
|
const treeRef = useRef<HTMLDivElement>(null) |
|
|
|
const treeRef = useRef<HTMLDivElement>(null) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const { plugin } = useContext(FileSystemContext) |
|
|
|
|
|
|
|
const [feTarget, setFeTarget] = useState<{ key: string, type: 'file' | 'folder' }[]>({} as { key: string, type: 'file' | 'folder' }[]) |
|
|
|
|
|
|
|
const [filesSelected, setFilesSelected] = useState<string[]>([]) |
|
|
|
|
|
|
|
const feWindow = (window as any) |
|
|
|
|
|
|
|
|
|
|
|
useEffect(() => { |
|
|
|
useEffect(() => { |
|
|
|
if (contextMenuItems) { |
|
|
|
if (contextMenuItems) { |
|
|
|
addMenuItems(contextMenuItems) |
|
|
|
addMenuItems(contextMenuItems) |
|
|
@ -96,6 +102,100 @@ export const FileExplorer = (props: FileExplorerProps) => { |
|
|
|
} |
|
|
|
} |
|
|
|
}, [treeRef.current]) |
|
|
|
}, [treeRef.current]) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
useEffect(() => { |
|
|
|
|
|
|
|
const performDeletion = async () => { |
|
|
|
|
|
|
|
const path: string[] = [] |
|
|
|
|
|
|
|
if (feTarget?.length > 0 && feTarget[0]?.key.length > 0) { |
|
|
|
|
|
|
|
feTarget.forEach((one) => { |
|
|
|
|
|
|
|
path.push(one.key) |
|
|
|
|
|
|
|
}) |
|
|
|
|
|
|
|
await deletePath(path) |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (treeRef.current) { |
|
|
|
|
|
|
|
const deleteKeyPressHandler = async (eve: KeyboardEvent) => { |
|
|
|
|
|
|
|
if (eve.key === 'Delete' ) { |
|
|
|
|
|
|
|
feWindow._paq.push(['trackEvent', 'fileExplorer', 'deleteKey', 'deletePath']) |
|
|
|
|
|
|
|
setState((prevState) => { |
|
|
|
|
|
|
|
return { ...prevState, deleteKey: true } |
|
|
|
|
|
|
|
}) |
|
|
|
|
|
|
|
performDeletion() |
|
|
|
|
|
|
|
return |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
if (eve.metaKey) { |
|
|
|
|
|
|
|
if (eve.key === 'Backspace') { |
|
|
|
|
|
|
|
feWindow._paq.push(['trackEvent', 'fileExplorer', 'osxDeleteKey', 'deletePath']) |
|
|
|
|
|
|
|
setState((prevState) => { |
|
|
|
|
|
|
|
return { ...prevState, deleteKey: true } |
|
|
|
|
|
|
|
}) |
|
|
|
|
|
|
|
performDeletion() |
|
|
|
|
|
|
|
return |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
const deleteKeyPressUpHandler = async (eve: KeyboardEvent) => { |
|
|
|
|
|
|
|
if (eve.key === 'Delete' ) { |
|
|
|
|
|
|
|
setState((prevState) => { |
|
|
|
|
|
|
|
return { ...prevState, deleteKey: false } |
|
|
|
|
|
|
|
}) |
|
|
|
|
|
|
|
return |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
if (eve.metaKey) { |
|
|
|
|
|
|
|
if (eve.key === 'Backspace') { |
|
|
|
|
|
|
|
setState((prevState) => { |
|
|
|
|
|
|
|
return { ...prevState, deleteKey: false } |
|
|
|
|
|
|
|
}) |
|
|
|
|
|
|
|
return |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
treeRef.current?.addEventListener('keydown', deleteKeyPressHandler) |
|
|
|
|
|
|
|
treeRef.current?.addEventListener('keyup', deleteKeyPressUpHandler) |
|
|
|
|
|
|
|
return () => { |
|
|
|
|
|
|
|
treeRef.current?.removeEventListener('keydown', deleteKeyPressHandler) |
|
|
|
|
|
|
|
treeRef.current?.removeEventListener('keyup', deleteKeyPressUpHandler) |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
}, [treeRef.current, feTarget]) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
useEffect(() => { |
|
|
|
|
|
|
|
const performRename = async () => { |
|
|
|
|
|
|
|
if (feTarget?.length > 1 && feTarget[0]?.key.length > 1) { |
|
|
|
|
|
|
|
await plugin.call('notification', 'alert', { id: 'renameAlert', message: 'You cannot rename multiple files at once!' }) |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
props.editModeOn(feTarget[0].key, feTarget[0].type, false) |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
if (treeRef.current) { |
|
|
|
|
|
|
|
const F2KeyPressHandler = async (eve: KeyboardEvent) => { |
|
|
|
|
|
|
|
if (eve.key === 'F2' ) { |
|
|
|
|
|
|
|
feWindow._paq.push(['trackEvent', 'fileExplorer', 'f2ToRename', 'RenamePath']) |
|
|
|
|
|
|
|
await performRename() |
|
|
|
|
|
|
|
setState((prevState) => { |
|
|
|
|
|
|
|
return { ...prevState, F2Key: true } |
|
|
|
|
|
|
|
}) |
|
|
|
|
|
|
|
return |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
const F2KeyPressUpHandler = async (eve: KeyboardEvent) => { |
|
|
|
|
|
|
|
if (eve.key === 'F2' ) { |
|
|
|
|
|
|
|
setState((prevState) => { |
|
|
|
|
|
|
|
return { ...prevState, F2Key: false } |
|
|
|
|
|
|
|
}) |
|
|
|
|
|
|
|
return |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
treeRef.current?.addEventListener('keydown', F2KeyPressHandler) |
|
|
|
|
|
|
|
treeRef.current?.addEventListener('keyup', F2KeyPressUpHandler) |
|
|
|
|
|
|
|
return () => { |
|
|
|
|
|
|
|
treeRef.current?.removeEventListener('keydown', F2KeyPressHandler) |
|
|
|
|
|
|
|
treeRef.current?.removeEventListener('keyup', F2KeyPressUpHandler) |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
}, [treeRef.current, feTarget]) |
|
|
|
|
|
|
|
|
|
|
|
const hasReservedKeyword = (content: string): boolean => { |
|
|
|
const hasReservedKeyword = (content: string): boolean => { |
|
|
|
if (state.reservedKeywords.findIndex((value) => content.startsWith(value)) !== -1) return true |
|
|
|
if (state.reservedKeywords.findIndex((value) => content.startsWith(value)) !== -1) return true |
|
|
|
else return false |
|
|
|
else return false |
|
|
@ -292,17 +392,18 @@ export const FileExplorer = (props: FileExplorerProps) => { |
|
|
|
props.dispatchHandleExpandPath(expandPath) |
|
|
|
props.dispatchHandleExpandPath(expandPath) |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
const handleFileMove = async (dest: string, src: string) => { |
|
|
|
/** |
|
|
|
|
|
|
|
* This offers the ability to move a file to a new location |
|
|
|
|
|
|
|
* without showing a modal dialong to the user. |
|
|
|
|
|
|
|
* @param dest path of the destination |
|
|
|
|
|
|
|
* @param src path of the source |
|
|
|
|
|
|
|
* @returns {Promise<void>} |
|
|
|
|
|
|
|
*/ |
|
|
|
|
|
|
|
const moveFileSilently = async (dest: string, src: string) => { |
|
|
|
|
|
|
|
if (dest.length === 0 || src.length === 0) return |
|
|
|
if (await moveFileIsAllowed(src, dest) === false) return |
|
|
|
if (await moveFileIsAllowed(src, dest) === false) return |
|
|
|
try { |
|
|
|
try { |
|
|
|
props.modal( |
|
|
|
props.dispatchMoveFile(src, dest) |
|
|
|
intl.formatMessage({ id: 'filePanel.moveFile' }), |
|
|
|
|
|
|
|
intl.formatMessage({ id: 'filePanel.moveFileMsg1' }, { src, dest }), |
|
|
|
|
|
|
|
intl.formatMessage({ id: 'filePanel.yes' }), |
|
|
|
|
|
|
|
() => props.dispatchMoveFile(src, dest), |
|
|
|
|
|
|
|
intl.formatMessage({ id: 'filePanel.cancel' }), |
|
|
|
|
|
|
|
() => { } |
|
|
|
|
|
|
|
) |
|
|
|
|
|
|
|
} catch (error) { |
|
|
|
} catch (error) { |
|
|
|
props.modal( |
|
|
|
props.modal( |
|
|
|
intl.formatMessage({ id: 'filePanel.movingFileFailed' }), |
|
|
|
intl.formatMessage({ id: 'filePanel.movingFileFailed' }), |
|
|
@ -313,17 +414,24 @@ export const FileExplorer = (props: FileExplorerProps) => { |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
const handleFolderMove = async (dest: string, src: string) => { |
|
|
|
const resetMultiselect = () => { |
|
|
|
|
|
|
|
setState((prevState) => { |
|
|
|
|
|
|
|
return { ...prevState, ctrlKey: false } |
|
|
|
|
|
|
|
}) |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
|
|
|
* This offers the ability to move a folder to a new location |
|
|
|
|
|
|
|
* without showing a modal dialong to the user. |
|
|
|
|
|
|
|
* @param dest path of the destination |
|
|
|
|
|
|
|
* @param src path of the source |
|
|
|
|
|
|
|
* @returns {Promise<void>} |
|
|
|
|
|
|
|
*/ |
|
|
|
|
|
|
|
const moveFolderSilently = async (dest: string, src: string) => { |
|
|
|
|
|
|
|
if (dest.length === 0 || src.length === 0) return |
|
|
|
if (await moveFolderIsAllowed(src, dest) === false) return |
|
|
|
if (await moveFolderIsAllowed(src, dest) === false) return |
|
|
|
try { |
|
|
|
try { |
|
|
|
props.modal( |
|
|
|
props.dispatchMoveFolder(src, dest) |
|
|
|
intl.formatMessage({ id: 'filePanel.moveFile' }), |
|
|
|
|
|
|
|
intl.formatMessage({ id: 'filePanel.moveFileMsg1' }, { src, dest }), |
|
|
|
|
|
|
|
intl.formatMessage({ id: 'filePanel.yes' }), |
|
|
|
|
|
|
|
() => props.dispatchMoveFolder(src, dest), |
|
|
|
|
|
|
|
intl.formatMessage({ id: 'filePanel.cancel' }), |
|
|
|
|
|
|
|
() => { } |
|
|
|
|
|
|
|
) |
|
|
|
|
|
|
|
} catch (error) { |
|
|
|
} catch (error) { |
|
|
|
props.modal( |
|
|
|
props.modal( |
|
|
|
intl.formatMessage({ id: 'filePanel.movingFolderFailed' }), |
|
|
|
intl.formatMessage({ id: 'filePanel.movingFolderFailed' }), |
|
|
@ -334,6 +442,19 @@ export const FileExplorer = (props: FileExplorerProps) => { |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const warnMovingItems = async (src: string[], dest: string): Promise<void> => { |
|
|
|
|
|
|
|
return new Promise((resolve, reject) => { |
|
|
|
|
|
|
|
props.modal( |
|
|
|
|
|
|
|
intl.formatMessage({ id: 'filePanel.moveFile' }), |
|
|
|
|
|
|
|
intl.formatMessage({ id: 'filePanel.moveFileMsg1' }, { src: src.join(', '), dest }), |
|
|
|
|
|
|
|
intl.formatMessage({ id: 'filePanel.yes' }), |
|
|
|
|
|
|
|
() => resolve(null), |
|
|
|
|
|
|
|
intl.formatMessage({ id: 'filePanel.cancel' }), |
|
|
|
|
|
|
|
() => reject() |
|
|
|
|
|
|
|
) |
|
|
|
|
|
|
|
}) |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
const handleTreeClick = (event: SyntheticEvent) => { |
|
|
|
const handleTreeClick = (event: SyntheticEvent) => { |
|
|
|
let target = event.target as HTMLElement |
|
|
|
let target = event.target as HTMLElement |
|
|
|
while (target && target.getAttribute && !target.getAttribute('data-path')) { |
|
|
|
while (target && target.getAttribute && !target.getAttribute('data-path')) { |
|
|
@ -401,13 +522,18 @@ export const FileExplorer = (props: FileExplorerProps) => { |
|
|
|
fileState={fileState} |
|
|
|
fileState={fileState} |
|
|
|
expandPath={props.expandPath} |
|
|
|
expandPath={props.expandPath} |
|
|
|
handleContextMenu={handleContextMenu} |
|
|
|
handleContextMenu={handleContextMenu} |
|
|
|
moveFile={handleFileMove} |
|
|
|
warnMovingItems={warnMovingItems} |
|
|
|
moveFolder={handleFolderMove} |
|
|
|
moveFolderSilently={moveFolderSilently} |
|
|
|
|
|
|
|
moveFileSilently={moveFileSilently} |
|
|
|
|
|
|
|
resetMultiselect={resetMultiselect} |
|
|
|
|
|
|
|
setFilesSelected={setFilesSelected} |
|
|
|
handleClickFolder={handleClickFolder} |
|
|
|
handleClickFolder={handleClickFolder} |
|
|
|
createNewFile={props.createNewFile} |
|
|
|
createNewFile={props.createNewFile} |
|
|
|
createNewFolder={props.createNewFolder} |
|
|
|
createNewFolder={props.createNewFolder} |
|
|
|
deletePath={deletePath} |
|
|
|
deletePath={deletePath} |
|
|
|
editPath={props.editModeOn} |
|
|
|
editPath={props.editModeOn} |
|
|
|
|
|
|
|
fileTarget={feTarget} |
|
|
|
|
|
|
|
setTargetFiles={setFeTarget} |
|
|
|
/> |
|
|
|
/> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|