diff --git a/frontend/src/components/Navigator/__tests__/index.js b/frontend/src/components/Navigator/__tests__/index.js index cebe7d708c..d0ff0f42c6 100644 --- a/frontend/src/components/Navigator/__tests__/index.js +++ b/frontend/src/components/Navigator/__tests__/index.js @@ -6,6 +6,7 @@ import { } from '@testing-library/react'; import Navigator from '../index'; +import { NOT_STARTED } from '../constants'; const pages = [ { @@ -32,13 +33,22 @@ const pages = [ }, ]; +const renderReview = (allComplete, onSubmit) => ( +
+ +
+); + describe('Navigator', () => { const renderNavigator = (onSubmit = () => {}) => { render( , ); }; @@ -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()); }); diff --git a/frontend/src/components/Navigator/components/Form.js b/frontend/src/components/Navigator/components/Form.js index 3862b9a712..a623bd3ed4 100644 --- a/frontend/src/components/Navigator/components/Form.js +++ b/frontend/src/components/Navigator/components/Form.js @@ -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 @@ -49,7 +49,7 @@ function Form({ getValuesRef.current = getValues; return ( - + {renderForm(hookForm)} @@ -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, diff --git a/frontend/src/components/Navigator/components/SideNav.js b/frontend/src/components/Navigator/components/SideNav.js index 5a55dda33b..96ffe1ab0c 100644 --- a/frontend/src/components/Navigator/components/SideNav.js +++ b/frontend/src/components/Navigator/components/SideNav.js @@ -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) => (
  • @@ -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, }; diff --git a/frontend/src/components/Navigator/components/__tests__/Form.js b/frontend/src/components/Navigator/components/__tests__/Form.js index 1b59129a9f..73d9c8af61 100644 --- a/frontend/src/components/Navigator/components/__tests__/Form.js +++ b/frontend/src/components/Navigator/components/__tests__/Form.js @@ -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(
    ( @@ -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); diff --git a/frontend/src/components/Navigator/components/__tests__/SideNav.js b/frontend/src/components/Navigator/components/__tests__/SideNav.js index 2c1be51f18..16b63cc488 100644 --- a/frontend/src/components/Navigator/components/__tests__/SideNav.js +++ b/frontend/src/components/Navigator/components/__tests__/SideNav.js @@ -17,17 +17,18 @@ describe('SideNav', () => { label: 'test', current, state, + onClick: () => onNavigation(0), }, { label: 'second', current: false, state: '', + onClick: () => onNavigation(1), }, ]; render( , diff --git a/frontend/src/components/Navigator/index.js b/frontend/src/components/Navigator/index.js index f53410abb3..20ac6c5075 100644 --- a/frontend/src/components/Navigator/index.js +++ b/frontend/src/components/Navigator/index.js @@ -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'; @@ -22,53 +23,76 @@ 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 ( @@ -76,25 +100,30 @@ function Navigator({ skipTo="navigator-form" skipToMessage="Skip to report content" onNavigation={onNavigation} - pages={navigatorPages(pages, navigatorState, currentPage)} + pages={navigatorPages} /> @@ -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, diff --git a/frontend/src/pages/ActivityReport/Pages/ReviewSubmit.js b/frontend/src/pages/ActivityReport/Pages/ReviewSubmit.js index d61b47fabb..b41959458c 100644 --- a/frontend/src/pages/ActivityReport/Pages/ReviewSubmit.js +++ b/frontend/src/pages/ActivityReport/Pages/ReviewSubmit.js @@ -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 }) => ( <> Review and submit
    Review and submit +
    +
    ); -ReviewSubmit.propTypes = {}; +ReviewSubmit.propTypes = { + allComplete: PropTypes.bool.isRequired, + onSubmit: PropTypes.func.isRequired, +}; export default ReviewSubmit; diff --git a/frontend/src/pages/ActivityReport/index.js b/frontend/src/pages/ActivityReport/index.js index f9da0ecc49..e1cdd29571 100644 --- a/frontend/src/pages/ActivityReport/index.js +++ b/frontend/src/pages/ActivityReport/index.js @@ -8,7 +8,6 @@ import React, { useState } from 'react'; import PropTypes from 'prop-types'; import { Helmet } from 'react-helmet'; -import { Button } from '@trussworks/react-uswds'; import ActivitySummary from './Pages/ActivitySummary'; import TopicsResources from './Pages/TopicsResources'; @@ -16,9 +15,9 @@ import NextSteps from './Pages/NextSteps'; import ReviewSubmit from './Pages/ReviewSubmit'; import GoalsObjectives from './Pages/GoalsObjectives'; import Navigator from '../../components/Navigator'; -import Container from '../../components/Container'; import './index.css'; +import { NOT_STARTED } from '../../components/Navigator/constants'; const pages = [ { @@ -62,12 +61,6 @@ const pages = [ ), }, - { - label: 'Review and submit', - renderForm: () => ( - - ), - }, ]; const defaultValues = { @@ -91,6 +84,8 @@ const defaultValues = { topics: [], }; +const initialPageState = pages.map(() => NOT_STARTED); + function ActivityReport({ initialData }) { const [submitted, updateSubmitted] = useState(false); @@ -100,31 +95,23 @@ function ActivityReport({ initialData }) { updateSubmitted(true); }; - const resetForm = () => { - updateSubmitted(false); - }; - return ( <>

    New activity report for Region 14

    - {submitted - && ( - - Thank you for submitted the form! - - - )} - {!submitted - && ( ( + + )} + submitted={submitted} + initialPageState={initialPageState} defaultValues={{ ...defaultValues, ...initialData }} pages={pages} onFormSubmit={onFormSubmit} /> - )} ); }