Merge pull request #1154 from nowcommunity/codex/species-taxonomy-suggestions

[codex] Add species taxonomy suggestions
This commit is contained in:
Kari Lintulaakso
2026-05-19 14:59:37 +03:00
committed by GitHub
4 changed files with 144 additions and 17 deletions
@@ -7,14 +7,35 @@ 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 { useMemo, useState } from 'react'
import { taxonStatusOptions } from '@/shared/taxonStatusOptions'
import { TaxonomySuggestionField } from '../TaxonomySuggestionField'
import {
buildTaxonomySuggestionOptions,
TaxonomySuggestionField as TaxonomySuggestionFieldName,
} from '../taxonomySuggestions'
export const TaxonomyTab = () => {
const { textField, dropdown, bigTextField, editData, setEditData, mode } = useDetailContext<SpeciesDetailsType>()
const { data: speciesQueryData, isError } = useGetAllSpeciesQuery()
const [selectedSpecies, setSelectedSpecies] = useState<string | undefined>()
const [modalOpen, setModalOpen] = useState<boolean>(false)
const taxonomySuggestionOptions = useMemo(
() => ({
subclass_or_superorder_name: buildTaxonomySuggestionOptions(speciesQueryData, 'subclass_or_superorder_name'),
order_name: buildTaxonomySuggestionOptions(speciesQueryData, 'order_name'),
suborder_or_superfamily_name: buildTaxonomySuggestionOptions(speciesQueryData, 'suborder_or_superfamily_name'),
family_name: buildTaxonomySuggestionOptions(speciesQueryData, 'family_name'),
subfamily_name: buildTaxonomySuggestionOptions(speciesQueryData, 'subfamily_name'),
genus_name: buildTaxonomySuggestionOptions(speciesQueryData, 'genus_name'),
species_name: buildTaxonomySuggestionOptions(speciesQueryData, 'species_name'),
}),
[speciesQueryData]
)
const taxonomySuggestionField = (field: TaxonomySuggestionFieldName) => (
<TaxonomySuggestionField field={field} options={taxonomySuggestionOptions[field]} />
)
const handleRowActionClick = (row: Species) => {
setSelectedSpecies(row.species_id.toString())
@@ -55,28 +76,18 @@ export const TaxonomyTab = () => {
)
const classification = [
[
'Class',
'Mammalia',
'Subclass or Superorder',
textField('subclass_or_superorder_name', { type: 'text', trim: true }),
],
['Class', 'Mammalia', 'Subclass or Superorder', taxonomySuggestionField('subclass_or_superorder_name')],
[
'Order',
textField('order_name', { type: 'text', trim: true }),
taxonomySuggestionField('order_name'),
'Suborder or Superfamily',
textField('suborder_or_superfamily_name', { type: 'text', trim: true }),
taxonomySuggestionField('suborder_or_superfamily_name'),
],
[
'Family',
textField('family_name', { type: 'text', trim: true }),
'Subfamily or Tribe',
textField('subfamily_name', { type: 'text', trim: true }),
],
['Genus', textField('genus_name', { type: 'text', trim: true })],
['Family', taxonomySuggestionField('family_name'), 'Subfamily or Tribe', taxonomySuggestionField('subfamily_name')],
['Genus', taxonomySuggestionField('genus_name')],
[
'Species',
textField('species_name', { type: 'text', trim: true }),
taxonomySuggestionField('species_name'),
'Unique Identifier',
textField('unique_identifier', { type: 'text', trim: true }),
],
@@ -0,0 +1,59 @@
import { TextField } from '@mui/material'
import { useEffect } from 'react'
import { useDetailContext } from '@/components/DetailView/Context/DetailContext'
import { checkFieldErrors } from '@/components/DetailView/common/checkFieldErrors'
import { DataValue } from '@/components/DetailView/common/tabLayoutHelpers'
import type { EditDataType, SpeciesDetailsType } from '@/shared/types'
import type { TaxonomySuggestionField as TaxonomySuggestionFieldName } from './taxonomySuggestions'
const FIELD_WIDTH = '14em'
type TaxonomySuggestionFieldProps = {
field: TaxonomySuggestionFieldName
options: string[]
}
export const TaxonomySuggestionField = ({ field, options }: TaxonomySuggestionFieldProps) => {
const { editData, setEditData, validator, fieldsWithErrors, setFieldsWithErrors } =
useDetailContext<SpeciesDetailsType>()
const errorObject = validator(editData, field)
const { error } = errorObject
const suggestionListId = `${field}-taxonomy-suggestions`
const updateField = (value: string) => {
const nextEditData: EditDataType<SpeciesDetailsType> = { ...editData, [field]: value }
setEditData(nextEditData)
const nextErrorObject = validator(nextEditData, field)
checkFieldErrors(field, nextErrorObject, fieldsWithErrors, setFieldsWithErrors)
}
useEffect(() => {
checkFieldErrors(field, errorObject, fieldsWithErrors, setFieldsWithErrors)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [errorObject.error, errorObject.name])
const editingComponent = (
<>
<TextField
id={`${field}-textfield`}
inputProps={{ list: suggestionListId }}
value={editData[field] ?? ''}
onChange={event => updateField(event.currentTarget.value)}
variant="outlined"
size="small"
error={!!error}
helperText={error ?? ''}
onBlur={event => updateField(event.currentTarget.value.trim())}
sx={{ width: FIELD_WIDTH }}
/>
<datalist id={suggestionListId}>
{options.map(option => (
<option key={option} value={option} />
))}
</datalist>
</>
)
return <DataValue<SpeciesDetailsType> field={field} EditElement={editingComponent} />
}
@@ -0,0 +1,30 @@
import type { Species } from '@/shared/types'
export type TaxonomySuggestionField =
| 'subclass_or_superorder_name'
| 'order_name'
| 'suborder_or_superfamily_name'
| 'family_name'
| 'subfamily_name'
| 'genus_name'
| 'species_name'
export const buildTaxonomySuggestionOptions = (
speciesData: Species[] | undefined,
field: TaxonomySuggestionField
): string[] => {
if (!speciesData) return []
return Array.from(
speciesData.reduce<Set<string>>((options, species) => {
const value = species[field]
if (typeof value === 'string') {
const trimmedValue = value.trim()
if (trimmedValue.length > 0) options.add(trimmedValue)
}
return options
}, new Set<string>())
).sort((a, b) => a.localeCompare(b))
}
@@ -0,0 +1,27 @@
import { describe, expect, it } from '@jest/globals'
import { buildTaxonomySuggestionOptions } from '@/components/Species/taxonomySuggestions'
import type { Species } from '@/shared/types'
const species = (overrides: Partial<Species>): Species => overrides as Species
describe('buildTaxonomySuggestionOptions', () => {
it('returns sorted unique non-empty taxonomy values for a field', () => {
const options = buildTaxonomySuggestionOptions(
[
species({ family_name: ' Felidae ' }),
species({ family_name: 'Bovidae' }),
species({ family_name: 'Felidae' }),
species({ family_name: '' }),
species({ family_name: null }),
],
'family_name'
)
expect(options).toEqual(['Bovidae', 'Felidae'])
})
it('returns an empty option list before species data is loaded', () => {
expect(buildTaxonomySuggestionOptions(undefined, 'genus_name')).toEqual([])
})
})