Skip to content

Commit b66ac01

Browse files
authored
Merge pull request #1282 from Northeastern-Electric-Racing/#1203-Add-Button-for-CR-reviewer-request
#1203 Added cr reviewers request dropdown with submit button
2 parents 674c590 + c5c71a3 commit b66ac01

9 files changed

Lines changed: 296 additions & 140 deletions

File tree

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/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+
};
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
import { ChangeRequest, ChangeRequestStatus, isLeadership, wbsPipe } from 'shared';
2+
import ActionsMenu from '../../components/ActionsMenu';
3+
import { Autocomplete, Checkbox, TextField, Box } from '@mui/material';
4+
import EditIcon from '@mui/icons-material/Edit';
5+
import DeleteIcon from '@mui/icons-material/Delete';
6+
import ContentPasteIcon from '@mui/icons-material/ContentPaste';
7+
import CheckBoxIcon from '@mui/icons-material/CheckBox';
8+
import CheckBoxOutlineBlankIcon from '@mui/icons-material/CheckBoxOutlineBlank';
9+
import PostAddIcon from '@mui/icons-material/PostAdd';
10+
import CreateNewFolderIcon from '@mui/icons-material/CreateNewFolder';
11+
import { useHistory } from 'react-router-dom';
12+
import { NERButton } from '../../components/NERButton';
13+
import { useRequestCRReview } from '../../hooks/change-requests.hooks';
14+
import { useToast } from '../../hooks/toasts.hooks';
15+
import { useCurrentUser, useAllUsers } from '../../hooks/users.hooks';
16+
import { projectWbsPipe } from '../../utils/pipes';
17+
import { routes } from '../../utils/routes';
18+
import { userToAutocompleteOption } from '../../utils/task.utils';
19+
import { useState } from 'react';
20+
import ErrorPage from '../ErrorPage';
21+
import LoadingIndicator from '../../components/LoadingIndicator';
22+
23+
interface ChangeRequestActionMenuProps {
24+
isUserAllowedToReview: boolean;
25+
isUserAllowedToImplement: boolean;
26+
isUserAllowedToDelete: boolean;
27+
changeRequest: ChangeRequest;
28+
handleReviewOpen: () => void;
29+
handleDeleteOpen: () => void;
30+
}
31+
32+
const ChangeRequestActionMenu: React.FC<ChangeRequestActionMenuProps> = ({
33+
isUserAllowedToReview,
34+
isUserAllowedToImplement,
35+
isUserAllowedToDelete,
36+
changeRequest,
37+
handleReviewOpen,
38+
handleDeleteOpen
39+
}: ChangeRequestActionMenuProps) => {
40+
const { mutateAsync: requestCRReview } = useRequestCRReview(changeRequest.crId.toString());
41+
const toast = useToast();
42+
const currentUser = useCurrentUser();
43+
const history = useHistory();
44+
const [reviewerIds, setReviewerIds] = useState<number[]>([]);
45+
const { data: users, isLoading: isLoadingAllUsers, isError: isErrorAllUsers, error: errorAllUsers } = useAllUsers();
46+
47+
if (isErrorAllUsers) return <ErrorPage message={errorAllUsers?.message} />;
48+
if (isLoadingAllUsers || !users) return <LoadingIndicator />;
49+
50+
const handleRequestReviewerClick = async () => {
51+
if (reviewerIds.length === 0) {
52+
toast.error('Must select at least one reviewer to request review from');
53+
} else {
54+
try {
55+
await requestCRReview({ userIds: reviewerIds });
56+
} catch (e) {
57+
if (e instanceof Error) {
58+
toast.error(e.message);
59+
}
60+
}
61+
}
62+
};
63+
64+
const isRequestAllowed =
65+
changeRequest.submitter.userId === currentUser.userId && changeRequest.status === ChangeRequestStatus.Open;
66+
67+
const UnreviewedActionsDropdown = () => (
68+
<div style={{ marginTop: '10px' }}>
69+
<ActionsMenu
70+
buttons={[
71+
{
72+
title: 'Review',
73+
onClick: handleReviewOpen,
74+
disabled: !isUserAllowedToReview,
75+
icon: <ContentPasteIcon fontSize="small" />
76+
},
77+
{
78+
title: 'Delete',
79+
onClick: handleDeleteOpen,
80+
disabled: !isUserAllowedToDelete,
81+
icon: <DeleteIcon fontSize="small" />,
82+
dividerTop: true
83+
}
84+
]}
85+
/>
86+
</div>
87+
);
88+
89+
const requestReviewerDropdown = () => (
90+
<>
91+
<Autocomplete
92+
isOptionEqualToValue={(option, value) => option.id === value.id}
93+
limitTags={1}
94+
disableCloseOnSelect
95+
multiple
96+
options={users.filter((user) => isLeadership(user.role)).map(userToAutocompleteOption)}
97+
getOptionLabel={(option) => option.label}
98+
onChange={(_, values) => setReviewerIds(values.map((value) => value.id))}
99+
renderTags={() => null}
100+
renderOption={(props, option, { selected }) => (
101+
<li {...props}>
102+
<Checkbox
103+
icon={<CheckBoxOutlineBlankIcon />}
104+
checkedIcon={<CheckBoxIcon />}
105+
style={{ marginRight: 8 }}
106+
checked={selected}
107+
/>
108+
{option.label}
109+
</li>
110+
)}
111+
renderInput={(params) => (
112+
<TextField {...params} variant="standard" placeholder={`${reviewerIds.length} Reviewers Selected`} />
113+
)}
114+
/>
115+
116+
<Box display="flex" flexDirection="row" gap="10px" justifyContent="right">
117+
<NERButton
118+
sx={{ mt: '10px', float: 'right' }}
119+
variant="contained"
120+
disabled={!isRequestAllowed}
121+
onClick={handleRequestReviewerClick}
122+
>
123+
Request Review
124+
</NERButton>
125+
<UnreviewedActionsDropdown />
126+
</Box>
127+
</>
128+
);
129+
130+
const renderUnreviewedActionsDropdown = () =>
131+
isRequestAllowed ? requestReviewerDropdown() : <UnreviewedActionsDropdown />;
132+
133+
const ImplementCrDropdown = () => (
134+
<ActionsMenu
135+
buttons={[
136+
{
137+
title: 'Create New Project',
138+
onClick: () =>
139+
history.push(`${routes.PROJECTS_NEW}?crId=${changeRequest.crId}&wbs=${projectWbsPipe(changeRequest.wbsNum)}`),
140+
disabled: !isUserAllowedToImplement,
141+
icon: <CreateNewFolderIcon fontSize="small" />
142+
},
143+
{
144+
title: 'Create New Work Package',
145+
onClick: () =>
146+
history.push(
147+
`${routes.WORK_PACKAGE_NEW}?crId=${changeRequest.crId}&wbs=${projectWbsPipe(changeRequest.wbsNum)}`
148+
),
149+
disabled: !isUserAllowedToImplement,
150+
icon: <PostAddIcon fontSize="small" />
151+
},
152+
{
153+
title: `Edit ${changeRequest.wbsNum.workPackageNumber === 0 ? 'Project' : 'Work Package'}`,
154+
onClick: () =>
155+
history.push(`${routes.PROJECTS}/${wbsPipe(changeRequest.wbsNum)}?crId=${changeRequest.crId}&edit=${true}`),
156+
disabled: !isUserAllowedToImplement,
157+
icon: <EditIcon fontSize="small" />
158+
}
159+
]}
160+
title="Implement Change Request"
161+
/>
162+
);
163+
164+
return changeRequest.accepted ? <ImplementCrDropdown /> : <>{renderUnreviewedActionsDropdown()}</>;
165+
};
166+
167+
export default ChangeRequestActionMenu;

