Implement occurrence detail tabs with updates placeholder

This commit is contained in:
Kari Lintulaakso
2026-02-20 13:19:20 +02:00
parent dcabbce054
commit a5dc78285e
7 changed files with 324 additions and 2 deletions
@@ -0,0 +1,28 @@
import { ReactNode } from 'react'
import { describe, expect, it, jest } from '@jest/globals'
import { render, screen } from '@testing-library/react'
import { UpdateTab } from './UpdateTab'
jest.mock('@/components/DetailView/Context/DetailContext', () => ({
useDetailContext: () => ({ data: {} }),
}))
jest.mock('./EditingModal', () => ({
EditingModal: ({ children }: { children: ReactNode }) => <div>{children}</div>,
}))
jest.mock('./SimpleTable', () => ({
SimpleTable: () => <div data-testid="simple-table" />,
}))
jest.mock('./ReferenceList', () => ({
ReferenceList: () => <div data-testid="reference-list" />,
}))
describe('UpdateTab', () => {
it('renders placeholder message when placeholder mode is enabled', () => {
render(<UpdateTab prefix="occ" placeholderMessage="Updates tab placeholder. Implementation is pending." />)
expect(screen.getByText('Updates tab placeholder. Implementation is pending.')).toBeTruthy()
})
})
@@ -16,13 +16,25 @@ export const UpdateTab = <T, UpdateType extends MRT_RowData & { updates: UpdateL
prefix,
refFieldName,
updatesFieldName,
placeholderMessage,
}: {
prefix: string
refFieldName: keyof UpdateType
updatesFieldName: keyof T
refFieldName?: keyof UpdateType
updatesFieldName?: keyof T
placeholderMessage?: string
}) => {
const { data } = useDetailContext<T>()
if (placeholderMessage) {
return (
<Grouped title="Updates">
<Box>{placeholderMessage}</Box>
</Grouped>
)
}
if (!refFieldName || !updatesFieldName) return null
const columns: MRT_ColumnDef<UpdateType>[] = [
{
accessorKey: `${prefix}_date`,
@@ -0,0 +1,81 @@
import { CircularProgress } from '@mui/material'
import { useParams } from 'react-router-dom'
import { DetailView, TabType } from '@/components/DetailView/DetailView'
import { UpdateTab } from '@/components/DetailView/common/UpdateTab'
import { OccurrenceCoreTab } from './Tabs/OccurrenceCoreTab'
import { OccurrenceWearTab } from './Tabs/OccurrenceWearTab'
import { OccurrenceIsotopeTab } from './Tabs/OccurrenceIsotopeTab'
import { useOccurrenceDetails } from '@/hooks/useOccurrenceDetails'
import { OccurrenceDetailsType } from '@/shared/types'
import { ValidationObject } from '@/shared/validators/validator'
const validateOccurrence = (): ValidationObject => ({ error: null, name: '' })
const emptyOccurrence: OccurrenceDetailsType = {
lid: 0,
species_id: 0,
loc_status: null,
loc_name: '',
country: '',
genus_name: '',
species_name: '',
nis: null,
pct: null,
quad: null,
mni: null,
qua: null,
id_status: null,
orig_entry: null,
source_name: null,
body_mass: null,
mesowear: null,
mw_or_high: null,
mw_or_low: null,
mw_cs_sharp: null,
mw_cs_round: null,
mw_cs_blunt: null,
mw_scale_min: null,
mw_scale_max: null,
mw_value: null,
microwear: null,
dc13_mean: null,
dc13_n: null,
dc13_max: null,
dc13_min: null,
dc13_stdev: null,
do18_mean: null,
do18_n: null,
do18_max: null,
do18_min: null,
do18_stdev: null,
}
export const OccurrenceDetails = () => {
const { lid, speciesId } = useParams()
const parsedLid = lid ? parseInt(lid, 10) : null
const parsedSpeciesId = speciesId ? parseInt(speciesId, 10) : null
const { occurrence, isLoading, isError } = useOccurrenceDetails(parsedLid, parsedSpeciesId)
if (isError) return <div>Error loading occurrence data</div>
if (isLoading || !occurrence) return <CircularProgress />
document.title = `Occurrence - ${occurrence.lid}/${occurrence.species_id}`
const tabs: TabType[] = [
{ title: 'Core', content: <OccurrenceCoreTab /> },
{ title: 'Wear', content: <OccurrenceWearTab /> },
{ title: 'Isotopes', content: <OccurrenceIsotopeTab /> },
{
title: 'Updates',
content: <UpdateTab prefix="occ" placeholderMessage="Updates tab placeholder. Implementation is pending." />,
},
]
return (
<DetailView<OccurrenceDetailsType>
tabs={tabs}
data={occurrence ?? emptyOccurrence}
validator={validateOccurrence}
/>
)
}
@@ -0,0 +1,40 @@
import { useDetailContext } from '@/components/DetailView/Context/DetailContext'
import { ArrayFrame, HalfFrames } from '@/components/DetailView/common/tabLayoutHelpers'
import { OccurrenceDetailsType } from '@/shared/types'
const toText = (value: string | number | null) => (value === null || value === '' ? '-' : String(value))
export const OccurrenceCoreTab = () => {
const { data } = useDetailContext<OccurrenceDetailsType>()
return (
<HalfFrames>
{[
<ArrayFrame
key="identification"
title="Identification"
array={[
['Locality ID', toText(data.lid)],
['Species ID', toText(data.species_id)],
['Genus', toText(data.genus_name)],
['Species', toText(data.species_name)],
['ID status', toText(data.id_status)],
['Quality', toText(data.qua)],
]}
/>,
<ArrayFrame
key="counts"
title="Occurrence counts"
array={[
['NISP', toText(data.nis)],
['Percent', toText(data.pct)],
['Quadrate', toText(data.quad)],
['MNI', toText(data.mni)],
['Original entry', toText(data.orig_entry)],
['Source name', toText(data.source_name)],
]}
/>,
]}
</HalfFrames>
)
}
@@ -0,0 +1,38 @@
import { useDetailContext } from '@/components/DetailView/Context/DetailContext'
import { ArrayFrame, HalfFrames } from '@/components/DetailView/common/tabLayoutHelpers'
import { OccurrenceDetailsType } from '@/shared/types'
const toText = (value: number | null) => (value === null ? '-' : String(value))
export const OccurrenceIsotopeTab = () => {
const { data } = useDetailContext<OccurrenceDetailsType>()
return (
<HalfFrames>
{[
<ArrayFrame
key="dc13"
title="δ13C"
array={[
['Mean', toText(data.dc13_mean)],
['N', toText(data.dc13_n)],
['Max', toText(data.dc13_max)],
['Min', toText(data.dc13_min)],
['Stdev', toText(data.dc13_stdev)],
]}
/>,
<ArrayFrame
key="do18"
title="δ18O"
array={[
['Mean', toText(data.do18_mean)],
['N', toText(data.do18_n)],
['Max', toText(data.do18_max)],
['Min', toText(data.do18_min)],
['Stdev', toText(data.do18_stdev)],
]}
/>,
]}
</HalfFrames>
)
}
@@ -0,0 +1,39 @@
import { useDetailContext } from '@/components/DetailView/Context/DetailContext'
import { ArrayFrame, HalfFrames } from '@/components/DetailView/common/tabLayoutHelpers'
import { OccurrenceDetailsType } from '@/shared/types'
const toText = (value: string | number | null) => (value === null || value === '' ? '-' : String(value))
export const OccurrenceWearTab = () => {
const { data } = useDetailContext<OccurrenceDetailsType>()
return (
<HalfFrames>
{[
<ArrayFrame
key="mesowear"
title="Mesowear"
array={[
['Mesowear', toText(data.mesowear)],
['MW OR High', toText(data.mw_or_high)],
['MW OR Low', toText(data.mw_or_low)],
['MW CS Sharp', toText(data.mw_cs_sharp)],
['MW CS Round', toText(data.mw_cs_round)],
['MW CS Blunt', toText(data.mw_cs_blunt)],
]}
/>,
<ArrayFrame
key="microwear"
title="Microwear"
array={[
['Microwear', toText(data.microwear)],
['MW scale min', toText(data.mw_scale_min)],
['MW scale max', toText(data.mw_scale_max)],
['MW value', toText(data.mw_value)],
['Body mass', toText(data.body_mass)],
]}
/>,
]}
</HalfFrames>
)
}
@@ -0,0 +1,84 @@
import { describe, expect, it, jest, beforeEach } from '@jest/globals'
import { render, screen } from '@testing-library/react'
import { MemoryRouter, Route, Routes } from 'react-router-dom'
import { OccurrenceDetails } from '../OccurrenceDetails'
import { useOccurrenceDetails } from '@/hooks/useOccurrenceDetails'
jest.mock('@/hooks/useOccurrenceDetails', () => ({
useOccurrenceDetails: jest.fn(),
}))
jest.mock('../Tabs/OccurrenceCoreTab', () => ({ OccurrenceCoreTab: () => <div>Core tab</div> }))
jest.mock('../Tabs/OccurrenceWearTab', () => ({ OccurrenceWearTab: () => <div>Wear tab</div> }))
jest.mock('../Tabs/OccurrenceIsotopeTab', () => ({ OccurrenceIsotopeTab: () => <div>Isotope tab</div> }))
jest.mock('@/components/DetailView/common/UpdateTab', () => ({ UpdateTab: () => <div>Updates placeholder</div> }))
jest.mock('@/components/DetailView/DetailView', () => ({
DetailView: ({ tabs }: { tabs: Array<{ title: string }> }) => (
<div data-testid="occurrence-detail-view">{tabs.map(tab => tab.title).join('|')}</div>
),
}))
const mockUseOccurrenceDetails = useOccurrenceDetails as jest.MockedFunction<typeof useOccurrenceDetails>
describe('OccurrenceDetails', () => {
beforeEach(() => {
mockUseOccurrenceDetails.mockReset()
})
it('renders occurrence tabs including Updates placeholder tab', () => {
mockUseOccurrenceDetails.mockReturnValue({
occurrence: {
lid: 1,
species_id: 2,
loc_status: false,
loc_name: 'Loc',
country: 'Country',
genus_name: 'Genus',
species_name: 'species',
nis: null,
pct: null,
quad: null,
mni: null,
qua: null,
id_status: null,
orig_entry: null,
source_name: null,
body_mass: null,
mesowear: null,
mw_or_high: null,
mw_or_low: null,
mw_cs_sharp: null,
mw_cs_round: null,
mw_cs_blunt: null,
mw_scale_min: null,
mw_scale_max: null,
mw_value: null,
microwear: null,
dc13_mean: null,
dc13_n: null,
dc13_max: null,
dc13_min: null,
dc13_stdev: null,
do18_mean: null,
do18_n: null,
do18_max: null,
do18_min: null,
do18_stdev: null,
},
isLoading: false,
isError: false,
refetch: jest.fn(() => Promise.resolve(undefined)),
})
render(
<MemoryRouter initialEntries={['/occurrence/1/2?tab=3']}>
<Routes>
<Route path="/occurrence/:lid/:speciesId" element={<OccurrenceDetails />} />
</Routes>
</MemoryRouter>
)
expect(screen.getByTestId('occurrence-detail-view').textContent).toBe('Core|Wear|Isotopes|Updates')
})
})