From 357d6bacee40cd4c2815a164ab4cba50edfa91f8 Mon Sep 17 00:00:00 2001 From: Josh Salisbury Date: Mon, 7 Dec 2020 16:59:06 -0600 Subject: [PATCH 1/3] Add review page to navigator The review page is the last page of the navigator. It does not use the navigator form component like the other pages of the form. It Only shows nav state when the form as been submitted. --- .../components/Navigator/__tests__/index.js | 24 ++++- .../components/Navigator/components/Form.js | 6 +- .../Navigator/components/SideNav.js | 18 ++-- .../Navigator/components/__tests__/Form.js | 20 ++-- .../Navigator/components/__tests__/SideNav.js | 3 +- frontend/src/components/Navigator/index.js | 94 +++++++++++++------ .../ActivityReport/Pages/ReviewSubmit.js | 10 +- frontend/src/pages/ActivityReport/index.js | 35 +++---- 8 files changed, 129 insertions(+), 81 deletions(-) diff --git a/frontend/src/components/Navigator/__tests__/index.js b/frontend/src/components/Navigator/__tests__/index.js index dbd5f9292d..8dc489a3ad 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 waitFor(() => screen.getByTestId('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 waitFor(() => screen.getByTestId('second')); userEvent.click(screen.getByRole('button', { name: 'Continue' })); + await waitFor(() => screen.getByTestId('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 c1a06549b1..4a18ec4eae 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 @@ -46,7 +46,7 @@ function Form({ getValuesRef.current = getValues; return ( - + {renderForm(hookForm)} @@ -55,7 +55,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..9014676e43 100644 --- a/frontend/src/pages/ActivityReport/Pages/ReviewSubmit.js +++ b/frontend/src/pages/ActivityReport/Pages/ReviewSubmit.js @@ -1,17 +1,23 @@ 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 581e7f9f9f..2385066ad7 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} /> - )} ); } From 27263eb51d2d451ea2eb0d45dbc6aebd81e25d7d Mon Sep 17 00:00:00 2001 From: Josh Salisbury Date: Wed, 9 Dec 2020 13:14:02 -0600 Subject: [PATCH 2/3] Lint fixes --- frontend/src/components/Navigator/__tests__/index.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/src/components/Navigator/__tests__/index.js b/frontend/src/components/Navigator/__tests__/index.js index a93e45104f..d0ff0f42c6 100644 --- a/frontend/src/components/Navigator/__tests__/index.js +++ b/frontend/src/components/Navigator/__tests__/index.js @@ -66,7 +66,7 @@ describe('Navigator', () => { it('shows the review page after showing the last form page', async () => { renderNavigator(); userEvent.click(screen.getByRole('button', { name: 'Continue' })); - await waitFor(() => screen.getByTestId('second')); + await screen.findByTestId('second'); userEvent.click(screen.getByRole('button', { name: 'Continue' })); await waitFor(() => expect(screen.getByTestId('review')).toBeVisible()); }); @@ -75,9 +75,9 @@ describe('Navigator', () => { const onSubmit = jest.fn(); renderNavigator(onSubmit); userEvent.click(screen.getByRole('button', { name: 'Continue' })); - await waitFor(() => screen.getByTestId('second')); + await screen.findByTestId('second'); userEvent.click(screen.getByRole('button', { name: 'Continue' })); - await waitFor(() => screen.getByTestId('review')); + await screen.findByTestId('review'); userEvent.click(screen.getByTestId('review')); await waitFor(() => expect(onSubmit).toHaveBeenCalled()); }); From f1b528c602a974fa13bd68366604b369eb5f198a Mon Sep 17 00:00:00 2001 From: Josh Salisbury Date: Thu, 10 Dec 2020 16:19:16 -0600 Subject: [PATCH 3/3] Add newline to review and submit page --- frontend/src/pages/ActivityReport/Pages/ReviewSubmit.js | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/src/pages/ActivityReport/Pages/ReviewSubmit.js b/frontend/src/pages/ActivityReport/Pages/ReviewSubmit.js index 9014676e43..b41959458c 100644 --- a/frontend/src/pages/ActivityReport/Pages/ReviewSubmit.js +++ b/frontend/src/pages/ActivityReport/Pages/ReviewSubmit.js @@ -10,6 +10,7 @@ const ReviewSubmit = ({ allComplete, onSubmit }) => (
    Review and submit +