From bfa07b7af87b6addb77d4930e70aaf119228940b Mon Sep 17 00:00:00 2001 From: Josh Salisbury Date: Mon, 21 Dec 2020 15:12:52 -0600 Subject: [PATCH 1/7] Add review page accordions * Date picker now accepts a string MM/DD/YYYY and spits out the same * Each page defines sections that are used in the review accordion * Simplified how the activity report imports the pages for the report --- config/config.js | 2 + frontend/package.json | 1 + frontend/src/components/DatePicker.js | 47 +++--- frontend/src/components/MultiSelect.js | 5 +- .../components/Navigator/__tests__/index.js | 2 +- .../components/Navigator/components/Form.js | 2 +- .../Navigator/components/SideNav.css | 8 + frontend/src/components/Navigator/index.js | 25 ++-- frontend/src/fetchers/activityReports.js | 30 ++++ .../ActivityReport/Pages/ReviewSubmit.js | 128 ++++++++++++++-- .../Pages/__tests__/ReviewSubmit.js | 78 ++++++++++ .../Pages/__tests__/reviewItem.js | 78 ++++++++++ ...{ActivitySummary.js => activitySummary.js} | 77 +++++++++- ...{GoalsObjectives.js => goalsObjectives.js} | 11 +- .../src/pages/ActivityReport/Pages/index.js | 42 ++++++ .../Pages/{NextSteps.js => nextSteps.js} | 12 +- .../pages/ActivityReport/Pages/reviewItem.js | 138 ++++++++++++++++++ ...{TopicsResources.js => topicsResources.js} | 44 +++++- .../pages/ActivityReport/__tests__/index.js | 4 +- frontend/src/pages/ActivityReport/index.css | 28 ++++ frontend/src/pages/ActivityReport/index.js | 85 ++--------- frontend/yarn.lock | 7 + package.json | 6 +- src/routes/activityReports/handlers.js | 71 +++++++++ src/routes/activityReports/index.js | 15 ++ src/routes/apiDirectory.js | 2 + src/seeders/20201209172017-approvers.js | 98 +++++++++++++ 27 files changed, 909 insertions(+), 137 deletions(-) create mode 100644 frontend/src/fetchers/activityReports.js create mode 100644 frontend/src/pages/ActivityReport/Pages/__tests__/ReviewSubmit.js create mode 100644 frontend/src/pages/ActivityReport/Pages/__tests__/reviewItem.js rename frontend/src/pages/ActivityReport/Pages/{ActivitySummary.js => activitySummary.js} (83%) rename frontend/src/pages/ActivityReport/Pages/{GoalsObjectives.js => goalsObjectives.js} (60%) create mode 100644 frontend/src/pages/ActivityReport/Pages/index.js rename frontend/src/pages/ActivityReport/Pages/{NextSteps.js => nextSteps.js} (59%) create mode 100644 frontend/src/pages/ActivityReport/Pages/reviewItem.js rename frontend/src/pages/ActivityReport/Pages/{TopicsResources.js => topicsResources.js} (74%) create mode 100644 src/routes/activityReports/handlers.js create mode 100644 src/routes/activityReports/index.js create mode 100644 src/seeders/20201209172017-approvers.js diff --git a/config/config.js b/config/config.js index 46d3d4a52f..773ed35925 100644 --- a/config/config.js +++ b/config/config.js @@ -7,6 +7,7 @@ module.exports = { database: process.env.POSTGRES_DB, host: process.env.POSTGRES_HOST, dialect: 'postgres', + logging: true, }, test: { username: process.env.POSTGRES_USERNAME, @@ -22,5 +23,6 @@ module.exports = { database: process.env.POSTGRES_DB, host: process.env.POSTGRES_HOST, dialect: 'postgres', + logging: true, }, }; diff --git a/frontend/package.json b/frontend/package.json index 485c0e6db8..a4d89d55aa 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -25,6 +25,7 @@ "react-responsive": "^8.1.1", "react-router": "^5.2.0", "react-router-dom": "^5.2.0", + "react-router-hash-link": "^2.3.1", "react-router-prop-types": "^1.0.5", "react-scripts": "^3.4.4", "react-select": "^3.1.0", diff --git a/frontend/src/components/DatePicker.js b/frontend/src/components/DatePicker.js index 44100768a1..b1f66b1343 100644 --- a/frontend/src/components/DatePicker.js +++ b/frontend/src/components/DatePicker.js @@ -21,6 +21,8 @@ import moment from 'moment'; import './DatePicker.css'; +const dateFmt = 'MM/DD/YYYY'; + const DateInput = ({ control, label, minDate, name, disabled, maxDate, openUp, required, }) => { @@ -30,8 +32,8 @@ const DateInput = ({ const openDirection = openUp ? OPEN_UP : OPEN_DOWN; const isOutsideRange = (date) => { - const isBefore = minDate && date.isBefore(minDate); - const isAfter = maxDate && date.isAfter(maxDate); + const isBefore = minDate && date.isBefore(moment(minDate, dateFmt)); + const isAfter = maxDate && date.isAfter(moment(maxDate, dateFmt)); return isBefore || isAfter; }; @@ -41,23 +43,26 @@ const DateInput = ({
mm/dd/yyyy
( -
-
- )} + render={({ onChange, value, ref }) => { + const date = value ? moment(value, dateFmt) : null; + return ( +
+
+ ); + }} control={control} name={name} disabled={disabled} @@ -76,8 +81,8 @@ DateInput.propTypes = { control: PropTypes.object.isRequired, name: PropTypes.string.isRequired, label: PropTypes.string.isRequired, - minDate: PropTypes.instanceOf(moment), - maxDate: PropTypes.instanceOf(moment), + minDate: PropTypes.string, + maxDate: PropTypes.string, openUp: PropTypes.bool, disabled: PropTypes.bool, required: PropTypes.bool, diff --git a/frontend/src/components/MultiSelect.js b/frontend/src/components/MultiSelect.js index 28fbc9b32a..75f7b5436b 100644 --- a/frontend/src/components/MultiSelect.js +++ b/frontend/src/components/MultiSelect.js @@ -96,7 +96,10 @@ MultiSelect.propTypes = { name: PropTypes.string.isRequired, options: PropTypes.arrayOf( PropTypes.shape({ - value: PropTypes.string.isRequired, + value: PropTypes.oneOfType([ + PropTypes.string, + PropTypes.number, + ]).isRequired, label: PropTypes.string.isRequired, }), ).isRequired, diff --git a/frontend/src/components/Navigator/__tests__/index.js b/frontend/src/components/Navigator/__tests__/index.js index 2ec91ddd15..d02ab393a4 100644 --- a/frontend/src/components/Navigator/__tests__/index.js +++ b/frontend/src/components/Navigator/__tests__/index.js @@ -41,7 +41,7 @@ const pages = [ path: 'review', label: 'review page', review: true, - render: (allComplete, onSubmit) => ( + render: (allComplete, formData, submitted, onSubmit) => (
diff --git a/frontend/src/components/Navigator/components/Form.js b/frontend/src/components/Navigator/components/Form.js index a623bd3ed4..52082ac54f 100644 --- a/frontend/src/components/Navigator/components/Form.js +++ b/frontend/src/components/Navigator/components/Form.js @@ -51,7 +51,7 @@ function Form({ return ( {renderForm(hookForm)} - + ); } diff --git a/frontend/src/components/Navigator/components/SideNav.css b/frontend/src/components/Navigator/components/SideNav.css index 9672e27d84..685fc85f03 100644 --- a/frontend/src/components/Navigator/components/SideNav.css +++ b/frontend/src/components/Navigator/components/SideNav.css @@ -80,3 +80,11 @@ top: -2.5rem; transition: 0.2s ease-in-out; } + +.smart-hub--navigator-item:first-child .smart-hub--navigator-link-active { + border-top-right-radius: 4px; +} + +.smart-hub--navigator-item:last-child .smart-hub--navigator-link-active { + border-bottom-right-radius: 4px; +} \ No newline at end of file diff --git a/frontend/src/components/Navigator/index.js b/frontend/src/components/Navigator/index.js index 241e328bbf..104eaa5d2b 100644 --- a/frontend/src/components/Navigator/index.js +++ b/frontend/src/components/Navigator/index.js @@ -2,8 +2,6 @@ The navigator is a component used to show multiple form pages. It displays a stickied nav window 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. - In the future logic will be added to the navigator to prevent the complete form from being - submitted until every page is completed. */ import React, { useState, useCallback } from 'react'; import PropTypes from 'prop-types'; @@ -19,10 +17,6 @@ import SideNav from './components/SideNav'; import Form from './components/Form'; 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. -*/ function Navigator({ defaultValues, pages, @@ -31,6 +25,7 @@ function Navigator({ submitted, currentPage, updatePage, + additionalData, }) { const [formData, updateFormData] = useState(defaultValues); const [pageState, updatePageState] = useState(initialPageState); @@ -80,13 +75,12 @@ function Navigator({ /> - - ); @@ -122,10 +115,12 @@ Navigator.propTypes = { ).isRequired, currentPage: PropTypes.string.isRequired, updatePage: PropTypes.func.isRequired, + additionalData: PropTypes.shape({}), }; Navigator.defaultProps = { defaultValues: {}, + additionalData: {}, }; export default Navigator; diff --git a/frontend/src/fetchers/activityReports.js b/frontend/src/fetchers/activityReports.js new file mode 100644 index 0000000000..243ac7b9be --- /dev/null +++ b/frontend/src/fetchers/activityReports.js @@ -0,0 +1,30 @@ +import join from 'url-join'; + +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')); + 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, + }), + }); +}; diff --git a/frontend/src/pages/ActivityReport/Pages/ReviewSubmit.js b/frontend/src/pages/ActivityReport/Pages/ReviewSubmit.js index b41959458c..3a6a8892f1 100644 --- a/frontend/src/pages/ActivityReport/Pages/ReviewSubmit.js +++ b/frontend/src/pages/ActivityReport/Pages/ReviewSubmit.js @@ -1,24 +1,122 @@ -import React from 'react'; +import React, { useEffect, useState } from 'react'; import PropTypes from 'prop-types'; +import { + Form, Label, Fieldset, Textarea, Alert, Button, Accordion, +} from '@trussworks/react-uswds'; +import { useForm } from 'react-hook-form'; import { Helmet } from 'react-helmet'; -import { Button } from '@trussworks/react-uswds'; - -const ReviewSubmit = ({ allComplete, onSubmit }) => ( - <> - - Review and submit - -
- Review and submit -
- -
- -); + +import { fetchApprovers } from '../../../fetchers/activityReports'; +import MultiSelect from '../../../components/MultiSelect'; +import Container from '../../../components/Container'; + +const defaultValues = { + approvingManagers: null, + additionalNotes: null, +}; + +const ReviewSubmit = ({ + initialData, allComplete, onSubmit, submitted, reviewItems, +}) => { + const [loading, updateLoading] = useState(true); + const [possibleApprovers, updatePossibleApprovers] = useState([]); + + useEffect(() => { + updateLoading(true); + const fetch = async () => { + const approvers = await fetchApprovers(); + updatePossibleApprovers(approvers); + updateLoading(false); + }; + fetch(); + }, []); + + const { + handleSubmit, register, formState, control, + } = useForm({ + mode: 'onChange', + defaultValues: { ...defaultValues, ...initialData }, + }); + + const onFormSubmit = (data) => { + onSubmit(data); + }; + + const { + isValid, + } = formState; + + const valid = allComplete && isValid; + + return ( + <> + + Review and submit + + + +

Submit Report

+ {submitted + && ( + + Success +
+ This report was successfully submitted for approval +
+ )} + {!allComplete + && ( + + Incomplete report +
+ This report cannot be submitted until all sections are complete +
+ )} +
+
+ +