Skip to content
Prev Previous commit
Next Next commit
SIMSBIOHUB-307: Telemetry Device Validation Fixes (#1119)
* Refactored FloatingPointField component into more flexible CustomNumberField that adds integer support.
* Added more constraints to telemetry device schema to better align with BCTW API limitations.
* Added logic to reduce unnecessary calls to BCTW API from survey animals page.
  • Loading branch information
JeremyQuartech authored Oct 16, 2023
commit 9cb813d1ffc3e3169e20ae17dfe5f93546b1a951
73 changes: 73 additions & 0 deletions app/src/components/fields/CustomNumberField.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import TextField, { TextFieldProps } from '@mui/material/TextField';
import { useFormikContext } from 'formik';
import get from 'lodash-es/get';
import React from 'react';
import NumberFormat, { NumberFormatProps } from 'react-number-format';

type BaseNumberFieldProps = {
name: string;
min?: number;
max?: number;
float?: boolean;
};

type INumberFieldProps = TextFieldProps & BaseNumberFieldProps & { required?: boolean; label: string };

type NumberFormatCustomProps = BaseNumberFieldProps & {
onChange: (event: { target: { name: string; value: number } }) => void;
};

const NumberFormatCustom = React.forwardRef<NumberFormatProps, NumberFormatCustomProps>(function NumericFormatCustom(
props,
ref
) {
const { onChange, min, max, float, ...other } = props;

return (
<NumberFormat
{...other}
getInputRef={ref}
onValueChange={(values) => {
onChange({
target: {
name: props.name,
value: float ? parseFloat(values.value) : parseInt(values.value)
}
});
}}
decimalScale={float ? 7 : 0}
isAllowed={(values) => {
const value = float ? parseFloat(values.value) : parseInt(values.value);
return (
values.value === '' ||
(value >= (min ?? Number.MIN_SAFE_INTEGER) && value <= (max ?? Number.MAX_SAFE_INTEGER))
);
}}
/>
);
});

const CustomNumberField: React.FC<INumberFieldProps> = (props) => {
const { values, handleChange, touched, errors } = useFormikContext<INumberFieldProps>();

const { name, min, max, float, ...rest } = props;

return (
<TextField
{...rest}
name={name}
variant="outlined"
value={get(values, name)}
onChange={handleChange}
error={get(touched, name) && Boolean(get(errors, name))}
helperText={get(touched, name) && (get(errors, name) as string)}
InputProps={{
inputComponent: NumberFormatCustom as any
}}
inputProps={{ 'data-testid': name, min, max }}
fullWidth={true}
/>
);
};

export default CustomNumberField;
73 changes: 0 additions & 73 deletions app/src/components/fields/FloatingPointField.tsx

This file was deleted.

2 changes: 2 additions & 0 deletions app/src/constants/misc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,5 @@ export enum SurveyStatusType {
export enum DocumentReviewStatus {
PENDING = 'Pending Review'
}

export const PG_MAX_INT = 2147483647;
32 changes: 20 additions & 12 deletions app/src/features/surveys/view/SurveyAnimals.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,10 @@ const SurveyAnimals: React.FC = () => {
.string()
.required('Required')
.test('checkDeviceMakeIsNotChanged', '', async (value, context) => {
// Bypass to avoid an api call when invalid device_id
if (!context.parent.device_id) {
return true;
}
const deviceDetails = await telemetryApi.devices.getDeviceDetails(Number(context.parent.device_id));
if (deviceDetails.device?.device_make && deviceDetails.device?.device_make !== value) {
return context.createError({
Expand All @@ -193,12 +197,14 @@ const SurveyAnimals: React.FC = () => {
.test('checkDeploymentRange', '', async (value, context) => {
const upperLevelIndex = Number(context.path.match(/\[(\d+)\]/)?.[1]); //Searches [0].deployments[0].attachment_start for the number contained in first index.
const deviceId = context.options.context?.[upperLevelIndex]?.device_id;
const errStr = await deploymentOverlapTest(
deviceId,
context.parent.deployment_id,
value,
context.parent.attachment_end
);
const errStr = deviceId
? await deploymentOverlapTest(
deviceId,
context.parent.deployment_id,
value,
context.parent.attachment_end
)
: '';
if (errStr.length) {
return context.createError({ message: errStr });
} else {
Expand All @@ -213,12 +219,14 @@ const SurveyAnimals: React.FC = () => {
.test('checkDeploymentRangeEnd', '', async (value, context) => {
const upperLevelIndex = Number(context.path.match(/\[(\d+)\]/)?.[1]); //Searches [0].deployments[0].attachment_start for the number contained in first index.
const deviceId = context.options.context?.[upperLevelIndex]?.device_id;
const errStr = await deploymentOverlapTest(
deviceId,
context.parent.deployment_id,
context.parent.attachment_start,
value
);
const errStr = deviceId
? await deploymentOverlapTest(
deviceId,
context.parent.deployment_id,
context.parent.attachment_start,
value
)
: '';
if (errStr.length) {
return context.createError({ message: errStr });
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,12 @@ import CardHeader from '@mui/material/CardHeader';
import { grey } from '@mui/material/colors';
import Grid from '@mui/material/Grid';
import YesNoDialog from 'components/dialog/YesNoDialog';
import CustomNumberField from 'components/fields/CustomNumberField';
import CustomTextField from 'components/fields/CustomTextField';
import SingleDateField from 'components/fields/SingleDateField';
import TelemetrySelectField from 'components/fields/TelemetrySelectField';
import { AttachmentType } from 'constants/attachments';
import { PG_MAX_INT } from 'constants/misc';
import { Form, useFormikContext } from 'formik';
import useDataLoader from 'hooks/useDataLoader';
import { useTelemetryApi } from 'hooks/useTelemetryApi';
Expand Down Expand Up @@ -156,7 +158,13 @@ const DeviceFormSection = ({ values, index, mode, removeAction }: IDeviceFormSec
</Typography>
<Grid container spacing={3}>
<Grid item xs={6}>
<CustomTextField label="Device ID" name={`${index}.device_id`} other={{ disabled: mode === 'edit' }} />
<CustomNumberField
label="Device ID"
name={`${index}.device_id`}
disabled={mode === 'edit'}
min={1}
max={PG_MAX_INT}
/>
</Grid>
<Grid item xs={6}>
<Grid container>
Expand Down
8 changes: 6 additions & 2 deletions app/src/features/surveys/view/survey-animals/device.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,11 @@ export type ITelemetryPointCollection = { points: FeatureCollection; tracks: Fea

const req = 'Required.';
const mustBeNum = 'Must be a number';
const numSchema = yup.number().typeError(mustBeNum);
const mustBePos = 'Must be positive';
const mustBeInt = 'Must be an integer';
const maxInt = 2147483647;
const numSchema = yup.number().typeError(mustBeNum).min(0, mustBePos);
const intSchema = numSchema.max(maxInt, `Must be less than ${maxInt}`).integer(mustBeInt).required(req);

export const AnimalDeploymentTimespanSchema = yup.object({}).shape({
deployment_id: yup.string(),
Expand All @@ -21,7 +25,7 @@ export const AnimalDeploymentTimespanSchema = yup.object({}).shape({
});

export const AnimalTelemetryDeviceSchema = yup.object({}).shape({
device_id: numSchema.required(req),
device_id: intSchema,
device_make: yup.string().required(req),
frequency: numSchema,
frequency_unit: yup.string().nullable(),
Expand Down