Skip to content

Commit e6f2c30

Browse files
authored
Merge pull request #4154 from Northeastern-Electric-Racing/#4000-guest-definition-admin
#4000 guest definition admin tools
2 parents 81ec3de + 84fc224 commit e6f2c30

6 files changed

Lines changed: 327 additions & 1 deletion

File tree

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import ErrorPage from '../../ErrorPage';
2+
import LoadingIndicator from '../../../components/LoadingIndicator';
3+
import { useCreateGuestDefinition } from '../../../hooks/recruitment.hooks';
4+
import { GuestDefinitionType } from 'shared';
5+
import GuestDefinitionFormModal from './GuestDefinitionFormModal';
6+
7+
interface CreateGuestDefinitionFormModalProps {
8+
open: boolean;
9+
handleClose: () => void;
10+
type: GuestDefinitionType;
11+
}
12+
13+
const CreateGuestDefinitionFormModal = ({ open, handleClose, type }: CreateGuestDefinitionFormModalProps) => {
14+
const { isLoading, isError, error, mutateAsync } = useCreateGuestDefinition();
15+
16+
if (isError) return <ErrorPage message={error?.message} />;
17+
if (isLoading) return <LoadingIndicator />;
18+
19+
return <GuestDefinitionFormModal open={open} handleClose={handleClose} type={type} onSubmit={mutateAsync} />;
20+
};
21+
22+
export default CreateGuestDefinitionFormModal;
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import ErrorPage from '../../ErrorPage';
2+
import LoadingIndicator from '../../../components/LoadingIndicator';
3+
import { useEditGuestDefinitions } from '../../../hooks/recruitment.hooks';
4+
import { GuestDefinition } from 'shared';
5+
import GuestDefinitionFormModal from './GuestDefinitionFormModal';
6+
7+
interface EditGuestDefinitionFormModalProps {
8+
open: boolean;
9+
handleClose: () => void;
10+
definition: GuestDefinition;
11+
}
12+
13+
const EditGuestDefinitionFormModal = ({ open, handleClose, definition }: EditGuestDefinitionFormModalProps) => {
14+
const { isLoading, isError, error, mutateAsync } = useEditGuestDefinitions(definition.definitionId);
15+
16+
if (isError) return <ErrorPage message={error?.message} />;
17+
if (isLoading) return <LoadingIndicator />;
18+
19+
return (
20+
<GuestDefinitionFormModal
21+
open={open}
22+
handleClose={handleClose}
23+
type={definition.type}
24+
defaultValues={definition}
25+
onSubmit={mutateAsync}
26+
/>
27+
);
28+
};
29+
30+
export default EditGuestDefinitionFormModal;
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
import { useForm } from 'react-hook-form';
2+
import NERFormModal from '../../../components/NERFormModal';
3+
import { FormControl, FormLabel, FormHelperText } from '@mui/material';
4+
import ReactHookTextField from '../../../components/ReactHookTextField';
5+
import * as yup from 'yup';
6+
import { yupResolver } from '@hookform/resolvers/yup';
7+
import useFormPersist from 'react-hook-form-persist';
8+
import { FormStorageKey } from '../../../utils/form';
9+
import { GuestDefinitionPayload } from '../../../hooks/recruitment.hooks';
10+
import { GuestDefinition, GuestDefinitionType } from 'shared';
11+
12+
interface GuestDefinitionFormModalProps {
13+
open: boolean;
14+
handleClose: () => void;
15+
type: GuestDefinitionType;
16+
defaultValues?: GuestDefinition;
17+
onSubmit: (data: GuestDefinitionPayload) => Promise<GuestDefinition>;
18+
}
19+
20+
const schema = yup.object().shape({
21+
term: yup.string().required('Term is required'),
22+
description: yup.string().required('Description is required'),
23+
order: yup.number().required('Order is required').min(0, 'Order must be non-negative'),
24+
icon: yup.string().optional(),
25+
buttonText: yup.string().optional(),
26+
buttonLink: yup.string().optional()
27+
});
28+
29+
const GuestDefinitionFormModal: React.FC<GuestDefinitionFormModalProps> = ({
30+
open,
31+
handleClose,
32+
type,
33+
defaultValues,
34+
onSubmit
35+
}) => {
36+
const onFormSubmit = async (data: Omit<GuestDefinitionPayload, 'type'>) => {
37+
await onSubmit({
38+
...data,
39+
type,
40+
icon: data.icon || undefined,
41+
buttonText: data.buttonText || undefined,
42+
buttonLink: data.buttonLink || undefined
43+
});
44+
handleClose();
45+
};
46+
47+
const {
48+
handleSubmit,
49+
control,
50+
reset,
51+
formState: { errors },
52+
watch,
53+
setValue
54+
} = useForm({
55+
resolver: yupResolver(schema),
56+
defaultValues: {
57+
term: defaultValues?.term ?? '',
58+
description: defaultValues?.description ?? '',
59+
order: defaultValues?.order ?? 0,
60+
icon: defaultValues?.icon ?? '',
61+
buttonText: defaultValues?.buttonText ?? '',
62+
buttonLink: defaultValues?.buttonLink ?? ''
63+
}
64+
});
65+
66+
const formStorageKey = defaultValues ? FormStorageKey.EDIT_GUEST_DEFINITION : FormStorageKey.CREATE_GUEST_DEFINITION;
67+
68+
useFormPersist(formStorageKey, { watch, setValue });
69+
70+
const handleCancel = () => {
71+
reset({ term: '', description: '', order: 0, icon: '', buttonText: '', buttonLink: '' });
72+
sessionStorage.removeItem(formStorageKey);
73+
handleClose();
74+
};
75+
76+
return (
77+
<NERFormModal
78+
open={open}
79+
onHide={handleCancel}
80+
title={`${defaultValues ? 'Edit' : 'New'} ${type === GuestDefinitionType.PROJECT_MANAGEMENT ? 'Project Management' : 'Info Page'} Definition`}
81+
reset={() => reset({ term: '', description: '', order: 0, icon: '', buttonText: '', buttonLink: '' })}
82+
handleUseFormSubmit={handleSubmit}
83+
onFormSubmit={onFormSubmit}
84+
formId="guest-definition-form"
85+
showCloseButton
86+
>
87+
<FormControl fullWidth>
88+
<FormLabel>Term*</FormLabel>
89+
<ReactHookTextField name="term" control={control} placeholder="Enter term" sx={{ width: 1 }} />
90+
<FormHelperText error>{errors.term?.message}</FormHelperText>
91+
</FormControl>
92+
<FormControl fullWidth>
93+
<FormLabel>Description*</FormLabel>
94+
<ReactHookTextField name="description" control={control} placeholder="Enter description" />
95+
<FormHelperText error>{errors.description?.message}</FormHelperText>
96+
</FormControl>
97+
<FormControl fullWidth>
98+
<FormLabel>Order*</FormLabel>
99+
<ReactHookTextField name="order" control={control} type="number" placeholder="Enter display order" />
100+
<FormHelperText error>{errors.order?.message}</FormHelperText>
101+
</FormControl>
102+
<FormControl fullWidth>
103+
<FormLabel>Icon</FormLabel>
104+
<ReactHookTextField name="icon" control={control} placeholder="Enter icon name (optional)" />
105+
<FormHelperText error>{errors.icon?.message}</FormHelperText>
106+
</FormControl>
107+
<FormControl fullWidth>
108+
<FormLabel>Button Text</FormLabel>
109+
<ReactHookTextField name="buttonText" control={control} placeholder="Enter button text (optional)" />
110+
<FormHelperText error>{errors.buttonText?.message}</FormHelperText>
111+
</FormControl>
112+
<FormControl fullWidth>
113+
<FormLabel>Button Link</FormLabel>
114+
<ReactHookTextField name="buttonLink" control={control} placeholder="Enter button link (optional)" />
115+
<FormHelperText error>{errors.buttonLink?.message}</FormHelperText>
116+
</FormControl>
117+
</NERFormModal>
118+
);
119+
};
120+
121+
export default GuestDefinitionFormModal;
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
import React, { useState } from 'react';
2+
import { TableRow, TableCell, Box, Table as MuiTable, TableHead, TableBody, Typography, Button } from '@mui/material';
3+
import EditIcon from '@mui/icons-material/Edit';
4+
import DeleteIcon from '@mui/icons-material/Delete';
5+
import { GuestDefinition, GuestDefinitionType } from 'shared';
6+
import { NERButton } from '../../../components/NERButton';
7+
import { useAllGuestDefinitions, useDeleteGuestDefinition } from '../../../hooks/recruitment.hooks';
8+
import LoadingIndicator from '../../../components/LoadingIndicator';
9+
import { useHistoryState } from '../../../hooks/misc.hooks';
10+
import ErrorPage from '../../ErrorPage';
11+
import NERDeleteModal from '../../../components/NERDeleteModal';
12+
import { useToast } from '../../../hooks/toasts.hooks';
13+
import CreateGuestDefinitionFormModal from './CreateGuestDefinitionFormModal';
14+
import EditGuestDefinitionFormModal from './EditGuestDefinitionFormModal';
15+
16+
interface GuestDefinitionsTableProps {
17+
type: GuestDefinitionType;
18+
}
19+
20+
const GuestDefinitionsTable = ({ type }: GuestDefinitionsTableProps) => {
21+
const [createModalShow, setCreateModalShow] = useHistoryState<boolean>('', false);
22+
const [definitionEditing, setDefinitionEditing] = useHistoryState<GuestDefinition | undefined>('', undefined);
23+
const [definitionToDelete, setDefinitionToDelete] = useState<GuestDefinition | undefined>(undefined);
24+
const { mutateAsync: deleteGuestDefinition } = useDeleteGuestDefinition();
25+
const toast = useToast();
26+
27+
const { isLoading, isError, error, data: allDefinitions } = useAllGuestDefinitions();
28+
29+
const handleDelete = async (id: string) => {
30+
setDefinitionToDelete(undefined);
31+
try {
32+
await deleteGuestDefinition(id);
33+
toast.success('Guest definition deleted successfully');
34+
} catch (e: unknown) {
35+
if (e instanceof Error) {
36+
toast.error(e.message, 3000);
37+
}
38+
}
39+
};
40+
41+
if (isError) return <ErrorPage message={error.message} />;
42+
if (!allDefinitions || isLoading) return <LoadingIndicator />;
43+
44+
const definitions = allDefinitions.filter((d) => d.type === type);
45+
46+
const rows = definitions.map((definition: GuestDefinition, index: number) => (
47+
<TableRow key={definition.definitionId}>
48+
<TableCell
49+
align="left"
50+
sx={{ borderBottom: index === definitions.length - 1 ? 'none' : 'default', alignItems: 'center' }}
51+
>
52+
<Typography>{definition.term}</Typography>
53+
</TableCell>
54+
<TableCell
55+
sx={{
56+
display: 'flex',
57+
justifyContent: 'space-between',
58+
alignItems: 'center',
59+
borderBottom: index === definitions.length - 1 ? 'none' : 'default',
60+
minHeight: '50px'
61+
}}
62+
>
63+
<Typography sx={{ maxWidth: 300 }}>{definition.description}</Typography>
64+
<Box sx={{ display: 'flex' }}>
65+
<Button sx={{ p: 0.5, color: 'white' }} onClick={() => setDefinitionEditing(definition)}>
66+
<EditIcon />
67+
</Button>
68+
<Button sx={{ p: 0.5, color: 'white' }} onClick={() => setDefinitionToDelete(definition)}>
69+
<DeleteIcon />
70+
</Button>
71+
</Box>
72+
</TableCell>
73+
</TableRow>
74+
));
75+
76+
return (
77+
<Box>
78+
{createModalShow && (
79+
<CreateGuestDefinitionFormModal open={createModalShow} handleClose={() => setCreateModalShow(false)} type={type} />
80+
)}
81+
{definitionEditing && (
82+
<EditGuestDefinitionFormModal
83+
open={!!definitionEditing}
84+
handleClose={() => setDefinitionEditing(undefined)}
85+
definition={definitionEditing}
86+
/>
87+
)}
88+
<MuiTable>
89+
<TableHead>
90+
<TableRow>
91+
<TableCell
92+
sx={{
93+
fontWeight: 'bold',
94+
fontSize: '1em',
95+
backgroundColor: '#ef4345',
96+
color: 'white',
97+
borderRadius: '10px 0px 0px 0px'
98+
}}
99+
>
100+
Term
101+
</TableCell>
102+
<TableCell
103+
sx={{
104+
fontWeight: 'bold',
105+
fontSize: '1em',
106+
backgroundColor: '#ef4345',
107+
color: 'white',
108+
borderRadius: '0px 10px 0px 0px'
109+
}}
110+
>
111+
Description
112+
</TableCell>
113+
</TableRow>
114+
</TableHead>
115+
<TableBody>{rows}</TableBody>
116+
</MuiTable>
117+
<Box sx={{ display: 'flex', justifyContent: 'right', marginTop: '20px' }}>
118+
<NERButton variant="contained" onClick={() => setCreateModalShow(true)}>
119+
Add Guest Definition
120+
</NERButton>
121+
</Box>
122+
{definitionToDelete && (
123+
<NERDeleteModal
124+
open={!!definitionToDelete}
125+
onHide={() => setDefinitionToDelete(undefined)}
126+
formId="delete-guest-definition-form"
127+
dataType="Guest Definition"
128+
onFormSubmit={() => {
129+
handleDelete(definitionToDelete.definitionId);
130+
}}
131+
/>
132+
)}
133+
</Box>
134+
);
135+
};
136+
137+
export default GuestDefinitionsTable;

