Skip to content

Commit

Permalink
Merge pull request #166 from adhocteam/js-6-frontend-validations
Browse files Browse the repository at this point in the history
Add validations and error messages to the activity report
  • Loading branch information
jasalisbury authored Feb 22, 2021
2 parents 15ee16d + 959f1e1 commit 6cd157f
Show file tree
Hide file tree
Showing 29 changed files with 624 additions and 340 deletions.
2 changes: 1 addition & 1 deletion .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ parameters:
default: "main"
type: string
sandbox_git_branch: # change to feature branch to test deployment
default: "js-117-selection-of-goals"
default: "js-6-frontend-validations"
type: string
jobs:
build_and_lint:
Expand Down
3 changes: 2 additions & 1 deletion frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"@fortawesome/fontawesome-svg-core": "^1.2.32",
"@fortawesome/free-solid-svg-icons": "^5.15.1",
"@fortawesome/react-fontawesome": "^0.1.11",
"@hookform/error-message": "^0.0.5",
"@testing-library/jest-dom": "^4.2.4",
"@trussworks/react-uswds": "^1.9.1",
"@use-it/interval": "^1.0.0",
Expand All @@ -21,7 +22,7 @@
"react-dom": "^16.14.0",
"react-dropzone": "^11.2.0",
"react-helmet": "^6.1.0",
"react-hook-form": "^6.9.0",
"react-hook-form": "^6.15.0",
"react-idle-timer": "^4.4.2",
"react-input-autosize": "^3.0.0",
"react-responsive": "^8.1.1",
Expand Down
6 changes: 6 additions & 0 deletions frontend/src/components/DatePicker.css
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,9 @@
.DateInput {
width: fit-content;
}

.usa-hint {
font-size: 14px;
margin-top: 5px;
margin-bottom: 10px;
}
16 changes: 7 additions & 9 deletions frontend/src/components/DatePicker.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,8 @@
2. react-dates had easily readable documentation and conveniences such as `maxDate`
and `minDate`. I couldn't find great docs using the USWDS datepicker javascript
*/

import React, { useState } from 'react';
import PropTypes from 'prop-types';
import { Label } from '@trussworks/react-uswds';
import { SingleDatePicker } from 'react-dates';
import { OPEN_UP, OPEN_DOWN } from 'react-dates/constants';
import { Controller } from 'react-hook-form';
Expand All @@ -24,9 +22,8 @@ import './DatePicker.css';
const dateFmt = 'MM/DD/YYYY';

