add validation and other checks for species synonym adding

This commit is contained in:
juhanikat
2025-09-08 10:57:46 +03:00
parent 6f65cd2c3f
commit 33f54dcebb
7 changed files with 124 additions and 26 deletions
@@ -8,12 +8,13 @@ import { useDetailContext } from './Context/DetailContext'
import { EditDataType, Editable, Reference, Species } from '@/shared/types'
import { useState, useEffect, Fragment } from 'react'
import { referenceValidator } from '@/shared/validators/validator'
import { checkTaxonomy, convertTaxonomyFields } from '../../util/taxonomyUtilities'
import { useLazyGetAllSpeciesQuery, useLazyGetAllSynonymsQuery } from '@/redux/speciesReducer'
import { checkSpeciesTaxonomy, convertSpeciesTaxonomyFields } from '../../util/taxonomyUtilities'
import { useGetAllSpeciesQuery, useGetAllSynonymsQuery } from '@/redux/speciesReducer'
import { useNotify } from '@/hooks/notification'
import { countryBoundingBoxes } from '@/country_data/countryBoundingBoxes'
import { boundingBoxSplit, isPointInBoxes } from '@/util/isPointInBox'
import { OutOfBoundsWarningModal, OutOfBoundsWarningModalState } from './OutOfBoundsWarningModal'
import { skipToken } from '@reduxjs/toolkit/query'
export const WriteButton = <T,>({
onWrite,
@@ -28,8 +29,8 @@ export const WriteButton = <T,>({
useDetailContext<T>()
const [loading, setLoading] = useState(false)
const { notify } = useNotify()
const [getSpeciesData] = useLazyGetAllSpeciesQuery()
const [getSynonyms] = useLazyGetAllSynonymsQuery()
const { data: speciesData } = useGetAllSpeciesQuery(!taxonomy ? skipToken : undefined)
const { data: synonymData } = useGetAllSynonymsQuery(!taxonomy ? skipToken : undefined)
// For coordinate out of bounds warning
const [warningModalState, setWarningModalState] = useState<OutOfBoundsWarningModalState>({
@@ -50,6 +51,11 @@ export const WriteButton = <T,>({
return !isPointInBoxes(dec_lat, dec_long, boundingBoxSplit(countryBoundingBoxes[country]))
}
useEffect(() => {
if (taxonomy && (!speciesData || !synonymData)) setLoading(true)
else setLoading(false)
}, [speciesData, synonymData])
/* validates all fields when entering new/editing mode
to disable write button when unvisited tabs have
validation errors (e.g. missing required fields) */
@@ -107,23 +113,21 @@ export const WriteButton = <T,>({
// @ts-expect-error Reason: Typescript doesn't recognise that references do exist. Unable to find a way around it. Fix if extra time
}, [mode, editData.references])
const writeWithTaxonomyCheck = async () => {
const writeWithTaxonomyCheck = () => {
let speciesEditData: EditDataType<Species> | undefined = undefined
if (!mode.staging) {
const { data: speciesData } = await getSpeciesData(undefined, true)
const { data: synonyms } = await getSynonyms(undefined, true)
if (!speciesData) {
notify('Could not fetch species to check taxonomy data.', 'error')
return
}
if (!synonyms) {
if (!synonymData) {
notify('Could not fetch synonyms to check taxonomy data.', 'error')
return
}
// converts taxonomy fields to capitalized/lowercased
speciesEditData = convertTaxonomyFields(editData as EditDataType<Species>)
const errors = checkTaxonomy(speciesEditData, speciesData, synonyms)
speciesEditData = convertSpeciesTaxonomyFields(editData as EditDataType<Species>)
const errors = checkSpeciesTaxonomy(speciesEditData, speciesData, synonymData)
if (errors.size > 0) {
const errorMessage = [...errors].reduce((acc, currentError) => acc + `\n${currentError}`)
notify(errorMessage, 'error', null)
@@ -144,10 +148,10 @@ export const WriteButton = <T,>({
})
}
const handleWriteButtonClick = async () => {
const handleWriteButtonClick = () => {
if (taxonomy) {
setLoading(true)
await writeWithTaxonomyCheck()
writeWithTaxonomyCheck()
setLoading(false)
return
}
@@ -186,13 +190,13 @@ export const WriteButton = <T,>({
!('dec_long' in (editData as object)) ||
!('country' in (editData as object))
) {
void handleWriteButtonClick()
handleWriteButtonClick()
return
}
const localityObject = editData as unknown as { dec_lat: number; dec_long: number; country: string }
if (!isCoordinateOutOfBounds(localityObject.dec_lat, localityObject.dec_long, localityObject.country)) {
void handleWriteButtonClick()
handleWriteButtonClick()
return
}
@@ -15,7 +15,11 @@ import { useGetAllSpeciesQuery } from '@/redux/speciesReducer'
import { Box, CircularProgress } from '@mui/material'
import { skipToken } from '@reduxjs/toolkit/query'
import { MRT_ColumnDef } from 'material-react-table'
import { checkTaxonomy, convertTaxonomyFields, fixNullValuesInTaxonomyFields } from '@/util/taxonomyUtilities'
import {
checkSpeciesTaxonomy,
convertSpeciesTaxonomyFields,
fixNullValuesInTaxonomyFields,
} from '@/util/taxonomyUtilities'
import { useNotify } from '@/hooks/notification'
import { validateSpecies } from '@/shared/validators/species'
import { smallSpeciesTableColumns } from '@/common'
@@ -83,11 +87,11 @@ export const SpeciesTab = () => {
return false
}
const convertedSpecies = convertTaxonomyFields(newSpecies)
const convertedSpecies = convertSpeciesTaxonomyFields(newSpecies)
const everyLs = editData.now_ls.map(ls => ls.com_species) as unknown as Editable<Species>[]
const filteredLs = everyLs.filter(species => species.rowState === 'new')
const taxonomyErrors = checkTaxonomy(convertedSpecies, speciesData!.concat(filteredLs), [])
const taxonomyErrors = checkSpeciesTaxonomy(convertedSpecies, speciesData!.concat(filteredLs), [])
if (taxonomyErrors.size > 0) {
const errorMessage = [...taxonomyErrors].reduce((acc, currentError) => acc + `\n${currentError}`)
notify(errorMessage, 'error', null)
@@ -4,9 +4,47 @@ import { Grouped } from '@/components/DetailView/common/tabLayoutHelpers'
import { MRT_ColumnDef } from 'material-react-table'
import { EditableTable } from '@/components/DetailView/common/EditableTable'
import { EditingForm, EditingFormField } from '@/components/DetailView/common/EditingForm'
import { useNotify } from '@/hooks/notification'
import { validateSynonym } from '@/shared/validators/synonym'
import { convertSynonymTaxonomyFields } from '@/util/taxonomyUtilities'
export const SynonymTab = () => {
const { mode, setEditData, editData } = useDetailContext<SpeciesDetailsType>()
const { notify } = useNotify()
const convertAndCheckNewSynonymTaxonomy = (newSynonym: EditDataType<SpeciesSynonym>) => {
const errors = []
for (const field in newSynonym) {
const errorObject = validateSynonym(
newSynonym as unknown as EditDataType<SpeciesSynonym>,
field as unknown as keyof EditDataType<SpeciesSynonym>
)
const { error } = errorObject
if (error) errors.push(errorObject)
}
if (errors.length > 0) {
notify('Following validators failed: \n' + errors.map(e => `${e.name}: ${e.error}`).join('\n'), 'error')
return false
}
const convertedSynonym = convertSynonymTaxonomyFields(newSynonym)
for (const existingSynonym of editData.com_taxa_synonym) {
if (
convertedSynonym.syn_genus_name === existingSynonym.syn_genus_name &&
convertedSynonym.syn_species_name === existingSynonym.syn_species_name
) {
notify(
`${convertedSynonym.syn_genus_name} ${convertedSynonym.syn_species_name} \
has already been added as a synonym for this species.`,
'error',
null
)
return false
}
}
return convertedSynonym
}
const columns: MRT_ColumnDef<SpeciesSynonym>[] = [
{
@@ -34,12 +72,14 @@ export const SynonymTab = () => {
buttonText="Add new Species"
formFields={formFields}
editAction={(newSynonym: EditDataType<SpeciesSynonym>) => {
const convertedSynonym = convertAndCheckNewSynonymTaxonomy(newSynonym)
if (!convertedSynonym) return
setEditData({
...editData,
com_taxa_synonym: [
...editData.com_taxa_synonym,
{
...newSynonym,
...convertedSynonym,
rowState: 'new',
},
],
+1
View File
@@ -57,6 +57,7 @@ export const api = createApi({
'localities',
'species',
'specieslist',
'speciessynonyms',
'reference',
'references',
'timeunits',
+6 -6
View File
@@ -7,18 +7,19 @@ const speciesApi = api.injectEndpoints({
query: () => ({
url: `/species/all`,
}),
providesTags: result => (result ? [{ type: 'specieslist' }] : []),
providesTags: result => (result ? [{ type: 'specieslist' }, 'speciessynonyms'] : []),
}),
getAllSynonyms: builder.query<SpeciesSynonym[], void>({
query: () => ({
url: `/species/synonyms`,
}),
providesTags: result => (result ? ['speciessynonyms'] : []),
}),
getSpeciesDetails: builder.query<SpeciesDetailsType, string>({
query: id => ({
url: `/species/${id}`,
}),
providesTags: result => (result ? [{ type: 'species', id: result.species_id }] : []),
providesTags: result => (result ? [{ type: 'species', id: result.species_id }, 'speciessynonyms'] : []),
}),
editSpecies: builder.mutation<SpeciesDetailsType, EditDataType<SpeciesDetailsType>>({
query: species => ({
@@ -27,7 +28,7 @@ const speciesApi = api.injectEndpoints({
body: { species },
}),
invalidatesTags: (result, _error, { species_id }) =>
result ? [{ type: 'species', id: species_id }, 'specieslist'] : [],
result ? [{ type: 'species', id: species_id }, 'specieslist', 'speciessynonyms'] : [],
}),
deleteSpecies: builder.mutation<void, number>({
query: id => ({
@@ -36,15 +37,14 @@ const speciesApi = api.injectEndpoints({
}),
invalidatesTags: (result, _error, species_id) =>
typeof result !== 'undefined' ? [{ type: 'species', id: species_id }, 'specieslist'] : [],
typeof result !== 'undefined' ? [{ type: 'species', id: species_id }, 'specieslist', 'speciessynonyms'] : [],
}),
}),
})
export const {
useGetAllSpeciesQuery,
useLazyGetAllSpeciesQuery,
useLazyGetAllSynonymsQuery,
useGetAllSynonymsQuery,
useGetSpeciesDetailsQuery,
useEditSpeciesMutation,
useDeleteSpeciesMutation,
+31
View File
@@ -0,0 +1,31 @@
import { EditDataType, SpeciesSynonym } from '../types'
import { Validators, validator } from './validator'
export const validateSynonym = (
editData: EditDataType<SpeciesSynonym>,
fieldName: keyof EditDataType<SpeciesSynonym>
) => {
const validators: Validators<Partial<EditDataType<SpeciesSynonym>>> = {
syn_genus_name: {
name: 'Synonym genus name',
required: true,
asString: (synGenusName: string) => {
if (synGenusName.indexOf(' ') !== -1) return 'Genus must not contain any spaces.'
return
},
},
syn_species_name: {
name: 'Synonym species name',
required: true,
asString: (synSpeciesName: string) => {
if (synSpeciesName !== 'indet.' && editData.syn_genus_name === 'indet.')
return 'when the Genus is indet., Species must also be indet.'
if (synSpeciesName !== 'sp.' && editData.syn_genus_name === 'gen.')
return 'when the Genus is gen., Species must be sp.'
return
},
},
}
return validator<EditDataType<SpeciesSynonym>>(validators, editData, fieldName)
}
+20 -2
View File
@@ -146,7 +146,7 @@ const checkSubFamilyGenus = (newSpecies: EditDataType<Species>, existingSpecies:
)
}
export const checkTaxonomy = (
export const checkSpeciesTaxonomy = (
newSpecies: EditDataType<Species>,
existingSpeciesArray: Species[],
synonyms: SpeciesSynonym[]
@@ -327,7 +327,7 @@ const convertTaxonomyField = (value: string | null | undefined) => {
return convertedValue
}
export const convertTaxonomyFields = (species: EditDataType<Species>) => {
export const convertSpeciesTaxonomyFields = (species: EditDataType<Species>) => {
const speciesName = species.species_name
let lowercasedSpeciesName = ''
if (speciesName) {
@@ -358,3 +358,21 @@ export const fixNullValuesInTaxonomyFields = (species: Species | SpeciesDetailsT
subfamily_name: species.subfamily_name ?? '',
}
}
export const convertSynonymTaxonomyFields = (synonym: EditDataType<SpeciesSynonym>) => {
const synSpeciesName = synonym.syn_species_name
let lowercasedSynSpeciesName = ''
if (synSpeciesName) {
if (synSpeciesName !== 'indet.') {
lowercasedSynSpeciesName = synSpeciesName.toLowerCase()
} else {
lowercasedSynSpeciesName = synSpeciesName
}
}
const convertedSynonym = {
...synonym,
syn_genus_name: convertTaxonomyField(synonym.syn_genus_name),
syn_species_name: lowercasedSynSpeciesName,
}
return convertedSynonym as EditDataType<SpeciesSynonym>
}