cnap-cobre/synapse

View on GitHub
frontend/src/components/ContextMenu/ContextMenu.js

Summary

Maintainability
B
4 hrs
Test Coverage
File `ContextMenu.js` has 337 lines of code (exceeds 250 allowed). Consider refactoring.
// @flow
import { connect } from 'react-redux';
import EventListener from 'react-event-listener';
import React from 'react';
import { addModal } from '../../store/ui/modals/Modals';
import { startTransfer } from '../../store/transferFiles/TransferFiles';
import { fileActions, fileListActions } from '../../store/files/Files';
import './ContextMenu.scss';
import { getFocusedFilePaths } from '../../store/ui/reducer';
import DownloadLink from './DownloadLink';
import type { FileType } from '../../types/fileTypes';
import JupyterHubLink from './JupyterHubLink';
 
type Props = {
focusedFiles: Array<FileType>,
focusedFilePaths: Array<string>,
dispatch(any): typeof undefined,
}
 
type State = {
visible: boolean
}
 
class ContextMenu extends React.Component<Props, State> {
state = {
visible: false,
};
 
root: HTMLElement | null = null;
 
_eventPathContainsClass = (event, className) => event.path.map(
element => element.className,
).filter(cn => cn) // Remove undefined values or any other "falsey" values
.map(
// Check if the supplied className is found in the element's className property
cn => (typeof cn === 'string') && cn.indexOf(className) !== -1,
).includes(true);
 
_handleContextMenu = (event) => {
// console.log("context target", event.currentTarget);
// console.log("event", event);
// console.log("classDetection", this._eventPathContainsClass(event, "rightClickableFile"));
if (!this._eventPathContainsClass(event, 'rightClickableFile')) {
return;
}
 
event.preventDefault();
 
this.setState({ visible: true });
 
if (this.root === null) {
return;
}
 
const clickX = event.clientX;
const clickY = event.clientY;
const screenW = window.innerWidth;
const screenH = window.innerHeight;
const rootW = this.root.offsetWidth;
const rootH = this.root.offsetHeight;
 
const right = (screenW - clickX) > rootW;
const left = !right;
const top = (screenH - clickY) > rootH;
const bottom = !top;
 
if (right) {
this.root.style.left = `${clickX + 5}px`;
}
 
if (left) {
this.root.style.left = `${clickX - rootW - 5}px`;
}
 
if (top) {
this.root.style.top = `${clickY + 5}px`;
}
 
if (bottom) {
this.root.style.top = `${clickY - rootH - 5}px`;
}
};
 
_handleClick = (event) => {
const { visible } = this.state;
const wasOutside = !(event.target.contains === this.root);
 
if (wasOutside && visible) this.setState({ visible: false });
 
return true;
};
 
_handleScroll = () => {
const { visible } = this.state;
 
if (visible) this.setState({ visible: false });
};
 
delayedRefresh = (path) => {
const { dispatch } = this.props;
// We delay a bit here so that Dropbox has a chance to be consistent.
// See "Brewers Cap Theorem" - Consistency, Availability, Partition tolerance
setTimeout(() => {
dispatch(fileListActions.pending(path));
}, 200);
};
 
handleSingleShareFile = () => {
console.log('share');
};
 
handleMultiShareFile = () => {
console.log('share');
};
 
handleTransferFiles = () => {
const { focusedFiles, dispatch } = this.props;
dispatch(addModal({
modalType: 'transferModal',
files: focusedFiles,
action: (targetPath) => {
const transferOrders = focusedFiles.map(
// Files get no trailing slash (obviously)
// Directories must get a trailing slash
file => ({
fromPath: file.fullPath + (file.type === 'dir' ? '/' : ''),
toPath: targetPath + file.name + (file.type === 'dir' ? '/' : ''),
}),
);
dispatch(
startTransfer(transferOrders),
);
},
}));
};
 
handleRenameFile = () => {
const { dispatch, focusedFiles } = this.props;
const file = focusedFiles[0];
dispatch(addModal({
modalType: 'renameFileModal',
fileName: file.name,
action: (newName) => {
dispatch(
fileActions.renameFile(file, newName),
);
this.delayedRefresh(`${file.fullPath.split('/').slice(0, -1).join('/')}/`);
},
}));
};
 
handleMoveFile = () => {
const { dispatch, focusedFiles } = this.props;
const currentDirectoryPath = `${focusedFiles[0]
.fullPath.split('/').slice(0, -1).join('/')}/`;
 
dispatch(addModal({
modalType: 'moveCopyModal',
title: `Move File${focusedFiles.length > 1 ? 's' : ''}`,
files: focusedFiles,
promptVerb: 'move',
submitText: 'Move Here',
path: currentDirectoryPath,
systemName: focusedFiles[0].system,
action: (newPath) => {
const newDirectoryPath = [
...currentDirectoryPath.split('/').slice(0, 3),
...newPath.split('/').slice(1, -1),
'',
].join('/');
 
// Copy each file
Promise.all(
focusedFiles.map(
(file) => {
console.log(file, newPath);
return fileActions.moveFile(file, `${newPath}/${file.name}`);
},
).map(
moveAction => dispatch(moveAction),
),
)
 
// ... then refresh each affected directory
.then(() => {
this.delayedRefresh(currentDirectoryPath);
this.delayedRefresh(newDirectoryPath);
});
},
}));
};
 
handleCopyFiles = () => {
const { dispatch, focusedFiles } = this.props;
const currentDirectoryPath = `${focusedFiles[0]
.fullPath.split('/').slice(0, -1).join('/')}/`;
 
dispatch(addModal({
modalType: 'moveCopyFileModal',
title: `Copy File${focusedFiles.length > 1 ? 's' : ''}`,
files: focusedFiles,
promptVerb: 'copy',
submitText: 'Copy Here',
path: currentDirectoryPath,
systemName: focusedFiles[0].system,
action: (newPath) => {
const newDirectoryPath = [
...currentDirectoryPath.split('/').slice(0, 3),
...newPath.split('/').slice(1, -1),
'',
].join('/');
 
// Copy each file
Promise.all(
focusedFiles.map(
file => fileActions.copyFile(file, newPath + file.name),
).map(
copyAction => dispatch(copyAction),
),
)
 
// ... then refresh each affected directory
.then(() => {
this.delayedRefresh(newDirectoryPath);
});
},
}));
};
 
handleDeleteFiles = () => {
const { dispatch, focusedFiles } = this.props;
dispatch(addModal({
modalType: 'deleteModal',
files: focusedFiles,
action: () => {
const uniqueDirectories = focusedFiles.map(
// Map from files to their directory paths
file => `${file.fullPath.split('/').slice(0, -1).join('/')}/`,
).filter(
// Get unique
(x, i, a) => a.indexOf(x) === i,
);
 
// Delete each file
Promise.all(
focusedFiles.map(
file => fileActions.deleteFile(file),
).map(
deleteAction => dispatch(deleteAction),
),
)
 
// ..then refresh each of their containing directories
.then(() => {
console.log(uniqueDirectories);
uniqueDirectories.forEach(
directoryPath => this.delayedRefresh(directoryPath),
);
});
},
}));
};
 
singleFileContextMenu = () => {
const { focusedFiles } = this.props;
return (
<React.Fragment>
<JupyterHubLink
file={focusedFiles[0]}
/>
<div
className="contextMenu--option contextMenu--option__disabled"
onClick={this.handleSingleShareFile}
>
Share (coming soon)
</div>
<div
className="contextMenu--option"
onClick={this.handleTransferFiles}
>
Transfer
</div>
<DownloadLink
file={focusedFiles[0]}
disabled={focusedFiles[0].type === 'dir'}
>
Download
</DownloadLink>
<div
className="contextMenu--option"
onClick={this.handleRenameFile}
>
Rename
</div>
<div
className="contextMenu--option"
onClick={this.handleMoveFile}
>
Move
</div>
<div
className="contextMenu--option"
onClick={this.handleCopyFiles}
>
Copy
</div>
<div
className="contextMenu--option"
onClick={this.handleDeleteFiles}
>
Delete
</div>
</React.Fragment>
);
}
 
multipleFileContextMenu = () => {
const { focusedFiles } = this.props;
return (
<React.Fragment>
<div
className="contextMenu--option contextMenu--option__diabled"
onClick={this.handleMultiShareFile}
>
Share (coming soon)
</div>
<div
className="contextMenu--option"
onClick={this.handleTransferFiles}
>
Transfer
</div>
<DownloadLink
file={focusedFiles[0]}
disabled
>
Download
</DownloadLink>
<div
className="contextMenu--option"
onClick={this.handleMoveFile}
>
Move
</div>
<div
className="contextMenu--option"
onClick={this.handleCopyFiles}
>
Copy
</div>
<div
className="contextMenu--option"
onClick={this.handleDeleteFiles}
>
Delete
</div>
</React.Fragment>
);
}
 
render() {
const { visible } = this.state;
const { focusedFilePaths } = this.props;
 
return (
<div>
<EventListener
target="window"
onScroll={this._handleScroll}
onClick={this._handleClick}
onContextMenu={this._handleContextMenu}
>
{(visible || null) && (
<div
ref={(ref) => { this.root = ref; }}
className="contextMenu"
>
{
focusedFilePaths.length === 1
? this.singleFileContextMenu()
: this.multipleFileContextMenu()
}
</div>
)}
</EventListener>
</div>
);
}
}
 
const mapStateToProps = store => ({
focusedFilePaths: getFocusedFilePaths(store),
focusedFiles: getFocusedFilePaths(store).map(f => store.filesFlat[f]),
});
 
 
export default connect(mapStateToProps)(ContextMenu);