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
11 changes: 8 additions & 3 deletions frontend/src/components/DetailView/Context/DetailContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand Down Expand Up @@ -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!)
Expand Down
10 changes: 8 additions & 2 deletions frontend/src/components/DetailView/DetailView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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(() => {
Expand Down Expand Up @@ -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} />
)}
Expand Down
18 changes: 4 additions & 14 deletions frontend/src/components/DetailView/common/EditableTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand Down Expand Up @@ -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 />

Expand Down
24 changes: 24 additions & 0 deletions frontend/src/components/DetailView/common/checkFieldErrors.tsx
Original file line number Diff line number Diff line change
@@ -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
})
}
}
145 changes: 37 additions & 108 deletions frontend/src/components/DetailView/common/editingComponents.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand Down Expand Up @@ -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}>
Expand Down Expand Up @@ -113,26 +104,16 @@ export const DropdownSelectorWithSearch = <T extends object>({
disabled?: boolean
}) => {
const { setEditData, editData, validator, fieldsWithErrors, setFieldsWithErrors } = useDetailContext<T>()
const { error } = validator(editData, field)

//current state of the search, not the selected option that is set into editData
const [inputValue, setInputValue] = useState('')
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])

//current state of the search, not the selected option that is set into editData
const [inputValue, setInputValue] = useState('')

const editingComponent = (
<FormControl size="small" error={!!error}>
Expand Down Expand Up @@ -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])
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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"
Expand Down Expand Up @@ -384,28 +343,18 @@ 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] })
setOpen(false)
}

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)
Expand Down Expand Up @@ -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) => {
Expand All @@ -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)
Expand All @@ -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}
Expand Down Expand Up @@ -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') {
Expand Down Expand Up @@ -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)
Expand Down
Loading