Skip to content

Commit d772d94

Browse files
authored
Merge pull request #4296 from Northeastern-Electric-Racing/#4260-edit-car-names
#4260 edit car names
2 parents 730b682 + c64f55d commit d772d94

8 files changed

Lines changed: 154 additions & 7 deletions

File tree

src/backend/src/controllers/cars.controllers.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,4 +22,15 @@ export default class CarsController {
2222
next(error);
2323
}
2424
}
25+
26+
static async editCar(req: Request, res: Response, next: NextFunction) {
27+
try {
28+
const { carId } = req.params as Record<string, string>;
29+
const { name } = req.body;
30+
const car = await CarsService.editCar(carId, req.organization, req.currentUser, name);
31+
res.status(200).json(car);
32+
} catch (error) {
33+
next(error);
34+
}
35+
}
2536
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
import express from 'express';
22
import CarsController from '../controllers/cars.controllers.js';
3+
import { nonEmptyString, validateInputs } from '../utils/validation.utils.js';
4+
import { body } from 'express-validator';
35

46
const carsRouter = express.Router();
57

68
carsRouter.get('/', CarsController.getAllCars);
79

810
carsRouter.post('/create', CarsController.createCar);
11+
carsRouter.post('/:carId/edit', nonEmptyString(body('name')), validateInputs, CarsController.editCar);
912

1013
export default carsRouter;

src/backend/src/services/car.services.ts

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { isAdmin, User } from 'shared';
33
import { getCarQueryArgs } from '../prisma-query-args/cars.query-args.js';
44
import prisma from '../prisma/prisma.js';
55
import { carTransformer } from '../transformers/cars.transformer.js';
6-
import { AccessDeniedAdminOnlyException } from '../utils/errors.utils.js';
6+
import { AccessDeniedAdminOnlyException, NotFoundException } from '../utils/errors.utils.js';
77
import { userHasPermission } from '../utils/users.utils.js';
88

99
export default class CarsService {
@@ -53,4 +53,27 @@ export default class CarsService {
5353

5454
return carTransformer(car);
5555
}
56+
57+
static async editCar(carId: string, organization: Organization, user: User, name: string) {
58+
if (!(await userHasPermission(user.userId, organization.organizationId, isAdmin)))
59+
throw new AccessDeniedAdminOnlyException('edit a car');
60+
61+
const car = await prisma.car.findFirst({
62+
where: { carId, wbsElement: { organizationId: organization.organizationId } }
63+
});
64+
65+
if (!car) throw new NotFoundException('Car', carId);
66+
67+
const editedCar = await prisma.car.update({
68+
where: { carId: car.carId },
69+
data: {
70+
wbsElement: {
71+
update: { name }
72+
}
73+
},
74+
...getCarQueryArgs(organization.organizationId)
75+
});
76+
77+
return carTransformer(editedCar);
78+
}
5679
}

src/frontend/src/apis/cars.api.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,7 @@ export const getAllCars = async () => {
1010
export const createCar = async (payload: CreateCarPayload) => {
1111
return await axios.post<Car>(apiUrls.carsCreate(), payload);
1212
};
13+
14+
export const editCar = async (id: string, payload: CreateCarPayload) => {
15+
return await axios.post<Car>(apiUrls.carEdit(id), payload);
16+
};

src/frontend/src/hooks/cars.hooks.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { useMutation, useQuery, useQueryClient } from 'react-query';
22
import { Car } from 'shared';
3-
import { createCar, getAllCars } from '../apis/cars.api';
3+
import { createCar, getAllCars, editCar } from '../apis/cars.api';
44

55
export interface CreateCarPayload {
66
name: string;
@@ -39,3 +39,19 @@ export const useCreateCar = () => {
3939
}
4040
);
4141
};
42+
43+
export const useEditCar = (carId: string) => {
44+
const queryClient = useQueryClient();
45+
return useMutation<Car, Error, CreateCarPayload>(
46+
['cars', 'edit'],
47+
async (formData: CreateCarPayload) => {
48+
const { data } = await editCar(carId, formData);
49+
return data;
50+
},
51+
{
52+
onSuccess: () => {
53+
queryClient.invalidateQueries(['cars']);
54+
}
55+
}
56+
);
57+
};

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

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,21 @@
1-
import { TableRow, TableCell, Box } from '@mui/material';
1+
import { TableRow, TableCell, Box, IconButton } from '@mui/material';
2+
import EditIcon from '@mui/icons-material/Edit';
23
import LoadingIndicator from '../../../components/LoadingIndicator';
34
import { datePipe } from '../../../utils/pipes';
45
import ErrorPage from '../../ErrorPage';
56
import { NERButton } from '../../../components/NERButton';
67
import NERTable from '../../../components/NERTable';
78
import { useGetAllCars } from '../../../hooks/cars.hooks';
89
import CreateCarModal from './CreateCarFormModal';
10+
import EditCarModal from './EditCarFormModal';
11+
import { Car } from 'shared';
912
import { useState } from 'react';
1013

