Skip to content
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
4,308 changes: 3,134 additions & 1,174 deletions package-lock.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@
"react-dom": "^18.2.0",
"react-paginate": "^8.2.0",
"react-qr-code": "^2.0.18",
"react-router-dom": "^6.10.0",
"react-router-dom": "^6.30.6",
"react-router-hash-link": "^2.4.3",
"react-webcam": "^7.2.0",
"styled-components": "^5.3.10",
Expand Down
2 changes: 1 addition & 1 deletion src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ const App = () => (
<ThemeProvider theme={theme}>
<NotificationContextProvider>
<UserDataContextProvider>
<BrowserRouter>
<BrowserRouter future={{ v7_relativeSplatPath: true }}>
<Layout>
<AppRoutes />
</Layout>
Expand Down
6 changes: 4 additions & 2 deletions src/AppRoutes.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@ const ProtectedRoute = ({ isLoggedIn, children }) =>
*/
const AppRoutes = () => {
const { session } = useSession();
const restorePath = localStorage.getItem('restorePath');
const storedPath = localStorage.getItem('restorePath');
// Only trust a plain absolute path; a backslash makes history.replaceState throw.
const restorePath = /^\/[\w\-/]*$/.test(storedPath ?? '') ? storedPath : '';
const loggedIn = session.info.isLoggedIn;
const path = loggedIn ? restorePath || '/contacts' : '/';
const location = useLocation();
Expand Down Expand Up @@ -73,7 +75,7 @@ const AppRoutes = () => {
/>
))}
</Route>
<Route path="*" element={<Navigate to={restorePath} replace />} />
<Route path="*" element={<Navigate to={path} replace />} />
</Route>
</Routes>
);
Expand Down
6 changes: 4 additions & 2 deletions src/components/CivicProfileForms/FormLayout.jsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// React Imports
import React from 'react';
import React, { useEffect } from 'react';
import { Link as RouterLink, useLocation } from 'react-router-dom';
// Material UI Imports
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
Expand All @@ -23,7 +23,9 @@ const FormLayout = ({ children }) => {
const location = useLocation();
const path = location.pathname.split('/').pop();

localStorage.setItem('restorePath', location.pathname);
useEffect(() => {
localStorage.setItem('restorePath', location.pathname);
}, [location.pathname]);
const pageIdx = HMIS_FORM_LIST.findIndex((form) => form.path === path);

return (
Expand Down
6 changes: 4 additions & 2 deletions src/components/Messages/Inbox.jsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
// React Imports
import React from 'react';
import React, { useEffect } from 'react';
// Custom Hooks Imports
import { useMessageList } from '@hooks';
// Component Imports
import MessageFolder from './MessageFolder';

const Inbox = () => {
localStorage.setItem('restorePath', '/messages/inbox');
useEffect(() => {
localStorage.setItem('restorePath', '/messages/inbox');
}, []);
const { data, refetch, isFetching } = useMessageList('Inbox');

return (
Expand Down
6 changes: 4 additions & 2 deletions src/components/Messages/Outbox.jsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
// React Imports
import React from 'react';
import React, { useEffect } from 'react';
// Custom Hooks Imports
import { useMessageList } from '@hooks';
// Component Imports
import MessageFolder from './MessageFolder';

const Outbox = () => {
localStorage.setItem('restorePath', '/messages/outbox');
useEffect(() => {
localStorage.setItem('restorePath', '/messages/outbox');
}, []);
const { data, refetch, isFetching } = useMessageList('Outbox');

return (
Expand Down
6 changes: 4 additions & 2 deletions src/pages/CivicProfile.jsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// React Imports
import React from 'react';
import React, { useEffect } from 'react';
import { Link, Outlet, useLocation } from 'react-router-dom';
// Material UI Imports
import Box from '@mui/material/Box';
Expand All @@ -19,7 +19,9 @@ import { CIVIC_FORM_LIST } from '@components/CivicProfileForms';
const CivicProfile = () => {
const location = useLocation();

localStorage.setItem('restorePath', location.pathname);
useEffect(() => {
localStorage.setItem('restorePath', location.pathname);
}, [location.pathname]);
const currentForm = location.pathname.split('/').pop();

return (
Expand Down
4 changes: 3 additions & 1 deletion src/pages/Contacts.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,9 @@ import { truncateText } from '@utils';
* @returns {React.JSX.Component} The Contacts Page
*/
const Contacts = () => {
localStorage.setItem('restorePath', '/contacts');
useEffect(() => {
localStorage.setItem('restorePath', '/contacts');
}, []);
const theme = useTheme();
const isSmallScreen = useMediaQuery(theme.breakpoints.down('sm'));

Expand Down
9 changes: 4 additions & 5 deletions src/pages/Documents.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,10 @@ import { DocumentTable } from '@components/Documents';
const Documents = () => {
// Route related states
const location = useLocation();
if (location.pathname.split('/')[1] === 'contacts') {
localStorage.setItem('restorePath', '/contacts');
} else {
localStorage.setItem('restorePath', '/profile');
}
useEffect(() => {
const isContactRoute = location.pathname.split('/')[1] === 'contacts';
localStorage.setItem('restorePath', isContactRoute ? '/contacts' : '/profile');
}, [location.pathname]);
const { setContact } = useContext(DocumentListContext);
const theme = useTheme();
const isSmallScreen = useMediaQuery(theme.breakpoints.down('sm'));
Expand Down
9 changes: 4 additions & 5 deletions src/pages/Profile.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,10 @@ import { fetchProfileInfo } from '../model-helpers';
const Profile = () => {
// Route related states
const location = useLocation();
if (location.pathname.split('/')[1] === 'contacts') {
localStorage.setItem('restorePath', '/contacts');
} else {
localStorage.setItem('restorePath', '/profile');
}
useEffect(() => {
const isContactRoute = location.pathname.split('/')[1] === 'contacts';
localStorage.setItem('restorePath', isContactRoute ? '/contacts' : '/profile');
}, [location.pathname]);
const theme = useTheme();
const isSmallScreen = useMediaQuery(theme.breakpoints.down('sm'));

Expand Down
54 changes: 54 additions & 0 deletions test/AppRoutes.test.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { render, cleanup, screen } from '@testing-library/react';
import { MemoryRouter, useLocation } from 'react-router-dom';
import { SessionContext } from '@contexts';
import React from 'react';
import { expect, it, vi, afterEach, describe } from 'vitest';

import AppRoutes from '../src/AppRoutes';

vi.mock('../src/pages', () => ({
CivicProfile: () => <div>civic profile</div>,
Documents: () => <div>documents</div>,
Home: () => <div>home</div>,
Contacts: () => <div>contacts</div>,
Profile: () => <div>profile</div>,
Messages: () => <div>messages</div>,
Signup: () => <div>signup</div>
}));

const LocationProbe = () => <div data-testid="loc">{useLocation().pathname}</div>;

const renderAt = (path) =>
render(
<SessionContext.Provider value={{ session: { info: { isLoggedIn: true } } }}>
<MemoryRouter initialEntries={[path]} future={{ v7_relativeSplatPath: true }}>
<AppRoutes />
<LocationProbe />
</MemoryRouter>
</SessionContext.Provider>
);

describe('AppRoutes unknown path redirect', () => {
afterEach(() => {
localStorage.clear();
vi.clearAllMocks();
cleanup();
});

it('redirects to the stored restorePath', () => {
localStorage.setItem('restorePath', '/documents');
renderAt('/does-not-exist');
expect(screen.getByTestId('loc').textContent).toBe('/documents');
});

it('redirects to /contacts when no restorePath is stored', () => {
renderAt('/does-not-exist');
expect(screen.getByTestId('loc').textContent).toBe('/contacts');
});

it('ignores a stored path that is not a plain absolute path', () => {
localStorage.setItem('restorePath', '/\\evil.com');
renderAt('/does-not-exist');
expect(screen.getByTestId('loc').textContent).toBe('/contacts');
});
});
27 changes: 27 additions & 0 deletions test/components/CivicProfileForms/FormLayout.restorePath.test.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { render, cleanup } from '@testing-library/react';
import { MemoryRouter } from 'react-router-dom';
import React from 'react';
import { expect, it, afterEach, describe } from 'vitest';

import FormLayout from '../../../src/components/CivicProfileForms/FormLayout';

describe('FormLayout restorePath', () => {
afterEach(() => {
localStorage.clear();
cleanup();
});

it('stores the current path so an unknown URL can restore it', () => {
render(
<MemoryRouter
initialEntries={['/civic-profile/housing-info']}
future={{ v7_relativeSplatPath: true }}
>
<FormLayout>
<div>form body</div>
</FormLayout>
</MemoryRouter>
);
expect(localStorage.getItem('restorePath')).toBe('/civic-profile/housing-info');
});
});
Loading