diff --git a/package.json b/package.json index 4f3bd242f..014e9f1a8 100644 --- a/package.json +++ b/package.json @@ -118,8 +118,8 @@ "react-dom": "18.2.0", "react-emojione": "5.0.1", "react-final-form": "6.5.9", - "react-router": "5.3.4", - "react-router-dom": "5.3.4", + "react-router": "6.16.0", + "react-router-dom": "6.16.0", "react-transition-group": "4.4.5", "ts-loader": "9.4.4", "typescript": "5.2.2" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2a13ba9e5..7e3362eda 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -45,11 +45,11 @@ dependencies: specifier: 6.5.9 version: 6.5.9(final-form@4.20.10)(react@18.2.0) react-router: - specifier: 5.3.4 - version: 5.3.4(react@18.2.0) + specifier: 6.16.0 + version: 6.16.0(react@18.2.0) react-router-dom: - specifier: 5.3.4 - version: 5.3.4(react@18.2.0) + specifier: 6.16.0 + version: 6.16.0(react-dom@18.2.0)(react@18.2.0) react-transition-group: specifier: 4.4.5 version: 4.4.5(react-dom@18.2.0)(react@18.2.0) @@ -894,6 +894,11 @@ packages: react: 18.2.0 dev: false + /@remix-run/router@1.9.0: + resolution: {integrity: sha512-bV63itrKBC0zdT27qYm6SDZHlkXwFL1xMBuhkn+X7l0+IIhNaH5wuuvZKp6eKhCD4KFhujhfhCT1YxXW6esUIA==} + engines: {node: '>=14.0.0'} + dev: false + /@sinclair/typebox@0.27.8: resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==} dev: true @@ -2980,12 +2985,6 @@ packages: value-equal: 1.0.1 dev: false - /hoist-non-react-statics@3.3.2: - resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==} - dependencies: - react-is: 16.13.1 - dev: false - /hosted-git-info@4.1.0: resolution: {integrity: sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==} engines: {node: '>=10'} @@ -3296,10 +3295,6 @@ packages: get-intrinsic: 1.2.1 dev: true - /isarray@0.0.1: - resolution: {integrity: sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==} - dev: false - /isarray@1.0.0: resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} @@ -4404,12 +4399,6 @@ packages: /path-parse@1.0.7: resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} - /path-to-regexp@1.8.0: - resolution: {integrity: sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==} - dependencies: - isarray: 0.0.1 - dev: false - /path-type@4.0.0: resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} engines: {node: '>=8'} @@ -4748,36 +4737,27 @@ packages: resolution: {integrity: sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==} dev: true - /react-router-dom@5.3.4(react@18.2.0): - resolution: {integrity: sha512-m4EqFMHv/Ih4kpcBCONHbkT68KoAeHN4p3lAGoNryfHi0dMy0kCzEZakiKRsvg5wHZ/JLrLW8o8KomWiz/qbYQ==} + /react-router-dom@6.16.0(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-aTfBLv3mk/gaKLxgRDUPbPw+s4Y/O+ma3rEN1u8EgEpLpPe6gNjIsWt9rxushMHHMb7mSwxRGdGlGdvmFsyPIg==} + engines: {node: '>=14.0.0'} peerDependencies: - react: '>=15' + react: '>=16.8' + react-dom: '>=16.8' dependencies: - '@babel/runtime': 7.21.5 - history: 4.10.1 - loose-envify: 1.4.0 - prop-types: 15.8.1 + '@remix-run/router': 1.9.0 react: 18.2.0 - react-router: 5.3.4(react@18.2.0) - tiny-invariant: 1.3.1 - tiny-warning: 1.0.3 + react-dom: 18.2.0(react@18.2.0) + react-router: 6.16.0(react@18.2.0) dev: false - /react-router@5.3.4(react@18.2.0): - resolution: {integrity: sha512-Ys9K+ppnJah3QuaRiLxk+jDWOR1MekYQrlytiXxC1RyfbdsZkS5pvKAzCCr031xHixZwpnsYNT5xysdFHQaYsA==} + /react-router@6.16.0(react@18.2.0): + resolution: {integrity: sha512-VT4Mmc4jj5YyjpOi5jOf0I+TYzGpvzERy4ckNSvSh2RArv8LLoCxlsZ2D+tc7zgjxcY34oTz2hZaeX5RVprKqA==} + engines: {node: '>=14.0.0'} peerDependencies: - react: '>=15' + react: '>=16.8' dependencies: - '@babel/runtime': 7.21.5 - history: 4.10.1 - hoist-non-react-statics: 3.3.2 - loose-envify: 1.4.0 - path-to-regexp: 1.8.0 - prop-types: 15.8.1 + '@remix-run/router': 1.9.0 react: 18.2.0 - react-is: 16.13.1 - tiny-invariant: 1.3.1 - tiny-warning: 1.0.3 dev: false /react-shallow-renderer@16.15.0(react@18.2.0): diff --git a/src/app.tsx b/src/app.tsx index 95cf9fbbf..73155c100 100644 --- a/src/app.tsx +++ b/src/app.tsx @@ -1,9 +1,9 @@ import React, { useContext } from 'react'; import { - Redirect, + Navigate, HashRouter as Router, Route, - Switch, + Routes, useLocation, } from 'react-router-dom'; @@ -23,7 +23,7 @@ function RequireAuth({ children }) { return isLoggedIn ? ( children ) : ( - + ); } @@ -34,31 +34,30 @@ export const App = () => {
- - - - - - - - - - - - - - - - - - - - - - - - - + + + + + } + /> + + + + } + /> + } /> + } + /> + } /> +
diff --git a/src/components/Sidebar.test.tsx b/src/components/Sidebar.test.tsx index 065d3ddd6..5372c5a20 100644 --- a/src/components/Sidebar.test.tsx +++ b/src/components/Sidebar.test.tsx @@ -12,6 +12,12 @@ import { mockedAccountNotifications } from '../__mocks__/mockedData'; import { AppContext } from '../context/App'; import { Sidebar } from './Sidebar'; +const mockNavigate = jest.fn(); +jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), + useNavigate: () => mockNavigate, +})); + describe('components/Sidebar.tsx', () => { const fetchNotifications = jest.fn(); const history = createMemoryHistory(); @@ -73,17 +79,15 @@ describe('components/Sidebar.tsx', () => { }); it('go to the settings route', () => { - const pushMock = jest.spyOn(history, 'push'); - const { getByLabelText } = render( - + , ); fireEvent.click(getByLabelText('Settings')); - expect(pushMock).toHaveBeenCalledTimes(1); + expect(mockNavigate).toHaveBeenNthCalledWith(1, '/settings'); }); it('opens github in the notifications page', () => { diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx index 8a07f57a1..87b673263 100644 --- a/src/components/Sidebar.tsx +++ b/src/components/Sidebar.tsx @@ -1,7 +1,7 @@ import { BellIcon } from '@primer/octicons-react'; import { ipcRenderer, shell } from 'electron'; import React, { useCallback, useContext, useMemo } from 'react'; -import { useHistory, useLocation } from 'react-router-dom'; +import { useNavigate, useLocation } from 'react-router-dom'; import { Logo } from '../components/Logo'; import { AppContext } from '../context/App'; @@ -11,7 +11,7 @@ import { IconRefresh } from '../icons/Refresh'; import { Constants } from '../utils/constants'; export const Sidebar: React.FC = () => { - const history = useHistory(); + const navigate = useNavigate(); const location = useLocation(); const { isLoggedIn } = useContext(AppContext); @@ -66,7 +66,7 @@ export const Sidebar: React.FC = () => { diff --git a/src/routes/LoginWithToken.test.tsx b/src/routes/LoginWithToken.test.tsx index 96c7441b8..c4f5ccb12 100644 --- a/src/routes/LoginWithToken.test.tsx +++ b/src/routes/LoginWithToken.test.tsx @@ -9,9 +9,14 @@ import { shell } from 'electron'; import { AppContext } from '../context/App'; import { LoginWithToken, validate } from './LoginWithToken'; +const mockNavigate = jest.fn(); +jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), + useNavigate: () => mockNavigate, +})); + describe('routes/LoginWithToken.js', () => { const history = createMemoryHistory(); - const goBackMock = jest.spyOn(history, 'goBack'); const openExternalMock = jest.spyOn(shell, 'openExternal'); const mockValidateToken = jest.fn(); @@ -19,7 +24,7 @@ describe('routes/LoginWithToken.js', () => { beforeEach(function () { mockValidateToken.mockReset(); openExternalMock.mockReset(); - goBackMock.mockReset(); + mockNavigate.mockReset(); }); it('renders correctly', () => { @@ -37,16 +42,14 @@ describe('routes/LoginWithToken.js', () => { }); it('let us go back', () => { - const goBackMock = jest.spyOn(history, 'goBack'); - const { getByLabelText } = render( - + , ); fireEvent.click(getByLabelText('Go Back')); - expect(goBackMock).toHaveBeenCalledTimes(1); + expect(mockNavigate).toHaveBeenNthCalledWith(1, -1); }); it('should validate the form values', () => { @@ -74,7 +77,7 @@ describe('routes/LoginWithToken.js', () => { it("should click on the 'personal access tokens' link and open the browser", async () => { const { getByText } = render( - + , @@ -90,7 +93,7 @@ describe('routes/LoginWithToken.js', () => { const { getByLabelText, getByTitle } = render( - + , @@ -108,7 +111,7 @@ describe('routes/LoginWithToken.js', () => { await waitFor(() => expect(mockValidateToken).toHaveBeenCalledTimes(1)); expect(mockValidateToken).toHaveBeenCalledTimes(1); - expect(goBackMock).toHaveBeenCalledTimes(1); + expect(mockNavigate).toHaveBeenNthCalledWith(1, -1); }); it('should login using a token - failure', async () => { @@ -116,7 +119,7 @@ describe('routes/LoginWithToken.js', () => { const { getByLabelText, getByTitle } = render( - + , @@ -135,7 +138,7 @@ describe('routes/LoginWithToken.js', () => { await waitFor(() => expect(mockValidateToken).toHaveBeenCalledTimes(1)); expect(mockValidateToken).toHaveBeenCalledTimes(1); - expect(goBackMock).not.toHaveBeenCalled(); + expect(mockNavigate).toHaveBeenCalledTimes(0); }); it('should render the form with errors', () => { diff --git a/src/routes/LoginWithToken.tsx b/src/routes/LoginWithToken.tsx index b93365041..32b3660f3 100644 --- a/src/routes/LoginWithToken.tsx +++ b/src/routes/LoginWithToken.tsx @@ -1,7 +1,7 @@ import React, { useCallback, useContext, useState } from 'react'; import { Form, FormRenderProps } from 'react-final-form'; import { ArrowLeftIcon } from '@primer/octicons-react'; -import { useHistory } from 'react-router-dom'; +import { useNavigate } from 'react-router-dom'; import { shell } from 'electron'; import { AppContext } from '../context/App'; @@ -42,7 +42,7 @@ export const validate = (values: IValues): IFormErrors => { export const LoginWithToken: React.FC = () => { const { validateToken } = useContext(AppContext); - const history = useHistory(); + const navigate = useNavigate(); const [isValidToken, setIsValidToken] = useState(true); const openLink = useCallback((url: string) => { @@ -109,7 +109,7 @@ export const LoginWithToken: React.FC = () => { setIsValidToken(true); try { await validateToken(data as AuthTokenOptions); - history.goBack(); + navigate(-1); } catch (err) { setIsValidToken(false); } @@ -121,7 +121,7 @@ export const LoginWithToken: React.FC = () => { diff --git a/src/routes/Settings.test.tsx b/src/routes/Settings.test.tsx index c1ace8ba3..185b7ac29 100644 --- a/src/routes/Settings.test.tsx +++ b/src/routes/Settings.test.tsx @@ -11,14 +11,18 @@ import { SettingsRoute } from './Settings'; import { AppContext } from '../context/App'; import { mockSettings } from '../__mocks__/mock-state'; +const mockNavigate = jest.fn(); +jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), + useNavigate: () => mockNavigate, +})); + describe('routes/Settings.tsx', () => { const history = createMemoryHistory(); - const goBackMock = jest.spyOn(history, 'goBack'); - const replaceMock = jest.spyOn(history, 'replace'); const updateSetting = jest.fn(); beforeEach(() => { - goBackMock.mockReset(); + mockNavigate.mockReset(); updateSetting.mockReset(); }); @@ -40,7 +44,7 @@ describe('routes/Settings.tsx', () => { - + , @@ -52,19 +56,19 @@ describe('routes/Settings.tsx', () => { expect(ipcRenderer.send).toHaveBeenCalledTimes(1); expect(ipcRenderer.send).toHaveBeenCalledWith('update-icon'); - expect(goBackMock).toHaveBeenCalledTimes(1); + expect(mockNavigate).toHaveBeenNthCalledWith(1, -1); }); it('should go back by pressing the icon', () => { const { getByLabelText } = render( - + , ); fireEvent.click(getByLabelText('Go Back')); - expect(goBackMock).toHaveBeenCalledTimes(1); + expect(mockNavigate).toHaveBeenNthCalledWith(1, -1); }); it('should toggle the showOnlyParticipating checkbox', () => { @@ -170,13 +174,15 @@ describe('routes/Settings.tsx', () => { it('should go to the enterprise login route', () => { const { getByLabelText } = render( - + , ); fireEvent.click(getByLabelText('Login with GitHub Enterprise')); - expect(replaceMock).toHaveBeenCalledWith('/login-enterprise'); + expect(mockNavigate).toHaveBeenNthCalledWith(1, '/login-enterprise', { + replace: true, + }); }); it('should quit the app', () => { diff --git a/src/routes/Settings.tsx b/src/routes/Settings.tsx index 24c14b112..a7b929fb6 100644 --- a/src/routes/Settings.tsx +++ b/src/routes/Settings.tsx @@ -1,6 +1,6 @@ import React, { useCallback, useContext } from 'react'; import { ipcRenderer, remote } from 'electron'; -import { useHistory } from 'react-router-dom'; +import { useNavigate } from 'react-router-dom'; import { ArrowLeftIcon } from '@primer/octicons-react'; import { AppContext } from '../context/App'; @@ -17,7 +17,7 @@ const isLinux = remote.process.platform === 'linux'; export const SettingsRoute: React.FC = () => { const { settings, updateSetting, logout } = useContext(AppContext); - const history = useHistory(); + const navigate = useNavigate(); ipcRenderer.on('update-native-theme', (_, updatedAppearance: Appearance) => { if (settings.appearance === Appearance.SYSTEM) { @@ -27,7 +27,7 @@ export const SettingsRoute: React.FC = () => { const logoutUser = useCallback(() => { logout(); - history.goBack(); + navigate(-1); updateTrayIcon(); }, []); @@ -36,7 +36,7 @@ export const SettingsRoute: React.FC = () => { }, []); const goToEnterprise = useCallback(() => { - return history.replace('/login-enterprise'); + return navigate('/login-enterprise', { replace: true }); }, []); const footerButtonClass = @@ -48,7 +48,7 @@ export const SettingsRoute: React.FC = () => {