1114
const CarsTable: React.FC = () => {
1215
const { data: cars, isLoading: carsIsLoading, isError: carsIsError, error: carsError } = useGetAllCars();
1316

14-
const [openModal, setOpenModal] = useState(false);
17+
const [openCreateModal, setOpenCreateModal] = useState(false);
18+
const [editingCar, setEditingCar] = useState<Car | null>(null);
1519

1620
if (!cars || carsIsLoading) {
1721
return <LoadingIndicator />;
@@ -27,15 +31,31 @@ const CarsTable: React.FC = () => {
2731
<TableCell sx={{ borderBottom: index === cars.length - 1 ? 'none' : 'default' }}>
2832
{datePipe(car.dateCreated)}
2933
</TableCell>
34+
<TableCell sx={{ borderBottom: index === cars.length - 1 ? 'none' : 'default' }}>
35+
<IconButton onClick={() => setEditingCar(car)} size="small">
36+
<EditIcon fontSize="small" />
37+
</IconButton>
38+
</TableCell>
3039
</TableRow>
3140
));
3241

3342
return (
3443
<Box>
35-
<CreateCarModal showModal={openModal} handleClose={() => setOpenModal(false)} />
36-
<NERTable columns={[{ name: 'Car Number' }, { name: 'Car Name' }, { name: 'Date Created' }]} rows={carsTableRows} />
44+
<CreateCarModal showModal={openCreateModal} handleClose={() => setOpenCreateModal(false)} />
45+
{editingCar && (
46+
<EditCarModal
47+
showModal={!!editingCar}
48+
handleClose={() => setEditingCar(null)}
49+
carId={editingCar.id}
50+
carName={editingCar.name}
51+
/>
52+
)}
53+
<NERTable
54+
columns={[{ name: 'Car Number' }, { name: 'Car Name' }, { name: 'Date Created' }, { name: '' }]}
55+
rows={carsTableRows}
56+
/>
3757
<Box sx={{ display: 'flex', justifyContent: 'right', marginTop: '10px' }}>
38-
<NERButton variant="contained" onClick={() => setOpenModal(true)}>
58+
<NERButton variant="contained" onClick={() => setOpenCreateModal(true)}>
3959
New Car
4060
</NERButton>
4161
</Box>
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
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 { useToast } from '../../../hooks/toasts.hooks';
6+
import * as yup from 'yup';
7+
import { yupResolver } from '@hookform/resolvers/yup';
8+
import { useEditCar } from '../../../hooks/cars.hooks';
9+
10+
const schema = yup.object().shape({
11+
name: yup.string().required('Car Name is Required')
12+
});
13+
14+
interface EditCarModalProps {
15+
showModal: boolean;
16+
handleClose: () => void;
17+
carId: string;
18+
carName: string;
19+
}
20+
21+
const EditCarModal: React.FC<EditCarModalProps> = ({ showModal, handleClose, carId, carName }) => {
22+
const toast = useToast();
23+
const { mutateAsync } = useEditCar(carId);
24+
25+
const onSubmit = async (data: { name: string }) => {
26+
try {
27+
await mutateAsync(data);
28+
handleClose();
29+
} catch (error: unknown) {
30+
if (error instanceof Error) {
31+
toast.error(error.message);
32+
}
33+
}
34+
};
35+
36+
const {
37+
handleSubmit,
38+
control,
39+
reset,
40+
formState: { errors }
41+
} = useForm({
42+
resolver: yupResolver(schema),
43+
defaultValues: {
44+
name: carName
45+
}
46+
});
47+
48+
return (
49+
<NERFormModal
50+
open={showModal}
51+
onHide={handleClose}
52+
title="Edit Car"
53+
reset={() => reset({ name: carName })}
54+
handleUseFormSubmit={handleSubmit}
55+
onFormSubmit={onSubmit}
56+
formId="edit-car-form"
57+
showCloseButton
58+
>
59+
<FormControl>
60+
<FormLabel>Car Name</FormLabel>
61+
<ReactHookTextField name="name" control={control} sx={{ width: 1 }} />
62+
<FormHelperText error>{errors.name?.message}</FormHelperText>
63+
</FormControl>
64+
</NERFormModal>
65+
);
66+
};
67+
68+
export default EditCarModal;

src/frontend/src/utils/urls.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -390,6 +390,7 @@ const organizationsSetFinanceDelegates = () => `${organizationsFinanceDelegates(
390390
/******************* Car Endpoints ********************/
391391
const cars = () => `${API_URL}/cars`;
392392
const carsCreate = () => `${cars()}/create`;
393+
const carEdit = (id: string) => `${cars()}/${id}/edit`;
393394

394395
/************** Recruitment Endpoints ***************/
395396
const recruitment = () => `${API_URL}/recruitment`;
@@ -796,6 +797,7 @@ export const apiUrls = {
796797

797798
cars,
798799
carsCreate,
800+
carEdit,
799801

800802
recruitment,
801803
allMilestones,

0 commit comments

Comments
 (0)