Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions backend/src/routes/locality.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,13 @@ router.post('/export/dwc-archive', requireOneOf([Role.Admin]), async (req, res)
return sendDwcArchive(ids, res)
})

router.get('/:id/occurrences', async (req, res) => {
const id = parseInt(req.params.id)
const locality = await getLocalityDetails(id, req.user)
if (!locality) return res.status(404).send()
return res.status(200).send(fixBigInt(locality.now_ls))
})

router.get('/:id', async (req, res) => {
const id = parseInt(req.params.id)
const locality = await getLocalityDetails(id, req.user)
Expand Down
5 changes: 5 additions & 0 deletions backend/src/services/write/locality.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,11 @@ export const writeLocality = async (
localitySpecies.species_id = species_id as number
}

for (const localitySpecies of locality.now_ls) {
if (localitySpecies.rowState === 'new' || localitySpecies.rowState === 'removed') continue
await writeHandler.updateObject('now_ls', localitySpecies, ['lid', 'species_id'])
}

await writeHandler.applyListChanges('now_ls', locality.now_ls, ['lid', 'species_id'])
await writeHandler.applyListChanges('now_mus', locality.now_mus, ['lid', 'museum'])
await writeHandler.applyListChanges('now_ss', locality.now_ss, ['lid', 'sed_struct'])
Expand Down
2 changes: 1 addition & 1 deletion cypress.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ module.exports = defineConfig({
specPattern: ['cypress/e2e/**/*.cy.{js,jsx,ts,tsx}', 'cypress/e2e/**/*.spec.{js,jsx,ts,tsx}'],
// Performance optimizations
experimentalMemoryManagement: true,
numTestsKeptInMemory: 0,
numTestsKeptInMemory: 20,
defaultCommandTimeout: 8000,
requestTimeout: 8000,
responseTimeout: 8000,
Expand Down
35 changes: 35 additions & 0 deletions cypress/e2e/locality.cy.js
Original file line number Diff line number Diff line change
Expand Up @@ -411,6 +411,10 @@ describe('Creating a new locality', () => {
cy.get('[data-value=""]').click()
cy.get('[id=max_age-textfield]').should('have.value', '15.97')
})
it('and visiting the occurrence tabs shows the create new occurrence button as disabled', () => {
cy.visit('locality/new?tab=3')
cy.get('#create-occurrence-button').should('exist').should('be.disabled')
})
})

describe('Editing a locality', () => {
Expand Down Expand Up @@ -493,6 +497,37 @@ describe('Editing a locality', () => {
cy.visit(`/locality/20920?tab=5`)
cy.contains('wet_screen')
})

it('and creating a new occurrence opens a new tab', () => {
cy.loginWithSession('testSu')
cy.visit('locality/20920?tab=3', {
onBeforeLoad(win) {
cy.stub(win, 'open').as('windowOpen')
},
})
cy.get('#edit-button').should('exist').click()
cy.get('#create-occurrence-button').should('not.be.disabled').click()
cy.get('@windowOpen').should('be.called')
})

it('and removing existing occurrence rows, then clicking the refresh occurrences button does not make the rows reappear', () => {
cy.loginWithSession('testSu')
cy.visit('locality/21050?tab=3')
cy.contains('meneghinii').should('exist')
cy.get('#edit-button').should('exist').click()

cy.get('[data-cy="table-row-85729"]').find('[data-testid=RemoveCircleOutlineIcon]').click()
cy.get('[data-cy="table-row-85729"]').find('[data-testid=AddCircleOutlineIcon]').should('exist')
cy.get('#refresh-occurrences-button').click()
cy.get('[data-cy="table-row-85729"]').find('[data-testid=AddCircleOutlineIcon]').should('exist')
cy.addReferenceAndSave()
cy.visit('locality/21050?tab=3')
cy.contains('meneghinii').should('not.exist')
})

it(
'TODO: adding occurrence to it (in another window), then clicking the refresh occurrences button makes the new occurrence appear'
)
})

describe('Locality table filtering', () => {
Expand Down
File renamed without changes.
19 changes: 9 additions & 10 deletions cypress/e2e/ui.cy.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,15 @@ describe('Button Tests', () => {
cy.contains('Genus').should('be.visible')
})

it('Occurrences button works', () => {
cy.visit('/')
cy.contains('Occurrences').click()
cy.url().should('include', '/occurrence')
cy.contains('Locality name').should('be.visible')
cy.contains('Genus').should('be.visible')
cy.contains('Species').should('be.visible')
})

it('Time Units button works', () => {
cy.visit('/')
cy.contains('Time Units').click()
Expand All @@ -47,16 +56,6 @@ describe('Button Tests', () => {
cy.contains('Sequence').should('be.visible')
})

it('Cross Search button works', () => {
cy.visit('/')
cy.visit('/occurrence')
cy.url().should('include', '/occurrence')
cy.contains('Occurrences').should('be.visible')
cy.contains('Country').should('be.visible')
cy.contains('Genus').should('be.visible')
cy.contains('Species').should('be.visible')
})

it('Link to species details from species tab', () => {
cy.visit('/species')
cy.get('tbody tr', { timeout: 10000 }).first().click()
Expand Down
7 changes: 6 additions & 1 deletion cypress/e2e/userRights.cy.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,16 @@ describe('Broadly test what different user rights see', () => {

it('Sees new button in tableviews', () => {
cy.visit('/locality')
cy.contains('New').click()
cy.get('#new-button').click()
cy.contains('Time Unit')
cy.contains('Creating new locality')
})

it('Does not see new button in Occurrence tableview', () => {
cy.visit('/occurrence')
cy.get('#new-button').should('not.exist')
})

it('Regions view shows correctly', () => {
cy.visit('/region')
cy.contains('region 4452477e')
Expand Down
97 changes: 63 additions & 34 deletions frontend/src/components/Locality/Tabs/OccurrencesTab.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,22 @@
import { Editable, LocalityDetailsType, LocalitySpecies } from '@/shared/types'
import { applyDefaultSpeciesOrdering, hasActiveSortingInSearch } from '@/components/DetailView/common/DetailTabTable'
import { EditableTable } from '@/components/DetailView/common/EditableTable'
import { EditingModal } from '@/components/DetailView/common/EditingModal'
import { EntryUpdateHistory } from '@/components/DetailView/common/FieldUpdateHistory'
import { Grouped } from '@/components/DetailView/common/tabLayoutHelpers'
import { useDetailContext } from '@/components/DetailView/Context/DetailContext'
import { Box, TextField } from '@mui/material'
import { MRT_ColumnDef, MRT_Row, MRT_RowData, MRT_TableInstance } from 'material-react-table'
import { useForm } from 'react-hook-form'
import { calculateNormalizedMesowearScore } from '@/shared/utils/mesowear'
import { applyDefaultSpeciesOrdering, hasActiveSortingInSearch } from '@/components/DetailView/common/DetailTabTable'
import { useLocation } from 'react-router-dom'
import { useMemo } from 'react'
import { occurrenceLabels } from '@/constants/occurrenceLabels'
import {
exportOccurrenceMapKml,
exportOccurrenceMapSvg,
getUniqueLocalityOccurrenceMapExportLocalities,
} from '@/components/Species/localitySpeciesMapExport'
import { EntryUpdateHistory } from '@/components/DetailView/common/FieldUpdateHistory'
import { occurrenceLabels } from '@/constants/occurrenceLabels'
import { useLazyGetLocalityOccurrencesQuery } from '@/redux/localityReducer'
import { Editable, LocalityDetailsType, LocalitySpecies, RowState } from '@/shared/types'
import { calculateNormalizedMesowearScore } from '@/shared/utils/mesowear'
import RefreshIcon from '@mui/icons-material/Refresh'
import { Box, Button } from '@mui/material'
import { MRT_ColumnDef, MRT_Row, MRT_RowData, MRT_TableInstance } from 'material-react-table'
import { useMemo } from 'react'
import { useLocation } from 'react-router-dom'

const hasMesowearScoreInputs = (row: LocalitySpecies) => {
return (
Expand All @@ -30,12 +30,9 @@ const hasMesowearScoreInputs = (row: LocalitySpecies) => {
}

export const OccurrencesTab = () => {
const { mode, data, editData } = useDetailContext<LocalityDetailsType>()
const { mode, data, editData, setEditData } = useDetailContext<LocalityDetailsType>()
const [refreshOccurrences, { isFetching }] = useLazyGetLocalityOccurrencesQuery()
const location = useLocation()
const {
register,
formState: { errors },
} = useForm()

const sortedOccurrenceRows = useMemo(() => {
const sourceRows = (mode.read ? data.now_ls : editData.now_ls) as unknown as Editable<LocalitySpecies>[]
Expand Down Expand Up @@ -215,12 +212,6 @@ export const OccurrencesTab = () => {
},
]

// eslint-disable-next-line @typescript-eslint/require-await
const onSave = async () => {
// TODO: Saving logic here (add Occurrence to editData)
return Object.keys(errors).length === 0
}

const getExportLocalities = <T extends MRT_RowData>(table: MRT_TableInstance<T>) => {
const rows = table.getPrePaginationRowModel().rows.map(row => row.original as unknown as LocalitySpecies)
return getUniqueLocalityOccurrenceMapExportLocalities(data, rows)
Expand All @@ -234,21 +225,59 @@ export const OccurrencesTab = () => {
await exportOccurrenceMapSvg(table, 'locality-occurrences-map', getExportLocalities)
}

const editingModal = (
<EditingModal buttonText={occurrenceLabels.addNewButton} onSave={onSave}>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: '1em' }}>
<TextField {...register('com_species.order_name', { required: true })} label="Order" />
<TextField {...register('com_species.family_name', { required: true })} label="Family" />
<TextField {...register('com_species.genus_name', { required: true })} label="Genus" />
<TextField {...register('com_species.species_name', { required: true })} label="Species" />
<TextField {...register('com_species.unique_identifier', { required: true })} label="Unique Identifier" />
</Box>
</EditingModal>
)
const handleRefresh = async () => {
// Since Occurrences are created in a new tab when clicking the "Create new Occurrence" button
// in this tab, the data shown in this tab is not updated automatically by redux cache invalidation
// once the new occurrence is created. Thus refresh button.
const result = await refreshOccurrences(String(editData.lid)).unwrap()

const filteredResult = result.filter(row => {
const localRow = editData.now_ls.find(ls => ls.species_id == row.species_id)
return localRow?.rowState !== 'removed'
})

const refreshedRows = filteredResult.map(row => ({
...row,
rowState: 'clean' as RowState,
}))

const removedRows = editData.now_ls.filter(row => row.rowState! === 'removed')

setEditData({
...editData,
now_ls: [...refreshedRows, ...removedRows],
})
}

let infoText
if (mode.new) infoText = 'Creating new occurrences is only possible after the locality is created.'
else if (!mode.read) {
infoText =
'Clicking the button opens a new tab. After the occurrence has been created successfully, click the refresh button to update this table.'
}

return (
<Grouped title={occurrenceLabels.informationSectionTitle}>
{!mode.read && editingModal}
<Box>
{!mode.read && (
<Button
id="create-occurrence-button"
disabled={mode.new}
variant="contained"
onClick={() =>
window.open(`${window.location.origin}/occurrence/new?lid=${data.lid}&loc_name=${data.loc_name}`)
}
>
Create new occurrence
</Button>
)}
<Button id="refresh-occurrences-button" onClick={() => void handleRefresh()} disabled={isFetching}>
<RefreshIcon></RefreshIcon>
Refresh Occurrences
</Button>
{infoText && <p>{infoText}</p>}
</Box>

<EditableTable<Editable<LocalitySpecies>, LocalityDetailsType>
columns={columns}
field="now_ls"
Expand Down
Loading
Loading