Skip to content
Merged
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
16 changes: 11 additions & 5 deletions cypress/support/commands.js
Original file line number Diff line number Diff line change
Expand Up @@ -82,8 +82,9 @@ Cypress.Commands.add('pageForbidden', url => {
})

Cypress.Commands.add('resetDatabase', () => {
cy.task('waitForDbHealthy')
cy.request(Cypress.env('databaseResetUrl')).its('status').should('eq', 200)
const dbWaitTimeoutMs = Number(Cypress.env('dbWaitTimeoutMs') ?? 30000)
cy.task('waitForDbHealthy', undefined, { timeout: dbWaitTimeoutMs })
cy.request({ url: Cypress.env('databaseResetUrl'), timeout: dbWaitTimeoutMs }).its('status').should('eq', 200)
})

// Optimized database reset that only resets once per test file
Expand Down Expand Up @@ -118,9 +119,14 @@ Cypress.Commands.add('loginWithSession', (username) => {
})
}, {
validate: () => {
// Validate the session is still valid by checking for the username box
cy.visit('/')
cy.contains('.username-box', username, { timeout: 10000 }).should('be.visible')
cy.window().should(window => {
const storedUserState = window.localStorage.getItem('userState')
expect(storedUserState, 'stored user state').to.not.be.null

const parsedUserState = JSON.parse(storedUserState)
expect(parsedUserState?.username, 'stored username').to.eq(username)
expect(parsedUserState?.token, 'stored login token').to.be.a('string').and.not.be.empty
})
},
})
})
Expand Down
4 changes: 4 additions & 0 deletions frontend/src/components/DetailView/Context/DetailContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@ export type DetailContextType<T> = {
optionalRadioSelectionProps?: OptionalRadioSelectionProps
) => JSX.Element
validator: (editData: EditDataType<T>, field: keyof EditDataType<T>) => ValidationObject
validateFields?: (editData: EditDataType<T>) => ValidationObject[]
fieldsWithErrors: FieldsWithErrorsType
setFieldsWithErrors: SetFieldsWithErrorsType
isDirty: boolean
Expand Down Expand Up @@ -211,6 +212,9 @@ export const DetailContextProvider = <T extends object>({
markEditDataClean,
validator: (editData: unknown, fieldName: keyof EditDataType<T>) =>
contextState.validator(editData as EditDataType<T>, fieldName),
validateFields: contextState.validateFields
? (editData: unknown) => contextState.validateFields?.(editData as EditDataType<T>) ?? []
: undefined,
}}
>
{children}
Expand Down
3 changes: 3 additions & 0 deletions frontend/src/components/DetailView/DetailView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ export const DetailView = <T extends object>({
data,
onWrite,
validator,
validateFields,
isNew = false,
isUserPage = false,
isPersonPage = false,
Expand All @@ -124,6 +125,7 @@ export const DetailView = <T extends object>({
markEditDataClean?: (editData?: EditDataType<T>) => void
) => Promise<WriteResult>
validator: (editData: EditDataType<T>, field: keyof EditDataType<T>) => ValidationObject
validateFields?: (editData: EditDataType<T>) => ValidationObject[]
isNew?: boolean
isUserPage?: boolean
isPersonPage?: boolean
Expand Down Expand Up @@ -193,6 +195,7 @@ export const DetailView = <T extends object>({
radioSelection,
bigTextField,
validator,
validateFields,
fieldsWithErrors,
setFieldsWithErrors,
}
Expand Down
23 changes: 11 additions & 12 deletions frontend/src/components/DetailView/StagingView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import type { DropdownOption } from './common/editingComponents'
import type { FieldsWithErrorsType, OptionalRadioSelectionProps, TextFieldOptions } from './DetailView'
import type { ValidationObject } from '@/shared/validators/validator'
import {
createReferenceFieldsValidatorWithLabels,
createReferenceValidatorWithLabels,
ReferenceDisplayLabelMap,
ReferenceFieldDisplayNames,
Expand All @@ -32,16 +33,13 @@ import { formatReferenceValidationErrorMessage } from '@/components/Reference/re
const NewReferenceDialogContent = ({
onClose,
onCreated,
referenceValidator,
validateReferenceFields,
referenceFieldDisplayLabelMap,
referenceTypes,
}: {
onClose: () => void
onCreated: (reference: ReferenceDetailsType) => void
referenceValidator: (
editData: EditDataType<ReferenceDetailsType>,
field: keyof EditDataType<ReferenceDetailsType>
) => ValidationObject
validateReferenceFields: (editData: EditDataType<ReferenceDetailsType>) => ValidationObject[]
referenceFieldDisplayLabelMap?: ReferenceDisplayLabelMap
referenceTypes?: ReferenceType[]
}) => {
Expand All @@ -52,12 +50,8 @@ const NewReferenceDialogContent = ({
const validateAllFields = () => {
const nextFieldsWithErrors: FieldsWithErrorsType = {}

for (const field in editData) {
const fieldKey = field as keyof EditDataType<ReferenceDetailsType>
const errorObject = referenceValidator(editData, fieldKey)
if (errorObject.error) {
nextFieldsWithErrors[String(fieldKey)] = errorObject
}
for (const errorObject of validateReferenceFields(editData)) {
nextFieldsWithErrors[String(errorObject.field ?? errorObject.name)] = errorObject
}

setFieldsWithErrors(() => nextFieldsWithErrors)
Expand Down Expand Up @@ -136,6 +130,10 @@ const NewReferenceDialog = ({
() => createReferenceValidatorWithLabels(referenceFieldDisplayLabelMap),
[referenceFieldDisplayLabelMap]
)
const validateReferenceFields = useMemo(
() => createReferenceFieldsValidatorWithLabels(referenceFieldDisplayLabelMap),
[referenceFieldDisplayLabelMap]
)

const textField = (field: keyof EditDataType<ReferenceDetailsType>, options?: TextFieldOptions) => (
<EditableTextField<ReferenceDetailsType> field={field} {...options} />
Expand Down Expand Up @@ -193,14 +191,15 @@ const NewReferenceDialog = ({
radioSelection,
bigTextField,
validator: referenceValidator,
validateFields: validateReferenceFields,
fieldsWithErrors,
setFieldsWithErrors,
}}
>
<NewReferenceDialogContent
onClose={onClose}
onCreated={onCreated}
referenceValidator={referenceValidator}
validateReferenceFields={validateReferenceFields}
referenceFieldDisplayLabelMap={referenceFieldDisplayLabelMap}
referenceTypes={referenceTypes}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -557,10 +557,8 @@ export const TimeBoundSelection = ({
}) => {
const { editData, setEditData, validator, fieldsWithErrors, setFieldsWithErrors } =
useDetailContext<TimeUnitDetailsType>()
const errorObject = validator(
editData,
(targetField === 'up_bnd' ? 'up_bound' : 'low_bound') as keyof EditDataType<TimeUnitDetailsType>
)
const boundField = (targetField === 'up_bnd' ? 'up_bound' : 'low_bound') as keyof EditDataType<TimeUnitDetailsType>
const errorObject = validator(editData, boundField)
const [open, setOpen] = useState(false)

const selectorFn = (selected: TimeBoundDetailsType) => {
Expand All @@ -573,7 +571,7 @@ export const TimeBoundSelection = ({
}

useEffect(() => {
checkFieldErrors(String(targetField), errorObject, fieldsWithErrors, setFieldsWithErrors)
checkFieldErrors(String(boundField), errorObject, fieldsWithErrors, setFieldsWithErrors)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [errorObject])

Expand Down
26 changes: 22 additions & 4 deletions frontend/src/components/DetailView/components.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ export const WriteButton = <T,>({
mode,
setMode,
validator,
validateFields,
fieldsWithErrors,
setFieldsWithErrors,
isDirty,
Expand Down Expand Up @@ -108,10 +109,27 @@ export const WriteButton = <T,>({

useEffect(() => {
if (mode.option === 'edit' || mode.option === 'new') {
for (const field in editData) {
const fieldAsString = String(field)
const errorObject = validator(editData, field)
checkFieldErrors(fieldAsString, errorObject, fieldsWithErrors, setFieldsWithErrors)
if (validateFields) {
const validationErrors = validateFields(editData)
setFieldsWithErrors(prevFieldsWithErrors => {
const nextFieldsWithErrors = { ...prevFieldsWithErrors }

for (const field of Object.keys(nextFieldsWithErrors)) {
if (field !== 'mandatoryReference') delete nextFieldsWithErrors[field]
}

for (const errorObject of validationErrors) {
nextFieldsWithErrors[String(errorObject.field ?? errorObject.name)] = errorObject
}

return nextFieldsWithErrors
})
} else {
for (const field in editData) {
const fieldAsString = String(field)
const errorObject = validator(editData, field)
checkFieldErrors(fieldAsString, errorObject, fieldsWithErrors, setFieldsWithErrors)
}
}
}
if (mode.staging == true) {
Expand Down
3 changes: 2 additions & 1 deletion frontend/src/components/Locality/LocalityDetails.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import { ProjectTab } from './Tabs/ProjectTab'
import { SpeciesTab } from './Tabs/SpeciesTab'
import { TaphonomyTab } from './Tabs/TaphonomyTab'
import { EditDataType, LocalityDetailsType, ValidationErrors } from '@/shared/types'
import { validateLocality } from '@/shared/validators/locality'
import { validateLocality, validateLocalityFields } from '@/shared/validators/locality'
import { UpdateTab } from '../DetailView/common/UpdateTab'
import { emptyLocality } from '../DetailView/common/defaultValues'
import { useNotify } from '@/hooks/notification'
Expand Down Expand Up @@ -164,6 +164,7 @@ export const LocalityDetails = ({
isNew={isNew}
onWrite={onWrite}
validator={validateLocality}
validateFields={validateLocalityFields}
deleteFunction={deleteFunction}
hasStagingMode
wrapWithUnsavedChangesProvider={wrapWithUnsavedChangesProvider}
Expand Down
3 changes: 2 additions & 1 deletion frontend/src/components/Museum/MuseumDetails.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { useEditMuseumMutation, useGetMuseumDetailsQuery } from '../../redux/mus
import { DetailView, TabType } from '../DetailView/DetailView'
import { MuseumInfoTab } from './Tabs/MuseumInfoTab'
import { emptyMuseum } from '../DetailView/common/defaultValues'
import { validateMuseum } from '@/shared/validators/museum'
import { validateMuseum, validateMuseumFields } from '@/shared/validators/museum'
import { useNavigate, useParams } from 'react-router-dom'
import { CircularProgress } from '@mui/material'
import { Museum, EditDataType, ValidationErrors } from '@/shared/types'
Expand Down Expand Up @@ -58,6 +58,7 @@ export const MuseumDetails = ({
onWrite={onWrite}
isNew={isNew}
validator={validateMuseum}
validateFields={validateMuseumFields}
deleteFunction={undefined}
wrapWithUnsavedChangesProvider={wrapWithUnsavedChangesProvider}
/>
Expand Down
3 changes: 2 additions & 1 deletion frontend/src/components/Person/PersonDetails.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { PersonTab } from './Tabs/PersonTab'
import { PersonRelationsTab } from './Tabs/PersonRelationsTab'
import { useUser } from '@/hooks/user'
import { EditDataType, PersonDetailsType, Role, ValidationErrors } from '@/shared/types'
import { validatePerson } from '@/shared/validators/person'
import { validatePerson, validatePersonFields } from '@/shared/validators/person'
import { useNotify } from '@/hooks/notification'
import { useEffect } from 'react'
import { emptyPerson } from '../DetailView/common/defaultValues'
Expand Down Expand Up @@ -127,6 +127,7 @@ export const PersonDetails = () => {
tabs={tabs}
data={isNew ? emptyPerson : data!}
validator={validatePerson}
validateFields={validatePersonFields}
deleteFunction={deleteFunction}
/>
)
Expand Down
6 changes: 6 additions & 0 deletions frontend/src/components/Reference/ReferenceDetails.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
import { EditDataType, ReferenceDetailsType } from '@/shared/types'
import { emptyReference } from '../DetailView/common/defaultValues'
import {
createReferenceFieldsValidatorWithLabels,
createReferenceValidatorWithLabels,
ReferenceFieldDisplayNames,
ReferenceDisplayLabelMap,
Expand Down Expand Up @@ -82,6 +83,10 @@ export const ReferenceDetails = ({
() => createReferenceValidatorWithLabels(referenceFieldDisplayLabelMap),
[referenceFieldDisplayLabelMap]
)
const validateReferenceFields = useMemo(
() => createReferenceFieldsValidatorWithLabels(referenceFieldDisplayLabelMap),
[referenceFieldDisplayLabelMap]
)

useEffect(() => {
if (deleteSuccess) {
Expand Down Expand Up @@ -138,6 +143,7 @@ export const ReferenceDetails = ({
data={isNew ? emptyReference : data!}
onWrite={onWrite}
validator={referenceValidator}
validateFields={validateReferenceFields}
deleteFunction={deleteFunction}
wrapWithUnsavedChangesProvider={wrapWithUnsavedChangesProvider}
/>
Expand Down
57 changes: 42 additions & 15 deletions frontend/src/components/Reference/Tabs/ReferenceTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,28 +8,55 @@ import { JournalTab } from './JournalTab'
import { useEffect } from 'react'

export const ReferenceTab = () => {
const { dropdown, data, editData, mode, textField, bigTextField, fieldsWithErrors, validator, setFieldsWithErrors } =
useDetailContext<ReferenceDetailsType>()
const {
dropdown,
data,
editData,
mode,
textField,
bigTextField,
fieldsWithErrors,
validator,
validateFields,
setFieldsWithErrors,
} = useDetailContext<ReferenceDetailsType>()
const { data: referenceTypes } = useGetReferenceTypesQuery()

useEffect(() => {
if (mode.option === 'edit' || mode.option === 'new') {
for (const field in editData) {
const fieldAsKey = field as keyof typeof editData
const fieldAsString = String(fieldAsKey)
const errorObject = validator(editData, fieldAsKey)
if (errorObject.error) {
if (!(fieldAsString in fieldsWithErrors)) {
if (validateFields) {
const validationErrors = validateFields(editData)
setFieldsWithErrors(prevFieldsWithErrors => {
const nextFieldsWithErrors = { ...prevFieldsWithErrors }

for (const field of Object.keys(nextFieldsWithErrors)) {
if (field !== 'mandatoryReference') delete nextFieldsWithErrors[field]
}

for (const errorObject of validationErrors) {
nextFieldsWithErrors[String(errorObject.field ?? errorObject.name)] = errorObject
}

return nextFieldsWithErrors
})
} else {
for (const field in editData) {
const fieldAsKey = field as keyof typeof editData
const fieldAsString = String(fieldAsKey)
const errorObject = validator(editData, fieldAsKey)
if (errorObject.error) {
if (!(fieldAsString in fieldsWithErrors)) {
setFieldsWithErrors(prevFieldsWithErrors => {
return { ...prevFieldsWithErrors, [fieldAsString]: errorObject }
})
}
} else if (!errorObject.error && fieldAsString in fieldsWithErrors) {
setFieldsWithErrors(prevFieldsWithErrors => {
return { ...prevFieldsWithErrors, [fieldAsString]: errorObject }
const newFieldsWithErrors = { ...prevFieldsWithErrors }
delete newFieldsWithErrors[fieldAsString]
return newFieldsWithErrors
})
}
} else if (!errorObject.error && fieldAsString in fieldsWithErrors) {
setFieldsWithErrors(prevFieldsWithErrors => {
const newFieldsWithErrors = { ...prevFieldsWithErrors }
delete newFieldsWithErrors[fieldAsString]
return newFieldsWithErrors
})
}
}
}
Expand Down
3 changes: 2 additions & 1 deletion frontend/src/components/Region/RegionDetails.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { DetailView, TabType } from '../DetailView/DetailView'
import { CoordinatorTab } from './Tabs/CoordinatorTab'
import { EditDataType, RegionDetails as RegionDetailsType, ValidationErrors } from '@/shared/types'
import { useNotify } from '@/hooks/notification'
import { validateRegion } from '@/shared/validators/region'
import { validateRegion, validateRegionFields } from '@/shared/validators/region'
import { useEffect } from 'react'
import { emptyRegion } from '../DetailView/common/defaultValues'

Expand Down Expand Up @@ -66,6 +66,7 @@ export const RegionDetails = () => {
data={isNew ? emptyRegion : data!}
onWrite={onWrite}
validator={validateRegion}
validateFields={validateRegionFields}
deleteFunction={deleteFunction}
/>
)
Expand Down
3 changes: 2 additions & 1 deletion frontend/src/components/Species/SpeciesDetails.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { TaxonomyTab } from './Tabs/TaxonomyTab'
import { TeethTab } from './Tabs/TeethTab'
import { UpdateTab } from '../DetailView/common/UpdateTab'
import { EditDataType, SpeciesDetailsType, ValidationErrors } from '@/shared/types'
import { validateSpecies } from '@/shared/validators/species'
import { validateSpecies, validateSpeciesFields } from '@/shared/validators/species'
import { emptySpecies } from '../DetailView/common/defaultValues'
import { useNotify } from '@/hooks/notification'
import { useEffect, useState } from 'react'
Expand Down Expand Up @@ -124,6 +124,7 @@ export const SpeciesDetails = ({
taxonomy={true}
hasStagingMode
validator={validateSpecies}
validateFields={validateSpeciesFields}
deleteFunction={deleteFunction}
wrapWithUnsavedChangesProvider={wrapWithUnsavedChangesProvider}
/>
Expand Down
Loading
Loading