diff --git a/client/modules/IDE/components/Modal.jsx b/client/modules/IDE/components/Modal.jsx new file mode 100644 index 0000000000..5d1bbc88e0 --- /dev/null +++ b/client/modules/IDE/components/Modal.jsx @@ -0,0 +1,64 @@ +import classNames from 'classnames'; +import PropTypes from 'prop-types'; +import React, { useEffect, useRef } from 'react'; +import ExitIcon from '../../../images/exit.svg'; + +// Common logic from NewFolderModal, NewFileModal, UploadFileModal + +const Modal = ({ + title, + onClose, + closeAriaLabel, + contentClassName, + children +}) => { + const modalRef = useRef(null); + + const handleOutsideClick = (e) => { + // ignore clicks on the component itself + if (modalRef.current?.contains?.(e.target)) return; + + onClose(); + }; + + useEffect(() => { + modalRef.current?.focus(); + document.addEventListener('click', handleOutsideClick, false); + + return () => { + document.removeEventListener('click', handleOutsideClick, false); + }; + }, []); + + return ( +
+
+
+

{title}

+ +
+ {children} +
+
+ ); +}; + +Modal.propTypes = { + title: PropTypes.string.isRequired, + onClose: PropTypes.func.isRequired, + closeAriaLabel: PropTypes.string.isRequired, + contentClassName: PropTypes.string, + children: PropTypes.node.isRequired +}; + +Modal.defaultProps = { + contentClassName: '' +}; + +export default Modal; diff --git a/client/modules/IDE/components/NewFileModal.jsx b/client/modules/IDE/components/NewFileModal.jsx index d85cf44ea2..bf56249279 100644 --- a/client/modules/IDE/components/NewFileModal.jsx +++ b/client/modules/IDE/components/NewFileModal.jsx @@ -1,83 +1,22 @@ -import PropTypes from 'prop-types'; import React from 'react'; -import { connect } from 'react-redux'; -import { bindActionCreators } from 'redux'; -import { withTranslation } from 'react-i18next'; +import { useDispatch } from 'react-redux'; +import { useTranslation } from 'react-i18next'; +import Modal from './Modal'; import NewFileForm from './NewFileForm'; import { closeNewFileModal } from '../actions/ide'; -import ExitIcon from '../../../images/exit.svg'; -// At some point this will probably be generalized to a generic modal -// in which you can insert different content -// but for now, let's just make this work -class NewFileModal extends React.Component { - constructor(props) { - super(props); - this.focusOnModal = this.focusOnModal.bind(this); - this.handleOutsideClick = this.handleOutsideClick.bind(this); - } - - componentDidMount() { - this.focusOnModal(); - document.addEventListener('click', this.handleOutsideClick, false); - } - - componentWillUnmount() { - document.removeEventListener('click', this.handleOutsideClick, false); - } - - handleOutsideClick(e) { - // ignore clicks on the component itself - if (e.path.includes(this.modal)) return; - - this.props.closeNewFileModal(); - } - - focusOnModal() { - this.modal.focus(); - } - - render() { - return ( -
{ - this.modal = element; - }} - > -
-
-

- {this.props.t('NewFileModal.Title')} -

