diff --git a/examples/demo/package.json b/examples/demo/package.json index fe35aebe977..be27f97e729 100644 --- a/examples/demo/package.json +++ b/examples/demo/package.json @@ -21,8 +21,8 @@ "react-app-polyfill": "^0.1.3", "react-dom": "^16.9.0", "react-redux": "^7.1.0", - "react-router": "^5.0.1", - "react-router-dom": "^5.0.1", + "react-router": "^5.1.0", + "react-router-dom": "^5.1.0", "react-scripts": "^3.0.0", "recompose": "~0.26.0", "redux-saga": "^1.0.0", diff --git a/examples/demo/src/layout/Menu.js b/examples/demo/src/layout/Menu.js index fa910bb3966..93c069dd04e 100644 --- a/examples/demo/src/layout/Menu.js +++ b/examples/demo/src/layout/Menu.js @@ -4,7 +4,6 @@ import { useSelector } from 'react-redux'; import SettingsIcon from '@material-ui/icons/Settings'; import LabelIcon from '@material-ui/icons/Label'; import { useMediaQuery } from '@material-ui/core'; -import { withRouter } from 'react-router-dom'; import { useTranslate, DashboardMenuItem, MenuItemLink } from 'react-admin'; import visitors from '../visitors'; @@ -151,4 +150,4 @@ Menu.propTypes = { logout: PropTypes.object, }; -export default withRouter(Menu); +export default Menu; diff --git a/examples/demo/src/reviews/ReviewList.js b/examples/demo/src/reviews/ReviewList.js index cddae331b9d..2c078ac7bcb 100644 --- a/examples/demo/src/reviews/ReviewList.js +++ b/examples/demo/src/reviews/ReviewList.js @@ -1,9 +1,7 @@ import React, { Fragment, useCallback } from 'react'; import classnames from 'classnames'; import { BulkDeleteButton, List } from 'react-admin'; -import { useDispatch } from 'react-redux'; -import { push } from 'connected-react-router'; -import { Route } from 'react-router'; +import { Route, useHistory } from 'react-router'; import { Drawer, useMediaQuery, makeStyles } from '@material-ui/core'; import BulkAcceptButton from './BulkAcceptButton'; import BulkRejectButton from './BulkRejectButton'; @@ -41,12 +39,12 @@ const useStyles = makeStyles(theme => ({ const ReviewList = props => { const classes = useStyles(); - const dispatch = useDispatch(); const isXSmall = useMediaQuery(theme => theme.breakpoints.down('xs')); + const history = useHistory(); const handleClose = useCallback(() => { - dispatch(push('/reviews')); - }, [dispatch]); + history.push('/reviews'); + }, [history]); return (
diff --git a/packages/ra-core/package.json b/packages/ra-core/package.json index 627c69e3e78..29b2615641f 100644 --- a/packages/ra-core/package.json +++ b/packages/ra-core/package.json @@ -31,8 +31,8 @@ "@types/history": "^4.7.2", "@types/node-polyglot": "^0.4.31", "@types/react-redux": "^7.1.1", - "@types/react-router": "^5.0.1", - "@types/react-router-dom": "^4.3.1", + "@types/react-router": "^5.1.0", + "@types/react-router-dom": "^5.1.0", "@types/recompose": "^0.27.0", "cross-env": "^5.2.0", "enzyme": "~3.9.0", @@ -51,7 +51,7 @@ "dependencies": { "@testing-library/react": "^8.0.7", "classnames": "~2.2.5", - "connected-react-router": "^6.4.0", + "connected-react-router": "^6.5.2", "date-fns": "^1.29.0", "eventemitter3": "^3.0.0", "final-form": "^4.18.5", @@ -61,8 +61,8 @@ "query-string": "~5.1.1", "react-final-form": "^6.3.0", "react-redux": "^7.1.0", - "react-router": "^5.0.1", - "react-router-dom": "^5.0.1", + "react-router": "^5.1.0", + "react-router-dom": "^5.1.0", "recompose": "~0.26.0", "redux": "^3.7.2 || ^4.0.3", "redux-saga": "^1.0.0", diff --git a/packages/ra-core/src/auth/Authenticated.spec.tsx b/packages/ra-core/src/auth/Authenticated.spec.tsx index e892d9856d1..f4bdb885e3a 100644 --- a/packages/ra-core/src/auth/Authenticated.spec.tsx +++ b/packages/ra-core/src/auth/Authenticated.spec.tsx @@ -1,12 +1,13 @@ import React from 'react'; import expect from 'expect'; import { cleanup, wait } from '@testing-library/react'; -import { push } from 'connected-react-router'; import Authenticated from './Authenticated'; import AuthContext from './AuthContext'; import renderWithRedux from '../util/renderWithRedux'; import { showNotification } from '../actions/notificationActions'; +import { createMemoryHistory } from 'history'; +import { Router } from 'react-router'; describe('', () => { afterEach(cleanup); @@ -32,17 +33,22 @@ describe('', () => { checkError: jest.fn().mockResolvedValue(''), getPermissions: jest.fn().mockResolvedValue(''), }; + + const history = createMemoryHistory(); + const { dispatch } = renderWithRedux( - - - - - + + + + + + + ); await wait(); expect(authProvider.checkAuth.mock.calls[0][0]).toEqual({}); expect(authProvider.logout.mock.calls[0][0]).toEqual({}); - expect(dispatch).toHaveBeenCalledTimes(3); + expect(dispatch).toHaveBeenCalledTimes(2); expect(dispatch.mock.calls[0][0]).toEqual( showNotification('ra.auth.auth_check_error', 'warning', { messageArgs: {}, @@ -50,8 +56,7 @@ describe('', () => { }) ); expect(dispatch.mock.calls[1][0]).toEqual({ type: 'RA/CLEAR_STATE' }); - expect(dispatch.mock.calls[2][0]).toEqual( - push({ pathname: '/login', state: { nextPathname: '/' } }) - ); + expect(history.location.pathname).toEqual('/login'); + expect(history.location.state).toEqual({ nextPathname: '/' }); }); }); diff --git a/packages/ra-core/src/auth/useAuthenticated.spec.tsx b/packages/ra-core/src/auth/useAuthenticated.spec.tsx index b199716b1bd..1b245c6f449 100644 --- a/packages/ra-core/src/auth/useAuthenticated.spec.tsx +++ b/packages/ra-core/src/auth/useAuthenticated.spec.tsx @@ -1,12 +1,13 @@ import React from 'react'; import expect from 'expect'; import { cleanup, wait } from '@testing-library/react'; -import { push } from 'connected-react-router'; import Authenticated from './Authenticated'; import AuthContext from './AuthContext'; import renderWithRedux from '../util/renderWithRedux'; import { showNotification } from '../actions/notificationActions'; +import { createMemoryHistory } from 'history'; +import { Router } from 'react-router'; describe('useAuthenticated', () => { afterEach(cleanup); @@ -74,17 +75,22 @@ describe('useAuthenticated', () => { checkError: jest.fn().mockResolvedValue(''), getPermissions: jest.fn().mockResolvedValue(''), }; + + const history = createMemoryHistory(); + const { dispatch } = renderWithRedux( - - - - - + + + + + + + ); await wait(); expect(authProvider.checkAuth.mock.calls[0][0]).toEqual({}); expect(authProvider.logout.mock.calls[0][0]).toEqual({}); - expect(dispatch).toHaveBeenCalledTimes(3); + expect(dispatch).toHaveBeenCalledTimes(2); expect(dispatch.mock.calls[0][0]).toEqual( showNotification('ra.auth.auth_check_error', 'warning', { messageArgs: {}, @@ -92,8 +98,7 @@ describe('useAuthenticated', () => { }) ); expect(dispatch.mock.calls[1][0]).toEqual({ type: 'RA/CLEAR_STATE' }); - expect(dispatch.mock.calls[2][0]).toEqual( - push({ pathname: '/login', state: { nextPathname: '/' } }) - ); + expect(history.location.pathname).toEqual('/login'); + expect(history.location.state).toEqual({ nextPathname: '/' }); }); }); diff --git a/packages/ra-core/src/auth/useLogin.ts b/packages/ra-core/src/auth/useLogin.ts index 5041b3a2026..2e5d1bcaacf 100644 --- a/packages/ra-core/src/auth/useLogin.ts +++ b/packages/ra-core/src/auth/useLogin.ts @@ -1,9 +1,7 @@ import { useCallback } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; -import { push } from 'connected-react-router'; import useAuthProvider, { defaultAuthParams } from './useAuthProvider'; -import { ReduxState } from '../types'; +import { useLocation, useHistory } from 'react-router'; /** * Get a callback for calling the authProvider.login() method @@ -30,28 +28,25 @@ import { ReduxState } from '../types'; */ const useLogin = (): Login => { const authProvider = useAuthProvider(); - const currentLocation = useSelector( - (state: ReduxState) => state.router.location - ); - const nextPathName = - currentLocation.state && currentLocation.state.nextPathname; - const dispatch = useDispatch(); + const location = useLocation(); + const history = useHistory(); + const nextPathName = location.state && location.state.nextPathname; const login = useCallback( (params: any = {}, pathName = defaultAuthParams.afterLoginUrl) => authProvider.login(params).then(ret => { - dispatch(push(nextPathName || pathName)); + history.push(nextPathName || pathName); return ret; }), - [authProvider, dispatch, nextPathName] + [authProvider, history, nextPathName] ); const loginWithoutProvider = useCallback( (_, __) => { - dispatch(push(defaultAuthParams.afterLoginUrl)); + history.push(defaultAuthParams.afterLoginUrl); return Promise.resolve(); }, - [dispatch] + [history] ); return authProvider ? login : loginWithoutProvider; diff --git a/packages/ra-core/src/auth/useLogout.ts b/packages/ra-core/src/auth/useLogout.ts index a7316f69b98..6986db55c6c 100644 --- a/packages/ra-core/src/auth/useLogout.ts +++ b/packages/ra-core/src/auth/useLogout.ts @@ -1,9 +1,9 @@ import { useCallback } from 'react'; import { useDispatch, useStore } from 'react-redux'; -import { push } from 'connected-react-router'; import useAuthProvider, { defaultAuthParams } from './useAuthProvider'; import { clearState } from '../actions/clearActions'; +import { useHistory, useLocation } from 'react-router'; /** * Get a callback for calling the authProvider.logout() method, @@ -25,57 +25,50 @@ import { clearState } from '../actions/clearActions'; */ const useLogout = (): Logout => { const authProvider = useAuthProvider(); + const dispatch = useDispatch(); + /** * We need the current location to pass in the router state * so that the login hook knows where to redirect to as next route after login. * - * But if we used useSelector to read it from the store, the logout function + * But if we used useLocation to get it, the logout function * would be rebuilt each time the user changes location. Consequently, that - * would force a rerender of the Logout button upon navigation. + * would force a rerender of all components using this hook upon navigation + * (CoreAdminRouter for example). * - * To avoid that, we don't subscribe to the store using useSelector; - * instead, we get a pointer to the store, and determine the location only - * after the logout function was called. + * To avoid that, we read the location directly from history which is mutable. + * See: https://reacttraining.com/react-router/web/api/history/history-is-mutable */ - - const store = useStore(); - const dispatch = useDispatch(); + const history = useHistory(); const logout = useCallback( (params = {}, redirectTo = defaultAuthParams.loginUrl) => authProvider.logout(params).then(redirectToFromProvider => { dispatch(clearState()); - const currentLocation = store.getState().router.location; - dispatch( - push({ - pathname: redirectToFromProvider || redirectTo, - state: { - nextPathname: - currentLocation && currentLocation.pathname, - }, - }) - ); + history.push({ + pathname: redirectToFromProvider || redirectTo, + state: { + nextPathname: + history.location && history.location.pathname, + }, + }); return redirectToFromProvider; }), - [authProvider, store, dispatch] + [authProvider, history, dispatch] ); const logoutWithoutProvider = useCallback( _ => { - const currentLocation = store.getState().router.location; - dispatch( - push({ - pathname: defaultAuthParams.loginUrl, - state: { - nextPathname: - currentLocation && currentLocation.pathname, - }, - }) - ); + history.push({ + pathname: defaultAuthParams.loginUrl, + state: { + nextPathname: history.location && history.location.pathname, + }, + }); dispatch(clearState()); return Promise.resolve(); }, - [store, dispatch] + [dispatch, history] ); return authProvider ? logout : logoutWithoutProvider; diff --git a/packages/ra-core/src/controller/useListParams.ts b/packages/ra-core/src/controller/useListParams.ts index 1237ada6f31..f1f57b7dfff 100644 --- a/packages/ra-core/src/controller/useListParams.ts +++ b/packages/ra-core/src/controller/useListParams.ts @@ -1,7 +1,6 @@ import { useCallback, useState, useMemo } from 'react'; import { useSelector, useDispatch, shallowEqual } from 'react-redux'; import { parse, stringify } from 'query-string'; -import { push } from 'connected-react-router'; import lodashDebounce from 'lodash/debounce'; import pickBy from 'lodash/pickBy'; import { Location } from 'history'; @@ -17,6 +16,7 @@ import { changeListParams, ListParams } from '../actions/listActions'; import { Sort, ReduxState } from '../types'; import removeEmpty from '../util/removeEmpty'; import removeKey from '../util/removeKey'; +import { useHistory } from 'react-router'; interface ListParamsOptions { resource: string; @@ -111,6 +111,7 @@ const useListParams = ({ }: ListParamsOptions): [Parameters, Modifiers] => { const [displayedFilters, setDisplayedFilters] = useState({}); const dispatch = useDispatch(); + const history = useHistory(); const { params } = useSelector( (reduxState: ReduxState) => reduxState.admin.resources[resource].list, @@ -140,14 +141,12 @@ const useListParams = ({ const changeParams = useCallback(action => { const newParams = queryReducer(query, action); - dispatch( - push({ - search: `?${stringify({ - ...newParams, - filter: JSON.stringify(newParams.filter), - })}`, - }) - ); + history.push({ + search: `?${stringify({ + ...newParams, + filter: JSON.stringify(newParams.filter), + })}`, + }); dispatch(changeListParams(resource, newParams)); }, requestSignature); // eslint-disable-line react-hooks/exhaustive-deps diff --git a/packages/ra-core/src/dataProvider/Mutation.spec.tsx b/packages/ra-core/src/dataProvider/Mutation.spec.tsx index 998d02d32ba..10b94c259d6 100644 --- a/packages/ra-core/src/dataProvider/Mutation.spec.tsx +++ b/packages/ra-core/src/dataProvider/Mutation.spec.tsx @@ -7,7 +7,6 @@ import { render, } from '@testing-library/react'; import expect from 'expect'; -import { push } from 'connected-react-router'; import Mutation from './Mutation'; import renderWithRedux from '../util/renderWithRedux'; @@ -15,6 +14,7 @@ import { showNotification, refreshView, setListSelectedIds } from '../actions'; import DataProviderContext from './DataProviderContext'; import TestContext from '../util/TestContext'; import { useNotify } from '../sideEffect'; +import { History } from 'history'; describe('Mutation', () => { afterEach(cleanup); @@ -52,6 +52,8 @@ describe('Mutation', () => { it('supports declarative onSuccess side effects', async () => { let dispatchSpy; + let historyForAssertions: History; + const dataProvider = { mytype: jest.fn(() => Promise.resolve({ data: { foo: 'bar' } })), }; @@ -61,8 +63,9 @@ describe('Mutation', () => { const res = render( - {({ store }) => { + {({ store, history }) => { dispatchSpy = jest.spyOn(store, 'dispatch'); + historyForAssertions = history; return ( { undoable: false, }) ); - expect(dispatchSpy).toHaveBeenCalledWith(push('/a_path')); + expect(historyForAssertions.location.pathname).toEqual('/a_path'); expect(dispatchSpy).toHaveBeenCalledWith(refreshView()); expect(dispatchSpy).toHaveBeenCalledWith(setListSelectedIds('foo', [])); }); @@ -166,6 +169,8 @@ describe('Mutation', () => { it('supports declarative onFailure side effects', async () => { let dispatchSpy; + let historyForAssertions: History; + const dataProvider = { mytype: jest.fn(() => Promise.reject({ message: 'provider error' }) @@ -177,8 +182,9 @@ describe('Mutation', () => { const res = render( - {({ store }) => { + {({ store, history }) => { dispatchSpy = jest.spyOn(store, 'dispatch'); + historyForAssertions = history; return ( { undoable: false, }) ); - expect(dispatchSpy).toHaveBeenCalledWith(push('/a_path')); + expect(historyForAssertions.location.pathname).toEqual('/a_path'); expect(dispatchSpy).toHaveBeenCalledWith(refreshView()); expect(dispatchSpy).toHaveBeenCalledWith(setListSelectedIds('foo', [])); }); diff --git a/packages/ra-core/src/dataProvider/Query.spec.tsx b/packages/ra-core/src/dataProvider/Query.spec.tsx index 0d74558eebf..1c98bfac946 100644 --- a/packages/ra-core/src/dataProvider/Query.spec.tsx +++ b/packages/ra-core/src/dataProvider/Query.spec.tsx @@ -7,7 +7,6 @@ import { waitForDomChange, } from '@testing-library/react'; import expect from 'expect'; -import { push } from 'connected-react-router'; import Query from './Query'; import CoreAdmin from '../CoreAdmin'; @@ -17,6 +16,7 @@ import TestContext from '../util/TestContext'; import DataProviderContext from './DataProviderContext'; import { showNotification, refreshView, setListSelectedIds } from '../actions'; import { useNotify } from '../sideEffect'; +import { History } from 'history'; describe('Query', () => { afterEach(cleanup); @@ -261,6 +261,8 @@ describe('Query', () => { it('supports declarative onSuccess side effects', async () => { expect.assertions(4); let dispatchSpy; + let historyForAssertions: History; + const dataProvider = { getList: jest.fn(() => Promise.resolve({ data: [{ id: 1, foo: 'bar' }], total: 42 }) @@ -272,8 +274,9 @@ describe('Query', () => { const res = render( - {({ store }) => { + {({ store, history }) => { dispatchSpy = jest.spyOn(store, 'dispatch'); + historyForAssertions = history; return ( { undoable: false, }) ); - expect(dispatchSpy).toHaveBeenCalledWith(push('/a_path')); + expect(historyForAssertions.location.pathname).toEqual('/a_path'); expect(dispatchSpy).toHaveBeenCalledWith(refreshView()); expect(dispatchSpy).toHaveBeenCalledWith(setListSelectedIds('foo', [])); }); @@ -382,6 +385,8 @@ describe('Query', () => { it('supports declarative onFailure side effects', async () => { let dispatchSpy; + let historyForAssertions: History; + const dataProvider = { getList: jest.fn(() => Promise.reject({ message: 'provider error' }) @@ -393,7 +398,8 @@ describe('Query', () => { const res = render( - {({ store }) => { + {({ store, history }) => { + historyForAssertions = history; dispatchSpy = jest.spyOn(store, 'dispatch'); return ( { undoable: false, }) ); - expect(dispatchSpy).toHaveBeenCalledWith(push('/a_path')); + expect(historyForAssertions.location.pathname).toEqual('/a_path'); expect(dispatchSpy).toHaveBeenCalledWith(refreshView()); expect(dispatchSpy).toHaveBeenCalledWith(setListSelectedIds('foo', [])); }); diff --git a/packages/ra-core/src/reducer/admin/index.ts b/packages/ra-core/src/reducer/admin/index.ts index 15311e725f9..052420938fa 100644 --- a/packages/ra-core/src/reducer/admin/index.ts +++ b/packages/ra-core/src/reducer/admin/index.ts @@ -9,7 +9,6 @@ import record from './record'; import references, { getPossibleReferenceValues as referencesGetPossibleReferenceValues, } from './references'; -import saving from './saving'; import ui from './ui'; import customQueries from './customQueries'; @@ -20,7 +19,6 @@ export default combineReducers({ notifications, record, references, - saving, ui, }); diff --git a/packages/ra-core/src/reducer/admin/saving.ts b/packages/ra-core/src/reducer/admin/saving.ts deleted file mode 100644 index 51b80af2516..00000000000 --- a/packages/ra-core/src/reducer/admin/saving.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { LOCATION_CHANGE } from 'connected-react-router'; -import { - CRUD_CREATE, - CRUD_CREATE_SUCCESS, - CRUD_CREATE_FAILURE, - CRUD_UPDATE, - CRUD_UPDATE_SUCCESS, - CRUD_UPDATE_FAILURE, -} from '../../actions'; - -export default (previousState = false, { type, meta }) => { - switch (type) { - case CRUD_CREATE: - case CRUD_UPDATE: - return { - redirect: meta.onSuccess && meta.onSuccess.redirectTo, - }; - case LOCATION_CHANGE: - case CRUD_CREATE_SUCCESS: - case CRUD_CREATE_FAILURE: - case CRUD_UPDATE_SUCCESS: - case CRUD_UPDATE_FAILURE: - return false; - default: - return previousState; - } -}; diff --git a/packages/ra-core/src/sideEffect/useRedirect.ts b/packages/ra-core/src/sideEffect/useRedirect.ts index af1807bf315..cecc173ff14 100644 --- a/packages/ra-core/src/sideEffect/useRedirect.ts +++ b/packages/ra-core/src/sideEffect/useRedirect.ts @@ -1,10 +1,10 @@ import { useCallback } from 'react'; -import { push } from 'connected-react-router'; import { useDispatch } from 'react-redux'; import { Identifier, Record } from '../types'; import resolveRedirectTo from '../util/resolveRedirectTo'; import { refreshView } from '../actions/uiActions'; +import { useHistory } from 'react-router'; type RedirectToFunction = ( basePath?: string, @@ -31,6 +31,7 @@ export type RedirectionSideEffect = string | false | RedirectToFunction; */ const useRedirect = () => { const dispatch = useDispatch(); + const history = useHistory(); return useCallback( ( redirectTo: RedirectionSideEffect, @@ -43,9 +44,9 @@ const useRedirect = () => { return; } - dispatch(push(resolveRedirectTo(redirectTo, basePath, id, data))); + history.push(resolveRedirectTo(redirectTo, basePath, id, data)); }, - [dispatch] + [dispatch, history] ); }; diff --git a/packages/ra-core/src/util/TestContext.spec.tsx b/packages/ra-core/src/util/TestContext.spec.tsx index 866f11f8dc0..860a6918818 100644 --- a/packages/ra-core/src/util/TestContext.spec.tsx +++ b/packages/ra-core/src/util/TestContext.spec.tsx @@ -15,7 +15,6 @@ const primedStore = { possibleValues: {}, }, resources: {}, - saving: false, ui: { viewVersion: 1, }, diff --git a/packages/ra-core/src/util/TestContext.tsx b/packages/ra-core/src/util/TestContext.tsx index 86d2c2b6fc3..cc218cb495c 100644 --- a/packages/ra-core/src/util/TestContext.tsx +++ b/packages/ra-core/src/util/TestContext.tsx @@ -1,11 +1,13 @@ -import React, { Component } from 'react'; -import { createStore } from 'redux'; +import React, { Component, ReactNode } from 'react'; +import { createStore, Store } from 'redux'; import { Provider } from 'react-redux'; import merge from 'lodash/merge'; -import { createMemoryHistory } from 'history'; +import { createMemoryHistory, History } from 'history'; +import { Router } from 'react-router'; import createAdminStore from '../createAdminStore'; import { convertLegacyDataProvider } from '../dataProvider'; +import { ReduxState } from '../types'; export const defaultStore = { admin: { @@ -15,9 +17,18 @@ export const defaultStore = { }, }; +type ChildrenFunction = ({ + store, + history, +}: { + store: Store; + history: History; +}) => ReactNode; + interface Props { initialState?: object; enableReducers?: boolean; + children: ReactNode | ChildrenFunction; } const dataProviderDefaultResponse = { data: null }; @@ -48,9 +59,11 @@ const dataProviderDefaultResponse = { data: null }; */ class TestContext extends Component { storeWithDefault = null; + history: History = null; constructor(props) { super(props); + this.history = props.history || createMemoryHistory(); const { initialState = {}, enableReducers = false } = props; this.storeWithDefault = enableReducers @@ -67,14 +80,17 @@ class TestContext extends Component { renderChildren = () => { const { children } = this.props; return typeof children === 'function' - ? children({ store: this.storeWithDefault }) + ? (children as ChildrenFunction)({ + store: this.storeWithDefault, + history: this.history, + }) : children; }; render() { return ( - {this.renderChildren()} + {this.renderChildren()} ); } diff --git a/packages/ra-realtime/package.json b/packages/ra-realtime/package.json index 8e9f9a2f7b2..75bf449463b 100644 --- a/packages/ra-realtime/package.json +++ b/packages/ra-realtime/package.json @@ -35,16 +35,16 @@ "lodash": "~4.17.5" }, "peerDependencies": { - "connected-react-router": "^6.4.0", + "connected-react-router": "^6.5.2", "ra-core": "^3.0.0-alpha.0", "react-admin": "^2.5.3", - "react-router": "^4.2.0", + "react-router": "^5.1.0", "redux-saga": "^1.0.0" }, "devDependencies": { - "connected-react-router": "^6.4.0", + "connected-react-router": "^6.5.2", "cross-env": "^5.2.0", - "react-router": "^4.2.0", + "react-router": "^5.1.0", "redux-saga": "^1.0.0", "rimraf": "^2.6.3" } diff --git a/packages/ra-ui-materialui/package.json b/packages/ra-ui-materialui/package.json index 71d41743716..4e12ebfbb0a 100644 --- a/packages/ra-ui-materialui/package.json +++ b/packages/ra-ui-materialui/package.json @@ -47,7 +47,7 @@ "@material-ui/styles": "^4.3.3", "autosuggest-highlight": "^3.1.1", "classnames": "~2.2.5", - "connected-react-router": "^6.4.0", + "connected-react-router": "^6.5.2", "css-mediaquery": "^0.1.2", "downshift": "3.2.7", "final-form": "^4.18.5", @@ -61,8 +61,8 @@ "react-final-form": "^6.3.0", "react-final-form-arrays": "^3.1.1", "react-redux": "^7.1.0", - "react-router": "^5.0.1", - "react-router-dom": "^5.0.1", + "react-router": "^5.1.0", + "react-router-dom": "^5.1.0", "react-transition-group": "^2.2.1", "recompose": "~0.26.0", "redux": "^3.7.2 || ^4.0.3" diff --git a/packages/ra-ui-materialui/src/auth/Login.tsx b/packages/ra-ui-materialui/src/auth/Login.tsx index 4392df72778..6c15ba07214 100644 --- a/packages/ra-ui-materialui/src/auth/Login.tsx +++ b/packages/ra-ui-materialui/src/auth/Login.tsx @@ -16,9 +16,7 @@ import { } from '@material-ui/core'; import { ThemeProvider } from '@material-ui/styles'; import LockIcon from '@material-ui/icons/Lock'; -import { StaticContext } from 'react-router'; -import { push } from 'connected-react-router'; -import { useDispatch } from 'react-redux'; +import { StaticContext, useHistory } from 'react-router'; import { useCheckAuth } from 'ra-core'; import defaultTheme from '../defaultTheme'; @@ -90,17 +88,17 @@ const Login: React.FunctionComponent< const muiTheme = useMemo(() => createMuiTheme(theme), [theme]); let backgroundImageLoaded = false; const checkAuth = useCheckAuth(); - const dispatch = useDispatch(); + const history = useHistory(); useEffect(() => { checkAuth({}, false) .then(() => { // already authenticated, redirect to the home page - dispatch(push('/')); + history.push('/'); }) .catch(() => { // not authenticated, stay on the login page }); - }, [checkAuth, dispatch]); + }, [checkAuth, history]); const updateBackgroundImage = () => { if (!backgroundImageLoaded && containerRef.current) { diff --git a/packages/ra-ui-materialui/src/button/CloneButton.js b/packages/ra-ui-materialui/src/button/CloneButton.js index 9af3d13c1a5..5fa1802d6f4 100644 --- a/packages/ra-ui-materialui/src/button/CloneButton.js +++ b/packages/ra-ui-materialui/src/button/CloneButton.js @@ -34,7 +34,7 @@ export const CloneButton = ({ component={Link} to={{ pathname: `${basePath}/create`, - search: stringify(omitId(record)), // FIXME use location state when https://github.com/supasate/connected-react-router/issues/301 is fixed + search: stringify(omitId(record)), }} label={label} onClick={stopPropagation} diff --git a/packages/ra-ui-materialui/src/detail/Create.js b/packages/ra-ui-materialui/src/detail/Create.js index c2a35d9fd26..93b83277cfc 100644 --- a/packages/ra-ui-materialui/src/detail/Create.js +++ b/packages/ra-ui-materialui/src/detail/Create.js @@ -122,6 +122,7 @@ export const CreateView = props => { redirect, resource, save, + saving, title, version, ...rest @@ -161,6 +162,7 @@ export const CreateView = props => { : children.props.redirect, resource, save, + saving, version, })} @@ -170,6 +172,7 @@ export const CreateView = props => { record, resource, save, + saving, version, })}
diff --git a/packages/ra-ui-materialui/src/detail/Edit.js b/packages/ra-ui-materialui/src/detail/Edit.js index 4966ef19a3d..5c39955a247 100644 --- a/packages/ra-ui-materialui/src/detail/Edit.js +++ b/packages/ra-ui-materialui/src/detail/Edit.js @@ -121,6 +121,7 @@ export const EditView = ({ redirect, resource, save, + saving, title, undoable, version, @@ -169,6 +170,7 @@ export const EditView = ({ : children.props.redirect, resource, save, + saving, undoable, version, }) @@ -183,6 +185,7 @@ export const EditView = ({ resource, version, save, + saving, })} diff --git a/packages/ra-ui-materialui/src/detail/TabbedShowLayout.js b/packages/ra-ui-materialui/src/detail/TabbedShowLayout.js index a11202dfe88..4caaa5062bf 100644 --- a/packages/ra-ui-materialui/src/detail/TabbedShowLayout.js +++ b/packages/ra-ui-materialui/src/detail/TabbedShowLayout.js @@ -1,10 +1,11 @@ import React, { Children, cloneElement, isValidElement } from 'react'; import PropTypes from 'prop-types'; import Divider from '@material-ui/core/Divider'; -import { withRouter, Route } from 'react-router-dom'; +import { Route } from 'react-router-dom'; import { makeStyles } from '@material-ui/core/styles'; -import TabbedShowLayoutTabs from './TabbedShowLayoutTabs'; +import TabbedShowLayoutTabs, { getTabFullPath } from './TabbedShowLayoutTabs'; +import { useRouteMatch } from 'react-router'; const sanitizeRestProps = ({ children, @@ -20,11 +21,6 @@ const sanitizeRestProps = ({ ...rest }) => rest; -const getTabFullPath = (tab, index, baseUrl) => - `${baseUrl}${ - tab.props.path ? `/${tab.props.path}` : index > 0 ? `/${index}` : '' - }`; - const useStyles = makeStyles(theme => ({ content: { paddingTop: theme.spacing(1), @@ -77,8 +73,6 @@ const TabbedShowLayout = ({ children, classes: classesOverride, className, - location, - match, record, resource, version, @@ -86,19 +80,12 @@ const TabbedShowLayout = ({ tabs, ...rest }) => { + const match = useRouteMatch(); + const classes = useStyles({ classes: classesOverride }); return (
- {cloneElement( - tabs, - { - // The location pathname will contain the page path including the current tab path - // so we can use it as a way to determine the current tab - value: location.pathname, - match, - }, - children - )} + {cloneElement(tabs, {}, children)}
@@ -140,4 +127,4 @@ TabbedShowLayout.defaultProps = { tabs: , }; -export default withRouter(TabbedShowLayout); +export default TabbedShowLayout; diff --git a/packages/ra-ui-materialui/src/detail/TabbedShowLayoutTabs.js b/packages/ra-ui-materialui/src/detail/TabbedShowLayoutTabs.js index 639164691be..8639209962d 100644 --- a/packages/ra-ui-materialui/src/detail/TabbedShowLayoutTabs.js +++ b/packages/ra-ui-materialui/src/detail/TabbedShowLayoutTabs.js @@ -1,34 +1,42 @@ import React, { Children, cloneElement, isValidElement } from 'react'; import PropTypes from 'prop-types'; import Tabs from '@material-ui/core/Tabs'; +import { useLocation, useRouteMatch } from 'react-router'; -const getTabFullPath = (tab, index, baseUrl) => +export const getTabFullPath = (tab, index, baseUrl) => `${baseUrl}${ tab.props.path ? `/${tab.props.path}` : index > 0 ? `/${index}` : '' }`; -const TabbedShowLayoutTabs = ({ children, match, value, ...rest }) => ( - - {Children.map(children, (tab, index) => { - if (!tab || !isValidElement(tab)) return null; - // Builds the full tab tab which is the concatenation of the last matched route in the - // TabbedShowLayout hierarchy (ex: '/posts/create', '/posts/12', , '/posts/12/show') - // and the tab path. - // This will be used as the Tab's value - const tabPath = getTabFullPath(tab, index, match.url); +const TabbedShowLayoutTabs = ({ children, ...rest }) => { + const location = useLocation(); + const match = useRouteMatch(); - return cloneElement(tab, { - context: 'header', - value: tabPath, - }); - })} - -); + // The location pathname will contain the page path including the current tab path + // so we can use it as a way to determine the current tab + const value = location.pathname; + + return ( + + {Children.map(children, (tab, index) => { + if (!tab || !isValidElement(tab)) return null; + // Builds the full tab tab which is the concatenation of the last matched route in the + // TabbedShowLayout hierarchy (ex: '/posts/create', '/posts/12', , '/posts/12/show') + // and the tab path. + // This will be used as the Tab's value + const tabPath = getTabFullPath(tab, index, match.url); + + return cloneElement(tab, { + context: 'header', + value: tabPath, + }); + })} + + ); +}; TabbedShowLayoutTabs.propTypes = { children: PropTypes.node, - match: PropTypes.object, - value: PropTypes.string, }; export default TabbedShowLayoutTabs; diff --git a/packages/ra-ui-materialui/src/field/ArrayField.spec.js b/packages/ra-ui-materialui/src/field/ArrayField.spec.js index 8cabc742437..1534945bc2f 100644 --- a/packages/ra-ui-materialui/src/field/ArrayField.spec.js +++ b/packages/ra-ui-materialui/src/field/ArrayField.spec.js @@ -1,92 +1,60 @@ import React from 'react'; -import { shallow, mount } from 'enzyme'; -import { createStore } from 'redux'; -import { Provider } from 'react-redux'; +import { render, cleanup } from '@testing-library/react'; import { ArrayField } from './ArrayField'; import NumberField from './NumberField'; import TextField from './TextField'; import Datagrid from '../list/Datagrid'; +import { TestContext } from 'ra-core'; describe('', () => { + afterEach(cleanup); + + const DummyIterator = props => ( + + + + + ); + it('should not fail for empty records', () => { - const IteratorMock = jest.fn(); - shallow( - - - - ).dive(); - expect(IteratorMock.mock.calls.length).toBe(1); - expect(IteratorMock.mock.calls[0][0]).toMatchObject({ - data: {}, - ids: [], - }); - }); + const { queryByText } = render( + + + + + + ); - it('should pass the embedded array as data and ids props to child', () => { - const IteratorMock = jest.fn(); - shallow( - - - - ).dive(); - expect(IteratorMock.mock.calls.length).toBe(1); - expect(IteratorMock.mock.calls[0][0]).toMatchObject({ - data: { - '{"id":123,"foo":"bar"}': { foo: 'bar', id: 123 }, - '{"id":456,"foo":"baz"}': { foo: 'baz', id: 456 }, - }, - ids: ['{"id":123,"foo":"bar"}', '{"id":456,"foo":"baz"}'], - }); + // Test the datagrid know about the fields + expect(queryByText('resources.posts.fields.id')).not.toBeNull(); + expect(queryByText('resources.posts.fields.foo')).not.toBeNull(); }); it('should render the underlying iterator component', () => { - const Dummy = () => ( - - - - - - + const { queryByText } = render( + + + + + ); - const wrapper = mount( - ({}))}> - - - ); - expect( - wrapper - .find('DatagridRow TextField span') - .at(0) - .text() - ).toBe('bar'); - expect( - wrapper - .find('DatagridRow NumberField span') - .at(0) - .text() - ).toBe('123'); - expect( - wrapper - .find('DatagridRow TextField span') - .at(1) - .text() - ).toBe('baz'); - expect( - wrapper - .find('DatagridRow NumberField span') - .at(1) - .text() - ).toBe('456'); + + // Test the datagrid know about the fields + expect(queryByText('resources.posts.fields.id')).not.toBeNull(); + expect(queryByText('resources.posts.fields.foo')).not.toBeNull(); + + // Test the fields values + expect(queryByText('bar')).not.toBeNull(); + expect(queryByText('123')).not.toBeNull(); + + expect(queryByText('baz')).not.toBeNull(); + expect(queryByText('456')).not.toBeNull(); }); }); diff --git a/packages/ra-ui-materialui/src/form/SimpleForm.js b/packages/ra-ui-materialui/src/form/SimpleForm.js index 326a31daaa6..9ae37e8c6ea 100644 --- a/packages/ra-ui-materialui/src/form/SimpleForm.js +++ b/packages/ra-ui-materialui/src/form/SimpleForm.js @@ -2,7 +2,6 @@ import React, { Children, useCallback, useRef } from 'react'; import PropTypes from 'prop-types'; import { Form } from 'react-final-form'; import arrayMutators from 'final-form-arrays'; -import { useSelector } from 'react-redux'; import classnames from 'classnames'; import { useTranslate, useInitializeFormWithRecord } from 'ra-core'; @@ -10,7 +9,7 @@ import FormInput from './FormInput'; import Toolbar from './Toolbar'; import CardContentInner from '../layout/CardContentInner'; -const SimpleForm = ({ initialValues, ...props }) => { +const SimpleForm = ({ initialValues, saving, ...props }) => { let redirect = useRef(props.redirect); // We don't use state here for two reasons: // 1. There no way to execute code only after the state has been updated @@ -19,7 +18,6 @@ const SimpleForm = ({ initialValues, ...props }) => { redirect.current = newRedirect; }; - const saving = useSelector(state => state.admin.saving); const translate = useTranslate(); const submit = values => { const finalRedirect = diff --git a/packages/ra-ui-materialui/src/form/TabbedForm.js b/packages/ra-ui-materialui/src/form/TabbedForm.js index f782162d3a3..1f976e5eae7 100644 --- a/packages/ra-ui-materialui/src/form/TabbedForm.js +++ b/packages/ra-ui-materialui/src/form/TabbedForm.js @@ -3,14 +3,14 @@ import PropTypes from 'prop-types'; import classnames from 'classnames'; import { Form } from 'react-final-form'; import arrayMutators from 'final-form-arrays'; -import { useSelector } from 'react-redux'; -import { withRouter, Route } from 'react-router-dom'; +import { Route } from 'react-router-dom'; import Divider from '@material-ui/core/Divider'; import { makeStyles } from '@material-ui/core/styles'; import { useTranslate, useInitializeFormWithRecord } from 'ra-core'; import Toolbar from './Toolbar'; -import TabbedFormTabs from './TabbedFormTabs'; +import TabbedFormTabs, { getTabFullPath } from './TabbedFormTabs'; +import { useRouteMatch, useLocation } from 'react-router'; const useStyles = makeStyles(theme => ({ errorTabButton: { color: theme.palette.error.main }, @@ -21,7 +21,7 @@ const useStyles = makeStyles(theme => ({ }, })); -const TabbedForm = ({ initialValues, ...props }) => { +const TabbedForm = ({ initialValues, saving, ...props }) => { let redirect = useRef(props.redirect); // We don't use state here for two reasons: // 1. There no way to execute code only after the state has been updated @@ -29,7 +29,7 @@ const TabbedForm = ({ initialValues, ...props }) => { const setRedirect = newRedirect => { redirect.current = newRedirect; }; - const saving = useSelector(state => state.admin.saving); + const translate = useTranslate(); const classes = useStyles(); @@ -75,7 +75,7 @@ const defaultSubscription = { invalid: true, }; -export default withRouter(TabbedForm); +export default TabbedForm; export const TabbedFormView = ({ basePath, @@ -85,8 +85,6 @@ export const TabbedFormView = ({ form, handleSubmit, invalid, - location, - match, pristine, record, redirect: defaultRedirect, @@ -116,6 +114,9 @@ export const TabbedFormView = ({ const tabsWithErrors = findTabsWithErrors(children, form.getState().errors); + const match = useRouteMatch(); + const location = useLocation(); + const url = match ? match.url : location.pathname; return (
props; -export const getTabFullPath = (tab, index, baseUrl) => - `${baseUrl}${ - tab.props.path ? `/${tab.props.path}` : index > 0 ? `/${index}` : '' - }`; - export const findTabsWithErrors = (children, errors) => { return Children.toArray(children).reduce((acc, child) => { if (!isValidElement(child)) { diff --git a/packages/ra-ui-materialui/src/form/TabbedFormTabs.js b/packages/ra-ui-materialui/src/form/TabbedFormTabs.js index 3fd5f62810e..c1b8b7b5053 100644 --- a/packages/ra-ui-materialui/src/form/TabbedFormTabs.js +++ b/packages/ra-ui-materialui/src/form/TabbedFormTabs.js @@ -1,8 +1,9 @@ import React, { Children, cloneElement, isValidElement } from 'react'; import PropTypes from 'prop-types'; import Tabs from '@material-ui/core/Tabs'; +import { useLocation } from 'react-router'; -const getTabFullPath = (tab, index, baseUrl) => +export const getTabFullPath = (tab, index, baseUrl) => `${baseUrl}${ tab.props.path ? `/${tab.props.path}` : index > 0 ? `/${index}` : '' }`; @@ -10,11 +11,12 @@ const getTabFullPath = (tab, index, baseUrl) => const TabbedFormTabs = ({ children, classes, - currentLocationPath, url, tabsWithErrors, ...rest }) => { + const location = useLocation(); + const validTabPaths = Children.toArray(children).map((tab, index) => getTabFullPath(tab, index, url) ); @@ -26,8 +28,8 @@ const TabbedFormTabs = ({ // available tab. The current location will be applied again on the // first render containing the targeted tab. This is almost transparent // for the user who may just see an short tab selection animation - const tabValue = validTabPaths.includes(currentLocationPath) - ? currentLocationPath + const tabValue = validTabPaths.includes(location.pathname) + ? location.pathname : validTabPaths[0]; return ( @@ -46,7 +48,7 @@ const TabbedFormTabs = ({ value: tabPath, className: tabsWithErrors.includes(tab.props.label) && - currentLocationPath !== tabPath + location.pathname !== tabPath ? classes.errorTabButton : null, }); @@ -58,7 +60,6 @@ const TabbedFormTabs = ({ TabbedFormTabs.propTypes = { children: PropTypes.node, classes: PropTypes.object, - currentLocationPath: PropTypes.string, url: PropTypes.string, tabsWithErrors: PropTypes.arrayOf(PropTypes.string), }; diff --git a/packages/ra-ui-materialui/src/form/TabbedFormTabs.spec.js b/packages/ra-ui-materialui/src/form/TabbedFormTabs.spec.js index 7bd3c53b004..6ea5dd2adf4 100644 --- a/packages/ra-ui-materialui/src/form/TabbedFormTabs.spec.js +++ b/packages/ra-ui-materialui/src/form/TabbedFormTabs.spec.js @@ -1,50 +1,48 @@ import React from 'react'; -import { shallow } from 'enzyme'; -import assert from 'assert'; +import { render, cleanup } from '@testing-library/react'; import TabbedFormTabs from './TabbedFormTabs'; import FormTab from './FormTab'; +import { MemoryRouter } from 'react-router'; describe('', () => { + afterEach(cleanup); + it('should set the style of an inactive Tab button with errors', () => { - const wrapper = shallow( - - - - + const { getAllByRole } = render( + + + + + + ); - const tabs = wrapper.find(FormTab); - const tab1 = tabs.at(0); - const tab2 = tabs.at(1); - - assert.equal(tab1.prop('className'), null); - assert.equal(tab2.prop('className'), 'error'); + const tabs = getAllByRole('tab'); + expect(tabs[0].classList.contains('error')).toEqual(false); + expect(tabs[1].classList.contains('error')).toEqual(true); }); it('should not set the style of an active Tab button with errors', () => { - const wrapper = shallow( - - - - + const { getAllByRole } = render( + + + + + + ); - const tabs = wrapper.find(FormTab); - const tab1 = tabs.at(0); - const tab2 = tabs.at(1); - - assert.equal(tab1.prop('className'), null); - assert.equal(tab2.prop('className'), null); + const tabs = getAllByRole('tab'); + expect(tabs[0].classList.contains('error')).toEqual(false); + expect(tabs[1].classList.contains('error')).toEqual(false); }); }); diff --git a/packages/ra-ui-materialui/src/list/DatagridRow.js b/packages/ra-ui-materialui/src/list/DatagridRow.js index e57abeb81dd..283694ebf55 100644 --- a/packages/ra-ui-materialui/src/list/DatagridRow.js +++ b/packages/ra-ui-materialui/src/list/DatagridRow.js @@ -11,13 +11,12 @@ import React, { import isEqual from 'lodash/isEqual'; import PropTypes from 'prop-types'; import classnames from 'classnames'; -import { useDispatch } from 'react-redux'; -import { push } from 'connected-react-router'; import { TableCell, TableRow, Checkbox } from '@material-ui/core'; import { linkToRecord } from 'ra-core'; import DatagridCell from './DatagridCell'; import ExpandRowButton from './ExpandRowButton'; +import { useHistory } from 'react-router'; const computeNbColumns = (expand, children, hasBulkActions) => expand @@ -58,7 +57,8 @@ const DatagridRow = ({ setNbColumns(newNbColumns); } }, [expand, nbColumns, children, hasBulkActions]); - const dispatch = useDispatch(); + + const history = useHistory(); const handleToggleExpand = useCallback( event => { @@ -83,10 +83,10 @@ const DatagridRow = ({ : rowClick; switch (effect) { case 'edit': - dispatch(push(linkToRecord(basePath, id))); + history.push(linkToRecord(basePath, id)); return; case 'show': - dispatch(push(linkToRecord(basePath, id, 'show'))); + history.push(linkToRecord(basePath, id, 'show')); return; case 'expand': handleToggleExpand(event); @@ -95,13 +95,13 @@ const DatagridRow = ({ handleToggleSelection(event); return; default: - if (effect) dispatch(push(effect)); + if (effect) history.push(effect); return; } }, [ basePath, - dispatch, + history, handleToggleExpand, handleToggleSelection, id, diff --git a/packages/ra-ui-materialui/src/list/DatagridRow.spec.js b/packages/ra-ui-materialui/src/list/DatagridRow.spec.js index 1ca88aa4e9e..76db50db86f 100644 --- a/packages/ra-ui-materialui/src/list/DatagridRow.spec.js +++ b/packages/ra-ui-materialui/src/list/DatagridRow.spec.js @@ -4,6 +4,8 @@ import { push } from 'connected-react-router'; import { renderWithRedux, linkToRecord } from 'ra-core'; import DatagridRow from './DatagridRow'; +import { createMemoryHistory } from 'history'; +import { Router } from 'react-router'; const TitleField = ({ record }) => {record.title}; const ExpandPanel = () => expanded; @@ -26,35 +28,42 @@ describe('', () => { record: { id: 15, title: 'hello' }, }; + const renderWithRouter = children => { + const history = createMemoryHistory(); + + return { + history, + ...render({children}), + }; + }; + describe('rowClick', () => { it("should redirect to edit page if the 'edit' option is selected", () => { - const { getByText, dispatch } = render( + const { getByText, history } = renderWithRouter( ); fireEvent.click(getByText('hello')); - expect(dispatch.mock.calls[0][0]).toEqual( - push(linkToRecord(defaultProps.basePath, defaultProps.id)) + expect(history.location.pathname).toEqual( + linkToRecord(defaultProps.basePath, defaultProps.id) ); }); it("should redirect to show page if the 'show' option is selected", () => { - const { getByText, dispatch } = render( + const { getByText, history } = renderWithRouter( ); fireEvent.click(getByText('hello')); - expect(dispatch.mock.calls[0][0]).toEqual( - push( - linkToRecord(defaultProps.basePath, defaultProps.id, 'show') - ) + expect(history.location.pathname).toEqual( + linkToRecord(defaultProps.basePath, defaultProps.id, 'show') ); }); it("should change the expand state if the 'expand' option is selected", () => { - const { queryAllByText, getByText } = render( + const { queryAllByText, getByText } = renderWithRouter( ', () => { it("should execute the onToggleItem function if the 'toggleSelection' option is selected", () => { const onToggleItem = jest.fn(); - const { getByText } = render( + const { getByText } = renderWithRouter( ', () => { it('should redirect to the custom path if onRowClick is a string', () => { const path = '/foo/bar'; - const { getByText, dispatch } = render( + const { getByText, history } = renderWithRouter( ); fireEvent.click(getByText('hello')); - expect(dispatch.mock.calls[0][0]).toEqual(push(path)); + expect(history.location.pathname).toEqual(path); }); it('should evaluate the function and redirect to the result of that function if onRowClick is a custom function', async () => { const customRowClick = () => '/bar/foo'; - const { getByText, dispatch } = render( + const { getByText, history } = renderWithRouter( ); fireEvent.click(getByText('hello')); await wait(); // wait one tick - expect(dispatch.mock.calls[0][0]).toEqual(push('/bar/foo')); + expect(history.location.pathname).toEqual('/bar/foo'); }); it('should not call push if onRowClick is falsy', () => { - const { getByText, dispatch } = render( + const { getByText, history } = renderWithRouter( ); fireEvent.click(getByText('hello')); - expect(dispatch.mock.calls).toHaveLength(0); + expect(history.location.pathname).toEqual('/'); }); }); }); diff --git a/yarn.lock b/yarn.lock index 9bb499886c0..c583ef929ea 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1480,16 +1480,16 @@ hoist-non-react-statics "^3.3.0" redux "^4.0.0" -"@types/react-router-dom@^4.3.1": - version "4.3.5" - resolved "https://registry.yarnpkg.com/@types/react-router-dom/-/react-router-dom-4.3.5.tgz#72f229967690c890d00f96e6b85e9ee5780db31f" - integrity sha512-eFajSUASYbPHg2BDM1G8Btx+YqGgvROPIg6sBhl3O4kbDdYXdFdfrgQFf/pcBuQVObjfT9AL/dd15jilR5DIEA== +"@types/react-router-dom@^5.1.0": + version "5.1.0" + resolved "https://registry.yarnpkg.com/@types/react-router-dom/-/react-router-dom-5.1.0.tgz#8baa84a7fa8c8e7797fb3650ca51f93038cb4caf" + integrity sha512-YCh8r71pL5p8qDwQf59IU13hFy/41fDQG/GeOI3y+xmD4o0w3vEPxE8uBe+dvOgMoDl0W1WUZsWH0pxc1mcZyQ== dependencies: "@types/history" "*" "@types/react" "*" "@types/react-router" "*" -"@types/react-router@*", "@types/react-router@^5.0.1": +"@types/react-router@*": version "5.0.3" resolved "https://registry.yarnpkg.com/@types/react-router/-/react-router-5.0.3.tgz#855a1606e62de3f4d69ea34fb3c0e50e98e964d5" integrity sha512-j2Gge5cvxca+5lK9wxovmGPgpVJMwjyu5lTA/Cd6fLGoPq7FXcUE1jFkEdxeyqGGz8VfHYSHCn5Lcn24BzaNKA== @@ -1497,6 +1497,14 @@ "@types/history" "*" "@types/react" "*" +"@types/react-router@^5.1.0": + version "5.1.0" + resolved "https://registry.yarnpkg.com/@types/react-router/-/react-router-5.1.0.tgz#ebb47bb7fdc75741286b5fb5a82ba5b4d62518c8" + integrity sha512-XqxaIqG+LJTh9wsGpZCVQNOAQyEjXcfYRqoIXEFqxc49BKnmvJ5FLylsNUUCTckSffD468cOn4NJvxcWuLwiDw== + dependencies: + "@types/history" "*" + "@types/react" "*" + "@types/react-transition-group@^4.2.0": version "4.2.2" resolved "https://registry.yarnpkg.com/@types/react-transition-group/-/react-transition-group-4.2.2.tgz#8c851c4598a23a3a34173069fb4c5c9e41c02e3f" @@ -4252,7 +4260,7 @@ connect-history-api-fallback@^1.3.0: resolved "https://registry.yarnpkg.com/connect-history-api-fallback/-/connect-history-api-fallback-1.6.0.tgz#8b32089359308d111115d81cad3fceab888f97bc" integrity sha512-e54B99q/OUoH64zYYRf3HBP5z24G38h5D3qXu23JGRoigpX5Ss4r9ZnDk3g0Z8uQC2x2lPaJ+UlWBc1ZWBWdLg== -connected-react-router@^6.4.0: +connected-react-router@^6.5.2: version "6.5.2" resolved "https://registry.yarnpkg.com/connected-react-router/-/connected-react-router-6.5.2.tgz#422af70f86cb276681e20ab4295cf27dd9b6c7e3" integrity sha512-qzsLPZCofSI80fwy+HgxtEgSGS4ndYUUZAWaw1dqaOGPLKX/FVwIOEb7q+hjHdnZ4v5pKZcNv5GG4urjujIoyA== @@ -12738,23 +12746,23 @@ react-redux@^7.1.0: prop-types "^15.7.2" react-is "^16.9.0" -react-router-dom@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-5.0.1.tgz#ee66f4a5d18b6089c361958e443489d6bab714be" - integrity sha512-zaVHSy7NN0G91/Bz9GD4owex5+eop+KvgbxXsP/O+iW1/Ln+BrJ8QiIR5a6xNPtrdTvLkxqlDClx13QO1uB8CA== +react-router-dom@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-5.1.0.tgz#48ad018d71fb7835212587e4c90bd2e3d2417e31" + integrity sha512-OkxKbMKjO7IkYqnoaZNX19MnwgjhxwZE871cPUTq0YU2wpIw7QwGxSnSoNRMOa7wO1TwvJJMFpgiEB4C/gVhTw== dependencies: "@babel/runtime" "^7.1.2" history "^4.9.0" loose-envify "^1.3.1" prop-types "^15.6.2" - react-router "5.0.1" + react-router "5.1.0" tiny-invariant "^1.0.2" tiny-warning "^1.0.0" -react-router@5.0.1, react-router@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/react-router/-/react-router-5.0.1.tgz#04ee77df1d1ab6cb8939f9f01ad5702dbadb8b0f" - integrity sha512-EM7suCPNKb1NxcTZ2LEOWFtQBQRQXecLxVpdsP4DW4PbbqYWeRiLyV/Tt1SdCrvT2jcyXAXmVTmzvSzrPR63Bg== +react-router@5.1.0, react-router@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/react-router/-/react-router-5.1.0.tgz#739d0f3a57476363374e20d6e33e97f5ce2e00a3" + integrity sha512-n9HXxaL/6yRlig9XPfGyagI8+bUNdqcu7FUAx0/Z+Us22Z8iHsbkyJ21Inebn9HOxI5Nxlfc8GNabkNSeXfhqw== dependencies: "@babel/runtime" "^7.1.2" history "^4.9.0" @@ -12767,19 +12775,6 @@ react-router@5.0.1, react-router@^5.0.1: tiny-invariant "^1.0.2" tiny-warning "^1.0.0" -react-router@^4.2.0: - version "4.3.1" - resolved "https://registry.yarnpkg.com/react-router/-/react-router-4.3.1.tgz#aada4aef14c809cb2e686b05cee4742234506c4e" - integrity sha512-yrvL8AogDh2X42Dt9iknk4wF4V8bWREPirFfS9gLU1huk6qK41sg7Z/1S81jjTrGHxa3B8R3J6xIkDAA6CVarg== - dependencies: - history "^4.7.2" - hoist-non-react-statics "^2.5.0" - invariant "^2.2.4" - loose-envify "^1.3.1" - path-to-regexp "^1.7.0" - prop-types "^15.6.1" - warning "^4.0.1" - react-scripts@^3.0.0: version "3.1.1" resolved "https://registry.yarnpkg.com/react-scripts/-/react-scripts-3.1.1.tgz#1796bc92447f3a2d3072c3b71ca99f88d099c48d"