diff --git a/app/package.json b/app/package.json index 8287782667..ac8111c987 100644 --- a/app/package.json +++ b/app/package.json @@ -31,8 +31,8 @@ "@material-ui/lab": "latest", "@material-ui/pickers": "~3.2.10", "@material-ui/styles": "~4.10.0", - "@mdi/js": "^5.9.55", - "@mdi/react": "^1.4.0", + "@mdi/js": "~5.9.55", + "@mdi/react": "~1.4.0", "@react-keycloak/web": "~2.1.0", "@react-leaflet/core": "~1.0.2", "@rjsf/core": "~2.4.1", diff --git a/app/src/constants/dateFormats.ts b/app/src/constants/dateFormats.ts index a83b77af85..9fc1565857 100644 --- a/app/src/constants/dateFormats.ts +++ b/app/src/constants/dateFormats.ts @@ -1,4 +1,8 @@ -// See BC Gov standards: https://www2.gov.bc.ca/gov/content/governments/services-for-government/policies-procedures/web-content-development-guides/writing-for-the-web/web-style-guide/numbers +/** + * Date formats. + * + * See BC Gov standards: https://www2.gov.bc.ca/gov/content/governments/services-for-government/policies-procedures/web-content-development-guides/writing-for-the-web/web-style-guide/numbers + */ export enum DATE_FORMAT { ShortDateFormat = 'YYYY-MM-DD', //2020-01-05 ShortDateTimeFormat = 'YYYY-MM-DD, H:mm a', //2020-01-05, 3:30 pm @@ -9,3 +13,11 @@ export enum DATE_FORMAT { LongDateFormat = 'dddd, MMMM D, YYYY, H:mm a', //Monday, January 5, 2020, 3:30 pm LongDateTimeFormat = 'dddd, MMMM D, YYYY, H:mm a' //Monday, January 5, 2020, 3:30 pm } + +/** + * Used to set the `min` and `max` values for `type="date"` fields. + */ +export enum DATE_LIMIT { + min = '1900-01-01', + max = '2100-12-31' +} diff --git a/app/src/features/projects/components/ProjectCoordinatorForm.tsx b/app/src/features/projects/components/ProjectCoordinatorForm.tsx index 17f4306e0c..e1366408d5 100644 --- a/app/src/features/projects/components/ProjectCoordinatorForm.tsx +++ b/app/src/features/projects/components/ProjectCoordinatorForm.tsx @@ -33,10 +33,14 @@ export const ProjectCoordinatorInitialValues: IProjectCoordinatorForm = { }; export const ProjectCoordinatorYupSchema = yup.object().shape({ - first_name: yup.string().required('Required'), - last_name: yup.string().required('Required'), - email_address: yup.string().email('Must be a valid email address').required('Required'), - coordinator_agency: yup.string().required('Required'), + first_name: yup.string().max(50, 'Cannot exceed 50 characters').required('Required'), + last_name: yup.string().max(50, 'Cannot exceed 50 characters').required('Required'), + email_address: yup + .string() + .max(500, 'Cannot exceed 500 characters') + .email('Must be a valid email address') + .required('Required'), + coordinator_agency: yup.string().max(300, 'Cannot exceed 300 characters').required('Required'), share_contact_details: yup.string().required('Required') }); diff --git a/app/src/features/projects/components/ProjectDetailsForm.tsx b/app/src/features/projects/components/ProjectDetailsForm.tsx index 6686b88c60..e5b06c2218 100644 --- a/app/src/features/projects/components/ProjectDetailsForm.tsx +++ b/app/src/features/projects/components/ProjectDetailsForm.tsx @@ -2,8 +2,10 @@ import { FormControl, FormHelperText, Grid, InputLabel, MenuItem, Select, TextFi import MultiAutocompleteFieldVariableSize, { IMultiAutocompleteFieldOption } from 'components/fields/MultiAutocompleteFieldVariableSize'; +import { DATE_LIMIT } from 'constants/dateFormats'; import { useFormikContext } from 'formik'; import React from 'react'; +import { getEndDateStringValidator, getStartDateStringValidator } from 'utils/YupValidations'; import * as yup from 'yup'; export interface IProjectDetailsForm { @@ -26,12 +28,10 @@ export const ProjectDetailsFormInitialValues: IProjectDetailsForm = { }; export const ProjectDetailsFormYupSchema = yup.object().shape({ - project_name: yup.string().required('Required'), + project_name: yup.string().max(50, 'Cannot exceed 50 characters').required('Required'), project_type: yup.string().required('Required'), - start_date: yup.date().required('Required'), - end_date: yup.date().when('start_date', (start_date: any, schema: any) => { - return start_date && schema.min(start_date, 'End Date is before Start Date'); - }) + start_date: getStartDateStringValidator().required('Required'), + end_date: getEndDateStringValidator('start_date') }); export interface IProjectDetailsFormProps { @@ -116,6 +116,7 @@ const ProjectDetailsForm: React.FC = (props) => { required={true} value={values.start_date} type="date" + inputProps={{ min: DATE_LIMIT.min, max: DATE_LIMIT.max }} onChange={handleChange} error={touched.start_date && Boolean(errors.start_date)} helperText={errors.start_date} @@ -132,6 +133,7 @@ const ProjectDetailsForm: React.FC = (props) => { variant="outlined" value={values.end_date} type="date" + inputProps={{ min: DATE_LIMIT.min, max: DATE_LIMIT.max }} onChange={handleChange} error={touched.end_date && Boolean(errors.end_date)} helperText={errors.end_date} diff --git a/app/src/features/projects/components/ProjectFundingItemForm.tsx b/app/src/features/projects/components/ProjectFundingItemForm.tsx index 41c6ccdcd1..f692476d06 100644 --- a/app/src/features/projects/components/ProjectFundingItemForm.tsx +++ b/app/src/features/projects/components/ProjectFundingItemForm.tsx @@ -17,8 +17,10 @@ import { Typography } from '@material-ui/core'; import { IMultiAutocompleteFieldOption } from 'components/fields/MultiAutocompleteFieldVariableSize'; +import { DATE_LIMIT } from 'constants/dateFormats'; import { Formik, FormikHelpers } from 'formik'; import React from 'react'; +import { getEndDateStringValidator, getStartDateStringValidator } from 'utils/YupValidations'; import * as yup from 'yup'; import { IInvestmentActionCategoryOption } from './ProjectFundingForm'; @@ -46,20 +48,15 @@ export const ProjectFundingFormArrayItemYupSchema = yup.object().shape({ .transform((value) => (isNaN(value) && null) || value) .required('Required'), investment_action_category: yup.number().required('Required'), - agency_project_id: yup.string(), + agency_project_id: yup.string().max(50, 'Cannot exceed 50 characters'), funding_amount: yup .number() .transform((value) => (isNaN(value) && null) || value) .typeError('Must be a number') .min(0, 'Must be a positive number') .required('Required'), - start_date: yup.date().required('Required'), - end_date: yup - .date() - .when('start_date', (start_date: any, schema: any) => { - return start_date && schema.min(start_date, 'End Date is before Start Date'); - }) - .required('Required') + start_date: getStartDateStringValidator().required('Required'), + end_date: getEndDateStringValidator('start_date') }); export interface IProjectFundingItemFormProps { @@ -227,6 +224,7 @@ const ProjectFundingItemForm: React.FC = (props) = required={true} value={values.start_date} type="date" + inputProps={{ min: DATE_LIMIT.min, max: DATE_LIMIT.max }} onChange={handleChange} error={touched.start_date && Boolean(errors.start_date)} helperText={errors.start_date} @@ -244,6 +242,7 @@ const ProjectFundingItemForm: React.FC = (props) = required={true} value={values.end_date} type="date" + inputProps={{ min: DATE_LIMIT.min, max: DATE_LIMIT.max }} onChange={handleChange} error={touched.end_date && Boolean(errors.end_date)} helperText={errors.end_date} diff --git a/app/src/features/projects/components/ProjectLocationForm.tsx b/app/src/features/projects/components/ProjectLocationForm.tsx index f943eac68d..c1793c1d26 100644 --- a/app/src/features/projects/components/ProjectLocationForm.tsx +++ b/app/src/features/projects/components/ProjectLocationForm.tsx @@ -34,7 +34,7 @@ export const ProjectLocationFormInitialValues: IProjectLocationForm = { export const ProjectLocationFormYupSchema = yup.object().shape({ regions: yup.array().of(yup.string()).min(1).required('Required'), - location_description: yup.string() + location_description: yup.string().max(3000, 'Cannot exceed 3000 characters') }); export interface IProjectLocationFormProps { diff --git a/app/src/features/projects/components/ProjectObjectivesForm.tsx b/app/src/features/projects/components/ProjectObjectivesForm.tsx index 9a5e2bd8a9..129de2ab03 100644 --- a/app/src/features/projects/components/ProjectObjectivesForm.tsx +++ b/app/src/features/projects/components/ProjectObjectivesForm.tsx @@ -16,9 +16,9 @@ export const ProjectObjectivesFormInitialValues: IProjectObjectivesForm = { export const ProjectObjectivesFormYupSchema = yup.object().shape({ objectives: yup .string() - .max(3000, 'Cannot exceed 3000 characters.') - .required('You must provide objectives for the project.'), - caveats: yup.string().max(3000, 'Cannot exceed 3000 characters.') + .max(3000, 'Cannot exceed 3000 characters') + .required('You must provide objectives for the project'), + caveats: yup.string().max(3000, 'Cannot exceed 3000 characters') }); /** diff --git a/app/src/features/projects/components/ProjectPermitForm.tsx b/app/src/features/projects/components/ProjectPermitForm.tsx index e0e5a30b68..069d138a4f 100644 --- a/app/src/features/projects/components/ProjectPermitForm.tsx +++ b/app/src/features/projects/components/ProjectPermitForm.tsx @@ -37,7 +37,7 @@ export const ProjectPermitFormInitialValues: IProjectPermitForm = { export const ProjectPermitFormYupSchema = yup.object().shape({ permits: yup.array().of( yup.object().shape({ - permit_number: yup.string().required('Required'), + permit_number: yup.string().max(100, 'Cannot exceed 100 characters').required('Required'), sampling_conducted: yup.string().required('Required') }) ) diff --git a/app/src/features/projects/components/__snapshots__/ProjectDetailsForm.test.tsx.snap b/app/src/features/projects/components/__snapshots__/ProjectDetailsForm.test.tsx.snap index a4463c6c4a..c3fde93747 100644 --- a/app/src/features/projects/components/__snapshots__/ProjectDetailsForm.test.tsx.snap +++ b/app/src/features/projects/components/__snapshots__/ProjectDetailsForm.test.tsx.snap @@ -361,6 +361,8 @@ exports[`ProjectDetailsForm renders correctly with default empty values 1`] = ` aria-invalid="false" class="MuiInputBase-input MuiOutlinedInput-input" id="start_date" + max="2100-12-31" + min="1900-01-01" name="start_date" required="" type="date" @@ -402,6 +404,8 @@ exports[`ProjectDetailsForm renders correctly with default empty values 1`] = ` aria-invalid="false" class="MuiInputBase-input MuiOutlinedInput-input" id="end_date" + max="2100-12-31" + min="1900-01-01" name="end_date" type="date" value="" @@ -874,6 +878,8 @@ exports[`ProjectDetailsForm renders correctly with existing details values 1`] = aria-invalid="false" class="MuiInputBase-input MuiOutlinedInput-input" id="start_date" + max="2100-12-31" + min="1900-01-01" name="start_date" required="" type="date" @@ -915,6 +921,8 @@ exports[`ProjectDetailsForm renders correctly with existing details values 1`] = aria-invalid="false" class="MuiInputBase-input MuiOutlinedInput-input" id="end_date" + max="2100-12-31" + min="1900-01-01" name="end_date" type="date" value="2021-04-14" diff --git a/app/src/features/projects/components/__snapshots__/ProjectFundingItemForm.test.tsx.snap b/app/src/features/projects/components/__snapshots__/ProjectFundingItemForm.test.tsx.snap index e8fe543ed3..64860fb485 100644 --- a/app/src/features/projects/components/__snapshots__/ProjectFundingItemForm.test.tsx.snap +++ b/app/src/features/projects/components/__snapshots__/ProjectFundingItemForm.test.tsx.snap @@ -282,6 +282,8 @@ exports[`ProjectFundingItemForm renders correctly with default empty values 1`] aria-invalid="false" class="MuiInputBase-input MuiOutlinedInput-input" id="start_date" + max="2100-12-31" + min="1900-01-01" name="start_date" required="" type="date" @@ -331,6 +333,8 @@ exports[`ProjectFundingItemForm renders correctly with default empty values 1`] aria-invalid="false" class="MuiInputBase-input MuiOutlinedInput-input" id="end_date" + max="2100-12-31" + min="1900-01-01" name="end_date" required="" type="date" @@ -758,6 +762,8 @@ exports[`ProjectFundingItemForm renders correctly with existing funding item val aria-invalid="false" class="MuiInputBase-input MuiOutlinedInput-input" id="start_date" + max="2100-12-31" + min="1900-01-01" name="start_date" required="" type="date" @@ -807,6 +813,8 @@ exports[`ProjectFundingItemForm renders correctly with existing funding item val aria-invalid="false" class="MuiInputBase-input MuiOutlinedInput-input" id="end_date" + max="2100-12-31" + min="1900-01-01" name="end_date" required="" type="date" @@ -1234,6 +1242,8 @@ exports[`ProjectFundingItemForm renders correctly with existing funding item val aria-invalid="false" class="MuiInputBase-input MuiOutlinedInput-input" id="start_date" + max="2100-12-31" + min="1900-01-01" name="start_date" required="" type="date" @@ -1283,6 +1293,8 @@ exports[`ProjectFundingItemForm renders correctly with existing funding item val aria-invalid="false" class="MuiInputBase-input MuiOutlinedInput-input" id="end_date" + max="2100-12-31" + min="1900-01-01" name="end_date" required="" type="date" @@ -1639,6 +1651,8 @@ exports[`ProjectFundingItemForm renders correctly with existing funding item val aria-invalid="false" class="MuiInputBase-input MuiOutlinedInput-input" id="start_date" + max="2100-12-31" + min="1900-01-01" name="start_date" required="" type="date" @@ -1688,6 +1702,8 @@ exports[`ProjectFundingItemForm renders correctly with existing funding item val aria-invalid="false" class="MuiInputBase-input MuiOutlinedInput-input" id="end_date" + max="2100-12-31" + min="1900-01-01" name="end_date" required="" type="date" diff --git a/app/src/utils/YupValidations.ts b/app/src/utils/YupValidations.ts new file mode 100644 index 0000000000..ed4da14078 --- /dev/null +++ b/app/src/utils/YupValidations.ts @@ -0,0 +1,37 @@ +import moment from 'moment'; +import * as yup from 'yup'; + +const getBaseDateStringValidator = () => { + return yup.string().test('is-valid-date', 'Invalid date', (value) => { + if (!value) { + return true; + } + + return moment(value, 'YYYY-MM-DD', true).isValid(); + }); +}; + +export const getStartDateStringValidator = () => { + return getBaseDateStringValidator(); +}; + +export const getEndDateStringValidator = (startDateName: string) => { + return getBaseDateStringValidator().test( + 'is-end-date-after-start-date', + 'End Date is before Start Date', + function (value) { + if (!value) { + // end date is null, no validation required + return true; + } + + if (!moment(this.parent[startDateName], 'YYYY-MM-DD', true).isValid()) { + // cant validate end_date if start_date is invalid + return true; + } + + // compare valid start and end dates + return moment(this.parent.start_date, 'YYYY-MM-DD').isBefore(moment(value, 'YYYY-MM-DD')); + } + ); +};