Skip to content

Combined Modal component for New File, New Folder, and Upload. #2049

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Apr 28, 2023
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 64 additions & 0 deletions client/modules/IDE/components/Modal.jsx
Original file line number Diff line number Diff line change
@@ -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 (e.path.includes(modalRef.current)) return;

onClose();
};

useEffect(() => {
modalRef.current.focus();
document.addEventListener('click', handleOutsideClick, false);

return () => {
document.removeEventListener('click', handleOutsideClick, false);
};
}, []);

return (
<section className="modal" ref={modalRef}>
<div className={classNames('modal-content', contentClassName)}>
<div className="modal__header">
<h2 className="modal__title">{title}</h2>
<button
className="modal__exit-button"
onClick={onClose}
aria-label={closeAriaLabel}
>
<ExitIcon focusable="false" aria-hidden="true" />
</button>
</div>
{children}
</div>
</section>
);
};

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;
93 changes: 16 additions & 77 deletions client/modules/IDE/components/NewFileModal.jsx
Original file line number Diff line number Diff line change
@@ -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 (
<section
className="modal"
ref={(element) => {
this.modal = element;
}}
>
<div className="modal-content">
<div className="modal__header">
<h2 className="modal__title">
{this.props.t('NewFileModal.Title')}
</h2>
<button
className="modal__exit-button"
onClick={this.props.closeNewFileModal}
aria-label={this.props.t('NewFileModal.CloseButtonARIA')}
>
<ExitIcon focusable="false" aria-hidden="true" />
</button>
</div>
<NewFileForm focusOnModal={this.focusOnModal} />
</div>
</section>
);
}
}

NewFileModal.propTypes = {
closeNewFileModal: PropTypes.func.isRequired,
t: PropTypes.func.isRequired
const NewFileModal = () => {
const { t } = useTranslation();
const dispatch = useDispatch();
return (
<Modal
title={t('NewFileModal.Title')}
closeAriaLabel={t('NewFileModal.CloseButtonARIA')}
onClose={() => dispatch(closeNewFileModal())}
>
<NewFileForm />
</Modal>
);
};

function mapStateToProps() {
return {};
}

function mapDispatchToProps(dispatch) {
return bindActionCreators({ closeNewFileModal }, dispatch);
}

export default withTranslation()(
connect(mapStateToProps, mapDispatchToProps)(NewFileModal)
);
export default NewFileModal;
75 changes: 18 additions & 57 deletions client/modules/IDE/components/NewFolderModal.jsx
Original file line number Diff line number Diff line change
@@ -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 (
<section
className="modal"
ref={(element) => {
this.newFolderModal = element;
}}
>
<div className="modal-content-folder">
<div className="modal__header">
<h2 className="modal__title">
{this.props.t('NewFolderModal.Title')}
</h2>
<button
className="modal__exit-button"
onClick={this.props.closeModal}
aria-label={this.props.t('NewFolderModal.CloseButtonARIA')}
>
<ExitIcon focusable="false" aria-hidden="true" />
</button>
</div>
<NewFolderForm />
</div>
</section>
);
}
}

NewFolderModal.propTypes = {
closeModal: PropTypes.func.isRequired,
t: PropTypes.func.isRequired
const NewFolderModal = () => {
const { t } = useTranslation();
const dispatch = useDispatch();
return (
<Modal
title={t('NewFolderModal.Title')}
closeAriaLabel={t('NewFolderModal.CloseButtonARIA')}
onClose={() => dispatch(closeNewFolderModal())}
contentClassName="modal-content-folder"
>
<NewFolderForm />
</Modal>
);
};

export default withTranslation()(NewFolderModal);
export default NewFolderModal;
100 changes: 33 additions & 67 deletions client/modules/IDE/components/UploadFileModal.jsx
Original file line number Diff line number Diff line change
@@ -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 (
<section
className="modal"
ref={(element) => {
this.modal = element;
}}
>
<div className="modal-content">
<div className="modal__header">
<h2 className="modal__title">
{this.props.t('UploadFileModal.Title')}
</h2>
<button
className="modal__exit-button"
onClick={this.props.closeModal}
aria-label={this.props.t('UploadFileModal.CloseButtonARIA')}
>
<ExitIcon focusable="false" aria-hidden="true" />
</button>
</div>
{this.props.reachedTotalSizeLimit && (
<p>
{this.props.t('UploadFileModal.SizeLimitError', {
sizeLimit: limitText
})}
<Link to="/assets" onClick={this.props.closeModal}>
assets
</Link>
.
</p>
)}
{!this.props.reachedTotalSizeLimit && (
<div>
<FileUploader />
</div>
)}
const UploadFileModal = () => {
const { t } = useTranslation();
const dispatch = useDispatch();
const reachedTotalSizeLimit = useSelector(getreachedTotalSizeLimit);
const onClose = () => dispatch(closeUploadFileModal());
return (
<Modal
title={t('UploadFileModal.Title')}
closeAriaLabel={t('UploadFileModal.CloseButtonARIA')}
onClose={onClose}
>
{reachedTotalSizeLimit ? (
<p>
{t('UploadFileModal.SizeLimitError', {
sizeLimit: limitText
})}
<Link to="/assets" onClick={onClose}>
assets
</Link>
.
</p>
) : (
<div>
<FileUploader />
</div>
</section>
);
}
}

function mapStateToProps(state) {
return {
reachedTotalSizeLimit: getreachedTotalSizeLimit(state)
};
}
)}
</Modal>
);
};

export default withTranslation()(connect(mapStateToProps)(UploadFileModal));
export default UploadFileModal;
8 changes: 2 additions & 6 deletions client/modules/IDE/pages/IDEView.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -378,12 +378,8 @@ class IDEView extends React.Component {
</SplitPane>
</main>
{this.props.ide.modalIsVisible && <NewFileModal />}
{this.props.ide.newFolderModalVisible && (
<NewFolderModal closeModal={this.props.closeNewFolderModal} />
)}
{this.props.ide.uploadFileModalVisible && (
<UploadFileModal closeModal={this.props.closeUploadFileModal} />
)}
{this.props.ide.newFolderModalVisible && <NewFolderModal />}
{this.props.ide.uploadFileModalVisible && <UploadFileModal />}
{this.props.location.pathname === '/about' && (
<Overlay
title={this.props.t('About.Title')}
Expand Down