mirror of
https://github.com/nowcommunity/nowdatabase.git
synced 2026-10-12 19:13:52 +03:00
Broaden full-field validation coverage
This commit is contained in:
@@ -18,7 +18,7 @@ import { ProjectTab } from './Tabs/ProjectTab'
|
|||||||
import { SpeciesTab } from './Tabs/SpeciesTab'
|
import { SpeciesTab } from './Tabs/SpeciesTab'
|
||||||
import { TaphonomyTab } from './Tabs/TaphonomyTab'
|
import { TaphonomyTab } from './Tabs/TaphonomyTab'
|
||||||
import { EditDataType, LocalityDetailsType, ValidationErrors } from '@/shared/types'
|
import { EditDataType, LocalityDetailsType, ValidationErrors } from '@/shared/types'
|
||||||
import { validateLocality } from '@/shared/validators/locality'
|
import { validateLocality, validateLocalityFields } from '@/shared/validators/locality'
|
||||||
import { UpdateTab } from '../DetailView/common/UpdateTab'
|
import { UpdateTab } from '../DetailView/common/UpdateTab'
|
||||||
import { emptyLocality } from '../DetailView/common/defaultValues'
|
import { emptyLocality } from '../DetailView/common/defaultValues'
|
||||||
import { useNotify } from '@/hooks/notification'
|
import { useNotify } from '@/hooks/notification'
|
||||||
@@ -164,6 +164,7 @@ export const LocalityDetails = ({
|
|||||||
isNew={isNew}
|
isNew={isNew}
|
||||||
onWrite={onWrite}
|
onWrite={onWrite}
|
||||||
validator={validateLocality}
|
validator={validateLocality}
|
||||||
|
validateFields={validateLocalityFields}
|
||||||
deleteFunction={deleteFunction}
|
deleteFunction={deleteFunction}
|
||||||
hasStagingMode
|
hasStagingMode
|
||||||
wrapWithUnsavedChangesProvider={wrapWithUnsavedChangesProvider}
|
wrapWithUnsavedChangesProvider={wrapWithUnsavedChangesProvider}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { useEditMuseumMutation, useGetMuseumDetailsQuery } from '../../redux/mus
|
|||||||
import { DetailView, TabType } from '../DetailView/DetailView'
|
import { DetailView, TabType } from '../DetailView/DetailView'
|
||||||
import { MuseumInfoTab } from './Tabs/MuseumInfoTab'
|
import { MuseumInfoTab } from './Tabs/MuseumInfoTab'
|
||||||
import { emptyMuseum } from '../DetailView/common/defaultValues'
|
import { emptyMuseum } from '../DetailView/common/defaultValues'
|
||||||
import { validateMuseum } from '@/shared/validators/museum'
|
import { validateMuseum, validateMuseumFields } from '@/shared/validators/museum'
|
||||||
import { useNavigate, useParams } from 'react-router-dom'
|
import { useNavigate, useParams } from 'react-router-dom'
|
||||||
import { CircularProgress } from '@mui/material'
|
import { CircularProgress } from '@mui/material'
|
||||||
import { Museum, EditDataType, ValidationErrors } from '@/shared/types'
|
import { Museum, EditDataType, ValidationErrors } from '@/shared/types'
|
||||||
@@ -58,6 +58,7 @@ export const MuseumDetails = ({
|
|||||||
onWrite={onWrite}
|
onWrite={onWrite}
|
||||||
isNew={isNew}
|
isNew={isNew}
|
||||||
validator={validateMuseum}
|
validator={validateMuseum}
|
||||||
|
validateFields={validateMuseumFields}
|
||||||
deleteFunction={undefined}
|
deleteFunction={undefined}
|
||||||
wrapWithUnsavedChangesProvider={wrapWithUnsavedChangesProvider}
|
wrapWithUnsavedChangesProvider={wrapWithUnsavedChangesProvider}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import { PersonTab } from './Tabs/PersonTab'
|
|||||||
import { PersonRelationsTab } from './Tabs/PersonRelationsTab'
|
import { PersonRelationsTab } from './Tabs/PersonRelationsTab'
|
||||||
import { useUser } from '@/hooks/user'
|
import { useUser } from '@/hooks/user'
|
||||||
import { EditDataType, PersonDetailsType, Role, ValidationErrors } from '@/shared/types'
|
import { EditDataType, PersonDetailsType, Role, ValidationErrors } from '@/shared/types'
|
||||||
import { validatePerson } from '@/shared/validators/person'
|
import { validatePerson, validatePersonFields } from '@/shared/validators/person'
|
||||||
import { useNotify } from '@/hooks/notification'
|
import { useNotify } from '@/hooks/notification'
|
||||||
import { useEffect } from 'react'
|
import { useEffect } from 'react'
|
||||||
import { emptyPerson } from '../DetailView/common/defaultValues'
|
import { emptyPerson } from '../DetailView/common/defaultValues'
|
||||||
@@ -127,6 +127,7 @@ export const PersonDetails = () => {
|
|||||||
tabs={tabs}
|
tabs={tabs}
|
||||||
data={isNew ? emptyPerson : data!}
|
data={isNew ? emptyPerson : data!}
|
||||||
validator={validatePerson}
|
validator={validatePerson}
|
||||||
|
validateFields={validatePersonFields}
|
||||||
deleteFunction={deleteFunction}
|
deleteFunction={deleteFunction}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { DetailView, TabType } from '../DetailView/DetailView'
|
|||||||
import { CoordinatorTab } from './Tabs/CoordinatorTab'
|
import { CoordinatorTab } from './Tabs/CoordinatorTab'
|
||||||
import { EditDataType, RegionDetails as RegionDetailsType, ValidationErrors } from '@/shared/types'
|
import { EditDataType, RegionDetails as RegionDetailsType, ValidationErrors } from '@/shared/types'
|
||||||
import { useNotify } from '@/hooks/notification'
|
import { useNotify } from '@/hooks/notification'
|
||||||
import { validateRegion } from '@/shared/validators/region'
|
import { validateRegion, validateRegionFields } from '@/shared/validators/region'
|
||||||
import { useEffect } from 'react'
|
import { useEffect } from 'react'
|
||||||
import { emptyRegion } from '../DetailView/common/defaultValues'
|
import { emptyRegion } from '../DetailView/common/defaultValues'
|
||||||
|
|
||||||
@@ -66,6 +66,7 @@ export const RegionDetails = () => {
|
|||||||
data={isNew ? emptyRegion : data!}
|
data={isNew ? emptyRegion : data!}
|
||||||
onWrite={onWrite}
|
onWrite={onWrite}
|
||||||
validator={validateRegion}
|
validator={validateRegion}
|
||||||
|
validateFields={validateRegionFields}
|
||||||
deleteFunction={deleteFunction}
|
deleteFunction={deleteFunction}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import { TaxonomyTab } from './Tabs/TaxonomyTab'
|
|||||||
import { TeethTab } from './Tabs/TeethTab'
|
import { TeethTab } from './Tabs/TeethTab'
|
||||||
import { UpdateTab } from '../DetailView/common/UpdateTab'
|
import { UpdateTab } from '../DetailView/common/UpdateTab'
|
||||||
import { EditDataType, SpeciesDetailsType, ValidationErrors } from '@/shared/types'
|
import { EditDataType, SpeciesDetailsType, ValidationErrors } from '@/shared/types'
|
||||||
import { validateSpecies } from '@/shared/validators/species'
|
import { validateSpecies, validateSpeciesFields } from '@/shared/validators/species'
|
||||||
import { emptySpecies } from '../DetailView/common/defaultValues'
|
import { emptySpecies } from '../DetailView/common/defaultValues'
|
||||||
import { useNotify } from '@/hooks/notification'
|
import { useNotify } from '@/hooks/notification'
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
@@ -124,6 +124,7 @@ export const SpeciesDetails = ({
|
|||||||
taxonomy={true}
|
taxonomy={true}
|
||||||
hasStagingMode
|
hasStagingMode
|
||||||
validator={validateSpecies}
|
validator={validateSpecies}
|
||||||
|
validateFields={validateSpeciesFields}
|
||||||
deleteFunction={deleteFunction}
|
deleteFunction={deleteFunction}
|
||||||
wrapWithUnsavedChangesProvider={wrapWithUnsavedChangesProvider}
|
wrapWithUnsavedChangesProvider={wrapWithUnsavedChangesProvider}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import { UpdateTab } from '../DetailView/common/UpdateTab'
|
|||||||
import { DetailView, TabType } from '../DetailView/DetailView'
|
import { DetailView, TabType } from '../DetailView/DetailView'
|
||||||
import { BoundTab } from './Tabs/BoundTab'
|
import { BoundTab } from './Tabs/BoundTab'
|
||||||
import { TimeUnitTab } from './Tabs/TimeUnitTab.tsx'
|
import { TimeUnitTab } from './Tabs/TimeUnitTab.tsx'
|
||||||
import { validateTimeBound } from '@/shared/validators/timeBound'
|
import { validateTimeBound, validateTimeBoundFields } from '@/shared/validators/timeBound'
|
||||||
|
|
||||||
export const TimeBoundDetails = ({
|
export const TimeBoundDetails = ({
|
||||||
wrapWithUnsavedChangesProvider = true,
|
wrapWithUnsavedChangesProvider = true,
|
||||||
@@ -108,6 +108,7 @@ export const TimeBoundDetails = ({
|
|||||||
isNew={isNew}
|
isNew={isNew}
|
||||||
onWrite={onWrite}
|
onWrite={onWrite}
|
||||||
validator={validateTimeBound}
|
validator={validateTimeBound}
|
||||||
|
validateFields={validateTimeBoundFields}
|
||||||
deleteFunction={deleteFunction}
|
deleteFunction={deleteFunction}
|
||||||
wrapWithUnsavedChangesProvider={wrapWithUnsavedChangesProvider}
|
wrapWithUnsavedChangesProvider={wrapWithUnsavedChangesProvider}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import { DetailView, TabType } from '../DetailView/DetailView'
|
|||||||
import { LocalityTab } from './Tabs/LocalityTab'
|
import { LocalityTab } from './Tabs/LocalityTab'
|
||||||
import { TimeUnitTab } from './Tabs/TimeUnitTab'
|
import { TimeUnitTab } from './Tabs/TimeUnitTab'
|
||||||
import { TimeUnitUpdateTab } from './Tabs/TimeUnitUpdateTab'
|
import { TimeUnitUpdateTab } from './Tabs/TimeUnitUpdateTab'
|
||||||
import { validateTimeUnit } from '@/shared/validators/timeUnit'
|
import { validateTimeUnit, validateTimeUnitFields } from '@/shared/validators/timeUnit'
|
||||||
import { makeEditData } from '../DetailView/Context/DetailContext'
|
import { makeEditData } from '../DetailView/Context/DetailContext'
|
||||||
import { useDeleteTimeUnit } from '@/hooks/useDeleteTimeUnit'
|
import { useDeleteTimeUnit } from '@/hooks/useDeleteTimeUnit'
|
||||||
import { getApiErrorMessage, isDuplicateNameError } from '@/utils/api'
|
import { getApiErrorMessage, isDuplicateNameError } from '@/utils/api'
|
||||||
@@ -112,6 +112,7 @@ export const TimeUnitDetails = ({
|
|||||||
data={isNew ? emptyTimeUnit : data!}
|
data={isNew ? emptyTimeUnit : data!}
|
||||||
onWrite={onWrite}
|
onWrite={onWrite}
|
||||||
validator={validateTimeUnit}
|
validator={validateTimeUnit}
|
||||||
|
validateFields={validateTimeUnitFields}
|
||||||
deleteFunction={deleteFunction}
|
deleteFunction={deleteFunction}
|
||||||
wrapWithUnsavedChangesProvider={wrapWithUnsavedChangesProvider}
|
wrapWithUnsavedChangesProvider={wrapWithUnsavedChangesProvider}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { EditDataType, LocalityDetailsType } from '../types'
|
import { EditDataType, LocalityDetailsType } from '../types'
|
||||||
import { Validators, validator } from './validator'
|
import { Validators, validateFields, validator } from './validator'
|
||||||
import { validCountries } from './countryList'
|
import { validCountries } from './countryList'
|
||||||
|
|
||||||
const pollenFields = ['pers_pollen_ap', 'pers_pollen_nap', 'pers_pollen_other'] as const
|
const pollenFields = ['pers_pollen_ap', 'pers_pollen_nap', 'pers_pollen_other'] as const
|
||||||
@@ -58,10 +58,9 @@ const validatePollenRecordTotal = (editData: Partial<EditDataType<LocalityDetail
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
export const validateLocality = (
|
const createLocalityValidators = (
|
||||||
editData: EditDataType<LocalityDetailsType>,
|
editData: EditDataType<LocalityDetailsType>
|
||||||
fieldName: keyof EditDataType<LocalityDetailsType>
|
): Validators<Partial<EditDataType<LocalityDetailsType>>> => {
|
||||||
) => {
|
|
||||||
const isAbsoluteDatingMethod = editData.date_meth === 'absolute'
|
const isAbsoluteDatingMethod = editData.date_meth === 'absolute'
|
||||||
const isTimeUnitDatingMethod = editData.date_meth === 'time_unit'
|
const isTimeUnitDatingMethod = editData.date_meth === 'time_unit'
|
||||||
const isCompositeDatingMethod = editData.date_meth === 'composite'
|
const isCompositeDatingMethod = editData.date_meth === 'composite'
|
||||||
@@ -80,7 +79,7 @@ export const validateLocality = (
|
|||||||
|
|
||||||
const compositeDatingMethodRequiredText = 'One age row must follow the rules for Absolute, the other for Time Unit'
|
const compositeDatingMethodRequiredText = 'One age row must follow the rules for Absolute, the other for Time Unit'
|
||||||
|
|
||||||
const validators: Validators<Partial<EditDataType<LocalityDetailsType>>> = {
|
return {
|
||||||
// const isNew = editData.lid === undefined
|
// const isNew = editData.lid === undefined
|
||||||
date_meth: {
|
date_meth: {
|
||||||
name: 'Dating method',
|
name: 'Dating method',
|
||||||
@@ -266,6 +265,18 @@ export const validateLocality = (
|
|||||||
asString: (value: string) => validateFraction('Maximum fraction', value),
|
asString: (value: string) => validateFraction('Maximum fraction', value),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const validateLocality = (
|
||||||
|
editData: EditDataType<LocalityDetailsType>,
|
||||||
|
fieldName: keyof EditDataType<LocalityDetailsType>
|
||||||
|
) => {
|
||||||
|
const validators = createLocalityValidators(editData)
|
||||||
return validator<EditDataType<LocalityDetailsType>>(validators, editData, fieldName)
|
return validator<EditDataType<LocalityDetailsType>>(validators, editData, fieldName)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const validateLocalityFields = (editData: Partial<EditDataType<LocalityDetailsType>>) =>
|
||||||
|
validateFields<EditDataType<LocalityDetailsType>>(
|
||||||
|
createLocalityValidators(editData as EditDataType<LocalityDetailsType>),
|
||||||
|
editData
|
||||||
|
)
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { EditDataType, Museum } from '../types'
|
import { EditDataType, Museum } from '../types'
|
||||||
import { Validators, validator } from './validator'
|
import { Validators, validateFields, validator } from './validator'
|
||||||
|
|
||||||
/*
|
/*
|
||||||
museum: string;
|
museum: string;
|
||||||
@@ -14,36 +14,39 @@ import { Validators, validator } from './validator'
|
|||||||
used_gene: boolean | null;
|
used_gene: boolean | null;
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export const validateMuseum = (editData: EditDataType<Museum>, fieldName: keyof EditDataType<Museum>) => {
|
const museumValidators: Validators<Partial<EditDataType<Museum>>> = {
|
||||||
const validators: Validators<Partial<EditDataType<Museum>>> = {
|
museum: {
|
||||||
museum: {
|
name: 'Museum',
|
||||||
name: 'Museum',
|
required: true,
|
||||||
required: true,
|
asString: museumCode => {
|
||||||
asString: museumCode => {
|
if (museumCode.indexOf(' ') !== -1) return 'Museum code must not contain a space'
|
||||||
if (museumCode.indexOf(' ') !== -1) return 'Museum code must not contain a space'
|
return null
|
||||||
return null
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
institution: {
|
},
|
||||||
name: 'Institution',
|
institution: {
|
||||||
required: true,
|
name: 'Institution',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
city: {
|
||||||
|
name: 'City',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
country: {
|
||||||
|
name: 'Country',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
state_code: {
|
||||||
|
name: 'State code',
|
||||||
|
asString: value => {
|
||||||
|
if (value.length > 5) return 'State code must contain a maximum of 5 characters'
|
||||||
|
return null
|
||||||
},
|
},
|
||||||
city: {
|
},
|
||||||
name: 'City',
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
country: {
|
|
||||||
name: 'Country',
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
state_code: {
|
|
||||||
name: 'State code',
|
|
||||||
asString: value => {
|
|
||||||
if (value.length > 5) return 'State code must contain a maximum of 5 characters'
|
|
||||||
return null
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
return validator<EditDataType<Museum>>(validators, editData, fieldName)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const validateMuseum = (editData: EditDataType<Museum>, fieldName: keyof EditDataType<Museum>) => {
|
||||||
|
return validator<EditDataType<Museum>>(museumValidators, editData, fieldName)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const validateMuseumFields = (editData: Partial<EditDataType<Museum>>) =>
|
||||||
|
validateFields<EditDataType<Museum>>(museumValidators, editData)
|
||||||
|
|||||||
@@ -1,51 +1,54 @@
|
|||||||
import { EditDataType, PersonDetailsType } from '../types'
|
import { EditDataType, PersonDetailsType } from '../types'
|
||||||
import { Validators, validator } from './validator'
|
import { Validators, validateFields, validator } from './validator'
|
||||||
|
|
||||||
|
const personValidators: Validators<Partial<EditDataType<PersonDetailsType>>> = {
|
||||||
|
initials: {
|
||||||
|
name: 'initials',
|
||||||
|
required: true,
|
||||||
|
asString: true,
|
||||||
|
minLength: 2,
|
||||||
|
maxLength: 8,
|
||||||
|
},
|
||||||
|
first_name: {
|
||||||
|
name: 'First Name',
|
||||||
|
required: true,
|
||||||
|
asString: true,
|
||||||
|
minLength: 2,
|
||||||
|
maxLength: 20,
|
||||||
|
},
|
||||||
|
surname: {
|
||||||
|
name: 'Surname',
|
||||||
|
required: true,
|
||||||
|
asString: true,
|
||||||
|
minLength: 2,
|
||||||
|
maxLength: 20,
|
||||||
|
},
|
||||||
|
email: {
|
||||||
|
name: 'Email',
|
||||||
|
required: true,
|
||||||
|
asString: true,
|
||||||
|
minLength: 5,
|
||||||
|
maxLength: 30,
|
||||||
|
},
|
||||||
|
organization: {
|
||||||
|
name: 'Organization',
|
||||||
|
required: true,
|
||||||
|
asString: true,
|
||||||
|
minLength: 2,
|
||||||
|
maxLength: 30,
|
||||||
|
},
|
||||||
|
country: {
|
||||||
|
name: 'Country',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
export const validatePerson = (
|
export const validatePerson = (
|
||||||
editData: EditDataType<PersonDetailsType>,
|
editData: EditDataType<PersonDetailsType>,
|
||||||
fieldName: keyof EditDataType<PersonDetailsType>
|
fieldName: keyof EditDataType<PersonDetailsType>
|
||||||
) => {
|
) => {
|
||||||
const validators: Validators<Partial<EditDataType<PersonDetailsType>>> = {
|
return validator<EditDataType<PersonDetailsType>>(personValidators, editData, fieldName)
|
||||||
initials: {
|
|
||||||
name: 'initials',
|
|
||||||
required: true,
|
|
||||||
asString: true,
|
|
||||||
minLength: 2,
|
|
||||||
maxLength: 8,
|
|
||||||
},
|
|
||||||
first_name: {
|
|
||||||
name: 'First Name',
|
|
||||||
required: true,
|
|
||||||
asString: true,
|
|
||||||
minLength: 2,
|
|
||||||
maxLength: 20,
|
|
||||||
},
|
|
||||||
surname: {
|
|
||||||
name: 'Surname',
|
|
||||||
required: true,
|
|
||||||
asString: true,
|
|
||||||
minLength: 2,
|
|
||||||
maxLength: 20,
|
|
||||||
},
|
|
||||||
email: {
|
|
||||||
name: 'Email',
|
|
||||||
required: true,
|
|
||||||
asString: true,
|
|
||||||
minLength: 5,
|
|
||||||
maxLength: 30,
|
|
||||||
},
|
|
||||||
organization: {
|
|
||||||
name: 'Organization',
|
|
||||||
required: true,
|
|
||||||
asString: true,
|
|
||||||
minLength: 2,
|
|
||||||
maxLength: 30,
|
|
||||||
},
|
|
||||||
country: {
|
|
||||||
name: 'Country',
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
return validator<EditDataType<PersonDetailsType>>(validators, editData, fieldName)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const validatePersonFields = (editData: Partial<EditDataType<PersonDetailsType>>) =>
|
||||||
|
validateFields<EditDataType<PersonDetailsType>>(personValidators, editData)
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { EditDataType, RegionCoordinator, RegionCountry, RegionDetails } from '../types'
|
import { EditDataType, RegionCoordinator, RegionCountry, RegionDetails } from '../types'
|
||||||
import { Validators, validator, ValidationError } from './validator'
|
import { Validators, validateFields, validator, ValidationError } from './validator'
|
||||||
|
|
||||||
const countryCheck = (countries: EditDataType<RegionCountry[]>) => {
|
const countryCheck = (countries: EditDataType<RegionCountry[]>) => {
|
||||||
for (const country of countries) {
|
for (const country of countries) {
|
||||||
@@ -21,29 +21,32 @@ const coordinatorCheck = (coordinators: EditDataType<RegionCoordinator[]>) => {
|
|||||||
}
|
}
|
||||||
return null as ValidationError
|
return null as ValidationError
|
||||||
}
|
}
|
||||||
export const validateRegion = (editData: EditDataType<RegionDetails>, fieldName: keyof EditDataType<RegionDetails>) => {
|
const regionValidators: Validators<Partial<EditDataType<RegionDetails>>> = {
|
||||||
const validators: Validators<Partial<EditDataType<RegionDetails>>> = {
|
reg_coord_id: {
|
||||||
reg_coord_id: {
|
name: 'reg_coord_id',
|
||||||
name: 'reg_coord_id',
|
required: true,
|
||||||
required: true,
|
},
|
||||||
|
region: {
|
||||||
|
name: 'Region',
|
||||||
|
required: true,
|
||||||
|
asString: value => {
|
||||||
|
if (value.trim().length === 0) return 'Region name must not be empty'
|
||||||
|
return null
|
||||||
},
|
},
|
||||||
region: {
|
},
|
||||||
name: 'Region',
|
now_reg_coord_country: {
|
||||||
required: true,
|
name: 'Countries',
|
||||||
asString: value => {
|
miscArray: countryCheck,
|
||||||
if (value.trim().length === 0) return 'Region name must not be empty'
|
},
|
||||||
return null
|
now_reg_coord_people: {
|
||||||
},
|
name: 'Region Coordinators',
|
||||||
},
|
miscArray: coordinatorCheck,
|
||||||
now_reg_coord_country: {
|
},
|
||||||
name: 'Countries',
|
|
||||||
miscArray: countryCheck,
|
|
||||||
},
|
|
||||||
now_reg_coord_people: {
|
|
||||||
name: 'Region Coordinators',
|
|
||||||
miscArray: coordinatorCheck,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
return validator<EditDataType<RegionDetails>>(validators, editData, fieldName)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const validateRegion = (editData: EditDataType<RegionDetails>, fieldName: keyof EditDataType<RegionDetails>) => {
|
||||||
|
return validator<EditDataType<RegionDetails>>(regionValidators, editData, fieldName)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const validateRegionFields = (editData: Partial<EditDataType<RegionDetails>>) =>
|
||||||
|
validateFields<EditDataType<RegionDetails>>(regionValidators, editData)
|
||||||
|
|||||||
@@ -1,194 +1,201 @@
|
|||||||
import { taxonStatusOptions } from '../taxonStatusOptions'
|
import { taxonStatusOptions } from '../taxonStatusOptions'
|
||||||
import { EditDataType, SpeciesDetailsType } from '../types'
|
import { EditDataType, SpeciesDetailsType } from '../types'
|
||||||
import { Validators, validator } from './validator'
|
import { Validators, validateFields, validator } from './validator'
|
||||||
|
|
||||||
const isEmptyUniqIdentifier = (unique_identifier: string) => {
|
const isEmptyUniqIdentifier = (unique_identifier: string) => {
|
||||||
if (unique_identifier === '' || unique_identifier === '-') return true
|
if (unique_identifier === '' || unique_identifier === '-') return true
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const validatePositiveInteger = (name: string, value: number) => {
|
||||||
|
if (!Number.isInteger(value) || value < 1) return `${name} must be a positive integer.`
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const validateNonNegativeInteger = (name: string, value: number) => {
|
||||||
|
if (!Number.isInteger(value) || value < 0) return `${name} must be a non-negative integer.`
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const validateIntegerInRange = (name: string, value: number, min: number, max: number) => {
|
||||||
|
if (!Number.isInteger(value)) return `${name} must be a whole number.`
|
||||||
|
if (value < min || value > max) return `${name} must be between ${min} and ${max}.`
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const createSpeciesValidators = (
|
||||||
|
editData: EditDataType<SpeciesDetailsType>
|
||||||
|
): Validators<Partial<EditDataType<SpeciesDetailsType>>> => ({
|
||||||
|
subclass_or_superorder_name: {
|
||||||
|
name: 'Subclass or Superorder',
|
||||||
|
asString: (subClassName: string) => {
|
||||||
|
if (subClassName.indexOf(' ') !== -1) return 'Subclass must not contain any spaces.'
|
||||||
|
if (subClassName.indexOf('/') !== -1 && editData.taxonomic_status !== 'informal species')
|
||||||
|
return 'Subclass must not contain a forward slash (/), unless Taxonomic Status is set to "informal species".'
|
||||||
|
return
|
||||||
|
},
|
||||||
|
},
|
||||||
|
order_name: {
|
||||||
|
name: 'Order',
|
||||||
|
required: true,
|
||||||
|
asString: (orderName: string) => {
|
||||||
|
if (orderName !== 'incertae sedis' && orderName.indexOf(' ') !== -1)
|
||||||
|
return 'Order must not contain any spaces, unless the value is "incertae sedis".'
|
||||||
|
if (orderName.indexOf('/') !== -1 && editData.taxonomic_status !== 'informal species')
|
||||||
|
return 'Order must not contain a forward slash (/), unless Taxonomic Status is set to "informal species".'
|
||||||
|
return
|
||||||
|
},
|
||||||
|
},
|
||||||
|
suborder_or_superfamily_name: {
|
||||||
|
name: 'Suborder or Superfamily',
|
||||||
|
asString: (subOrderName: string) => {
|
||||||
|
if (subOrderName.indexOf(' ') !== -1) return 'Suborder must not contain any spaces.'
|
||||||
|
if (subOrderName.indexOf('/') !== -1 && editData.taxonomic_status !== 'informal species')
|
||||||
|
return 'Suborder must not contain a forward slash (/), unless Taxonomic Status is set to "informal species".'
|
||||||
|
return
|
||||||
|
},
|
||||||
|
},
|
||||||
|
family_name: {
|
||||||
|
name: 'Family',
|
||||||
|
required: true,
|
||||||
|
asString: (familyName: string) => {
|
||||||
|
if (familyName !== 'incertae sedis' && familyName.indexOf(' ') !== -1)
|
||||||
|
return 'Family must not contain any spaces, unless the value is "incertae sedis".'
|
||||||
|
if (familyName !== 'indet.' && editData.order_name === 'indet.')
|
||||||
|
return 'when the Family is indet., Genus must also be indet.'
|
||||||
|
if (familyName.indexOf('/') !== -1 && editData.taxonomic_status !== 'informal species')
|
||||||
|
return 'Family must not contain a forward slash (/), unless Taxonomic Status is set to "informal species".'
|
||||||
|
return
|
||||||
|
},
|
||||||
|
},
|
||||||
|
subfamily_name: {
|
||||||
|
name: 'Subfamily or Tribe',
|
||||||
|
asString: (subFamilyName: string) => {
|
||||||
|
if (subFamilyName.indexOf(' ') !== -1) return 'Subfamily must not contain any spaces.'
|
||||||
|
if (subFamilyName.indexOf('/') !== -1 && editData.taxonomic_status !== 'informal species')
|
||||||
|
return 'Subfamily must not contain a forward slash (/), unless Taxonomic Status is set to "informal species".'
|
||||||
|
return
|
||||||
|
},
|
||||||
|
},
|
||||||
|
genus_name: {
|
||||||
|
name: 'Genus',
|
||||||
|
required: true,
|
||||||
|
asString: (genusName: string) => {
|
||||||
|
if (genusName.indexOf(' ') !== -1) return 'Genus must not contain any spaces.'
|
||||||
|
if (genusName !== 'indet.' && editData.family_name === 'indet.')
|
||||||
|
return 'when the Family is indet., Genus must also be indet.'
|
||||||
|
if (genusName !== 'gen.' && editData.family_name === 'fam.') return 'when the Family is fam., Genus must be gen.'
|
||||||
|
if (genusName.indexOf('/') !== -1 && editData.taxonomic_status !== 'informal species')
|
||||||
|
return 'Genus must not contain a forward slash (/), unless Taxonomic Status is set to "informal species".'
|
||||||
|
return
|
||||||
|
},
|
||||||
|
},
|
||||||
|
species_name: {
|
||||||
|
name: 'Species',
|
||||||
|
required: true,
|
||||||
|
asString: (speciesName: string) => {
|
||||||
|
if (speciesName.indexOf(' ') !== -1) return 'Species name must not contain any spaces.'
|
||||||
|
if (speciesName !== 'indet.' && editData.genus_name === 'indet.')
|
||||||
|
return 'when the Genus is indet., Species must also be indet.'
|
||||||
|
if (speciesName !== 'sp.' && editData.genus_name === 'gen.') return 'when the Genus is gen., Species must be sp.'
|
||||||
|
if (speciesName.indexOf('/') !== -1 && editData.taxonomic_status !== 'informal species')
|
||||||
|
return 'Species must not contain a forward slash (/), unless Taxonomic Status is set to "informal species".'
|
||||||
|
return
|
||||||
|
},
|
||||||
|
},
|
||||||
|
taxonomic_status: {
|
||||||
|
name: 'Taxonomic Status',
|
||||||
|
asString: (taxonomicStatus: string) => {
|
||||||
|
const allowedTaxonStatuses = taxonStatusOptions
|
||||||
|
.slice(1)
|
||||||
|
.filter((option): option is string => typeof option === 'string')
|
||||||
|
|
||||||
|
if (!allowedTaxonStatuses.includes(taxonomicStatus) && taxonomicStatus !== '')
|
||||||
|
return `Taxonomic Status must be one of the following (or left empty): ${allowedTaxonStatuses.join(', ')}.`
|
||||||
|
return
|
||||||
|
},
|
||||||
|
},
|
||||||
|
body_mass: {
|
||||||
|
name: 'Body Mass (g)',
|
||||||
|
asNumber: (value: number) => validatePositiveInteger('Body Mass (g)', value),
|
||||||
|
},
|
||||||
|
brain_mass: {
|
||||||
|
name: 'Brain Mass (g)',
|
||||||
|
asNumber: (value: number) => validatePositiveInteger('Brain Mass (g)', value),
|
||||||
|
},
|
||||||
|
mw_or_low: {
|
||||||
|
name: 'Cusp Relief Low (OR%)',
|
||||||
|
asNumber: (value: number) => validateIntegerInRange('Cusp Relief Low (OR%)', value, 0, 100),
|
||||||
|
},
|
||||||
|
mw_or_high: {
|
||||||
|
name: 'Cusp Relief High (OR%)',
|
||||||
|
asNumber: (value: number) => validateIntegerInRange('Cusp Relief High (OR%)', value, 0, 100),
|
||||||
|
},
|
||||||
|
mw_cs_sharp: {
|
||||||
|
name: 'Cusp Shape Sharp (CS%)',
|
||||||
|
asNumber: (value: number) => validateIntegerInRange('Cusp Shape Sharp (CS%)', value, 0, 100),
|
||||||
|
},
|
||||||
|
mw_cs_round: {
|
||||||
|
name: 'Cusp Shape Rounded (CS%)',
|
||||||
|
asNumber: (value: number) => validateIntegerInRange('Cusp Shape Rounded (CS%)', value, 0, 100),
|
||||||
|
},
|
||||||
|
mw_cs_blunt: {
|
||||||
|
name: 'Cusp Shape Blunt (CS%)',
|
||||||
|
asNumber: (value: number) => validateIntegerInRange('Cusp Shape Blunt (CS%)', value, 0, 100),
|
||||||
|
},
|
||||||
|
|
||||||
|
mw_scale_min: {
|
||||||
|
name: 'Scale Minimum',
|
||||||
|
asNumber: (value: number) => {
|
||||||
|
const nonNegativeError = validateNonNegativeInteger('Scale Minimum', value)
|
||||||
|
if (nonNegativeError) return nonNegativeError
|
||||||
|
if (typeof editData.mw_scale_max === 'number' && value > editData.mw_scale_max)
|
||||||
|
return 'Scale Minimum cannot be greater than Scale Maximum.'
|
||||||
|
return
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mw_scale_max: {
|
||||||
|
name: 'Scale Maximum',
|
||||||
|
asNumber: (value: number) => {
|
||||||
|
const nonNegativeError = validateNonNegativeInteger('Scale Maximum', value)
|
||||||
|
if (nonNegativeError) return nonNegativeError
|
||||||
|
if (typeof editData.mw_scale_min === 'number' && value < editData.mw_scale_min)
|
||||||
|
return 'Scale Maximum cannot be less than Scale Minimum.'
|
||||||
|
return
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mw_value: {
|
||||||
|
name: 'Reported Value',
|
||||||
|
asNumber: (value: number) => {
|
||||||
|
if (value < 0) return 'Reported Value cannot be negative.'
|
||||||
|
if (typeof editData.mw_scale_min === 'number' && value < editData.mw_scale_min)
|
||||||
|
return 'Reported Value must be between Scale Minimum and Scale Maximum.'
|
||||||
|
if (typeof editData.mw_scale_max === 'number' && value > editData.mw_scale_max)
|
||||||
|
return 'Reported Value must be between Scale Minimum and Scale Maximum.'
|
||||||
|
return
|
||||||
|
},
|
||||||
|
},
|
||||||
|
unique_identifier: {
|
||||||
|
name: 'Unique Identifier',
|
||||||
|
required: true,
|
||||||
|
asString: (uniqueIdentifier: string) => {
|
||||||
|
if (editData.species_name === 'sp.' && isEmptyUniqIdentifier(uniqueIdentifier ?? ''))
|
||||||
|
return 'when the species is sp., Unique Identifier must have a value.'
|
||||||
|
return
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
export const validateSpecies = (
|
export const validateSpecies = (
|
||||||
editData: EditDataType<SpeciesDetailsType>,
|
editData: EditDataType<SpeciesDetailsType>,
|
||||||
fieldName: keyof EditDataType<SpeciesDetailsType>
|
fieldName: keyof EditDataType<SpeciesDetailsType>
|
||||||
) => {
|
) => {
|
||||||
const validatePositiveInteger = (name: string, value: number) => {
|
const validators = createSpeciesValidators(editData)
|
||||||
if (!Number.isInteger(value) || value < 1) return `${name} must be a positive integer.`
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const validateNonNegativeInteger = (name: string, value: number) => {
|
|
||||||
if (!Number.isInteger(value) || value < 0) return `${name} must be a non-negative integer.`
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const validateIntegerInRange = (name: string, value: number, min: number, max: number) => {
|
|
||||||
if (!Number.isInteger(value)) return `${name} must be a whole number.`
|
|
||||||
if (value < min || value > max) return `${name} must be between ${min} and ${max}.`
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const validators: Validators<Partial<EditDataType<SpeciesDetailsType>>> = {
|
|
||||||
subclass_or_superorder_name: {
|
|
||||||
name: 'Subclass or Superorder',
|
|
||||||
asString: (subClassName: string) => {
|
|
||||||
if (subClassName.indexOf(' ') !== -1) return 'Subclass must not contain any spaces.'
|
|
||||||
if (subClassName.indexOf('/') !== -1 && editData.taxonomic_status !== 'informal species')
|
|
||||||
return 'Subclass must not contain a forward slash (/), unless Taxonomic Status is set to "informal species".'
|
|
||||||
return
|
|
||||||
},
|
|
||||||
},
|
|
||||||
order_name: {
|
|
||||||
name: 'Order',
|
|
||||||
required: true,
|
|
||||||
asString: (orderName: string) => {
|
|
||||||
if (orderName !== 'incertae sedis' && orderName.indexOf(' ') !== -1)
|
|
||||||
return 'Order must not contain any spaces, unless the value is "incertae sedis".'
|
|
||||||
if (orderName.indexOf('/') !== -1 && editData.taxonomic_status !== 'informal species')
|
|
||||||
return 'Order must not contain a forward slash (/), unless Taxonomic Status is set to "informal species".'
|
|
||||||
return
|
|
||||||
},
|
|
||||||
},
|
|
||||||
suborder_or_superfamily_name: {
|
|
||||||
name: 'Suborder or Superfamily',
|
|
||||||
asString: (subOrderName: string) => {
|
|
||||||
if (subOrderName.indexOf(' ') !== -1) return 'Suborder must not contain any spaces.'
|
|
||||||
if (subOrderName.indexOf('/') !== -1 && editData.taxonomic_status !== 'informal species')
|
|
||||||
return 'Suborder must not contain a forward slash (/), unless Taxonomic Status is set to "informal species".'
|
|
||||||
return
|
|
||||||
},
|
|
||||||
},
|
|
||||||
family_name: {
|
|
||||||
name: 'Family',
|
|
||||||
required: true,
|
|
||||||
asString: (familyName: string) => {
|
|
||||||
if (familyName !== 'incertae sedis' && familyName.indexOf(' ') !== -1)
|
|
||||||
return 'Family must not contain any spaces, unless the value is "incertae sedis".'
|
|
||||||
if (familyName !== 'indet.' && editData.order_name === 'indet.')
|
|
||||||
return 'when the Family is indet., Genus must also be indet.'
|
|
||||||
if (familyName.indexOf('/') !== -1 && editData.taxonomic_status !== 'informal species')
|
|
||||||
return 'Family must not contain a forward slash (/), unless Taxonomic Status is set to "informal species".'
|
|
||||||
return
|
|
||||||
},
|
|
||||||
},
|
|
||||||
subfamily_name: {
|
|
||||||
name: 'Subfamily or Tribe',
|
|
||||||
asString: (subFamilyName: string) => {
|
|
||||||
if (subFamilyName.indexOf(' ') !== -1) return 'Subfamily must not contain any spaces.'
|
|
||||||
if (subFamilyName.indexOf('/') !== -1 && editData.taxonomic_status !== 'informal species')
|
|
||||||
return 'Subfamily must not contain a forward slash (/), unless Taxonomic Status is set to "informal species".'
|
|
||||||
return
|
|
||||||
},
|
|
||||||
},
|
|
||||||
genus_name: {
|
|
||||||
name: 'Genus',
|
|
||||||
required: true,
|
|
||||||
asString: (genusName: string) => {
|
|
||||||
if (genusName.indexOf(' ') !== -1) return 'Genus must not contain any spaces.'
|
|
||||||
if (genusName !== 'indet.' && editData.family_name === 'indet.')
|
|
||||||
return 'when the Family is indet., Genus must also be indet.'
|
|
||||||
if (genusName !== 'gen.' && editData.family_name === 'fam.')
|
|
||||||
return 'when the Family is fam., Genus must be gen.'
|
|
||||||
if (genusName.indexOf('/') !== -1 && editData.taxonomic_status !== 'informal species')
|
|
||||||
return 'Genus must not contain a forward slash (/), unless Taxonomic Status is set to "informal species".'
|
|
||||||
return
|
|
||||||
},
|
|
||||||
},
|
|
||||||
species_name: {
|
|
||||||
name: 'Species',
|
|
||||||
required: true,
|
|
||||||
asString: (speciesName: string) => {
|
|
||||||
if (speciesName.indexOf(' ') !== -1) return 'Species name must not contain any spaces.'
|
|
||||||
if (speciesName !== 'indet.' && editData.genus_name === 'indet.')
|
|
||||||
return 'when the Genus is indet., Species must also be indet.'
|
|
||||||
if (speciesName !== 'sp.' && editData.genus_name === 'gen.')
|
|
||||||
return 'when the Genus is gen., Species must be sp.'
|
|
||||||
if (speciesName.indexOf('/') !== -1 && editData.taxonomic_status !== 'informal species')
|
|
||||||
return 'Species must not contain a forward slash (/), unless Taxonomic Status is set to "informal species".'
|
|
||||||
return
|
|
||||||
},
|
|
||||||
},
|
|
||||||
taxonomic_status: {
|
|
||||||
name: 'Taxonomic Status',
|
|
||||||
asString: (taxonomicStatus: string) => {
|
|
||||||
const allowedTaxonStatuses = taxonStatusOptions
|
|
||||||
.slice(1)
|
|
||||||
.filter((option): option is string => typeof option === 'string')
|
|
||||||
|
|
||||||
if (!allowedTaxonStatuses.includes(taxonomicStatus) && taxonomicStatus !== '')
|
|
||||||
return `Taxonomic Status must be one of the following (or left empty): ${allowedTaxonStatuses.join(', ')}.`
|
|
||||||
return
|
|
||||||
},
|
|
||||||
},
|
|
||||||
body_mass: {
|
|
||||||
name: 'Body Mass (g)',
|
|
||||||
asNumber: (value: number) => validatePositiveInteger('Body Mass (g)', value),
|
|
||||||
},
|
|
||||||
brain_mass: {
|
|
||||||
name: 'Brain Mass (g)',
|
|
||||||
asNumber: (value: number) => validatePositiveInteger('Brain Mass (g)', value),
|
|
||||||
},
|
|
||||||
mw_or_low: {
|
|
||||||
name: 'Cusp Relief Low (OR%)',
|
|
||||||
asNumber: (value: number) => validateIntegerInRange('Cusp Relief Low (OR%)', value, 0, 100),
|
|
||||||
},
|
|
||||||
mw_or_high: {
|
|
||||||
name: 'Cusp Relief High (OR%)',
|
|
||||||
asNumber: (value: number) => validateIntegerInRange('Cusp Relief High (OR%)', value, 0, 100),
|
|
||||||
},
|
|
||||||
mw_cs_sharp: {
|
|
||||||
name: 'Cusp Shape Sharp (CS%)',
|
|
||||||
asNumber: (value: number) => validateIntegerInRange('Cusp Shape Sharp (CS%)', value, 0, 100),
|
|
||||||
},
|
|
||||||
mw_cs_round: {
|
|
||||||
name: 'Cusp Shape Rounded (CS%)',
|
|
||||||
asNumber: (value: number) => validateIntegerInRange('Cusp Shape Rounded (CS%)', value, 0, 100),
|
|
||||||
},
|
|
||||||
mw_cs_blunt: {
|
|
||||||
name: 'Cusp Shape Blunt (CS%)',
|
|
||||||
asNumber: (value: number) => validateIntegerInRange('Cusp Shape Blunt (CS%)', value, 0, 100),
|
|
||||||
},
|
|
||||||
|
|
||||||
mw_scale_min: {
|
|
||||||
name: 'Scale Minimum',
|
|
||||||
asNumber: (value: number) => {
|
|
||||||
const nonNegativeError = validateNonNegativeInteger('Scale Minimum', value)
|
|
||||||
if (nonNegativeError) return nonNegativeError
|
|
||||||
if (typeof editData.mw_scale_max === 'number' && value > editData.mw_scale_max)
|
|
||||||
return 'Scale Minimum cannot be greater than Scale Maximum.'
|
|
||||||
return
|
|
||||||
},
|
|
||||||
},
|
|
||||||
mw_scale_max: {
|
|
||||||
name: 'Scale Maximum',
|
|
||||||
asNumber: (value: number) => {
|
|
||||||
const nonNegativeError = validateNonNegativeInteger('Scale Maximum', value)
|
|
||||||
if (nonNegativeError) return nonNegativeError
|
|
||||||
if (typeof editData.mw_scale_min === 'number' && value < editData.mw_scale_min)
|
|
||||||
return 'Scale Maximum cannot be less than Scale Minimum.'
|
|
||||||
return
|
|
||||||
},
|
|
||||||
},
|
|
||||||
mw_value: {
|
|
||||||
name: 'Reported Value',
|
|
||||||
asNumber: (value: number) => {
|
|
||||||
if (value < 0) return 'Reported Value cannot be negative.'
|
|
||||||
if (typeof editData.mw_scale_min === 'number' && value < editData.mw_scale_min)
|
|
||||||
return 'Reported Value must be between Scale Minimum and Scale Maximum.'
|
|
||||||
if (typeof editData.mw_scale_max === 'number' && value > editData.mw_scale_max)
|
|
||||||
return 'Reported Value must be between Scale Minimum and Scale Maximum.'
|
|
||||||
return
|
|
||||||
},
|
|
||||||
},
|
|
||||||
unique_identifier: {
|
|
||||||
name: 'Unique Identifier',
|
|
||||||
required: true,
|
|
||||||
asString: (uniqueIdentifier: string) => {
|
|
||||||
if (editData.species_name === 'sp.' && isEmptyUniqIdentifier(uniqueIdentifier ?? ''))
|
|
||||||
return 'when the species is sp., Unique Identifier must have a value.'
|
|
||||||
return
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
return validator<EditDataType<SpeciesDetailsType>>(validators, editData, fieldName)
|
return validator<EditDataType<SpeciesDetailsType>>(validators, editData, fieldName)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const validateSpeciesFields = (editData: Partial<EditDataType<SpeciesDetailsType>>) =>
|
||||||
|
validateFields<EditDataType<SpeciesDetailsType>>(
|
||||||
|
createSpeciesValidators(editData as EditDataType<SpeciesDetailsType>),
|
||||||
|
editData
|
||||||
|
)
|
||||||
|
|||||||
@@ -1,64 +1,74 @@
|
|||||||
import { EditDataType, TimeUnitDetailsType } from '../types'
|
import { EditDataType, TimeUnitDetailsType } from '../types'
|
||||||
import { Validators, validator } from './validator'
|
import { Validators, validateFields, validator } from './validator'
|
||||||
|
|
||||||
|
const createTimeUnitValidators = (
|
||||||
|
editData: EditDataType<TimeUnitDetailsType>
|
||||||
|
): Validators<EditDataType<Partial<TimeUnitDetailsType>>> => ({
|
||||||
|
tu_display_name: {
|
||||||
|
name: 'Name',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
sequence: {
|
||||||
|
name: 'Sequence',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
up_bnd: {
|
||||||
|
name: 'New Upper Bound',
|
||||||
|
required: true,
|
||||||
|
asNumber: (num: number) => {
|
||||||
|
if (num === editData.low_bnd) return 'Upper and lower bounds cannot be the same'
|
||||||
|
return
|
||||||
|
},
|
||||||
|
},
|
||||||
|
low_bnd: {
|
||||||
|
name: 'New Lower Bound',
|
||||||
|
required: true,
|
||||||
|
asNumber: (num: number) => {
|
||||||
|
if (num === editData.up_bnd) return 'Upper and lower bounds cannot be the same'
|
||||||
|
return
|
||||||
|
},
|
||||||
|
},
|
||||||
|
up_bound: {
|
||||||
|
name: 'Upper Bound',
|
||||||
|
required: () =>
|
||||||
|
editData.up_bnd !== undefined && editData.up_bnd !== null
|
||||||
|
? `Upper bound with ID ${editData.up_bnd} does not exist`
|
||||||
|
: 'This field is required',
|
||||||
|
miscCheck: () => {
|
||||||
|
if (editData.low_bound && editData.low_bound.age! === editData.up_bound!.age!) {
|
||||||
|
return 'Upper bound age cannot be the same as lower bound age'
|
||||||
|
}
|
||||||
|
if (editData.low_bound && editData.low_bound.age! < editData.up_bound!.age!) {
|
||||||
|
return 'Upper bound age has to be lower than lower bound age'
|
||||||
|
}
|
||||||
|
return
|
||||||
|
},
|
||||||
|
},
|
||||||
|
low_bound: {
|
||||||
|
name: 'Lower Bound',
|
||||||
|
required: () =>
|
||||||
|
editData.low_bnd !== undefined && editData.low_bnd !== null
|
||||||
|
? `Lower bound with ID ${editData.low_bnd} does not exist`
|
||||||
|
: 'This field is required',
|
||||||
|
miscCheck: () => {
|
||||||
|
if (editData.up_bound && editData.up_bound.age! === editData.low_bound!.age!) {
|
||||||
|
return 'Lower bound age cannot be the same as upper bound age'
|
||||||
|
}
|
||||||
|
if (editData.up_bound && editData.up_bound.age! > editData.low_bound!.age!) {
|
||||||
|
return 'Lower bound age has to be higher than upper bound age'
|
||||||
|
}
|
||||||
|
return
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
export const validateTimeUnit = (editData: EditDataType<TimeUnitDetailsType>, fieldName: keyof TimeUnitDetailsType) => {
|
export const validateTimeUnit = (editData: EditDataType<TimeUnitDetailsType>, fieldName: keyof TimeUnitDetailsType) => {
|
||||||
const validators: Validators<EditDataType<Partial<TimeUnitDetailsType>>> = {
|
const validators = createTimeUnitValidators(editData)
|
||||||
tu_display_name: {
|
|
||||||
name: 'Name',
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
sequence: {
|
|
||||||
name: 'Sequence',
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
up_bnd: {
|
|
||||||
name: 'New Upper Bound',
|
|
||||||
required: true,
|
|
||||||
asNumber: (num: number) => {
|
|
||||||
if (num === editData.low_bnd) return 'Upper and lower bounds cannot be the same'
|
|
||||||
return
|
|
||||||
},
|
|
||||||
},
|
|
||||||
low_bnd: {
|
|
||||||
name: 'New Lower Bound',
|
|
||||||
required: true,
|
|
||||||
asNumber: (num: number) => {
|
|
||||||
if (num === editData.up_bnd) return 'Upper and lower bounds cannot be the same'
|
|
||||||
return
|
|
||||||
},
|
|
||||||
},
|
|
||||||
up_bound: {
|
|
||||||
name: 'Upper Bound',
|
|
||||||
required: () =>
|
|
||||||
editData.up_bnd !== undefined && editData.up_bnd !== null
|
|
||||||
? `Upper bound with ID ${editData.up_bnd} does not exist`
|
|
||||||
: 'This field is required',
|
|
||||||
miscCheck: () => {
|
|
||||||
if (editData.low_bound && editData.low_bound.age! === editData.up_bound!.age!) {
|
|
||||||
return 'Upper bound age cannot be the same as lower bound age'
|
|
||||||
}
|
|
||||||
if (editData.low_bound && editData.low_bound.age! < editData.up_bound!.age!) {
|
|
||||||
return 'Upper bound age has to be lower than lower bound age'
|
|
||||||
}
|
|
||||||
return
|
|
||||||
},
|
|
||||||
},
|
|
||||||
low_bound: {
|
|
||||||
name: 'Lower Bound',
|
|
||||||
required: () =>
|
|
||||||
editData.low_bnd !== undefined && editData.low_bnd !== null
|
|
||||||
? `Lower bound with ID ${editData.low_bnd} does not exist`
|
|
||||||
: 'This field is required',
|
|
||||||
miscCheck: () => {
|
|
||||||
if (editData.up_bound && editData.up_bound.age! === editData.low_bound!.age!) {
|
|
||||||
return 'Lower bound age cannot be the same as upper bound age'
|
|
||||||
}
|
|
||||||
if (editData.up_bound && editData.up_bound.age! > editData.low_bound!.age!) {
|
|
||||||
return 'Lower bound age has to be higher than upper bound age'
|
|
||||||
}
|
|
||||||
return
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
return validator<EditDataType<TimeUnitDetailsType>>(validators, editData, fieldName)
|
return validator<EditDataType<TimeUnitDetailsType>>(validators, editData, fieldName)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const validateTimeUnitFields = (editData: Partial<EditDataType<TimeUnitDetailsType>>) =>
|
||||||
|
validateFields<EditDataType<TimeUnitDetailsType>>(
|
||||||
|
createTimeUnitValidators(editData as EditDataType<TimeUnitDetailsType>),
|
||||||
|
editData
|
||||||
|
)
|
||||||
|
|||||||
@@ -167,6 +167,7 @@ jest.mock('@/util/taxonomyUtilities', () => ({
|
|||||||
|
|
||||||
jest.mock('@/shared/validators/species', () => ({
|
jest.mock('@/shared/validators/species', () => ({
|
||||||
validateSpecies: jest.fn(() => ({ name: '', error: null })),
|
validateSpecies: jest.fn(() => ({ name: '', error: null })),
|
||||||
|
validateSpeciesFields: jest.fn(() => []),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
const mockUseDetailContext = useDetailContext as jest.MockedFunction<() => DetailContextType<LocalityDetailsType>>
|
const mockUseDetailContext = useDetailContext as jest.MockedFunction<() => DetailContextType<LocalityDetailsType>>
|
||||||
|
|||||||
@@ -1,6 +1,12 @@
|
|||||||
import { describe, expect, it } from '@jest/globals'
|
import { describe, expect, it } from '@jest/globals'
|
||||||
|
|
||||||
|
import { validateLocalityFields } from '@/shared/validators/locality'
|
||||||
|
import { validateMuseumFields } from '@/shared/validators/museum'
|
||||||
|
import { validatePersonFields } from '@/shared/validators/person'
|
||||||
|
import { validateRegionFields } from '@/shared/validators/region'
|
||||||
|
import { validateSpeciesFields } from '@/shared/validators/species'
|
||||||
import { validateTimeBoundFields } from '@/shared/validators/timeBound'
|
import { validateTimeBoundFields } from '@/shared/validators/timeBound'
|
||||||
|
import { validateTimeUnitFields } from '@/shared/validators/timeUnit'
|
||||||
import { validateFields, type Validators } from '@/shared/validators/validator'
|
import { validateFields, type Validators } from '@/shared/validators/validator'
|
||||||
|
|
||||||
type TestData = {
|
type TestData = {
|
||||||
@@ -43,3 +49,74 @@ describe('validateTimeBoundFields', () => {
|
|||||||
])
|
])
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('entity full-field validators', () => {
|
||||||
|
it('reports missing Time Unit required fields', () => {
|
||||||
|
const errors = validateTimeUnitFields({})
|
||||||
|
|
||||||
|
expect(errors).toEqual(
|
||||||
|
expect.arrayContaining([
|
||||||
|
{ name: 'Name', error: 'This field is required' },
|
||||||
|
{ name: 'Sequence', error: 'This field is required' },
|
||||||
|
{ name: 'New Upper Bound', error: 'This field is required' },
|
||||||
|
{ name: 'New Lower Bound', error: 'This field is required' },
|
||||||
|
{ name: 'Upper Bound', error: 'This field is required' },
|
||||||
|
{ name: 'Lower Bound', error: 'This field is required' },
|
||||||
|
])
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('reports missing simple entity required fields', () => {
|
||||||
|
expect(validatePersonFields({})).toEqual(
|
||||||
|
expect.arrayContaining([
|
||||||
|
{ name: 'initials', error: 'This field is required' },
|
||||||
|
{ name: 'First Name', error: 'This field is required' },
|
||||||
|
{ name: 'Country', error: 'This field is required' },
|
||||||
|
])
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(validateMuseumFields({})).toEqual(
|
||||||
|
expect.arrayContaining([
|
||||||
|
{ name: 'Museum', error: 'This field is required' },
|
||||||
|
{ name: 'Institution', error: 'This field is required' },
|
||||||
|
{ name: 'Country', error: 'This field is required' },
|
||||||
|
])
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(validateRegionFields({})).toEqual(
|
||||||
|
expect.arrayContaining([
|
||||||
|
{ name: 'reg_coord_id', error: 'This field is required' },
|
||||||
|
{ name: 'Region', error: 'This field is required' },
|
||||||
|
])
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('reports missing Species taxonomy fields', () => {
|
||||||
|
const errors = validateSpeciesFields({})
|
||||||
|
|
||||||
|
expect(errors).toEqual(
|
||||||
|
expect.arrayContaining([
|
||||||
|
{ name: 'Order', error: 'This field is required' },
|
||||||
|
{ name: 'Family', error: 'This field is required' },
|
||||||
|
{ name: 'Genus', error: 'This field is required' },
|
||||||
|
{ name: 'Species', error: 'This field is required' },
|
||||||
|
{ name: 'Unique Identifier', error: 'This field is required' },
|
||||||
|
])
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('reports missing Locality required and conditional fields', () => {
|
||||||
|
const errors = validateLocalityFields({ date_meth: 'time_unit' })
|
||||||
|
|
||||||
|
expect(errors).toEqual(
|
||||||
|
expect.arrayContaining([
|
||||||
|
{ name: 'Age (max)', error: 'This field is required' },
|
||||||
|
{ name: 'Age (min)', error: 'This field is required' },
|
||||||
|
{ name: 'Basis for age (Time unit, min)', error: 'This field is required' },
|
||||||
|
{ name: 'Basis for age (Time unit, max)', error: 'This field is required' },
|
||||||
|
{ name: 'Locality name', error: 'This field is required' },
|
||||||
|
{ name: 'Country', error: 'This field is required' },
|
||||||
|
])
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|||||||
@@ -9,12 +9,20 @@ import { PageContextProvider } from '@/components/Page'
|
|||||||
import type { TimeUnit, TimeUnitDetailsType } from '@/shared/types'
|
import type { TimeUnit, TimeUnitDetailsType } from '@/shared/types'
|
||||||
import '@testing-library/jest-dom'
|
import '@testing-library/jest-dom'
|
||||||
|
|
||||||
|
jest.setTimeout(15000)
|
||||||
|
|
||||||
jest.mock('lodash-es', () => ({
|
jest.mock('lodash-es', () => ({
|
||||||
cloneDeep: (value: unknown) => value,
|
cloneDeep: (value: unknown) => value,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
jest.mock('@/shared/validators/timeUnit', () => ({
|
jest.mock('@/shared/validators/timeUnit', () => ({
|
||||||
validateTimeUnit: () => ({ name: 'tu_display_name', error: '' }),
|
validateTimeUnit: () => ({ name: 'tu_display_name', error: '' }),
|
||||||
|
validateTimeUnitFields: () => [],
|
||||||
|
}))
|
||||||
|
|
||||||
|
jest.mock('@/shared/validators/validator', () => ({
|
||||||
|
...jest.requireActual<typeof import('@/shared/validators/validator')>('@/shared/validators/validator'),
|
||||||
|
referenceValidator: () => null,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
jest.mock('@/util/config', () => ({
|
jest.mock('@/util/config', () => ({
|
||||||
@@ -23,6 +31,10 @@ jest.mock('@/util/config', () => ({
|
|||||||
ENV: 'test',
|
ENV: 'test',
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
jest.mock('@/hooks/user', () => ({
|
||||||
|
useUser: () => ({ role: 1, initials: 'TEST' }),
|
||||||
|
}))
|
||||||
|
|
||||||
const mockEditTimeUnitMutation = jest.fn()
|
const mockEditTimeUnitMutation = jest.fn()
|
||||||
const mockGetAllTimeUnitsQuery = jest.fn()
|
const mockGetAllTimeUnitsQuery = jest.fn()
|
||||||
|
|
||||||
@@ -38,6 +50,17 @@ jest.mock('@/redux/timeUnitReducer', () => ({
|
|||||||
useGetAllTimeUnitsQuery: (...args: unknown[]) => mockGetAllTimeUnitsQuery(...args),
|
useGetAllTimeUnitsQuery: (...args: unknown[]) => mockGetAllTimeUnitsQuery(...args),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
jest.mock('@/redux/speciesReducer', () => ({
|
||||||
|
useGetAllSpeciesQuery: () => ({ data: undefined, isFetching: false, isLoading: false }),
|
||||||
|
useGetAllSynonymsQuery: () => ({ data: undefined, isFetching: false, isLoading: false }),
|
||||||
|
}))
|
||||||
|
|
||||||
|
jest.mock('@/redux/referenceReducer', () => ({
|
||||||
|
useGetAllReferencesQuery: () => ({ data: [], isError: false }),
|
||||||
|
useGetReferenceTypesQuery: () => ({ data: [] }),
|
||||||
|
useEditReferenceMutation: () => [jest.fn(), { isLoading: false }],
|
||||||
|
}))
|
||||||
|
|
||||||
jest.mock('@/components/Sequence/SequenceSelect', () => ({
|
jest.mock('@/components/Sequence/SequenceSelect', () => ({
|
||||||
SequenceSelect: () => <div data-testid="sequence-select" />,
|
SequenceSelect: () => <div data-testid="sequence-select" />,
|
||||||
}))
|
}))
|
||||||
@@ -178,10 +201,13 @@ describe('TimeUnit creation duplicate name handling', () => {
|
|||||||
return { unwrap: unwrapMock }
|
return { unwrap: unwrapMock }
|
||||||
})
|
})
|
||||||
|
|
||||||
renderTimeUnitCreation()
|
const { container } = renderTimeUnitCreation()
|
||||||
|
|
||||||
const user = userEvent.setup()
|
const user = userEvent.setup()
|
||||||
|
|
||||||
|
const nameInput = container.querySelector('input#tu_display_name-textfield') as HTMLInputElement
|
||||||
|
await user.type(nameInput, 'Rank Test Time Unit')
|
||||||
|
|
||||||
const rankSelect = screen.getByLabelText('Rank')
|
const rankSelect = screen.getByLabelText('Rank')
|
||||||
await user.click(rankSelect)
|
await user.click(rankSelect)
|
||||||
await user.click(await screen.findByText('No value'))
|
await user.click(await screen.findByText('No value'))
|
||||||
|
|||||||
Reference in New Issue
Block a user