Skip to content

Commit 3e18f8e

Browse files
authored
Merge pull request #2646 from Northeastern-Electric-Racing/2157-work-package-templates---createedit-form
#2157 work package templates createedit form
2 parents a98e0ef + b60366a commit 3e18f8e

12 files changed

Lines changed: 436 additions & 16 deletions

src/backend/src/services/work-package-template.services.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ export default class WorkPackageTemplatesService {
4343
throw new AccessDeniedGuestException('get a work package template');
4444
}
4545

46-
const template = await prisma.work_Package_Template.findFirst({
46+
const template = await prisma.work_Package_Template.findUnique({
4747
where: {
4848
workPackageTemplateId
4949
},
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
/*
2+
* This file is part of NER's FinishLine and licensed under GNU AGPLv3.
3+
* See the LICENSE file in the repository root folder for details.
4+
*/
5+
6+
import { WorkPackageTemplate } from 'shared';
7+
import { descriptionBulletTransformer } from './projects.transformers';
8+
9+
/**
10+
* Transforms a work package template to ensure deep field transformation of date objects.
11+
*
12+
* @param workPackageTemplate Incoming work package object supplied by the HTTP response.
13+
* @returns Properly transformed work package template object.
14+
*/
15+
export const workPackageTemplateTransformer = (workPackageTemplate: WorkPackageTemplate): WorkPackageTemplate => {
16+
return {
17+
...workPackageTemplate,
18+
dateCreated: new Date(workPackageTemplate.dateCreated),
19+
dateDeleted: workPackageTemplate.dateDeleted ? new Date(workPackageTemplate.dateDeleted) : undefined,
20+
descriptionBullets: workPackageTemplate.descriptionBullets.map(descriptionBulletTransformer)
21+
};
22+
};

src/frontend/src/apis/work-packages.api.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { DescriptionBulletPreview, WbsNumber, WorkPackage, WorkPackageStage, Wor
88
import { wbsPipe } from '../utils/pipes';
99
import { apiUrls } from '../utils/urls';
1010
import { workPackageTransformer } from './transformers/work-packages.transformers';
11+
import { workPackageTemplateTransformer } from './transformers/work-package-templates.transformer';
1112

1213
export interface WorkPackageApiInputs {
1314
name: string;
@@ -132,7 +133,7 @@ export const slackUpcomingDeadlines = (deadline: Date) => {
132133
*/
133134
export const getAllWorkPackageTemplates = () => {
134135
return axios.get<WorkPackageTemplate[]>(apiUrls.workPackageTemplates(), {
135-
transformResponse: (data) => JSON.parse(data)
136+
transformResponse: (data) => JSON.parse(data).map(workPackageTemplateTransformer)
136137
});
137138
};
138139

@@ -151,7 +152,7 @@ export const deleteWorkPackageTemplate = (workPackageTemplateId: string) => {
151152
*/
152153
export const getSingleWorkPackageTemplate = (workPackageTemplateId: string) => {
153154
return axios.get<WorkPackageTemplate>(apiUrls.workPackageTemplatesById(workPackageTemplateId), {
154-
transformResponse: (data) => JSON.parse(data)
155+
transformResponse: (data) => workPackageTemplateTransformer(JSON.parse(data))
155156
});
156157
};
157158

src/frontend/src/hooks/work-packages.hooks.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,7 @@ export const useDeleteWorkPackageTemplate = () => {
184184
* Custom React Hook to get a single workpackage template
185185
*/
186186
export const useSingleWorkPackageTemplate = (workPackageTemplateId: string) => {
187-
return useQuery<WorkPackageTemplate, Error>(['work package templates'], async () => {
187+
return useQuery<WorkPackageTemplate, Error>(['work package templates', workPackageTemplateId], async () => {
188188
const { data } = await getSingleWorkPackageTemplate(workPackageTemplateId);
189189
return data;
190190
});

src/frontend/src/pages/AdminToolsPage/AdminTools.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import { useAuth } from '../../hooks/auth.hooks';
44
import { routes } from '../../utils/routes';
55
import AdminToolsPage from './AdminToolsPage';
66
import { canAccessAdminTools } from '../../utils/users';
7+
import CreateWorkPackageTemplate from '../WorkPackageTemplateForm/CreateWorkPackageTemplate';
8+
import EditWorkPackageTemplate from '../WorkPackageTemplateForm/EditWorkPackageTemplate';
79

810
const AdminTools: React.FC = () => {
911
const auth = useAuth();
@@ -22,6 +24,8 @@ const AdminTools: React.FC = () => {
2224

2325
return (
2426
<Switch>
27+
<Route path={routes.WORK_PACKAGE_TEMPLATE_NEW} component={CreateWorkPackageTemplate} />
28+
<Route path={routes.WORK_PACKAGE_TEMPLATE_EDIT} component={EditWorkPackageTemplate} />
2529
<Route path={routes.ADMIN_TOOLS} component={AdminToolsPage} />
2630
</Switch>
2731
);

src/frontend/src/pages/AdminToolsPage/ProjectsConfig/WorkPackageTemplateTable.tsx

Lines changed: 26 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,18 @@ import { isAdmin } from 'shared/src/permission-utils';
55
import { useCurrentUser } from '../../../hooks/users.hooks';
66
import LoadingIndicator from '../../../components/LoadingIndicator';
77
import ErrorPage from '../../ErrorPage';
8-
import { useAllWorkPackageTemplates, useDeleteWorkPackageTemplate } from '../../../hooks/work-packages.hooks';
8+
import { WorkPackageTemplate } from 'shared';
9+
import { routes } from '../../../utils/routes';
910
import { Delete } from '@mui/icons-material';
11+
import { useAllWorkPackageTemplates, useDeleteWorkPackageTemplate } from '../../../hooks/work-packages.hooks';
12+
import { useHistory } from 'react-router-dom';
1013
import { useState } from 'react';
1114
import NERModal from '../../../components/NERModal';
12-
import { WorkPackageTemplate } from 'shared';
1315

1416
const WorkPackageTemplateTable = () => {
1517
const currentUser = useCurrentUser();
18+
const history = useHistory();
19+
1620
const {
1721
data: workPackageTemplates,
1822
isLoading: workPackageTemplatesIsLoading,
@@ -28,26 +32,37 @@ const WorkPackageTemplateTable = () => {
2832
if (workPackageTemplatesIsError) return <ErrorPage message={workPackageTemplatesError.message} />;
2933

3034
const workPackageTemplateRows = workPackageTemplates.map((workPackageTemplate) => (
31-
<TableRow>
35+
<TableRow
36+
key={workPackageTemplate.workPackageTemplateId}
37+
onClick={() => history.push(`${routes.WORK_PACKAGE_TEMPLATE_EDIT}?id=${workPackageTemplate.workPackageTemplateId}`)}
38+
sx={{ cursor: 'pointer' }}
39+
>
3240
<TableCell align="left" sx={{ border: '2px solid black' }}>
3341
{workPackageTemplate.templateName}
3442
</TableCell>
3543
<TableCell sx={{ border: '2px solid black', verticalAlign: 'middle' }}>{workPackageTemplate.templateNotes}</TableCell>
3644
<TableCell align="center" sx={{ border: '2px solid black', verticalAlign: 'middle' }}>
37-
<IconButton onClick={() => setTemplateToDelete(workPackageTemplate)}>
45+
<IconButton
46+
onClick={(event) => {
47+
event.stopPropagation();
48+
setTemplateToDelete(workPackageTemplate);
49+
}}
50+
>
3851
<Delete />
3952
</IconButton>
4053
</TableCell>
4154
</TableRow>
4255
));
4356

4457
return (
45-
<>
46-
<Box>
47-
<AdminToolTable columns={[{ name: 'Name' }, { name: 'Description' }, { name: '' }]} rows={workPackageTemplateRows} />
48-
<Box sx={{ display: 'flex', justifyContent: 'right', marginTop: '10px' }}>
49-
{isAdmin(currentUser.role) && <NERButton variant="contained">New Work Package Template</NERButton>}
50-
</Box>
58+
<Box>
59+
<AdminToolTable columns={[{ name: 'Name' }, { name: 'Description' }]} rows={workPackageTemplateRows} />
60+
<Box sx={{ display: 'flex', justifyContent: 'right', marginTop: '10px' }}>
61+
{isAdmin(currentUser.role) && (
62+
<NERButton variant="contained" size="small" onClick={() => history.push(routes.WORK_PACKAGE_TEMPLATE_NEW)}>
63+
New Work Package Template
64+
</NERButton>
65+
)}
5166
</Box>
5267
<NERModal
5368
open={!!templateToDelete}
@@ -68,7 +83,7 @@ const WorkPackageTemplateTable = () => {
6883
</Typography>
6984
<Typography fontWeight="bold">This action cannot be undone!</Typography>
7085
</NERModal>
71-
</>
86+
</Box>
7287
);
7388
};
7489

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { useCreateSingleWorkPackageTemplate } from '../../hooks/work-packages.hooks';
2+
import WorkPackageTemplateForm from './WorkPackageTemplateForm';
3+
4+
const CreateWorkPackageTemplate: React.FC = () => {
5+
const { mutateAsync: createWorkPackageTemplateScopeCR } = useCreateSingleWorkPackageTemplate();
6+
7+
return <WorkPackageTemplateForm workPackageTemplateMutateAsync={createWorkPackageTemplateScopeCR} />;
8+
};
9+
10+
export default CreateWorkPackageTemplate;
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { useEditWorkPackageTemplate, useSingleWorkPackageTemplate } from '../../hooks/work-packages.hooks';
2+
import WorkPackageTemplateForm from './WorkPackageTemplateForm';
3+
import { useQuery } from '../../hooks/utils.hooks';
4+
import { WorkPackageTemplateFormViewPayload as WorkPackageTemplateFormInputs } from './WorkPackageTemplateFormView';
5+
import LoadingIndicator from '../../components/LoadingIndicator';
6+
import ErrorPage from '../ErrorPage';
7+
8+
const EditWorkPackageTemplate: React.FC = () => {
9+
const query = useQuery();
10+
11+
const workPackageTemplateId = query.get('id');
12+
13+
const { mutateAsync: editWorkPackageTemplate } = useEditWorkPackageTemplate(workPackageTemplateId!);
14+
15+
const { data: workPackageTemplate, isLoading, isError, error } = useSingleWorkPackageTemplate(workPackageTemplateId!);
16+
17+
if (!workPackageTemplate || isLoading) return <LoadingIndicator />;
18+
19+
if (isError) return <ErrorPage message={error.message} />;
20+
21+
const defaultValues: WorkPackageTemplateFormInputs = {
22+
...workPackageTemplate,
23+
stage: workPackageTemplate?.stage ?? 'NONE',
24+
blockedBy:
25+
workPackageTemplate?.blockedBy.map((wp) => ({
26+
id: wp.workPackageTemplateId,
27+
label: `${wp.templateName}`
28+
})) || []
29+
};
30+
31+
return (
32+
<WorkPackageTemplateForm
33+
workPackageTemplateId={workPackageTemplateId!}
34+
workPackageTemplateMutateAsync={editWorkPackageTemplate}
35+
defaultValues={defaultValues}
36+
/>
37+
);
38+
};
39+
40+
export default EditWorkPackageTemplate;
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import React from 'react';
2+
import ErrorPage from '../ErrorPage';
3+
import { useAllWorkPackageTemplates } from '../../hooks/projects.hooks';
4+
import { WorkPackageTemplateApiInputs } from '../../apis/work-packages.api';
5+
import WorkPackageTemplateFormView, { WorkPackageTemplateFormViewPayload } from './WorkPackageTemplateFormView';
6+
import { useHistory } from 'react-router-dom';
7+
import * as yup from 'yup';
8+
9+
interface WorkPackageTemplateFormProps {
10+
workPackageTemplateId?: string;
11+
workPackageTemplateMutateAsync: (data: WorkPackageTemplateApiInputs) => void;
12+
defaultValues?: WorkPackageTemplateFormViewPayload;
13+
}
14+
15+
const WorkPackageTemplateForm: React.FC<WorkPackageTemplateFormProps> = ({
16+
workPackageTemplateId,
17+
workPackageTemplateMutateAsync,
18+
defaultValues
19+
}) => {
20+
const { data: workPackageTemplates, isError: wpIsError, error: wpError } = useAllWorkPackageTemplates();
21+
22+
const history = useHistory();
23+
24+
const schema = yup.object().shape({
25+
workPackageName: yup.string().optional(),
26+
stage: yup.string().required('Stage is required'),
27+
duration: yup.number().optional(),
28+
templateName: yup.string().required('Template Name is required'),
29+
templateNotes: yup.string(),
30+
blockedBy: yup.array(),
31+
descriptionBullets: yup.array()
32+
});
33+
34+
if (wpIsError) return <ErrorPage message={wpError.message} />;
35+
36+
const blockedByOptions =
37+
workPackageTemplates
38+
?.filter((wp) => wp.workPackageTemplateId !== workPackageTemplateId)
39+
.map((wp) => ({
40+
id: wp.workPackageTemplateId,
41+
label: `${wp.templateName}`
42+
})) || [];
43+
44+
return (
45+
<WorkPackageTemplateFormView
46+
exitActiveMode={() => history.goBack()}
47+
workPackageTemplateMutateAsync={workPackageTemplateMutateAsync}
48+
defaultValues={defaultValues}
49+
blockedByOptions={blockedByOptions}
50+
schema={schema}
51+
/>
52+
);
53+
};
54+
55+
export default WorkPackageTemplateForm;
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
/*
2+
* This file is part of NER's FinishLine and licensed under GNU AGPLv3.
3+
* See the LICENSE file in the repository root folder for details.
4+
*/
5+
6+
import { WorkPackageStage } from 'shared';
7+
import { FormControl, FormLabel, Grid, MenuItem, TextField, Typography } from '@mui/material';
8+
import { Control, Controller, FieldErrorsImpl } from 'react-hook-form';
9+
import { Box } from '@mui/system';
10+
import ReactHookTextField from '../../components/ReactHookTextField';
11+
import { WorkPackageTemplateFormViewPayload } from './WorkPackageTemplateFormView';
12+
13+
interface Props {
14+
control: Control<WorkPackageTemplateFormViewPayload>;
15+
errors: Partial<FieldErrorsImpl<WorkPackageTemplateFormViewPayload>>;
16+
}
17+
18+
const WorkPackageTemplateFormDetails: React.FC<Props> = ({ control, errors }) => {
19+
const StageSelect = () => (
20+
<FormControl fullWidth>
21+
<FormLabel>Work Package Stage</FormLabel>
22+
<Controller
23+
name="stage"
24+
control={control}
25+
render={({ field: { onChange, value } }) => (
26+
<TextField select onChange={onChange} value={value} fullWidth>
27+
<MenuItem value={'NONE'}>NONE</MenuItem>
28+
{Object.values(WorkPackageStage).map((stage) => (
29+
<MenuItem key={stage} value={stage}>
30+
{stage}
31+
</MenuItem>
32+
))}
33+
</TextField>
34+
)}
35+
/>
36+
</FormControl>
37+
);
38+
39+
return (
40+
<Box>
41+
<Typography variant="h5" sx={{ marginBottom: '10px', color: 'white' }}>
42+
Work Package Details
43+
</Typography>
44+
<Grid container rowSpacing={2} spacing={1} xs={12}>
45+
<Grid item xs={12} md={4}>
46+
<FormControl fullWidth>
47+
<FormLabel>Work Package Name</FormLabel>
48+
<ReactHookTextField
49+
name="workPackageName"
50+
required={false}
51+
control={control}
52+
placeholder="Enter work package name..."
53+
errorMessage={errors.workPackageName}
54+
/>
55+
</FormControl>
56+
</Grid>
57+
<Grid item xs={12} md={4}>
58+
<StageSelect />
59+
</Grid>
60+
<Grid item xs={12} md={4}>
61+
<FormControl fullWidth>
62+
<FormLabel>Duration</FormLabel>
63+
<ReactHookTextField
64+
name="duration"
65+
required={false}
66+
control={control}
67+
type="number"
68+
placeholder="Enter duration..."
69+
errorMessage={errors.duration}
70+
/>
71+
</FormControl>
72+
</Grid>
73+
<Grid item xs={12} md={4}>
74+
<FormControl fullWidth>
75+
<FormLabel>Template Name</FormLabel>
76+
<ReactHookTextField
77+
name="templateName"
78+
control={control}
79+
placeholder="Enter template name..."
80+
errorMessage={errors.templateName}
81+
/>
82+
</FormControl>
83+
</Grid>
84+
<Grid item xs={12} md={4}>
85+
<FormControl fullWidth>
86+
<FormLabel>Template Notes</FormLabel>
87+
<ReactHookTextField
88+
name="templateNotes"
89+
control={control}
90+
placeholder="Enter notes..."
91+
errorMessage={errors.templateNotes}
92+
/>
93+
</FormControl>
94+
</Grid>
95+
</Grid>
96+
</Box>
97+
);
98+
};
99+
100+
export default WorkPackageTemplateFormDetails;

0 commit comments

Comments
 (0)