src/frontend/src/pages/AdminToolsPage/EditGuestView/GuestViewConfig.tsx

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ import { useToast } from '../../../hooks/toasts.hooks';
2020
import { MAX_FILE_SIZE } from 'shared';
2121
import UsefulLinksTable from '../OnboardingConfig/UsefulLinks/UsefulLinksTable';
2222
import LinkTypeTable from '../ProjectsConfig/LinkTypes/LinkTypeTable';
23+
import GuestDefinitionsTable from './GuestDefinitionsTable';
24+
import { GuestDefinitionType } from 'shared';
2325

2426
const platformDescriptionSchema = yup.object().shape({
2527
platformDescription: yup.string().required()
@@ -178,6 +180,18 @@ const GuestViewConfig: React.FC = () => {
178180
</Typography>
179181
<LinkTypeTable isOnGuestHomePage={true} />
180182
</Grid>
183+
<Grid item xs={6}>
184+
<Typography variant="h5" gutterBottom borderBottom={1} color="#ef4345" borderColor={'white'}>
185+
Project Management Definitions
186+
</Typography>
187+
<GuestDefinitionsTable type={GuestDefinitionType.PROJECT_MANAGEMENT} />
188+
</Grid>
189+
<Grid item xs={6}>
190+
<Typography variant="h5" gutterBottom borderBottom={1} color="#ef4345" borderColor={'white'}>
191+
Info Page Definitions
192+
</Typography>
193+
<GuestDefinitionsTable type={GuestDefinitionType.INFO_PAGE} />
194+
</Grid>
181195
</Grid>
182196
);
183197
};

src/frontend/src/utils/form.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,5 +86,7 @@ export enum FormStorageKey {
8686
CREATE_MACHINERY = 'CREATE_MACHINERY',
8787
EDIT_MACHINERY = 'EDIT_MACHINERY',
8888
CREATE_EVENT_TYPE = 'CREATE_EVENT_TYPE',
89-
EDIT_EVENT_TYPE = 'EDIT_EVENT_TYPE'
89+
EDIT_EVENT_TYPE = 'EDIT_EVENT_TYPE',
90+
EDIT_GUEST_DEFINITION = 'EDIT_GUEST_DEFINITION',
91+
CREATE_GUEST_DEFINITION = 'CREATE_GUEST_DEFINITION'
9092
}

0 commit comments

Comments
 (0)