mirror of
https://github.com/nowcommunity/nowdatabase.git
synced 2026-10-12 19:13:52 +03:00
Merge branch 'main' into time-unit-test-invalid-bounds
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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')],
|
||||
|
||||
Reference in New Issue
Block a user