Skip to content

Commit

Permalink
FI-2075: Input validation (#388)
Browse files Browse the repository at this point in the history
* prevent modal from closing when inputs have been edited

* remove log

* FI-2003: Add codecov.yml (#384)

* add codecov

* turn codecov on

* modify backend target

* add patch coverage

---------

Co-authored-by: Alyssa Wang <[email protected]>

* add carryforward

* check edits on serial inputs

* add close modal button to inputs dialog

* keep edited status consistent between input types

* add change listening to inputs

* add required input warning

* update oauth serial parsing

* include null checks on oauth refresh nested fields

---------

Co-authored-by: Alyssa Wang <[email protected]>
  • Loading branch information
AlyssaWang and AlyssaWang authored Sep 15, 2023
1 parent e974bc7 commit c7ca4bf
Show file tree
Hide file tree
Showing 8 changed files with 86 additions and 31 deletions.
5 changes: 4 additions & 1 deletion client/src/components/InputsModal/FieldLabel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@ import React, { FC } from 'react';
import LockIcon from '@mui/icons-material/Lock';
import { TestInput } from '~/models/testSuiteModels';
import useStyles from './styles';
import RequiredInputWarning from './RequiredInputWarning';

export interface FieldLabelProps {
requirement: TestInput;
isMissingInput?: boolean;
}

const FieldLabel: FC<FieldLabelProps> = ({ requirement }) => {
const FieldLabel: FC<FieldLabelProps> = ({ requirement, isMissingInput = false }) => {
const { classes } = useStyles();

const fieldLabelText = requirement.title || requirement.name;
Expand All @@ -20,6 +22,7 @@ const FieldLabel: FC<FieldLabelProps> = ({ requirement }) => {

return (
<>
{isMissingInput && <RequiredInputWarning />}
{fieldLabelText}
{requiredLabel}
{lockedIcon}
Expand Down
16 changes: 14 additions & 2 deletions client/src/components/InputsModal/InputCheckboxGroup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ const InputCheckboxGroup: FC<InputCheckboxGroupProps> = ({
setInputsMap,
}) => {
const { classes } = useStyles();
const [hasBeenModified, setHasBeenModified] = React.useState(false);

const [values, setValues] = React.useState<CheckboxValues>(() => {
// Default values should be in form ['value'] where all values are checked
Expand Down Expand Up @@ -61,6 +62,9 @@ const InputCheckboxGroup: FC<InputCheckboxGroupProps> = ({
return startingValues as CheckboxValues;
});

const isMissingInput =
hasBeenModified && !requirement.optional && inputsMap.get(requirement.name) === '[]';

useEffect(() => {
// Make sure starting values get set in inputsMap
inputsMap.set(requirement.name, transformValuesToJSONArray(values));
Expand All @@ -75,6 +79,7 @@ const InputCheckboxGroup: FC<InputCheckboxGroupProps> = ({
inputsMap.set(requirement.name, transformValuesToJSONArray(newValues));
setInputsMap(new Map(inputsMap));
setValues(newValues);
setHasBeenModified(true);
};

// Convert map from item name to checked status back to array of checked values
Expand All @@ -93,11 +98,13 @@ const InputCheckboxGroup: FC<InputCheckboxGroupProps> = ({
component="fieldset"
id={`requirement${index}_input`}
disabled={requirement.locked}
required={!requirement.optional}
error={isMissingInput}
fullWidth
className={classes.inputField}
>
<FormLabel required={!requirement.optional} className={classes.inputLabel}>
<FieldLabel requirement={requirement} />
<FormLabel className={classes.inputLabel}>
<FieldLabel requirement={requirement} isMissingInput={isMissingInput} />
</FormLabel>
{requirement.description && (
<FormHelperText sx={{ mx: 0 }}>{requirement.description}</FormHelperText>
Expand All @@ -111,6 +118,11 @@ const InputCheckboxGroup: FC<InputCheckboxGroupProps> = ({
color="secondary"
name={option.value}
checked={values[option.value as keyof CheckboxValues] || false}
onBlur={(e) => {
if (e.currentTarget === e.target) {
setHasBeenModified(true);
}
}}
onChange={handleChange}
/>
}
Expand Down
49 changes: 34 additions & 15 deletions client/src/components/InputsModal/InputOAuthCredentials.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
import { OAuthCredentials, TestInput } from '~/models/testSuiteModels';
import FieldLabel from './FieldLabel';
import useStyles from './styles';
import RequiredInputWarning from './RequiredInputWarning';

export interface InputOAuthCredentialsProps {
requirement: TestInput;
Expand All @@ -35,22 +36,20 @@ const InputOAuthCredentials: FC<InputOAuthCredentialsProps> = ({
setInputsMap,
}) => {
const { classes } = useStyles();
const [hasBeenModified, setHasBeenModified] = React.useState({});

// Convert OAuth string to Object
// OAuth should be an Object while in this component but should be converted to a string
// before being updated in the inputs map
const oAuthCredentials = (
inputsMap.get(requirement.name)
? JSON.parse(inputsMap.get(requirement.name) as string)
: {
access_token: '',
refresh_token: '',
expires_in: '',
client_id: '',
client_secret: '',
token_url: '',
}
) as OAuthCredentials;
const oAuthCredentials = {
access_token: '',
refresh_token: '',
expires_in: '',
client_id: '',
client_secret: '',
token_url: '',
...JSON.parse((inputsMap.get(requirement.name) as string) || '{}'),
} as OAuthCredentials;

const showRefreshDetails = !!oAuthCredentials.refresh_token;

Expand Down Expand Up @@ -91,16 +90,32 @@ const InputOAuthCredentials: FC<InputOAuthCredentialsProps> = ({
},
];

const getIsMissingInput = (field: InputOAuthField) => {
return (
hasBeenModified[field.name as keyof typeof hasBeenModified] &&
field.required &&
!oAuthCredentials[field.name as keyof OAuthCredentials]
);
};

const oAuthField = (field: InputOAuthField) => {
const fieldLabel = field.required
const fieldName = field.required
? `${(field.label || field.name) as string} (required)`
: field.label || field.name;

const fieldLabel = (
<>
{getIsMissingInput(field) && <RequiredInputWarning />}
{fieldName}
</>
);

return (
<ListItem disabled={field.locked} key={field.name}>
<TextField
disabled={requirement.locked}
required={field.required}
error={field.required && !oAuthCredentials[field.name as keyof OAuthCredentials]}
error={getIsMissingInput(field)}
id={`requirement${index}_${field.name}`}
label={fieldLabel}
helperText={requirement.description}
Expand All @@ -109,6 +124,11 @@ const InputOAuthCredentials: FC<InputOAuthCredentialsProps> = ({
variant="standard"
color="secondary"
fullWidth
onBlur={(e) => {
if (e.currentTarget === e.target) {
setHasBeenModified({ ...hasBeenModified, [field.name]: true });
}
}}
onChange={(event) => {
const value = event.target.value;
oAuthCredentials[field.name as keyof OAuthCredentials] = value;
Expand All @@ -129,7 +149,6 @@ const InputOAuthCredentials: FC<InputOAuthCredentialsProps> = ({
required={!requirement.optional}
disabled={requirement.locked}
className={classes.inputLabel}
shrink
>
<FieldLabel requirement={requirement} />
</InputLabel>
Expand Down
13 changes: 10 additions & 3 deletions client/src/components/InputsModal/InputTextArea.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,27 +16,34 @@ const InputTextArea: FC<InputTextAreaProps> = ({ requirement, index, inputsMap,
const { classes } = useStyles();
const [hasBeenModified, setHasBeenModified] = React.useState(false);

const isMissingInput =
hasBeenModified && !requirement.optional && !inputsMap.get(requirement.name);

return (
<ListItem>
<TextField
disabled={requirement.locked}
required={!requirement.optional}
error={hasBeenModified && !requirement.optional && !inputsMap.get(requirement.name)}
error={isMissingInput}
id={`requirement${index}_input`}
className={classes.inputField}
variant="standard"
color="secondary"
fullWidth
label={<FieldLabel requirement={requirement} />}
label={<FieldLabel requirement={requirement} isMissingInput={isMissingInput} />}
helperText={requirement.description}
value={inputsMap.get(requirement.name)}
multiline
rows={4}
onBlur={(e) => {
if (e.currentTarget === e.target) {
setHasBeenModified(true);
}
}}
onChange={(event) => {
const value = event.target.value;
inputsMap.set(requirement.name, value);
setInputsMap(new Map(inputsMap));
setHasBeenModified(true);
}}
FormHelperTextProps={{
sx: { '&.Mui-disabled': { color: lightTheme.palette.common.grayDark } },
Expand Down
13 changes: 10 additions & 3 deletions client/src/components/InputsModal/InputTextField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,25 +21,32 @@ const InputTextField: FC<InputTextFieldProps> = ({
const { classes } = useStyles();
const [hasBeenModified, setHasBeenModified] = React.useState(false);

const isMissingInput =
hasBeenModified && !requirement.optional && !inputsMap.get(requirement.name);

return (
<ListItem>
<TextField
disabled={requirement.locked}
required={!requirement.optional}
error={hasBeenModified && !requirement.optional && !inputsMap.get(requirement.name)}
error={isMissingInput}
id={`requirement${index}_input`}
className={classes.inputField}
variant="standard"
color="secondary"
fullWidth
label={<FieldLabel requirement={requirement} />}
label={<FieldLabel requirement={requirement} isMissingInput={isMissingInput} />}
helperText={requirement.description}
value={inputsMap.get(requirement.name)}
onBlur={(e) => {
if (e.currentTarget === e.target) {
setHasBeenModified(true);
}
}}
onChange={(event) => {
const value = event.target.value;
inputsMap.set(requirement.name, value);
setInputsMap(new Map(inputsMap));
setHasBeenModified(true);
}}
InputLabelProps={{ shrink: true }}
FormHelperTextProps={{
Expand Down
6 changes: 3 additions & 3 deletions client/src/components/InputsModal/InputsModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -98,8 +98,7 @@ const InputsModal: FC<InputsModalProps> = ({
) as OAuthCredentials;
const accessTokenIsEmpty = oAuthJSON.access_token === '';
const refreshIsEmpty =
oAuthJSON.refresh_token !== '' &&
(oAuthJSON.token_url === '' || oAuthJSON.client_id === '');
oAuthJSON.refresh_token !== '' && (!oAuthJSON.token_url || !oAuthJSON.client_id);
oAuthMissingRequiredInput = (accessTokenIsEmpty && !input.optional) || refreshIsEmpty;
} catch (e: unknown) {
const errorMessage = e instanceof Error ? e.message : String(e);
Expand Down Expand Up @@ -281,8 +280,9 @@ const InputsModal: FC<InputsModalProps> = ({
const parsedChanges = parseSerialChanges(serialChanges);
if (parsedChanges !== undefined && parsedChanges.keys !== undefined) {
parsedChanges.forEach((change: TestInput) => {
if (!change.locked && change.value !== undefined)
if (!change.locked && change.value !== undefined) {
inputsMap.set(change.name, change.value || '');
}
});
}
handleSetInputsMap(new Map(inputsMap), true);
Expand Down
12 changes: 10 additions & 2 deletions client/src/components/InputsModal/RequiredInputWarning.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,19 @@
import React, { FC } from 'react';
import WarningIcon from '@mui/icons-material/Warning';
import { Report } from '@mui/icons-material';
import CustomTooltip from '~/components/_common/CustomTooltip';

const RequiredInputWarning: FC = () => {
return (
<CustomTooltip title="Missing value for required input">
<WarningIcon fontSize="small" color="error" />
<Report
color="error"
aria-hidden={false}
tabIndex={0}
sx={{
marginRight: '4px',
verticalAlign: 'bottom',
}}
/>
</CustomTooltip>
);
};
Expand Down
3 changes: 1 addition & 2 deletions client/src/components/InputsModal/styles.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,8 @@ export default makeStyles()((theme: Theme) => ({
},
},
inputLabel: {
color: theme.palette.common.grayDarkest,
color: theme.palette.common.grayDarker,
fontWeight: 600,
fontSize: '.75rem',
},
lockedIcon: {
marginLeft: '5px',
Expand Down

0 comments on commit c7ca4bf

Please sign in to comment.