const DateInput = ({
control, label, minDate, name, disabled, maxDate, openUp, required,
control, minDate, name, disabled, maxDate, openUp, required,
}) => {
const labelId = `${name}-id`;
const hintId = `${name}-hint`;
const [isFocused, updateFocus] = useState(false);
const openDirection = openUp ? OPEN_UP : OPEN_DOWN;
Expand All @@ -40,8 +37,7 @@ const DateInput = ({

return (
<>
<Label id={labelId} htmlFor={name}>{label}</Label>
<div className="usa-hint" id={hintId}>mm/dd/yyyy</div>
<div className="usa-hint font-body-2xs" id={hintId}>mm/dd/yyyy</div>
<Controller
render={({ onChange, value, ref }) => {
const date = value ? moment(value, dateFmt) : null;
Expand All @@ -57,7 +53,10 @@ const DateInput = ({
numberOfMonths={1}
openDirection={openDirection}
disabled={disabled}
onDateChange={(d) => { onChange(d.format(dateFmt)); }}
onDateChange={(d) => {
const newDate = d ? d.format(dateFmt) : d;
onChange(newDate);
}}
onFocusChange={({ focused }) => updateFocus(focused)}
/>
</div>
Expand All @@ -68,7 +67,7 @@ const DateInput = ({
disabled={disabled}
defaultValue={null}
rules={{
required,
required: required ? 'Please select a date' : false,
}}
/>
</>
Expand All @@ -80,7 +79,6 @@ DateInput.propTypes = {
// eslint-disable-next-line react/forbid-prop-types
control: PropTypes.object.isRequired,
name: PropTypes.string.isRequired,
label: PropTypes.string.isRequired,
minDate: PropTypes.string,
maxDate: PropTypes.string,
openUp: PropTypes.bool,
Expand Down
5 changes: 5 additions & 0 deletions frontend/src/components/FormItem.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
.smart-hub--form-required {
font-family: SourceSansPro;
font-size: 16px;
color: #d42240;
}
79 changes: 79 additions & 0 deletions frontend/src/components/FormItem.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import React from 'react';
import PropTypes from 'prop-types';
import { useFormContext } from 'react-hook-form';
import { ErrorMessage as ReactHookFormError } from '@hookform/error-message';
import {
Label, FormGroup, ErrorMessage, Fieldset,
} from '@trussworks/react-uswds';

import './FormItem.css';

const labelPropTypes = {
label: PropTypes.node.isRequired,
children: PropTypes.node.isRequired,
};

function Checkbox({ label, children }) {
return (
<Fieldset unstyled>
<legend>{label}</legend>
{children}
</Fieldset>
);
}

Checkbox.propTypes = labelPropTypes;

function Field({ label, children }) {
return (
<Label>
{label}
{children}
</Label>
);
}

Field.propTypes = labelPropTypes;

function FormItem({
label, children, required, name, isCheckbox,
}) {
const { formState: { errors } } = useFormContext();
const fieldErrors = errors[name];
const labelWithRequiredTag = (
<>
{label}
{required && (<span className="smart-hub--form-required"> (Required)</span>)}
</>
);

const LabelType = isCheckbox ? Checkbox : Field;

return (
<FormGroup error={fieldErrors}>
<LabelType label={labelWithRequiredTag}>
<ReactHookFormError
errors={errors}
name={name}
render={({ message }) => <ErrorMessage>{message}</ErrorMessage>}
/>
{children}
</LabelType>
</FormGroup>
);
}

FormItem.propTypes = {
label: PropTypes.string.isRequired,
children: PropTypes.node.isRequired,
name: PropTypes.string.isRequired,
isCheckbox: PropTypes.bool,
required: PropTypes.bool,
};

FormItem.defaultProps = {
required: true,
isCheckbox: false,
};

export default FormItem;
84 changes: 41 additions & 43 deletions frontend/src/components/MultiSelect.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@
import React from 'react';
import PropTypes from 'prop-types';
import Select, { components } from 'react-select';
import { Label } from '@trussworks/react-uswds';
import { Controller } from 'react-hook-form';

import arrowBoth from '../images/arrow-both.svg';
Expand Down Expand Up @@ -73,7 +72,6 @@ const styles = {
};

function MultiSelect({
label,
name,
options,
disabled,
Expand Down Expand Up @@ -117,44 +115,45 @@ function MultiSelect({
};

return (
<Label>
{label}
<Controller
render={({ onChange: controllerOnChange, value }) => {
const values = value ? getValues(value) : value;
return (
<Select
className="margin-top-1"
id={name}
value={values}
onChange={(event) => {
if (event) {
onChange(event, controllerOnChange);
} else {
controllerOnChange([]);
}
}}
styles={styles}
components={{ ...componentReplacements, DropdownIndicator }}
options={options}
isDisabled={disabled}
isClearable={multiSelectOptions.isClearable}
closeMenuOnSelect={multiSelectOptions.closeMenuOnSelect}
controlShouldRenderValue={multiSelectOptions.controlShouldRenderValue}
hideSelectedOptions={multiSelectOptions.hideSelectedOptions}
placeholder=""
isMulti
/>
);
}}
control={control}
defaultValue={[]}
rules={{
required,
}}
name={name}
/>
</Label>
<Controller
render={({ onChange: controllerOnChange, value }) => {
const values = value ? getValues(value) : value;
return (
<Select
className="margin-top-1"
id={name}
value={values}
onChange={(event) => {
if (event) {
onChange(event, controllerOnChange);
} else {
controllerOnChange([]);
}
}}
styles={styles}
components={{ ...componentReplacements, DropdownIndicator }}
options={options}
isDisabled={disabled}
isClearable={multiSelectOptions.isClearable}
closeMenuOnSelect={multiSelectOptions.closeMenuOnSelect}
controlShouldRenderValue={multiSelectOptions.controlShouldRenderValue}
hideSelectedOptions={multiSelectOptions.hideSelectedOptions}
placeholder=""
isMulti
/>
);
}}
control={control}
rules={{
validate: (value) => {
if (required && (!value || value.length === 0)) {
return required;
}
return true;
},
}}
name={name}
/>
);
}

Expand All @@ -164,7 +163,6 @@ const value = PropTypes.oneOfType([
]);

MultiSelect.propTypes = {
label: PropTypes.string.isRequired,
name: PropTypes.string.isRequired,
labelProperty: PropTypes.string,
valueProperty: PropTypes.string,
Expand All @@ -191,12 +189,12 @@ MultiSelect.propTypes = {
hideSelectedOptions: PropTypes.bool,
}),
disabled: PropTypes.bool,
required: PropTypes.bool,
required: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]),
};

MultiSelect.defaultProps = {
disabled: false,
required: true,
required: 'Please select at least one item',
simple: true,
labelProperty: 'label',
valueProperty: 'value',
Expand Down
28 changes: 22 additions & 6 deletions frontend/src/components/Navigator/__tests__/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,14 @@ import Navigator from '../index';
import { NOT_STARTED } from '../constants';

// eslint-disable-next-line react/prop-types
const Input = ({ name }) => {
const Input = ({ name, required }) => {
const { register } = useFormContext();
return (
<input
type="radio"
data-testid={name}
name={name}
ref={register}
ref={register({ required })}
/>
);
};
Expand All @@ -43,12 +43,21 @@ const pages = [
},
{
position: 3,
path: 'third',
label: 'third page',
review: false,
render: () => (
<Input name="third" required />
),
},
{
position: 4,
label: 'review page',
path: 'review',
review: true,
render: (allComplete, formData, onSubmit) => (
render: (formData, onFormSubmit) => (
<div>
<button type="button" data-testid="review" onClick={onSubmit}>Continue</button>
<button type="button" data-testid="review" onClick={onFormSubmit}>Continue</button>
</div>
),
},
Expand Down Expand Up @@ -88,7 +97,7 @@ describe('Navigator', () => {
const onSave = jest.fn();
renderNavigator('second', () => {}, onSave);
userEvent.click(screen.getByRole('button', { name: 'Continue' }));
await waitFor(() => expect(onSave).toHaveBeenCalledWith({ pageState: { ...initialData.pageState, 2: 'Complete' }, second: '' }, 3));
await waitFor(() => expect(onSave).toHaveBeenCalledWith({ pageState: { ...initialData.pageState, 2: 'Complete' }, second: null }, 3));
});

it('submits data when "continuing" from the review page', async () => {
Expand All @@ -98,10 +107,17 @@ describe('Navigator', () => {
await waitFor(() => expect(onSubmit).toHaveBeenCalled());
});

it('shows an error message if the form is not valid', async () => {
renderNavigator('third');
const button = await screen.findByRole('button', { name: 'Continue' });
userEvent.click(button);
expect(await screen.findByTestId('alert')).toHaveTextContent('Please complete all required fields before submitting this report.');
});

it('calls onSave on navigation', async () => {
const onSave = jest.fn();
renderNavigator('second', () => {}, onSave);
userEvent.click(screen.getByRole('button', { name: 'first page' }));
await waitFor(() => expect(onSave).toHaveBeenCalledWith({ ...initialData, second: '' }, 1));
await waitFor(() => expect(onSave).toHaveBeenCalledWith({ ...initialData, second: null }, 1));
});
});
Loading

0 comments on commit 6cd157f

Please sign in to comment.