Skip to content

Commit

Permalink
Merge pull request #210 from adhocteam/main
Browse files Browse the repository at this point in the history
Add review page to navigator
  • Loading branch information
rahearn authored Dec 14, 2020
2 parents 4d96715 + 131e612 commit 39a5461
Show file tree
Hide file tree
Showing 8 changed files with 130 additions and 81 deletions.
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,
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

0 comments on commit 39a5461

Please sign in to comment.