Skip to content
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

FIX Reinstate unsaved changes dialog #1458

Merged
Show file tree
Hide file tree
Changes from all 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
2 changes: 1 addition & 1 deletion client/dist/js/bundle.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion client/dist/js/vendor.js

Large diffs are not rendered by default.

63 changes: 29 additions & 34 deletions client/src/boot/BootRoutes.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import $ from 'jquery';
import React from 'react';
import { Provider as ReduxProvider } from 'react-redux';
import ReactDOM from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom';
import { createBrowserRouter, createRoutesFromElements, RouterProvider } from 'react-router-dom';
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to use createBrowserRouter() and createRoutesFromElements() instead of adding the BrowserRouter and Routes components ourselves because the RouterProvider doesn't like us doing it ourself - and we have to use the RouterProvider because this instantiates a "data router" which is necessary to use the new useBlocker hook.

import Config from 'lib/Config';
import pageRouter from 'lib/Router';
import reactRouteRegister from 'lib/ReactRouteRegister';
Expand All @@ -15,8 +15,9 @@ import { ApolloProvider } from '@apollo/client';
import i18n from 'i18n';
import { isDirty } from 'redux-form';
import getFormState from 'lib/getFormState';
import { Routes, Route } from 'react-router';
import { Route } from 'react-router';
import { joinUrlPaths } from 'lib/urls';
import NavigationBlocker from '../components/NavigationBlocker/NavigationBlocker';

