From 50ac8f55d2e75a1afa030974dee15353fc4035b8 Mon Sep 17 00:00:00 2001 From: juhanikat Date: Sun, 3 Aug 2025 14:25:48 +0300 Subject: [PATCH 01/32] add validation for species added inside locality edit view --- backend/src/services/locality.ts | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/backend/src/services/locality.ts b/backend/src/services/locality.ts index 25878923..0e1288ad 100644 --- a/backend/src/services/locality.ts +++ b/backend/src/services/locality.ts @@ -1,5 +1,15 @@ -import { EditDataType, EditMetaData, LocalityDetailsType, User, Role } from '../../../frontend/src/shared/types' +import { + EditDataType, + EditMetaData, + LocalityDetailsType, + User, + Role, + LocalitySpeciesDetailsType, + Editable, + SpeciesDetailsType, +} from '../../../frontend/src/shared/types' import { validateLocality } from '../../../frontend/src/shared/validators/locality' +import { validateSpecies } from '../../../frontend/src/shared/validators/species' import { ValidationObject, referenceValidator } from '../../../frontend/src/shared/validators/validator' import Prisma from '../../prisma/generated/now_test_client' import { AccessError } from '../middlewares/authorizer' @@ -200,6 +210,7 @@ export const getLocalityDetails = async (id: number, user: User | undefined) => return JSON.parse(fixBigInt(result)!) as LocalityDetailsType } +// also validates possible new species that were added to this locality export const validateEntireLocality = async (editedFields: EditDataType & EditMetaData) => { const keys = Object.keys(editedFields) const errors: ValidationObject[] = [] @@ -207,6 +218,17 @@ export const validateEntireLocality = async (editedFields: EditDataType, key as keyof LocalityDetailsType) if (error.error) errors.push(error) } + if ('now_ls' in editedFields) { + for (const species of editedFields.now_ls as Array>) { + if (species.rowState === 'new') { + const keys = Object.keys(species.com_species) + for (const key of keys) { + const newSpeciesError = validateSpecies(species.com_species, key as keyof SpeciesDetailsType) + if (newSpeciesError.error) errors.push(newSpeciesError) + } + } + } + } let error = null if ('references' in editedFields && editedFields.references) { error = referenceValidator(editedFields.references) From 8153dcaaf019939997bf97e877c7e18586376833 Mon Sep 17 00:00:00 2001 From: juhanikat Date: Sun, 3 Aug 2025 14:40:03 +0300 Subject: [PATCH 02/32] filterDuplicateLocalitySpecies no longer filters out species that do not have a species_id --- backend/src/services/locality.ts | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/backend/src/services/locality.ts b/backend/src/services/locality.ts index 0e1288ad..2b987df4 100644 --- a/backend/src/services/locality.ts +++ b/backend/src/services/locality.ts @@ -273,12 +273,10 @@ export const filterDuplicateLocalitySpecies = async ( // Logic is flipped for filter as we want to skip the ones the check matches return !(localityDetailsSpeciesIds.includes(localitySpecies.species_id) && localitySpecies.rowState === 'new') } - - // code shouldn't get here ever - logger.info( - `Filter already existing species: localitySpecies didn't have species_id. Skipping. ${JSON.stringify(localitySpecies)}` - ) - return false + // if localitySpecies doesn't have species_id (i.e. it was created with the "add new species" button in Species tab), + // it will never be filtered out. However, once the taxonomy check for the "add new species" button is working, + // it should not let the user create a duplicate species in this way. + return true }) return updatedLocalityNow_ls From 8cc818ea1d9a2088244dc20f87501601ce39874f Mon Sep 17 00:00:00 2001 From: juhanikat Date: Mon, 25 Aug 2025 14:02:32 +0300 Subject: [PATCH 03/32] add taxonomy checks for locality -> species tab --- .../src/components/Locality/Tabs/SpeciesTab.tsx | 17 +++++++++++++++-- frontend/src/util/taxonomyFunctions.tsx | 6 +++++- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/frontend/src/components/Locality/Tabs/SpeciesTab.tsx b/frontend/src/components/Locality/Tabs/SpeciesTab.tsx index 6b268803..1ca78fbc 100755 --- a/frontend/src/components/Locality/Tabs/SpeciesTab.tsx +++ b/frontend/src/components/Locality/Tabs/SpeciesTab.tsx @@ -1,17 +1,20 @@ -import { LocalityDetailsType, LocalitySpecies, Species, SpeciesDetailsType } from '@/shared/types' +import { Editable, LocalityDetailsType, LocalitySpecies, Species, SpeciesDetailsType } from '@/shared/types' import { EditableTable } from '@/components/DetailView/common/EditableTable' import { EditingForm } from '@/components/DetailView/common/EditingForm' import { SelectingTable } from '@/components/DetailView/common/SelectingTable' import { Grouped } from '@/components/DetailView/common/tabLayoutHelpers' import { useDetailContext } from '@/components/DetailView/Context/DetailContext' import { useGetAllSpeciesQuery } from '@/redux/speciesReducer' -import { Box } from '@mui/material' +import { Box, CircularProgress } from '@mui/material' import { skipToken } from '@reduxjs/toolkit/query' import { MRT_ColumnDef } from 'material-react-table' +import { checkTaxonomy } from '@/util/taxonomyFunctions' +import { useNotify } from '@/hooks/notification' export const SpeciesTab = () => { const { mode, editData, setEditData } = useDetailContext() const { data: speciesData, isError } = useGetAllSpeciesQuery(mode.read ? skipToken : undefined) + const { notify } = useNotify() const speciesColumns: MRT_ColumnDef[] = [ { @@ -92,6 +95,8 @@ export const SpeciesTab = () => { { name: 'taxonomic_status', label: 'Taxon status' }, ] + if (!mode.read && !speciesData) return + return ( {!mode.read && ( @@ -100,6 +105,14 @@ export const SpeciesTab = () => { buttonText="Add new Species" formFields={formFields} editAction={(newSpecies: Species) => { + const allSpecies = editData.now_ls.map(ls => ls.com_species) as unknown as Editable[] + const filteredSpecies = allSpecies.filter(species => species.rowState === 'new') + const taxonomyErrors = checkTaxonomy(newSpecies, speciesData!.concat(filteredSpecies), []) + if (taxonomyErrors.size > 0) { + const errorMessage = [...taxonomyErrors].reduce((acc, currentError) => acc + `\n${currentError}`) + notify(errorMessage, 'error', null) + return + } setEditData({ ...editData, now_ls: [ diff --git a/frontend/src/util/taxonomyFunctions.tsx b/frontend/src/util/taxonomyFunctions.tsx index 5bdc259f..4a64cc63 100644 --- a/frontend/src/util/taxonomyFunctions.tsx +++ b/frontend/src/util/taxonomyFunctions.tsx @@ -11,7 +11,10 @@ Please contact the NOW administration to fix this taxonomy.` const isDuplicateTaxon = (newSpecies: EditDataType, existingSpecies: Species) => { if ( - newSpecies.species_id !== existingSpecies.species_id && + // the check for undefined species_id is here to make sure you cannot add two new, + // identical species at the same time (e.g. in Locality -> Species -> Add new species) + ((newSpecies.species_id === undefined && existingSpecies.species_id === undefined) || + newSpecies.species_id !== existingSpecies.species_id) && newSpecies.genus_name === existingSpecies.genus_name && newSpecies.species_name === existingSpecies.species_name && newSpecies.unique_identifier === existingSpecies.unique_identifier @@ -156,6 +159,7 @@ export const checkTaxonomy = ( let speciesWithSameSubfamily = undefined for (const existingSpecies of existingSpeciesArray) { + if (existingSpecies.unique_identifier === newSpecies.unique_identifier) console.log('should throw error') if (isDuplicateTaxon(newSpecies, existingSpecies)) { errors.add('The taxon already exists in the database.') return errors From dcca0aefcd112857926f21b66a93b53b98a7e91e Mon Sep 17 00:00:00 2001 From: juhanikat Date: Mon, 25 Aug 2025 15:45:22 +0300 Subject: [PATCH 04/32] add some tests for locality -> species tab and fix issue with null subclass, suborder and subfamily when selecting species --- cypress/e2e/locality.cy.js | 67 +++++++++++++++++++ .../components/Locality/Tabs/SpeciesTab.tsx | 17 +++-- frontend/src/util/taxonomyFunctions.tsx | 1 - 3 files changed, 80 insertions(+), 5 deletions(-) diff --git a/cypress/e2e/locality.cy.js b/cypress/e2e/locality.cy.js index 3b25271b..a4a2dc01 100644 --- a/cypress/e2e/locality.cy.js +++ b/cypress/e2e/locality.cy.js @@ -1,3 +1,70 @@ +describe('Adding species in Locality -> Species tab', () => { + beforeEach('Login as admin', () => { + cy.login('testSu') + }) + + it('does not work if the species has already been added', () => { + cy.visit(`/locality/20920?tab=2`) + cy.contains('Lantian-Shuijiazui') + cy.get('[id=edit-button]').click() + cy.contains('Select Species').click() + cy.get('[data-cy=add-button-21052]').first().click() + cy.get('[data-cy=add-button-21426]').first().click() + cy.contains('Close').click() + cy.contains('Rodentia') + cy.contains('Eulipotyphla') + + // adding species that already exists in the database + cy.contains('Add new Species').click() + cy.get('[name=order_name]').type('Rodentia') + cy.get('[name=family_name]').type('Gliridae') + cy.get('[name=genus_name]').type('Simplomys') + cy.get('[name=species_name]').type('Simplicidens') + cy.get('[name=unique_identifier]').type('-') + cy.contains('Save').click() + cy.contains('The taxon already exists in the database.') + + // add new, unique species + cy.contains('Add new Species').click() + cy.get('[name=order_name]').clear() + cy.get('[name=order_name]').type('new order') + cy.get('[name=family_name]').clear() + cy.get('[name=family_name]').type('new family') + cy.get('[name=genus_name]').clear() + cy.get('[name=genus_name]').type('new genus') + cy.get('[name=species_name]').clear() + cy.get('[name=species_name]').type('new species') + cy.get('[name=unique_identifier]').clear() + cy.get('[name=unique_identifier]').type('-') + cy.contains('Save').click() + + cy.get('[id=write-button]').click() + cy.get('[id=write-button]').should('be.disabled') + cy.contains('button', 'Add existing reference').click() + cy.get('button[data-cy^="add-button"]').first().click() + cy.contains('button', 'Close').click() + cy.get('[id=write-button]').should('not.be.disabled') + cy.get('[id=write-button]').click() + cy.contains('Edited item successfully.') + cy.visit(`/locality/20920?tab=2`) + cy.contains('Lantian-Shuijiazui') + cy.get('[id=edit-button]').click() + cy.contains('Rodentia') + cy.contains('Eulipotyphla') + cy.contains('New order') + + // adding species that has been added earlier through "add new species" button + cy.contains('Add new Species').click() + cy.get('[name=order_name]').type('anything') + cy.get('[name=family_name]').type('anything') + cy.get('[name=genus_name]').type('new genus') + cy.get('[name=species_name]').type('new species') + cy.get('[name=unique_identifier]').type('-') + cy.contains('Save').click() + cy.contains('The taxon already exists in the database.') + }) +}) + describe('Creating a new locality', () => { beforeEach('Login as admin', () => { cy.login('testSu') diff --git a/frontend/src/components/Locality/Tabs/SpeciesTab.tsx b/frontend/src/components/Locality/Tabs/SpeciesTab.tsx index 1ca78fbc..a1bda3b1 100755 --- a/frontend/src/components/Locality/Tabs/SpeciesTab.tsx +++ b/frontend/src/components/Locality/Tabs/SpeciesTab.tsx @@ -8,7 +8,7 @@ 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 } from '@/util/taxonomyFunctions' +import { checkTaxonomy, convertTaxonomyFields } from '@/util/taxonomyFunctions' import { useNotify } from '@/hooks/notification' export const SpeciesTab = () => { @@ -105,9 +105,11 @@ export const SpeciesTab = () => { buttonText="Add new Species" formFields={formFields} editAction={(newSpecies: Species) => { + const convertedSpecies = convertTaxonomyFields(newSpecies) const allSpecies = editData.now_ls.map(ls => ls.com_species) as unknown as Editable[] const filteredSpecies = allSpecies.filter(species => species.rowState === 'new') - const taxonomyErrors = checkTaxonomy(newSpecies, speciesData!.concat(filteredSpecies), []) + + const taxonomyErrors = checkTaxonomy(convertedSpecies, speciesData!.concat(filteredSpecies), []) if (taxonomyErrors.size > 0) { const errorMessage = [...taxonomyErrors].reduce((acc, currentError) => acc + `\n${currentError}`) notify(errorMessage, 'error', null) @@ -120,7 +122,7 @@ export const SpeciesTab = () => { { lid: editData.lid, species_id: newSpecies.species_id, - com_species: { ...(newSpecies as unknown as SpeciesDetailsType) }, + com_species: { ...(convertedSpecies as unknown as SpeciesDetailsType) }, rowState: 'new', }, ], @@ -142,7 +144,14 @@ export const SpeciesTab = () => { { lid: editData.lid, species_id: newSpecies.species_id, - com_species: { ...(newSpecies as unknown as SpeciesDetailsType) }, + com_species: { + ...(newSpecies as unknown as SpeciesDetailsType), + // changes subclass, suborder, and subfamily from null to empty string to make validators work + // this should be removed if validators are improved in the future + subclass_or_superorder_name: newSpecies.subclass_or_superorder_name ?? '', + suborder_or_superfamily_name: newSpecies.suborder_or_superfamily_name ?? '', + subfamily_name: newSpecies.subfamily_name ?? '', + }, rowState: 'new', }, ], diff --git a/frontend/src/util/taxonomyFunctions.tsx b/frontend/src/util/taxonomyFunctions.tsx index 4a64cc63..202dd09b 100644 --- a/frontend/src/util/taxonomyFunctions.tsx +++ b/frontend/src/util/taxonomyFunctions.tsx @@ -159,7 +159,6 @@ export const checkTaxonomy = ( let speciesWithSameSubfamily = undefined for (const existingSpecies of existingSpeciesArray) { - if (existingSpecies.unique_identifier === newSpecies.unique_identifier) console.log('should throw error') if (isDuplicateTaxon(newSpecies, existingSpecies)) { errors.add('The taxon already exists in the database.') return errors From 6a0b1a288f3c05b9b1c06eade35a79ccfa1f41c9 Mon Sep 17 00:00:00 2001 From: juhanikat Date: Mon, 25 Aug 2025 16:33:03 +0300 Subject: [PATCH 05/32] validate species immediately when adding it in locality -> species tab, and refactor null value fix --- .../components/Locality/Tabs/SpeciesTab.tsx | 33 ++++++++++++++----- .../src/components/Species/SpeciesDetails.tsx | 9 ++--- frontend/src/util/taxonomyFunctions.tsx | 11 ++++++- 3 files changed, 38 insertions(+), 15 deletions(-) diff --git a/frontend/src/components/Locality/Tabs/SpeciesTab.tsx b/frontend/src/components/Locality/Tabs/SpeciesTab.tsx index a1bda3b1..8aa28e08 100755 --- a/frontend/src/components/Locality/Tabs/SpeciesTab.tsx +++ b/frontend/src/components/Locality/Tabs/SpeciesTab.tsx @@ -1,4 +1,11 @@ -import { Editable, LocalityDetailsType, LocalitySpecies, Species, SpeciesDetailsType } from '@/shared/types' +import { + Editable, + EditDataType, + LocalityDetailsType, + LocalitySpecies, + Species, + SpeciesDetailsType, +} from '@/shared/types' import { EditableTable } from '@/components/DetailView/common/EditableTable' import { EditingForm } from '@/components/DetailView/common/EditingForm' import { SelectingTable } from '@/components/DetailView/common/SelectingTable' @@ -8,8 +15,9 @@ 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 } from '@/util/taxonomyFunctions' +import { checkTaxonomy, convertTaxonomyFields, fixNullValuesInTaxonomyFields } from '@/util/taxonomyFunctions' import { useNotify } from '@/hooks/notification' +import { validateSpecies } from '@/shared/validators/species' export const SpeciesTab = () => { const { mode, editData, setEditData } = useDetailContext() @@ -105,6 +113,20 @@ export const SpeciesTab = () => { buttonText="Add new Species" formFields={formFields} editAction={(newSpecies: Species) => { + const errors = [] + for (const field in newSpecies) { + const errorObject = validateSpecies( + newSpecies as unknown as EditDataType, + field as unknown as keyof EditDataType + ) + 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 + } + const convertedSpecies = convertTaxonomyFields(newSpecies) const allSpecies = editData.now_ls.map(ls => ls.com_species) as unknown as Editable[] const filteredSpecies = allSpecies.filter(species => species.rowState === 'new') @@ -145,12 +167,7 @@ export const SpeciesTab = () => { lid: editData.lid, species_id: newSpecies.species_id, com_species: { - ...(newSpecies as unknown as SpeciesDetailsType), - // changes subclass, suborder, and subfamily from null to empty string to make validators work - // this should be removed if validators are improved in the future - subclass_or_superorder_name: newSpecies.subclass_or_superorder_name ?? '', - suborder_or_superfamily_name: newSpecies.suborder_or_superfamily_name ?? '', - subfamily_name: newSpecies.subfamily_name ?? '', + ...(fixNullValuesInTaxonomyFields(newSpecies) as SpeciesDetailsType), }, rowState: 'new', }, diff --git a/frontend/src/components/Species/SpeciesDetails.tsx b/frontend/src/components/Species/SpeciesDetails.tsx index c2ed6f33..d9106b0c 100755 --- a/frontend/src/components/Species/SpeciesDetails.tsx +++ b/frontend/src/components/Species/SpeciesDetails.tsx @@ -16,6 +16,7 @@ import { validateSpecies } from '@/shared/validators/species' import { emptySpecies } from '../DetailView/common/defaultValues' import { useNotify } from '@/hooks/notification' import { useEffect, useState } from 'react' +import { fixNullValuesInTaxonomyFields } from '@/util/taxonomyFunctions' export const SpeciesDetails = () => { const { id } = useParams() @@ -43,12 +44,8 @@ export const SpeciesDetails = () => { // changes subclass, suborder, and subfamily from null to empty string to make validators work // this should be removed if validators are improved in the future if (data) { - setModifiedData({ - ...data, - subclass_or_superorder_name: data.subclass_or_superorder_name ?? '', - suborder_or_superfamily_name: data.suborder_or_superfamily_name ?? '', - subfamily_name: data.subfamily_name ?? '', - }) + const fixedSpeciesData = fixNullValuesInTaxonomyFields(data) + setModifiedData(fixedSpeciesData as SpeciesDetailsType) } }, [data]) diff --git a/frontend/src/util/taxonomyFunctions.tsx b/frontend/src/util/taxonomyFunctions.tsx index 202dd09b..9f6d92f6 100644 --- a/frontend/src/util/taxonomyFunctions.tsx +++ b/frontend/src/util/taxonomyFunctions.tsx @@ -1,4 +1,4 @@ -import { EditDataType, Species, SpeciesSynonym } from '@/shared/types' +import { EditDataType, Species, SpeciesDetailsType, SpeciesSynonym } from '@/shared/types' const generateMultipleParentsError = ( invalidField: string, @@ -339,3 +339,12 @@ export const convertTaxonomyFields = (species: EditDataType) => { } return convertedSpecies as EditDataType } + +export const fixNullValuesInTaxonomyFields = (species: EditDataType | SpeciesDetailsType) => { + return { + ...species, + subclass_or_superorder_name: species.subclass_or_superorder_name ?? '', + suborder_or_superfamily_name: species.suborder_or_superfamily_name ?? '', + subfamily_name: species.subfamily_name ?? '', + } +} From 7e77f4dd15c3c4e19964c1e467890ed4dffb3bcf Mon Sep 17 00:00:00 2001 From: juhanikat Date: Mon, 25 Aug 2025 16:42:24 +0300 Subject: [PATCH 06/32] add skeletons for future tests --- cypress/e2e/locality.cy.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/cypress/e2e/locality.cy.js b/cypress/e2e/locality.cy.js index a4a2dc01..7d87a20e 100644 --- a/cypress/e2e/locality.cy.js +++ b/cypress/e2e/locality.cy.js @@ -3,6 +3,9 @@ describe('Adding species in Locality -> Species tab', () => { cy.login('testSu') }) + it.skip('works with valid, unique species') + it.skip('does not work with invalid species') + it('does not work if the species has already been added', () => { cy.visit(`/locality/20920?tab=2`) cy.contains('Lantian-Shuijiazui') @@ -63,6 +66,8 @@ describe('Adding species in Locality -> Species tab', () => { cy.contains('Save').click() cy.contains('The taxon already exists in the database.') }) + + it.skip('does not work with species with invalid taxonomic order') }) describe('Creating a new locality', () => { From df85100d4a946157507be513efc739dcb4c1603a Mon Sep 17 00:00:00 2001 From: juhanikat Date: Mon, 25 Aug 2025 16:47:11 +0300 Subject: [PATCH 07/32] fix test and add before-section to locality tests --- cypress/e2e/locality.cy.js | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/cypress/e2e/locality.cy.js b/cypress/e2e/locality.cy.js index 7d87a20e..0ba59ae5 100644 --- a/cypress/e2e/locality.cy.js +++ b/cypress/e2e/locality.cy.js @@ -1,3 +1,7 @@ +before('Reset database', () => { + cy.request(Cypress.env('databaseResetUrl')) +}) + describe('Adding species in Locality -> Species tab', () => { beforeEach('Login as admin', () => { cy.login('testSu') @@ -30,13 +34,13 @@ describe('Adding species in Locality -> Species tab', () => { // add new, unique species cy.contains('Add new Species').click() cy.get('[name=order_name]').clear() - cy.get('[name=order_name]').type('new order') + cy.get('[name=order_name]').type('neworder') cy.get('[name=family_name]').clear() - cy.get('[name=family_name]').type('new family') + cy.get('[name=family_name]').type('newfamily') cy.get('[name=genus_name]').clear() - cy.get('[name=genus_name]').type('new genus') + cy.get('[name=genus_name]').type('newgenus') cy.get('[name=species_name]').clear() - cy.get('[name=species_name]').type('new species') + cy.get('[name=species_name]').type('newspecies') cy.get('[name=unique_identifier]').clear() cy.get('[name=unique_identifier]').type('-') cy.contains('Save').click() @@ -54,14 +58,14 @@ describe('Adding species in Locality -> Species tab', () => { cy.get('[id=edit-button]').click() cy.contains('Rodentia') cy.contains('Eulipotyphla') - cy.contains('New order') + cy.contains('Neworder') // adding species that has been added earlier through "add new species" button cy.contains('Add new Species').click() cy.get('[name=order_name]').type('anything') cy.get('[name=family_name]').type('anything') - cy.get('[name=genus_name]').type('new genus') - cy.get('[name=species_name]').type('new species') + cy.get('[name=genus_name]').type('newgenus') + cy.get('[name=species_name]').type('newspecies') cy.get('[name=unique_identifier]').type('-') cy.contains('Save').click() cy.contains('The taxon already exists in the database.') From 2175a177abd52ed71d2640e7dc3dfa724c2b7885 Mon Sep 17 00:00:00 2001 From: juhanikat Date: Wed, 27 Aug 2025 13:08:10 +0300 Subject: [PATCH 08/32] invalidate specieslist in locality editing redux to fix bug with locality -> species tab --- frontend/src/components/Locality/Tabs/SpeciesTab.tsx | 6 +++--- frontend/src/redux/localityReducer.ts | 3 ++- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/frontend/src/components/Locality/Tabs/SpeciesTab.tsx b/frontend/src/components/Locality/Tabs/SpeciesTab.tsx index 8aa28e08..9a9cdcbc 100755 --- a/frontend/src/components/Locality/Tabs/SpeciesTab.tsx +++ b/frontend/src/components/Locality/Tabs/SpeciesTab.tsx @@ -128,10 +128,10 @@ export const SpeciesTab = () => { } const convertedSpecies = convertTaxonomyFields(newSpecies) - const allSpecies = editData.now_ls.map(ls => ls.com_species) as unknown as Editable[] - const filteredSpecies = allSpecies.filter(species => species.rowState === 'new') + const everyLs = editData.now_ls.map(ls => ls.com_species) as unknown as Editable[] + const filteredLs = everyLs.filter(species => species.rowState === 'new') - const taxonomyErrors = checkTaxonomy(convertedSpecies, speciesData!.concat(filteredSpecies), []) + const taxonomyErrors = checkTaxonomy(convertedSpecies, speciesData!.concat(filteredLs), []) if (taxonomyErrors.size > 0) { const errorMessage = [...taxonomyErrors].reduce((acc, currentError) => acc + `\n${currentError}`) notify(errorMessage, 'error', null) diff --git a/frontend/src/redux/localityReducer.ts b/frontend/src/redux/localityReducer.ts index 1db32eec..3b3b2d72 100755 --- a/frontend/src/redux/localityReducer.ts +++ b/frontend/src/redux/localityReducer.ts @@ -28,7 +28,8 @@ const localitiesApi = api.injectEndpoints({ method: 'PUT', body: { locality }, }), - invalidatesTags: (result, _error, { lid }) => (result ? [{ type: 'locality', id: lid }, 'localities'] : []), + invalidatesTags: (result, _error, { lid }) => + result ? [{ type: 'locality', id: lid }, 'localities', 'specieslist'] : [], }), deleteLocality: builder.mutation({ query: id => ({ From 4b07fe0bcfe3532bc3b97735c91664a7b6ca5131 Mon Sep 17 00:00:00 2001 From: juhanikat Date: Wed, 27 Aug 2025 14:04:50 +0300 Subject: [PATCH 09/32] refactor add new species button's editAction --- .../components/Locality/Tabs/SpeciesTab.tsx | 54 ++++++++++--------- 1 file changed, 30 insertions(+), 24 deletions(-) diff --git a/frontend/src/components/Locality/Tabs/SpeciesTab.tsx b/frontend/src/components/Locality/Tabs/SpeciesTab.tsx index 9a9cdcbc..c5de4344 100755 --- a/frontend/src/components/Locality/Tabs/SpeciesTab.tsx +++ b/frontend/src/components/Locality/Tabs/SpeciesTab.tsx @@ -24,6 +24,34 @@ export const SpeciesTab = () => { const { data: speciesData, isError } = useGetAllSpeciesQuery(mode.read ? skipToken : undefined) const { notify } = useNotify() + const convertAndCheckNewSpeciesTaxonomy = (newSpecies: Species) => { + const errors = [] + for (const field in newSpecies) { + const errorObject = validateSpecies( + newSpecies as unknown as EditDataType, + field as unknown as keyof EditDataType + ) + 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 convertedSpecies = convertTaxonomyFields(newSpecies) + const everyLs = editData.now_ls.map(ls => ls.com_species) as unknown as Editable[] + const filteredLs = everyLs.filter(species => species.rowState === 'new') + + const taxonomyErrors = checkTaxonomy(convertedSpecies, speciesData!.concat(filteredLs), []) + if (taxonomyErrors.size > 0) { + const errorMessage = [...taxonomyErrors].reduce((acc, currentError) => acc + `\n${currentError}`) + notify(errorMessage, 'error', null) + return false + } + return convertedSpecies + } + const speciesColumns: MRT_ColumnDef[] = [ { accessorKey: 'order_name', @@ -113,30 +141,8 @@ export const SpeciesTab = () => { buttonText="Add new Species" formFields={formFields} editAction={(newSpecies: Species) => { - const errors = [] - for (const field in newSpecies) { - const errorObject = validateSpecies( - newSpecies as unknown as EditDataType, - field as unknown as keyof EditDataType - ) - 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 - } - - const convertedSpecies = convertTaxonomyFields(newSpecies) - const everyLs = editData.now_ls.map(ls => ls.com_species) as unknown as Editable[] - const filteredLs = everyLs.filter(species => species.rowState === 'new') - - const taxonomyErrors = checkTaxonomy(convertedSpecies, speciesData!.concat(filteredLs), []) - if (taxonomyErrors.size > 0) { - const errorMessage = [...taxonomyErrors].reduce((acc, currentError) => acc + `\n${currentError}`) - notify(errorMessage, 'error', null) - return - } + const convertedSpecies = convertAndCheckNewSpeciesTaxonomy(newSpecies) + if (!convertedSpecies) return setEditData({ ...editData, now_ls: [ From 7e6c62aff0c308d2076ffba29187eda543eab7e6 Mon Sep 17 00:00:00 2001 From: juhanikat Date: Wed, 27 Aug 2025 14:22:07 +0300 Subject: [PATCH 10/32] refactor taxonStatusOptions, add comment and author fields to editingForm inside locality -> species tab, and update Species type --- frontend/src/components/Locality/Tabs/SpeciesTab.tsx | 2 ++ frontend/src/components/Species/Tabs/TaxonomyTab.tsx | 12 +----------- frontend/src/shared/types/data.ts | 1 + frontend/src/util/taxonStatusOptions.ts | 11 +++++++++++ 4 files changed, 15 insertions(+), 11 deletions(-) create mode 100644 frontend/src/util/taxonStatusOptions.ts diff --git a/frontend/src/components/Locality/Tabs/SpeciesTab.tsx b/frontend/src/components/Locality/Tabs/SpeciesTab.tsx index c5de4344..61e6e238 100755 --- a/frontend/src/components/Locality/Tabs/SpeciesTab.tsx +++ b/frontend/src/components/Locality/Tabs/SpeciesTab.tsx @@ -129,6 +129,8 @@ export const SpeciesTab = () => { { name: 'suborder_or_superfamily_name', label: 'Suborder or Superfamily' }, { name: 'unique_identifier', label: 'Unique Identifier', required: true }, { name: 'taxonomic_status', label: 'Taxon status' }, + { name: 'sp_comment', label: 'Comment' }, + { name: 'sp_author', label: 'Author' }, ] if (!mode.read && !speciesData) return diff --git a/frontend/src/components/Species/Tabs/TaxonomyTab.tsx b/frontend/src/components/Species/Tabs/TaxonomyTab.tsx index 2504cea6..8f327723 100755 --- a/frontend/src/components/Species/Tabs/TaxonomyTab.tsx +++ b/frontend/src/components/Species/Tabs/TaxonomyTab.tsx @@ -1,7 +1,7 @@ import { SpeciesDetailsType, Species } from '@/shared/types' import { ArrayFrame, HalfFrames } from '@/components/DetailView/common/tabLayoutHelpers' import { useDetailContext } from '@/components/DetailView/Context/DetailContext' -import { emptyOption } from '@/components/DetailView/common/misc' +import { taxonStatusOptions } from '@/util/taxonStatusOptions' import { Box } from '@mui/material' import { SelectingTable } from '@/components/DetailView/common/SelectingTable' import { useGetAllSpeciesQuery } from '@/redux/speciesReducer' @@ -49,16 +49,6 @@ export const TaxonomyTab = () => { ) - const taxonStatusOptions = [ - emptyOption, - 'family attrib of genus uncertain', - 'genus attrib of species uncertain', - 'informal species', - 'species validity uncertain', - 'taxonomic validity uncertain', - 'NOW synonym', - ] - const classification = [ [ 'Class', diff --git a/frontend/src/shared/types/data.ts b/frontend/src/shared/types/data.ts index 8b47bb03..7e8bdce3 100755 --- a/frontend/src/shared/types/data.ts +++ b/frontend/src/shared/types/data.ts @@ -332,6 +332,7 @@ export type Species = { locomo2: string | null locomo3: string | null sp_comment: string | null + sp_author: string | null } export type RegionDetails = Prisma.now_reg_coord & { now_reg_coord_people: Array } & { diff --git a/frontend/src/util/taxonStatusOptions.ts b/frontend/src/util/taxonStatusOptions.ts new file mode 100644 index 00000000..6073943a --- /dev/null +++ b/frontend/src/util/taxonStatusOptions.ts @@ -0,0 +1,11 @@ +import { emptyOption } from '@/components/DetailView/common/misc' + +export const taxonStatusOptions = [ + emptyOption, + 'family attrib of genus uncertain', + 'genus attrib of species uncertain', + 'informal species', + 'species validity uncertain', + 'taxonomic validity uncertain', + 'NOW synonym', +] From 6c2c951a5cbf4336eeae3c644753247ff79daa5e Mon Sep 17 00:00:00 2001 From: juhanikat Date: Wed, 27 Aug 2025 15:52:49 +0300 Subject: [PATCH 11/32] rename taxonomyFunctions to taxonomyUtilities and add copy taxonomy button to the "add new species" form --- .../DetailView/common/EditingForm.tsx | 18 +++++++++- .../DetailView/common/defaultValues.ts | 2 +- .../src/components/DetailView/components.tsx | 2 +- .../components/Locality/Tabs/SpeciesTab.tsx | 34 ++++++++++++++++++- .../src/components/Species/SpeciesDetails.tsx | 2 +- .../components/Species/Tabs/TaxonomyTab.tsx | 11 ++---- frontend/src/util/taxonStatusOptions.ts | 11 ------ ...omyFunctions.tsx => taxonomyUtilities.tsx} | 12 ++++++- 8 files changed, 66 insertions(+), 26 deletions(-) delete mode 100644 frontend/src/util/taxonStatusOptions.ts rename frontend/src/util/{taxonomyFunctions.tsx => taxonomyUtilities.tsx} (97%) diff --git a/frontend/src/components/DetailView/common/EditingForm.tsx b/frontend/src/components/DetailView/common/EditingForm.tsx index c0ba3d55..8a7030c4 100755 --- a/frontend/src/components/DetailView/common/EditingForm.tsx +++ b/frontend/src/components/DetailView/common/EditingForm.tsx @@ -3,6 +3,7 @@ import { EditingModal } from './EditingModal' import { useForm } from 'react-hook-form' import { Editable, EditDataType } from '@/shared/types' import { useDetailContext } from '../Context/DetailContext' +import { useEffect } from 'react' export type EditingFormField = { name: string; label: string; required?: boolean; type?: 'number' | 'string' } @@ -11,6 +12,10 @@ Renders a button, that will open EditingModal, which can be used to add a new entry to a list, or edit existing one. If using for adding new, provide arrayFieldName. For editing existing row, use existingObject and editAction. + +If you want to autofill the form's fields once the user e.g. clicks a button, +pass the updated values into the replacedValues prop. The form will be reset and uses those values. +The copyTaxonomyButton prop is used only in Locality -> Species tab. */ export const EditingForm = ({ dataCy, @@ -19,6 +24,8 @@ export const EditingForm = ({ editAction, existingObject, arrayFieldName, + replacedValues, + copyTaxonomyButton, }: { dataCy?: string buttonText: string @@ -26,15 +33,22 @@ export const EditingForm = ({ arrayFieldName?: keyof ParentType editAction?: (newObject: T) => void existingObject?: T | undefined + replacedValues?: T | undefined + copyTaxonomyButton?: JSX.Element | undefined }) => { const getDefaultValues: () => { [x: string]: unknown } = () => { if (!existingObject) return {} return existingObject } - const { register, trigger, formState, getValues } = useForm({ defaultValues: getDefaultValues() }) + const { register, trigger, formState, getValues, setValue, reset } = useForm({ defaultValues: getDefaultValues() }) const { errors } = formState const { editData, setEditData } = useDetailContext() + useEffect(() => { + if (replacedValues) reset(replacedValues) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [replacedValues]) + const onSave = async () => { const result = await trigger() if (!result) return false @@ -52,9 +66,11 @@ export const EditingForm = ({ return ( + {copyTaxonomyButton} {formFields.map(field => ( { const { mode, editData, setEditData } = useDetailContext() const { data: speciesData, isError } = useGetAllSpeciesQuery(mode.read ? skipToken : undefined) const { notify } = useNotify() + const [replacedValues, setReplacedValues] = useState() + const [selectedSpecies, setSelectedSpecies] = useState() + const [modalOpen, setModalOpen] = useState(false) + + const handleRowActionClick = (row: Species) => { + setSelectedSpecies(row.species_id.toString()) + setModalOpen(true) + } + + const copyTaxonomyButton = ( + + + buttonText="Copy existing taxonomy" + data={speciesData} + isError={isError} + columns={smallSpeciesTableColumns} + fieldName="order_name" // this doesn't do anything here but is required + idFieldName="species_id" + useObject={true} + tableRowAction={handleRowActionClick} + editingAction={(selectedSpecies: Species) => { + setReplacedValues(fixNullValuesInTaxonomyFields(selectedSpecies) as Species) + }} + /> + setModalOpen(false)} selectedSpecies={selectedSpecies} /> + + ) const convertAndCheckNewSpeciesTaxonomy = (newSpecies: Species) => { const errors = [] @@ -142,6 +172,8 @@ export const SpeciesTab = () => { buttonText="Add new Species" formFields={formFields} + replacedValues={replacedValues} + copyTaxonomyButton={copyTaxonomyButton} editAction={(newSpecies: Species) => { const convertedSpecies = convertAndCheckNewSpeciesTaxonomy(newSpecies) if (!convertedSpecies) return diff --git a/frontend/src/components/Species/SpeciesDetails.tsx b/frontend/src/components/Species/SpeciesDetails.tsx index d9106b0c..db18cf01 100755 --- a/frontend/src/components/Species/SpeciesDetails.tsx +++ b/frontend/src/components/Species/SpeciesDetails.tsx @@ -16,7 +16,7 @@ import { validateSpecies } from '@/shared/validators/species' import { emptySpecies } from '../DetailView/common/defaultValues' import { useNotify } from '@/hooks/notification' import { useEffect, useState } from 'react' -import { fixNullValuesInTaxonomyFields } from '@/util/taxonomyFunctions' +import { fixNullValuesInTaxonomyFields } from '@/util/taxonomyUtilities' export const SpeciesDetails = () => { const { id } = useParams() diff --git a/frontend/src/components/Species/Tabs/TaxonomyTab.tsx b/frontend/src/components/Species/Tabs/TaxonomyTab.tsx index 8f327723..142a6ac0 100755 --- a/frontend/src/components/Species/Tabs/TaxonomyTab.tsx +++ b/frontend/src/components/Species/Tabs/TaxonomyTab.tsx @@ -1,7 +1,7 @@ import { SpeciesDetailsType, Species } from '@/shared/types' import { ArrayFrame, HalfFrames } from '@/components/DetailView/common/tabLayoutHelpers' import { useDetailContext } from '@/components/DetailView/Context/DetailContext' -import { taxonStatusOptions } from '@/util/taxonStatusOptions' +import { fixNullValuesInTaxonomyFields, taxonStatusOptions } from '@/util/taxonomyUtilities' import { Box } from '@mui/material' import { SelectingTable } from '@/components/DetailView/common/SelectingTable' import { useGetAllSpeciesQuery } from '@/redux/speciesReducer' @@ -34,14 +34,7 @@ export const TaxonomyTab = () => { editingAction={(selectedSpecies: Species) => { setEditData({ ...editData, - subclass_or_superorder_name: selectedSpecies.subclass_or_superorder_name ?? '', - order_name: selectedSpecies.order_name ?? '', - suborder_or_superfamily_name: selectedSpecies.suborder_or_superfamily_name ?? '', - family_name: selectedSpecies.family_name ?? '', - subfamily_name: selectedSpecies.subfamily_name ?? '', - genus_name: selectedSpecies.genus_name ?? '', - species_name: selectedSpecies.species_name ?? '', - unique_identifier: selectedSpecies.unique_identifier ?? '', + ...(fixNullValuesInTaxonomyFields(selectedSpecies) as SpeciesDetailsType), }) }} /> diff --git a/frontend/src/util/taxonStatusOptions.ts b/frontend/src/util/taxonStatusOptions.ts deleted file mode 100644 index 6073943a..00000000 --- a/frontend/src/util/taxonStatusOptions.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { emptyOption } from '@/components/DetailView/common/misc' - -export const taxonStatusOptions = [ - emptyOption, - 'family attrib of genus uncertain', - 'genus attrib of species uncertain', - 'informal species', - 'species validity uncertain', - 'taxonomic validity uncertain', - 'NOW synonym', -] diff --git a/frontend/src/util/taxonomyFunctions.tsx b/frontend/src/util/taxonomyUtilities.tsx similarity index 97% rename from frontend/src/util/taxonomyFunctions.tsx rename to frontend/src/util/taxonomyUtilities.tsx index 9f6d92f6..256f8dea 100644 --- a/frontend/src/util/taxonomyFunctions.tsx +++ b/frontend/src/util/taxonomyUtilities.tsx @@ -1,5 +1,15 @@ import { EditDataType, Species, SpeciesDetailsType, SpeciesSynonym } from '@/shared/types' +import { emptyOption } from '@/components/DetailView/common/misc' +export const taxonStatusOptions = [ + emptyOption, + 'family attrib of genus uncertain', + 'genus attrib of species uncertain', + 'informal species', + 'species validity uncertain', + 'taxonomic validity uncertain', + 'NOW synonym', +] const generateMultipleParentsError = ( invalidField: string, invalidValue1: string | null, @@ -340,7 +350,7 @@ export const convertTaxonomyFields = (species: EditDataType) => { return convertedSpecies as EditDataType } -export const fixNullValuesInTaxonomyFields = (species: EditDataType | SpeciesDetailsType) => { +export const fixNullValuesInTaxonomyFields = (species: Species | SpeciesDetailsType) => { return { ...species, subclass_or_superorder_name: species.subclass_or_superorder_name ?? '', From 6f9800f9be12c2b153b414beb7a727899311eff5 Mon Sep 17 00:00:00 2001 From: juhanikat Date: Wed, 27 Aug 2025 16:19:45 +0300 Subject: [PATCH 12/32] fixed bug with setting values through species forms --- .../DetailView/common/EditingForm.tsx | 2 +- .../components/Locality/Tabs/SpeciesTab.tsx | 30 +++++++++++++++---- .../components/Species/Tabs/TaxonomyTab.tsx | 10 ++++++- 3 files changed, 34 insertions(+), 8 deletions(-) diff --git a/frontend/src/components/DetailView/common/EditingForm.tsx b/frontend/src/components/DetailView/common/EditingForm.tsx index 8a7030c4..46217c72 100755 --- a/frontend/src/components/DetailView/common/EditingForm.tsx +++ b/frontend/src/components/DetailView/common/EditingForm.tsx @@ -40,7 +40,7 @@ export const EditingForm = ({ if (!existingObject) return {} return existingObject } - const { register, trigger, formState, getValues, setValue, reset } = useForm({ defaultValues: getDefaultValues() }) + const { register, trigger, formState, getValues, reset } = useForm({ defaultValues: getDefaultValues() }) const { errors } = formState const { editData, setEditData } = useDetailContext() diff --git a/frontend/src/components/Locality/Tabs/SpeciesTab.tsx b/frontend/src/components/Locality/Tabs/SpeciesTab.tsx index acf9b924..629cf5d1 100755 --- a/frontend/src/components/Locality/Tabs/SpeciesTab.tsx +++ b/frontend/src/components/Locality/Tabs/SpeciesTab.tsx @@ -26,7 +26,7 @@ export const SpeciesTab = () => { const { mode, editData, setEditData } = useDetailContext() const { data: speciesData, isError } = useGetAllSpeciesQuery(mode.read ? skipToken : undefined) const { notify } = useNotify() - const [replacedValues, setReplacedValues] = useState() + const [replacedValues, setReplacedValues] = useState | undefined>() const [selectedSpecies, setSelectedSpecies] = useState() const [modalOpen, setModalOpen] = useState(false) @@ -47,14 +47,24 @@ export const SpeciesTab = () => { useObject={true} tableRowAction={handleRowActionClick} editingAction={(selectedSpecies: Species) => { - setReplacedValues(fixNullValuesInTaxonomyFields(selectedSpecies) as Species) + const fixedSpecies = fixNullValuesInTaxonomyFields(selectedSpecies) + setReplacedValues({ + subclass_or_superorder_name: fixedSpecies.subclass_or_superorder_name, + order_name: fixedSpecies.order_name!, + suborder_or_superfamily_name: fixedSpecies.suborder_or_superfamily_name, + family_name: fixedSpecies.family_name!, + subfamily_name: fixedSpecies.subfamily_name, + genus_name: fixedSpecies.genus_name!, + species_name: fixedSpecies.species_name!, + unique_identifier: fixedSpecies.unique_identifier!, + }) }} /> setModalOpen(false)} selectedSpecies={selectedSpecies} /> ) - const convertAndCheckNewSpeciesTaxonomy = (newSpecies: Species) => { + const convertAndCheckNewSpeciesTaxonomy = (newSpecies: EditDataType) => { const errors = [] for (const field in newSpecies) { const errorObject = validateSpecies( @@ -149,6 +159,14 @@ export const SpeciesTab = () => { accessorKey: 'com_species.taxonomic_status', header: 'Taxon status', }, + { + accessorKey: 'com_species.sp_comment', + header: 'Comment', + }, + { + accessorKey: 'com_species.sp_author', + header: 'Author', + }, ] const formFields: { name: string; label: string; required?: boolean }[] = [ { name: 'order_name', label: 'Order', required: true }, @@ -169,12 +187,12 @@ export const SpeciesTab = () => { {!mode.read && ( - + , LocalityDetailsType> buttonText="Add new Species" formFields={formFields} replacedValues={replacedValues} copyTaxonomyButton={copyTaxonomyButton} - editAction={(newSpecies: Species) => { + editAction={(newSpecies: EditDataType) => { const convertedSpecies = convertAndCheckNewSpeciesTaxonomy(newSpecies) if (!convertedSpecies) return setEditData({ @@ -183,7 +201,7 @@ export const SpeciesTab = () => { ...editData.now_ls, { lid: editData.lid, - species_id: newSpecies.species_id, + species_id: undefined, com_species: { ...(convertedSpecies as unknown as SpeciesDetailsType) }, rowState: 'new', }, diff --git a/frontend/src/components/Species/Tabs/TaxonomyTab.tsx b/frontend/src/components/Species/Tabs/TaxonomyTab.tsx index 142a6ac0..5468f2d7 100755 --- a/frontend/src/components/Species/Tabs/TaxonomyTab.tsx +++ b/frontend/src/components/Species/Tabs/TaxonomyTab.tsx @@ -32,9 +32,17 @@ export const TaxonomyTab = () => { useObject={true} tableRowAction={handleRowActionClick} editingAction={(selectedSpecies: Species) => { + const fixedSpecies = fixNullValuesInTaxonomyFields(selectedSpecies) setEditData({ ...editData, - ...(fixNullValuesInTaxonomyFields(selectedSpecies) as SpeciesDetailsType), + subclass_or_superorder_name: fixedSpecies.subclass_or_superorder_name, + order_name: fixedSpecies.order_name!, + suborder_or_superfamily_name: fixedSpecies.suborder_or_superfamily_name, + family_name: fixedSpecies.family_name!, + subfamily_name: fixedSpecies.subfamily_name, + genus_name: fixedSpecies.genus_name!, + species_name: fixedSpecies.species_name!, + unique_identifier: fixedSpecies.unique_identifier!, }) }} /> From 8c8a285ab73537ed49c1f20db70e3273884de2bf Mon Sep 17 00:00:00 2001 From: juhanikat Date: Wed, 27 Aug 2025 16:27:49 +0300 Subject: [PATCH 13/32] add more skipped tests --- cypress/e2e/locality.cy.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/cypress/e2e/locality.cy.js b/cypress/e2e/locality.cy.js index 0ba59ae5..41b7c715 100644 --- a/cypress/e2e/locality.cy.js +++ b/cypress/e2e/locality.cy.js @@ -8,6 +8,7 @@ describe('Adding species in Locality -> Species tab', () => { }) it.skip('works with valid, unique species') + it.skip('works through "copy species taxonomy" button') it.skip('does not work with invalid species') it('does not work if the species has already been added', () => { @@ -72,6 +73,7 @@ describe('Adding species in Locality -> Species tab', () => { }) it.skip('does not work with species with invalid taxonomic order') + it.skip('does not work through "copy species taxonomy" button if nothing is changed') }) describe('Creating a new locality', () => { From 47268e879dfbf26b46e35630d5731525d76b1e72 Mon Sep 17 00:00:00 2001 From: juhanikat Date: Wed, 27 Aug 2025 16:33:57 +0300 Subject: [PATCH 14/32] reset author, comment and taxon status when copying existing species, and add default value for unique identifier --- frontend/src/components/Locality/Tabs/SpeciesTab.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/frontend/src/components/Locality/Tabs/SpeciesTab.tsx b/frontend/src/components/Locality/Tabs/SpeciesTab.tsx index 629cf5d1..62abfbf0 100755 --- a/frontend/src/components/Locality/Tabs/SpeciesTab.tsx +++ b/frontend/src/components/Locality/Tabs/SpeciesTab.tsx @@ -57,6 +57,9 @@ export const SpeciesTab = () => { genus_name: fixedSpecies.genus_name!, species_name: fixedSpecies.species_name!, unique_identifier: fixedSpecies.unique_identifier!, + taxonomic_status: '', + sp_comment: '', + sp_author: '', }) }} /> @@ -190,6 +193,7 @@ export const SpeciesTab = () => { , LocalityDetailsType> buttonText="Add new Species" formFields={formFields} + existingObject={{ unique_identifier: '-' }} replacedValues={replacedValues} copyTaxonomyButton={copyTaxonomyButton} editAction={(newSpecies: EditDataType) => { From 390c69dd93fd38cec7d26d4648d127ab808f98cd Mon Sep 17 00:00:00 2001 From: juhanikat Date: Wed, 27 Aug 2025 16:42:16 +0300 Subject: [PATCH 15/32] change taxonomic_status from empty string to null when clicking copy button --- frontend/src/components/Locality/Tabs/SpeciesTab.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/components/Locality/Tabs/SpeciesTab.tsx b/frontend/src/components/Locality/Tabs/SpeciesTab.tsx index 62abfbf0..76daac3c 100755 --- a/frontend/src/components/Locality/Tabs/SpeciesTab.tsx +++ b/frontend/src/components/Locality/Tabs/SpeciesTab.tsx @@ -57,7 +57,7 @@ export const SpeciesTab = () => { genus_name: fixedSpecies.genus_name!, species_name: fixedSpecies.species_name!, unique_identifier: fixedSpecies.unique_identifier!, - taxonomic_status: '', + taxonomic_status: null, sp_comment: '', sp_author: '', }) From 32e94495a9c4cf39417a5c3bfdba2957fcb48c55 Mon Sep 17 00:00:00 2001 From: juhanikat Date: Thu, 28 Aug 2025 11:59:02 +0300 Subject: [PATCH 16/32] add command for adding reference and saving an entry, and add test for locality -> species tab --- cypress/e2e/locality.cy.js | 16 +++++++++++++++- cypress/support/commands.js | 11 +++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/cypress/e2e/locality.cy.js b/cypress/e2e/locality.cy.js index 41b7c715..d11f4656 100644 --- a/cypress/e2e/locality.cy.js +++ b/cypress/e2e/locality.cy.js @@ -7,7 +7,21 @@ describe('Adding species in Locality -> Species tab', () => { cy.login('testSu') }) - it.skip('works with valid, unique species') + it('works with valid, unique species', () => { + cy.visit(`/locality/20920?tab=2`) + cy.contains('Lantian-Shuijiazui') + cy.get('[id=edit-button]').click() + cy.contains('Add new Species').click() + cy.get('[name=order_name]').type('Rodentia') + cy.get('[name=family_name]').type('Gliridae') + cy.get('[name=genus_name]').type('Simplomys') + cy.get('[name=species_name]').type('someSpecies') + cy.get('[name=unique_identifier]').type('veryunique') + cy.contains('Save').click() + cy.contains('somespecies') + + cy.addReferenceAndSave() + }) it.skip('works through "copy species taxonomy" button') it.skip('does not work with invalid species') diff --git a/cypress/support/commands.js b/cypress/support/commands.js index 04316378..84e3c7f4 100644 --- a/cypress/support/commands.js +++ b/cypress/support/commands.js @@ -42,3 +42,14 @@ Cypress.Commands.add('pageForbidden', url => { Cypress.Commands.add('resetDatabase', () => { cy.task('resetDatabase') }) + +// use this once you have edited some data and want to save it +Cypress.Commands.add('addReferenceAndSave', () => { + cy.get('[id=write-button]').click() + cy.get('[id=write-button]').should('be.disabled') + cy.contains('button', 'Add existing reference').click() + cy.get('button[data-cy^="add-button"]').first().click() + cy.contains('button', 'Close').click() + cy.get('[id=write-button]').should('not.be.disabled') + cy.get('[id=write-button]').click() +}) From 805a33473be7457ccbb1b8629b79cc4ff1a1c8f7 Mon Sep 17 00:00:00 2001 From: juhanikat Date: Thu, 28 Aug 2025 12:21:26 +0300 Subject: [PATCH 17/32] update existing locality tests --- cypress/e2e/locality.cy.js | 48 +++++++++++-------------------------- cypress/support/commands.js | 13 ++++++++++ 2 files changed, 27 insertions(+), 34 deletions(-) diff --git a/cypress/e2e/locality.cy.js b/cypress/e2e/locality.cy.js index d11f4656..51e205b1 100644 --- a/cypress/e2e/locality.cy.js +++ b/cypress/e2e/locality.cy.js @@ -21,6 +21,9 @@ describe('Adding species in Locality -> Species tab', () => { cy.contains('somespecies') cy.addReferenceAndSave() + cy.contains('Edited item successfully.') + cy.visit(`/locality/20920?tab=2`) + cy.contains('somespecies') }) it.skip('works through "copy species taxonomy" button') it.skip('does not work with invalid species') @@ -38,6 +41,7 @@ describe('Adding species in Locality -> Species tab', () => { // adding species that already exists in the database cy.contains('Add new Species').click() + cy.clearNewSpeciesForm() cy.get('[name=order_name]').type('Rodentia') cy.get('[name=family_name]').type('Gliridae') cy.get('[name=genus_name]').type('Simplomys') @@ -48,25 +52,15 @@ describe('Adding species in Locality -> Species tab', () => { // add new, unique species cy.contains('Add new Species').click() - cy.get('[name=order_name]').clear() + cy.clearNewSpeciesForm() cy.get('[name=order_name]').type('neworder') - cy.get('[name=family_name]').clear() cy.get('[name=family_name]').type('newfamily') - cy.get('[name=genus_name]').clear() cy.get('[name=genus_name]').type('newgenus') - cy.get('[name=species_name]').clear() cy.get('[name=species_name]').type('newspecies') - cy.get('[name=unique_identifier]').clear() cy.get('[name=unique_identifier]').type('-') cy.contains('Save').click() - cy.get('[id=write-button]').click() - cy.get('[id=write-button]').should('be.disabled') - cy.contains('button', 'Add existing reference').click() - cy.get('button[data-cy^="add-button"]').first().click() - cy.contains('button', 'Close').click() - cy.get('[id=write-button]').should('not.be.disabled') - cy.get('[id=write-button]').click() + cy.addReferenceAndSave() cy.contains('Edited item successfully.') cy.visit(`/locality/20920?tab=2`) cy.contains('Lantian-Shuijiazui') @@ -77,8 +71,9 @@ describe('Adding species in Locality -> Species tab', () => { // adding species that has been added earlier through "add new species" button cy.contains('Add new Species').click() - cy.get('[name=order_name]').type('anything') - cy.get('[name=family_name]').type('anything') + cy.clearNewSpeciesForm() + cy.get('[name=order_name]').type('neworder') + cy.get('[name=family_name]').type('newfamily') cy.get('[name=genus_name]').type('newgenus') cy.get('[name=species_name]').type('newspecies') cy.get('[name=unique_identifier]').type('-') @@ -122,13 +117,8 @@ describe('Creating a new locality', () => { cy.get('[id=dec_long-textfield]').type('103.67') cy.get('[id=dms_lat-textfield]').should('have.value', '49 4 12 N') cy.get('[id=dms_long-textfield]').should('have.value', '103 40 12 E') - cy.get('[id=write-button]').click() - cy.get('[id=write-button]').should('be.disabled') - cy.contains('button', 'Add existing reference').click() - cy.get('button[data-cy^="add-button"]').first().click() - cy.contains('button', 'Close').click() - cy.get('[id=write-button]').should('not.be.disabled') - cy.get('[id=write-button]').click() + + cy.addReferenceAndSave() cy.contains('Edited item successfully.') cy.contains('Bugat') cy.get('[id=delete-button]').should('exist') @@ -242,15 +232,8 @@ describe('Creating a new locality', () => { cy.get('[id=dec_lat-textfield]').type('49.07') cy.get('[id=dec_long-textfield]').type('103.67') cy.get('[id=write-button]').should('not.be.disabled') - cy.get('[id=write-button]').click() - - cy.get('[id=write-button]').should('be.disabled') - cy.contains('button', 'Add existing reference').click() - cy.get('button[data-cy^="add-button"]').first().click() - cy.contains('button', 'Close').click() - cy.get('[id=write-button]').should('not.be.disabled') - cy.get('[id=write-button]').click() + cy.addReferenceAndSave() cy.contains('Edited item successfully.') cy.contains('Bugat') cy.contains('8.676') @@ -406,11 +389,8 @@ describe("Locality's coordinate selection map works", () => { cy.contains('Central Finland').first().click() // first item on the list cy.contains('Latitude: 63.05484, Longitude: 24.75291') cy.contains('Save').click() - cy.contains('Finalize entry').click() - cy.contains('button', 'Add existing reference').click() - cy.get('button[data-cy^="add-button"]').first().click() - cy.contains('button', 'Close').click() - cy.get('[id=write-button]').click() + + cy.addReferenceAndSave() cy.visit(`/locality/20920?tab=1`) cy.contains('63.05484') cy.contains('24.75291') diff --git a/cypress/support/commands.js b/cypress/support/commands.js index 84e3c7f4..18be7a25 100644 --- a/cypress/support/commands.js +++ b/cypress/support/commands.js @@ -53,3 +53,16 @@ Cypress.Commands.add('addReferenceAndSave', () => { cy.get('[id=write-button]').should('not.be.disabled') cy.get('[id=write-button]').click() }) + +Cypress.Commands.add('clearNewSpeciesForm', () => { + cy.get('[name=order_name]').clear() + cy.get('[name=subclass_or_superorder_name]').clear() + cy.get('[name=family_name]').clear() + cy.get('[name=suborder_or_superfamily_name]').clear() + cy.get('[name=genus_name]').clear() + cy.get('[name=species_name]').clear() + cy.get('[name=unique_identifier]').clear() + cy.get('[name=taxonomic_status]').clear() + cy.get('[name=sp_comment]').clear() + cy.get('[name=sp_author]').clear() +}) From 9af3e1dd91bdffba62aed74b67447938940f7b67 Mon Sep 17 00:00:00 2001 From: juhanikat Date: Thu, 28 Aug 2025 13:10:09 +0300 Subject: [PATCH 18/32] update every test with the new reference adding command, update copy taxonomy button id, and add more locality -> species tests --- cypress/e2e/locality.cy.js | 42 ++++++++--- cypress/e2e/species.cy.js | 50 ++++--------- cypress/e2e/timeBound.cy.js | 17 ++--- cypress/e2e/timeUnit.cy.js | 72 +++++-------------- .../DetailView/common/SelectingTable.tsx | 4 +- .../components/Locality/Tabs/SpeciesTab.tsx | 3 +- .../components/Species/Tabs/TaxonomyTab.tsx | 3 +- 7 files changed, 70 insertions(+), 121 deletions(-) diff --git a/cypress/e2e/locality.cy.js b/cypress/e2e/locality.cy.js index 51e205b1..b9e8a3fb 100644 --- a/cypress/e2e/locality.cy.js +++ b/cypress/e2e/locality.cy.js @@ -2,7 +2,7 @@ before('Reset database', () => { cy.request(Cypress.env('databaseResetUrl')) }) -describe('Adding species in Locality -> Species tab', () => { +describe('Adding species in Locality -> Species tab for an existing locality', () => { beforeEach('Login as admin', () => { cy.login('testSu') }) @@ -25,19 +25,30 @@ describe('Adding species in Locality -> Species tab', () => { cy.visit(`/locality/20920?tab=2`) cy.contains('somespecies') }) - it.skip('works through "copy species taxonomy" button') + + it('works through "copy species taxonomy" button', () => { + cy.visit(`/locality/20920?tab=2`) + cy.contains('Lantian-Shuijiazui') + cy.get('[id=edit-button]').click() + cy.contains('Add new Species').click() + cy.get('[data-cy=copy_existing_taxonomy_button]').click() + cy.get('[data-cy=add-button-21426]').click() + cy.contains('Close').click() + cy.get('[name=species_name]').clear() + cy.get('[name=species_name]').type('Newspecies') + cy.contains('Save').click() + + cy.addReferenceAndSave() + cy.contains('Edited item successfully.') + cy.visit(`/locality/20920?tab=2`) + cy.contains('newspecies') + }) it.skip('does not work with invalid species') it('does not work if the species has already been added', () => { cy.visit(`/locality/20920?tab=2`) cy.contains('Lantian-Shuijiazui') cy.get('[id=edit-button]').click() - cy.contains('Select Species').click() - cy.get('[data-cy=add-button-21052]').first().click() - cy.get('[data-cy=add-button-21426]').first().click() - cy.contains('Close').click() - cy.contains('Rodentia') - cy.contains('Eulipotyphla') // adding species that already exists in the database cy.contains('Add new Species').click() @@ -65,8 +76,6 @@ describe('Adding species in Locality -> Species tab', () => { cy.visit(`/locality/20920?tab=2`) cy.contains('Lantian-Shuijiazui') cy.get('[id=edit-button]').click() - cy.contains('Rodentia') - cy.contains('Eulipotyphla') cy.contains('Neworder') // adding species that has been added earlier through "add new species" button @@ -82,7 +91,18 @@ describe('Adding species in Locality -> Species tab', () => { }) it.skip('does not work with species with invalid taxonomic order') - it.skip('does not work through "copy species taxonomy" button if nothing is changed') + + it('does not work through "copy species taxonomy" button if nothing is changed', () => { + cy.visit(`/locality/20920?tab=2`) + cy.contains('Lantian-Shuijiazui') + cy.get('[id=edit-button]').click() + cy.contains('Add new Species').click() + cy.get('[data-cy=copy_existing_taxonomy_button]').click() + cy.get('[data-cy=add-button-21052]').click() + cy.contains('Close').click() + cy.contains('Save').click() + cy.contains('The taxon already exists in the database.') + }) }) describe('Creating a new locality', () => { diff --git a/cypress/e2e/species.cy.js b/cypress/e2e/species.cy.js index e04d6f0a..261bf7c2 100644 --- a/cypress/e2e/species.cy.js +++ b/cypress/e2e/species.cy.js @@ -19,13 +19,7 @@ describe('Creating a species', () => { cy.get('[id=species_name-textfield]').type('testSpecies') cy.get('[id=unique_identifier-textfield]').type('{backspace}testUniqueIdentifier') cy.get('[id=write-button]').should('not.be.disabled') - cy.get('[id=write-button]').click() - cy.get('[id=write-button]').should('be.disabled') - cy.contains('button', 'Add existing reference').click() - cy.get('button[data-cy^="add-button"]').first().click() - cy.contains('button', 'Close').click() - cy.get('[id=write-button]').should('not.be.disabled') - cy.get('[id=write-button]').click() + cy.addReferenceAndSave() // tests that values are capitalised and trimmed properly cy.contains('Testsuperorder') @@ -66,12 +60,7 @@ describe('Editing a species', () => { cy.get('[id=subfamily_name-textfield]').type('heimo') cy.get('[id=write-button]').should('not.be.disabled') - cy.get('[id=write-button]').click() - cy.contains('button', 'Add existing reference').click() - cy.get('button[data-cy^="add-button"]').first().click() - cy.contains('button', 'Close').click() - cy.get('[id=write-button]').should('not.be.disabled') - cy.get('[id=write-button]').click() + cy.addReferenceAndSave() cy.contains('Saved species entry successfully.') cy.contains('Modifiedgenus') cy.contains('modifiedspecies') @@ -101,14 +90,8 @@ describe('Editing a species', () => { cy.get('[id=species_name-textfield]').type('newSpecies') cy.contains('Species: This field is required').should('not.exist') - cy.get('[id=write-button]').click() - - cy.contains('button', 'Add existing reference').click() - cy.get('button[data-cy^="add-button"]').first().click() - cy.contains('button', 'Close').click() - cy.get('[id=write-button]').should('not.be.disabled') - cy.get('[id=write-button]').click() + cy.addReferenceAndSave() cy.contains('Saved species entry successfully.') cy.contains('Newgenus') cy.contains('newspecies') @@ -126,8 +109,7 @@ describe('Taxonomy checks work', () => { it('Creating a species that already exists in database does not work', () => { cy.visit('/species/new') - cy.get('[id=copy_existing_taxonomy_button]').contains('Copy existing taxonomy') - cy.get('[id=copy_existing_taxonomy_button]').click() + cy.get('[data-cy=copy_existing_taxonomy_button]').click() cy.get('[data-cy=add-button-21052]').click() cy.contains('Close').click() cy.get('[id=write-button]').click() @@ -136,8 +118,7 @@ describe('Taxonomy checks work', () => { it('Invalid main taxonomy values do not work', () => { cy.visit('/species/new') - cy.get('[id=copy_existing_taxonomy_button]').contains('Copy existing taxonomy') - cy.get('[id=copy_existing_taxonomy_button]').click() + cy.get('[data-cy=copy_existing_taxonomy_button]').click() cy.get('[data-cy=add-button-21052]').click() cy.contains('Close').click() cy.get('[id=unique_identifier-textfield]').type('{backspace}new identifier') // change identifier so it's not a duplicate species @@ -158,8 +139,7 @@ describe('Taxonomy checks work', () => { it('Invalid superorder/order pair does not work', () => { cy.visit('/species/new') - cy.get('[id=copy_existing_taxonomy_button]').contains('Copy existing taxonomy') - cy.get('[id=copy_existing_taxonomy_button]').click() + cy.get('[data-cy=copy_existing_taxonomy_button]').click() cy.get('[data-cy=add-button-21052]').click() cy.contains('Close').click() cy.get('[id=unique_identifier-textfield]').type('{backspace}new identifier') @@ -172,8 +152,7 @@ describe('Taxonomy checks work', () => { it('Invalid order/suborder/family trio does not work', () => { cy.visit('/species/new') - cy.get('[id=copy_existing_taxonomy_button]').contains('Copy existing taxonomy') - cy.get('[id=copy_existing_taxonomy_button]').click() + cy.get('[data-cy=copy_existing_taxonomy_button]').click() cy.get('[data-cy=add-button-84357]').click() cy.contains('Close').click() cy.get('[id=unique_identifier-textfield]').type('{backspace}new identifier') @@ -187,8 +166,7 @@ describe('Taxonomy checks work', () => { it('Invalid suborder/family pair does not work', () => { cy.visit('/species/new') - cy.get('[id=copy_existing_taxonomy_button]').contains('Copy existing taxonomy') - cy.get('[id=copy_existing_taxonomy_button]').click() + cy.get('[data-cy=copy_existing_taxonomy_button]').click() cy.get('[data-cy=add-button-84357]').click() cy.contains('Close').click() cy.get('[id=unique_identifier-textfield]').type('{backspace}new identifier') @@ -201,8 +179,7 @@ describe('Taxonomy checks work', () => { it('Invalid family/subfamily/genus trio does not work', () => { cy.visit('/species/new') - cy.get('[id=copy_existing_taxonomy_button]').contains('Copy existing taxonomy') - cy.get('[id=copy_existing_taxonomy_button]').click() + cy.get('[data-cy=copy_existing_taxonomy_button]').click() cy.get('[data-cy=add-button-23065]').click() cy.contains('Close').click() cy.get('[id=unique_identifier-textfield]').type('{backspace}new identifier') @@ -216,8 +193,7 @@ describe('Taxonomy checks work', () => { it('Invalid subfamily/genus pair does not work', () => { cy.visit('/species/new') - cy.get('[id=copy_existing_taxonomy_button]').contains('Copy existing taxonomy') - cy.get('[id=copy_existing_taxonomy_button]').click() + cy.get('[data-cy=copy_existing_taxonomy_button]').click() cy.get('[data-cy=add-button-23065]').click() cy.contains('Close').click() cy.get('[id=unique_identifier-textfield]').type('{backspace}new identifier') @@ -230,8 +206,7 @@ describe('Taxonomy checks work', () => { it('When genus is indet. species has to be indet.', () => { cy.visit('/species/new') - cy.get('[id=copy_existing_taxonomy_button]').contains('Copy existing taxonomy') - cy.get('[id=copy_existing_taxonomy_button]').click() + cy.get('[data-cy=copy_existing_taxonomy_button]').click() cy.get('[data-cy=add-button-21052]').click() cy.contains('Close').click() cy.get('[id=unique_identifier-textfield]').type('{backspace}new identifier') @@ -246,8 +221,7 @@ describe('Taxonomy checks work', () => { it('When genus is gen. species has to be sp.', () => { cy.visit('/species/new') - cy.get('[id=copy_existing_taxonomy_button]').contains('Copy existing taxonomy') - cy.get('[id=copy_existing_taxonomy_button]').click() + cy.get('[data-cy=copy_existing_taxonomy_button]').click() cy.get('[data-cy=add-button-21052]').click() cy.contains('Close').click() cy.get('[id=unique_identifier-textfield]').type('{backspace}new identifier') diff --git a/cypress/e2e/timeBound.cy.js b/cypress/e2e/timeBound.cy.js index 71d81cf5..73800ba5 100644 --- a/cypress/e2e/timeBound.cy.js +++ b/cypress/e2e/timeBound.cy.js @@ -17,12 +17,8 @@ describe('Creating time bound', () => { cy.get('[id=write-button]').should('be.disabled') cy.get('[id=age-textfield]').first().type('20') cy.contains('Age (Ma): This field is required').should('not.exist') - cy.get('[id=write-button]').click() - cy.contains('button', 'Add existing reference').click() - cy.get('button[data-cy^="add-button"]').first().click() - cy.contains('button', 'Close').click() - cy.get('[id=write-button]').should('not.be.disabled') - cy.get('[id=write-button]').click() + + cy.addReferenceAndSave() cy.contains('Edited item successfully.') }) }) @@ -38,13 +34,8 @@ describe('Editing time bound', () => { cy.get('[id=edit-button]').click() cy.get('[id=age-textfield]').first().type('{backspace}{backspace}{backspace}{backspace}{backspace}1.5') cy.contains('This field is required').should('not.exist') - cy.get('[id=write-button]').click() - cy.get('[id=write-button]').should('be.disabled') - cy.contains('button', 'Add existing reference').click() - cy.get('button[data-cy^="add-button"]').first().click() - cy.contains('button', 'Close').click() - cy.get('[id=write-button]').should('not.be.disabled') - cy.get('[id=write-button]').click() + + cy.addReferenceAndSave() cy.contains('Edited item successfully.') cy.contains('1.5') }) diff --git a/cypress/e2e/timeUnit.cy.js b/cypress/e2e/timeUnit.cy.js index bdf17a3d..58d3199a 100644 --- a/cypress/e2e/timeUnit.cy.js +++ b/cypress/e2e/timeUnit.cy.js @@ -16,13 +16,8 @@ describe('Creating a time unit', () => { cy.get('[data-cy=add-button-11]').first().click() cy.get('[id=low_bnd-tableselection]').first().click() // Select Lower Bound Button cy.get('[data-cy=add-button-14]').first().click() - cy.get('[id=write-button]').click() - cy.get('[id=write-button]').should('be.disabled') - cy.contains('button', 'Add existing reference').click() - cy.get('button[data-cy^="add-button"]').first().click() - cy.contains('button', 'Close').click() - cy.get('[id=write-button]').should('not.be.disabled') - cy.get('[id=write-button]').click() + + cy.addReferenceAndSave() cy.contains('Testing Time Unit Creation') cy.contains('ALMAAsianlandmammalage') cy.contains('C2N-o') @@ -130,13 +125,8 @@ describe('Creating a time unit', () => { cy.get('[id=sequence-tableselection]').should('have.value', 'ALMAAsianlandmammalage') cy.contains('C2N-o') cy.contains('C2N-y') - cy.get('[id=write-button]').click() - cy.get('[id=write-button]').should('be.disabled') - cy.contains('button', 'Add existing reference').click() - cy.get('button[data-cy^="add-button"]').first().click() - cy.contains('button', 'Close').click() - cy.get('[id=write-button]').should('not.be.disabled') - cy.get('[id=write-button]').click() + + cy.addReferenceAndSave() cy.contains('C2N-o') cy.contains('C2N-y') cy.contains('Creating new time-unit').should('not.exist') @@ -151,13 +141,8 @@ describe('Creating a time unit', () => { cy.get('[data-cy=add-button-11]').first().click() cy.get('[id=low_bnd-tableselection]').first().click() cy.get('[data-cy=add-button-14]').first().click() - cy.get('[id=write-button]').click() - cy.get('[id=write-button]').should('be.disabled') - cy.contains('button', 'Add existing reference').click() - cy.get('button[data-cy^="add-button"]').first().click() - cy.contains('button', 'Close').click() - cy.get('[id=write-button]').should('not.be.disabled') - cy.get('[id=write-button]').click() + + cy.addReferenceAndSave() cy.contains('C2N-o') cy.contains('C2N-y') cy.get('[id=edit-button]').click() @@ -165,13 +150,8 @@ describe('Creating a time unit', () => { cy.get('[data-cy=add-button-CalatayudTeruellocalbiozone]').first().click() cy.get('[id=low_bnd-tableselection]').first().click() cy.get('[data-cy=add-button-49]').first().click() - cy.get('[id=write-button]').click() - cy.get('[id=write-button]').should('be.disabled') - cy.contains('button', 'Add existing reference').click() - cy.get('button[data-cy^="add-button"]').first().click() - cy.contains('button', 'Close').click() - cy.get('[id=write-button]').should('not.be.disabled') - cy.get('[id=write-button]').click() + + cy.addReferenceAndSave() cy.contains('CalatayudTeruellocalbiozone') cy.contains('Langhian/Serravallian') }) @@ -189,13 +169,8 @@ describe('Editing a time unit', () => { cy.get('[id=tu_display_name-textfield]').should('be.disabled') // name field is disabled when editing cy.get('[id=sequence-tableselection]').first().click() cy.get('[data-cy=add-button-ALMAAsianlandmammalage]').first().click() - cy.get('[id=write-button]').click() - cy.get('[id=write-button]').should('be.disabled') - cy.contains('button', 'Add existing reference').click() - cy.get('button[data-cy^="add-button"]').first().click() - cy.contains('button', 'Close').click() - cy.get('[id=write-button]').should('not.be.disabled') - cy.get('[id=write-button]').click() + + cy.addReferenceAndSave() cy.contains('ALMAAsianlandmammalage') }) @@ -274,13 +249,8 @@ describe('Editing a time unit', () => { cy.get('[id=sequence-tableselection]').should('have.value', 'CalatayudTeruellocalbiozone') cy.contains('C2N-y') cy.contains('MioceneLate-low') - cy.get('[id=write-button]').click() - cy.get('[id=write-button]').should('be.disabled') - cy.contains('button', 'Add existing reference').click() - cy.get('button[data-cy^="add-button"]').first().click() - cy.contains('button', 'Close').click() - cy.get('[id=write-button]').should('not.be.disabled') - cy.get('[id=write-button]').click() + + cy.addReferenceAndSave() cy.contains('CalatayudTeruellocalbiozone') cy.contains('C2N-y') cy.contains('MioceneLate-low') @@ -292,13 +262,8 @@ describe('Editing a time unit', () => { cy.get('[id=edit-button]').click() cy.get('[id=sequence-tableselection]').first().click() cy.get('[data-cy=add-button-easternparatethys]').first().click() - cy.get('[id=write-button]').click() - cy.get('[id=write-button]').should('be.disabled') - cy.contains('button', 'Add existing reference').click() - cy.get('button[data-cy^="add-button"]').first().click() - cy.contains('button', 'Close').click() - cy.get('[id=write-button]').should('not.be.disabled') - cy.get('[id=write-button]').click() + + cy.addReferenceAndSave() cy.contains('Bahean') cy.contains('easternparatethys') cy.get('[id=edit-button]').click() @@ -307,13 +272,8 @@ describe('Editing a time unit', () => { cy.get('[data-cy=add-button-CalatayudTeruellocalbiozone]').first().click() cy.get('[id=up_bnd-tableselection]').first().click() cy.get('[data-cy=add-button-14]').first().click() - cy.get('[id=write-button]').click() - cy.get('[id=write-button]').should('be.disabled') - cy.contains('button', 'Add existing reference').click() - cy.get('button[data-cy^="add-button"]').first().click() - cy.contains('button', 'Close').click() - cy.get('[id=write-button]').should('not.be.disabled') - cy.get('[id=write-button]').click() + + cy.addReferenceAndSave() cy.contains('CalatayudTeruellocalbiozone') cy.contains('C2N-o') }) diff --git a/frontend/src/components/DetailView/common/SelectingTable.tsx b/frontend/src/components/DetailView/common/SelectingTable.tsx index ca28ec7a..5ba84919 100755 --- a/frontend/src/components/DetailView/common/SelectingTable.tsx +++ b/frontend/src/components/DetailView/common/SelectingTable.tsx @@ -12,6 +12,7 @@ give an array in prop "selectedValues" that contains the string values that already exist in editdata */ export const SelectingTable = ({ + dataCy, buttonText, data, columns, @@ -23,6 +24,7 @@ export const SelectingTable = isError, useObject, }: { + dataCy?: string buttonText: string data: T[] | undefined columns: MRT_ColumnDef[] @@ -69,7 +71,7 @@ export const SelectingTable = if (isError) return Error fetching data for the selecting table. if (!data) return return ( - + {data ? ( data={filteredData} diff --git a/frontend/src/components/Locality/Tabs/SpeciesTab.tsx b/frontend/src/components/Locality/Tabs/SpeciesTab.tsx index 76daac3c..5bf0fce9 100755 --- a/frontend/src/components/Locality/Tabs/SpeciesTab.tsx +++ b/frontend/src/components/Locality/Tabs/SpeciesTab.tsx @@ -36,8 +36,9 @@ export const SpeciesTab = () => { } const copyTaxonomyButton = ( - + + dataCy="copy_existing_taxonomy_button" buttonText="Copy existing taxonomy" data={speciesData} isError={isError} diff --git a/frontend/src/components/Species/Tabs/TaxonomyTab.tsx b/frontend/src/components/Species/Tabs/TaxonomyTab.tsx index 5468f2d7..1a1efa05 100755 --- a/frontend/src/components/Species/Tabs/TaxonomyTab.tsx +++ b/frontend/src/components/Species/Tabs/TaxonomyTab.tsx @@ -21,8 +21,9 @@ export const TaxonomyTab = () => { } const copyTaxonomyButton = ( - + + dataCy="copy_existing_taxonomy_button" buttonText="Copy existing taxonomy" data={speciesQueryData} isError={isError} From 976902f933e23a2c1f66ee2554310c4762e04cea Mon Sep 17 00:00:00 2001 From: juhanikat Date: Thu, 28 Aug 2025 13:42:16 +0300 Subject: [PATCH 19/32] add more locality -> species tests --- cypress/e2e/locality.cy.js | 56 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 54 insertions(+), 2 deletions(-) diff --git a/cypress/e2e/locality.cy.js b/cypress/e2e/locality.cy.js index b9e8a3fb..85d5a682 100644 --- a/cypress/e2e/locality.cy.js +++ b/cypress/e2e/locality.cy.js @@ -11,6 +11,7 @@ describe('Adding species in Locality -> Species tab for an existing locality', ( cy.visit(`/locality/20920?tab=2`) cy.contains('Lantian-Shuijiazui') cy.get('[id=edit-button]').click() + cy.contains('Add new Species').click() cy.get('[name=order_name]').type('Rodentia') cy.get('[name=family_name]').type('Gliridae') @@ -43,7 +44,22 @@ describe('Adding species in Locality -> Species tab for an existing locality', ( cy.visit(`/locality/20920?tab=2`) cy.contains('newspecies') }) - it.skip('does not work with invalid species') + + it('does not work with invalid species', () => { + cy.visit(`/locality/20920?tab=2`) + cy.contains('Lantian-Shuijiazui') + cy.get('[id=edit-button]').click() + + cy.contains('Add new Species').click() + cy.clearNewSpeciesForm() + cy.get('[name=order_name]').type('Order with spaces') + cy.get('[name=family_name]').type('newfamily') + cy.get('[name=genus_name]').type('newgenus') + cy.get('[name=species_name]').type('newspecies') + cy.get('[name=unique_identifier]').type('veryunique') + cy.contains('Save').click() + cy.contains('Order must not contain any spaces, unless the value is "incertae sedis".') + }) it('does not work if the species has already been added', () => { cy.visit(`/locality/20920?tab=2`) @@ -90,7 +106,43 @@ describe('Adding species in Locality -> Species tab for an existing locality', ( cy.contains('The taxon already exists in the database.') }) - it.skip('does not work with species with invalid taxonomic order') + it('does not work with species with invalid taxonomic order', () => { + cy.visit(`/locality/20920?tab=2`) + cy.contains('Lantian-Shuijiazui') + cy.get('[id=edit-button]').click() + + // adding species that already exists in the database + cy.contains('Add new Species').click() + cy.clearNewSpeciesForm() + cy.get('[name=order_name]').type('Rodentia') + cy.get('[name=family_name]').type('Soricidae') + cy.get('[name=genus_name]').type('Simplomys') + cy.get('[name=species_name]').type('Simplicidens') + cy.get('[name=unique_identifier]').type('unique') + cy.contains('Save').click() + cy.contains('Family Soricidae belongs to order Eulipotyphla, not Rodentia.') + cy.contains('Genus Simplomys belongs to family Gliridae, not Soricidae.') + + cy.contains('Add new Species').click() + cy.clearNewSpeciesForm() + cy.get('[name=order_name]').type('Carnivora') + cy.get('[name=family_name]').type('Canidae') + cy.get('[name=genus_name]').type('Simplomys') + cy.get('[name=species_name]').type('Simplicidens') + cy.get('[name=unique_identifier]').type('moreunique') + cy.contains('Save').click() + cy.contains('Genus Simplomys belongs to family Gliridae, not Canidae.') + + cy.contains('Add new Species').click() + cy.clearNewSpeciesForm() + cy.get('[name=order_name]').type('Artiodactyla') + cy.get('[name=family_name]').type('Bovidae') + cy.get('[name=genus_name]').type('Amblycoptus') + cy.get('[name=species_name]').type('indet.') + cy.get('[name=unique_identifier]').type('uniquest') + cy.contains('Save').click() + cy.contains('Genus Amblycoptus belongs to family Soricidae, not Bovidae.') + }) it('does not work through "copy species taxonomy" button if nothing is changed', () => { cy.visit(`/locality/20920?tab=2`) From 575c4f3e681fe7bb8d146f77ff69fd582b48cfa7 Mon Sep 17 00:00:00 2001 From: juhanikat Date: Thu, 28 Aug 2025 13:44:47 +0300 Subject: [PATCH 20/32] add subfamily field to add new species form in locality -> species tab --- cypress/support/commands.js | 5 +++-- frontend/src/components/Locality/Tabs/SpeciesTab.tsx | 1 + 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/cypress/support/commands.js b/cypress/support/commands.js index 18be7a25..f18eb987 100644 --- a/cypress/support/commands.js +++ b/cypress/support/commands.js @@ -56,11 +56,12 @@ Cypress.Commands.add('addReferenceAndSave', () => { Cypress.Commands.add('clearNewSpeciesForm', () => { cy.get('[name=order_name]').clear() - cy.get('[name=subclass_or_superorder_name]').clear() cy.get('[name=family_name]').clear() - cy.get('[name=suborder_or_superfamily_name]').clear() cy.get('[name=genus_name]').clear() cy.get('[name=species_name]').clear() + cy.get('[name=subclass_or_superorder_name]').clear() + cy.get('[name=suborder_or_superfamily_name]').clear() + cy.get('[name=subfamily_name]').clear() cy.get('[name=unique_identifier]').clear() cy.get('[name=taxonomic_status]').clear() cy.get('[name=sp_comment]').clear() diff --git a/frontend/src/components/Locality/Tabs/SpeciesTab.tsx b/frontend/src/components/Locality/Tabs/SpeciesTab.tsx index 5bf0fce9..09a6f363 100755 --- a/frontend/src/components/Locality/Tabs/SpeciesTab.tsx +++ b/frontend/src/components/Locality/Tabs/SpeciesTab.tsx @@ -179,6 +179,7 @@ export const SpeciesTab = () => { { name: 'species_name', label: 'Species', required: true }, { name: 'subclass_or_superorder_name', label: 'Subclass or Superorder' }, { name: 'suborder_or_superfamily_name', label: 'Suborder or Superfamily' }, + { name: 'subfamily_name', label: 'Subfamily or Tribe' }, { name: 'unique_identifier', label: 'Unique Identifier', required: true }, { name: 'taxonomic_status', label: 'Taxon status' }, { name: 'sp_comment', label: 'Comment' }, From b50f0028d78ad8139371e598143889706595f4b7 Mon Sep 17 00:00:00 2001 From: juhanikat Date: Thu, 28 Aug 2025 13:55:35 +0300 Subject: [PATCH 21/32] add class_name to species created with the add new species form --- frontend/src/components/Locality/Tabs/SpeciesTab.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/components/Locality/Tabs/SpeciesTab.tsx b/frontend/src/components/Locality/Tabs/SpeciesTab.tsx index 09a6f363..e948c9bd 100755 --- a/frontend/src/components/Locality/Tabs/SpeciesTab.tsx +++ b/frontend/src/components/Locality/Tabs/SpeciesTab.tsx @@ -208,7 +208,7 @@ export const SpeciesTab = () => { { lid: editData.lid, species_id: undefined, - com_species: { ...(convertedSpecies as unknown as SpeciesDetailsType) }, + com_species: { ...(convertedSpecies as unknown as SpeciesDetailsType), class_name: 'Mammalia' }, rowState: 'new', }, ], From 48ec4b5f432e299c5bbbd42c6c33988ada92a819 Mon Sep 17 00:00:00 2001 From: juhanikat Date: Thu, 28 Aug 2025 13:59:37 +0300 Subject: [PATCH 22/32] update documentation titles, and update reference types doc --- documentation/entities/reference_types.md | 372 +++++++++++----------- documentation/entities/species.md | 2 +- documentation/entities/time_bound.md | 2 +- documentation/entities/time_unit.md | 2 +- 4 files changed, 191 insertions(+), 187 deletions(-) diff --git a/documentation/entities/reference_types.md b/documentation/entities/reference_types.md index d49a3f0c..13c20a21 100644 --- a/documentation/entities/reference_types.md +++ b/documentation/entities/reference_types.md @@ -1,12 +1,15 @@ -Current reference types & their field labels / field names (29.09.2024) +# Reference types -As of 29.09.2024 there is no UI to add more entries however you can do it by altering the db itself. +Note: This file is old and should be checked and updated. -In the test_data/sqlfiles/now_test.sql code related to adding reference types is around rows 3665-3700 and the fields for the data types are added around rows 3175-3525. +Current reference types & their field labels / field names -Do concider other options before you try to alter the data by hand. +In the `test_data/sqlfiles/now_test.sql` file, code related to adding reference types is around rows 3665-3700 and the fields for the data types are added around rows 3175-3525. + +Do consider other options before you try to alter the data by hand. ## Existing fields & their ids + title_primary 1 authors_primary 2 date_primary 3 @@ -32,237 +35,238 @@ Do concider other options before you try to alter the data by hand. exact_date 23 "ref_type_id": 1, "ref_type": "Journal"' - Title / title_primary - Authors / authors_primary - Year / date_primary - Journal / journal_id - Volume / volume - Issue / issue - Start Page / start_page - End Page / end_page - Publisher / publisher - City / pub_place - Title of Issue / title_secondary - Editors of Issue / authors_secondary - Series Title / title_series - ISSN/ISBN / issn_isbn - Abstract / ref_abstract - Web/URL / web_url - Notes / gen_notes - Language / printed_language +Title / title_primary +Authors / authors_primary +Year / date_primary +Journal / journal_id +Volume / volume +Issue / issue +Start Page / start_page +End Page / end_page +Publisher / publisher +City / pub_place +Title of Issue / title_secondary +Editors of Issue / authors_secondary +Series Title / title_series +ISSN/ISBN / issn_isbn +Abstract / ref_abstract +Web/URL / web_url +Notes / gen_notes +Language / printed_language Mandatory fields: title_primary OR title_secondary OR title_series OR gen_notes, date_primary, authors_primary OR authors_secondary, journal "ref_type_id": 2, "ref_type": "Book", - Title / title_primary - Authors / authors_primary - Year / date_primary - Edition / volume - Volume / issue - No of Pages / end_page - Publisher / publisher - City / pub_place - Editors / authors_secondary - Series Title / title_series - Series Editors / authors_series - ISBN / issn_isbn - Abstract / ref_abstract - Web/URL / web_url - Notes / gen_notes - Language / printed_language +Title / title_primary +Authors / authors_primary +Year / date_primary +Edition / volume +Volume / issue +No of Pages / end_page +Publisher / publisher +City / pub_place +Editors / authors_secondary +Series Title / title_series +Series Editors / authors_series +ISBN / issn_isbn +Abstract / ref_abstract +Web/URL / web_url +Notes / gen_notes +Language / printed_language Mandatory fields: title_primary OR title_series OR gen_notes, date_primary, authors_primary OR authors_secondary OR authors_series "ref_type_id": 3, "ref_type": Book Chapter' - Title / title_primary - Authors / authors_primary - Year / date_primary - Edition / volume - Chapter No / issue - Start Page / start_page - End Page / end_page - Publisher / publisher - City / pub_place - Book Title / title_secondary - Editors / authors_secondary - Series Title / title_series - Series Editors / authors_series - ISBN / issn_isbn - Abstract / ref_abstract - Web/URL / web_url - Volume / misc_2 - Notes / gen_notes - Language / printed_language +Title / title_primary +Authors / authors_primary +Year / date_primary +Edition / volume +Chapter No / issue +Start Page / start_page +End Page / end_page +Publisher / publisher +City / pub_place +Book Title / title_secondary +Editors / authors_secondary +Series Title / title_series +Series Editors / authors_series +ISBN / issn_isbn +Abstract / ref_abstract +Web/URL / web_url +Volume / misc_2 +Notes / gen_notes +Language / printed_language Mandatory fields: title_primary OR title_secondary OR title_series OR gen_notes, date_primary, authors_primary OR authors_secondary OR authors_series "ref_type_id": 4, "ref_type": Thesis/Dissertation' - Title / title_primary - Authors / authors_primary - Year / date_primary - No of Pages / end_page - Institution / publisher - Abstract / ref_abstract - Web/URL / web_url - Degree / misc_1 - Notes / gen_notes - Language / printed_language +Title / title_primary +Authors / authors_primary +Year / date_primary +No of Pages / end_page +Institution / publisher +Abstract / ref_abstract +Web/URL / web_url +Degree / misc_1 +Notes / gen_notes +Language / printed_language Mandatory fields: title_primary OR gen_notes, date_primary, authors_primary "ref_type_id": 5, "ref_type": Conference Proceeding' - Title / title_primary - Authors / authors_primary - Publication Year / date_primary - Journal / journal_id - Volume / volume - Edition / issue - Start Page / start_page - End Page / end_page - Publisher / publisher - City / pub_place - Title of Conference / title_secondary - Editors / authors_secondary - Year of Conference / date_secondary - Series Title / title_series - Series Editors / authors_series - Abstract / ref_abstract - Web/URL / web_url - Notes / gen_notes - Language / printed_language +Title / title_primary +Authors / authors_primary +Publication Year / date_primary +Journal / journal_id +Volume / volume +Edition / issue +Start Page / start_page +End Page / end_page +Publisher / publisher +City / pub_place +Title of Conference / title_secondary +Editors / authors_secondary +Year of Conference / date_secondary +Series Title / title_series +Series Editors / authors_series +Abstract / ref_abstract +Web/URL / web_url +Notes / gen_notes +Language / printed_language Mandatory fields: title_primary OR title_secondary OR title_series OR gen_notes, date_primary, authors_primary OR authors_secondary OR authors_series, journal "ref_type_id": 6, "ref_type": Electronic Citation' - Title / title_primary - Authors / authors_primary - Last Update (Year) / date_primary - Organisation / title_secondary - Editors / authors_secondary - Abstract / ref_abstract - Web/URL / web_url - Last Update (Day Month) / misc_1 - Notes / gen_notes - Language / printed_language - Access Date / exact_date +Title / title_primary +Authors / authors_primary +Last Update (Year) / date_primary +Organisation / title_secondary +Editors / authors_secondary +Abstract / ref_abstract +Web/URL / web_url +Last Update (Day Month) / misc_1 +Notes / gen_notes +Language / printed_language +Access Date / exact_date Mandatory fields: title_primary OR title_secondary OR gen_notes, date_primary, authors_primary OR authors_secondary, exact_date "ref_type_id": 7, "ref_type": Internet Communication' - Subject / title_primary - Sender / authors_primary - Year / date_primary - Recipient / authors_secondary - Sender's e-mail / misc_1 - Recipient's e-mail / misc_2 - Notes / gen_notes - Language / printed_language - Date of Message / exact_date +Subject / title_primary +Sender / authors_primary +Year / date_primary +Recipient / authors_secondary +Sender's e-mail / misc_1 +Recipient's e-mail / misc_2 +Notes / gen_notes +Language / printed_language +Date of Message / exact_date Mandatory fields: title_primary OR gen_notes, date_primary, authors_primary OR authors_secondary, exact_date "ref_type_id": 8, "ref_type": Report' - Title / title_primary - Authors / authors_primary - Year / date_primary - Report Number / volume - Start Page / start_page - End Page / end_page - Publisher / publisher - Pub Place / pub_place - Report Name / title_secondary - Editors / authors_secondary - Series Title / title_series - Series Editors / authors_series - Abstract / ref_abstract - Web/URL / web_url - Notes / gen_notes - Language / printed_language +Title / title_primary +Authors / authors_primary +Year / date_primary +Report Number / volume +Start Page / start_page +End Page / end_page +Publisher / publisher +Pub Place / pub_place +Report Name / title_secondary +Editors / authors_secondary +Series Title / title_series +Series Editors / authors_series +Abstract / ref_abstract +Web/URL / web_url +Notes / gen_notes +Language / printed_language Mandatory fields: title_primary OR title_secondary OR title_series OR gen_notes, date_primary, authors_primary OR authors_secondary OR authors_series "ref_type_id": 9, "ref_type": Unpublished Work' - Title / title_primary - Authors / authors_primary - Year / date_primary - Organisation / title_secondary - Editors / authors_secondary - Series Title / title_series - Series Editors / authors_series - Abstract / ref_abstract - Web/URL / web_url - Notes / gen_notes - Language / printed_language +Title / title_primary +Authors / authors_primary +Year / date_primary +Organisation / title_secondary +Editors / authors_secondary +Series Title / title_series +Series Editors / authors_series +Abstract / ref_abstract +Web/URL / web_url +Notes / gen_notes +Language / printed_language Mandatory fields: title_primary OR title_secondary OR title_series OR gen_notes, date_primary, authors_primary OR authors_secondary OR authors_series "ref_type_id": 10, "ref_type": Personal Communication' - Authors / authors_primary - Year / date_primary - Recipients / authors_secondary - Type / misc_1 - Notes / gen_notes - Language / printed_language - Date Sent / exact_date +Authors / authors_primary +Year / date_primary +Recipients / authors_secondary +Type / misc_1 +Notes / gen_notes +Language / printed_language +Date Sent / exact_date Mandatory fields: gen_notes, date_primary, authors_primary OR authors_secondary, exact_date "ref_type_id": 11, "ref_type": Manuscript' - Title / title_primary - Authors / authors_primary - Date / date_primary - Organisation / title_secondary - Editors / authors_secondary - Series Title / title_series - Series Editors / authors_series - Notes / gen_notes - Language / printed_language - Date / exact_date +Title / title_primary +Authors / authors_primary +Date / date_primary +Organisation / title_secondary +Editors / authors_secondary +Series Title / title_series +Series Editors / authors_series +Notes / gen_notes +Language / printed_language +Date / exact_date Mandatory fields: title_primary OR title_secondary OR title_series OR gen_notes, date_primary, authors_primary OR authors_secondary OR authors_series, exact_date "ref_type_id": 12, "ref_type": Notes' - Subject / title_primary - Authors / authors_primary - Year / date_primary - Notes / gen_notes - Date / exact_date +Subject / title_primary +Authors / authors_primary +Year / date_primary +Notes / gen_notes +Date / exact_date Mandatory fields: title_primary OR gen_notes, date_primary, authors_primary, exact_date "ref_type_id": 13, "ref_type": Editing' - Title / title_primary - Authors / authors_primary - Year / date_primary - Notes / gen_notes - Date / exact_date +Title / title_primary +Authors / authors_primary +Year / date_primary +Notes / gen_notes +Date / exact_date Mandatory fields: title_primary OR gen_notes, date_primary, authors_primary, exact_date "ref_type_id": 14, "ref_type": Undefined' - title_primary / title_primary - authors_primary / authors_primary - date_primary / date_primary - journal_id / journal_id - volume / volume - issue / issue - start_page / start_page - end_page / end_page - publisher / publisher - pub_place / pub_place - title_secondary / title_secondary - authors_secondary / authors_secondary - date_secondary / date_secondary - title_series / title_series - authors_series / authors_series - issn_isbn / issn_isbn - abstract / abstract - web_url / web_url - misc_1 / misc_1 - misc_2 / misc_2 - gen_notes / gen_notes - language / language - exact_date / exact_date +title_primary / title_primary +authors_primary / authors_primary +date_primary / date_primary +journal_id / journal_id +volume / volume +issue / issue +start_page / start_page +end_page / end_page +publisher / publisher +pub_place / pub_place +title_secondary / title_secondary +authors_secondary / authors_secondary +date_secondary / date_secondary +title_series / title_series +authors_series / authors_series +issn_isbn / issn_isbn +abstract / abstract +web_url / web_url +misc_1 / misc_1 +misc_2 / misc_2 +gen_notes / gen_notes +language / language +exact_date / exact_date Mandatory fields: title_primary OR title_secondary OR title_series OR gen_notes, date_primary, authors_primary OR authors_secondary OR authors_series, exact_date, journal -* In old php test version instead of journal, journal_id had to be defined. That seems odd, since when creating a new journal, journal_id will not initally be defined. So that will not be done here. \ No newline at end of file + +- In old php test version instead of journal, journal_id had to be defined. That seems odd, since when creating a new journal, journal_id will not initially be defined. So that will not be done here. diff --git a/documentation/entities/species.md b/documentation/entities/species.md index bcd34fdd..e7c78287 100644 --- a/documentation/entities/species.md +++ b/documentation/entities/species.md @@ -1,4 +1,4 @@ -# Localities +# Species ## Required fields diff --git a/documentation/entities/time_bound.md b/documentation/entities/time_bound.md index 74139938..4b781105 100644 --- a/documentation/entities/time_bound.md +++ b/documentation/entities/time_bound.md @@ -1,4 +1,4 @@ -### Time Bound +### Time Bounds A Time Bound can be given the following information: diff --git a/documentation/entities/time_unit.md b/documentation/entities/time_unit.md index 74401e47..bb8cdb1b 100644 --- a/documentation/entities/time_unit.md +++ b/documentation/entities/time_unit.md @@ -1,4 +1,4 @@ -### Time Unit +### Time Units A Time Unit can be given the following information: From 6f65cd2c3faeb1505724a57201c8fb84fc3505de Mon Sep 17 00:00:00 2001 From: juhanikat Date: Fri, 29 Aug 2025 13:08:05 +0300 Subject: [PATCH 23/32] add com_taxa_synonym as an allowed table for species writeHandler --- backend/src/services/locality.ts | 3 +-- backend/src/services/write/locality.ts | 6 +++--- backend/src/services/write/species.ts | 2 +- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/backend/src/services/locality.ts b/backend/src/services/locality.ts index 2b987df4..d1f20be8 100644 --- a/backend/src/services/locality.ts +++ b/backend/src/services/locality.ts @@ -15,7 +15,6 @@ import Prisma from '../../prisma/generated/now_test_client' import { AccessError } from '../middlewares/authorizer' import { fixBigInt } from '../utils/common' import { logDb, nowDb } from '../utils/db' -import { logger } from '../utils/logger' import { getReferenceDetails } from './reference' const getIdsOfUsersProjects = async (user: User) => { @@ -274,7 +273,7 @@ export const filterDuplicateLocalitySpecies = async ( return !(localityDetailsSpeciesIds.includes(localitySpecies.species_id) && localitySpecies.rowState === 'new') } // if localitySpecies doesn't have species_id (i.e. it was created with the "add new species" button in Species tab), - // it will never be filtered out. However, once the taxonomy check for the "add new species" button is working, + // it will never be filtered out. However, due to the taxonomy check for the "add new species" button, // it should not let the user create a duplicate species in this way. return true }) diff --git a/backend/src/services/write/locality.ts b/backend/src/services/write/locality.ts index 5c0245e9..01f7b697 100644 --- a/backend/src/services/write/locality.ts +++ b/backend/src/services/write/locality.ts @@ -49,9 +49,9 @@ export const writeLocality = async ( try { await writeHandler.start() - const filteredLocality = await filterDuplicateLocalitySpecies(locality, user) - if (updateOrAdd === 'update' && filteredLocality) { - locality.now_ls = filteredLocality + const filteredLocalities = await filterDuplicateLocalitySpecies(locality, user) + if (updateOrAdd === 'update' && filteredLocalities && filteredLocalities.length > 0) { + locality.now_ls = filteredLocalities } if (updateOrAdd === 'add') { diff --git a/backend/src/services/write/species.ts b/backend/src/services/write/species.ts index fc7c965c..6ca0fee0 100644 --- a/backend/src/services/write/species.ts +++ b/backend/src/services/write/species.ts @@ -11,7 +11,7 @@ const getSpeciesWriteHandler = (type: ActionType) => { dbName: NOW_DB_NAME, table: 'com_species', idColumn: 'species_id', - allowedColumns: getFieldsOfTables(['com_species', 'now_ls', 'now_sau', 'now_lau']), + allowedColumns: getFieldsOfTables(['com_species', 'now_ls', 'now_sau', 'now_lau', 'com_taxa_synonym']), type, }) } From 33f54dcebbd676a9aff72fbe74af1366d873e21f Mon Sep 17 00:00:00 2001 From: juhanikat Date: Fri, 29 Aug 2025 15:33:10 +0300 Subject: [PATCH 24/32] add validation and other checks for species synonym adding --- .../src/components/DetailView/components.tsx | 32 +++++++------- .../components/Locality/Tabs/SpeciesTab.tsx | 10 +++-- .../components/Species/Tabs/SynonymTab.tsx | 42 ++++++++++++++++++- frontend/src/redux/api.ts | 1 + frontend/src/redux/speciesReducer.ts | 12 +++--- frontend/src/shared/validators/synonym.ts | 31 ++++++++++++++ frontend/src/util/taxonomyUtilities.tsx | 22 +++++++++- 7 files changed, 124 insertions(+), 26 deletions(-) create mode 100644 frontend/src/shared/validators/synonym.ts diff --git a/frontend/src/components/DetailView/components.tsx b/frontend/src/components/DetailView/components.tsx index dfed934f..d250302d 100755 --- a/frontend/src/components/DetailView/components.tsx +++ b/frontend/src/components/DetailView/components.tsx @@ -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 = ({ onWrite, @@ -28,8 +29,8 @@ export const WriteButton = ({ useDetailContext() 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({ @@ -50,6 +51,11 @@ export const WriteButton = ({ 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 = ({ // @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 | 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) - const errors = checkTaxonomy(speciesEditData, speciesData, synonyms) + speciesEditData = convertSpeciesTaxonomyFields(editData as EditDataType) + 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 = ({ }) } - const handleWriteButtonClick = async () => { + const handleWriteButtonClick = () => { if (taxonomy) { setLoading(true) - await writeWithTaxonomyCheck() + writeWithTaxonomyCheck() setLoading(false) return } @@ -186,13 +190,13 @@ export const WriteButton = ({ !('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 } diff --git a/frontend/src/components/Locality/Tabs/SpeciesTab.tsx b/frontend/src/components/Locality/Tabs/SpeciesTab.tsx index e948c9bd..28fc7735 100755 --- a/frontend/src/components/Locality/Tabs/SpeciesTab.tsx +++ b/frontend/src/components/Locality/Tabs/SpeciesTab.tsx @@ -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[] 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) diff --git a/frontend/src/components/Species/Tabs/SynonymTab.tsx b/frontend/src/components/Species/Tabs/SynonymTab.tsx index 560e7317..b97b1b0d 100755 --- a/frontend/src/components/Species/Tabs/SynonymTab.tsx +++ b/frontend/src/components/Species/Tabs/SynonymTab.tsx @@ -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() + const { notify } = useNotify() + + const convertAndCheckNewSynonymTaxonomy = (newSynonym: EditDataType) => { + const errors = [] + for (const field in newSynonym) { + const errorObject = validateSynonym( + newSynonym as unknown as EditDataType, + field as unknown as keyof EditDataType + ) + 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[] = [ { @@ -34,12 +72,14 @@ export const SynonymTab = () => { buttonText="Add new Species" formFields={formFields} editAction={(newSynonym: EditDataType) => { + const convertedSynonym = convertAndCheckNewSynonymTaxonomy(newSynonym) + if (!convertedSynonym) return setEditData({ ...editData, com_taxa_synonym: [ ...editData.com_taxa_synonym, { - ...newSynonym, + ...convertedSynonym, rowState: 'new', }, ], diff --git a/frontend/src/redux/api.ts b/frontend/src/redux/api.ts index 36c04706..a5ff1550 100755 --- a/frontend/src/redux/api.ts +++ b/frontend/src/redux/api.ts @@ -57,6 +57,7 @@ export const api = createApi({ 'localities', 'species', 'specieslist', + 'speciessynonyms', 'reference', 'references', 'timeunits', diff --git a/frontend/src/redux/speciesReducer.ts b/frontend/src/redux/speciesReducer.ts index 16964857..f57fbaa9 100755 --- a/frontend/src/redux/speciesReducer.ts +++ b/frontend/src/redux/speciesReducer.ts @@ -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({ query: () => ({ url: `/species/synonyms`, }), + providesTags: result => (result ? ['speciessynonyms'] : []), }), getSpeciesDetails: builder.query({ 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>({ 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({ 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, diff --git a/frontend/src/shared/validators/synonym.ts b/frontend/src/shared/validators/synonym.ts new file mode 100644 index 00000000..87cece94 --- /dev/null +++ b/frontend/src/shared/validators/synonym.ts @@ -0,0 +1,31 @@ +import { EditDataType, SpeciesSynonym } from '../types' +import { Validators, validator } from './validator' + +export const validateSynonym = ( + editData: EditDataType, + fieldName: keyof EditDataType +) => { + const validators: Validators>> = { + 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>(validators, editData, fieldName) +} diff --git a/frontend/src/util/taxonomyUtilities.tsx b/frontend/src/util/taxonomyUtilities.tsx index 256f8dea..55016316 100644 --- a/frontend/src/util/taxonomyUtilities.tsx +++ b/frontend/src/util/taxonomyUtilities.tsx @@ -146,7 +146,7 @@ const checkSubFamilyGenus = (newSpecies: EditDataType, existingSpecies: ) } -export const checkTaxonomy = ( +export const checkSpeciesTaxonomy = ( newSpecies: EditDataType, existingSpeciesArray: Species[], synonyms: SpeciesSynonym[] @@ -327,7 +327,7 @@ const convertTaxonomyField = (value: string | null | undefined) => { return convertedValue } -export const convertTaxonomyFields = (species: EditDataType) => { +export const convertSpeciesTaxonomyFields = (species: EditDataType) => { 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) => { + 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 +} From 289c8f2d34c2b99a334412eebd0264621c29796b Mon Sep 17 00:00:00 2001 From: juhanikat Date: Mon, 8 Sep 2025 10:41:57 +0300 Subject: [PATCH 25/32] change synonymTab button text and add validator for taxonomic_status, refactor taxonomicStatusOptions --- .../src/components/Species/Tabs/SynonymTab.tsx | 2 +- .../src/components/Species/Tabs/TaxonomyTab.tsx | 3 ++- frontend/src/shared/taxonStatusOptions.ts | 16 ++++++++++++++++ frontend/src/shared/validators/species.ts | 9 +++++++++ frontend/src/util/taxonomyUtilities.tsx | 10 ---------- 5 files changed, 28 insertions(+), 12 deletions(-) create mode 100644 frontend/src/shared/taxonStatusOptions.ts diff --git a/frontend/src/components/Species/Tabs/SynonymTab.tsx b/frontend/src/components/Species/Tabs/SynonymTab.tsx index b97b1b0d..914b5f12 100755 --- a/frontend/src/components/Species/Tabs/SynonymTab.tsx +++ b/frontend/src/components/Species/Tabs/SynonymTab.tsx @@ -69,7 +69,7 @@ export const SynonymTab = () => { const editingForm = ( - buttonText="Add new Species" + buttonText="Add synonym" formFields={formFields} editAction={(newSynonym: EditDataType) => { const convertedSynonym = convertAndCheckNewSynonymTaxonomy(newSynonym) diff --git a/frontend/src/components/Species/Tabs/TaxonomyTab.tsx b/frontend/src/components/Species/Tabs/TaxonomyTab.tsx index 1a1efa05..d7456d57 100755 --- a/frontend/src/components/Species/Tabs/TaxonomyTab.tsx +++ b/frontend/src/components/Species/Tabs/TaxonomyTab.tsx @@ -1,13 +1,14 @@ import { SpeciesDetailsType, Species } from '@/shared/types' import { ArrayFrame, HalfFrames } from '@/components/DetailView/common/tabLayoutHelpers' import { useDetailContext } from '@/components/DetailView/Context/DetailContext' -import { fixNullValuesInTaxonomyFields, taxonStatusOptions } from '@/util/taxonomyUtilities' +import { fixNullValuesInTaxonomyFields } from '@/util/taxonomyUtilities' import { Box } from '@mui/material' import { SelectingTable } from '@/components/DetailView/common/SelectingTable' import { useGetAllSpeciesQuery } from '@/redux/speciesReducer' import { smallSpeciesTableColumns } from '@/common' import { SynonymsModal } from '../SynonymsModal' import { useState } from 'react' +import { taxonStatusOptions } from '@/shared/taxonStatusOptions' export const TaxonomyTab = () => { const { textField, dropdown, bigTextField, editData, setEditData, mode } = useDetailContext() diff --git a/frontend/src/shared/taxonStatusOptions.ts b/frontend/src/shared/taxonStatusOptions.ts new file mode 100644 index 00000000..15a9a5c4 --- /dev/null +++ b/frontend/src/shared/taxonStatusOptions.ts @@ -0,0 +1,16 @@ +// cannot be imported from DetailView -> misc since files in shared folder cannot import from elsewhere in frontend +const emptyOption = { + optionDisplay: 'No value', + display: '', + value: '', +} + +export const taxonStatusOptions = [ + emptyOption, + 'family attrib of genus uncertain', + 'genus attrib of species uncertain', + 'informal species', + 'species validity uncertain', + 'taxonomic validity uncertain', + 'NOW synonym', +] diff --git a/frontend/src/shared/validators/species.ts b/frontend/src/shared/validators/species.ts index 8774c6ca..2cee705a 100755 --- a/frontend/src/shared/validators/species.ts +++ b/frontend/src/shared/validators/species.ts @@ -1,3 +1,4 @@ +import { taxonStatusOptions } from '../taxonStatusOptions' import { EditDataType, SpeciesDetailsType } from '../types' import { Validators, validator } from './validator' @@ -89,6 +90,14 @@ export const validateSpecies = ( return }, }, + taxonomic_status: { + name: 'Taxonomic Status', + asString: (taxonomicStatus: string) => { + if (!taxonStatusOptions.slice(1).includes(taxonomicStatus) && taxonomicStatus !== '') + return `Taxonomic Status must be one of the following: ${taxonStatusOptions.slice(1).join(', ')}, or empty.` + return + }, + }, unique_identifier: { name: 'Unique Identifier', required: true, diff --git a/frontend/src/util/taxonomyUtilities.tsx b/frontend/src/util/taxonomyUtilities.tsx index 55016316..8e161ee6 100644 --- a/frontend/src/util/taxonomyUtilities.tsx +++ b/frontend/src/util/taxonomyUtilities.tsx @@ -1,15 +1,5 @@ import { EditDataType, Species, SpeciesDetailsType, SpeciesSynonym } from '@/shared/types' -import { emptyOption } from '@/components/DetailView/common/misc' -export const taxonStatusOptions = [ - emptyOption, - 'family attrib of genus uncertain', - 'genus attrib of species uncertain', - 'informal species', - 'species validity uncertain', - 'taxonomic validity uncertain', - 'NOW synonym', -] const generateMultipleParentsError = ( invalidField: string, invalidValue1: string | null, From d3880d7019ef5e408fbeb3972108bb852a019a1c Mon Sep 17 00:00:00 2001 From: juhanikat Date: Mon, 8 Sep 2025 10:48:41 +0300 Subject: [PATCH 26/32] species with null taxonomic_status are now changed to use empty string instead, also change emptySpecies taxonomic_status from null to empty string --- frontend/src/components/DetailView/common/defaultValues.ts | 2 +- frontend/src/components/Species/SpeciesDetails.tsx | 2 +- frontend/src/util/taxonomyUtilities.tsx | 1 + 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/frontend/src/components/DetailView/common/defaultValues.ts b/frontend/src/components/DetailView/common/defaultValues.ts index 7639aaff..dfad421b 100755 --- a/frontend/src/components/DetailView/common/defaultValues.ts +++ b/frontend/src/components/DetailView/common/defaultValues.ts @@ -128,7 +128,7 @@ export const emptySpecies = { genus_name: '', species_name: '', unique_identifier: '-', - taxonomic_status: null, + taxonomic_status: '', common_name: '', sp_author: '', strain: null, diff --git a/frontend/src/components/Species/SpeciesDetails.tsx b/frontend/src/components/Species/SpeciesDetails.tsx index db18cf01..cd969a09 100755 --- a/frontend/src/components/Species/SpeciesDetails.tsx +++ b/frontend/src/components/Species/SpeciesDetails.tsx @@ -41,7 +41,7 @@ export const SpeciesDetails = () => { }, [deleteSuccess, deleteError, notify, navigate]) useEffect(() => { - // changes subclass, suborder, and subfamily from null to empty string to make validators work + // changes subclass, suborder, subfamily and taxonomic_status from null to empty string to make validators work // this should be removed if validators are improved in the future if (data) { const fixedSpeciesData = fixNullValuesInTaxonomyFields(data) diff --git a/frontend/src/util/taxonomyUtilities.tsx b/frontend/src/util/taxonomyUtilities.tsx index 8e161ee6..73644ae4 100644 --- a/frontend/src/util/taxonomyUtilities.tsx +++ b/frontend/src/util/taxonomyUtilities.tsx @@ -346,6 +346,7 @@ export const fixNullValuesInTaxonomyFields = (species: Species | SpeciesDetailsT subclass_or_superorder_name: species.subclass_or_superorder_name ?? '', suborder_or_superfamily_name: species.suborder_or_superfamily_name ?? '', subfamily_name: species.subfamily_name ?? '', + taxonomic_status: species.taxonomic_status ?? '', } } From 8629228e4a5311f5683b9ecf8de0bbee908228f2 Mon Sep 17 00:00:00 2001 From: juhanikat Date: Mon, 8 Sep 2025 10:51:55 +0300 Subject: [PATCH 27/32] change species validator to complain about spaces in species name --- frontend/src/shared/validators/species.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/src/shared/validators/species.ts b/frontend/src/shared/validators/species.ts index 2cee705a..a6df06a0 100755 --- a/frontend/src/shared/validators/species.ts +++ b/frontend/src/shared/validators/species.ts @@ -81,6 +81,7 @@ export const validateSpecies = ( 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.') From 2a74d9b60e8481ffd8e2bc6420321d6ad74c4573 Mon Sep 17 00:00:00 2001 From: juhanikat Date: Mon, 8 Sep 2025 11:43:34 +0300 Subject: [PATCH 28/32] update api test values for species, and change Locality -> SpeciesTab to set taxonomic_status to empty string instead of null when copying existing species --- backend/src/api-tests/species/data.ts | 6 +++--- frontend/src/components/Locality/Tabs/SpeciesTab.tsx | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/backend/src/api-tests/species/data.ts b/backend/src/api-tests/species/data.ts index cdf61659..7f22438d 100644 --- a/backend/src/api-tests/species/data.ts +++ b/backend/src/api-tests/species/data.ts @@ -43,9 +43,9 @@ export const newSpeciesBasis: EditDataType = suborder_or_superfamily_name: '', subfamily_name: 'Soricinae', genus_name: 'Petenyia', - species_name: 'dubia test', + species_name: 'dubiatest', unique_identifier: '-', - taxonomic_status: null, + taxonomic_status: '', common_name: null, sp_author: 'Bachmayer & Wilson, 1970', strain: null, @@ -145,7 +145,7 @@ export const newSpeciesBasis: EditDataType = export const editedSpecies: EditDataType = { species_id: 21426, sp_comment: 'Test comment', - species_name: 'Test species name', + species_name: 'Testspeciesname', now_ls: [{ rowState: 'new', lid: 20920 }], com_taxa_synonym: [], now_sau: [], diff --git a/frontend/src/components/Locality/Tabs/SpeciesTab.tsx b/frontend/src/components/Locality/Tabs/SpeciesTab.tsx index 28fc7735..d444d00c 100755 --- a/frontend/src/components/Locality/Tabs/SpeciesTab.tsx +++ b/frontend/src/components/Locality/Tabs/SpeciesTab.tsx @@ -62,7 +62,7 @@ export const SpeciesTab = () => { genus_name: fixedSpecies.genus_name!, species_name: fixedSpecies.species_name!, unique_identifier: fixedSpecies.unique_identifier!, - taxonomic_status: null, + taxonomic_status: '', sp_comment: '', sp_author: '', }) From 4f7499e7d9fe22ef54a279abcbf0a92c53b87d16 Mon Sep 17 00:00:00 2001 From: juhanikat Date: Wed, 17 Sep 2025 10:55:08 +0300 Subject: [PATCH 29/32] update taxonomic status validator error message --- frontend/src/shared/validators/species.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/shared/validators/species.ts b/frontend/src/shared/validators/species.ts index a6df06a0..e9a445d8 100755 --- a/frontend/src/shared/validators/species.ts +++ b/frontend/src/shared/validators/species.ts @@ -95,7 +95,7 @@ export const validateSpecies = ( name: 'Taxonomic Status', asString: (taxonomicStatus: string) => { if (!taxonStatusOptions.slice(1).includes(taxonomicStatus) && taxonomicStatus !== '') - return `Taxonomic Status must be one of the following: ${taxonStatusOptions.slice(1).join(', ')}, or empty.` + return `Taxonomic Status must be one of the following (or left empty): ${taxonStatusOptions.slice(1).join(', ')}.` return }, }, From 914585dd1174eab7564bd1886582b1c331285151 Mon Sep 17 00:00:00 2001 From: juhanikat Date: Sat, 27 Sep 2025 15:28:16 +0300 Subject: [PATCH 30/32] check that a synonym does not already exist as a species --- .../components/Species/Tabs/SynonymTab.tsx | 21 +++++++++++++++++++ frontend/src/util/taxonomyUtilities.tsx | 3 +++ 2 files changed, 24 insertions(+) diff --git a/frontend/src/components/Species/Tabs/SynonymTab.tsx b/frontend/src/components/Species/Tabs/SynonymTab.tsx index 914b5f12..8e8cd9ef 100755 --- a/frontend/src/components/Species/Tabs/SynonymTab.tsx +++ b/frontend/src/components/Species/Tabs/SynonymTab.tsx @@ -7,9 +7,13 @@ import { EditingForm, EditingFormField } from '@/components/DetailView/common/Ed import { useNotify } from '@/hooks/notification' import { validateSynonym } from '@/shared/validators/synonym' import { convertSynonymTaxonomyFields } from '@/util/taxonomyUtilities' +import { useGetAllSpeciesQuery } from '@/redux/speciesReducer' +import { skipToken } from '@reduxjs/toolkit/query' +import { CircularProgress } from '@mui/material' export const SynonymTab = () => { const { mode, setEditData, editData } = useDetailContext() + const { data: speciesQueryData, isFetching } = useGetAllSpeciesQuery(mode.read ? skipToken : undefined) const { notify } = useNotify() const convertAndCheckNewSynonymTaxonomy = (newSynonym: EditDataType) => { @@ -43,6 +47,21 @@ export const SynonymTab = () => { } } + for (const existingSpecies of speciesQueryData!) { + if ( + convertedSynonym.syn_genus_name === existingSpecies.genus_name && + convertedSynonym.syn_species_name === existingSpecies.species_name + ) { + notify( + `${convertedSynonym.syn_genus_name} ${convertedSynonym.syn_species_name} \ + has already been created as a separate species, adding it as a synonym is currently not allowed.`, + 'error', + null + ) + return false + } + } + return convertedSynonym } @@ -88,6 +107,8 @@ export const SynonymTab = () => { /> ) + if (isFetching) return + return ( <> diff --git a/frontend/src/util/taxonomyUtilities.tsx b/frontend/src/util/taxonomyUtilities.tsx index 73644ae4..4c226a66 100644 --- a/frontend/src/util/taxonomyUtilities.tsx +++ b/frontend/src/util/taxonomyUtilities.tsx @@ -19,6 +19,9 @@ const isDuplicateTaxon = (newSpecies: EditDataType, existingSpecies: Sp newSpecies.species_name === existingSpecies.species_name && newSpecies.unique_identifier === existingSpecies.unique_identifier ) { + console.log('joo') + console.log(newSpecies) + console.log(existingSpecies) return true } From bdab6ebc4c63d99808a087c78ca8d1dfbf33ba66 Mon Sep 17 00:00:00 2001 From: juhanikat Date: Tue, 30 Sep 2025 10:53:12 +0300 Subject: [PATCH 31/32] removed consolelogs --- frontend/src/util/taxonomyUtilities.tsx | 3 --- 1 file changed, 3 deletions(-) diff --git a/frontend/src/util/taxonomyUtilities.tsx b/frontend/src/util/taxonomyUtilities.tsx index 4c226a66..73644ae4 100644 --- a/frontend/src/util/taxonomyUtilities.tsx +++ b/frontend/src/util/taxonomyUtilities.tsx @@ -19,9 +19,6 @@ const isDuplicateTaxon = (newSpecies: EditDataType, existingSpecies: Sp newSpecies.species_name === existingSpecies.species_name && newSpecies.unique_identifier === existingSpecies.unique_identifier ) { - console.log('joo') - console.log(newSpecies) - console.log(existingSpecies) return true } From 2011f407c2fe482da4b29025116ebd672fdfff8e Mon Sep 17 00:00:00 2001 From: Kari Lintulaakso Date: Wed, 8 Oct 2025 13:43:04 +0300 Subject: [PATCH 32/32] Add DeepWiki documentation link to README Added link to DeepWiki documentation for NOW database. --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index aeb9c7d1..ddd5ac4f 100644 --- a/README.md +++ b/README.md @@ -40,3 +40,6 @@ The new version is not deployed yet. :mag_right: [Class diagram](documentation/class_diagram.md) See the relations of relevant tables. Notice that it omits some unimportant tables and does not show most columns. :earth_africa: [Map features](documentation/frontend/components/maps/README.md) Maps' functionality and development notes + +**DeepWiki documentation for the NOW database can be found here:** +[![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/nowcommunity/nowdatabase)