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

Add review page to navigator #210

Merged
merged 8 commits into from
Dec 14, 2020
24 changes: 22 additions & 2 deletions frontend/src/components/Navigator/__tests__/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
} from '@testing-library/react';

import Navigator from '../index';
import { NOT_STARTED } from '../constants';

const pages = [
{
Expand All @@ -32,13 +33,22 @@ const pages = [
},
];

const renderReview = (allComplete, onSubmit) => (
<div>
<button type="button" data-testid="review" onClick={onSubmit}>button</button>
</div>
);

describe('Navigator', () => {
const renderNavigator = (onSubmit = () => {}) => {
render(
<Navigator
submitted={false}
initialPageState={[NOT_STARTED, NOT_STARTED]}
defaultValues={{ first: '', second: '' }}
pages={pages}
onFormSubmit={onSubmit}
renderReview={renderReview}
/>,
);
};
Expand All @@ -53,12 +63,22 @@ describe('Navigator', () => {
await waitFor(() => expect(within(first.nextSibling).getByText('In progress')).toBeVisible());
});

it('submits data when "continuing" from the last page', async () => {
it('shows the review page after showing the last form page', async () => {
renderNavigator();
userEvent.click(screen.getByRole('button', { name: 'Continue' }));
await screen.findByTestId('second');
userEvent.click(screen.getByRole('button', { name: 'Continue' }));
await waitFor(() => expect(screen.getByTestId('review')).toBeVisible());
});

it('submits data when "continuing" from the review page', async () => {
const onSubmit = jest.fn();
renderNavigator(onSubmit);
userEvent.click(screen.getByRole('button', { name: 'Continue' }));
await waitFor(() => expect(screen.getByTestId('second')));
await screen.findByTestId('second');
userEvent.click(screen.getByRole('button', { name: 'Continue' }));
await screen.findByTestId('review');
userEvent.click(screen.getByTestId('review'));
await waitFor(() => expect(onSubmit).toHaveBeenCalled());
});

Expand Down
6 changes: 3 additions & 3 deletions frontend/src/components/Navigator/components/Form.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { Form as UswdsForm, Button } from '@trussworks/react-uswds';
import { useForm } from 'react-hook-form';

function Form({
initialData, onSubmit, onDirty, saveForm, renderForm,
Copy link
Contributor

Choose a reason for hiding this comment

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

Love using onContinue here, very clear that we're not submitting the whole process.

initialData, onContinue, onDirty, saveForm, renderForm,
}) {
/*
When the form unmounts we want to send any data in the form
Expand Down Expand Up @@ -49,7 +49,7 @@ function Form({
getValuesRef.current = getValues;

return (
<UswdsForm onSubmit={handleSubmit(onSubmit)} className="smart-hub--form-large">
<UswdsForm onSubmit={handleSubmit(onContinue)} className="smart-hub--form-large">
{renderForm(hookForm)}
<Button className="stepper-button" type="submit" disabled={!formState.isValid}>Continue</Button>
</UswdsForm>
Expand All @@ -58,7 +58,7 @@ function Form({

Form.propTypes = {
initialData: PropTypes.shape({}),
onSubmit: PropTypes.func.isRequired,
onContinue: PropTypes.func.isRequired,
onDirty: PropTypes.func.isRequired,
saveForm: PropTypes.func.isRequired,
renderForm: PropTypes.func.isRequired,
Expand Down
18 changes: 10 additions & 8 deletions frontend/src/components/Navigator/components/SideNav.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,22 +31,25 @@ const tagClass = (state) => {
};

function SideNav({
pages, onNavigation, skipTo, skipToMessage,
pages, skipTo, skipToMessage,
}) {
const isMobile = useMediaQuery({ maxWidth: 640 });
const navItems = () => pages.map((page, index) => (
const navItems = () => pages.map((page) => (
<li key={page.label} className="smart-hub--navigator-item">
<Button
onClick={() => onNavigation(index)}
onClick={page.onClick}
unstyled
role="button"
className={`smart-hub--navigator-link ${page.current ? 'smart-hub--navigator-link-active' : ''}`}
>
<span className="margin-left-2">{page.label}</span>
<span className="margin-left-auto margin-right-2">
<Tag className={`smart-hub--tag ${tagClass(page.state)}`}>
{page.state}
</Tag>
{page.state
&& (
<Tag className={`smart-hub--tag ${tagClass(page.state)}`}>
{page.state}
</Tag>
)}
</span>
</Button>
</li>
Expand All @@ -69,10 +72,9 @@ SideNav.propTypes = {
PropTypes.shape({
label: PropTypes.string.isRequired,
current: PropTypes.bool.isRequired,
state: PropTypes.string.isRequired,
state: PropTypes.string,
}),
).isRequired,
onNavigation: PropTypes.func.isRequired,
skipTo: PropTypes.string.isRequired,
skipToMessage: PropTypes.string.isRequired,
};
Expand Down
20 changes: 10 additions & 10 deletions frontend/src/components/Navigator/components/__tests__/Form.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@ import { render, screen, act } from '@testing-library/react';

import Form from '../Form';

const renderForm = (saveForm, onSubmit, onDirty) => render(
const renderForm = (saveForm, onContinue, onDirty) => render(
<Form
initialData={{ test: '' }}
onSubmit={onSubmit}
onContinue={onContinue}
saveForm={saveForm}
onDirty={onDirty}
renderForm={(hookForm) => (
Expand All @@ -25,33 +25,33 @@ const renderForm = (saveForm, onSubmit, onDirty) => render(
describe('Form', () => {
it('calls saveForm when unmounted', () => {
const saveForm = jest.fn();
const onSubmit = jest.fn();
const onContinue = jest.fn();
const dirty = jest.fn();
const { unmount } = renderForm(saveForm, onSubmit, dirty);
const { unmount } = renderForm(saveForm, onContinue, dirty);
unmount();
expect(saveForm).toHaveBeenCalled();
});

it('calls onSubmit when submitting', async () => {
it('calls onContinue when submitting', async () => {
const saveForm = jest.fn();
const onSubmit = jest.fn();
const onContinue = jest.fn();
const dirty = jest.fn();

renderForm(saveForm, onSubmit, dirty);
renderForm(saveForm, onContinue, dirty);
const submit = screen.getByRole('button');
await act(async () => {
userEvent.click(submit);
});

expect(onSubmit).toHaveBeenCalled();
expect(onContinue).toHaveBeenCalled();
});

it('calls onDirty when the form is dirty', async () => {
const saveForm = jest.fn();
const onSubmit = jest.fn();
const onContinue = jest.fn();
const dirty = jest.fn();

renderForm(saveForm, onSubmit, dirty);
renderForm(saveForm, onContinue, dirty);
const submit = screen.getByTestId('input');
userEvent.click(submit);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,17 +17,18 @@ describe('SideNav', () => {
label: 'test',
current,
state,
onClick: () => onNavigation(0),
},
{
label: 'second',
current: false,
state: '',
onClick: () => onNavigation(1),
},
];
render(
<SideNav
pages={pages}
onNavigation={onNavigation}
skipTo="skip"
skipToMessage="message"
/>,
Expand Down
94 changes: 63 additions & 31 deletions frontend/src/components/Navigator/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,13 @@
*/
import React, { useState, useCallback } from 'react';
import PropTypes from 'prop-types';
import _ from 'lodash';
import { Grid } from '@trussworks/react-uswds';

import Container from '../Container';

import {
NOT_STARTED, IN_PROGRESS, COMPLETE,
IN_PROGRESS, COMPLETE, SUBMITTED,
} from './constants';
import SideNav from './components/SideNav';
import Form from './components/Form';
Expand All @@ -22,79 +23,107 @@ import IndicatorHeader from './components/IndicatorHeader';
Get the current state of navigator items. Sets the currently selected item as "In Progress" and
sets a "current" flag which the side nav uses to style the selected component as selected.
*/
const navigatorPages = (pages, navigatorState, currentPage) => pages.map((page, index) => {
const current = currentPage === index;
const state = current ? IN_PROGRESS : navigatorState[index];
return {
label: page.label,
state,
current,
};
});

function Navigator({
defaultValues, pages, onFormSubmit,
defaultValues, pages, onFormSubmit, initialPageState, renderReview, submitted,
}) {
const [data, updateData] = useState(defaultValues);
const [formData, updateFormData] = useState(defaultValues);
const [viewReview, updateViewReview] = useState(false);
const [currentPage, updateCurrentPage] = useState(0);
const [navigatorState, updateNavigatorState] = useState(pages.map(() => (NOT_STARTED)));
const page = pages[currentPage];
const [pageState, updatePageState] = useState(initialPageState);
const lastPage = pages.length - 1;

const onNavigation = (index) => {
updateViewReview(false);
updateCurrentPage(index);
};

const onDirty = useCallback((isDirty) => {
updateNavigatorState((oldNavigatorState) => {
updatePageState((oldNavigatorState) => {
const newNavigatorState = [...oldNavigatorState];
newNavigatorState[currentPage] = isDirty ? IN_PROGRESS : oldNavigatorState[currentPage];
return newNavigatorState;
});
}, [updateNavigatorState, currentPage]);
}, [updatePageState, currentPage]);

const saveForm = useCallback((newData) => {
updateData((oldData) => ({ ...oldData, ...newData }));
}, [updateData]);
const onSaveForm = useCallback((newData) => {
updateFormData((oldData) => ({ ...oldData, ...newData }));
}, [updateFormData]);

const onSubmit = (formData) => {
const newNavigatorState = [...navigatorState];
const onContinue = () => {
const newNavigatorState = [...pageState];
newNavigatorState[currentPage] = COMPLETE;
updateNavigatorState(newNavigatorState);
updatePageState(newNavigatorState);

if (currentPage + 1 > lastPage) {
onFormSubmit({ ...data, ...formData });
if (currentPage >= lastPage) {
updateViewReview(true);
} else {
updateCurrentPage((prevPage) => prevPage + 1);
}
};

const navigatorPages = pages.map((page, index) => {
const current = !viewReview && currentPage === index;
const state = pageState[index];
return {
label: page.label,
onClick: () => onNavigation(index),
state,
current,
};
});

const onViewReview = () => {
updateViewReview(true);
};

const onSubmit = () => {
onFormSubmit(formData);
};

const allComplete = _.every(pageState, (state) => state === COMPLETE);

const reviewPage = {
label: 'Review and submit',
onClick: onViewReview,
state: submitted ? SUBMITTED : undefined,
current: viewReview,
renderForm: renderReview,
};

navigatorPages.push(reviewPage);
const page = viewReview ? reviewPage : pages[currentPage];

return (
<Grid row gap>
<Grid col={12} tablet={{ col: 6 }} desktop={{ col: 4 }}>
<SideNav
skipTo="navigator-form"
skipToMessage="Skip to report content"
onNavigation={onNavigation}
pages={navigatorPages(pages, navigatorState, currentPage)}
pages={navigatorPages}
/>
</Grid>
<Grid col={12} tablet={{ col: 6 }} desktop={{ col: 8 }}>
<Container skipTopPadding>
<IndicatorHeader
currentStep={currentPage + 1}
totalSteps={pages.length}
currentStep={viewReview ? navigatorPages.length : currentPage + 1}
totalSteps={navigatorPages.length}
label={page.label}
/>
<div id="navigator-form">
{viewReview
&& renderReview(allComplete, onSubmit)}
{!viewReview
&& (
<Form
key={page.label}
initialData={data}
onSubmit={onSubmit}
initialData={formData}
onContinue={onContinue}
onDirty={onDirty}
saveForm={saveForm}
saveForm={onSaveForm}
renderForm={page.renderForm}
/>
)}
</div>
</Container>
</Grid>
Expand All @@ -105,6 +134,9 @@ function Navigator({
Navigator.propTypes = {
defaultValues: PropTypes.shape({}),
onFormSubmit: PropTypes.func.isRequired,
initialPageState: PropTypes.arrayOf(PropTypes.string).isRequired,
renderReview: PropTypes.func.isRequired,
submitted: PropTypes.bool.isRequired,
pages: PropTypes.arrayOf(
PropTypes.shape({
renderForm: PropTypes.func.isRequired,
Expand Down
11 changes: 9 additions & 2 deletions frontend/src/pages/ActivityReport/Pages/ReviewSubmit.js
Original file line number Diff line number Diff line change
@@ -1,17 +1,24 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Helmet } from 'react-helmet';
import { Button } from '@trussworks/react-uswds';

const ReviewSubmit = () => (
const ReviewSubmit = ({ allComplete, onSubmit }) => (
<>
<Helmet>
<title>Review and submit</title>
</Helmet>
<div>
Review and submit
<br />
<Button disabled={!allComplete} onClick={onSubmit}>Submit</Button>
</div>
</>
);

ReviewSubmit.propTypes = {};
ReviewSubmit.propTypes = {
allComplete: PropTypes.bool.isRequired,
onSubmit: PropTypes.func.isRequired,
};

export default ReviewSubmit;
Loading