Skip to content

Commit 233a3d2

Browse files
authored
Merge pull request #96 from adhocteam/js-135-review-page-no-saving
Add review page accordions
2 parents a66470e + 7a17a64 commit 233a3d2

26 files changed

+852
-139
lines changed

.circleci/config.yml

+2-2
Original file line numberDiff line numberDiff line change
@@ -120,8 +120,8 @@ parameters:
120120
description: "Name of github branch that will deploy to dev"
121121
default: "main"
122122
type: string
123-
sandbox_git_branch: # change to feature branch to deploy to sandbox
124-
default: "js-52-assign-permissions-backend"
123+
sandbox_git_branch: # change to feature branch to test deployment
124+
default: "js-135-review-page-no-saving"
125125
type: string
126126
jobs:
127127
build_and_lint:

frontend/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
"react-responsive": "^8.1.1",
2626
"react-router": "^5.2.0",
2727
"react-router-dom": "^5.2.0",
28+
"react-router-hash-link": "^2.3.1",
2829
"react-router-prop-types": "^1.0.5",
2930
"react-scripts": "^3.4.4",
3031
"react-select": "^3.1.0",

frontend/src/components/DatePicker.js

+26-21
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ import moment from 'moment';
2121

2222
import './DatePicker.css';
2323

24+
const dateFmt = 'MM/DD/YYYY';
25+
2426
const DateInput = ({
2527
control, label, minDate, name, disabled, maxDate, openUp, required,
2628
}) => {
@@ -30,8 +32,8 @@ const DateInput = ({
3032
const openDirection = openUp ? OPEN_UP : OPEN_DOWN;
3133

3234
const isOutsideRange = (date) => {
33-
const isBefore = minDate && date.isBefore(minDate);
34-
const isAfter = maxDate && date.isAfter(maxDate);
35+
const isBefore = minDate && date.isBefore(moment(minDate, dateFmt));
36+
const isAfter = maxDate && date.isAfter(moment(maxDate, dateFmt));
3537

3638
return isBefore || isAfter;
3739
};
@@ -41,23 +43,26 @@ const DateInput = ({
4143
<Label id={labelId} htmlFor={name}>{label}</Label>
4244
<div className="usa-hint" id={hintId}>mm/dd/yyyy</div>
4345
<Controller
44-
render={({ onChange, value, ref }) => (
45-
<div className="display-flex smart-hub--date-picker-input">
46-
<button onClick={() => { updateFocus(true); }} disabled={disabled} tabIndex={-1} aria-label="open calendar" type="button" className="usa-date-picker__button margin-top-0" />
47-
<SingleDatePicker
48-
id={name}
49-
focused={isFocused}
50-
date={value}
51-
ref={ref}
52-
isOutsideRange={isOutsideRange}
53-
numberOfMonths={1}
54-
openDirection={openDirection}
55-
disabled={disabled}
56-
onDateChange={onChange}
57-
onFocusChange={({ focused }) => updateFocus(focused)}
58-
/>
59-
</div>
60-
)}
46+
render={({ onChange, value, ref }) => {
47+
const date = value ? moment(value, dateFmt) : null;
48+
return (
49+
<div className="display-flex smart-hub--date-picker-input">
50+
<button onClick={() => { updateFocus(true); }} disabled={disabled} tabIndex={-1} aria-label="open calendar" type="button" className="usa-date-picker__button margin-top-0" />
51+
<SingleDatePicker
52+
id={name}
53+
focused={isFocused}
54+
date={date}
55+
ref={ref}
56+
isOutsideRange={isOutsideRange}
57+
numberOfMonths={1}
58+
openDirection={openDirection}
59+
disabled={disabled}
60+
onDateChange={(d) => { onChange(d.format(dateFmt)); }}
61+
onFocusChange={({ focused }) => updateFocus(focused)}
62+
/>
63+
</div>
64+
);
65+
}}
6166
control={control}
6267
name={name}
6368
disabled={disabled}
@@ -76,8 +81,8 @@ DateInput.propTypes = {
7681
control: PropTypes.object.isRequired,
7782
name: PropTypes.string.isRequired,
7883
label: PropTypes.string.isRequired,
79-
minDate: PropTypes.instanceOf(moment),
80-
maxDate: PropTypes.instanceOf(moment),
84+
minDate: PropTypes.string,
85+
maxDate: PropTypes.string,
8186
openUp: PropTypes.bool,
8287
disabled: PropTypes.bool,
8388
required: PropTypes.bool,

frontend/src/components/MultiSelect.js

+5-1
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ function MultiSelect({
6565
}
6666
return (
6767
<Select
68+
className="margin-top-1"
6869
id={name}
6970
value={values}
7071
onChange={(e) => {
@@ -96,7 +97,10 @@ MultiSelect.propTypes = {
9697
name: PropTypes.string.isRequired,
9798
options: PropTypes.arrayOf(
9899
PropTypes.shape({
99-
value: PropTypes.string.isRequired,
100+
value: PropTypes.oneOfType([
101+
PropTypes.string,
102+
PropTypes.number,
103+
]).isRequired,
100104
label: PropTypes.string.isRequired,
101105
}),
102106
).isRequired,

frontend/src/components/Navigator/__tests__/index.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ const pages = [
4141
path: 'review',
4242
label: 'review page',
4343
review: true,
44-
render: (allComplete, onSubmit) => (
44+
render: (allComplete, formData, submitted, onSubmit) => (
4545
<div>
4646
<button type="button" data-testid="review" onClick={onSubmit}>Continue</button>
4747
</div>

frontend/src/components/Navigator/components/Form.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ function Form({
5151
return (
5252
<UswdsForm onSubmit={handleSubmit(onContinue)} className="smart-hub--form-large">
5353
{renderForm(hookForm)}
54-
<Button className="stepper-button" type="submit" disabled={!formState.isValid}>Continue</Button>
54+
<Button type="submit" disabled={!formState.isValid}>Continue</Button>
5555
</UswdsForm>
5656
);
5757
}

frontend/src/components/Navigator/components/SideNav.css

+8
Original file line numberDiff line numberDiff line change
@@ -80,3 +80,11 @@
8080
top: -2.5rem;
8181
transition: 0.2s ease-in-out;
8282
}
83+
84+
.smart-hub--navigator-item:first-child .smart-hub--navigator-link-active {
85+
border-top-right-radius: 4px;
86+
}
87+
88+
.smart-hub--navigator-item:last-child .smart-hub--navigator-link-active {
89+
border-bottom-right-radius: 4px;
90+
}

frontend/src/components/Navigator/index.js

+10-15
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@
22
The navigator is a component used to show multiple form pages. It displays a stickied nav window
33
on the left hand side with each page of the form listed. Clicking on an item in the nav list will
44
display that item in the content section. The navigator keeps track of the "state" of each page.
5-
In the future logic will be added to the navigator to prevent the complete form from being
6-
submitted until every page is completed.
75
*/
86
import React, { useState, useCallback } from 'react';
97
import PropTypes from 'prop-types';
@@ -19,10 +17,6 @@ import SideNav from './components/SideNav';
1917
import Form from './components/Form';
2018
import IndicatorHeader from './components/IndicatorHeader';
2119

22-
/*
23-
Get the current state of navigator items. Sets the currently selected item as "In Progress" and
24-
sets a "current" flag which the side nav uses to style the selected component as selected.
25-
*/
2620
function Navigator({
2721
defaultValues,
2822
pages,
@@ -31,6 +25,7 @@ function Navigator({
3125
submitted,
3226
currentPage,
3327
updatePage,
28+
additionalData,
3429
}) {
3530
const [formData, updateFormData] = useState(defaultValues);
3631
const [pageState, updatePageState] = useState(initialPageState);
@@ -80,13 +75,12 @@ function Navigator({
8075
/>
8176
</Grid>
8277
<Grid col={12} tablet={{ col: 6 }} desktop={{ col: 8 }}>
83-
<Container skipTopPadding>
84-
<div id="navigator-form">
85-
{page.review
86-
&& page.render(allComplete, onSubmit)}
87-
{!page.review
78+
<div id="navigator-form">
79+
{page.review
80+
&& page.render(allComplete, formData, submitted, onSubmit, additionalData)}
81+
{!page.review
8882
&& (
89-
<>
83+
<Container skipTopPadding>
9084
<IndicatorHeader
9185
currentStep={page.position}
9286
totalSteps={pages.filter((p) => !p.review).length}
@@ -100,10 +94,9 @@ function Navigator({
10094
saveForm={onSaveForm}
10195
renderForm={page.render}
10296
/>
103-
</>
97+
</Container>
10498
)}
105-
</div>
106-
</Container>
99+
</div>
107100
</Grid>
108101
</Grid>
109102
);
@@ -122,10 +115,12 @@ Navigator.propTypes = {
122115
).isRequired,
123116
currentPage: PropTypes.string.isRequired,
124117
updatePage: PropTypes.func.isRequired,
118+
additionalData: PropTypes.shape({}),
125119
};
126120

127121
Navigator.defaultProps = {
128122
defaultValues: {},
123+
additionalData: {},
129124
};
130125

131126
export default Navigator;
+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import join from 'url-join';
2+
3+
const activityReportUrl = join('/', 'api', 'activity-reports');
4+
5+
const callApi = async (url) => {
6+
const res = await fetch(url, {
7+
credentials: 'same-origin',
8+
});
9+
if (!res.ok) {
10+
throw new Error(res.statusText);
11+
}
12+
return res;
13+
};
14+
15+
export const fetchApprovers = async () => {
16+
const res = await callApi(join(activityReportUrl, 'approvers'));
17+
return res.json();
18+
};
19+
20+
export const submitReport = async (data, extraData) => {
21+
const url = join(activityReportUrl, 'submit');
22+
await fetch(url, {
23+
method: 'POST',
24+
credentials: 'same-origin',
25+
body: JSON.stringify({
26+
report: data,
27+
metaData: extraData,
28+
}),
29+
});
30+
};
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,122 @@
1-
import React from 'react';
1+
import React, { useEffect, useState } from 'react';
22
import PropTypes from 'prop-types';
3+
import {
4+
Form, Label, Fieldset, Textarea, Alert, Button, Accordion,
5+
} from '@trussworks/react-uswds';
6+
import { useForm } from 'react-hook-form';
37
import { Helmet } from 'react-helmet';
4-
import { Button } from '@trussworks/react-uswds';
5-
6-
const ReviewSubmit = ({ allComplete, onSubmit }) => (
7-
<>
8-
<Helmet>
9-
<title>Review and submit</title>
10-
</Helmet>
11-
<div>
12-
Review and submit
13-
<br />
14-
<Button disabled={!allComplete} onClick={onSubmit}>Submit</Button>
15-
</div>
16-
</>
17-
);
8+
9+
import { fetchApprovers } from '../../../fetchers/activityReports';
10+
import MultiSelect from '../../../components/MultiSelect';
11+
import Container from '../../../components/Container';
12+
13+
const defaultValues = {
14+
approvingManagers: null,
15+
additionalNotes: null,
16+
};
17+
18+
const ReviewSubmit = ({
19+
initialData, allComplete, onSubmit, submitted, reviewItems,
20+
}) => {
21+
const [loading, updateLoading] = useState(true);
22+
const [possibleApprovers, updatePossibleApprovers] = useState([]);
23+
24+
useEffect(() => {
25+
updateLoading(true);
26+
const fetch = async () => {
27+
const approvers = await fetchApprovers();
28+
updatePossibleApprovers(approvers);
29+
updateLoading(false);
30+
};
31+
fetch();
32+
}, []);
33+
34+
const {
35+
handleSubmit, register, formState, control,
36+
} = useForm({
37+
mode: 'onChange',
38+
defaultValues: { ...defaultValues, ...initialData },
39+
});
40+
41+
const onFormSubmit = (data) => {
42+
onSubmit(data);
43+
};
44+
45+
const {
46+
isValid,
47+
} = formState;
48+
49+
const valid = allComplete && isValid;
50+
51+
return (
52+
<>
53+
<Helmet>
54+
<title>Review and submit</title>
55+
</Helmet>
56+
<Accordion bordered={false} items={reviewItems} />
57+
<Container skipTopPadding className="margin-top-2 padding-top-2">
58+
<h3>Submit Report</h3>
59+
{submitted
60+
&& (
61+
<Alert noIcon className="margin-y-4" type="success">
62+
<b>Success</b>
63+
<br />
64+
This report was successfully submitted for approval
65+
</Alert>
66+
)}
67+
{!allComplete
68+
&& (
69+
<Alert noIcon className="margin-y-4" type="error">
70+
<b>Incomplete report</b>
71+
<br />
72+
This report cannot be submitted until all sections are complete
73+
</Alert>
74+
)}
75+
<Form className="smart-hub--form-large" onSubmit={handleSubmit(onFormSubmit)}>
76+
<Fieldset className="smart-hub--report-legend smart-hub--form-section" legend="Additional Notes">
77+
<Label htmlFor="additionalNotes">Additional notes for this activity (optional)</Label>
78+
<Textarea inputRef={register} id="additionalNotes" name="additionalNotes" />
79+
</Fieldset>
80+
<Fieldset className="smart-hub--report-legend smart-hub--form-section" legend="Review and submit report">
81+
<p className="margin-top-4">
82+
Submitting this form for approval means that you will no longer be in draft mode.
83+
Please review all information in each section before submitting to your manager for
84+
approval.
85+
</p>
86+
<MultiSelect
87+
label="Manager - you may choose more than one."
88+
name="approvingManagers"
89+
options={possibleApprovers.map((user) => ({
90+
label: user.name,
91+
value: user.id,
92+
}))}
93+
control={control}
94+
disabled={loading}
95+
/>
96+
</Fieldset>
97+
<Button type="submit" disabled={!valid}>Submit report for approval</Button>
98+
</Form>
99+
</Container>
100+
</>
101+
);
102+
};
18103

19104
ReviewSubmit.propTypes = {
105+
initialData: PropTypes.shape({}),
20106
allComplete: PropTypes.bool.isRequired,
21107
onSubmit: PropTypes.func.isRequired,
108+
submitted: PropTypes.bool.isRequired,
109+
reviewItems: PropTypes.arrayOf(
110+
PropTypes.shape({
111+
id: PropTypes.string.isRequired,
112+
title: PropTypes.string.isRequired,
113+
content: PropTypes.node.isRequired,
114+
}),
115+
).isRequired,
116+
};
117+
118+
ReviewSubmit.defaultProps = {
119+
initialData: {},
22120
};
23121

24122
export default ReviewSubmit;

0 commit comments

Comments
 (0)