Merge branch 'main' into time-unit-test-invalid-bounds

This commit is contained in:
Aaro Koivukangas
2024-11-06 14:33:33 +01:00
committed by GitHub
11 changed files with 114 additions and 56 deletions
+5 -5
View File
@@ -88,14 +88,14 @@ Docker:
:rocket: [Initialize app & restore db](documentation/init.md) How to get the app up & running, and initialize database from sql-dumps
:wrench: [Technical explanations](documentation/technical_explanations.md) Explanations of how things work, and guides for some more rare tasks like updating database-schema
:wrench: [Technical explanations](documentation/guides/technical_explanations.md) Explanations of how things work, and guides for some more rare tasks like updating database-schema
:raised_hands: [Contributing & integration flow](documentation/contributing.md) Where to commit? Where are tests run? How is the code deployed?
:raised_hands: [Contributing & integration flow](documentation/guides/contributing.md) Where to commit? Where are tests run? How is the code deployed?
:memo: [Frontend](documentation/frontend.md) Explanation of the frontend logic and how to add views
:memo: [Frontend](documentation/components/frontend.md) Explanation of the frontend logic and how to add views
:mag_right: [Validators](frontend/src/validators/) The readme explains how validators are written
:mag_right: [Validators](frontend/src/validators/) The [readme](frontend/src/validators/README.md) explains how validators are written
: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 feature](/documentation/map_feat.md) Map's functionality and development notes
:earth_africa: [Map feature](/documentation/components/map_feat.md) Map's functionality and development notes
+1 -6
View File
@@ -108,11 +108,6 @@ describe('Creating a new locality...', () => {
cy.get('[id=dec_lat-textfield]').type('48.68')
cy.get('[id=dms_lat-textfield]').should('have.value', '48 40 48 N')
cy.contains('This field is required')
cy.get('[id=write-button]').click()
cy.get('[id=write-button]').click()
cy.contains('Following validators failed: ')
cy.contains('Edited item successfully.').should('not.exist')
cy.visit('/locality/')
cy.contains('Eg').should('not.exist')
cy.get('[id=write-button]').should('be.disabled')
})
})
+1 -3
View File
@@ -6,9 +6,7 @@ describe('Species validators work', () => {
it('Creating a species with missing required fields does not work', () => {
cy.visit('/species/new')
cy.contains('This field is required')
cy.get('[id=write-button]').click()
cy.get('[id=write-button]').click()
cy.contains('This field is required')
cy.get('[id=write-button]').should('be.disabled')
})
it.skip('Creating a species with valid data works', () => {
+1 -4
View File
@@ -23,9 +23,6 @@ describe('Editing time bound', () => {
cy.get('[id=edit-button]').click()
cy.get('[id=age-textfield]').first().type('{backspace}{backspace}{backspace}{backspace}{backspace}')
cy.contains('This field is required')
cy.get('[id=write-button]').click()
cy.get('[id=write-button]').click()
// make sure the age value didn't change
cy.contains('1.778')
cy.get('[id=write-button]').should('be.disabled')
})
})
+1 -3
View File
@@ -26,8 +26,6 @@ describe('Editing time unit', () => {
cy.get('[data-cy=detailview-button-11]').first().click()
cy.contains('​Upper bound age has to be lower than lower bound age')
cy.contains('Lower bound age has to be higher than upper bound age')
cy.get('[id=write-button]').click()
cy.get('[id=write-button]').click()
cy.contains('Bahean')
cy.get('[id=write-button]').should('be.disabled')
})
})
-1
View File
@@ -25,7 +25,6 @@ export const referenceTableColumns: MRT_ColumnDef<Reference>[] = [
header: 'Year',
maxSize: 60,
filterVariant: 'range',
filterFn: 'between',
},
{
id: 'title_primary',
@@ -69,6 +69,8 @@ export type DetailContextType<T> = {
optionalRadioSelectionProps?: OptionalRadioSelectionProps
) => JSX.Element
validator: (editData: EditDataType<T>, field: keyof EditDataType<T>) => ValidationObject
allErrors: Array<string>
setAllErrors: (newAllErrors: Array<string>) => void
}
export const DetailContext = createContext<DetailContextType<unknown>>(null!)
@@ -70,6 +70,7 @@ export const DetailView = <T extends object>({
}
const { editRights } = usePageContext()
const [mode, setModeState] = useState<ModeType>(isNew ? modeOptionToMode['new'] : modeOptionToMode['read'])
const [allErrors, setAllErrorsState] = useState<Array<string>>([])
const [tab, setTab] = useState(getUrl())
useEffect(() => {
@@ -111,6 +112,10 @@ export const DetailView = <T extends object>({
const bigTextField = (field: keyof EditDataType<T>) => <EditableTextField<T> field={field} type="text" big />
const setAllErrors = (newAllErrors: Array<string>) => {
setAllErrorsState(newAllErrors)
}
const initialState = {
data,
mode,
@@ -121,6 +126,8 @@ export const DetailView = <T extends object>({
radioSelection,
bigTextField,
validator,
allErrors,
setAllErrors,
}
const paperProps = {
@@ -13,7 +13,7 @@ import {
Modal,
Button,
} from '@mui/material'
import { cloneElement, ChangeEvent, ReactNode, useState, ReactElement } from 'react'
import { cloneElement, ChangeEvent, ReactNode, useState, ReactElement, useEffect } from 'react'
import { RegisterOptions, FieldValues, UseFormRegisterReturn, FieldErrors } from 'react-hook-form'
import { useDetailContext } from '../Context/DetailContext'
import { DataValue } from './tabLayoutHelpers'
@@ -51,8 +51,20 @@ export const DropdownSelector = <T extends object>({
field: keyof EditDataType<T>
disabled?: boolean
}) => {
const { setEditData, editData, validator } = useDetailContext<T>()
const { setEditData, editData, validator, allErrors, setAllErrors } = useDetailContext<T>()
const { error } = validator(editData, field)
useEffect(() => {
const errorField = String(field)
if (error && !allErrors.includes(errorField)) {
// saves invalid field into array of errors in context
setAllErrors([...allErrors, errorField])
} else if (!error && allErrors.includes(errorField)) {
// removes valid field from the array
setAllErrors(allErrors.filter(err => err !== errorField))
}
}, [error, allErrors, field, setAllErrors])
const editingComponent = (
<FormControl size="small" error={!!error}>
<InputLabel id={`${name}-multiselect-label`}>{name}</InputLabel>
@@ -143,6 +155,7 @@ export const RadioSelector = <T extends object>({
if (editData[field] === null) {
setEditData({ ...editData, [field]: defaultValue })
}
const editingComponent = (
<FormControl>
<RadioGroup
@@ -208,10 +221,21 @@ export const EditableTextField = <T extends object>({
readonly?: boolean
handleSetEditData?: (value: number | string) => void
}) => {
const { setEditData, editData, validator } = useDetailContext<T>()
const { setEditData, editData, validator, allErrors, setAllErrors } = useDetailContext<T>()
const { error } = validator(editData, field)
const name = String(field)
useEffect(() => {
const errorField = String(field)
if (error && !allErrors.includes(errorField)) {
// saves invalid field into array of errors in context
setAllErrors([...allErrors, errorField])
} else if (!error && allErrors.includes(errorField)) {
// removes valid field from the array
setAllErrors(allErrors.filter(err => err !== errorField))
}
}, [error, allErrors, field, setAllErrors])
const editingComponent = (
<TextField
sx={{ width: fieldWidth, backgroundColor: disabled ? 'grey' : '' }}
@@ -254,7 +278,7 @@ export const FieldWithTableSelection = <T extends object, ParentType extends obj
selectorTable: ReactElement
disabled?: boolean
}) => {
const { editData, setEditData, validator } = useDetailContext<ParentType>()
const { editData, setEditData, validator, allErrors, setAllErrors } = useDetailContext<ParentType>()
const { error } = validator(editData, targetField as keyof EditDataType<ParentType>)
const [open, setOpen] = useState(false)
const selectorFn = (selected: T) => {
@@ -262,6 +286,17 @@ export const FieldWithTableSelection = <T extends object, ParentType extends obj
setOpen(false)
}
useEffect(() => {
const errorField = String(targetField)
if (error && !allErrors.includes(errorField)) {
// saves invalid field into array of errors in context
setAllErrors([...allErrors, errorField])
} else if (!error && allErrors.includes(errorField)) {
// removes valid field from the array
setAllErrors(allErrors.filter(err => err !== errorField))
}
}, [error, allErrors, targetField, setAllErrors])
const selectorTableWithFn = cloneElement(selectorTable, { selectorFn })
if (open)
return (
@@ -307,7 +342,7 @@ export const TimeBoundSelection = <T extends object, ParentType extends object>(
selectorTable: ReactElement
disabled?: boolean
}) => {
const { editData, setEditData, validator } = useDetailContext<ParentType>()
const { editData, setEditData, validator, allErrors, setAllErrors } = useDetailContext<ParentType>()
const { error: boundError } = validator(
editData,
(targetField === 'up_bnd' ? 'up_bound' : 'low_bound') as keyof EditDataType<ParentType>
@@ -323,6 +358,17 @@ export const TimeBoundSelection = <T extends object, ParentType extends object>(
setOpen(false)
}
useEffect(() => {
const errorField = String(targetField)
if (boundError && !allErrors.includes(errorField)) {
// saves invalid field into array of errors in context
setAllErrors([...allErrors, errorField])
} else if (!boundError && allErrors.includes(errorField)) {
// removes valid field from the array
setAllErrors(allErrors.filter(err => err !== errorField))
}
}, [boundError, allErrors, targetField, setAllErrors])
const selectorTableWithFn = cloneElement(selectorTable, { selectorFn })
if (open)
return (
@@ -14,15 +14,17 @@ export const WriteButton = <T,>({
onWrite: (editData: EditDataType<T>, setEditData: (editData: EditDataType<T>) => void) => Promise<void>
hasStagingMode?: boolean
}) => {
const { editData, setEditData, mode, setMode } = useDetailContext<T>()
const { editData, setEditData, mode, setMode, allErrors } = useDetailContext<T>()
const [loading, setLoading] = useState(false)
const getButtonText = () => {
if (!mode.staging) return hasStagingMode ? 'Finalize entry' : 'Save changes'
return 'Complete and save'
}
return (
<Button
disabled={allErrors.length > 0}
id="write-button"
sx={{ width: '20em' }}
onClick={() => {
@@ -54,45 +54,59 @@ export const LocalityTab = () => {
['Plate', textField('plate')],
]
const handleDmsLatChange = (value: number | string) => {
const valueAsString = String(value)
setEditData({ ...editData, dms_lat: valueAsString, dec_lat: convertDmsToDec(valueAsString, 'latitude') })
}
const handleCoordinateChange = (
value: number | string,
dmsOrDec: 'dms' | 'dec',
latitudeOrLongitude: 'latitude' | 'longitude'
) => {
const latOrLong = latitudeOrLongitude === 'latitude' ? 'lat' : 'long'
const dmsField = 'dms' + '_' + latOrLong
const decField = 'dec' + '_' + latOrLong
const handleDmsLongChange = (value: number | string) => {
const valueAsString = String(value)
setEditData({ ...editData, dms_long: valueAsString, dec_long: convertDmsToDec(valueAsString, 'longitude') })
}
const handleDecLatChange = (value: number | string) => {
if (value === '') {
setEditData({ ...editData, dec_lat: undefined, dms_lat: undefined })
return
if (dmsOrDec === 'dms') {
const valueAsString = String(value)
setEditData({
...editData,
[dmsField]: valueAsString,
[decField]: convertDmsToDec(valueAsString, latitudeOrLongitude),
})
} else {
if (value === '') {
setEditData({ ...editData, dec_long: undefined, dms_long: undefined })
return
}
const valueAsNumber = Number(value)
setEditData({
...editData,
[decField]: Number(value),
[dmsField]: convertDecToDms(valueAsNumber, latitudeOrLongitude),
})
}
const valueAsNumber = Number(value)
setEditData({ ...editData, dec_lat: Number(value), dms_lat: convertDecToDms(valueAsNumber, 'latitude') })
}
const handleDecLongChange = (value: number | string) => {
if (value === '') {
setEditData({ ...editData, dec_lat: undefined, dms_lat: undefined })
return
}
const valueAsNumber = Number(value)
setEditData({ ...editData, dec_long: Number(value), dms_long: convertDecToDms(valueAsNumber, 'longitude') })
}
const latlong = [
['', 'dms', 'dec'],
[
'Latitude',
textField('dms_lat', { type: 'text', handleSetEditData: handleDmsLatChange }),
textField('dec_lat', { type: 'number', handleSetEditData: handleDecLatChange }),
textField('dms_lat', {
type: 'text',
handleSetEditData: value => handleCoordinateChange(value, 'dms', 'latitude'),
}),
textField('dec_lat', {
type: 'number',
handleSetEditData: value => handleCoordinateChange(value, 'dec', 'latitude'),
}),
],
[
'Longitude',
textField('dms_long', { type: 'text', handleSetEditData: handleDmsLongChange }),
textField('dec_long', { type: 'number', handleSetEditData: handleDecLongChange }),
textField('dms_long', {
type: 'text',
handleSetEditData: value => handleCoordinateChange(value, 'dms', 'longitude'),
}),
textField('dec_long', {
type: 'number',
handleSetEditData: value => handleCoordinateChange(value, 'dec', 'longitude'),
}),
],
['Approximate Coordinates', dropdown('approx_coord', approximateCoordinatesOptions, 'Approximate Coordinates')],
['Altitude (m)', textField('altitude')],