Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

BHBC-1690: Add scroll to error formik component #749

Merged
merged 4 commits into from
Apr 25, 2022
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 9 additions & 9 deletions api/src/paths/project/{projectId}/survey/{surveyId}/update.ts
Original file line number Diff line number Diff line change
Expand Up @@ -163,14 +163,13 @@ GET.apiDoc = {
type: 'string'
},
start_date: {
type: 'string',
format: 'date',
description: 'ISO 8601 date string for the funding end_date'
oneOf: [{ type: 'object' }, { type: 'string', format: 'date' }],
description: 'ISO 8601 date string for the survey start date'
},
end_date: {
type: 'string',
format: 'date',
description: 'ISO 8601 date string for the funding end_date'
oneOf: [{ type: 'object' }, { type: 'string', format: 'date' }],
description: 'ISO 8601 date string for the survey end date',
nullable: true
},
funding_sources: {
type: 'array',
Expand Down Expand Up @@ -407,12 +406,13 @@ PUT.apiDoc = {
start_date: {
type: 'string',
format: 'date',
description: 'ISO 8601 date string for the funding end_date'
description: 'ISO 8601 date string for the survey start date'
},
end_date: {
type: 'string',
format: 'date',
description: 'ISO 8601 date string for the funding end_date'
oneOf: [{ maxLength: 0 }, { format: 'date' }],
nullable: true,
description: 'ISO 8601 date string for the survey end date'
},
funding_sources: {
type: 'array',
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import Checkbox from '@material-ui/core/Checkbox';
import ListSubheader from '@material-ui/core/ListSubheader';
import makeStyles from '@material-ui/core/styles/makeStyles';
import TextField from '@material-ui/core/TextField';
import CheckBox from '@material-ui/icons/CheckBox';
import CheckBoxOutlineBlank from '@material-ui/icons/CheckBoxOutlineBlank';
import Autocomplete from '@material-ui/lab/Autocomplete';
import makeStyles from '@material-ui/core/styles/makeStyles';
import { useFormikContext } from 'formik';
import get from 'lodash-es/get';
import React from 'react';
import { ListChildComponentProps, VariableSizeList } from 'react-window';
import get from 'lodash-es/get';

const LISTBOX_PADDING = 8; // px

Expand Down Expand Up @@ -169,6 +169,7 @@ const MultiAutocompleteFieldVariableSize: React.FC<IMultiAutocompleteField> = (p
renderInput={(params) => (
<TextField
{...params}
name={props.id}
required={props.required}
label={props.label}
variant="outlined"
Expand Down
105 changes: 105 additions & 0 deletions app/src/components/formik/ScrollToFormikError.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import Snackbar from '@material-ui/core/Snackbar';
import Alert from '@material-ui/lab/Alert';
import { useFormikContext } from 'formik';
import { IGetProjectForViewResponse } from 'interfaces/useProjectApi.interface';
import React, { useEffect, useState } from 'react';

export interface IScrollToFormikErrorProps {
/**
* An ordered list of field names, which informs which field this component will scroll to when multiple fields are
* in error.
*
* Note: A regex is required if the field name has dynamic components (ie: a field with an array of objects where
* part of the field name is its index in the array field).
*
* @type {((string | RegExp)[])}
* @memberof IScrollToFormikErrorProps
*/
fieldOrder: (string | RegExp)[];
}

export const ScrollToFormikError: React.FC<IScrollToFormikErrorProps> = (props) => {
const formikProps = useFormikContext<IGetProjectForViewResponse>();
const { errors, submitCount } = formikProps;
const [openSnackbar, setOpenSnackbar] = useState({ open: false, msg: '' });

useEffect(() => {
const showSnackBar = (message: string) => {
setOpenSnackbar({ open: true, msg: message });
};

const getAllFieldErrorNames = (obj: object, prefix = '', result: string[] = []) => {
Object.keys(obj).forEach((key) => {
const value = obj[key];
if (!value) return;

key = Number(key) || key === '0' ? `[${key}]` : key;

const nextKey = prefix ? `${prefix}.${key}` : key;

if (typeof value === 'object') {
getAllFieldErrorNames(value, nextKey, result);
} else {
result.push(nextKey);
}
});
return result;
};

const getFirstErrorField = (errorArray: string[]): string | undefined => {
for (const listError of props.fieldOrder) {
for (const trueError of errorArray) {
if (trueError.match(listError) || listError === trueError) {
return trueError;
}
}
}
};

const getFieldTitle = (absoluteErrorName: string) => {
const fieldTitleArray = absoluteErrorName.split('.');
const fieldTitleSplit = fieldTitleArray[fieldTitleArray.length - 1].split('_');
let fieldTitleUpperCase = '';
fieldTitleSplit.forEach((item) => {
fieldTitleUpperCase += `${item.charAt(0).toUpperCase() + item.slice(1)} `;
});
return fieldTitleUpperCase;
};

const fieldErrorNames = getAllFieldErrorNames(errors);

const topFieldError = getFirstErrorField(fieldErrorNames);

if (!topFieldError) {
return;
}

const fieldTitle = getFieldTitle(topFieldError);
showSnackBar(`Error Invalid Form Value: ${fieldTitle}`);

const errorElement = document.getElementsByName(topFieldError);

if (errorElement.length <= 0) {
return;
}

errorElement[0].scrollIntoView({ behavior: 'smooth', block: 'center' });

// eslint-disable-next-line react-hooks/exhaustive-deps
}, [errors, submitCount]);

return (
<>
<Snackbar
anchorOrigin={{
vertical: 'bottom',
horizontal: 'center'
}}
open={openSnackbar.open}
autoHideDuration={4000}
onClose={() => setOpenSnackbar({ open: false, msg: '' })}>
<Alert severity="error">{openSnackbar.msg}</Alert>
</Snackbar>
</>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,7 @@ exports[`ProjectDetailsForm renders correctly with default empty values 1`] = `
autocomplete="off"
class="MuiInputBase-input MuiOutlinedInput-input MuiAutocomplete-input MuiAutocomplete-inputFocused MuiInputBase-inputAdornedEnd MuiOutlinedInput-inputAdornedEnd"
id="project_activities"
name="project_activities"
spellcheck="false"
type="text"
value=""
Expand Down Expand Up @@ -528,6 +529,7 @@ exports[`ProjectDetailsForm renders correctly with existing details values 1`] =
autocomplete="off"
class="MuiInputBase-input MuiOutlinedInput-input MuiAutocomplete-input MuiAutocomplete-inputFocused MuiInputBase-inputAdornedStart MuiOutlinedInput-inputAdornedStart MuiInputBase-inputAdornedEnd MuiOutlinedInput-inputAdornedEnd"
id="project_activities"
name="project_activities"
spellcheck="false"
type="text"
value=""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ exports[`ProjectPartnershipsForm renders correctly with default empty values 1`]
autocomplete="off"
class="MuiInputBase-input MuiOutlinedInput-input MuiAutocomplete-input MuiAutocomplete-inputFocused MuiInputBase-inputAdornedEnd MuiOutlinedInput-inputAdornedEnd"
id="indigenous_partnerships"
name="indigenous_partnerships"
spellcheck="false"
type="text"
value=""
Expand Down Expand Up @@ -140,6 +141,7 @@ exports[`ProjectPartnershipsForm renders correctly with default empty values 1`]
autocomplete="off"
class="MuiInputBase-input MuiOutlinedInput-input MuiAutocomplete-input MuiAutocomplete-inputFocused MuiInputBase-inputAdornedEnd MuiOutlinedInput-inputAdornedEnd"
id="stakeholder_partnerships"
name="stakeholder_partnerships"
spellcheck="false"
type="text"
value=""
Expand Down Expand Up @@ -299,6 +301,7 @@ exports[`ProjectPartnershipsForm renders correctly with existing funding values
autocomplete="off"
class="MuiInputBase-input MuiOutlinedInput-input MuiAutocomplete-input MuiAutocomplete-inputFocused MuiInputBase-inputAdornedStart MuiOutlinedInput-inputAdornedStart MuiInputBase-inputAdornedEnd MuiOutlinedInput-inputAdornedEnd"
id="indigenous_partnerships"
name="indigenous_partnerships"
spellcheck="false"
type="text"
value=""
Expand Down Expand Up @@ -403,6 +406,7 @@ exports[`ProjectPartnershipsForm renders correctly with existing funding values
autocomplete="off"
class="MuiInputBase-input MuiOutlinedInput-input MuiAutocomplete-input MuiAutocomplete-inputFocused MuiInputBase-inputAdornedEnd MuiOutlinedInput-inputAdornedEnd"
id="stakeholder_partnerships"
name="stakeholder_partnerships"
spellcheck="false"
type="text"
value=""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ exports[`ProjectPermitForm renders correctly with default empty values 1`] = `
autocomplete="off"
class="MuiInputBase-input MuiOutlinedInput-input MuiAutocomplete-input MuiAutocomplete-inputFocused MuiInputBase-inputAdornedEnd MuiOutlinedInput-inputAdornedEnd"
id="existing_permits"
name="existing_permits"
spellcheck="false"
type="text"
value=""
Expand Down Expand Up @@ -170,6 +171,7 @@ exports[`ProjectPermitForm renders correctly with error on the permits field due
autocomplete="off"
class="MuiInputBase-input MuiOutlinedInput-input MuiAutocomplete-input MuiAutocomplete-inputFocused MuiInputBase-inputAdornedEnd MuiOutlinedInput-inputAdornedEnd"
id="existing_permits"
name="existing_permits"
spellcheck="false"
type="text"
value=""
Expand Down Expand Up @@ -626,6 +628,7 @@ exports[`ProjectPermitForm renders correctly with errors on the permit_number an
autocomplete="off"
class="MuiInputBase-input MuiOutlinedInput-input MuiAutocomplete-input MuiAutocomplete-inputFocused MuiInputBase-inputAdornedEnd MuiOutlinedInput-inputAdornedEnd"
id="existing_permits"
name="existing_permits"
spellcheck="false"
type="text"
value=""
Expand Down Expand Up @@ -927,6 +930,7 @@ exports[`ProjectPermitForm renders correctly with existing permit values 1`] = `
autocomplete="off"
class="MuiInputBase-input MuiOutlinedInput-input MuiAutocomplete-input MuiAutocomplete-inputFocused MuiInputBase-inputAdornedEnd MuiOutlinedInput-inputAdornedEnd"
id="existing_permits"
name="existing_permits"
spellcheck="false"
type="text"
value=""
Expand Down
59 changes: 14 additions & 45 deletions app/src/features/surveys/CreateSurveyPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import makeStyles from '@material-ui/core/styles/makeStyles';
import Typography from '@material-ui/core/Typography';
import { IErrorDialogProps } from 'components/dialog/ErrorDialog';
import HorizontalSplitFormComponent from 'components/fields/HorizontalSplitFormComponent';
import { ScrollToFormikError } from 'components/formik/ScrollToFormikError';
import { DATE_FORMAT, DATE_LIMIT } from 'constants/dateTimeFormats';
import { CreateSurveyI18N } from 'constants/i18n';
import { DialogContext } from 'contexts/dialogContext';
Expand All @@ -24,7 +25,6 @@ import { ICreateSurveyRequest, SurveyFundingSources, SurveyPermits } from 'inter
import moment from 'moment';
import React, { useCallback, useContext, useEffect, useRef, useState } from 'react';
import { Prompt, useHistory, useParams } from 'react-router';
import { validateFormFieldsAndReportCompletion } from 'utils/customValidation';
import { getFormattedAmount, getFormattedDate, getFormattedDateRangeString } from 'utils/Utils';
import yup from 'utils/YupSchema';
import AgreementsForm, { AgreementsInitialValues, AgreementsYupSchema } from './components/AgreementsForm';
Expand Down Expand Up @@ -118,10 +118,10 @@ const CreateSurveyPage = () => {
};

// Initial values for the survey form sections
const [surveyInitialValues] = useState({
const [surveyInitialValues] = useState<ICreateSurveyRequest>({
...GeneralInformationInitialValues,
...StudyAreaInitialValues,
...PurposeAndMethodologyInitialValues,
...StudyAreaInitialValues,
...ProprietaryDataInitialValues,
...AgreementsInitialValues
});
Expand Down Expand Up @@ -230,51 +230,18 @@ const CreateSurveyPage = () => {
};

/**
* Creates a new project survey record
* Handle creation of surveys.
*
* @param {ICreateSurveyRequest} surveyPostObject
* @return {*}
*/
const createSurvey = async (surveyPostObject: ICreateSurveyRequest) => {
const response = await biohubApi.survey.createSurvey(Number(projectWithDetails?.id), surveyPostObject);

if (!response?.id) {
showCreateErrorDialog({ dialogError: 'The response from the server was null, or did not contain a survey ID.' });
return;
}

return response;
};

/**
* Handle creation of surveys.
*/
const handleSubmit = async () => {
if (!formikRef?.current) {
return;
}

await formikRef.current?.submitForm();

const isValid = await validateFormFieldsAndReportCompletion(
formikRef.current?.values,
formikRef.current?.validateForm
);

if (!isValid) {
showCreateErrorDialog({
dialogTitle: 'Create Survey Form Incomplete',
dialogText:
'The form is missing some required fields/sections highlighted in red. Please fill them out and try again.'
});

return;
}

const handleSubmit = async (values: ICreateSurveyRequest) => {
try {
const response = await createSurvey(formikRef.current?.values);
const response = await biohubApi.survey.createSurvey(Number(projectWithDetails?.id), values);

if (!response) {
if (!response?.id) {
showCreateErrorDialog({
dialogError: 'The response from the server was null, or did not contain a survey ID.'
});
return;
}

Expand Down Expand Up @@ -352,8 +319,10 @@ const CreateSurveyPage = () => {
validationSchema={surveyYupSchemas}
validateOnBlur={true}
validateOnChange={false}
onSubmit={() => {}}>
onSubmit={handleSubmit}>
<>
<ScrollToFormikError fieldOrder={Object.keys(surveyInitialValues)} />

<HorizontalSplitFormComponent
title="General Information"
summary=""
Expand Down Expand Up @@ -460,7 +429,7 @@ const CreateSurveyPage = () => {
type="submit"
variant="contained"
color="primary"
onClick={handleSubmit}
onClick={() => formikRef.current?.submitForm()}
className={classes.actionButton}>
Save and Exit
</Button>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,7 @@ exports[`CreateSurveyPage renders correctly when codes and project data are load
autocomplete="off"
class="MuiInputBase-input MuiOutlinedInput-input MuiAutocomplete-input MuiAutocomplete-inputFocused MuiInputBase-inputAdornedEnd MuiOutlinedInput-inputAdornedEnd"
id="focal_species"
name="focal_species"
required=""
spellcheck="false"
type="text"
Expand Down Expand Up @@ -411,6 +412,7 @@ exports[`CreateSurveyPage renders correctly when codes and project data are load
autocomplete="off"
class="MuiInputBase-input MuiOutlinedInput-input MuiAutocomplete-input MuiAutocomplete-inputFocused MuiInputBase-inputAdornedEnd MuiOutlinedInput-inputAdornedEnd"
id="ancillary_species"
name="ancillary_species"
spellcheck="false"
type="text"
value=""
Expand Down Expand Up @@ -794,6 +796,7 @@ exports[`CreateSurveyPage renders correctly when codes and project data are load
autocomplete="off"
class="MuiInputBase-input MuiOutlinedInput-input MuiAutocomplete-input MuiAutocomplete-inputFocused MuiInputBase-inputAdornedEnd MuiOutlinedInput-inputAdornedEnd"
id="funding_sources"
name="funding_sources"
spellcheck="false"
type="text"
value=""
Expand Down Expand Up @@ -1200,6 +1203,7 @@ exports[`CreateSurveyPage renders correctly when codes and project data are load
autocomplete="off"
class="MuiInputBase-input MuiOutlinedInput-input MuiAutocomplete-input MuiAutocomplete-inputFocused MuiInputBase-inputAdornedEnd MuiOutlinedInput-inputAdornedEnd"
id="vantage_code_ids"
name="vantage_code_ids"
required=""
spellcheck="false"
type="text"
Expand Down
Loading