Merge branch 'main' into make-some-tables-unclickable

This commit is contained in:
Kari Lintulaakso
2025-10-13 16:02:24 +03:00
committed by GitHub
51 changed files with 848 additions and 430 deletions
+3
View File
@@ -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)
+1 -1
View File
@@ -45,7 +45,7 @@ export const newSpeciesBasis: EditDataType<SpeciesDetailsType & EditMetaData> =
genus_name: 'Petenyia',
species_name: 'dubiatest',
unique_identifier: '-',
taxonomic_status: null,
taxonomic_status: '',
common_name: null,
sp_author: 'Bachmayer & Wilson, 1970',
strain: null,
+2 -2
View File
@@ -132,8 +132,8 @@ router.get(`/export/:columnfilters/:sorting`, async (req, res) => {
})
res.attachment(`cross_search${currentDateAsString()}.csv`) // filename will get overwritten in frontend when fetching data from this route
// quoteColumns is needed to make sure linebreaks do not mess up the data
const stream = format({ headers: true, quoteColumns: true }).transform(
// this should match the csvConfig in frontend exportRows() function as closely as possible
const stream = format({ delimiter: ',', headers: true, quoteColumns: true }).transform(
transformFunction as FormatterRowTransformFunction<FormatterRow, FormatterRow>
)
+27 -8
View File
@@ -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<Prisma.now_loc> & EditMetaData) => {
const keys = Object.keys(editedFields)
const errors: ValidationObject[] = []
@@ -207,6 +217,17 @@ export const validateEntireLocality = async (editedFields: EditDataType<Prisma.n
const error = validateLocality(editedFields as EditDataType<LocalityDetailsType>, key as keyof LocalityDetailsType)
if (error.error) errors.push(error)
}
if ('now_ls' in editedFields) {
for (const species of editedFields.now_ls as Array<Editable<LocalitySpeciesDetailsType>>) {
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
+2 -3
View File
@@ -94,14 +94,13 @@ export const getSpeciesDetails = async (id: number) => {
now_sau: {
include: {
now_sr: {
select: {
include: {
ref_ref: {
select: {
include: {
ref_authors: true,
ref_journal: true,
},
},
rid: true,
},
},
},
+3 -3
View File
@@ -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') {
+1 -1
View File
@@ -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,
})
}
+164 -20
View File
@@ -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')
+12 -38
View File
@@ -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')
+4 -13
View File
@@ -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')
})
+16 -56
View File
@@ -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')
})
+26 -1
View File
@@ -27,7 +27,7 @@
Cypress.Commands.add('login', username => {
cy.clearLocalStorage()
cy.visit('/login')
cy.contains('Login').click()
cy.get('[data-cy="login-button"]', { timeout: 10000 }).should('be.visible').click()
cy.get('[data-cy="username-basic"]').type(username)
cy.get('[data-cy="password-basic"]').type('test')
cy.get('[data-cy="login-button"]').click()
@@ -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()
})
+1 -1
View File
@@ -1,3 +1,3 @@
### Cross Search Export
Cross search has a [`/export` route](../../backend/src/routes/crossSearch.ts) which is used by the ["Export Table"](../../../frontend/src/components/CrossSearch/CrossSearchExportButton.tsx) button in a cross-search table view. This route uses the same parameter parsing and validation function as the `/all` route, and it also uses the same SQL query, but with LIMIT set to undefined. The resulting data is [streamed](https://developer.mozilla.org/en-US/docs/Web/API/Streams_API) to the user, where the browser will download it as a csv file. See the "Exporting" section in [TableView docs](../frontend/components/TableView.md) for more information.
Cross search has a [`/export` route](../../backend/src/routes/crossSearch.ts) which is used by the ["Export Table"](../../frontend/src/components/CrossSearch/CrossSearchExportMenuItem.tsx) button in a cross-search table view. This route uses the same parameter parsing and validation function as the `/all` route, and it also uses the same SQL query, but with LIMIT set to undefined. The resulting data is [streamed](https://developer.mozilla.org/en-US/docs/Web/API/Streams_API) to the user, where the browser will download it as a csv file. See the "Exporting" section in [TableView docs](../frontend/components/TableView.md) for more information.
+9 -5
View File
@@ -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
@@ -265,4 +268,5 @@ Mandatory fields: title_primary OR gen_notes, date_primary, authors_primary, exa
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.
- 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.
+1 -1
View File
@@ -1,4 +1,4 @@
# Localities
# Species
## Required fields
+1 -1
View File
@@ -1,4 +1,4 @@
### Time Bound
### Time Bounds
A Time Bound can be given the following information:
+1 -1
View File
@@ -1,4 +1,4 @@
### Time Unit
### Time Units
A Time Unit can be given the following information:
@@ -14,7 +14,7 @@ export const CrossSearchExportMenuItem = ({ handleClose }: { handleClose: () =>
const token = useUser().token
const fetchOptions = token ? { headers: { Authorization: `Bearer ${token}` } } : {}
const filename = `cross_search${currentDateAsString()}.csv`
const filename = `locality-species-${currentDateAsString()}.csv`
const fetchCSVFile = async () => {
setLoading(true)
@@ -25,6 +25,7 @@ export const StagingView = <T extends MRT_RowData>() => {
<SelectingTable<Reference, T>
buttonText="Add existing reference"
data={data}
title="References"
isError={isError}
columns={referenceTableColumns}
idFieldName="rid"
@@ -22,7 +22,6 @@ export const ContactModal = ({
setOpen(false)
}
return <></>
return (
<Box>
<Button onClick={() => setOpen(true)} variant="outlined" startIcon={<EmailIcon />} className="button">
@@ -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 = <T extends object, ParentType extends object>({
dataCy,
@@ -19,6 +24,8 @@ export const EditingForm = <T extends object, ParentType extends object>({
editAction,
existingObject,
arrayFieldName,
replacedValues,
copyTaxonomyButton,
}: {
dataCy?: string
buttonText: string
@@ -26,15 +33,22 @@ export const EditingForm = <T extends object, ParentType extends object>({
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<ParentType>()
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 = <T extends object, ParentType extends object>({
return (
<EditingModal {...{ dataCy, buttonText, onSave }}>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: '1em' }}>
{copyTaxonomyButton}
{formFields.map(field => (
<TextField
key={field.name}
slotProps={{ inputLabel: { shrink: true } }}
{...register(field.name, {
required: field.required ? 'This field is required' : false,
...(field.type === 'number' && {
@@ -12,9 +12,11 @@ give an array in prop "selectedValues" that contains the string values
that already exist in editdata
*/
export const SelectingTable = <T extends MRT_RowData, ParentType extends object>({
dataCy,
buttonText,
data,
columns,
title,
editingAction,
tableRowAction,
fieldName,
@@ -23,9 +25,11 @@ export const SelectingTable = <T extends MRT_RowData, ParentType extends object>
isError,
useObject,
}: {
dataCy?: string
buttonText: string
data: T[] | undefined
columns: MRT_ColumnDef<T>[]
title: string
editingAction?: (object: T) => void
tableRowAction?: (row: T) => void
fieldName: keyof ParentType
@@ -69,11 +73,12 @@ export const SelectingTable = <T extends MRT_RowData, ParentType extends object>
if (isError) return <Box>Error fetching data for the selecting table.</Box>
if (!data) return <CircularProgress />
return (
<EditingModal buttonText={buttonText}>
<EditingModal buttonText={buttonText} dataCy={dataCy}>
{data ? (
<TableView<T>
data={filteredData}
columns={columns}
title={title}
isFetching={false}
visibleColumns={visibleColumns}
selectorFn={editingAction ?? defaultEditingAction}
@@ -1,4 +1,4 @@
import { ReferenceOfUpdate, UpdateLog } from '@/shared/types'
import { AnyReference, ReferenceOfUpdate, UpdateLog } from '@/shared/types'
import { useDetailContext } from '@/components/DetailView/Context/DetailContext'
import { EditingModal } from '@/components/DetailView/common/EditingModal'
import { SimpleTable } from '@/components/DetailView/common/SimpleTable'
@@ -18,9 +18,10 @@ const makeNameList = (names: Array<string | null>) => {
return names[0] ?? ''
}
const ReferenceList = ({ references, big }: { references: ReferenceOfUpdate[]; big: boolean }) => {
const ReferenceList = ({ references, big }: { references: AnyReference[]; big: boolean }) => {
const getReferenceText = (ref: ReferenceOfUpdate) => {
// php version: html/include/database.php -> referenceCitation()
//const ref = referenceOfUpdate.ref_ref
const authors = makeNameList(ref.ref_authors.map(author => author.author_surname))
const issue = ref.issue ? ` (${ref.issue}) ` : ''
if (ref.ref_type_id === 1) {
@@ -37,6 +38,7 @@ const ReferenceList = ({ references, big }: { references: ReferenceOfUpdate[]; b
// All other types
return `${authors} ${ref.date_primary ? `(${ref.date_primary}). ` : ''} ${ref.title_primary?.concat('. ') ?? ''}${ref.title_secondary?.concat('. ') ?? ''}${ref.title_series?.concat('. ') ?? ''}${ref.gen_notes?.concat('.') ?? ''}`
}
return (
<Stack>
{references.map(ref => (
@@ -44,7 +46,7 @@ const ReferenceList = ({ references, big }: { references: ReferenceOfUpdate[]; b
key={ref.rid}
sx={{ padding: '0.4em', margin: '0.5em', maxWidth: big ? '50em' : '30em', backgroundColor: 'lightblue' }}
>
<div style={{ wordWrap: 'break-word', whiteSpace: 'normal' }}>{getReferenceText(ref)}</div>
<div style={{ wordWrap: 'break-word', whiteSpace: 'normal' }}>{getReferenceText(ref.ref_ref)}</div>
{big && (
<div>
<Link to={`/reference/${ref.rid}`}>View</Link>
@@ -90,10 +92,7 @@ export const UpdateTab = <T, UpdateType extends MRT_RowData & { updates: UpdateL
accessorKey: refFieldName as string,
header: 'Reference',
Cell: ({ row }: { row: MRT_Row<UpdateType> }) => (
<ReferenceList
references={(row.original[refFieldName] as { ref_ref: ReferenceOfUpdate }[]).map(item => item.ref_ref)}
big={false}
/>
<ReferenceList references={row.original[refFieldName]} big={false} />
),
},
{
@@ -105,7 +104,7 @@ export const UpdateTab = <T, UpdateType extends MRT_RowData & { updates: UpdateL
coordinator={row.original[`${prefix}_coordinator`] as string}
updates={row.original.updates}
comment={row.original[`${prefix}_comment` as keyof UpdateType] as string}
references={row.original[refFieldName] as { ref_ref: ReferenceOfUpdate }[]}
references={row.original[refFieldName]}
/>
),
},
@@ -118,7 +117,7 @@ export const UpdateTab = <T, UpdateType extends MRT_RowData & { updates: UpdateL
)
}
const DetailsModal = <RefType extends { ref_ref: ReferenceOfUpdate }>({
const DetailsModal = ({
updates,
references,
comment,
@@ -127,7 +126,7 @@ const DetailsModal = <RefType extends { ref_ref: ReferenceOfUpdate }>({
coordinator,
}: {
updates: UpdateLog[]
references: RefType[]
references: AnyReference[]
comment: string
date: Date
authorizer: string
@@ -179,11 +178,7 @@ const DetailsModal = <RefType extends { ref_ref: ReferenceOfUpdate }>({
</Card>
<Divider />
<h3>References</h3>
{references.length === 0 ? (
<Box>Update has no references.</Box>
) : (
<ReferenceList references={references.map(ref => ref.ref_ref)} big />
)}
{references.length === 0 ? <Box>Update has no references.</Box> : <ReferenceList references={references} big />}
<Divider />
<h3>Changed database values</h3>
<SimpleTable columns={columns} data={updates} />
@@ -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,
@@ -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 = <T,>({
onWrite,
@@ -28,8 +29,8 @@ export const WriteButton = <T,>({
useDetailContext<T>()
const [loading, setLoading] = useState(false)
const { notify } = useNotify()
const [getSpeciesData] = useLazyGetAllSpeciesQuery()
const [getSynonyms] = useLazyGetAllSynonymsQuery()
const { data: speciesData } = useGetAllSpeciesQuery(!taxonomy ? skipToken : undefined)
const { data: synonymData } = useGetAllSynonymsQuery(!taxonomy ? skipToken : undefined)
// For coordinate out of bounds warning
const [warningModalState, setWarningModalState] = useState<OutOfBoundsWarningModalState>({
@@ -50,6 +51,11 @@ export const WriteButton = <T,>({
return !isPointInBoxes(dec_lat, dec_long, boundingBoxSplit(countryBoundingBoxes[country]))
}
useEffect(() => {
if (taxonomy && (!speciesData || !synonymData)) setLoading(true)
else setLoading(false)
}, [speciesData, synonymData])
/* validates all fields when entering new/editing mode
to disable write button when unvisited tabs have
validation errors (e.g. missing required fields) */
@@ -107,23 +113,21 @@ export const WriteButton = <T,>({
// @ts-expect-error Reason: Typescript doesn't recognise that references do exist. Unable to find a way around it. Fix if extra time
}, [mode, editData.references])
const writeWithTaxonomyCheck = async () => {
const writeWithTaxonomyCheck = () => {
let speciesEditData: EditDataType<Species> | undefined = undefined
if (!mode.staging) {
const { data: speciesData } = await getSpeciesData(undefined, true)
const { data: synonyms } = await getSynonyms(undefined, true)
if (!speciesData) {
notify('Could not fetch species to check taxonomy data.', 'error')
return
}
if (!synonyms) {
if (!synonymData) {
notify('Could not fetch synonyms to check taxonomy data.', 'error')
return
}
// converts taxonomy fields to capitalized/lowercased
speciesEditData = convertTaxonomyFields(editData as EditDataType<Species>)
const errors = checkTaxonomy(speciesEditData, speciesData, synonyms)
speciesEditData = convertSpeciesTaxonomyFields(editData as EditDataType<Species>)
const errors = checkSpeciesTaxonomy(speciesEditData, speciesData, synonymData)
if (errors.size > 0) {
const errorMessage = [...errors].reduce((acc, currentError) => acc + `\n${currentError}`)
notify(errorMessage, 'error', null)
@@ -144,10 +148,10 @@ export const WriteButton = <T,>({
})
}
const handleWriteButtonClick = async () => {
const handleWriteButtonClick = () => {
if (taxonomy) {
setLoading(true)
await writeWithTaxonomyCheck()
writeWithTaxonomyCheck()
setLoading(false)
return
}
@@ -186,13 +190,13 @@ export const WriteButton = <T,>({
!('dec_long' in (editData as object)) ||
!('country' in (editData as object))
) {
void handleWriteButtonClick()
handleWriteButtonClick()
return
}
const localityObject = editData as unknown as { dec_lat: number; dec_long: number; country: string }
if (!isCoordinateOutOfBounds(localityObject.dec_lat, localityObject.dec_long, localityObject.country)) {
void handleWriteButtonClick()
handleWriteButtonClick()
return
}
@@ -9,6 +9,7 @@ import { generateKml } from '@/util/kml'
import { generateSvg } from '../Map/generateSvg'
import { usePageContext } from '../Page'
import { LocalitySynonymsModal } from './LocalitySynonymsModal'
import { currentDateAsString } from '@/shared/currentDateAsString'
const decimalCount = (num: number) => {
const numAsString = num.toString()
@@ -403,7 +404,8 @@ export const LocalityTable = ({ selectorFn }: { selectorFn?: (newObject: Localit
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = 'localitiesWithSpecies.csv'
a.download = `localities-with-species-${currentDateAsString()}.csv`
a.click()
}
@@ -416,7 +418,7 @@ export const LocalityTable = ({ selectorFn }: { selectorFn?: (newObject: Localit
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = 'localities.kml'
a.download = `localities-${currentDateAsString()}.kml`
a.click()
}
@@ -427,7 +429,7 @@ export const LocalityTable = ({ selectorFn }: { selectorFn?: (newObject: Localit
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = 'map.svg'
a.download = `localities-map-${currentDateAsString()}.svg`
a.click()
}
@@ -192,6 +192,7 @@ export const LithologyTab = () => {
isError={isError}
columns={selectingTableColumns}
data={sedimentaryStructuresData}
title="Sedimentary structures"
fieldName="now_ss"
idFieldName="ss_value"
editingAction={(newSed: SedimentaryStructureValues) => {
@@ -38,6 +38,7 @@ export const MuseumTab = () => {
<SelectingTable<Museum, LocalityDetailsType>
buttonText="Select Museum"
data={museumData}
title="Museums"
isError={isError}
columns={columns}
fieldName="now_mus"
@@ -43,6 +43,7 @@ export const ProjectTab = () => {
<SelectingTable<Project, LocalityDetailsType>
buttonText="Select Project"
data={projectData}
title="Projects"
columns={columns}
isError={isError}
idFieldName="pid"
@@ -1,17 +1,105 @@
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<LocalityDetailsType>()
const { data: speciesData, isError } = useGetAllSpeciesQuery(mode.read ? skipToken : undefined)
const { notify } = useNotify()
const [replacedValues, setReplacedValues] = useState<EditDataType<Species> | undefined>()
const [selectedSpecies, setSelectedSpecies] = useState<string | undefined>()
const [modalOpen, setModalOpen] = useState<boolean>(false)
const handleRowActionClick = (row: Species) => {
setSelectedSpecies(row.species_id.toString())
setModalOpen(true)
}
const copyTaxonomyButton = (
<Box key="copy_existing_taxonomy_button">
<SelectingTable<Species, Species>
dataCy="copy_existing_taxonomy_button"
buttonText="Copy existing taxonomy"
title="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: '',
})
}}
/>
<SynonymsModal open={modalOpen} onClose={() => setModalOpen(false)} selectedSpecies={selectedSpecies} />
</Box>
)
const convertAndCheckNewSpeciesTaxonomy = (newSpecies: EditDataType<Species>) => {
const errors = []
for (const field in newSpecies) {
const errorObject = validateSpecies(
newSpecies as unknown as EditDataType<SpeciesDetailsType>,
field as unknown as keyof EditDataType<SpeciesDetailsType>
)
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<Species>[]
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<Species>[] = [
{
@@ -80,6 +168,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 +184,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 <CircularProgress />
return (
<Grouped title="Species">
{!mode.read && (
<Box display="flex" gap={1}>
<EditingForm<Species, LocalityDetailsType>
<EditingForm<EditDataType<Species>, LocalityDetailsType>
buttonText="Add new Species"
formFields={formFields}
editAction={(newSpecies: Species) => {
existingObject={{ unique_identifier: '-' }}
replacedValues={replacedValues}
copyTaxonomyButton={copyTaxonomyButton}
editAction={(newSpecies: EditDataType<Species>) => {
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',
},
],
@@ -117,6 +223,7 @@ export const SpeciesTab = () => {
<SelectingTable<Species, LocalityDetailsType>
buttonText="Select Species"
data={speciesData}
title="Species"
isError={isError}
columns={speciesColumns}
fieldName="now_ls"
@@ -129,7 +236,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',
},
],
@@ -64,7 +64,7 @@ export const MuseumTable = ({ selectorFn }: { selectorFn?: (museum: Museum) => v
return (
<TableView<Museum>
title="Museum"
title="Museums"
selectorFn={selectorFn}
idFieldName="museum"
columns={columns}
@@ -124,6 +124,7 @@ export const AuthorTab: React.FC<AuthorTabProps> = ({ field_num_param, tab_name
<SelectingTable<ReferenceAuthorType, ReferenceDetailsType>
buttonText={`Select ${buttonText}`}
data={authorData}
title="Authors"
isError={isError}
columns={authorColumns}
fieldName="ref_authors"
@@ -88,6 +88,7 @@ export const JournalTab: React.FC<JournalTabProps> = ({ tab_name = 'Journal' })
<SelectingTable<ReferenceJournalType, ReferenceDetailsType>
buttonText="Select Journal"
data={journalData}
title="Journals"
isError={isError}
columns={journalColumns}
fieldName="ref_journal"
@@ -106,6 +106,7 @@ export const CoordinatorTab = () => {
<SelectingTable<PersonDetailsType, RegionDetails>
buttonText="Select Coordinator"
data={personsData}
title="Coordinators"
isError={isError}
columns={personTableColumns}
fieldName="now_reg_coord_people"
@@ -31,6 +31,7 @@ export const SequenceTable = ({ selectorFn }: { selectorFn?: (id: Sequence) => v
selectorFn={selectorFn}
idFieldName="sequence"
columns={columns}
title="Sequences"
isFetching={isFetching}
visibleColumns={visibleColumns}
clickableRows={false}
@@ -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()
@@ -24,7 +25,7 @@ export const SpeciesDetails = () => {
document.title = 'New species'
}
const [editSpeciesRequest, { isLoading: mutationLoading }] = useEditSpeciesMutation()
const { isError, isFetching, data } = useGetSpeciesDetailsQuery(id!, { skip: isNew })
const { isError, isFetching, data } = useGetSpeciesDetailsQuery(encodeURIComponent(id!), { skip: isNew })
const { notify } = useNotify()
const navigate = useNavigate()
const [deleteMutation, { isSuccess: deleteSuccess, isError: deleteError }] = useDeleteSpeciesMutation()
@@ -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])
@@ -50,6 +50,7 @@ export const LocalityTab = () => {
<SelectingTable<Editable<Locality>, SpeciesDetailsType>
buttonText="Select Locality"
columns={columns}
title="Localities"
isError={isError}
data={localitiesData}
fieldName="now_ls"
@@ -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<SpeciesDetailsType>()
const { data: speciesQueryData, isFetching } = useGetAllSpeciesQuery(mode.read ? skipToken : undefined)
const { notify } = useNotify()
const convertAndCheckNewSynonymTaxonomy = (newSynonym: EditDataType<SpeciesSynonym>) => {
const errors = []
for (const field in newSynonym) {
const errorObject = validateSynonym(
newSynonym as unknown as EditDataType<SpeciesSynonym>,
field as unknown as keyof EditDataType<SpeciesSynonym>
)
const { error } = errorObject
if (error) errors.push(errorObject)
}
if (errors.length > 0) {
notify('Following validators failed: \n' + errors.map(e => `${e.name}: ${e.error}`).join('\n'), 'error')
return false
}
const convertedSynonym = convertSynonymTaxonomyFields(newSynonym)
for (const existingSynonym of editData.com_taxa_synonym) {
if (
convertedSynonym.syn_genus_name === existingSynonym.syn_genus_name &&
convertedSynonym.syn_species_name === existingSynonym.syn_species_name
) {
notify(
`${convertedSynonym.syn_genus_name} ${convertedSynonym.syn_species_name} \
has already been added as a synonym for this species.`,
'error',
null
)
return false
}
}
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<SpeciesSynonym>[] = [
{
@@ -31,15 +88,17 @@ export const SynonymTab = () => {
const editingForm = (
<EditingForm<Species, SpeciesDetailsType>
buttonText="Add new Species"
buttonText="Add synonym"
formFields={formFields}
editAction={(newSynonym: EditDataType<SpeciesSynonym>) => {
const convertedSynonym = convertAndCheckNewSynonymTaxonomy(newSynonym)
if (!convertedSynonym) return
setEditData({
...editData,
com_taxa_synonym: [
...editData.com_taxa_synonym,
{
...newSynonym,
...convertedSynonym,
rowState: 'new',
},
],
@@ -48,6 +107,8 @@ export const SynonymTab = () => {
/>
)
if (isFetching) return <CircularProgress />
return (
<>
<Grouped title="Synonyms">
@@ -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<SpeciesDetailsType>()
@@ -21,10 +22,12 @@ export const TaxonomyTab = () => {
}
const copyTaxonomyButton = (
<Box key="copy_existing_taxonomy_button" id="copy_existing_taxonomy_button">
<Box key="copy_existing_taxonomy_button">
<SelectingTable<Species, Species>
dataCy="copy_existing_taxonomy_button"
buttonText="Copy existing taxonomy"
data={speciesQueryData}
title="Species"
isError={isError}
columns={smallSpeciesTableColumns}
fieldName="order_name" // this doesn't do anything here but is required
@@ -32,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!,
})
}}
/>
@@ -49,16 +53,6 @@ export const TaxonomyTab = () => {
</Box>
)
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',
@@ -12,6 +12,7 @@ import { usePageContext } from '../Page'
export const TableToolBar = <T extends MRT_RowData>({
table,
tableName,
combinedExport,
kmlExport,
svgExport,
@@ -21,6 +22,7 @@ export const TableToolBar = <T extends MRT_RowData>({
showNewButton,
}: {
table: MRT_TableInstance<T>
tableName: string
combinedExport?: (lids: number[]) => Promise<void>
kmlExport?: (table: MRT_TableInstance<T>) => void
svgExport?: (table: MRT_TableInstance<T>) => void
@@ -87,7 +89,7 @@ export const TableToolBar = <T extends MRT_RowData>({
) : (
<MenuItem
onClick={() => {
exportRows(table)
exportRows(table, tableName)
handleClose()
}}
>
@@ -57,10 +57,9 @@ export const TableView = <T extends MRT_RowData>({
idFieldName: keyof T
checkRowRestriction?: (row: T) => boolean
selectorFn?: (id: T) => void
// here
tableRowAction?: (row: T) => void
url?: string
title?: string
title: string
combinedExport?: (lids: number[]) => Promise<void>
kmlExport?: (table: MRT_TableInstance<T>) => void
svgExport?: (table: MRT_TableInstance<T>) => void
@@ -259,6 +258,7 @@ export const TableView = <T extends MRT_RowData>({
{title && <Box className="title">{title}</Box>}
<TableToolBar<T>
table={table}
tableName={title}
combinedExport={combinedExport}
kmlExport={kmlExport}
svgExport={svgExport}
@@ -1,15 +1,17 @@
import { MRT_RowData, MRT_TableInstance } from 'material-react-table'
import { mkConfig, generateCsv, download } from 'export-to-csv'
import { AcceptedData } from 'node_modules/export-to-csv/output/lib/types'
import { currentDateAsString } from '@/shared/currentDateAsString'
export type ExportFn<T> = (data: T) => { [k: string]: AcceptedData }
export const exportRows = <T extends MRT_RowData>(table: MRT_TableInstance<T>) => {
export const exportRows = <T extends MRT_RowData>(table: MRT_TableInstance<T>, tableName: string) => {
const csvConfig = mkConfig({
fieldSeparator: ',',
decimalSeparator: '.',
useKeysAsHeaders: true,
quoteStrings: true, // If true, will wrap all values in double quotes
filename: `${tableName.toLowerCase()}-${currentDateAsString()}`,
})
const rowData = table.getPrePaginationRowModel().rows.map(row => {
const r = {} as Record<string, AcceptedData>
@@ -11,6 +11,15 @@ export const LocalityTab = () => {
if (isError) return 'Error loading Localities.'
if (!localitiesData) return <CircularProgress />
// changes min and max age values to checkmarks or X symbols, to show whether this time unit is set as the basis for min/max age or not
const modifiedData = localitiesData.map(locality => {
return {
...locality,
min_age: locality.bfa_min === data.tu_name ? '\u2713' : '\u0058',
max_age: locality.bfa_max === data.tu_name ? '\u2713' : '\u0058',
}
})
const columns = [
{
accessorKey: 'loc_name',
@@ -30,5 +39,5 @@ export const LocalityTab = () => {
},
]
return <SimpleTable columns={columns} data={localitiesData} idFieldName="lid" url="locality" />
return <SimpleTable columns={columns} data={modifiedData} idFieldName="lid" url="locality" />
}
+1
View File
@@ -57,6 +57,7 @@ export const api = createApi({
'localities',
'species',
'specieslist',
'speciessynonyms',
'reference',
'references',
'timeunits',
+2 -1
View File
@@ -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<void, number>({
query: id => ({
+6 -6
View File
@@ -7,18 +7,19 @@ const speciesApi = api.injectEndpoints({
query: () => ({
url: `/species/all`,
}),
providesTags: result => (result ? [{ type: 'specieslist' }] : []),
providesTags: result => (result ? [{ type: 'specieslist' }, 'speciessynonyms'] : []),
}),
getAllSynonyms: builder.query<SpeciesSynonym[], void>({
query: () => ({
url: `/species/synonyms`,
}),
providesTags: result => (result ? ['speciessynonyms'] : []),
}),
getSpeciesDetails: builder.query<SpeciesDetailsType, string>({
query: id => ({
url: `/species/${id}`,
}),
providesTags: result => (result ? [{ type: 'species', id: result.species_id }] : []),
providesTags: result => (result ? [{ type: 'species', id: result.species_id }, 'speciessynonyms'] : []),
}),
editSpecies: builder.mutation<SpeciesDetailsType, EditDataType<SpeciesDetailsType>>({
query: species => ({
@@ -27,7 +28,7 @@ const speciesApi = api.injectEndpoints({
body: { species },
}),
invalidatesTags: (result, _error, { species_id }) =>
result ? [{ type: 'species', id: species_id }, 'specieslist'] : [],
result ? [{ type: 'species', id: species_id }, 'specieslist', 'speciessynonyms'] : [],
}),
deleteSpecies: builder.mutation<void, number>({
query: id => ({
@@ -36,15 +37,14 @@ const speciesApi = api.injectEndpoints({
}),
invalidatesTags: (result, _error, species_id) =>
typeof result !== 'undefined' ? [{ type: 'species', id: species_id }, 'specieslist'] : [],
typeof result !== 'undefined' ? [{ type: 'species', id: species_id }, 'specieslist', 'speciessynonyms'] : [],
}),
}),
})
export const {
useGetAllSpeciesQuery,
useLazyGetAllSpeciesQuery,
useLazyGetAllSynonymsQuery,
useGetAllSynonymsQuery,
useGetSpeciesDetailsQuery,
useEditSpeciesMutation,
useDeleteSpeciesMutation,
+16
View File
@@ -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',
]
+7 -2
View File
@@ -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<RegionCoordinator> } & {
@@ -443,8 +444,12 @@ export type ReferenceOfUpdate = Prisma.ref_ref & {
ref_journal: Prisma.ref_journal
}
export type LocalityReference = Prisma.now_lr & ReferenceOfUpdate
export type SpeciesReference = Prisma.now_sr & ReferenceOfUpdate
export type LocalityReference = Prisma.now_lr & { ref_ref: ReferenceOfUpdate }
export type SpeciesReference = Prisma.now_sr & { ref_ref: ReferenceOfUpdate }
export type TimeUnitReference = Prisma.now_tr & { ref_ref: ReferenceOfUpdate }
export type TimeBoundReference = Prisma.now_br & { ref_ref: ReferenceOfUpdate }
export type AnyReference = LocalityReference | SpeciesReference | TimeUnitReference | TimeBoundReference
export type ReferenceJournalType = {
journal_id?: number
+10
View File
@@ -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,
+31
View File
@@ -0,0 +1,31 @@
import { EditDataType, SpeciesSynonym } from '../types'
import { Validators, validator } from './validator'
export const validateSynonym = (
editData: EditDataType<SpeciesSynonym>,
fieldName: keyof EditDataType<SpeciesSynonym>
) => {
const validators: Validators<Partial<EditDataType<SpeciesSynonym>>> = {
syn_genus_name: {
name: 'Synonym genus name',
required: true,
asString: (synGenusName: string) => {
if (synGenusName.indexOf(' ') !== -1) return 'Genus must not contain any spaces.'
return
},
},
syn_species_name: {
name: 'Synonym species name',
required: true,
asString: (synSpeciesName: string) => {
if (synSpeciesName !== 'indet.' && editData.syn_genus_name === 'indet.')
return 'when the Genus is indet., Species must also be indet.'
if (synSpeciesName !== 'sp.' && editData.syn_genus_name === 'gen.')
return 'when the Genus is gen., Species must be sp.'
return
},
},
}
return validator<EditDataType<SpeciesSynonym>>(validators, editData, fieldName)
}
@@ -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<Species>, 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<Species>, existingSpecies:
)
}
export const checkTaxonomy = (
export const checkSpeciesTaxonomy = (
newSpecies: EditDataType<Species>,
existingSpeciesArray: Species[],
synonyms: SpeciesSynonym[]
@@ -314,7 +317,7 @@ const convertTaxonomyField = (value: string | null | undefined) => {
return convertedValue
}
export const convertTaxonomyFields = (species: EditDataType<Species>) => {
export const convertSpeciesTaxonomyFields = (species: EditDataType<Species>) => {
const speciesName = species.species_name
let lowercasedSpeciesName = ''
if (speciesName) {
@@ -336,3 +339,31 @@ export const convertTaxonomyFields = (species: EditDataType<Species>) => {
}
return convertedSpecies as EditDataType<Species>
}
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<SpeciesSynonym>) => {
const synSpeciesName = synonym.syn_species_name
let lowercasedSynSpeciesName = ''
if (synSpeciesName) {
if (synSpeciesName !== 'indet.') {
lowercasedSynSpeciesName = synSpeciesName.toLowerCase()
} else {
lowercasedSynSpeciesName = synSpeciesName
}
}
const convertedSynonym = {
...synonym,
syn_genus_name: convertTaxonomyField(synonym.syn_genus_name),
syn_species_name: lowercasedSynSpeciesName,
}
return convertedSynonym as EditDataType<SpeciesSynonym>
}