Merge pull request #593 from nowcommunity/show-missing-fields-to-user-next-to-finalize-entry-button

show user all invalid fields next to finalize entry button
This commit is contained in:
Aaro Koivukangas
2024-11-28 13:43:20 +01:00
committed by GitHub
9 changed files with 144 additions and 145 deletions
@@ -4,7 +4,12 @@ import { DropdownOption } from '../common/editingComponents'
import { cloneDeep } from 'lodash-es'
import { ValidationObject } from '@/validators/validator'
import { EditDataType } from '@/backendTypes'
import { TextFieldOptions, OptionalRadioSelectionProps } from '../DetailView'
import {
TextFieldOptions,
OptionalRadioSelectionProps,
FieldsWithErrorsType,
SetFieldsWithErrorsType,
} from '../DetailView'
export type ModeOptions = 'read' | 'new' | 'edit' | 'staging-edit' | 'staging-new'
@@ -75,8 +80,8 @@ export type DetailContextType<T> = {
optionalRadioSelectionProps?: OptionalRadioSelectionProps
) => JSX.Element
validator: (editData: EditDataType<T>, field: keyof EditDataType<T>) => ValidationObject
fieldsWithErrors: Array<string>
setFieldsWithErrors: (updaterFn: (prevErrors: Array<string>) => Array<string>) => void
fieldsWithErrors: FieldsWithErrorsType
setFieldsWithErrors: SetFieldsWithErrorsType
}
export const DetailContext = createContext<DetailContextType<unknown>>(null!)
@@ -13,7 +13,7 @@ import {
} from './common/editingComponents'
import { DetailBrowser } from './DetailBrowser'
import { StagingView } from './StagingView'
import { ReturnButton, WriteButton } from './components'
import { ErrorBox, ReturnButton, WriteButton } from './components'
import { ValidationObject } from '@/validators/validator'
import { EditDataType } from '@/backendTypes'
import { usePageContext } from '../Page'
@@ -46,6 +46,11 @@ export type OptionalRadioSelectionProps = {
handleSetEditData?: (value: number | string | boolean) => void
}
export type FieldsWithErrorsType = { [field: string]: ValidationObject }
export type SetFieldsWithErrorsType = (
updaterFn: (prevFieldsWithErrors: FieldsWithErrorsType) => FieldsWithErrorsType
) => void
export const DetailView = <T extends object>({
tabs,
data,
@@ -74,7 +79,7 @@ export const DetailView = <T extends object>({
}
const { editRights } = usePageContext()
const [mode, setModeState] = useState<ModeType>(isNew ? modeOptionToMode['new'] : modeOptionToMode['read'])
const [fieldsWithErrors, setFieldsWithErrors] = useState<Array<string>>([])
const [fieldsWithErrors, setFieldsWithErrors] = useState<FieldsWithErrorsType>({})
const [tab, setTab] = useState(getUrl())
useEffect(() => {
@@ -208,6 +213,7 @@ export const DetailView = <T extends object>({
Delete
</Button>
)}
{Object.keys(fieldsWithErrors).length > 0 && <ErrorBox />}
{(!mode.read || initialState.mode.new) && onWrite && (
<WriteButton onWrite={onWrite} hasStagingMode={hasStagingMode} />
)}
@@ -11,6 +11,7 @@ import { useDetailContext } from '../Context/DetailContext'
import RemoveCircleOutlineIcon from '@mui/icons-material/RemoveCircleOutline'
import AddCircleOutlineIcon from '@mui/icons-material/AddCircleOutline'
import { useState, useEffect } from 'react'
import { checkFieldErrors } from './checkFieldErrors'
import { ActionComponent } from '@/components/TableView/ActionComponent'
const defaultPagination: MRT_PaginationState = { pageIndex: 0, pageSize: 15 }
@@ -49,23 +50,12 @@ export const EditableTable = <
const [pagination, setPagination] = useState<MRT_PaginationState>(defaultPagination)
const { editData, setEditData, mode, data, validator, fieldsWithErrors, setFieldsWithErrors } =
useDetailContext<ParentType>()
const { error } = validator(editData, field)
const errorObject = validator(editData, field)
useEffect(() => {
const errorField = String(field)
if (error && !fieldsWithErrors.includes(errorField)) {
// saves invalid field into array of errors in context
setFieldsWithErrors(prevErrors => {
return [...prevErrors, errorField]
})
} else if (!error && fieldsWithErrors.includes(errorField)) {
// removes valid field from the array
setFieldsWithErrors(prevErrors => {
return prevErrors.filter(err => err !== errorField)
})
}
checkFieldErrors(String(field), errorObject, fieldsWithErrors, setFieldsWithErrors)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [error])
}, [errorObject])
if (tableData === null || editTableData === null) return <CircularProgress />
@@ -0,0 +1,24 @@
import { ValidationObject } from '@/validators/validator'
import { FieldsWithErrorsType, SetFieldsWithErrorsType } from '../DetailView'
export const checkFieldErrors = (
field: string,
errorObject: ValidationObject,
fieldsWithErrors: FieldsWithErrorsType,
setFieldsWithErrors: SetFieldsWithErrorsType
) => {
const fieldAsString = String(field)
if (errorObject.error) {
if (!(fieldAsString in fieldsWithErrors)) {
setFieldsWithErrors(prevFieldsWithErrors => {
return { ...prevFieldsWithErrors, [fieldAsString]: errorObject }
})
}
} else if (!errorObject.error && fieldAsString in fieldsWithErrors) {
setFieldsWithErrors(prevFieldsWithErrors => {
const newFieldsWithErrors = { ...prevFieldsWithErrors }
delete newFieldsWithErrors[fieldAsString]
return newFieldsWithErrors
})
}
}
@@ -21,6 +21,7 @@ import { DataValue } from './tabLayoutHelpers'
import { modalStyle } from './misc'
import { EditDataType } from '@/backendTypes'
import { calculateLocalityMinAge, calculateLocalityMaxAge } from '@/util/ageCalculator'
import { checkFieldErrors } from './checkFieldErrors'
const fieldWidth = '14em'
@@ -53,23 +54,13 @@ export const DropdownSelector = <T extends object>({
disabled?: boolean
}) => {
const { setEditData, editData, validator, fieldsWithErrors, setFieldsWithErrors } = useDetailContext<T>()
const { error } = validator(editData, field)
const errorObject = validator(editData, field)
const { error } = errorObject
useEffect(() => {
const errorField = String(field)
if (error && !fieldsWithErrors.includes(errorField)) {
// saves invalid field into array of errors in context
setFieldsWithErrors(prevErrors => {
return [...prevErrors, errorField]
})
} else if (!error && fieldsWithErrors.includes(errorField)) {
// removes valid field from the array
setFieldsWithErrors(prevErrors => {
return prevErrors.filter(err => err !== errorField)
})
}
checkFieldErrors(String(field), errorObject, fieldsWithErrors, setFieldsWithErrors)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [error])
}, [errorObject])
const editingComponent = (
<FormControl size="small" error={!!error}>
@@ -113,27 +104,17 @@ export const DropdownSelectorWithSearch = <T extends object>({
disabled?: boolean
}) => {
const { setEditData, editData, validator, fieldsWithErrors, setFieldsWithErrors } = useDetailContext<T>()
const { error } = validator(editData, field)
const errorObject = validator(editData, field)
const { error } = errorObject
useEffect(() => {
checkFieldErrors(String(field), errorObject, fieldsWithErrors, setFieldsWithErrors)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [errorObject])
//current state of the search, not the selected option that is set into editData
const [inputValue, setInputValue] = useState('')
useEffect(() => {
const errorField = String(field)
if (error && !fieldsWithErrors.includes(errorField)) {
// saves invalid field into array of errors in context
setFieldsWithErrors(prevErrors => {
return [...prevErrors, errorField]
})
} else if (!error && fieldsWithErrors.includes(errorField)) {
// removes valid field from the array
setFieldsWithErrors(prevErrors => {
return prevErrors.filter(err => err !== errorField)
})
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [error])
const editingComponent = (
<FormControl size="small" error={!!error}>
<InputLabel id={`${name}-multiselect-label`}></InputLabel>
@@ -222,7 +203,12 @@ export const RadioSelector = <T extends object>({
handleSetEditData?: (value: number | string | boolean) => void
}) => {
const { setEditData, editData, validator, fieldsWithErrors, setFieldsWithErrors } = useDetailContext<T>()
const { error } = validator(editData, field)
const errorObject = validator(editData, field)
useEffect(() => {
checkFieldErrors(String(field), errorObject, fieldsWithErrors, setFieldsWithErrors)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [errorObject])
if (defaultValue === undefined) {
defaultValue = getValue(options[0])
@@ -235,22 +221,6 @@ export const RadioSelector = <T extends object>({
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [editData])
useEffect(() => {
const errorField = String(field)
if (error && !fieldsWithErrors.includes(errorField)) {
// saves invalid field into array of errors in context
setFieldsWithErrors(prevErrors => {
return [...prevErrors, errorField]
})
} else if (!error && fieldsWithErrors.includes(errorField)) {
// removes valid field from the array
setFieldsWithErrors(prevErrors => {
return prevErrors.filter(err => err !== errorField)
})
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [error])
const editingComponent = (
<FormControl>
<RadioGroup
@@ -317,24 +287,13 @@ export const EditableTextField = <T extends object>({
handleSetEditData?: (value: number | string) => void
}) => {
const { setEditData, editData, validator, fieldsWithErrors, setFieldsWithErrors } = useDetailContext<T>()
const { error } = validator(editData, field)
const name = String(field)
const errorObject = validator(editData, field)
const { error } = errorObject
useEffect(() => {
const errorField = String(field)
if (error && !fieldsWithErrors.includes(errorField)) {
// saves invalid field into array of errors in context
setFieldsWithErrors(prevErrors => {
return [...prevErrors, errorField]
})
} else if (!error && fieldsWithErrors.includes(errorField)) {
// removes valid field from the array
setFieldsWithErrors(prevErrors => {
return prevErrors.filter(err => err !== errorField)
})
}
checkFieldErrors(String(field), errorObject, fieldsWithErrors, setFieldsWithErrors)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [error])
}, [errorObject])
const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
const value = event?.currentTarget?.value
@@ -356,7 +315,7 @@ export const EditableTextField = <T extends object>({
<TextField
sx={{ width: fieldWidth, backgroundColor: disabled ? 'grey' : '' }}
onChange={handleChange}
id={`${name}-textfield`}
id={`${String(field)}-textfield`}
value={editData[field] ?? ''}
variant="outlined"
size="small"
@@ -384,7 +343,8 @@ export const FieldWithTableSelection = <T extends object, ParentType extends obj
disabled?: boolean
}) => {
const { editData, setEditData, validator, fieldsWithErrors, setFieldsWithErrors } = useDetailContext<ParentType>()
const { error } = validator(editData, targetField as keyof EditDataType<ParentType>)
const errorObject = validator(editData, targetField as keyof EditDataType<ParentType>)
const { error } = errorObject
const [open, setOpen] = useState(false)
const selectorFn = (selected: T) => {
setEditData({ ...editData, [targetField]: selected[sourceField] })
@@ -392,20 +352,9 @@ export const FieldWithTableSelection = <T extends object, ParentType extends obj
}
useEffect(() => {
const errorField = String(targetField)
if (error && !fieldsWithErrors.includes(errorField)) {
// saves invalid field into array of errors in context
setFieldsWithErrors(prevErrors => {
return [...prevErrors, errorField]
})
} else if (!error && fieldsWithErrors.includes(errorField)) {
// removes valid field from the array
setFieldsWithErrors(prevErrors => {
return prevErrors.filter(err => err !== errorField)
})
}
checkFieldErrors(String(targetField), errorObject, fieldsWithErrors, setFieldsWithErrors)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [error])
}, [errorObject])
const selectorTableWithFn = cloneElement(selectorTable, { selectorFn })
if (open)
@@ -454,10 +403,11 @@ export const TimeBoundSelection = <T extends object, ParentType extends object>(
disabled?: boolean
}) => {
const { editData, setEditData, validator, fieldsWithErrors, setFieldsWithErrors } = useDetailContext<ParentType>()
const { error: boundError } = validator(
const errorObject = validator(
editData,
(targetField === 'up_bnd' ? 'up_bound' : 'low_bound') as keyof EditDataType<ParentType>
)
const { error } = errorObject
const [open, setOpen] = useState(false)
const selectorFn = (selected: T) => {
@@ -470,20 +420,9 @@ export const TimeBoundSelection = <T extends object, ParentType extends object>(
}
useEffect(() => {
const errorField = String(targetField)
if (boundError && !fieldsWithErrors.includes(errorField)) {
// saves invalid field into array of errors in context
setFieldsWithErrors(prevErrors => {
return [...prevErrors, errorField]
})
} else if (!boundError && fieldsWithErrors.includes(errorField)) {
// removes valid field from the array
setFieldsWithErrors(prevErrors => {
return prevErrors.filter(err => err !== errorField)
})
}
checkFieldErrors(String(targetField), errorObject, fieldsWithErrors, setFieldsWithErrors)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [boundError])
}, [errorObject])
const selectorTableWithFn = cloneElement(selectorTable, { selectorFn })
if (open)
@@ -508,8 +447,8 @@ export const TimeBoundSelection = <T extends object, ParentType extends object>(
id={`${String(targetField)}-tableselection`}
variant="outlined"
size="small"
error={!!boundError}
helperText={boundError ?? ''}
error={!!error}
helperText={error ?? ''}
value={editData[targetField as keyof EditDataType<ParentType>] ?? undefined}
onClick={() => setOpen(true)}
disabled={disabled}
@@ -538,7 +477,8 @@ export const BasisForAgeSelection = <T extends object, ParentType extends object
disabled?: boolean
}) => {
const { editData, setEditData, validator, fieldsWithErrors, setFieldsWithErrors } = useDetailContext<ParentType>()
const { error } = validator(editData, targetField as keyof EditDataType<ParentType>)
const errorObject = validator(editData, targetField as keyof EditDataType<ParentType>)
const { error } = errorObject
const [open, setOpen] = useState(false)
const selectorFn = (selected: T) => {
if (targetField === 'bfa_min') {
@@ -568,20 +508,9 @@ export const BasisForAgeSelection = <T extends object, ParentType extends object
}
useEffect(() => {
const errorField = String(targetField)
if (error && !fieldsWithErrors.includes(errorField)) {
// saves invalid field into array of errors in context
setFieldsWithErrors(prevErrors => {
return [...prevErrors, errorField]
})
} else if (!error && fieldsWithErrors.includes(errorField)) {
// removes valid field from the array
setFieldsWithErrors(prevErrors => {
return prevErrors.filter(err => err !== errorField)
})
}
checkFieldErrors(String(targetField), errorObject, fieldsWithErrors, setFieldsWithErrors)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [error])
}, [errorObject])
const selectorTableWithFn = cloneElement(selectorTable, { selectorFn })
if (open)
@@ -1,5 +1,5 @@
import { useNavigate } from 'react-router-dom'
import { Button, CircularProgress } from '@mui/material'
import { Button, Box, Typography, CircularProgress, Divider, alpha, List, ListItemText } from '@mui/material'
import ArrowBackIcon from '@mui/icons-material/ArrowBack'
import SaveIcon from '@mui/icons-material/Save'
import { usePageContext } from '../Page'
@@ -31,14 +31,18 @@ export const WriteButton = <T,>({
if (mode.option === 'edit' || mode.option === 'new') {
for (const field in editData) {
const fieldAsString = String(field)
const { error } = validator(editData, field)
if (error && !fieldsWithErrors.includes(fieldAsString)) {
setFieldsWithErrors(prevErrors => {
return [...prevErrors, field]
})
} else if (!error && fieldsWithErrors.includes(fieldAsString)) {
setFieldsWithErrors(prevErrors => {
return prevErrors.filter(err => err !== fieldAsString)
const errorObject = validator(editData, field)
if (errorObject.error) {
if (!(fieldAsString in fieldsWithErrors)) {
setFieldsWithErrors(prevFieldsWithErrors => {
return { ...prevFieldsWithErrors, [fieldAsString]: errorObject }
})
}
} else if (!errorObject.error && fieldAsString in fieldsWithErrors) {
setFieldsWithErrors(prevFieldsWithErrors => {
const newFieldsWithErrors = { ...prevFieldsWithErrors }
delete newFieldsWithErrors[fieldAsString]
return newFieldsWithErrors
})
}
}
@@ -48,7 +52,7 @@ export const WriteButton = <T,>({
return (
<Button
disabled={fieldsWithErrors.length > 0}
disabled={Object.keys(fieldsWithErrors).length > 0}
id="write-button"
sx={{ width: '20em' }}
onClick={() => {
@@ -74,6 +78,40 @@ export const WriteButton = <T,>({
)
}
export const ErrorBox = <T,>() => {
const { fieldsWithErrors } = useDetailContext<T>()
const fields = Object.keys(fieldsWithErrors)
return (
<Box
sx={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
borderRadius: 1,
padding: '0.7em',
boxShadow: 2,
bgcolor: alpha('#ff0000', 0.5),
}}
>
<Typography color="text.secondary" gutterBottom>
{`${fields.length} Invalid fields`}
</Typography>
<List sx={{ maxHeight: '5em', maxWidth: '30em', padding: '0em 0.8em 0em 0.8em', overflow: 'auto' }}>
{fields.map(field => (
<>
<ListItemText
sx={{ color: 'text.secondary' }}
primary={`${fieldsWithErrors[field].name}: ${fieldsWithErrors[field].error}`}
/>
<Divider />
</>
))}
</List>
</Box>
)
}
export const ReturnButton = () => {
const navigate = useNavigate()
const { tableUrl } = usePageContext()
@@ -40,7 +40,7 @@ function checkIndexes(editData: EditDataType<ReferenceDetailsType>): boolean {
export const AuthorTab: React.FC<AuthorTabProps> = ({ field_num_param, tab_name = 'Authors' }) => {
const { mode, editData, setEditData, fieldsWithErrors } = useDetailContext<ReferenceDetailsType>()
const { data: authorData, isError } = useGetReferenceAuthorsQuery(mode.read ? skipToken : undefined)
const hasError = fieldsWithErrors.includes('ref_authors')
const hasError = !!fieldsWithErrors.ref_authors
useEffect(() => {
// Check and update indexes of authors only if they are incorrect
@@ -17,7 +17,7 @@ interface JournalTabProps {
export const JournalTab: React.FC<JournalTabProps> = ({ tab_name = 'Journal' }) => {
const { mode, editData, setEditData, fieldsWithErrors } = useDetailContext<ReferenceDetailsType>()
const { data: journalData, isError } = useGetReferenceJournalsQuery(mode.read ? skipToken : undefined)
const hasError = fieldsWithErrors.includes('ref_journal')
const hasError = !!fieldsWithErrors.ref_journal
let visible_ref_journal: Array<ReferenceJournalType> = []
if (editData.ref_journal && Object.keys(editData.ref_journal).length !== 0) {
visible_ref_journal = [Object.assign({}, editData.ref_journal)]
@@ -17,12 +17,19 @@ export const ReferenceTab = () => {
for (const field in editData) {
const fieldAsKey = field as keyof typeof editData
const fieldAsString = String(fieldAsKey)
const { error } = validator(editData, fieldAsKey)
if (error && !fieldsWithErrors.includes(fieldAsString)) {
setFieldsWithErrors(prevErrors => [...prevErrors, fieldAsString])
} else if (!error && fieldsWithErrors.includes(fieldAsString)) {
setFieldsWithErrors(prevErrors => prevErrors.filter(err => err !== fieldAsString))
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 => {
const newFieldsWithErrors = { ...prevFieldsWithErrors }
delete newFieldsWithErrors[fieldAsString]
return newFieldsWithErrors
})
}
}
}