Skip to content

Commit c96bf30

Browse files
authored
Merge branch 'develop' into #1310-enforce-secure-settings-on-create-RR-form
2 parents fb5bc69 + c26df1e commit c96bf30

30 files changed

Lines changed: 451 additions & 201 deletions

src/backend/src/services/tasks.services.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,10 @@ export default class TasksService {
9494
*/
9595
static async editTask(user: User, taskId: string, title: string, notes: string, priority: Task_Priority, deadline: Date) {
9696
const hasPermission = await hasPermissionToEditTask(user, taskId);
97-
if (!hasPermission) throw new AccessDeniedException();
97+
if (!hasPermission)
98+
throw new AccessDeniedException(
99+
'Only admins, app admins, heads, task creators, project leads, project managers, or project assignees can edit a task'
100+
);
98101

99102
const originalTask = await prisma.task.findUnique({ where: { taskId } });
100103
if (!originalTask) throw new NotFoundException('Task', taskId);
@@ -129,7 +132,7 @@ export default class TasksService {
129132
const hasPermission = await hasPermissionToEditTask(user, taskId);
130133
if (!hasPermission)
131134
throw new AccessDeniedException(
132-
'Only admins, app admins, task creators, project leads, project managers, or project assignees can edit a task'
135+
'Only admins, app admins, heads, task creators, project leads, project managers, or project assignees can edit a task'
133136
);
134137

135138
const updatedTask = await prisma.task.update({ where: { taskId }, data: { status }, ...taskQueryArgs });
@@ -158,7 +161,7 @@ export default class TasksService {
158161
const hasPermission = await hasPermissionToEditTask(user, taskId);
159162
if (!hasPermission)
160163
throw new AccessDeniedException(
161-
'Only admins, app admins, task creators, project leads, project managers, or project assignees can edit a task'
164+
'Only admins, app admins, heads, task creators, project leads, project managers, or project assignees can edit a task'
162165
);
163166

164167
// this throws if any of the users aren't found

src/backend/tests/tasks.test.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -201,7 +201,7 @@ describe('Tasks', () => {
201201
// Try updating from IN_PROGRESS to IN_BACKLOG
202202
await expect(() => TasksService.editTaskStatus(aquaman, taskId, Task_Status.IN_BACKLOG)).rejects.toThrow(
203203
new AccessDeniedException(
204-
'Only admins, app admins, task creators, project leads, project managers, or project assignees can edit a task'
204+
'Only admins, app admins, heads, task creators, project leads, project managers, or project assignees can edit a task'
205205
)
206206
);
207207
});
@@ -216,7 +216,7 @@ describe('Tasks', () => {
216216
// Aquaman is a leader, but did not create this task
217217
await expect(() => TasksService.editTaskStatus(aquaman, taskId, Task_Status.IN_BACKLOG)).rejects.toThrow(
218218
new AccessDeniedException(
219-
'Only admins, app admins, task creators, project leads, project managers, or project assignees can edit a task'
219+
'Only admins, app admins, heads, task creators, project leads, project managers, or project assignees can edit a task'
220220
)
221221
);
222222
});
@@ -288,7 +288,7 @@ describe('Tasks', () => {
288288
TasksService.editTaskAssignees(aquaman, taskId, [superman.userId, wonderwoman.userId])
289289
).rejects.toThrow(
290290
new AccessDeniedException(
291-
'Only admins, app admins, task creators, project leads, project managers, or project assignees can edit a task'
291+
'Only admins, app admins, heads, task creators, project leads, project managers, or project assignees can edit a task'
292292
)
293293
);
294294
});
@@ -391,7 +391,11 @@ describe('Tasks', () => {
391391
vi.spyOn(taskUtils, 'hasPermissionToEditTask').mockResolvedValue(false);
392392
await expect(() =>
393393
TasksService.editTask(wonderwoman, taskId, fakeTitle, fakeNotes, fakePriority, fakeDeadline)
394-
).rejects.toThrow(new AccessDeniedException());
394+
).rejects.toThrow(
395+
new AccessDeniedException(
396+
'Only admins, app admins, heads, task creators, project leads, project managers, or project assignees can edit a task'
397+
)
398+
);
395399
});
396400

397401
test('Task not found', async () => {

src/frontend/src/apis/change-requests.api.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,3 +141,12 @@ export const addProposedSolution = (
141141
budgetImpact
142142
});
143143
};
144+
145+
/**
146+
* Request reviewers in change request
147+
* @param crId The ID of the associated change request.
148+
* @param crReviewData The data to request reviewers
149+
*/
150+
export const requestCRReview = (crId: string, crReviewData: { userIds: number[] }) => {
151+
return axios.post<{ message: string }>(apiUrls.changeRequestRequestReviewer(crId), crReviewData);
152+
};

src/frontend/src/components/ActionsMenu.tsx

Lines changed: 21 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,22 @@ import { Box } from '@mui/system';
22
import { ReactElement, useState } from 'react';
33
import { NERButton } from './NERButton';
44
import { ArrowDropDown } from '@mui/icons-material';
5-
import { ListItemIcon, Menu, MenuItem } from '@mui/material';
5+
import { Divider, ListItemIcon, Menu, MenuItem } from '@mui/material';
66

77
export type ButtonInfo = {
88
title: string;
99
onClick: () => void;
1010
disabled?: boolean;
1111
icon?: ReactElement;
12+
dividerTop?: boolean;
1213
};
1314

1415
interface ActionsMenuProps {
1516
buttons: ButtonInfo[];
17+
title?: string;
1618
}
1719

18-
const ActionsMenu: React.FC<ActionsMenuProps> = ({ buttons }) => {
20+
const ActionsMenu: React.FC<ActionsMenuProps> = ({ buttons, title = 'Actions' }) => {
1921
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
2022

2123
const handleClick = (event: React.MouseEvent<HTMLElement>) => {
@@ -36,22 +38,25 @@ const ActionsMenu: React.FC<ActionsMenuProps> = ({ buttons }) => {
3638
id="reimbursement-request-actions-dropdown"
3739
onClick={handleClick}
3840
>
39-
Actions
41+
{title}
4042
</NERButton>
4143
<Menu open={dropdownOpen} anchorEl={anchorEl} onClose={handleDropdownClose}>
42-
{buttons.map((button, index) => (
43-
<MenuItem
44-
key={index}
45-
onClick={() => {
46-
handleDropdownClose();
47-
button.onClick();
48-
}}
49-
disabled={button.disabled}
50-
>
51-
<ListItemIcon>{button.icon}</ListItemIcon>
52-
{button.title}
53-
</MenuItem>
54-
))}
44+
{buttons.flatMap((button, index) => {
45+
return [
46+
button.dividerTop && <Divider key={`${index}-divider`} />,
47+
<MenuItem
48+
key={index}
49+
onClick={() => {
50+
handleDropdownClose();
51+
button.onClick();
52+
}}
53+
disabled={button.disabled}
54+
>
55+
<ListItemIcon>{button.icon}</ListItemIcon>
56+
{button.title}
57+
</MenuItem>
58+
];
59+
})}
5560
</Menu>
5661
</Box>
5762
);

src/frontend/src/components/CheckList.tsx

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { Tooltip } from '@mui/material';
1414
import { User } from 'shared';
1515
import NERModal from './NERModal';
1616
import { fullNamePipe } from '../utils/pipes';
17+
import { useToast } from '../hooks/toasts.hooks';
1718

1819
export type CheckListItem = {
1920
id: number;
@@ -34,14 +35,21 @@ const CheckList: React.FC<CheckListProps> = ({ title, items, isDisabled }) => {
3435
const { isLoading, mutateAsync } = useCheckDescriptionBullet();
3536
const [showConfirm, setShowConfirm] = useState<boolean>(false);
3637
const [currIdx, setCurrIdx] = useState<number>(-1);
38+
const toast = useToast();
3739

3840
const handleUncheck = async (idx: number) => {
3941
await handleCheck(idx);
4042
setShowConfirm(false);
4143
};
4244

4345
const handleCheck = async (idx: number) => {
44-
await mutateAsync({ userId: auth.user!.userId, descriptionId: items[idx].id });
46+
try {
47+
await mutateAsync({ userId: auth.user!.userId, descriptionId: items[idx].id });
48+
} catch (e) {
49+
if (e instanceof Error) {
50+
toast.error(e.message);
51+
}
52+
}
4553
};
4654

4755
items.sort((a: CheckListItem, b: CheckListItem) => {

src/frontend/src/components/NERFormModal.tsx

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,21 +28,24 @@ const NERFormModal = ({
2828
*/
2929
const onSubmitWrapper = async (data: any) => {
3030
await onFormSubmit(data);
31-
reset({ confirmDone: false });
31+
reset();
3232
};
3333

3434
return (
3535
<NERModal
3636
open={open}
37-
onHide={onHide}
37+
onHide={() => {
38+
onHide();
39+
reset();
40+
}}
3841
formId={formId}
3942
title={title}
4043
cancelText={cancelText ? cancelText : 'Cancel'}
4144
submitText={submitText ? submitText : 'Submit'}
4245
disabled={disabled}
4346
showCloseButton={showCloseButton}
4447
>
45-
<form id={formId} onSubmit={handleUseFormSubmit(onSubmitWrapper)}>
48+
<form id={formId} onSubmit={handleUseFormSubmit(onSubmitWrapper)} noValidate>
4649
{children}
4750
</form>
4851
</NERModal>

src/frontend/src/hooks/change-requests.hooks.ts

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@ import {
1313
getSingleChangeRequest,
1414
reviewChangeRequest,
1515
addProposedSolution,
16-
deleteChangeRequest
16+
deleteChangeRequest,
17+
requestCRReview
1718
} from '../apis/change-requests.api';
1819

1920
/**
@@ -153,3 +154,22 @@ export const useCreateProposeSolution = () => {
153154
}
154155
);
155156
};
157+
158+
/**
159+
* Custom React hook to request cr reviewers
160+
*/
161+
export const useRequestCRReview = (crId: string) => {
162+
const queryClient = useQueryClient();
163+
return useMutation<{ message: string }, Error, any>(
164+
['change requests', 'review'],
165+
async (crReviewPayload: { userIds: number[] }) => {
166+
const { data } = await requestCRReview(crId, crReviewPayload);
167+
return data;
168+
},
169+
{
170+
onSuccess: () => {
171+
queryClient.invalidateQueries(['change requests']);
172+
}
173+
}
174+
);
175+
};

src/frontend/src/pages/AdminToolsPage/FinanceConfig/AccountCodeFormModal.tsx

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,14 @@ import { ExpenseType } from 'shared';
22
import { ExpenseTypePayload } from '../../../hooks/finance.hooks';
33
import { Controller, useForm } from 'react-hook-form';
44
import NERFormModal from '../../../components/NERFormModal';
5-
import { Checkbox, FormControl, FormLabel } from '@mui/material';
5+
import { Checkbox, FormControl, FormLabel, FormHelperText } from '@mui/material';
66
import ReactHookTextField from '../../../components/ReactHookTextField';
77
import { useToast } from '../../../hooks/toasts.hooks';
88
import * as yup from 'yup';
99
import { yupResolver } from '@hookform/resolvers/yup';
1010

1111
const schema = yup.object().shape({
12-
code: yup.number().required('Account Code is Required'),
12+
code: yup.number().typeError('Account Code must be a number').required('Account Code is Required'),
1313
name: yup.string().required('Account Name is Required'),
1414
allowed: yup.boolean().required('Allowed is Required')
1515
});
@@ -26,10 +26,9 @@ const AccountCodeFormModal = ({ showModal, handleClose, defaultValues, onSubmit
2626
const {
2727
handleSubmit,
2828
control,
29-
formState: { isValid },
30-
reset
29+
reset,
30+
formState: { errors }
3131
} = useForm({
32-
mode: 'onChange',
3332
resolver: yupResolver(schema),
3433
defaultValues: {
3534
code: defaultValues?.code,
@@ -54,20 +53,21 @@ const AccountCodeFormModal = ({ showModal, handleClose, defaultValues, onSubmit
5453
open={showModal}
5554
onHide={handleClose}
5655
title={!!defaultValues ? 'Edit Account Code' : 'Create Account Code'}
57-
reset={reset}
56+
reset={() => reset({ name: '', code: undefined, allowed: false })}
5857
handleUseFormSubmit={handleSubmit}
5958
onFormSubmit={onFormSubmit}
6059
formId={!!defaultValues ? 'edit-vendor-form' : 'create-vendor-form'}
61-
disabled={!isValid}
6260
showCloseButton
6361
>
6462
<FormControl fullWidth>
6563
<FormLabel>Account Name</FormLabel>
6664
<ReactHookTextField name="name" control={control} fullWidth />
65+
<FormHelperText error>{errors.name?.message}</FormHelperText>
6766
</FormControl>
6867
<FormControl fullWidth>
6968
<FormLabel>Account Code</FormLabel>
7069
<ReactHookTextField name="code" control={control} fullWidth />
70+
<FormHelperText error>{errors.code?.message}</FormHelperText>
7171
</FormControl>
7272
<FormControl>
7373
<FormLabel>Allowed?</FormLabel>

src/frontend/src/pages/AdminToolsPage/FinanceConfig/AccountCodesTable.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,10 @@ const AccountCodesTable = () => {
4949
{clickedAccountCode && (
5050
<EditAccountCodeModal
5151
showModal={showEditModal}
52-
handleClose={() => setShowEditModal(false)}
52+
handleClose={() => {
53+
setShowEditModal(false);
54+
setClickedAccountCode(undefined);
55+
}}
5356
accountCode={clickedAccountCode}
5457
/>
5558
)}

src/frontend/src/pages/AdminToolsPage/FinanceConfig/CreateVendorModal.tsx

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { useForm } from 'react-hook-form';
22
import NERFormModal from '../../../components/NERFormModal';
3-
import { FormControl, FormLabel } from '@mui/material';
3+
import { FormControl, FormLabel, FormHelperText } from '@mui/material';
44
import ReactHookTextField from '../../../components/ReactHookTextField';
55
import { useCreateVendor } from '../../../hooks/finance.hooks';
66
import { useToast } from '../../../hooks/toasts.hooks';
@@ -10,7 +10,7 @@ import * as yup from 'yup';
1010
import { yupResolver } from '@hookform/resolvers/yup';
1111

1212
const schema = yup.object().shape({
13-
name: yup.string().required('Account Name is Required')
13+
name: yup.string().required('Vendor Name is Required')
1414
});
1515

1616
interface NewVendorProps {
@@ -36,10 +36,9 @@ const CreateVendorModal = ({ showModal, handleClose }: NewVendorProps) => {
3636
const {
3737
handleSubmit,
3838
control,
39-
formState: { isValid },
40-
reset
39+
reset,
40+
formState: { errors }
4141
} = useForm({
42-
mode: 'onChange',
4342
resolver: yupResolver(schema),
4443
defaultValues: {
4544
name: ''
@@ -54,16 +53,16 @@ const CreateVendorModal = ({ showModal, handleClose }: NewVendorProps) => {
5453
open={showModal}
5554
onHide={handleClose}
5655
title="New Vendor"
57-
reset={reset}
56+
reset={() => reset({ name: '' })}
5857
handleUseFormSubmit={handleSubmit}
5958
onFormSubmit={onSubmit}
6059
formId="new-vendor-form"
61-
disabled={!isValid}
6260
showCloseButton
6361
>
6462
<FormControl>
6563
<FormLabel>Vendor Name</FormLabel>
6664
<ReactHookTextField name="name" control={control} sx={{ width: 1 }} />
65+
<FormHelperText error>{errors.name?.message}</FormHelperText>
6766
</FormControl>
6867
</NERFormModal>
6968
);

0 commit comments

Comments
 (0)