Skip to content

Commit 590533b

Browse files
authored
Merge pull request #4205 from Northeastern-Electric-Racing/#4093-Team-vs-Channel-Slac-Notifications
Adding toggle for who to notify on Slack
2 parents 8934258 + ffdbb61 commit 590533b

8 files changed

Lines changed: 111 additions & 20 deletions

File tree

src/backend/src/controllers/calendar.controllers.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -279,7 +279,8 @@ export default class CalendarController {
279279
questionDocumentLink,
280280
location,
281281
zoomLink,
282-
description
282+
description,
283+
mention
283284
} = req.body;
284285

285286
const parsedScheduleSlots = scheduleSlots.map((slot: any) => ({
@@ -307,7 +308,8 @@ export default class CalendarController {
307308
questionDocumentLink,
308309
location,
309310
zoomLink,
310-
description
311+
description,
312+
mention
311313
);
312314
res.status(200).json(event);
313315
} catch (error: unknown) {

src/backend/src/routes/calendar.routes.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@ calendarRouter.post(
119119
isDate(body('scheduleSlots.*.startTime')),
120120
isDate(body('scheduleSlots.*.endTime')),
121121
body('scheduleSlots.*.allDay').isBoolean(),
122+
body('mention').isIn(['USER', 'CHANNEL']),
122123
validateInputs,
123124
CalendarController.createEvent
124125
);

src/backend/src/services/calendar.services.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@ import {
1616
ScheduleSlot,
1717
notGuest,
1818
isSameDay,
19-
EventInstance
19+
EventInstance,
20+
SlackMentionType
2021
} from 'shared';
2122
import { getCalendarQueryArgs } from '../prisma-query-args/calendar.query-args.js';
2223
import { getEventTypeQueryArgs } from '../prisma-query-args/event-type.query-args.js';
@@ -270,7 +271,8 @@ export default class CalendarService {
270271
questionDocumentLink?: string,
271272
location?: string,
272273
zoomLink?: string,
273-
description?: string
274+
description?: string,
275+
mention?: SlackMentionType
274276
): Promise<Event> {
275277
// Validate eventTypeId
276278
const foundEventType = await prisma.event_Type.findUnique({
@@ -549,7 +551,8 @@ export default class CalendarService {
549551
createdEvent,
550552
submitter,
551553
workPackageNames,
552-
organization.name
554+
organization.name,
555+
{ memberSlackIds: memberUserSettings.map((s) => s.slackId).filter((id): id is string => !!id), mention }
553556
);
554557
}
555558

src/backend/src/utils/slack.utils.ts

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@ import {
77
CreateSponsorTask,
88
User,
99
Event,
10-
formatForSlack
10+
formatForSlack,
11+
SlackMentionType
1112
} from 'shared';
1213
import { Account_Code, Reimbursement_Product_Other_Reason, Sponsor_Task } from '@prisma/client';
1314
import {
@@ -383,35 +384,49 @@ export const sendAndGetSlackCRNotifications = async (
383384
return notifications;
384385
};
385386

387+
export const buildSlackMentionPrefix = (mention: SlackMentionType, memberSlackIds: string[]): string => {
388+
if (mention === SlackMentionType.CHANNEL) return '<!channel> ';
389+
if (memberSlackIds.length > 0) return `${memberSlackIds.map((id) => `<@${id}>`).join(' ')} `;
390+
return '';
391+
};
392+
386393
export const sendSlackEventNotification = async (
387394
team: Team,
388395
message: string
389396
): Promise<{ channelId: string; ts: string }[]> => {
390397
if (process.env.NODE_ENV !== 'production' && !DEV_TESTING_OVERRIDE) return []; // don't send msgs unless in prod
391398
const msgs: { channelId: string; ts: string }[] = [];
392-
const fullMsg = `${message}`;
393399
const fullLink = `https://finishlinebyner.com/calendar`;
394400
const btnText = `View Calendar`;
395-
const notification = await sendMessage(team.slackId, fullMsg, fullLink, btnText);
401+
const notification = await sendMessage(team.slackId, message, fullLink, btnText);
396402
if (notification) msgs.push(notification);
397403

398404
return msgs;
399405
};
400406

407+
export interface EventNotificationOptions {
408+
memberSlackIds?: string[];
409+
mention?: SlackMentionType;
410+
}
411+
401412
export const sendSlackEventNotifications = async (
402413
teams: Team[],
403414
event: Event,
404415
submitter: User,
405416
workPackageName: string,
406-
projectName: string
417+
projectName: string,
418+
options: EventNotificationOptions = {}
407419
) => {
408420
if (process.env.NODE_ENV !== 'production' && !DEV_TESTING_OVERRIDE) return []; // don't send msgs unless in prod
409421
const notifications: { channelId: string; ts: string }[] = [];
422+
423+
const mentionPrefix = buildSlackMentionPrefix(options.mention ?? SlackMentionType.USER, options.memberSlackIds ?? []);
424+
410425
let message;
411426
if (workPackageName) {
412-
message = `:spiral_calendar_pad: ${event.title} for *${workPackageName}* is being scheduled by ${submitter.firstName} ${submitter.lastName} in project ${projectName}`;
427+
message = `${mentionPrefix}:spiral_calendar_pad: ${event.title} for *${workPackageName}* is being scheduled by ${submitter.firstName} ${submitter.lastName} in project ${projectName}`;
413428
} else {
414-
message = `:spiral_calendar_pad: ${event.title} is being scheduled by ${submitter.firstName} ${submitter.lastName} in project ${projectName}`;
429+
message = `${mentionPrefix}:spiral_calendar_pad: ${event.title} is being scheduled by ${submitter.firstName} ${submitter.lastName} in project ${projectName}`;
415430
}
416431

417432
const completion: Promise<void>[] = teams.map(async (team) => {
@@ -487,9 +502,14 @@ export const sendEventScheduledSlackNotif = async (threads: SlackMessageThread[]
487502

488503
const location = zoomLink && inPersonLocation ? `${inPersonLocation} and ${zoomLink}` : inPersonLocation || zoomLink || '';
489504

505+
const allMembers = [...event.requiredMembers, ...event.optionalMembers];
506+
const resolvedSlackIds = await Promise.all(allMembers.map((m) => getUserSlackId(m.userId)));
507+
const validSlackIds = resolvedSlackIds.filter((id): id is string => !!id);
508+
const mentionPrefix = buildSlackMentionPrefix(SlackMentionType.USER, validSlackIds);
509+
490510
const msg = `:spiral_calendar_pad: ${event.title} for *${drName}* has been scheduled for *${drTime}* ${location} by ${drSubmitter}`;
491511
const docLink = event.questionDocumentLink ? `<${event.questionDocumentLink}|Doc Link>` : '';
492-
const threadMsg = `This event has been Scheduled! \n` + docLink;
512+
const threadMsg = `${mentionPrefix}This event has been Scheduled! \n` + docLink;
493513

494514
if (threads && threads.length !== 0) {
495515
const msgs = threads.map((thread) => editMessage(thread.channelId, thread.timestamp, msg));

src/frontend/src/hooks/calendar.hooks.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@ import {
1212
ScheduleSlotCreateArgs,
1313
EventWithMembers,
1414
ScheduleSlot,
15-
EventInstance
15+
EventInstance,
16+
SlackMentionType
1617
} from 'shared';
1718
import {
1819
getAllShops,
@@ -82,6 +83,7 @@ export interface EventCreateArgs {
8283
description?: string;
8384
initialDateScheduled: Date;
8485
scheduleSlots: ScheduleSlotCreateArgs[];
86+
mention?: SlackMentionType;
8587
}
8688

8789
export interface EditEventArgs {

src/frontend/src/pages/CalendarPage/Components/CreateEventModal.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ const CreateEventModal: React.FC<CreateEventModalProps> = ({
3232

3333
const handleSubmit = async (payload: EventPayload) => {
3434
try {
35-
const { documentFiles, createScheduleSlotArgs, initialDateScheduled, ...eventData } = payload;
35+
const { documentFiles, createScheduleSlotArgs, initialDateScheduled, mention, ...eventData } = payload;
3636

3737
const scheduleSlots: Array<{
3838
startTime: Date;
@@ -82,7 +82,8 @@ const CreateEventModal: React.FC<CreateEventModalProps> = ({
8282
...eventData,
8383
initialDateScheduled: initialDateScheduled ?? new Date(),
8484
scheduleSlots,
85-
documentIds: []
85+
documentIds: [],
86+
mention
8687
};
8788

8889
const createdEvent = await createEvent(createArgs);

src/frontend/src/pages/CalendarPage/Components/EventModal.tsx

Lines changed: 62 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,10 @@ import {
1313
Button,
1414
Stack,
1515
Checkbox,
16-
FormControlLabel
16+
FormControlLabel,
17+
ToggleButtonGroup,
18+
ToggleButton,
19+
useTheme
1720
} from '@mui/material';
1821
import { DatePicker, TimePicker } from '@mui/x-date-pickers';
1922
import { Controller, useForm } from 'react-hook-form';
@@ -28,7 +31,8 @@ import {
2831
isHead,
2932
MAX_FILE_SIZE,
3033
getNextSevenDays,
31-
getDay
34+
getDay,
35+
SlackMentionType
3236
} from 'shared';
3337
import { useToast } from '../../../hooks/toasts.hooks';
3438
import { useAllMembers, useCurrentUser } from '../../../hooks/users.hooks';
@@ -79,6 +83,7 @@ export interface EventFormValues {
7983
recurrenceNumber: number;
8084
days: DayOfWeek[];
8185
selectedScheduleSlotId?: string;
86+
mention: SlackMentionType;
8287
}
8388

8489
export interface EventPayload {
@@ -96,6 +101,7 @@ export interface EventPayload {
96101
documentFiles: EventDocumentUploadArgs[];
97102
questionDocumentLink?: string;
98103
description?: string;
104+
mention: SlackMentionType;
99105
// If the event type requires confirmation, only intialDateScheduled will be populated. If not,
100106
// scheduleSlots will be populated based on if the event is being editted or created
101107
initialDateScheduled?: Date;
@@ -144,7 +150,8 @@ const schema = yup.object().shape({
144150
allDay: yup.boolean().required(),
145151
recurrenceNumber: yup.number().min(0).required('Recurrence is required'),
146152
days: yup.array().of(yup.mixed<DayOfWeek>().required()).default([]),
147-
selectedScheduleSlotId: yup.string().optional()
153+
selectedScheduleSlotId: yup.string().optional(),
154+
mention: yup.mixed<SlackMentionType>().required().default(SlackMentionType.USER)
148155
});
149156

150157
export interface BaseEventModalProps {
@@ -221,6 +228,7 @@ const EventModal: React.FC<BaseEventModalProps> = ({
221228
eventId,
222229
actionsLeftChildren
223230
}) => {
231+
const theme = useTheme();
224232
const toast = useToast();
225233
const user = useCurrentUser();
226234
const [datePickerOpen, setDatePickerOpen] = useState(false);
@@ -288,7 +296,8 @@ const EventModal: React.FC<BaseEventModalProps> = ({
288296
allDay: initialValues?.allDay ?? false,
289297
recurrenceNumber: 0,
290298
days: [],
291-
selectedScheduleSlotId: initialValues?.selectedScheduleSlotId
299+
selectedScheduleSlotId: initialValues?.selectedScheduleSlotId,
300+
mention: SlackMentionType.USER
292301
};
293302
}, [initialValues, defaultDate, defaultStartTime, defaultEndTime]);
294303

@@ -506,7 +515,8 @@ const EventModal: React.FC<BaseEventModalProps> = ({
506515
workPackageIds: data.workPackageIds,
507516
documentFiles: data.documentFiles,
508517
questionDocumentLink: data.questionDocumentLink,
509-
description: data.description
518+
description: data.description,
519+
mention: data.mention
510520
};
511521

512522
// If the event requires confirmation, only populate initialDateScheduled
@@ -1191,6 +1201,53 @@ const EventModal: React.FC<BaseEventModalProps> = ({
11911201
)}
11921202
</Box>
11931203
</Tooltip>
1204+
1205+
{/* Slack Mention Type Toggle */}
1206+
{selectedEventType.sendSlackNotifications && !initialValues && (
1207+
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, marginLeft: 'auto' }}>
1208+
<Controller
1209+
name="mention"
1210+
control={control}
1211+
render={({ field: { onChange, value } }) => (
1212+
<ToggleButtonGroup
1213+
value={value}
1214+
exclusive
1215+
onChange={(_, val) => {
1216+
if (val) onChange(val);
1217+
}}
1218+
size="small"
1219+
sx={{
1220+
'& .MuiToggleButton-root': {
1221+
borderRadius: 0,
1222+
textTransform: 'none',
1223+
py: 0.55,
1224+
px: 1.1,
1225+
borderColor: theme.palette.divider,
1226+
color: theme.palette.text.primary,
1227+
'&.Mui-selected': {
1228+
bgcolor: theme.palette.primary.main,
1229+
color: 'black',
1230+
'&:hover': { bgcolor: '#ff0000', color: 'white' }
1231+
},
1232+
'&:hover': { bgcolor: theme.palette.action.hover }
1233+
},
1234+
'& .MuiToggleButton-root:first-of-type': {
1235+
borderTopLeftRadius: 8,
1236+
borderBottomLeftRadius: 8
1237+
},
1238+
'& .MuiToggleButton-root:last-of-type': {
1239+
borderTopRightRadius: 8,
1240+
borderBottomRightRadius: 8
1241+
}
1242+
}}
1243+
>
1244+
<ToggleButton value={SlackMentionType.USER}>@user</ToggleButton>
1245+
<ToggleButton value={SlackMentionType.CHANNEL}>@channel</ToggleButton>
1246+
</ToggleButtonGroup>
1247+
)}
1248+
/>
1249+
</Box>
1250+
)}
11941251
</Box>
11951252
)}
11961253
{/* Required Members Section */}

src/shared/src/types/calendar-types.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,11 @@ export enum ConflictStatus {
8787
NO_CONFLICT = 'NO_CONFLICT'
8888
}
8989

90+
export enum SlackMentionType {
91+
USER = 'USER',
92+
CHANNEL = 'CHANNEL'
93+
}
94+
9095
export interface Calendar {
9196
calendarId: string;
9297
name: string;

0 commit comments

Comments
 (0)