diff --git a/.circleci/config.yml b/.circleci/config.yml index 2e710561b8..92b3690c35 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -133,7 +133,7 @@ parameters: default: "main" type: string sandbox_git_branch: # change to feature branch to test deployment - default: "kw-pull-hs-data" + default: "js-saving-activity-report-frontend" type: string jobs: build_and_lint: diff --git a/axe-urls b/axe-urls index 42b0286b4b..8cd81113d3 100644 --- a/axe-urls +++ b/axe-urls @@ -1,7 +1,7 @@ http://localhost:3000, -http://localhost:3000/activity-reports/activity-summary, -http://localhost:3000/activity-reports/topics-resources, -http://localhost:3000/activity-reports/goals-objectives, -http://localhost:3000/activity-reports/next-steps, -http://localhost:3000/activity-reports/review, +http://localhost:3000/activity-reports/new/activity-summary, +http://localhost:3000/activity-reports/new/topics-resources, +http://localhost:3000/activity-reports/new/goals-objectives, +http://localhost:3000/activity-reports/new/next-steps, +http://localhost:3000/activity-reports/new/review, http://localhost:3000/admin diff --git a/cucumber/features/steps/activityReportSteps.js b/cucumber/features/steps/activityReportSteps.js index b524bae1bc..089b147136 100644 --- a/cucumber/features/steps/activityReportSteps.js +++ b/cucumber/features/steps/activityReportSteps.js @@ -7,11 +7,12 @@ const scope = require('../support/scope'); Given('I am on the activity reports page', async () => { const page = scope.context.currentPage; - const selector = 'a[href$="activity-reports"]'; + const selector = 'a[href$="activity-reports/new"]'; await Promise.all([ page.waitForNavigation(), page.click(selector), ]); + await scope.context.currentPage.waitForSelector('h1'); }); When('I select {string}', async (inputLabel) => { diff --git a/cucumber/features/steps/homePageSteps.js b/cucumber/features/steps/homePageSteps.js index 7a53b3e102..38dfd55d64 100644 --- a/cucumber/features/steps/homePageSteps.js +++ b/cucumber/features/steps/homePageSteps.js @@ -22,7 +22,7 @@ Given('I am logged in', async () => { const loginLinkSelector = 'a[href$="api/login"]'; // const homeLinkSelector = 'a[href$="/"]'; - const activityReportsSelector = 'a[href$="activity-reports"]'; + const activityReportsSelector = 'a[href$="activity-reports/new"]'; await page.goto(scope.uri); await page.waitForSelector('em'); // Page title @@ -52,7 +52,7 @@ Then('I see {string} message', async (string) => { Then('I see {string} link', async (string) => { const page = scope.context.currentPage; - const selector = 'a[href$="activity-reports"]'; + const selector = 'a[href$="activity-reports/new"]'; await page.waitForSelector(selector); const value = await page.$eval(selector, (el) => el.textContent); diff --git a/docs/openapi/paths/activity-reports/activity-recipients.yaml b/docs/openapi/paths/activity-reports/activity-recipients.yaml index 779bb12500..eeef6782ba 100644 --- a/docs/openapi/paths/activity-reports/activity-recipients.yaml +++ b/docs/openapi/paths/activity-reports/activity-recipients.yaml @@ -14,9 +14,14 @@ get: type: object properties: grants: - type: array - items: - $ref: '../../index.yaml#/components/schemas/activityRecipient' + type: object + properties: + name: + type: string + grants: + type: array + items: + $ref: '../../index.yaml#/components/schemas/activityRecipient' nonGrantees: type: array items: diff --git a/frontend/package.json b/frontend/package.json index a4d89d55aa..621c875b96 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -10,6 +10,7 @@ "@fortawesome/react-fontawesome": "^0.1.11", "@testing-library/jest-dom": "^4.2.4", "@trussworks/react-uswds": "^1.9.1", + "@use-it/interval": "^1.0.0", "http-proxy-middleware": "^1.0.5", "lodash": "^4.17.20", "moment": "^2.29.1", @@ -32,6 +33,7 @@ "react-stickynode": "^3.0.4", "react-with-direction": "^1.3.1", "url-join": "^4.0.1", + "use-deep-compare-effect": "^1.6.1", "uswds": "^2.9.0" }, "engines": { diff --git a/frontend/src/App.js b/frontend/src/App.js index f12dcc30f4..c992efaef2 100644 --- a/frontend/src/App.js +++ b/frontend/src/App.js @@ -74,7 +74,7 @@ function App() { )} /> ( )} diff --git a/frontend/src/components/DatePicker.js b/frontend/src/components/DatePicker.js index b1f66b1343..beab959f6c 100644 --- a/frontend/src/components/DatePicker.js +++ b/frontend/src/components/DatePicker.js @@ -89,8 +89,8 @@ DateInput.propTypes = { }; DateInput.defaultProps = { - minDate: undefined, - maxDate: undefined, + minDate: '', + maxDate: '', disabled: false, openUp: false, required: true, diff --git a/frontend/src/components/Header.js b/frontend/src/components/Header.js index 496d5b7133..c37713f735 100644 --- a/frontend/src/components/Header.js +++ b/frontend/src/components/Header.js @@ -14,7 +14,7 @@ function Header({ authenticated, admin }) { Home , - + Activity Reports , ]; diff --git a/frontend/src/components/MultiSelect.js b/frontend/src/components/MultiSelect.js index 999dbef9e8..94353d0064 100644 --- a/frontend/src/components/MultiSelect.js +++ b/frontend/src/components/MultiSelect.js @@ -1,3 +1,25 @@ +/* + This multiselect component uses react-select. React select expects options and selected + values to be in a specific format, arrays of `{ label: x, value: y }` items. Sometimes + we want to just push in and pull out simple arrays of strings instead of these objects. + Dealing with arrays of strings is easier than arrays of objects. The `simple` prop being + true makes this component look for values that are arrays of strings (other primitives may + work, haven't tried though). In simple mode we must convert the selected array of strings to + the object react-select expects before passing the value to react-select (Right below the + render). When an "onChange" event happens we have to convert from the react-select object back + to an array of strings ( { - const newValue = e ? e.map((v) => v.value) : null; - onChange(newValue); + onChange={(event) => { + if (event) { + onChange(event, controllerOnChange); + } else { + controllerOnChange(event); + } }} styles={styles} components={{ DropdownIndicator }} @@ -92,15 +144,26 @@ function MultiSelect({ ); } +const value = PropTypes.oneOfType([ + PropTypes.string, + PropTypes.number, +]); + MultiSelect.propTypes = { label: PropTypes.string.isRequired, name: PropTypes.string.isRequired, + labelProperty: PropTypes.string, + valueProperty: PropTypes.string, + simple: PropTypes.bool, options: PropTypes.arrayOf( PropTypes.shape({ - value: PropTypes.oneOfType([ - PropTypes.string, - PropTypes.number, - ]).isRequired, + value, + options: PropTypes.arrayOf( + PropTypes.shape({ + label: PropTypes.string.isRequired, + value: value.isRequired, + }), + ), label: PropTypes.string.isRequired, }), ).isRequired, @@ -113,6 +176,9 @@ MultiSelect.propTypes = { MultiSelect.defaultProps = { disabled: false, required: true, + simple: true, + labelProperty: 'label', + valueProperty: 'value', }; export default MultiSelect; diff --git a/frontend/src/components/Navigator/__tests__/index.js b/frontend/src/components/Navigator/__tests__/index.js index d02ab393a4..85c1f481f4 100644 --- a/frontend/src/components/Navigator/__tests__/index.js +++ b/frontend/src/components/Navigator/__tests__/index.js @@ -1,6 +1,5 @@ import '@testing-library/jest-dom'; import React from 'react'; -import { MemoryRouter } from 'react-router'; import userEvent from '@testing-library/user-event'; import { render, screen, waitFor, within, @@ -14,6 +13,7 @@ const pages = [ position: 1, path: 'first', label: 'first page', + review: false, render: (hookForm) => ( ( (
@@ -53,17 +54,16 @@ describe('Navigator', () => { // eslint-disable-next-line arrow-body-style const renderNavigator = (currentPage = 'first', onSubmit = () => {}, updatePage = () => {}) => { render( - - - , + {}} + />, ); }; @@ -71,7 +71,7 @@ describe('Navigator', () => { renderNavigator(); const firstInput = screen.getByTestId('first'); userEvent.click(firstInput); - const first = await screen.findByRole('link', { name: 'first page' }); + const first = await screen.findByRole('button', { name: 'first page' }); await waitFor(() => expect(within(first).getByText('In progress')).toBeVisible()); }); @@ -89,10 +89,10 @@ describe('Navigator', () => { await waitFor(() => expect(onSubmit).toHaveBeenCalled()); }); - it('changes navigator state to complete when "continuing"', async () => { - renderNavigator(); - userEvent.click(screen.getByRole('button', { name: 'Continue' })); - const first = await screen.findByRole('link', { name: 'first page' }); - await waitFor(() => expect(within(first).getByText('Complete')).toBeVisible()); + it('calls updatePage on navigation', async () => { + const updatePage = jest.fn(); + renderNavigator('second', () => {}, updatePage); + userEvent.click(screen.getByRole('button', { name: 'first page' })); + await waitFor(() => expect(updatePage).toHaveBeenCalledWith(1)); }); }); diff --git a/frontend/src/components/Navigator/components/Form.js b/frontend/src/components/Navigator/components/Form.js deleted file mode 100644 index 52082ac54f..0000000000 --- a/frontend/src/components/Navigator/components/Form.js +++ /dev/null @@ -1,71 +0,0 @@ -/* - Content section for the navigator. It lets the navigator know when the form - becomes dirty so the navigator can update the navigator state. Also data from - the form is sent up when unmounted to be saved in the navigator component. -*/ -import React, { useEffect } from 'react'; -import PropTypes from 'prop-types'; -import { Form as UswdsForm, Button } from '@trussworks/react-uswds'; -import { useForm } from 'react-hook-form'; - -function Form({ - initialData, onContinue, onDirty, saveForm, renderForm, -}) { - /* - When the form unmounts we want to send any data in the form - to the parent component so the data can be saved in state - there. UseEffect callbacks run in order. If we place the - unmount save after "useForm" the form fields may be removed - from the DOM before we save their values. This means we have - to get a reference to the "getValues" method and place the - unmount save before the "useForm". See https://github.com/react-hook-form/react-hook-form/issues/494#issuecomment-552860874 - */ - const getValuesRef = React.useRef(null); - - useEffect(() => { - const onUnmount = () => { - if (getValuesRef.current) { - saveForm(getValuesRef.current()); - } - }; - return onUnmount; - }, [saveForm]); - - const hookForm = useForm({ - mode: 'onChange', - defaultValues: initialData, - }); - - const { - formState, - handleSubmit, - getValues, - } = hookForm; - - useEffect(() => { - onDirty(formState.isDirty); - }, [formState.isDirty, onDirty]); - - getValuesRef.current = getValues; - - return ( - - {renderForm(hookForm)} - - - ); -} - -Form.propTypes = { - initialData: PropTypes.shape({}), - onContinue: PropTypes.func.isRequired, - onDirty: PropTypes.func.isRequired, - saveForm: PropTypes.func.isRequired, - renderForm: PropTypes.func.isRequired, -}; - -Form.defaultProps = { - initialData: {}, -}; - -export default Form; diff --git a/frontend/src/components/Navigator/components/SideNav.js b/frontend/src/components/Navigator/components/SideNav.js index 70234c3235..f1e172ff23 100644 --- a/frontend/src/components/Navigator/components/SideNav.js +++ b/frontend/src/components/Navigator/components/SideNav.js @@ -6,9 +6,9 @@ import React from 'react'; import PropTypes from 'prop-types'; import Sticky from 'react-stickynode'; -import { Tag } from '@trussworks/react-uswds'; +import { Button, Tag, Alert } from '@trussworks/react-uswds'; import { useMediaQuery } from 'react-responsive'; -import { NavLink } from 'react-router-dom'; +import moment from 'moment'; import Container from '../../Container'; import './SideNav.css'; @@ -32,15 +32,16 @@ const tagClass = (state) => { }; function SideNav({ - pages, skipTo, skipToMessage, + pages, skipTo, skipToMessage, lastSaveTime, errorMessage, }) { const isMobile = useMediaQuery({ maxWidth: 640 }); const navItems = () => pages.map((page) => (
  • - {page.label} @@ -52,7 +53,7 @@ function SideNav({ )} - +
  • )); @@ -64,6 +65,20 @@ function SideNav({ {navItems()} + {errorMessage + && ( + + {errorMessage} + + )} + {lastSaveTime && !errorMessage + && ( + + This report was automatically saved on + {' '} + {lastSaveTime.format('MM/DD/YYYY [at] h:mm a')} + + )} ); } @@ -73,11 +88,19 @@ SideNav.propTypes = { PropTypes.shape({ label: PropTypes.string.isRequired, state: PropTypes.string, - path: PropTypes.string.isRequired, + current: PropTypes.bool.isRequired, + onNavigation: PropTypes.func.isRequired, }), ).isRequired, skipTo: PropTypes.string.isRequired, skipToMessage: PropTypes.string.isRequired, + errorMessage: PropTypes.string, + lastSaveTime: PropTypes.instanceOf(moment), +}; + +SideNav.defaultProps = { + lastSaveTime: undefined, + errorMessage: undefined, }; export default SideNav; diff --git a/frontend/src/components/Navigator/components/__tests__/Form.js b/frontend/src/components/Navigator/components/__tests__/Form.js deleted file mode 100644 index 73d9c8af61..0000000000 --- a/frontend/src/components/Navigator/components/__tests__/Form.js +++ /dev/null @@ -1,60 +0,0 @@ -import '@testing-library/jest-dom'; -import React from 'react'; -import userEvent from '@testing-library/user-event'; -import { render, screen, act } from '@testing-library/react'; - -import Form from '../Form'; - -const renderForm = (saveForm, onContinue, onDirty) => render( -
    ( - - )} - />, -); - -describe('Form', () => { - it('calls saveForm when unmounted', () => { - const saveForm = jest.fn(); - const onContinue = jest.fn(); - const dirty = jest.fn(); - const { unmount } = renderForm(saveForm, onContinue, dirty); - unmount(); - expect(saveForm).toHaveBeenCalled(); - }); - - it('calls onContinue when submitting', async () => { - const saveForm = jest.fn(); - const onContinue = jest.fn(); - const dirty = jest.fn(); - - renderForm(saveForm, onContinue, dirty); - const submit = screen.getByRole('button'); - await act(async () => { - userEvent.click(submit); - }); - - expect(onContinue).toHaveBeenCalled(); - }); - - it('calls onDirty when the form is dirty', async () => { - const saveForm = jest.fn(); - const onContinue = jest.fn(); - const dirty = jest.fn(); - - renderForm(saveForm, onContinue, dirty); - const submit = screen.getByTestId('input'); - userEvent.click(submit); - - expect(dirty).toHaveBeenCalled(); - }); -}); diff --git a/frontend/src/components/Navigator/components/__tests__/SideNav.js b/frontend/src/components/Navigator/components/__tests__/SideNav.js index 697ef82f6e..d4b53b459c 100644 --- a/frontend/src/components/Navigator/components/__tests__/SideNav.js +++ b/frontend/src/components/Navigator/components/__tests__/SideNav.js @@ -1,6 +1,4 @@ import '@testing-library/jest-dom'; -import { Router } from 'react-router'; -import { createMemoryHistory } from 'history'; import React from 'react'; import userEvent from '@testing-library/user-event'; import { @@ -13,31 +11,29 @@ import { } from '../../constants'; describe('SideNav', () => { - const renderNav = (state, path = '/path') => { - const history = createMemoryHistory(); + const renderNav = (state, onNavigation = () => {}, current = false) => { const pages = [ { label: 'test', state, - path: 'test', + current, + onNavigation, }, { label: 'second', state: '', - path: 'second', + current, + onNavigation, }, ]; - history.push(path); + render( - - - , + , ); - return history; }; describe('displays the correct status', () => { @@ -70,16 +66,17 @@ describe('SideNav', () => { }); }); - it('clicking a nav item navigates to that item', () => { - const history = renderNav(NOT_STARTED); + it('clicking a nav item calls onNavigation', () => { + const onNav = jest.fn(); + renderNav(NOT_STARTED, onNav); const notStarted = screen.getByText('Not started'); userEvent.click(notStarted); - expect(history.location.pathname).toBe('/test'); + expect(onNav).toHaveBeenCalled(); }); it('the currently selected page has the current class', () => { - renderNav(SUBMITTED, '/test'); - const submitted = screen.getByRole('link', { name: 'test' }); + renderNav(SUBMITTED, () => {}, true); + const submitted = screen.getByRole('button', { name: 'test' }); expect(submitted).toHaveClass('smart-hub--navigator-link-active'); }); }); diff --git a/frontend/src/components/Navigator/index.js b/frontend/src/components/Navigator/index.js index 104eaa5d2b..9a4a23baec 100644 --- a/frontend/src/components/Navigator/index.js +++ b/frontend/src/components/Navigator/index.js @@ -3,10 +3,14 @@ on the left hand side with each page of the form listed. Clicking on an item in the nav list will display that item in the content section. The navigator keeps track of the "state" of each page. */ -import React, { useState, useCallback } from 'react'; +import React, { useState } from 'react'; import PropTypes from 'prop-types'; import _ from 'lodash'; -import { Grid } from '@trussworks/react-uswds'; +import { useForm } from 'react-hook-form'; +import { Form, Button, Grid } from '@trussworks/react-uswds'; +import useDeepCompareEffect from 'use-deep-compare-effect'; +import useInterval from '@use-it/interval'; +import moment from 'moment'; import Container from '../Container'; @@ -14,57 +18,103 @@ import { IN_PROGRESS, COMPLETE, SUBMITTED, } from './constants'; import SideNav from './components/SideNav'; -import Form from './components/Form'; import IndicatorHeader from './components/IndicatorHeader'; function Navigator({ - defaultValues, + initialData, pages, onFormSubmit, - initialPageState, submitted, currentPage, updatePage, additionalData, + onSave, + autoSaveInterval, }) { - const [formData, updateFormData] = useState(defaultValues); - const [pageState, updatePageState] = useState(initialPageState); + const [formData, updateFormData] = useState(initialData); + const [errorMessage, updateErrorMessage] = useState(); + const [lastSaveTime, updateLastSaveTime] = useState(); + const { pageState } = formData; + const page = pages.find((p) => p.path === currentPage); const submittedNavState = submitted ? SUBMITTED : null; const allComplete = _.every(pageState, (state) => state === COMPLETE); - const navigatorPages = pages.map((p) => { - const state = p.review ? submittedNavState : pageState[p.position]; - return { - label: p.label, - path: p.path, - state, - }; + const hookForm = useForm({ + mode: 'onChange', + defaultValues: formData, }); - const onDirty = useCallback((isDirty) => { - updatePageState((oldNavigatorState) => { - const newNavigatorState = { ...oldNavigatorState }; - newNavigatorState[page.position] = isDirty ? IN_PROGRESS : oldNavigatorState[page.position]; - return newNavigatorState; - }); - }, [updatePageState, page.position]); + const { + formState, + handleSubmit, + getValues, + reset, + } = hookForm; - const onSaveForm = useCallback((newData) => { - updateFormData((oldData) => ({ ...oldData, ...newData })); - }, [updateFormData]); + const { isDirty, isValid } = formState; - const onContinue = () => { - const newNavigatorState = { ...pageState }; - newNavigatorState[page.position] = COMPLETE; - updatePageState(newNavigatorState); - updatePage(page.position + 1); + const newNavigatorState = (completed) => { + if (page.review) { + return pageState; + } + + const newPageState = { ...pageState }; + if (completed) { + newPageState[page.position] = COMPLETE; + } else { + newPageState[page.position] = isDirty ? IN_PROGRESS : pageState[page.position]; + } + return newPageState; + }; + + const onSaveForm = async (completed) => { + const data = { ...formData, ...getValues(), pageState: newNavigatorState(completed) }; + try { + updateFormData(data); + const result = await onSave(data); + if (result) { + updateLastSaveTime(moment()); + } + updateErrorMessage(); + } catch (error) { + // eslint-disable-next-line no-console + console.log(error); + updateErrorMessage('Unable to save activity report'); + } }; - const onSubmit = (data) => { - onFormSubmit(formData, data); + const navigateToPage = (index, completed) => { + onSaveForm(completed); + updatePage(index); }; + const onContinue = () => { + navigateToPage(page.position + 1, true); + }; + + useInterval(() => { + onSaveForm(false); + }, autoSaveInterval); + + // A new form page is being shown so we need to reset `react-hook-form` so validations are + // reset and the proper values are placed inside inputs + useDeepCompareEffect(() => { + reset(formData); + }, [currentPage, reset, formData]); + + const navigatorPages = pages.map((p) => { + const current = p.position === page.position; + const stateOfPage = current ? IN_PROGRESS : pageState[p.position]; + const state = p.review ? submittedNavState : stateOfPage; + return { + label: p.label, + onNavigation: () => navigateToPage(p.position), + state, + current, + }; + }); + return ( @@ -72,12 +122,15 @@ function Navigator({ skipTo="navigator-form" skipToMessage="Skip to report content" pages={navigatorPages} + lastSaveTime={lastSaveTime} + errorMessage={errorMessage} + navigateToPage={navigateToPage} /> @@ -103,24 +155,29 @@ function Navigator({ } Navigator.propTypes = { - defaultValues: PropTypes.shape({}), + initialData: PropTypes.shape({}), onFormSubmit: PropTypes.func.isRequired, - initialPageState: PropTypes.shape({}).isRequired, submitted: PropTypes.bool.isRequired, + onSave: PropTypes.func.isRequired, pages: PropTypes.arrayOf( PropTypes.shape({ + review: PropTypes.bool.isRequired, + position: PropTypes.number.isRequired, + path: PropTypes.string.isRequired, render: PropTypes.func.isRequired, label: PropTypes.isRequired, }), ).isRequired, currentPage: PropTypes.string.isRequired, updatePage: PropTypes.func.isRequired, + autoSaveInterval: PropTypes.number, additionalData: PropTypes.shape({}), }; Navigator.defaultProps = { - defaultValues: {}, + initialData: {}, additionalData: {}, + autoSaveInterval: 1000 * 60 * 2, }; export default Navigator; diff --git a/frontend/src/fetchers/activityReports.js b/frontend/src/fetchers/activityReports.js index 243ac7b9be..1693815d10 100644 --- a/frontend/src/fetchers/activityReports.js +++ b/frontend/src/fetchers/activityReports.js @@ -1,30 +1,36 @@ import join from 'url-join'; +import { get, put, post } from './index'; const activityReportUrl = join('/', 'api', 'activity-reports'); -const callApi = async (url) => { - const res = await fetch(url, { - credentials: 'same-origin', - }); - if (!res.ok) { - throw new Error(res.statusText); - } - return res; -}; - export const fetchApprovers = async () => { - const res = await callApi(join(activityReportUrl, 'approvers')); + const res = await get(join(activityReportUrl, 'approvers')); return res.json(); }; -export const submitReport = async (data, extraData) => { - const url = join(activityReportUrl, 'submit'); - await fetch(url, { - method: 'POST', - credentials: 'same-origin', - body: JSON.stringify({ - report: data, - metaData: extraData, - }), +export const submitReport = async (reportId, data) => { + const url = join(activityReportUrl, reportId, 'submit'); + await post(url, { + report: data, }); }; + +export const saveReport = async (reportId, data) => { + const report = await put(join(activityReportUrl, reportId.toString(10)), data); + return report.json(); +}; + +export const createReport = async (data) => { + const report = await post(activityReportUrl, data); + return report.json(); +}; + +export const getReport = async (reportId) => { + const report = await get(join(activityReportUrl, reportId.toString(10))); + return report.json(); +}; + +export const getRecipients = async () => { + const recipients = await get(join(activityReportUrl, 'activity-recipients')); + return recipients.json(); +}; diff --git a/frontend/src/fetchers/index.js b/frontend/src/fetchers/index.js index 781fceb9d6..a66cdf6e53 100644 --- a/frontend/src/fetchers/index.js +++ b/frontend/src/fetchers/index.js @@ -22,3 +22,18 @@ export const put = async (url, data) => { } return res; }; + +export const post = async (url, data) => { + const res = await fetch(url, { + method: 'POST', + credentials: 'same-origin', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(data), + }); + if (!res.ok) { + throw new Error(res.statusText); + } + return res; +}; diff --git a/frontend/src/pages/ActivityReport/Pages/activitySummary.js b/frontend/src/pages/ActivityReport/Pages/activitySummary.js index 5cb9fda917..9297fc9960 100644 --- a/frontend/src/pages/ActivityReport/Pages/activitySummary.js +++ b/frontend/src/pages/ActivityReport/Pages/activitySummary.js @@ -8,53 +8,13 @@ import { import DatePicker from '../../../components/DatePicker'; import MultiSelect from '../../../components/MultiSelect'; - -const grantees = [ - 'Grantee Name 1', - 'Grantee Name 2', - 'Grantee Name 3', -]; - -const nonGrantees = [ - 'CCDF / Child Care Administrator', - 'Head Start Collaboration Office', - 'QRIS System', - 'Regional Head Start Association', - 'Regional TTA/Other Specialists', - 'State CCR&R', - 'State Early Learning Standards', - 'State Education System', - 'State Health System', - 'State Head Start Association', - 'State Professional Development / Continuing Education', -]; - -const reasons = [ - 'reason 1', - 'reason 2', -]; - -const otherUsers = [ - 'User 1', - 'User 2', - 'User 3', -]; - -const programTypes = [ - 'program type 1', - 'program type 2', - 'program type 3', - 'program type 4', - 'program type 5', -]; - -const targetPopulations = [ - 'target pop 1', - 'target pop 2', - 'target pop 3', - 'target pop 4', - 'target pop 5', -]; +import { + otherParticipants, + reasons, + otherUsers, + programTypes, + targetPopulations, +} from './constants'; const ActivitySummary = ({ register, @@ -62,23 +22,38 @@ const ActivitySummary = ({ setValue, control, getValues, + recipients, }) => { - const participantSelection = watch('participant-category'); - const startDate = watch('start-date'); - const endDate = watch('end-date'); + const activityRecipientType = watch('activityRecipientType'); + const startDate = watch('startDate'); + const endDate = watch('endDate'); + const { nonGrantees: rawNonGrantees, grants: rawGrants } = recipients; + + const grants = rawGrants.map((grantee) => ({ + label: grantee.name, + options: grantee.grants.map((grant) => ({ + value: grant.activityRecipientId, + label: grant.name, + })), + })); - const disableParticipant = participantSelection === ''; - const nonGranteeSelected = participantSelection === 'non-grantee'; - const participants = nonGranteeSelected ? nonGrantees : grantees; - const previousParticipantSelection = useRef(participantSelection); - const participantLabel = nonGranteeSelected ? 'Non-grantee name(s)' : 'Grantee name(s)'; + const nonGrantees = rawNonGrantees.map((nonGrantee) => ({ + label: nonGrantee.name, + value: nonGrantee.activityRecipientId, + })); + + const disableRecipients = activityRecipientType === ''; + const nonGranteeSelected = activityRecipientType === 'non-grantee'; + const selectedRecipients = nonGranteeSelected ? nonGrantees : grants; + const previousActivityRecipientType = useRef(activityRecipientType); + const recipientLabel = nonGranteeSelected ? 'Non-grantee name(s)' : 'Grantee name(s)'; useEffect(() => { - if (previousParticipantSelection.current !== participantSelection) { - setValue('grantees', []); - previousParticipantSelection.current = participantSelection; + if (previousActivityRecipientType.current !== activityRecipientType) { + setValue('activityParticipants', []); + previousActivityRecipientType.current = activityRecipientType; } - }, [participantSelection, setValue]); + }, [activityRecipientType, setValue]); const renderCheckbox = (name, value, label) => (
    ({ value: participant, label: participant })) - } + valueProperty="activityRecipientId" + labelProperty="name" + simple={false} + options={selectedRecipients} />
    @@ -137,40 +113,25 @@ const ActivitySummary = ({ label="Collaborating Specialists" control={control} required={false} - options={ - otherUsers.map((user) => ({ value: user, label: user })) - } + options={otherUsers.map((user) => ({ value: user, label: user }))} />
    ({ value: user, label: user })) - } + options={programTypes.map((user) => ({ value: user, label: user }))} />
    ({ value: user, label: user })) - } - /> -
    -
    - - ({ value: user, label: user }))} />
    @@ -183,7 +144,7 @@ const ActivitySummary = ({ Use "Regional Office" for TTA not requested by grantee @@ -205,9 +166,7 @@ const ActivitySummary = ({ name="reason" label="What was the reason for this activity?" control={control} - options={ - reasons.map((reason) => ({ value: reason, label: reason })) - } + options={reasons.map((reason) => ({ value: reason, label: reason }))} />
    @@ -219,7 +178,7 @@ const ActivitySummary = ({ - + @@ -248,8 +207,8 @@ const ActivitySummary = ({
    What TTA was provided? - {renderCheckbox('activity-type', 'training', 'Training')} - {renderCheckbox('activity-type', 'technical-assistance', 'Technical Assistance')} + {renderCheckbox('ttaType', 'training', 'Training')} + {renderCheckbox('ttaType', 'technical-assistance', 'Technical Assistance')}
    @@ -257,16 +216,16 @@ const ActivitySummary = ({ How was this activity conducted? (select at least one)
    ({ value: participant, label: participant })) + otherParticipants.map((participant) => ({ value: participant, label: participant })) } />
    - +
    @@ -307,6 +266,25 @@ ActivitySummary.propTypes = { watch: PropTypes.func.isRequired, setValue: PropTypes.func.isRequired, getValues: PropTypes.func.isRequired, + recipients: PropTypes.shape({ + grants: PropTypes.arrayOf( + PropTypes.shape({ + name: PropTypes.string.isRequired, + grants: PropTypes.arrayOf( + PropTypes.shape({ + name: PropTypes.string.isRequired, + participantId: PropTypes.number.isRequired, + }), + ), + }), + ), + nonGrantees: PropTypes.arrayOf( + PropTypes.shape({ + name: PropTypes.string.isRequired, + participantId: PropTypes.number.isRequired, + }), + ), + }).isRequired, // eslint-disable-next-line react/forbid-prop-types control: PropTypes.object.isRequired, }; @@ -316,13 +294,11 @@ const sections = [ title: 'Who was the activity for?', anchor: 'activity-for', items: [ - { label: 'Grantee or Non-grantee', name: 'participant-category' }, - { label: 'Grantee name(s)', name: 'grantees' }, - { label: 'Grantee number(s)', name: '' }, - { label: 'Collaborating specialist(s)', name: 'other-users' }, - { label: 'CDI', name: 'cdi' }, - { label: 'Program type(s)', name: 'program-types' }, - { label: 'Target Populations addressed', name: 'target-populations' }, + { label: 'Grantee or Non-grantee', name: 'activityRecipientType' }, + { label: 'Activity Participants', name: 'activityRecipients', path: 'name' }, + { label: 'Collaborating specialist(s)', name: 'otherUsers', path: 'label' }, + { label: 'Program type(s)', name: 'programTypes' }, + { label: 'Target Populations addressed', name: 'targetPopulations' }, ], }, { @@ -338,8 +314,8 @@ const sections = [ title: 'Activity date', anchor: 'date', items: [ - { label: 'Start date', name: 'start-date' }, - { label: 'End date', name: 'end-date' }, + { label: 'Start date', name: 'startDate' }, + { label: 'End date', name: 'endDate' }, { label: 'Duration', name: 'duration' }, ], }, @@ -347,8 +323,8 @@ const sections = [ title: 'Training or Technical Assistance', anchor: 'tta', items: [ - { label: 'TTA Provided', name: 'activity-type' }, - { label: 'Conducted', name: 'activity-method' }, + { label: 'TTA Provided', name: 'ttaType' }, + { label: 'Conducted', name: 'deliveryMethod' }, ], }, { @@ -356,7 +332,7 @@ const sections = [ anchor: 'other-participants', items: [ { label: 'Grantee participants', name: 'participants' }, - { label: 'Number of participants', name: 'number-of-participants' }, + { label: 'Number of participants', name: 'numberOfParticipants' }, ], }, ]; @@ -366,14 +342,17 @@ export default { label: 'Activity summary', path: 'activity-summary', sections, - render: (hookForm) => { + review: false, + render: (hookForm, additionalData) => { const { register, watch, setValue, getValues, control, } = hookForm; + const { recipients } = additionalData; return ( ( diff --git a/frontend/src/pages/ActivityReport/Pages/index.js b/frontend/src/pages/ActivityReport/Pages/index.js index 30c416273d..5bcc100ad2 100644 --- a/frontend/src/pages/ActivityReport/Pages/index.js +++ b/frontend/src/pages/ActivityReport/Pages/index.js @@ -26,7 +26,7 @@ const reviewPage = { review: true, label: 'Review and submit', path: 'review', - render: (allComplete, formData, submitted, onSubmit, additionalData) => ( + render: (allComplete, formData, submitted, onSubmit) => ( reviewItem(p.path, p.label, p.sections, formData)) } - initialData={additionalData} + initialData={formData} /> ), }; diff --git a/frontend/src/pages/ActivityReport/Pages/nextSteps.js b/frontend/src/pages/ActivityReport/Pages/nextSteps.js index de60140ff2..4f05a4d7a5 100644 --- a/frontend/src/pages/ActivityReport/Pages/nextSteps.js +++ b/frontend/src/pages/ActivityReport/Pages/nextSteps.js @@ -20,6 +20,7 @@ export default { position: 4, label: 'Next steps', path: 'next-steps', + review: false, sections, render: () => ( diff --git a/frontend/src/pages/ActivityReport/Pages/reviewItem.js b/frontend/src/pages/ActivityReport/Pages/reviewItem.js index 9b2bc1229a..29d5c1431f 100644 --- a/frontend/src/pages/ActivityReport/Pages/reviewItem.js +++ b/frontend/src/pages/ActivityReport/Pages/reviewItem.js @@ -21,8 +21,10 @@ const itemType = { path: PropTypes.string, value: PropTypes.oneOfType([ PropTypes.string, - PropTypes.arrayOf(PropTypes.string), + PropTypes.number, PropTypes.shape({}), + PropTypes.arrayOf(PropTypes.string), + PropTypes.arrayOf(PropTypes.shape({})), ]), }; diff --git a/frontend/src/pages/ActivityReport/Pages/topicsResources.js b/frontend/src/pages/ActivityReport/Pages/topicsResources.js index cb71828b5f..6f8e97ecb9 100644 --- a/frontend/src/pages/ActivityReport/Pages/topicsResources.js +++ b/frontend/src/pages/ActivityReport/Pages/topicsResources.js @@ -10,11 +10,7 @@ import { import MultiSelect from '../../../components/MultiSelect'; import FileUploader from '../../../components/FileUploader'; - -const topics = [ - 'first', - 'second', -]; +import { topics } from './constants'; const TopicsResources = ({ register, @@ -41,14 +37,14 @@ const TopicsResources = ({
    -