Skip to content

Commit

Permalink
Merge pull request #96 from adhocteam/js-135-review-page-no-saving
Browse files Browse the repository at this point in the history
Add review page accordions
  • Loading branch information
jasalisbury authored Jan 5, 2021
2 parents a66470e + 7a17a64 commit 233a3d2
Show file tree
Hide file tree
Showing 26 changed files with 852 additions and 139 deletions.
4 changes: 2 additions & 2 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -120,8 +120,8 @@ parameters:
description: "Name of github branch that will deploy to dev"
default: "main"
type: string
sandbox_git_branch: # change to feature branch to deploy to sandbox
default: "js-52-assign-permissions-backend"
sandbox_git_branch: # change to feature branch to test deployment
default: "js-135-review-page-no-saving"
type: string
jobs:
build_and_lint:
Expand Down
1 change: 1 addition & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
47 changes: 26 additions & 21 deletions frontend/src/components/DatePicker.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}) => {
Expand All @@ -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;
};
Expand All @@ -41,23 +43,26 @@ const DateInput = ({
<Label id={labelId} htmlFor={name}>{label}</Label>
<div className="usa-hint" id={hintId}>mm/dd/yyyy</div>
<Controller
render={({ onChange, value, ref }) => (
<div className="display-flex smart-hub--date-picker-input">
<button onClick={() => { updateFocus(true); }} disabled={disabled} tabIndex={-1} aria-label="open calendar" type="button" className="usa-date-picker__button margin-top-0" />
<SingleDatePicker
id={name}
focused={isFocused}
date={value}
ref={ref}
isOutsideRange={isOutsideRange}
numberOfMonths={1}
openDirection={openDirection}
disabled={disabled}
onDateChange={onChange}
onFocusChange={({ focused }) => updateFocus(focused)}
/>
</div>
)}
render={({ onChange, value, ref }) => {
const date = value ? moment(value, dateFmt) : null;
return (
<div className="display-flex smart-hub--date-picker-input">
<button onClick={() => { updateFocus(true); }} disabled={disabled} tabIndex={-1} aria-label="open calendar" type="button" className="usa-date-picker__button margin-top-0" />
<SingleDatePicker
id={name}
focused={isFocused}
date={date}
ref={ref}
isOutsideRange={isOutsideRange}
numberOfMonths={1}
openDirection={openDirection}
disabled={disabled}
onDateChange={(d) => { onChange(d.format(dateFmt)); }}
onFocusChange={({ focused }) => updateFocus(focused)}
/>
</div>
);
}}
control={control}
name={name}
disabled={disabled}
Expand All @@ -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,
Expand Down
6 changes: 5 additions & 1 deletion frontend/src/components/MultiSelect.js
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ function MultiSelect({
}
return (
<Select
className="margin-top-1"
id={name}
value={values}
onChange={(e) => {
Expand Down Expand Up @@ -96,7 +97,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,
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/components/Navigator/__tests__/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ const pages = [
path: 'review',
label: 'review page',
review: true,
render: (allComplete, onSubmit) => (
render: (allComplete, formData, submitted, onSubmit) => (
<div>
<button type="button" data-testid="review" onClick={onSubmit}>Continue</button>
</div>
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/components/Navigator/components/Form.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ function Form({
return (
<UswdsForm onSubmit={handleSubmit(onContinue)} className="smart-hub--form-large">
{renderForm(hookForm)}
<Button className="stepper-button" type="submit" disabled={!formState.isValid}>Continue</Button>
<Button type="submit" disabled={!formState.isValid}>Continue</Button>
</UswdsForm>
);
}
Expand Down
8 changes: 8 additions & 0 deletions frontend/src/components/Navigator/components/SideNav.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
25 changes: 10 additions & 15 deletions frontend/src/components/Navigator/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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,
Expand All @@ -31,6 +25,7 @@ function Navigator({
submitted,
currentPage,
updatePage,
additionalData,
}) {
const [formData, updateFormData] = useState(defaultValues);
const [pageState, updatePageState] = useState(initialPageState);
Expand Down Expand Up @@ -80,13 +75,12 @@ function Navigator({
/>
</Grid>
<Grid col={12} tablet={{ col: 6 }} desktop={{ col: 8 }}>
<Container skipTopPadding>
<div id="navigator-form">
{page.review
&& page.render(allComplete, onSubmit)}
{!page.review
<div id="navigator-form">
{page.review
&& page.render(allComplete, formData, submitted, onSubmit, additionalData)}
{!page.review
&& (
<>
<Container skipTopPadding>
<IndicatorHeader
currentStep={page.position}
totalSteps={pages.filter((p) => !p.review).length}
Expand All @@ -100,10 +94,9 @@ function Navigator({
saveForm={onSaveForm}
renderForm={page.render}
/>
</>
</Container>
)}
</div>
</Container>
</div>
</Grid>
</Grid>
);
Expand All @@ -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;
30 changes: 30 additions & 0 deletions frontend/src/fetchers/activityReports.js
Original file line number Diff line number Diff line change
@@ -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,
}),
});
};
128 changes: 113 additions & 15 deletions frontend/src/pages/ActivityReport/Pages/ReviewSubmit.js
Original file line number Diff line number Diff line change
@@ -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 }) => (
<>
<Helmet>
<title>Review and submit</title>
</Helmet>
<div>
Review and submit
<br />
<Button disabled={!allComplete} onClick={onSubmit}>Submit</Button>
</div>
</>
);

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 (
<>
<Helmet>
<title>Review and submit</title>
</Helmet>
<Accordion bordered={false} items={reviewItems} />
<Container skipTopPadding className="margin-top-2 padding-top-2">
<h3>Submit Report</h3>
{submitted
&& (
<Alert noIcon className="margin-y-4" type="success">
<b>Success</b>
<br />
This report was successfully submitted for approval
</Alert>
)}
{!allComplete
&& (
<Alert noIcon className="margin-y-4" type="error">
<b>Incomplete report</b>
<br />
This report cannot be submitted until all sections are complete
</Alert>
)}
<Form className="smart-hub--form-large" onSubmit={handleSubmit(onFormSubmit)}>
<Fieldset className="smart-hub--report-legend smart-hub--form-section" legend="Additional Notes">
<Label htmlFor="additionalNotes">Additional notes for this activity (optional)</Label>
<Textarea inputRef={register} id="additionalNotes" name="additionalNotes" />
</Fieldset>
<Fieldset className="smart-hub--report-legend smart-hub--form-section" legend="Review and submit report">
<p className="margin-top-4">
Submitting this form for approval means that you will no longer be in draft mode.
Please review all information in each section before submitting to your manager for
approval.
</p>
<MultiSelect
label="Manager - you may choose more than one."
name="approvingManagers"
options={possibleApprovers.map((user) => ({
label: user.name,
value: user.id,
}))}
control={control}
disabled={loading}
/>
</Fieldset>
<Button type="submit" disabled={!valid}>Submit report for approval</Button>
</Form>
</Container>
</>
);
};

ReviewSubmit.propTypes = {
initialData: PropTypes.shape({}),
allComplete: PropTypes.bool.isRequired,
onSubmit: PropTypes.func.isRequired,
submitted: PropTypes.bool.isRequired,
reviewItems: PropTypes.arrayOf(
PropTypes.shape({
id: PropTypes.string.isRequired,
title: PropTypes.string.isRequired,
content: PropTypes.node.isRequired,
}),
).isRequired,
};

ReviewSubmit.defaultProps = {
initialData: {},
};

export default ReviewSubmit;
Loading

0 comments on commit 233a3d2

Please sign in to comment.