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) 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/backend/src/services/locality.ts b/backend/src/services/locality.ts index 25878923..d1f20be8 100644 --- a/backend/src/services/locality.ts +++ b/backend/src/services/locality.ts @@ -1,11 +1,20 @@ -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' 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) => { @@ -200,6 +209,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 +217,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) @@ -251,12 +272,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, 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 }) return updatedLocalityNow_ls 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, }) } diff --git a/cypress/e2e/locality.cy.js b/cypress/e2e/locality.cy.js index 3b25271b..85d5a682 100644 --- a/cypress/e2e/locality.cy.js +++ b/cypress/e2e/locality.cy.js @@ -1,3 +1,162 @@ +before('Reset database', () => { + cy.request(Cypress.env('databaseResetUrl')) +}) + +describe('Adding species in Locality -> Species tab for an existing locality', () => { + beforeEach('Login as admin', () => { + cy.login('testSu') + }) + + 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() + cy.contains('Edited item successfully.') + cy.visit(`/locality/20920?tab=2`) + cy.contains('somespecies') + }) + + 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('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`) + 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('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.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('-') + cy.contains('Save').click() + + cy.addReferenceAndSave() + cy.contains('Edited item successfully.') + cy.visit(`/locality/20920?tab=2`) + cy.contains('Lantian-Shuijiazui') + cy.get('[id=edit-button]').click() + cy.contains('Neworder') + + // adding species that has been added earlier through "add new species" button + cy.contains('Add new Species').click() + 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('-') + cy.contains('Save').click() + cy.contains('The taxon already exists in the database.') + }) + + 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`) + 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', () => { beforeEach('Login as admin', () => { cy.login('testSu') @@ -30,13 +189,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') @@ -150,15 +304,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') @@ -314,11 +461,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/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/cypress/support/commands.js b/cypress/support/commands.js index 04316378..f18eb987 100644 --- a/cypress/support/commands.js +++ b/cypress/support/commands.js @@ -42,3 +42,28 @@ 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() +}) + +Cypress.Commands.add('clearNewSpeciesForm', () => { + cy.get('[name=order_name]').clear() + cy.get('[name=family_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() + cy.get('[name=sp_author]').clear() +}) 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: diff --git a/frontend/src/components/DetailView/common/EditingForm.tsx b/frontend/src/components/DetailView/common/EditingForm.tsx index c0ba3d55..46217c72 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, 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 => ( ({ + dataCy, buttonText, data, columns, @@ -24,6 +25,7 @@ export const SelectingTable = isError, useObject, }: { + dataCy?: string buttonText: string data: T[] | undefined columns: MRT_ColumnDef[] @@ -71,7 +73,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/DetailView/common/defaultValues.ts b/frontend/src/components/DetailView/common/defaultValues.ts index a3ea4a59..dfad421b 100755 --- a/frontend/src/components/DetailView/common/defaultValues.ts +++ b/frontend/src/components/DetailView/common/defaultValues.ts @@ -119,7 +119,7 @@ export const emptyLocality = { } as unknown as LocalityDetailsType export const emptySpecies = { - class_name: '', + class_name: 'Mammalia', order_name: '', family_name: '', subclass_or_superorder_name: '', @@ -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/DetailView/components.tsx b/frontend/src/components/DetailView/components.tsx index b6a17ce8..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/taxonomyFunctions' -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 c3335f5c..ed952912 100755 --- a/frontend/src/components/Locality/Tabs/SpeciesTab.tsx +++ b/frontend/src/components/Locality/Tabs/SpeciesTab.tsx @@ -1,17 +1,104 @@ -import { 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' 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 { + checkSpeciesTaxonomy, + convertSpeciesTaxonomyFields, + fixNullValuesInTaxonomyFields, +} from '@/util/taxonomyUtilities' +import { useNotify } from '@/hooks/notification' +import { validateSpecies } from '@/shared/validators/species' +import { smallSpeciesTableColumns } from '@/common' +import { useState } from 'react' +import { SynonymsModal } from '@/components/Species/SynonymsModal' export const SpeciesTab = () => { const { mode, editData, setEditData } = useDetailContext() const { data: speciesData, isError } = useGetAllSpeciesQuery(mode.read ? skipToken : undefined) + const { notify } = useNotify() + const [replacedValues, setReplacedValues] = useState | undefined>() + const [selectedSpecies, setSelectedSpecies] = useState() + const [modalOpen, setModalOpen] = useState(false) + + const handleRowActionClick = (row: Species) => { + setSelectedSpecies(row.species_id.toString()) + setModalOpen(true) + } + + const copyTaxonomyButton = ( + + + dataCy="copy_existing_taxonomy_button" + 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) => { + 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!, + taxonomic_status: '', + sp_comment: '', + sp_author: '', + }) + }} + /> + setModalOpen(false)} selectedSpecies={selectedSpecies} /> + + ) + + const convertAndCheckNewSpeciesTaxonomy = (newSpecies: EditDataType) => { + 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 = 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 = checkSpeciesTaxonomy(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[] = [ { @@ -80,6 +167,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 }, @@ -88,26 +183,36 @@ 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' }, + { name: 'sp_author', label: 'Author' }, ] + if (!mode.read && !speciesData) return + return ( {!mode.read && ( - + , LocalityDetailsType> buttonText="Add new Species" formFields={formFields} - editAction={(newSpecies: Species) => { + existingObject={{ unique_identifier: '-' }} + replacedValues={replacedValues} + copyTaxonomyButton={copyTaxonomyButton} + editAction={(newSpecies: EditDataType) => { + const convertedSpecies = convertAndCheckNewSpeciesTaxonomy(newSpecies) + if (!convertedSpecies) return setEditData({ ...editData, now_ls: [ ...editData.now_ls, { lid: editData.lid, - species_id: newSpecies.species_id, - com_species: { ...(newSpecies as unknown as SpeciesDetailsType) }, + species_id: undefined, + com_species: { ...(convertedSpecies as unknown as SpeciesDetailsType), class_name: 'Mammalia' }, rowState: 'new', }, ], @@ -130,7 +235,9 @@ export const SpeciesTab = () => { { lid: editData.lid, species_id: newSpecies.species_id, - com_species: { ...(newSpecies as unknown as SpeciesDetailsType) }, + com_species: { + ...(fixNullValuesInTaxonomyFields(newSpecies) as SpeciesDetailsType), + }, rowState: 'new', }, ], diff --git a/frontend/src/components/Species/SpeciesDetails.tsx b/frontend/src/components/Species/SpeciesDetails.tsx index c2ed6f33..cd969a09 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/taxonomyUtilities' export const SpeciesDetails = () => { const { id } = useParams() @@ -40,15 +41,11 @@ 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) { - 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/components/Species/Tabs/SynonymTab.tsx b/frontend/src/components/Species/Tabs/SynonymTab.tsx index 560e7317..8e8cd9ef 100755 --- a/frontend/src/components/Species/Tabs/SynonymTab.tsx +++ b/frontend/src/components/Species/Tabs/SynonymTab.tsx @@ -4,9 +4,66 @@ 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' +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) => { + 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 + } + } + + 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 + } const columns: MRT_ColumnDef[] = [ { @@ -31,15 +88,17 @@ export const SynonymTab = () => { const editingForm = ( - buttonText="Add new Species" + buttonText="Add synonym" 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', }, ], @@ -48,6 +107,8 @@ export const SynonymTab = () => { /> ) + if (isFetching) return + return ( <> diff --git a/frontend/src/components/Species/Tabs/TaxonomyTab.tsx b/frontend/src/components/Species/Tabs/TaxonomyTab.tsx index 043a84f5..1f641701 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 { emptyOption } from '@/components/DetailView/common/misc' +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() @@ -21,8 +22,9 @@ export const TaxonomyTab = () => { } const copyTaxonomyButton = ( - + + dataCy="copy_existing_taxonomy_button" buttonText="Copy existing taxonomy" data={speciesQueryData} title="Species" @@ -33,16 +35,17 @@ export const TaxonomyTab = () => { useObject={true} tableRowAction={handleRowActionClick} editingAction={(selectedSpecies: Species) => { + const fixedSpecies = fixNullValuesInTaxonomyFields(selectedSpecies) 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 ?? '', + 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!, }) }} /> @@ -50,16 +53,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/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/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 => ({ 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/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/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/shared/validators/species.ts b/frontend/src/shared/validators/species.ts index 8774c6ca..e9a445d8 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' @@ -80,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.') @@ -89,6 +91,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 (or left empty): ${taxonStatusOptions.slice(1).join(', ')}.` + return + }, + }, unique_identifier: { name: 'Unique Identifier', required: true, 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/taxonomyFunctions.tsx b/frontend/src/util/taxonomyUtilities.tsx similarity index 89% rename from frontend/src/util/taxonomyFunctions.tsx rename to frontend/src/util/taxonomyUtilities.tsx index 5bdc259f..73644ae4 100644 --- a/frontend/src/util/taxonomyFunctions.tsx +++ b/frontend/src/util/taxonomyUtilities.tsx @@ -1,4 +1,4 @@ -import { EditDataType, Species, SpeciesSynonym } from '@/shared/types' +import { EditDataType, Species, SpeciesDetailsType, SpeciesSynonym } from '@/shared/types' const generateMultipleParentsError = ( invalidField: string, @@ -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 @@ -133,7 +136,7 @@ const checkSubFamilyGenus = (newSpecies: EditDataType, existingSpecies: ) } -export const checkTaxonomy = ( +export const checkSpeciesTaxonomy = ( newSpecies: EditDataType, existingSpeciesArray: Species[], synonyms: SpeciesSynonym[] @@ -314,7 +317,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) { @@ -336,3 +339,31 @@ export const convertTaxonomyFields = (species: EditDataType) => { } return convertedSpecies as EditDataType } + +export const fixNullValuesInTaxonomyFields = (species: Species | 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 ?? '', + taxonomic_status: species.taxonomic_status ?? '', + } +} + +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 +}