/**
* Bootstraps routes
Expand All @@ -35,8 +36,7 @@ class BootRoutes {
const base = Config.get('absoluteBaseUrl');
pageRouter.setAbsoluteBase(base);

this.handleBeforeRoute = this.handleBeforeRoute.bind(this);
this.handleBeforeUnload = this.handleBeforeUnload.bind(this);
Comment on lines -38 to -39
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Neither of these functions is passed into downstream components anymore, so we don't need explicit binds.

this.shouldConfirmBeforeUnload = this.shouldConfirmBeforeUnload.bind(this);
}

setStore(store) {
Expand Down Expand Up @@ -109,18 +109,30 @@ class BootRoutes {
initReactRouter() {
reactRouteRegister.updateRootRoute({ component: App });
const rootRoute = reactRouteRegister.getRootRoute();
const routes = reactRouteRegister.getChildRoutes().map((route) => (
<Route key={route.path} path={route.path} element={<route.component />} />
));
const router = createBrowserRouter(
createRoutesFromElements(
<Route
path={rootRoute.path}
element={
<rootRoute.component>
<NavigationBlocker shouldBlockFn={this.shouldConfirmBeforeUnload} blockMessage={this.getUnsavedChangesMessage()} />
</rootRoute.component>
}
>
{reactRouteRegister.getChildRoutes().map(
(route) => (
<Route key={route.path} path={route.path} element={<route.component />} />
)
)}
</Route>
),
{ basename: joinUrlPaths(Config.get('baseUrl'), Config.get('adminUrl')) }
);

ReactDOM.createRoot(document.getElementsByClassName('cms-content')[0]).render(
<ApolloProvider client={this.client}>
<ReduxProvider store={this.store}>
<BrowserRouter basename={joinUrlPaths(Config.get('baseUrl'), Config.get('adminUrl'))}>
<Routes>
<Route path={rootRoute.path} element={<rootRoute.component />}>{routes}</Route>
</Routes>
</BrowserRouter>
<RouterProvider router={router} />
</ReduxProvider>
</ApolloProvider>
Comment on lines -112 to 137
Copy link
Member Author

@GuySartorelli GuySartorelli Feb 23, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ultimately the only differences here are:

  • We now have a RouterProvider (which is necessary for the new useBlocker hook)
  • We're passing a component in as a child of the App component (rootRoute.component)

See #1458 (comment) for the reason we had to change the way we're doing the browserrouter etc.

);
Expand All @@ -134,12 +146,7 @@ class BootRoutes {
const store = this.store;

pageRouter('*', (ctx, next) => {
const msg = i18n._t(
'Admin.CONFIRMUNSAVED',
`Are you sure you want to navigate away from this page?\n\n
WARNING: Your changes have not been saved.\n\n
Press OK to continue, or Cancel to stay on the current page.`
);
const msg = this.getUnsavedChangesMessage();
if (!this.shouldConfirmBeforeUnload() || window.confirm(msg)) {
// eslint-disable-next-line no-param-reassign
ctx.store = store;
Expand Down Expand Up @@ -218,22 +225,10 @@ class BootRoutes {
return changedForms.length > 0;
}

handleBeforeUnload(content, callback) {
if (this.shouldConfirmBeforeUnload()) {
return callback(confirm(i18n._t('Admin.CONFIRMUNSAVEDSHORT', 'WARNING: Your changes have not been saved.')));
}

return callback(true);
}

handleBeforeRoute(content, callback) {
if (this.shouldConfirmBeforeUnload()) {
return callback(confirm(i18n._t('Admin.CONFIRMUNSAVED', `Are you sure you want to navigate away
from this page?\n\nWARNING: Your changes have not been saved.\n\n
Press OK to continue, or Cancel to stay on the current page.`)));
}

return callback(true);
Comment on lines -221 to -236
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Neither of these is needed anymore

getUnsavedChangesMessage() {
return i18n._t('Admin.CONFIRMUNSAVED', `Are you sure you want to navigate away
from this page?\n\nWARNING: Your changes have not been saved.\n\n
Press OK to continue, or Cancel to stay on the current page.`);
}
}

Expand Down
28 changes: 28 additions & 0 deletions client/src/components/NavigationBlocker/NavigationBlocker.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { useEffect } from 'react';
import PropTypes from 'prop-types';
import { unstable_useBlocker as useBlocker } from 'react-router-dom';

export default function NavigationBlocker({ shouldBlockFn, blockMessage }) {
const blocker = useBlocker(shouldBlockFn);
useEffect(() => {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Has to be in an effect to avoid the console error I mentioned in #1458 (comment)

if (blocker.state === 'blocked') {
// eslint-disable-next-line no-alert
const canUnload = confirm(blockMessage);
if (canUnload) {
blocker.proceed();
} else {
blocker.reset();
}
}
}, [blocker.state]);

// Don't actually render anything - this component exists purely to hold the blocker
// logic, which we put here to (hopefully) avoid rerendering the entire app when blocker
// state changes.
return null;
}

NavigationBlocker.propTypes = {
shouldBlockFn: PropTypes.func.isRequired,
blockMessage: PropTypes.string.isRequired,
};
4 changes: 2 additions & 2 deletions client/src/containers/App/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ import { Outlet } from 'react-router';
* Empty container for the moment, will eventually contain the CMS menu`
* and apply to document.body, rather than just one specific DOM element.
*/
const App = () => (
<div className="app"><Outlet /></div>
const App = ({ children }) => (
<div className="app">{children}<Outlet /></div>
Comment on lines +9 to +10
Copy link
Member Author

@GuySartorelli GuySartorelli Feb 23, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have to have the blocker as a child of the root component because it has to be a child of the RouteProvider and this is the only component we know for sure will be in there.
But because this replaceable via the reactRouteRegister we don't even know that the root component will specifically be App so we can't just slap it directly here, we have to pass it in as a child prop.

);

export default provideInjector(App);
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,8 @@
"react-dom": "^18.2.0",
"react-load-script": "^0.0.6",
"react-redux": "^8.0.4",
"react-router": "^6.4.2",
"react-router-dom": "^6.4.2",
"react-router": "^6.7",
"react-router-dom": "^6.7",
Comment on lines -74 to +75
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new useBlocker hook was only introduced in 6.7 (I'll update the changelog as well to reflect the new dependency constraint)

"react-select": "^5.5.8",
"reactstrap": "^8.9.0",
"redux": "^4.2.0",
Expand Down
30 changes: 15 additions & 15 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1651,10 +1651,10 @@
resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.11.6.tgz#cee20bd55e68a1720bdab363ecf0c821ded4cd45"
integrity sha512-50/17A98tWUfQ176raKiOGXuYpLyyVMkxxG6oylzL3BPOlA6ADGdK7EYunSa4I064xerltq9TGXs8HmOk5E+vw==

"@remix-run/router@1.0.3":
version "1.0.3"
resolved "https://registry.yarnpkg.com/@remix-run/router/-/router-1.0.3.tgz#953b88c20ea00d0eddaffdc1b115c08474aa295d"
integrity sha512-ceuyTSs7PZ/tQqi19YZNBc5X7kj1f8p+4DIyrcIYFY9h+hd1OKm4RqtiWldR9eGEvIiJfsqwM4BsuCtRIuEw6Q==
"@remix-run/router@1.3.2":
version "1.3.2"
resolved "https://registry.yarnpkg.com/@remix-run/router/-/router-1.3.2.tgz#58cd2bd25df2acc16c628e1b6f6150ea6c7455bc"
integrity sha512-t54ONhl/h75X94SWsHGQ4G/ZrCEguKSRQr7DrjTciJXW0YU1QhlwYeycvK5JgkzlxmvrK7wq1NB/PLtHxoiDcA==

"@sect/modernizr-loader@^1.0.3":
version "1.0.3"
Expand Down Expand Up @@ -7556,20 +7556,20 @@ react-redux@^8.0.4:
react-is "^18.0.0"
use-sync-external-store "^1.0.0"

react-router-dom@^6.4.2:
version "6.4.3"
resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-6.4.3.tgz#70093b5f65f85f1df9e5d4182eb7ff3a08299275"
integrity sha512-MiaYQU8CwVCaOfJdYvt84KQNjT78VF0TJrA17SIQgNHRvLnXDJO6qsFqq8F/zzB1BWZjCFIrQpu4QxcshitziQ==
react-router-dom@^6.7:
version "6.8.1"
resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-6.8.1.tgz#7e136b67d9866f55999e9a8482c7008e3c575ac9"
integrity sha512-67EXNfkQgf34P7+PSb6VlBuaacGhkKn3kpE51+P6zYSG2kiRoumXEL6e27zTa9+PGF2MNXbgIUHTVlleLbIcHQ==
dependencies:
"@remix-run/router" "1.0.3"
react-router "6.4.3"
"@remix-run/router" "1.3.2"
react-router "6.8.1"

react-router@6.4.3, react-router@^6.4.2:
version "6.4.3"
resolved "https://registry.yarnpkg.com/react-router/-/react-router-6.4.3.tgz#9ed3ee4d6e95889e9b075a5d63e29acc7def0d49"
integrity sha512-BT6DoGn6aV1FVP5yfODMOiieakp3z46P1Fk0RNzJMACzE7C339sFuHebfvWtnB4pzBvXXkHP2vscJzWRuUjTtA==
react-router@6.8.1, react-router@^6.7:
version "6.8.1"
resolved "https://registry.yarnpkg.com/react-router/-/react-router-6.8.1.tgz#e362caf93958a747c649be1b47cd505cf28ca63e"
integrity sha512-Jgi8BzAJQ8MkPt8ipXnR73rnD7EmZ0HFFb7jdQU24TynGW1Ooqin2KVDN9voSC+7xhqbbCd2cjGUepb6RObnyg==
dependencies:
"@remix-run/router" "1.0.3"
"@remix-run/router" "1.3.2"

react-select@^5.5.8:
version "5.7.0"
Expand Down