- -
- -
-
- ); - } -} - -NewFileModal.propTypes = { - closeNewFileModal: PropTypes.func.isRequired, - t: PropTypes.func.isRequired +const NewFileModal = () => { + const { t } = useTranslation(); + const dispatch = useDispatch(); + return ( + dispatch(closeNewFileModal())} + > + + + ); }; -function mapStateToProps() { - return {}; -} - -function mapDispatchToProps(dispatch) { - return bindActionCreators({ closeNewFileModal }, dispatch); -} - -export default withTranslation()( - connect(mapStateToProps, mapDispatchToProps)(NewFileModal) -); +export default NewFileModal; diff --git a/client/modules/IDE/components/NewFolderModal.jsx b/client/modules/IDE/components/NewFolderModal.jsx index fc5161ddc0..bd521ae970 100644 --- a/client/modules/IDE/components/NewFolderModal.jsx +++ b/client/modules/IDE/components/NewFolderModal.jsx @@ -1,62 +1,23 @@ -import PropTypes from 'prop-types'; import React from 'react'; -import { withTranslation } from 'react-i18next'; +import { useTranslation } from 'react-i18next'; +import { useDispatch } from 'react-redux'; +import { closeNewFolderModal } from '../actions/ide'; +import Modal from './Modal'; import NewFolderForm from './NewFolderForm'; -import ExitIcon from '../../../images/exit.svg'; -class NewFolderModal extends React.Component { - constructor(props) { - super(props); - this.handleOutsideClick = this.handleOutsideClick.bind(this); - } - - componentDidMount() { - this.newFolderModal.focus(); - document.addEventListener('click', this.handleOutsideClick, false); - } - - componentWillUnmount() { - document.removeEventListener('click', this.handleOutsideClick, false); - } - - handleOutsideClick(e) { - // ignore clicks on the component itself - if (e.path.includes(this.newFolderModal)) return; - - this.props.closeModal(); - } - - render() { - return ( -
{ - this.newFolderModal = element; - }} - > -
-
-

- {this.props.t('NewFolderModal.Title')} -

- -
- -
-
- ); - } -} - -NewFolderModal.propTypes = { - closeModal: PropTypes.func.isRequired, - t: PropTypes.func.isRequired +const NewFolderModal = () => { + const { t } = useTranslation(); + const dispatch = useDispatch(); + return ( + dispatch(closeNewFolderModal())} + contentClassName="modal-content-folder" + > + + + ); }; -export default withTranslation()(NewFolderModal); +export default NewFolderModal; diff --git a/client/modules/IDE/components/UploadFileModal.jsx b/client/modules/IDE/components/UploadFileModal.jsx index f2a9c09d1d..20aefc960b 100644 --- a/client/modules/IDE/components/UploadFileModal.jsx +++ b/client/modules/IDE/components/UploadFileModal.jsx @@ -1,79 +1,45 @@ import React from 'react'; -import PropTypes from 'prop-types'; -import { connect } from 'react-redux'; +import { useDispatch, useSelector } from 'react-redux'; import { Link } from 'react-router'; -import { withTranslation } from 'react-i18next'; +import { useTranslation } from 'react-i18next'; import prettyBytes from 'pretty-bytes'; import getConfig from '../../../utils/getConfig'; +import { closeUploadFileModal } from '../actions/ide'; import FileUploader from './FileUploader'; import { getreachedTotalSizeLimit } from '../selectors/users'; -import ExitIcon from '../../../images/exit.svg'; +import Modal from './Modal'; const limit = getConfig('UPLOAD_LIMIT') || 250000000; const limitText = prettyBytes(limit); -class UploadFileModal extends React.Component { - static propTypes = { - reachedTotalSizeLimit: PropTypes.bool.isRequired, - closeModal: PropTypes.func.isRequired, - t: PropTypes.func.isRequired - }; - - componentDidMount() { - this.focusOnModal(); - } - - focusOnModal = () => { - this.modal.focus(); - }; - - render() { - return ( -
{ - this.modal = element; - }} - > -
-
-

- {this.props.t('UploadFileModal.Title')} -

- -
- {this.props.reachedTotalSizeLimit && ( -

- {this.props.t('UploadFileModal.SizeLimitError', { - sizeLimit: limitText - })} - - assets - - . -

- )} - {!this.props.reachedTotalSizeLimit && ( -
- -
- )} +const UploadFileModal = () => { + const { t } = useTranslation(); + const dispatch = useDispatch(); + const reachedTotalSizeLimit = useSelector(getreachedTotalSizeLimit); + const onClose = () => dispatch(closeUploadFileModal()); + return ( + + {reachedTotalSizeLimit ? ( +

+ {t('UploadFileModal.SizeLimitError', { + sizeLimit: limitText + })} + + assets + + . +

+ ) : ( +
+
-
- ); - } -} - -function mapStateToProps(state) { - return { - reachedTotalSizeLimit: getreachedTotalSizeLimit(state) - }; -} + )} + + ); +}; -export default withTranslation()(connect(mapStateToProps)(UploadFileModal)); +export default UploadFileModal; diff --git a/client/modules/IDE/pages/IDEView.jsx b/client/modules/IDE/pages/IDEView.jsx index 5b71444fab..3c323fe88c 100644 --- a/client/modules/IDE/pages/IDEView.jsx +++ b/client/modules/IDE/pages/IDEView.jsx @@ -378,12 +378,8 @@ class IDEView extends React.Component { {this.props.ide.modalIsVisible && } - {this.props.ide.newFolderModalVisible && ( - - )} - {this.props.ide.uploadFileModalVisible && ( - - )} + {this.props.ide.newFolderModalVisible && } + {this.props.ide.uploadFileModalVisible && } {this.props.location.pathname === '/about' && (