src/frontend/src/pages/ChangeRequestDetailPage/ChangeRequestDetails.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { useAuth } from '../../hooks/auth.hooks';
99
import ChangeRequestDetailsView from './ChangeRequestDetailsView';
1010
import LoadingIndicator from '../../components/LoadingIndicator';
1111
import ErrorPage from '../ErrorPage';
12-
import { isAdmin, isGuest, isNotLeadership } from 'shared';
12+
import { ChangeRequestStatus, isAdmin, isGuest, isNotLeadership } from 'shared';
1313

1414
const ChangeRequestDetails: React.FC = () => {
1515
interface ParamTypes {
@@ -25,7 +25,11 @@ const ChangeRequestDetails: React.FC = () => {
2525

2626
return (
2727
<ChangeRequestDetailsView
28-
isUserAllowedToReview={!isNotLeadership(auth.user?.role) && auth.user?.userId !== data?.submitter.userId}
28+
isUserAllowedToReview={
29+
!isNotLeadership(auth.user?.role) &&
30+
auth.user?.userId !== data?.submitter.userId &&
31+
data!.status === ChangeRequestStatus.Open
32+
}
2933
isUserAllowedToImplement={!isGuest(auth.user?.role)}
3034
isUserAllowedToDelete={
3135
isAdmin(auth.user?.role) || (auth.user?.userId === data?.submitter.userId && !data?.dateReviewed)

0 commit comments

Comments
 (0)