From 9cca0d1afac7624df7c760d78659e546af7fe69b Mon Sep 17 00:00:00 2001 From: Amelia Vance Date: Tue, 13 Aug 2024 14:17:37 -0400 Subject: [PATCH] Update org form to with validation, tag selection, success confirmation --- .../OrganizationForm/OrganizationForm.tsx | 226 +++++++++--------- .../OrganizationList/OrganizationList.tsx | 25 +- 2 files changed, 137 insertions(+), 114 deletions(-) diff --git a/frontend/src/components/OrganizationForm/OrganizationForm.tsx b/frontend/src/components/OrganizationForm/OrganizationForm.tsx index 168f90dc..f40e7220 100644 --- a/frontend/src/components/OrganizationForm/OrganizationForm.tsx +++ b/frontend/src/components/OrganizationForm/OrganizationForm.tsx @@ -5,12 +5,10 @@ import { Autocomplete, Button, Chip, - createFilterOptions, DialogTitle, DialogContent, DialogActions, FormControlLabel, - Grid, MenuItem, Select, Switch, @@ -18,10 +16,12 @@ import { Typography } from '@mui/material'; import { SelectChangeEvent } from '@mui/material/Select'; -import { STATE_OPTIONS } from '../../constants/constants'; +import { + STATE_ABBREVIATED_OPTIONS, + STATE_OPTIONS +} from '../../constants/constants'; import { useAuthContext } from 'context'; -const classes = orgFormStyles.classes; const StyledDialog = orgFormStyles.StyledDialog; interface AutocompleteType extends Partial { @@ -33,11 +33,22 @@ export interface OrganizationFormValues { rootDomains: string; ipBlocks: string; isPassive: boolean; - tags: OrganizationTag[]; + tags: { name: string }[]; stateName?: string | null | undefined; acronym?: string | null; + state?: string | null; } +const getStateAbbreviation = (stateName: string | null): string | undefined => { + if (stateName) { + const index = STATE_OPTIONS.indexOf(stateName); + if (index !== -1) { + return STATE_ABBREVIATED_OPTIONS[index]; + } + } + return ''; +}; + export const OrganizationForm: React.FC<{ organization?: Organization; open: boolean; @@ -45,7 +56,18 @@ export const OrganizationForm: React.FC<{ onSubmit: (values: Object) => Promise; type: string; parent?: Organization; -}> = ({ organization, onSubmit, type, open, setOpen, parent }) => { + chosenTags: string[]; + setChosenTags: Function; +}> = ({ + organization, + onSubmit, + type, + open, + setOpen, + parent, + chosenTags, + setChosenTags +}) => { const defaultValues = () => ({ name: organization ? organization.name : '', rootDomains: organization ? organization.rootDomains.join(', ') : '', @@ -56,6 +78,14 @@ export const OrganizationForm: React.FC<{ acronym: organization ? organization.acronym : '' }); + const [values, setValues] = useState(defaultValues); + const [tags, setTags] = useState([]); + const [formErrors, setFormErrors] = useState({ + name: false, + acronym: false, + rootDomains: false, + stateName: false + }); const { apiGet } = useAuthContext(); const fetchTags = useCallback(async () => { @@ -71,15 +101,21 @@ export const OrganizationForm: React.FC<{ fetchTags(); }, [fetchTags]); - const [values, setValues] = useState(defaultValues); - const [tagValue, setTagValue] = React.useState(null); - const filter = createFilterOptions(); - const [tags, setTags] = useState([]); - const [chosenTags, setChosenTags] = useState([]); const onTextChange: React.ChangeEventHandler< HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement > = (e) => onChange(e.target.name, e.target.value); + const validateForm = (values: OrganizationFormValues) => { + const newFormErrors = { + name: values.name.trim() === '', + acronym: values.acronym?.trim() === '', + rootDomains: values.rootDomains.trim() === '', + stateName: values.stateName?.trim() === '' + }; + setFormErrors(newFormErrors); + return !Object.values(newFormErrors).some((error) => error); + }; + const onChange = (name: string, value: any) => { setValues((values) => ({ ...values, @@ -89,7 +125,16 @@ export const OrganizationForm: React.FC<{ const handleStateChange = (event: SelectChangeEvent) => { setValues((values) => ({ ...values, - [event.target.name]: event.target.value + [event.target.name]: event.target.value, + state: getStateAbbreviation(event.target.value) + })); + }; + + const handleTagChange = (event: any, newValue: string[]) => { + setChosenTags(newValue); + setValues((prevValues) => ({ + ...prevValues, + tags: newValue.map((tag) => ({ name: tag })) })); }; @@ -100,6 +145,7 @@ export const OrganizationForm: React.FC<{ } } }; + return ( Organization Acronym Root Domains IP Blocks Select your State + : () => ( + + Select a US State or Territory + + ) } + error={formErrors.stateName} > {STATE_OPTIONS.map((stateName: string, index: number) => ( @@ -188,105 +247,44 @@ export const OrganizationForm: React.FC<{ ))} - {/* TODO: Fix Tag selection issues. */} - Tags + {formErrors.stateName && ( + + Organization State is required +
+
+ )} + option.name) + .filter((name): name is string => name !== undefined)} + freeSolo + value={chosenTags} + onChange={handleTagChange} + renderTags={(value: readonly string[], getTagProps) => + value.map((option: string, index: number) => { + const { key, ...tagProps } = getTagProps({ index }); + return ( + + ); + }) + } + renderInput={(params) => ( + + )} + /> - Select an existing tag or add a new one. + Select an existing tag or type and press enter to add a new one. - - {chosenTags && - chosenTags.length > 0 && - chosenTags.map((value: AutocompleteType, index: number) => ( - { - const tagIndex = chosenTags?.indexOf(value); - if (tagIndex >= 0) { - chosenTags?.splice(tagIndex, 1); - setChosenTags([...chosenTags]); - } - }} - > - ))} - - - - { - if (typeof newValue === 'string') { - setTagValue({ - name: newValue - }); - } else { - setTagValue(newValue); - } - }} - filterOptions={(options, params) => { - const filtered = filter(options, params); - // Suggest the creation of a new value - if ( - params.inputValue !== '' && - !filtered.find( - (tag) => - tag.name?.toLowerCase() === - params.inputValue.toLowerCase() - ) - ) { - filtered.push({ - name: params.inputValue, - title: `Add "${params.inputValue}"` - }); - } - return filtered; - }} - selectOnFocus - clearOnBlur - handleHomeEndKeys - options={tags.filter((i) => !chosenTags.includes(i))} - getOptionLabel={(option) => { - if (typeof option === 'string') { - return option; - } - return (option as AutocompleteType).name ?? ''; - }} - renderOption={(props, option, { selected }) => { - if (option.title) return option.title; - return option.name ?? ''; - }} - fullWidth - freeSolo - renderInput={(params) => ( - - )} - /> - - - - - +
{ + if (!validateForm(values)) { + return; + } await onSubmit({ rootDomains: values.rootDomains === '' @@ -321,8 +322,9 @@ export const OrganizationForm: React.FC<{ : values.ipBlocks.split(',').map((ip) => ip.trim()), name: values.name, stateName: values.stateName, + state: values.state, isPassive: values.isPassive, - tags: chosenTags, + tags: values.tags, acronym: values.acronym, parent: parent ? parent.id : undefined }); diff --git a/frontend/src/components/OrganizationList/OrganizationList.tsx b/frontend/src/components/OrganizationList/OrganizationList.tsx index 3e4df042..86da8246 100644 --- a/frontend/src/components/OrganizationList/OrganizationList.tsx +++ b/frontend/src/components/OrganizationList/OrganizationList.tsx @@ -1,13 +1,14 @@ import React, { useCallback, useEffect, useState } from 'react'; import EditNoteOutlinedIcon from '@mui/icons-material/EditNoteOutlined'; import { Organization } from 'types'; -import { Button, IconButton, Paper } from '@mui/material'; +import { Button, IconButton, Paper, Typography } from '@mui/material'; import { DataGrid, GridColDef, GridRenderCellParams } from '@mui/x-data-grid'; import { useHistory } from 'react-router-dom'; -import { Add } from '@mui/icons-material'; +import { Add, CheckCircleOutline } from '@mui/icons-material'; import { OrganizationForm } from 'components/OrganizationForm'; import { useAuthContext } from 'context'; import CustomToolbar from 'components/DataGrid/CustomToolbar'; +import InfoDialog from 'components/Dialog/InfoDialog'; export const OrganizationList: React.FC<{ parent?: Organization; @@ -15,6 +16,8 @@ export const OrganizationList: React.FC<{ const { apiPost, apiGet, setFeedbackMessage, user } = useAuthContext(); const [organizations, setOrganizations] = useState([]); const [dialogOpen, setDialogOpen] = useState(false); + const [infoDialogOpen, setInfoDialogOpen] = useState(false); + const [chosenTags, setChosenTags] = useState([]); const history = useHistory(); const regionId = user?.regionId; @@ -67,6 +70,7 @@ export const OrganizationList: React.FC<{ body }); setOrganizations(organizations.concat(org)); + setInfoDialogOpen(true); } catch (e: any) { setFeedbackMessage({ message: @@ -75,6 +79,7 @@ export const OrganizationList: React.FC<{ : e.message ?? e.toString(), type: 'error' }); + setChosenTags([]); console.error(e); } }; @@ -124,7 +129,23 @@ export const OrganizationList: React.FC<{ setOpen={setDialogOpen} type="create" parent={parent} + chosenTags={chosenTags} + setChosenTags={setChosenTags} > + { + setInfoDialogOpen(false); + setChosenTags([]); + }} + icon={} + title={Success } + content={ + + The new organization was successfully added. + + } + /> ); };