From f48c589df0d47fe6b0bf6f059123191f69f6489f Mon Sep 17 00:00:00 2001 From: Linda Paiste Date: Sun, 26 Feb 2023 16:39:17 -0600 Subject: [PATCH 1/3] Upgrade from `react-router` "^3.2.6" to "^6.8.1" (incomplete). --- client/common/Button.jsx | 2 +- client/components/Nav.jsx | 4 +- client/components/PreviewNav.jsx | 2 +- .../components/createRedirectWithUsername.jsx | 42 ++- client/components/mobile/Tab.jsx | 2 +- client/index.integration.test.jsx | 7 +- client/index.jsx | 7 +- client/modules/App/components/Overlay.jsx | 9 +- client/modules/IDE/actions/collections.js | 4 +- client/modules/IDE/actions/project.js | 8 +- client/modules/IDE/components/About.jsx | 2 +- client/modules/IDE/components/AssetList.jsx | 2 +- .../CollectionList/CollectionListRow.jsx | 2 +- client/modules/IDE/components/ErrorModal.jsx | 2 +- .../components/QuickAddList/QuickAddList.jsx | 2 +- client/modules/IDE/components/SketchList.jsx | 2 +- client/modules/IDE/components/Toolbar.jsx | 2 +- .../IDE/components/UploadFileModal.jsx | 2 +- client/modules/IDE/pages/FullView.jsx | 16 +- client/modules/IDE/pages/IDEView.jsx | 3 +- client/modules/IDE/pages/Legal.jsx | 20 +- client/modules/IDE/pages/MobileIDEView.jsx | 9 +- client/modules/Mobile/MobileDashboardView.jsx | 19 +- client/modules/Mobile/MobilePreferences.jsx | 3 +- client/modules/User/actions.js | 10 +- client/modules/User/components/Collection.jsx | 2 +- .../modules/User/components/CookieConsent.jsx | 2 +- .../User/components/DashboardTabSwitcher.jsx | 2 +- client/modules/User/pages/AccountView.jsx | 5 +- client/modules/User/pages/DashboardView.jsx | 13 - .../User/pages/EmailVerificationView.jsx | 6 +- client/modules/User/pages/LoginView.jsx | 2 +- client/modules/User/pages/NewPasswordView.jsx | 13 +- .../modules/User/pages/ResetPasswordView.jsx | 2 +- client/modules/User/pages/SignupView.jsx | 2 +- client/router.js | 8 + client/routes.jsx | 247 +++++++++++------- client/utils/router-compatibilty.jsx | 39 +++ package-lock.json | 121 ++++----- package.json | 3 +- 40 files changed, 348 insertions(+), 302 deletions(-) create mode 100644 client/router.js create mode 100644 client/utils/router-compatibilty.jsx diff --git a/client/common/Button.jsx b/client/common/Button.jsx index f1f6cfe28c..0ee154514a 100644 --- a/client/common/Button.jsx +++ b/client/common/Button.jsx @@ -1,7 +1,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import styled from 'styled-components'; -import { Link } from 'react-router'; +import { Link } from 'react-router-dom'; import { remSize, prop } from '../theme'; diff --git a/client/components/Nav.jsx b/client/components/Nav.jsx index 071c326303..a3e9b5aba6 100644 --- a/client/components/Nav.jsx +++ b/client/components/Nav.jsx @@ -1,8 +1,7 @@ import PropTypes from 'prop-types'; import React from 'react'; import { connect } from 'react-redux'; -import { withRouter } from 'react-router'; -import { Link } from 'react-router'; +import { Link } from 'react-router-dom'; import classNames from 'classnames'; import { withTranslation } from 'react-i18next'; import { languageKeyToLabel } from '../i18n'; @@ -17,6 +16,7 @@ import { logoutUser } from '../modules/User/actions'; import getConfig from '../utils/getConfig'; import { metaKeyName, metaKey } from '../utils/metaKey'; +import { withRouter } from '../utils/router-compatibilty'; import { getIsUserOwner } from '../modules/IDE/selectors/users'; import CaretLeftIcon from '../images/left-arrow.svg'; diff --git a/client/components/PreviewNav.jsx b/client/components/PreviewNav.jsx index a45dea90c2..097e3e4d01 100644 --- a/client/components/PreviewNav.jsx +++ b/client/components/PreviewNav.jsx @@ -1,6 +1,6 @@ import PropTypes from 'prop-types'; import React from 'react'; -import { Link } from 'react-router'; +import { Link } from 'react-router-dom'; import { withTranslation } from 'react-i18next'; import LogoIcon from '../images/p5js-logo-small.svg'; diff --git a/client/components/createRedirectWithUsername.jsx b/client/components/createRedirectWithUsername.jsx index 760cd4fcd3..5e6e23bb5b 100644 --- a/client/components/createRedirectWithUsername.jsx +++ b/client/components/createRedirectWithUsername.jsx @@ -1,29 +1,23 @@ +import PropTypes from 'prop-types'; import React from 'react'; -import { connect } from 'react-redux'; -import { browserHistory } from 'react-router'; +import { useSelector } from 'react-redux'; +import { Navigate } from 'react-router-dom'; -const RedirectToUser = ({ username, url = '/:username/sketches' }) => { - React.useEffect(() => { - if (username == null) { - return; - } - - browserHistory.replace(url.replace(':username', username)); - }, [username]); - - return null; +/** + * Sets the current username to the `:username` template in the provided URL, + * eg. `/:username/sketches` => `/p5/sketches`. + */ +const RedirectToUser = ({ url = '/:username/sketches' }) => { + const username = useSelector((state) => + state.user ? state.user.username : null + ); + return username ? ( + + ) : null; }; -function mapStateToProps(state) { - return { - username: state.user ? state.user.username : null - }; -} - -const ConnectedRedirectToUser = connect(mapStateToProps)(RedirectToUser); - -const createRedirectWithUsername = (url) => (props) => ( - -); +RedirectToUser.propTypes = { + url: PropTypes.string.isRequired +}; -export default createRedirectWithUsername; +export default RedirectToUser; diff --git a/client/components/mobile/Tab.jsx b/client/components/mobile/Tab.jsx index 23741f82ec..bd064b3f36 100644 --- a/client/components/mobile/Tab.jsx +++ b/client/components/mobile/Tab.jsx @@ -1,5 +1,5 @@ import styled from 'styled-components'; -import { Link } from 'react-router'; +import { Link } from 'react-router-dom'; import { prop, remSize } from '../../theme'; export default styled(Link)` diff --git a/client/index.integration.test.jsx b/client/index.integration.test.jsx index c7a803089d..5c9a245706 100644 --- a/client/index.integration.test.jsx +++ b/client/index.integration.test.jsx @@ -1,16 +1,15 @@ import { setupServer } from 'msw/node'; import { rest } from 'msw'; import React from 'react'; -import { Router, browserHistory } from 'react-router'; +import { RouterProvider } from 'react-router-dom'; +import router from './router'; import { reduxRender, act, waitFor, screen, within } from './test-utils'; import configureStore from './store'; -import routes from './routes'; import * as Actions from './modules/User/actions'; import { userResponse } from './testData/testServerResponses'; // setup for the app -const history = browserHistory; const initialState = window.__INITIAL_STATE__; const store = configureStore(initialState); @@ -57,7 +56,7 @@ document.createRange = () => { describe('index.jsx integration', () => { // the subject under test const subject = () => - reduxRender(, { store }); + reduxRender(, { store }); // spy on this function and wait for it to be called before making assertions const spy = jest.spyOn(Actions, 'getUser'); diff --git a/client/index.jsx b/client/index.jsx index be714fc0d5..e938e2ed5a 100644 --- a/client/index.jsx +++ b/client/index.jsx @@ -2,10 +2,10 @@ import React, { Suspense } from 'react'; import { render } from 'react-dom'; import { hot } from 'react-hot-loader/root'; import { Provider } from 'react-redux'; -import { Router, browserHistory } from 'react-router'; +import { RouterProvider } from 'react-router-dom'; import configureStore from './store'; -import routes from './routes'; +import router from './router'; import ThemeProvider from './modules/App/components/ThemeProvider'; import Loader from './modules/App/components/loader'; import './i18n'; @@ -15,7 +15,6 @@ require('./styles/main.scss'); // Load the p5 png logo, so that webpack will use it require('./images/p5js-square-logo.png'); -const history = browserHistory; const initialState = window.__INITIAL_STATE__; const store = configureStore(initialState); @@ -23,7 +22,7 @@ const store = configureStore(initialState); const App = () => ( - + ); diff --git a/client/modules/App/components/Overlay.jsx b/client/modules/App/components/Overlay.jsx index 39f492fc6f..e1d4e22326 100644 --- a/client/modules/App/components/Overlay.jsx +++ b/client/modules/App/components/Overlay.jsx @@ -1,9 +1,9 @@ import PropTypes from 'prop-types'; import React from 'react'; -import { browserHistory } from 'react-router'; import { withTranslation } from 'react-i18next'; import ExitIcon from '../../../images/exit.svg'; +import { withRouter } from '../../../utils/router-compatibilty'; class Overlay extends React.Component { constructor(props) { @@ -55,7 +55,7 @@ class Overlay extends React.Component { return; if (!this.props.closeOverlay) { - browserHistory.push(this.props.previousPath); + this.props.navigate(this.props.previousPath); } else { this.props.closeOverlay(); } @@ -105,7 +105,8 @@ Overlay.propTypes = { ariaLabel: PropTypes.string, previousPath: PropTypes.string, isFixedHeight: PropTypes.bool, - t: PropTypes.func.isRequired + t: PropTypes.func.isRequired, + navigate: PropTypes.func.isRequired }; Overlay.defaultProps = { @@ -118,4 +119,4 @@ Overlay.defaultProps = { isFixedHeight: false }; -export default withTranslation()(Overlay); +export default withRouter(withTranslation()(Overlay)); diff --git a/client/modules/IDE/actions/collections.js b/client/modules/IDE/actions/collections.js index 69da0f23ba..b7599ed431 100644 --- a/client/modules/IDE/actions/collections.js +++ b/client/modules/IDE/actions/collections.js @@ -1,4 +1,4 @@ -import { browserHistory } from 'react-router'; +import { navigate } from '../../../router'; import apiClient from '../../../utils/apiClient'; import * as ActionTypes from '../../../constants'; import { startLoader, stopLoader } from './loader'; @@ -56,7 +56,7 @@ export function createCollection(collection) { const pathname = `/${newCollection.owner.username}/collections/${newCollection.id}`; const location = { pathname, state: { skipSavingPath: true } }; - browserHistory.push(location); + navigate(location); }) .catch((error) => { const { response } = error; diff --git a/client/modules/IDE/actions/project.js b/client/modules/IDE/actions/project.js index 9a528a34f6..67843c71c4 100644 --- a/client/modules/IDE/actions/project.js +++ b/client/modules/IDE/actions/project.js @@ -1,7 +1,7 @@ -import { browserHistory } from 'react-router'; import objectID from 'bson-objectid'; import each from 'async/each'; import isEqual from 'lodash/isEqual'; +import { navigate } from '../../../router'; import apiClient from '../../../utils/apiClient'; import getConfig from '../../../utils/getConfig'; import * as ActionTypes from '../../../constants'; @@ -214,7 +214,7 @@ export function saveProject( dispatch(setNewProject(synchedProject)); dispatch(setUnsavedChanges(false)); - browserHistory.push( + navigate( `/${response.data.user.username}/sketches/${response.data.id}` ); @@ -271,7 +271,7 @@ export function resetProject() { export function newProject() { setTimeout(() => { - browserHistory.push('/'); + navigate('/'); }, 0); return resetProject(); } @@ -334,7 +334,7 @@ export function cloneProject(project) { apiClient .post('/projects', formParams) .then((response) => { - browserHistory.push( + navigate( `/${response.data.user.username}/sketches/${response.data.id}` ); dispatch(setNewProject(response.data)); diff --git a/client/modules/IDE/components/About.jsx b/client/modules/IDE/components/About.jsx index 254dd5f8bb..2a33574102 100644 --- a/client/modules/IDE/components/About.jsx +++ b/client/modules/IDE/components/About.jsx @@ -1,7 +1,7 @@ import React from 'react'; import { Helmet } from 'react-helmet'; import { useTranslation } from 'react-i18next'; -import { Link } from 'react-router'; +import { Link } from 'react-router-dom'; import SquareLogoIcon from '../../../images/p5js-square-logo.svg'; // import PlayIcon from '../../../images/play.svg'; import AsteriskIcon from '../../../images/p5-asterisk.svg'; diff --git a/client/modules/IDE/components/AssetList.jsx b/client/modules/IDE/components/AssetList.jsx index a77a0d6d66..559f60c580 100644 --- a/client/modules/IDE/components/AssetList.jsx +++ b/client/modules/IDE/components/AssetList.jsx @@ -2,7 +2,7 @@ import PropTypes from 'prop-types'; import React from 'react'; import { connect } from 'react-redux'; import { bindActionCreators } from 'redux'; -import { Link } from 'react-router'; +import { Link } from 'react-router-dom'; import { Helmet } from 'react-helmet'; import prettyBytes from 'pretty-bytes'; import { withTranslation } from 'react-i18next'; diff --git a/client/modules/IDE/components/CollectionList/CollectionListRow.jsx b/client/modules/IDE/components/CollectionList/CollectionListRow.jsx index bb5282027c..ed109141d7 100644 --- a/client/modules/IDE/components/CollectionList/CollectionListRow.jsx +++ b/client/modules/IDE/components/CollectionList/CollectionListRow.jsx @@ -1,7 +1,7 @@ import PropTypes from 'prop-types'; import React from 'react'; import { connect } from 'react-redux'; -import { Link } from 'react-router'; +import { Link } from 'react-router-dom'; import { bindActionCreators } from 'redux'; import { withTranslation } from 'react-i18next'; import * as ProjectActions from '../../actions/project'; diff --git a/client/modules/IDE/components/ErrorModal.jsx b/client/modules/IDE/components/ErrorModal.jsx index c0646c8f36..d97aca10e9 100644 --- a/client/modules/IDE/components/ErrorModal.jsx +++ b/client/modules/IDE/components/ErrorModal.jsx @@ -1,6 +1,6 @@ import PropTypes from 'prop-types'; import React from 'react'; -import { Link } from 'react-router'; +import { Link } from 'react-router-dom'; import { withTranslation } from 'react-i18next'; class ErrorModal extends React.Component { diff --git a/client/modules/IDE/components/QuickAddList/QuickAddList.jsx b/client/modules/IDE/components/QuickAddList/QuickAddList.jsx index 2fcfc9223e..033ccc6ee6 100644 --- a/client/modules/IDE/components/QuickAddList/QuickAddList.jsx +++ b/client/modules/IDE/components/QuickAddList/QuickAddList.jsx @@ -1,6 +1,6 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { Link } from 'react-router'; +import { Link } from 'react-router-dom'; import { withTranslation } from 'react-i18next'; import Icons from './Icons'; diff --git a/client/modules/IDE/components/SketchList.jsx b/client/modules/IDE/components/SketchList.jsx index 757bacd904..e52fd40364 100644 --- a/client/modules/IDE/components/SketchList.jsx +++ b/client/modules/IDE/components/SketchList.jsx @@ -3,7 +3,7 @@ import React from 'react'; import { Helmet } from 'react-helmet'; import { withTranslation } from 'react-i18next'; import { connect } from 'react-redux'; -import { Link } from 'react-router'; +import { Link } from 'react-router-dom'; import { bindActionCreators } from 'redux'; import classNames from 'classnames'; import slugify from 'slugify'; diff --git a/client/modules/IDE/components/Toolbar.jsx b/client/modules/IDE/components/Toolbar.jsx index fc6d1d95b6..a69f26c1ec 100644 --- a/client/modules/IDE/components/Toolbar.jsx +++ b/client/modules/IDE/components/Toolbar.jsx @@ -1,7 +1,7 @@ import PropTypes from 'prop-types'; import React from 'react'; import { connect } from 'react-redux'; -import { Link } from 'react-router'; +import { Link } from 'react-router-dom'; import classNames from 'classnames'; import { withTranslation } from 'react-i18next'; import * as IDEActions from '../actions/ide'; diff --git a/client/modules/IDE/components/UploadFileModal.jsx b/client/modules/IDE/components/UploadFileModal.jsx index f2a9c09d1d..7b5d8c7325 100644 --- a/client/modules/IDE/components/UploadFileModal.jsx +++ b/client/modules/IDE/components/UploadFileModal.jsx @@ -1,7 +1,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import { connect } from 'react-redux'; -import { Link } from 'react-router'; +import { Link } from 'react-router-dom'; import { withTranslation } from 'react-i18next'; import prettyBytes from 'pretty-bytes'; import getConfig from '../../../utils/getConfig'; diff --git a/client/modules/IDE/pages/FullView.jsx b/client/modules/IDE/pages/FullView.jsx index e2ddd20b62..ce981edec6 100644 --- a/client/modules/IDE/pages/FullView.jsx +++ b/client/modules/IDE/pages/FullView.jsx @@ -1,7 +1,7 @@ -import PropTypes from 'prop-types'; import React, { useEffect, useState } from 'react'; import Helmet from 'react-helmet'; import { useDispatch, useSelector } from 'react-redux'; +import { useParams } from 'react-router-dom'; import PreviewFrame from '../components/PreviewFrame'; import PreviewNav from '../../../components/PreviewNav'; import { getProject } from '../actions/project'; @@ -14,13 +14,14 @@ import { import useInterval from '../hooks/useInterval'; import RootPage from '../../../components/RootPage'; -function FullView(props) { +function FullView() { + const params = useParams(); const dispatch = useDispatch(); const project = useSelector((state) => state.project); const [isRendered, setIsRendered] = useState(false); useEffect(() => { - dispatch(getProject(props.params.project_id, props.params.username)); + dispatch(getProject(params.project_id, params.username)); }, []); useEffect(() => { @@ -64,7 +65,7 @@ function FullView(props) { }} project={{ name: project.name, - id: props.params.project_id + id: params.project_id }} />
@@ -74,11 +75,4 @@ function FullView(props) { ); } -FullView.propTypes = { - params: PropTypes.shape({ - project_id: PropTypes.string, - username: PropTypes.string - }).isRequired -}; - export default FullView; diff --git a/client/modules/IDE/pages/IDEView.jsx b/client/modules/IDE/pages/IDEView.jsx index 04bbb2f9ad..9794bc4af2 100644 --- a/client/modules/IDE/pages/IDEView.jsx +++ b/client/modules/IDE/pages/IDEView.jsx @@ -2,7 +2,6 @@ import PropTypes from 'prop-types'; import React from 'react'; import { bindActionCreators } from 'redux'; import { connect } from 'react-redux'; -import { withRouter } from 'react-router'; import { withTranslation } from 'react-i18next'; import { Helmet } from 'react-helmet'; import SplitPane from 'react-split-pane'; @@ -618,5 +617,5 @@ function mapDispatchToProps(dispatch) { } export default withTranslation()( - withRouter(connect(mapStateToProps, mapDispatchToProps)(IDEView)) + connect(mapStateToProps, mapDispatchToProps)(IDEView) ); diff --git a/client/modules/IDE/pages/Legal.jsx b/client/modules/IDE/pages/Legal.jsx index b7849aae0e..b1de5d493a 100644 --- a/client/modules/IDE/pages/Legal.jsx +++ b/client/modules/IDE/pages/Legal.jsx @@ -1,7 +1,6 @@ import React, { useState, useEffect } from 'react'; -import PropTypes from 'prop-types'; import { Tab, Tabs, TabList, TabPanel } from 'react-tabs'; -import { browserHistory } from 'react-router'; +import { useLocation, useNavigate } from 'react-router-dom'; import styled from 'styled-components'; import { useTranslation } from 'react-i18next'; import PrivacyPolicy from './PrivacyPolicy'; @@ -29,7 +28,8 @@ const TabTitle = styled.p` } `; -function Legal({ location }) { +function Legal() { + const location = useLocation(); const [selectedIndex, setSelectedIndex] = useState(0); const { t } = useTranslation(); useEffect(() => { @@ -42,17 +42,19 @@ function Legal({ location }) { } }, [location]); + const navigate = useNavigate(); + function onSelect(index, lastIndex, event) { if (index === lastIndex) return; if (index === 0) { setSelectedIndex(0); - browserHistory.push('/privacy-policy'); + navigate('/privacy-policy'); } else if (index === 1) { setSelectedIndex(1); - browserHistory.push('/terms-of-use'); + navigate('/terms-of-use'); } else if (index === 2) { setSelectedIndex(2); - browserHistory.push('/code-of-conduct'); + navigate('/code-of-conduct'); } } @@ -85,10 +87,4 @@ function Legal({ location }) { ); } -Legal.propTypes = { - location: PropTypes.shape({ - pathname: PropTypes.string - }).isRequired -}; - export default Legal; diff --git a/client/modules/IDE/pages/MobileIDEView.jsx b/client/modules/IDE/pages/MobileIDEView.jsx index 201ad4e5c0..d8f4692df6 100644 --- a/client/modules/IDE/pages/MobileIDEView.jsx +++ b/client/modules/IDE/pages/MobileIDEView.jsx @@ -1,7 +1,7 @@ import React, { useEffect } from 'react'; import PropTypes from 'prop-types'; import { connect } from 'react-redux'; -import { withRouter } from 'react-router'; +import { useParams } from 'react-router-dom'; import { useState } from 'react'; import { useTranslation } from 'react-i18next'; import styled from 'styled-components'; @@ -265,7 +265,6 @@ const MobileIDEView = (props) => { project, selectedFile, user, - params, unsavedChanges, expandConsole, collapseConsole, @@ -282,6 +281,8 @@ const MobileIDEView = (props) => { isUserOwner } = props; + const params = useParams(); + const [cmController, setCmController] = useState(null); // eslint-disable-line const { username } = user; @@ -508,6 +509,4 @@ const mapDispatchToProps = (dispatch) => dispatch ); -export default withRouter( - connect(mapStateToProps, mapDispatchToProps)(MobileIDEView) -); +export default connect(mapStateToProps, mapDispatchToProps)(MobileIDEView); diff --git a/client/modules/Mobile/MobileDashboardView.jsx b/client/modules/Mobile/MobileDashboardView.jsx index f5a6beb770..b176ab57bc 100644 --- a/client/modules/Mobile/MobileDashboardView.jsx +++ b/client/modules/Mobile/MobileDashboardView.jsx @@ -1,8 +1,7 @@ import React from 'react'; -import PropTypes from 'prop-types'; import styled from 'styled-components'; import { useSelector } from 'react-redux'; -import { withRouter } from 'react-router'; +import { useLocation, useParams } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; import Screen from '../../components/mobile/MobileScreen'; @@ -178,8 +177,10 @@ const NavItem = styled.li` const renderPanel = (name, props) => ((Component) => Component && )(Panels[name]); -const MobileDashboard = ({ params, location }) => { +const MobileDashboard = () => { const user = useSelector((state) => state.user); + const location = useLocation(); + const params = useParams(); const { username: paramsUsername } = params; const { pathname } = location; const { t } = useTranslation(); @@ -253,14 +254,4 @@ const MobileDashboard = ({ params, location }) => { ); }; -MobileDashboard.propTypes = { - location: PropTypes.shape({ - pathname: PropTypes.string.isRequired - }).isRequired, - params: PropTypes.shape({ - username: PropTypes.string.isRequired - }) -}; -MobileDashboard.defaultProps = { params: {} }; - -export default withRouter(MobileDashboard); +export default MobileDashboard; diff --git a/client/modules/Mobile/MobilePreferences.jsx b/client/modules/Mobile/MobilePreferences.jsx index 307145f3a2..4f3e85935c 100644 --- a/client/modules/Mobile/MobilePreferences.jsx +++ b/client/modules/Mobile/MobilePreferences.jsx @@ -1,7 +1,6 @@ import React from 'react'; import { bindActionCreators } from 'redux'; import { useSelector, useDispatch } from 'react-redux'; -import { withRouter } from 'react-router'; import { useTranslation } from 'react-i18next'; import styled from 'styled-components'; @@ -154,4 +153,4 @@ const MobilePreferences = () => { ); }; -export default withRouter(MobilePreferences); +export default MobilePreferences; diff --git a/client/modules/User/actions.js b/client/modules/User/actions.js index 826ade8676..68ecd240e2 100644 --- a/client/modules/User/actions.js +++ b/client/modules/User/actions.js @@ -1,6 +1,6 @@ -import { browserHistory } from 'react-router'; import { FORM_ERROR } from 'final-form'; import * as ActionTypes from '../../constants'; +import { navigate } from '../../router'; import apiClient from '../../utils/apiClient'; import { showErrorModal, justOpenedProject } from '../IDE/actions/ide'; import { setLanguage } from '../IDE/actions/preferences'; @@ -57,7 +57,7 @@ export function validateAndLoginUser(formProps) { }) ); dispatch(justOpenedProject()); - browserHistory.push(previousPath); + navigate(previousPath); resolve(); }) .catch((error) => @@ -78,7 +78,7 @@ export function validateAndSignUpUser(formValues) { .then((response) => { dispatch(authenticateUser(response.data)); dispatch(justOpenedProject()); - browserHistory.push(previousPath); + navigate(previousPath); resolve(); }) .catch((error) => { @@ -138,7 +138,7 @@ export function resetProject(dispatch) { dispatch({ type: ActionTypes.CLEAR_CONSOLE }); - browserHistory.push('/'); + navigate('/'); } export function logoutUser() { @@ -250,7 +250,7 @@ export function updatePassword(formValues, token) { .post(`/reset-password/${token}`, formValues) .then((response) => { dispatch(authenticateUser(response.data)); - browserHistory.push('/'); + navigate('/'); resolve(); }) .catch((error) => { diff --git a/client/modules/User/components/Collection.jsx b/client/modules/User/components/Collection.jsx index 5c44bd1d14..1915d04671 100644 --- a/client/modules/User/components/Collection.jsx +++ b/client/modules/User/components/Collection.jsx @@ -2,7 +2,7 @@ import PropTypes from 'prop-types'; import React, { useState, useRef, useEffect } from 'react'; import { Helmet } from 'react-helmet'; import { connect } from 'react-redux'; -import { Link } from 'react-router'; +import { Link } from 'react-router-dom'; import { bindActionCreators } from 'redux'; import { withTranslation } from 'react-i18next'; import classNames from 'classnames'; diff --git a/client/modules/User/components/CookieConsent.jsx b/client/modules/User/components/CookieConsent.jsx index 7af4d482aa..4d21cba2e0 100644 --- a/client/modules/User/components/CookieConsent.jsx +++ b/client/modules/User/components/CookieConsent.jsx @@ -4,7 +4,7 @@ import Cookies from 'js-cookie'; import styled from 'styled-components'; import ReactGA from 'react-ga'; import { Transition } from 'react-transition-group'; -import { Link } from 'react-router'; +import { Link } from 'react-router-dom'; import { Trans, useTranslation } from 'react-i18next'; import { PropTypes } from 'prop-types'; import getConfig from '../../../utils/getConfig'; diff --git a/client/modules/User/components/DashboardTabSwitcher.jsx b/client/modules/User/components/DashboardTabSwitcher.jsx index 34c4380dba..8e30f3a72f 100644 --- a/client/modules/User/components/DashboardTabSwitcher.jsx +++ b/client/modules/User/components/DashboardTabSwitcher.jsx @@ -1,7 +1,7 @@ import PropTypes from 'prop-types'; import React from 'react'; import { withTranslation } from 'react-i18next'; -import { Link } from 'react-router'; +import { Link } from 'react-router-dom'; const TabKey = { assets: 'assets', diff --git a/client/modules/User/pages/AccountView.jsx b/client/modules/User/pages/AccountView.jsx index a388997af4..177596ad2e 100644 --- a/client/modules/User/pages/AccountView.jsx +++ b/client/modules/User/pages/AccountView.jsx @@ -5,7 +5,6 @@ import { bindActionCreators } from 'redux'; import { Tab, Tabs, TabList, TabPanel } from 'react-tabs'; import { Helmet } from 'react-helmet'; import { withTranslation } from 'react-i18next'; -import { withRouter, browserHistory } from 'react-router'; import { parse } from 'query-string'; import { createApiKey, removeApiKey } from '../actions'; import AccountForm from '../components/AccountForm'; @@ -15,6 +14,7 @@ import Nav from '../../../components/Nav'; import ErrorModal from '../../IDE/components/ErrorModal'; import Overlay from '../../App/components/Overlay'; import Toast from '../../IDE/components/Toast'; +import { withRouter } from '../../../utils/router-compatibilty'; function SocialLoginPanel(props) { const { user, t } = props; @@ -77,7 +77,7 @@ class AccountView extends React.Component { title={this.props.t('ErrorModal.LinkTitle')} ariaLabel={this.props.t('ErrorModal.LinkTitle')} closeOverlay={() => { - browserHistory.push(this.props.location.pathname); + this.props.navigate(this.props.location.pathname); }} > @@ -152,6 +152,7 @@ AccountView.propTypes = { search: PropTypes.string.isRequired, pathname: PropTypes.string.isRequired }).isRequired, + navigate: PropTypes.func.isRequired, toast: PropTypes.shape({ isVisible: PropTypes.bool.isRequired }).isRequired diff --git a/client/modules/User/pages/DashboardView.jsx b/client/modules/User/pages/DashboardView.jsx index 38958b2335..c609121476 100644 --- a/client/modules/User/pages/DashboardView.jsx +++ b/client/modules/User/pages/DashboardView.jsx @@ -1,7 +1,6 @@ import PropTypes from 'prop-types'; import React from 'react'; import { connect } from 'react-redux'; -import { browserHistory } from 'react-router'; import { withTranslation } from 'react-i18next'; import Button from '../../../common/Button'; @@ -30,9 +29,7 @@ class DashboardView extends React.Component { constructor(props) { super(props); - this.closeAccountPage = this.closeAccountPage.bind(this); this.createNewSketch = this.createNewSketch.bind(this); - this.gotoHomePage = this.gotoHomePage.bind(this); this.toggleCollectionCreate = this.toggleCollectionCreate.bind(this); this.state = { collectionCreateVisible: false @@ -43,18 +40,10 @@ class DashboardView extends React.Component { document.body.className = this.props.theme; } - closeAccountPage() { - browserHistory.push(this.props.previousPath); - } - createNewSketch() { this.props.newProject(); } - gotoHomePage() { - browserHistory.push('/'); - } - selectedTabKey() { const path = this.props.location.pathname; @@ -173,7 +162,6 @@ class DashboardView extends React.Component { function mapStateToProps(state) { return { - previousPath: state.ide.previousPath, user: state.user, theme: state.preferences.theme }; @@ -191,7 +179,6 @@ DashboardView.propTypes = { params: PropTypes.shape({ username: PropTypes.string.isRequired }).isRequired, - previousPath: PropTypes.string.isRequired, theme: PropTypes.string.isRequired, user: PropTypes.shape({ username: PropTypes.string diff --git a/client/modules/User/pages/EmailVerificationView.jsx b/client/modules/User/pages/EmailVerificationView.jsx index 4f9775e5fc..71584d96a7 100644 --- a/client/modules/User/pages/EmailVerificationView.jsx +++ b/client/modules/User/pages/EmailVerificationView.jsx @@ -2,7 +2,6 @@ import PropTypes from 'prop-types'; import React from 'react'; import { connect } from 'react-redux'; import { bindActionCreators } from 'redux'; -import { browserHistory } from 'react-router'; import { withTranslation } from 'react-i18next'; import get from 'lodash/get'; import { Helmet } from 'react-helmet'; @@ -34,7 +33,7 @@ class EmailVerificationView extends React.Component { status =

{this.props.t('EmailVerificationView.Checking')}

; } else if (emailVerificationTokenState === 'verified') { status =

{this.props.t('EmailVerificationView.Verified')}

; - setTimeout(() => browserHistory.push('/'), 1000); + setTimeout(() => this.props.navigate('/'), 1000); } else if (emailVerificationTokenState === 'invalid') { status =

{this.props.t('EmailVerificationView.InvalidState')}

; } @@ -80,7 +79,8 @@ EmailVerificationView.propTypes = { 'invalid' ]), verifyEmailConfirmation: PropTypes.func.isRequired, - t: PropTypes.func.isRequired + t: PropTypes.func.isRequired, + navigate: PropTypes.func.isRequired }; export default withTranslation()( diff --git a/client/modules/User/pages/LoginView.jsx b/client/modules/User/pages/LoginView.jsx index 4a81ea726c..6696b550d6 100644 --- a/client/modules/User/pages/LoginView.jsx +++ b/client/modules/User/pages/LoginView.jsx @@ -1,5 +1,5 @@ import React from 'react'; -import { Link } from 'react-router'; +import { Link } from 'react-router-dom'; import { Helmet } from 'react-helmet'; import { useTranslation } from 'react-i18next'; import LoginForm from '../components/LoginForm'; diff --git a/client/modules/User/pages/NewPasswordView.jsx b/client/modules/User/pages/NewPasswordView.jsx index 3bfeec86ad..f25bd010b3 100644 --- a/client/modules/User/pages/NewPasswordView.jsx +++ b/client/modules/User/pages/NewPasswordView.jsx @@ -1,17 +1,18 @@ -import PropTypes from 'prop-types'; import React, { useEffect } from 'react'; import classNames from 'classnames'; import { useDispatch, useSelector } from 'react-redux'; import { Helmet } from 'react-helmet'; import { useTranslation } from 'react-i18next'; +import { useParams } from 'react-router-dom'; import NewPasswordForm from '../components/NewPasswordForm'; import { validateResetPasswordToken } from '../actions'; import Nav from '../../../components/Nav'; import RootPage from '../../../components/RootPage'; -function NewPasswordView(props) { +function NewPasswordView() { const { t } = useTranslation(); - const resetPasswordToken = props.params.reset_password_token; + const params = useParams(); + const resetPasswordToken = params.reset_password_token; const resetPasswordInvalid = useSelector( (state) => state.user.resetPasswordInvalid ); @@ -48,10 +49,4 @@ function NewPasswordView(props) { ); } -NewPasswordView.propTypes = { - params: PropTypes.shape({ - reset_password_token: PropTypes.string - }).isRequired -}; - export default NewPasswordView; diff --git a/client/modules/User/pages/ResetPasswordView.jsx b/client/modules/User/pages/ResetPasswordView.jsx index d406f185e6..b97ab4a17a 100644 --- a/client/modules/User/pages/ResetPasswordView.jsx +++ b/client/modules/User/pages/ResetPasswordView.jsx @@ -1,5 +1,5 @@ import React from 'react'; -import { Link } from 'react-router'; +import { Link } from 'react-router-dom'; import classNames from 'classnames'; import { useSelector } from 'react-redux'; import { Helmet } from 'react-helmet'; diff --git a/client/modules/User/pages/SignupView.jsx b/client/modules/User/pages/SignupView.jsx index ee315c2fb3..04b5423e1e 100644 --- a/client/modules/User/pages/SignupView.jsx +++ b/client/modules/User/pages/SignupView.jsx @@ -1,5 +1,5 @@ import React from 'react'; -import { Link } from 'react-router'; +import { Link } from 'react-router-dom'; import { Helmet } from 'react-helmet'; import { useTranslation } from 'react-i18next'; import SignupForm from '../components/SignupForm'; diff --git a/client/router.js b/client/router.js new file mode 100644 index 0000000000..878b247935 --- /dev/null +++ b/client/router.js @@ -0,0 +1,8 @@ +import { createBrowserRouter } from 'react-router-dom'; +import routes from './routes'; + +const router = createBrowserRouter(routes); + +export const { navigate } = router; + +export default router; diff --git a/client/routes.jsx b/client/routes.jsx index d7762fc67e..9fdc5797b0 100644 --- a/client/routes.jsx +++ b/client/routes.jsx @@ -1,6 +1,8 @@ -import { Route, IndexRoute } from 'react-router'; -import React from 'react'; +import { useDispatch } from 'react-redux'; +import React, { useEffect } from 'react'; +import { useLocation, Outlet } from 'react-router-dom'; +import RedirectToUser from './components/createRedirectWithUsername'; import App from './modules/App/App'; import IDEView from './modules/IDE/pages/IDEView'; import MobileIDEView from './modules/IDE/pages/MobileIDEView'; @@ -15,7 +17,6 @@ import NewPasswordView from './modules/User/pages/NewPasswordView'; import AccountView from './modules/User/pages/AccountView'; import CollectionView from './modules/User/pages/CollectionView'; import DashboardView from './modules/User/pages/DashboardView'; -import createRedirectWithUsername from './components/createRedirectWithUsername'; import MobileDashboardView from './modules/Mobile/MobileDashboardView'; // import PrivacyPolicy from './modules/IDE/pages/PrivacyPolicy'; // import TermsOfUse from './modules/IDE/pages/TermsOfUse'; @@ -28,102 +29,162 @@ import { userIsAuthorized } from './utils/auth'; import { mobileFirst, responsiveForm } from './utils/responsive'; +import { ElementFromComponent } from './utils/router-compatibilty'; -const checkAuth = (store) => { - store.dispatch(getUser()); -}; - -// TODO: This short-circuit seems unnecessary - using the mobile navigator (future) should prevent this from being called -const onRouteChange = (store) => { - const path = window.location.pathname; - if (path.includes('preview')) return; +/** + * Wrapper around App for handling legacy 'onChange' and 'onEnter' functionality, + * injecting the location prop, and rendering child route content. + */ +const Main = () => { + const location = useLocation(); - store.dispatch(stopSketch()); -}; + const dispatch = useDispatch(); -const routes = (store) => ( - { - onRouteChange(store); - }} - > - + useEffect(() => { + dispatch(getUser()); + }, []); - - - - - - - - + // TODO: This short-circuit seems unnecessary - using the mobile navigator (future) should prevent this from being called + useEffect(() => { + if (location.pathname.includes('preview')) return; - - - - - - + dispatch(stopSketch()); + }, [location.pathname]); - - - - + return ( + + + + ); +}; - {/* Mobile-only Routes */} - - - - - - -); +const routes = [ + { + path: '/', + element:
, + children: [ + { + index: true, + element: ( + + ) + }, + { + path: '/login', + element: ( + + ) + }, + { + path: '/signup', + element: ( + + ) + }, + { + path: '/reset-password', + element: ( + + ) + }, + { path: '/verify', element: }, + { + path: '/reset-password/:reset_password_token', + element: + }, + { + path: '/projects/:project_id', + element: + }, + { path: '/:username/full/:project_id', element: }, + { path: '/full/:project_id', element: }, + { + path: '/:username/assets', + element: ( + + ) + }, + { + path: '/:username/sketches', + element: ( + + ) + }, + { + path: '/:username/sketches/:project_id', + element: ( + + ) + }, + { + path: '/:username/sketches/:project_id/add-to-collection', + element: ( + + ) + }, + { + path: '/:username/collections', + element: ( + + ) + }, + { + path: '/:username/collections/:collection_id', + element: + }, + { + path: '/sketches', + element: + }, + { + path: '/assets', + element: + }, + { + path: '/account', + element: ( + + ) + }, + { + path: '/about', + element: + }, + /* Mobile-only Routes */ + { path: '/preview', element: }, + { path: '/preferences', element: }, + { path: '/privacy-policy', element: }, + { path: '/terms-of-use', element: }, + { + path: '/code-of-conduct', + element: + } + ] + } +]; export default routes; diff --git a/client/utils/router-compatibilty.jsx b/client/utils/router-compatibilty.jsx new file mode 100644 index 0000000000..68284ec66a --- /dev/null +++ b/client/utils/router-compatibilty.jsx @@ -0,0 +1,39 @@ +import PropTypes from 'prop-types'; +import { useLocation, useNavigate, useParams } from 'react-router-dom'; +import React from 'react'; + +/** + * react-router v6+ uses `element` instead of `component`. + * This wrapper allows passing the component itself and will inject the necessary props to mimic the old behavior. + */ +export const ElementFromComponent = ({ component: Component }) => { + const location = useLocation(); + const params = useParams(); + const navigate = useNavigate(); + + return ; +}; + +ElementFromComponent.propTypes = { + component: PropTypes.elementType.isRequired +}; + +/** + * react-router no longer exports a `withRouter` HOC as hooks are the desired way to access this data. + * Create an HOC to use with legacy class components that cannot use hooks. + * Provides `navigate` (not `history`), `location`, and `params`. + */ +export const withRouter = (Component) => (props) => { + const location = useLocation(); + const params = useParams(); + const navigate = useNavigate(); + + return ( + + ); +}; diff --git a/package-lock.json b/package-lock.json index c6a5dee419..a8f2f0dded 100644 --- a/package-lock.json +++ b/package-lock.json @@ -84,7 +84,8 @@ "react-markdown": "^6.0.3", "react-redux": "^7.2.4", "react-responsive": "^8.2.0", - "react-router": "^3.2.6", + "react-router": "^6.8.1", + "react-router-dom": "^6.8.1", "react-split-pane": "^0.1.92", "react-tabs": "^2.3.1", "react-transition-group": "^4.4.2", @@ -5914,6 +5915,14 @@ "react": "^16.3.0 || ^17.0.0" } }, + "node_modules/@remix-run/router": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.3.2.tgz", + "integrity": "sha512-t54ONhl/h75X94SWsHGQ4G/ZrCEguKSRQr7DrjTciJXW0YU1QhlwYeycvK5JgkzlxmvrK7wq1NB/PLtHxoiDcA==", + "engines": { + "node": ">=14" + } + }, "node_modules/@sindresorhus/is": { "version": "0.14.0", "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-0.14.0.tgz", @@ -24418,29 +24427,6 @@ "node": "*" } }, - "node_modules/history": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/history/-/history-3.3.0.tgz", - "integrity": "sha1-/O3M6PEpdTcVRdc1RhAzV5ptrpw=", - "dependencies": { - "invariant": "^2.2.1", - "loose-envify": "^1.2.0", - "query-string": "^4.2.2", - "warning": "^3.0.0" - } - }, - "node_modules/history/node_modules/query-string": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/query-string/-/query-string-4.3.4.tgz", - "integrity": "sha1-u7aTucqRXCMlFbIosaArYJBD2+s=", - "dependencies": { - "object-assign": "^4.1.0", - "strict-uri-encode": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/hmac-drbg": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", @@ -43633,21 +43619,33 @@ } }, "node_modules/react-router": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-3.2.6.tgz", - "integrity": "sha512-nlxtQE8B22hb/JxdaslI1tfZacxFU8x8BJryXOnR2RxB4vc01zuHYAHAIgmBkdk1kzXaA25hZxK6KAH/+CXArw==", + "version": "6.8.1", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.8.1.tgz", + "integrity": "sha512-Jgi8BzAJQ8MkPt8ipXnR73rnD7EmZ0HFFb7jdQU24TynGW1Ooqin2KVDN9voSC+7xhqbbCd2cjGUepb6RObnyg==", "dependencies": { - "create-react-class": "^15.5.1", - "history": "^3.0.0", - "hoist-non-react-statics": "^3.3.2", - "invariant": "^2.2.1", - "loose-envify": "^1.2.0", - "prop-types": "^15.7.2", - "react-is": "^16.13.0", - "warning": "^3.0.0" + "@remix-run/router": "1.3.2" + }, + "engines": { + "node": ">=14" }, "peerDependencies": { - "react": "^0.14.0 || ^15.0.0 || ^16.0.0" + "react": ">=16.8" + } + }, + "node_modules/react-router-dom": { + "version": "6.8.1", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.8.1.tgz", + "integrity": "sha512-67EXNfkQgf34P7+PSb6VlBuaacGhkKn3kpE51+P6zYSG2kiRoumXEL6e27zTa9+PGF2MNXbgIUHTVlleLbIcHQ==", + "dependencies": { + "@remix-run/router": "1.3.2", + "react-router": "6.8.1" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" } }, "node_modules/react-select": { @@ -55439,6 +55437,11 @@ } } }, + "@remix-run/router": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.3.2.tgz", + "integrity": "sha512-t54ONhl/h75X94SWsHGQ4G/ZrCEguKSRQr7DrjTciJXW0YU1QhlwYeycvK5JgkzlxmvrK7wq1NB/PLtHxoiDcA==" + }, "@sindresorhus/is": { "version": "0.14.0", "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-0.14.0.tgz", @@ -70026,28 +70029,6 @@ "integrity": "sha512-Sc28JNQNDzaH6PORtRLMvif9RSn1mYuOoX3omVjnb0+HbpPygU2ALBI0R/wsiqCb4/fcp07Gdo8g+fhtFrQl6A==", "dev": true }, - "history": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/history/-/history-3.3.0.tgz", - "integrity": "sha1-/O3M6PEpdTcVRdc1RhAzV5ptrpw=", - "requires": { - "invariant": "^2.2.1", - "loose-envify": "^1.2.0", - "query-string": "^4.2.2", - "warning": "^3.0.0" - }, - "dependencies": { - "query-string": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/query-string/-/query-string-4.3.4.tgz", - "integrity": "sha1-u7aTucqRXCMlFbIosaArYJBD2+s=", - "requires": { - "object-assign": "^4.1.0", - "strict-uri-encode": "^1.0.0" - } - } - } - }, "hmac-drbg": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", @@ -85020,18 +85001,20 @@ } }, "react-router": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-3.2.6.tgz", - "integrity": "sha512-nlxtQE8B22hb/JxdaslI1tfZacxFU8x8BJryXOnR2RxB4vc01zuHYAHAIgmBkdk1kzXaA25hZxK6KAH/+CXArw==", + "version": "6.8.1", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.8.1.tgz", + "integrity": "sha512-Jgi8BzAJQ8MkPt8ipXnR73rnD7EmZ0HFFb7jdQU24TynGW1Ooqin2KVDN9voSC+7xhqbbCd2cjGUepb6RObnyg==", "requires": { - "create-react-class": "^15.5.1", - "history": "^3.0.0", - "hoist-non-react-statics": "^3.3.2", - "invariant": "^2.2.1", - "loose-envify": "^1.2.0", - "prop-types": "^15.7.2", - "react-is": "^16.13.0", - "warning": "^3.0.0" + "@remix-run/router": "1.3.2" + } + }, + "react-router-dom": { + "version": "6.8.1", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.8.1.tgz", + "integrity": "sha512-67EXNfkQgf34P7+PSb6VlBuaacGhkKn3kpE51+P6zYSG2kiRoumXEL6e27zTa9+PGF2MNXbgIUHTVlleLbIcHQ==", + "requires": { + "@remix-run/router": "1.3.2", + "react-router": "6.8.1" } }, "react-select": { diff --git a/package.json b/package.json index 90c91f5d12..add1761205 100644 --- a/package.json +++ b/package.json @@ -218,7 +218,8 @@ "react-markdown": "^6.0.3", "react-redux": "^7.2.4", "react-responsive": "^8.2.0", - "react-router": "^3.2.6", + "react-router": "^6.8.1", + "react-router-dom": "^6.8.1", "react-split-pane": "^0.1.92", "react-tabs": "^2.3.1", "react-transition-group": "^4.4.2", From 93f37e5f0b77666afdc4648cac51ee3577fd8517 Mon Sep 17 00:00:00 2001 From: Linda Paiste Date: Mon, 29 May 2023 18:18:47 -0500 Subject: [PATCH 2/3] Implement a replacement for `router.setRouteLeaveHook` using new hook `useBlocker`. --- client/components/Nav.jsx | 6 +- client/modules/IDE/pages/IDEView.jsx | 87 +++++++++++++++------------- 2 files changed, 49 insertions(+), 44 deletions(-) diff --git a/client/components/Nav.jsx b/client/components/Nav.jsx index a3e9b5aba6..6d83cf3cbf 100644 --- a/client/components/Nav.jsx +++ b/client/components/Nav.jsx @@ -99,12 +99,12 @@ class Nav extends React.PureComponent { } handleNew() { - const { unsavedChanges, warnIfUnsavedChanges } = this.props; + const { unsavedChanges } = this.props; if (!unsavedChanges) { this.props.showToast(1500); this.props.setToastText('Toast.OpenedNewSketch'); this.props.newProject(); - } else if (warnIfUnsavedChanges && warnIfUnsavedChanges()) { + } else if (window.confirm(this.props.t('Nav.WarningUnsavedChanges'))) { this.props.showToast(1500); this.props.setToastText('Toast.OpenedNewSketch'); this.props.newProject(); @@ -952,7 +952,6 @@ Nav.propTypes = { showShareModal: PropTypes.func.isRequired, showErrorModal: PropTypes.func.isRequired, unsavedChanges: PropTypes.bool.isRequired, - warnIfUnsavedChanges: PropTypes.func, showKeyboardShortcutModal: PropTypes.func.isRequired, cmController: PropTypes.shape({ tidyCode: PropTypes.func, @@ -985,7 +984,6 @@ Nav.defaultProps = { }, cmController: {}, layout: 'project', - warnIfUnsavedChanges: undefined, params: { username: undefined } diff --git a/client/modules/IDE/pages/IDEView.jsx b/client/modules/IDE/pages/IDEView.jsx index 9794bc4af2..041d7e212b 100644 --- a/client/modules/IDE/pages/IDEView.jsx +++ b/client/modules/IDE/pages/IDEView.jsx @@ -1,8 +1,12 @@ import PropTypes from 'prop-types'; -import React from 'react'; +import React, { useEffect } from 'react'; +import { + unstable_useBlocker as useBlocker, + useLocation +} from 'react-router-dom'; import { bindActionCreators } from 'redux'; -import { connect } from 'react-redux'; -import { withTranslation } from 'react-i18next'; +import { connect, useSelector } from 'react-redux'; +import { useTranslation, withTranslation } from 'react-i18next'; import { Helmet } from 'react-helmet'; import SplitPane from 'react-split-pane'; import Editor from '../components/Editor'; @@ -41,22 +45,45 @@ function getTitle(props) { return id ? `p5.js Web Editor | ${props.project.name}` : 'p5.js Web Editor'; } -function warnIfUnsavedChanges(props, nextLocation) { - const toAuth = - nextLocation && - nextLocation.action === 'PUSH' && - (nextLocation.pathname === '/login' || nextLocation.pathname === '/signup'); - const onAuth = - nextLocation && - (props.location.pathname === '/login' || - props.location.pathname === '/signup'); - if (props.ide.unsavedChanges && !toAuth && !onAuth) { - if (!window.confirm(props.t('Nav.WarningUnsavedChanges'))) { - return false; +function isAuth(pathname) { + return pathname === '/login' || pathname === '/signup'; +} + +function isOverlay(pathname) { + return pathname === '/about' || pathname === '/feedback'; +} + +function WarnIfUnsavedChanges() { + const hasUnsavedChanges = useSelector((state) => state.ide.unsavedChanges); + + const { t } = useTranslation(); + + const currentLocation = useLocation(); + + const blocker = useBlocker(hasUnsavedChanges); + + useEffect(() => { + if (blocker.state === 'blocked') { + const nextLocation = blocker.location; + if ( + isAuth(nextLocation.pathname) || + isAuth(currentLocation.pathname) || + isOverlay(nextLocation.pathname) || + isOverlay(currentLocation.pathname) + ) { + blocker.proceed(); + } else { + const didConfirm = window.confirm(t('Nav.WarningUnsavedChanges')); + if (didConfirm) { + blocker.proceed(); + } else { + blocker.reset(); + } + } } - return true; - } - return true; + }, [blocker, currentLocation.pathname, t]); + + return null; } class IDEView extends React.Component { @@ -86,11 +113,6 @@ class IDEView extends React.Component { this.isMac = navigator.userAgent.toLowerCase().indexOf('mac') !== -1; document.addEventListener('keydown', this.handleGlobalKeydown, false); - this.props.router.setRouteLeaveHook( - this.props.route, - this.handleUnsavedChanges - ); - // window.onbeforeunload = this.handleUnsavedChanges; window.addEventListener('beforeunload', this.handleBeforeUnload); @@ -140,12 +162,6 @@ class IDEView extends React.Component { clearTimeout(this.autosaveInterval); this.autosaveInterval = null; } - - if (this.props.route.path !== prevProps.route.path) { - this.props.router.setRouteLeaveHook(this.props.route, () => - warnIfUnsavedChanges(this.props) - ); - } } componentWillUnmount() { document.removeEventListener('keydown', this.handleGlobalKeydown, false); @@ -231,9 +247,6 @@ class IDEView extends React.Component { } } - handleUnsavedChanges = (nextLocation) => - warnIfUnsavedChanges(this.props, nextLocation); - handleBeforeUnload = (e) => { const confirmationMessage = this.props.t('Nav.WarningUnsavedChanges'); if (this.props.ide.unsavedChanges) { @@ -254,11 +267,9 @@ class IDEView extends React.Component { {getTitle(this.props)} + {this.props.toast.isVisible && } -