Skip to content

Commit ce32aa4

Browse files
authored
Merge pull request #3537 from Northeastern-Electric-Racing/#3536-late-project-notifs
Set slack ids
2 parents 03ee2d9 + 0c3fb42 commit ce32aa4

File tree

9 files changed

+198
-5
lines changed

9 files changed

+198
-5
lines changed

src/backend/src/controllers/teams.controllers.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,17 @@ export default class TeamsController {
7171
}
7272
}
7373

74+
static async editSlackId(req: Request, res: Response, next: NextFunction) {
75+
try {
76+
const { slackId } = req.body;
77+
78+
const team = await TeamsService.editSlackId(req.currentUser, req.organization, req.params.teamId, slackId);
79+
res.status(200).json(team);
80+
} catch (error: unknown) {
81+
next(error);
82+
}
83+
}
84+
7485
static async setTeamHead(req: Request, res: Response, next: NextFunction) {
7586
try {
7687
const { userId } = req.body;

src/backend/src/routes/teams.routes.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ teamsRouter.post(
3333
TeamsController.editDescription
3434
);
3535

36+
teamsRouter.post('/:teamId/edit-slackid', nonEmptyString(body('slackId')), validateInputs, TeamsController.editSlackId);
37+
3638
teamsRouter.post('/:teamId/set-head', nonEmptyString(body('userId')), validateInputs, TeamsController.setTeamHead);
3739
teamsRouter.post('/:teamId/delete', TeamsController.deleteTeam);
3840
teamsRouter.post(

src/backend/src/services/teams.services.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,37 @@ export default class TeamsService {
172172
return teamTransformer(updateTeam);
173173
}
174174

175+
/**
176+
* Updates the slack id of a given team
177+
* @param updater the user updating
178+
* @param organization the organizaiton
179+
* @param teamId the id of the team
180+
* @param slackId the new slack id
181+
* @returns a preview of the updated team
182+
*/
183+
static async editSlackId(updater: User, organization: Organization, teamId: string, slackId: string) {
184+
const team = await TeamsService.getSingleTeam(teamId, organization);
185+
if (team.dateArchived) throw new HttpException(400, 'Cannot edit the slack id of an archived team');
186+
187+
if (
188+
!(
189+
(await userHasPermission(updater.userId, organization.organizationId, isAdmin)) ||
190+
updater.userId === team.head.userId
191+
)
192+
)
193+
throw new AccessDeniedException('you must be an admin or the team head to update the slack id!');
194+
195+
const updatedTeam = await prisma.team.update({
196+
where: { teamId },
197+
data: {
198+
slackId
199+
},
200+
...getTeamPreviewQueryArgs(organization.organizationId)
201+
});
202+
203+
return teamPreviewTransformer(updatedTeam);
204+
}
205+
175206
/**
176207
* Update the team's head of the given team to the given user
177208
* @param submitter The submitter of this request

src/frontend/src/apis/teams.api.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,12 @@ export const setTeamDescription = (id: string, description: string) => {
4444
});
4545
};
4646

47+
export const setTeamSlackId = (id: string, slackId: string) => {
48+
return axios.post<TeamPreview>(apiUrls.teamsSetSlackId(id), {
49+
slackId
50+
});
51+
};
52+
4753
export const setTeamHead = (id: string, userId: string) => {
4854
return axios.post<Team>(apiUrls.teamsSetHead(id), {
4955
userId

src/frontend/src/hooks/teams.hooks.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@ import {
1717
archiveTeam,
1818
getAllArchivedTeams,
1919
getMyTeamsWorkpackages,
20-
getUsersTeams
20+
getUsersTeams,
21+
setTeamSlackId
2122
} from '../apis/teams.api';
2223

2324
export interface CreateTeamPayload {
@@ -120,6 +121,22 @@ export const useEditTeamDescription = (teamId: string) => {
120121
);
121122
};
122123

124+
export const useEditTeamSlackId = (teamId: string) => {
125+
const queryClient = useQueryClient();
126+
return useMutation<TeamPreview, Error, string>(
127+
['teams', 'edit'],
128+
async (slackId: string) => {
129+
const { data } = await setTeamSlackId(teamId, slackId);
130+
return data;
131+
},
132+
{
133+
onSuccess: () => {
134+
queryClient.invalidateQueries(['teams']);
135+
}
136+
}
137+
);
138+
};
139+
123140
export const useDeleteTeam = () => {
124141
const queryClient = useQueryClient();
125142
return useMutation<{ message: string }, Error, string>(

src/frontend/src/pages/AdminToolsPage/AdminToolsSlackIds.tsx

Lines changed: 45 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
*/
55

66
import { NERButton } from '../../components/NERButton';
7-
import { Box, Link, TextField, Typography } from '@mui/material';
7+
import { Box, Grid, Link, TableCell, TableRow, TextField, Typography } from '@mui/material';
88
import { useState } from 'react';
99
import { useToast } from '../../hooks/toasts.hooks';
1010
import {
@@ -14,8 +14,11 @@ import {
1414
} from '../../hooks/organizations.hooks';
1515
import LoadingIndicator from '../../components/LoadingIndicator';
1616
import ErrorPage from '../ErrorPage';
17-
import { Organization } from 'shared';
17+
import { Organization, TeamPreview } from 'shared';
1818
import HelpIcon from '@mui/icons-material/Help';
19+
import { useAllTeams } from '../../hooks/teams.hooks';
20+
import AdminToolTable from './AdminToolTable';
21+
import EditTeamSlackIdFormModal from './TeamConfig/EditTeamSlackIdFormModal';
1922

2023
interface AdminToolsWorkspaceIdViewProps {
2124
organization: Organization;
@@ -37,9 +40,29 @@ const AdminToolsSlackIdsView: React.FC<AdminToolsWorkspaceIdViewProps> = ({ orga
3740
const [sponsorshipChannelId, setSponsorshipChannelId] = useState(
3841
organization.sponsorshipNotificationsSlackChannelId ?? ''
3942
);
43+
const { data: allTeams, isLoading: allTeamsIsLoading, isError: allTeamsIsError, error: allTeamsError } = useAllTeams();
44+
const [clickedTeam, setClickedTeam] = useState<TeamPreview>();
45+
46+
if (!allTeams || allTeamsIsLoading) return <LoadingIndicator />;
47+
48+
if (allTeamsIsError) {
49+
return <ErrorPage message={allTeamsError.message} />;
50+
}
4051

4152
if (isLoading) return <LoadingIndicator />;
4253

54+
const teamTableRows = allTeams.map((team, index) => (
55+
<TableRow
56+
onClick={() => {
57+
setClickedTeam(team);
58+
}}
59+
sx={{ cursor: 'pointer', color: 'inherit', textDecoration: 'none' }}
60+
>
61+
<TableCell sx={{ borderBottom: index === allTeams.length - 1 ? 'none' : 'default' }}>{team.teamName}</TableCell>
62+
<TableCell sx={{ borderBottom: index === allTeams.length - 1 ? 'none' : 'default' }}>{team.slackId}</TableCell>
63+
</TableRow>
64+
));
65+
4366
const handleSubmitWorkspaceId = async () => {
4467
try {
4568
await setWorkspaceIdMutateAsync(workspaceId);
@@ -63,7 +86,7 @@ const AdminToolsSlackIdsView: React.FC<AdminToolsWorkspaceIdViewProps> = ({ orga
6386
};
6487

6588
return (
66-
<Box>
89+
<Box sx={{ display: 'flex', flexDirection: 'column' }}>
6790
<Typography variant="h5" gutterBottom color={'#ef4345'} borderBottom={1} borderColor={'white'}>
6891
{organization.name} Slack Workspace & Channel Ids
6992
</Typography>
@@ -113,6 +136,25 @@ const AdminToolsSlackIdsView: React.FC<AdminToolsWorkspaceIdViewProps> = ({ orga
113136
</NERButton>
114137
</Box>
115138
</Box>
139+
<Box>
140+
<Typography variant="h5" gutterBottom borderBottom={1} color="#ef4345" borderColor={'white'}>
141+
Team Slack IDs
142+
</Typography>
143+
<Grid container columnSpacing={2}>
144+
<Grid item xs={12} md={6} sx={{ marginTop: '24px' }}>
145+
<AdminToolTable columns={[{ name: 'Team Name' }, { name: 'Slack Channel ID' }]} rows={teamTableRows} />
146+
</Grid>
147+
</Grid>
148+
{clickedTeam && (
149+
<EditTeamSlackIdFormModal
150+
open={!!clickedTeam}
151+
handleClose={() => {
152+
setClickedTeam(undefined);
153+
}}
154+
team={clickedTeam}
155+
/>
156+
)}
157+
</Box>
116158
</Box>
117159
);
118160
};
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
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 { TeamPreview } from 'shared';
8+
import { useEffect } from 'react';
9+
import { useEditTeamSlackId } from '../../../hooks/teams.hooks';
10+
import LoadingIndicator from '../../../components/LoadingIndicator';
11+
import { useToast } from '../../../hooks/toasts.hooks';
12+
13+
interface EditTeamSlackIdFormModalProps {
14+
open: boolean;
15+
handleClose: () => void;
16+
team: TeamPreview;
17+
}
18+
19+
const schema = yup.object().shape({
20+
slackId: yup.string().required('Slack id is required')
21+
});
22+
23+
const EditTeamSlackIdFormModal: React.FC<EditTeamSlackIdFormModalProps> = ({ open, handleClose, team }) => {
24+
const { isLoading, mutateAsync } = useEditTeamSlackId(team.teamId);
25+
26+
const toast = useToast();
27+
28+
const onFormSubmit = async (data: { slackId: string }) => {
29+
try {
30+
await mutateAsync(data.slackId);
31+
toast.success('Slack id updated successfully!');
32+
} catch (e) {
33+
if (e instanceof Error) {
34+
toast.error(e.message);
35+
}
36+
}
37+
handleClose();
38+
};
39+
40+
const {
41+
handleSubmit,
42+
control,
43+
reset,
44+
formState: { errors }
45+
} = useForm({
46+
resolver: yupResolver(schema),
47+
defaultValues: {
48+
slackId: team.slackId
49+
}
50+
});
51+
52+
useEffect(() => {
53+
reset({
54+
slackId: team.slackId
55+
});
56+
}, [team, reset]);
57+
58+
if (isLoading) {
59+
return <LoadingIndicator />;
60+
}
61+
62+
return (
63+
<NERFormModal
64+
open={open}
65+
onHide={handleClose}
66+
title={`${team.teamName} Slack ID`}
67+
reset={() => reset({ slackId: team.slackId })}
68+
handleUseFormSubmit={handleSubmit}
69+
onFormSubmit={onFormSubmit}
70+
formId="team-slackId-form"
71+
showCloseButton
72+
>
73+
<FormControl>
74+
<FormLabel>Slack ID</FormLabel>
75+
<ReactHookTextField name="slackId" control={control} sx={{ width: 1 }} />
76+
<FormHelperText error>{errors.slackId?.message}</FormHelperText>
77+
</FormControl>
78+
</NERFormModal>
79+
);
80+
};
81+
82+
export default EditTeamSlackIdFormModal;

src/frontend/src/utils/urls.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,7 @@ const completeOnboarding = () => `${teams()}/teamType/complete-onboarding`;
137137
const teamsSetHead = (id: string) => `${teamsById(id)}/set-head`;
138138
const teamsArchive = (id: string) => `${teamsById(id)}/archive`;
139139
const teamsSetDescription = (id: string) => `${teamsById(id)}/edit-description`;
140+
const teamsSetSlackId = (id: string) => `${teamsById(id)}/edit-slackid`;
140141
const teamsCreate = () => `${teams()}/create`;
141142
const teamsSetLeads = (id: string) => `${teamsById(id)}/set-leads`;
142143
const usersTeams = () => `${teams()}/users-teams`;
@@ -515,6 +516,7 @@ export const apiUrls = {
515516
teamsArchive,
516517
teamsSetHead,
517518
teamsSetDescription,
519+
teamsSetSlackId,
518520
teamsCreate,
519521
teamsSetLeads,
520522
usersTeams,

src/shared/src/types/team-types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,4 +21,4 @@ export interface Team {
2121
teamType?: TeamType;
2222
}
2323

24-
export type TeamPreview = Pick<Team, 'teamId' | 'teamName' | 'members' | 'head' | 'leads' | 'teamType'>;
24+
export type TeamPreview = Pick<Team, 'teamId' | 'teamName' | 'members' | 'head' | 'leads' | 'teamType' | 'slackId'>;

0 commit comments

Comments
 (0)